Tax rules and reverse charge (#559)

Tax rules and reverse charge
This commit is contained in:
Raphael Michel
2017-08-23 13:13:16 +03:00
committed by GitHub
parent b9ec5ea83c
commit 56338be13e
82 changed files with 2934 additions and 428 deletions

View File

@@ -7,8 +7,8 @@ from django.db.models import Sum
from django.utils.functional import cached_property
from django.utils.timezone import now
from pretix.base.decimal import round_decimal
from pretix.base.models import CartPosition, OrderPosition
from pretix.base.models import CartPosition, InvoiceAddress, OrderPosition
from pretix.base.models.tax import TaxRule
from pretix.presale.signals import question_form_fields
@@ -20,7 +20,7 @@ class CartMixin:
"""
return list(get_cart(self.request))
def get_cart(self, answers=False, queryset=None, payment_fee=None, payment_fee_tax_rate=None, downloads=False):
def get_cart(self, answers=False, queryset=None, order=None, downloads=False):
if queryset:
prefetch = []
if answers:
@@ -90,6 +90,7 @@ class CartMixin:
group.total = group.count * group.price
group.net_total = group.count * group.net_price
group.has_questions = answers and k[0] != ""
group.tax_rule = group.item.tax_rule
if answers:
group.cache_answers()
group.additional_answers = pos_additional_fields.get(group.pk)
@@ -99,14 +100,35 @@ class CartMixin:
net_total = sum(p.net_total for p in positions)
tax_total = sum(p.total - p.net_total for p in positions)
payment_fee = payment_fee if payment_fee is not None else self.get_payment_fee(total)
payment_fee_tax_rate = round_decimal(payment_fee_tax_rate
if payment_fee_tax_rate is not None
else self.request.event.settings.tax_rate_default)
payment_fee_tax_value = round_decimal(payment_fee * (1 - 100 / (100 + payment_fee_tax_rate)))
payment_fee_net = payment_fee - payment_fee_tax_value
tax_total += payment_fee_tax_value
net_total += payment_fee_net
if order:
payment_fee = order.payment_fee
tax_total += order.payment_fee_tax_value
payment_fee_net = order.payment_fee - order.payment_fee_tax_value
net_total += payment_fee_net
payment_fee_tax_rule = order.payment_fee_tax_rule
payment_fee_tax_rate = order.payment_fee_tax_rate
else:
payment_fee = self.get_payment_fee(total)
payment_fee_tax_rule = self.request.event.settings.tax_rate_default or TaxRule.zero()
iapk = self.request.session.get('invoice_address_{}'.format(self.request.event.pk))
ia = None
if payment_fee_tax_rule.eu_reverse_charge and iapk:
try:
ia = InvoiceAddress.objects.get(pk=iapk, order__isnull=True)
except InvoiceAddress.DoesNotExist:
pass
if payment_fee_tax_rule.tax_applicable(ia):
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross')
tax_total += payment_fee_tax.tax
net_total += payment_fee_tax.net
payment_fee_net = payment_fee_tax.net
payment_fee_tax_rate = payment_fee_tax.rate
else:
net_total += payment_fee
payment_fee_net = payment_fee
payment_fee_tax_rate = Decimal('0.00')
try:
first_expiry = min(p.expires for p in positions) if positions else now()
@@ -124,6 +146,7 @@ class CartMixin:
'payment_fee': payment_fee,
'payment_fee_net': payment_fee_net,
'payment_fee_tax_rate': payment_fee_tax_rate,
'payment_fee_tax_rule': payment_fee_tax_rule,
'answers': answers,
'minutes_left': minutes_left,
'first_expiry': first_expiry,
@@ -147,7 +170,8 @@ def get_cart(request):
).order_by(
'item', 'variation'
).select_related(
'item', 'variation', 'subevent', 'subevent__event', 'subevent__event__organizer'
'item', 'variation', 'subevent', 'subevent__event', 'subevent__event__organizer',
'item__tax_rule'
).prefetch_related(
'item__questions', 'answers'
)

View File

@@ -6,13 +6,14 @@ from django.db.models import Count, Prefetch, Q
from django.http import FileResponse, Http404, JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView, View
from pretix.base.decimal import round_decimal
from pretix.base.models import (
CartPosition, ItemVariation, QuestionAnswer, Quota, SubEvent, Voucher,
CartPosition, InvoiceAddress, ItemVariation, QuestionAnswer, Quota,
SubEvent, Voucher,
)
from pretix.base.services.cart import (
CartError, add_items_to_cart, clear_cart, remove_cart_position,
@@ -37,6 +38,17 @@ class CartActionMixin:
def get_error_url(self):
return self.get_next_url()
@cached_property
def invoice_address(self):
iapk = self.request.session.get('invoice_address_{}'.format(self.request.event.pk))
if not iapk:
return InvoiceAddress()
try:
return InvoiceAddress.objects.get(pk=iapk, order__isnull=True)
except InvoiceAddress.DoesNotExist:
return InvoiceAddress()
def _item_from_post_value(self, key, value, voucher=None):
if value.strip() == '' or '_' not in key:
return
@@ -154,7 +166,8 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
def post(self, request, *args, **kwargs):
items = self._items_from_post_data()
if items:
return self.do(self.request.event.id, items, self.request.session.session_key, translation.get_language())
return self.do(self.request.event.id, items, self.request.session.session_key, translation.get_language(),
self.invoice_address.pk)
else:
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
return JsonResponse({
@@ -190,12 +203,12 @@ class RedeemView(EventViewMixin, TemplateView):
items = items.filter(quotas__in=[self.voucher.quota_id])
items = items.filter(vouchq).select_related(
'category', # for re-grouping
'category', 'tax_rule', # for re-grouping
).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=self.request.event.quotas.filter(subevent=self.subevent)),
Prefetch('variations', to_attr='avail_variations',
Prefetch('variations', to_attr='available_variations',
queryset=ItemVariation.objects.filter(active=True, quotas__isnull=False).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
@@ -217,13 +230,11 @@ class RedeemView(EventViewMixin, TemplateView):
var_price_override = {}
for item in items:
item.available_variations = list(item.variations.filter(active=True, quotas__isnull=False).distinct())
if self.voucher.item_id and self.voucher.variation_id:
item.available_variations = [v for v in item.available_variations if v.pk == self.voucher.variation_id]
item.order_max = item.max_per_order or int(self.request.event.settings.max_items_per_order)
item.has_variations = item.variations.exists()
if not item.has_variations:
item._remove = not bool(item._subevent_quotas)
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
@@ -231,32 +242,32 @@ class RedeemView(EventViewMixin, TemplateView):
else:
item.cached_availability = item.check_quotas(subevent=self.subevent, _cache=quota_cache)
item.price = item_price_override.get(item.pk, item.default_price)
item.price = self.voucher.calculate_price(item.price)
if self.request.event.settings.display_net_prices:
item.price -= round_decimal(item.price * (1 - 100 / (100 + item.tax_rate)))
price = item_price_override.get(item.pk, item.default_price)
price = self.voucher.calculate_price(price)
item.display_price = item.tax(price)
else:
item._remove = False
for var in item.avail_variations:
for var in item.available_variations:
if self.voucher.allow_ignore_quota or self.voucher.block_quota:
var.cached_availability = (Quota.AVAILABILITY_OK, 1)
else:
var.cached_availability = list(var.check_quotas(subevent=self.subevent, _cache=quota_cache))
var.display_price = var_price_override.get(var.pk, var.price)
var.display_price = self.voucher.calculate_price(var.display_price)
if self.request.event.settings.display_net_prices:
var.display_price -= round_decimal(var.display_price * (1 - 100 / (100 + item.tax_rate)))
price = var_price_override.get(var.pk, var.price)
price = self.voucher.calculate_price(price)
var.display_price = item.tax(price)
item.available_variations = [
v for v in item.avail_variations if v._subevent_quotas
v for v in item.available_variations if v._subevent_quotas
]
if self.voucher.variation_id:
item.available_variations = [v for v in item.available_variations
if v.pk == self.voucher.variation_id]
if len(item.available_variations) > 0:
item.min_price = min([v.display_price for v in item.avail_variations])
item.max_price = max([v.display_price for v in item.avail_variations])
item.min_price = min([v.display_price.net if self.request.event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item.max_price = max([v.display_price.net if self.request.event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
items = [item for item in items
if (len(item.available_variations) > 0 or not item.has_variations) and not item._remove]

View File

@@ -17,7 +17,6 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from pretix.base.decimal import round_decimal
from pretix.base.models import ItemVariation
from pretix.base.models.event import SubEvent
from pretix.multidomain.urlreverse import eventreverse
@@ -53,7 +52,7 @@ def get_grouped_items(event, subevent=None):
& Q(hide_without_voucher=False)
& ~Q(category__is_addon=True)
).select_related(
'category', # for re-grouping
'category', 'tax_rule', # for re-grouping
).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
@@ -88,17 +87,9 @@ def get_grouped_items(event, subevent=None):
item.order_max = min(item.cached_availability[1]
if item.cached_availability[1] is not None else sys.maxsize,
max_per_order)
item.price = item.default_price
price = item.default_price
item.display_price = item.tax(item_price_override.get(item.pk, price))
if event.settings.display_net_prices:
if item_price_override.get(item.pk):
_p = item_price_override.get(item.pk)
tax_value = round_decimal(_p * (1 - 100 / (100 + item.tax_rate)))
item.display_price = _p - tax_value
else:
item.display_price = item.default_price_net
else:
item.display_price = item_price_override.get(item.pk, item.price)
display_add_to_cart = display_add_to_cart or item.order_max > 0
else:
for var in item.available_variations:
@@ -107,15 +98,7 @@ def get_grouped_items(event, subevent=None):
if var.cached_availability[1] is not None else sys.maxsize,
max_per_order)
if event.settings.display_net_prices:
if var_price_override.get(var.pk):
_p = var_price_override.get(var.pk)
tax_value = round_decimal(_p * (1 - 100 / (100 + item.tax_rate)))
var.display_price = _p - tax_value
else:
var.display_price = var.net_price
else:
var.display_price = var_price_override.get(var.pk, var.price)
var.display_price = var.tax(var_price_override.get(var.pk, var.price))
display_add_to_cart = display_add_to_cart or var.order_max > 0
@@ -123,8 +106,10 @@ def get_grouped_items(event, subevent=None):
v for v in item.available_variations if v._subevent_quotas
]
if len(item.available_variations) > 0:
item.min_price = min([v.display_price for v in item.available_variations])
item.max_price = max([v.display_price for v in item.available_variations])
item.min_price = min([v.display_price.net if event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item.max_price = max([v.display_price.net if event.settings.display_net_prices else
v.display_price.gross for v in item.available_variations])
item._remove = not bool(item.available_variations)
items = [item for item in items

View File

@@ -97,8 +97,8 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView):
ctx['download_buttons'] = self.download_buttons
ctx['cart'] = self.get_cart(
answers=True, downloads=ctx['can_download'],
queryset=self.order.positions.all(),
payment_fee=self.order.payment_fee, payment_fee_tax_rate=self.order.payment_fee_tax_rate
queryset=self.order.positions.select_related('tax_rule'),
order=self.order
)
ctx['can_download_multi'] = any([b['multi'] for b in self.download_buttons]) and (
self.request.event.settings.ticket_download_nonadm or
@@ -412,7 +412,7 @@ class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, Template
def invoice_form(self):
return InvoiceAddressForm(data=self.request.POST if self.request.method == "POST" else None,
event=self.request.event,
instance=self.invoice_address)
instance=self.invoice_address, validate_vat_id=False)
def post(self, request, *args, **kwargs):
failed = not self.save() or not self.invoice_form.is_valid()