Allow country specific tax rules (#1714)

This commit is contained in:
Raphael Michel
2020-07-08 15:00:13 +02:00
committed by GitHub
parent 1c9a1b5e02
commit 6e9d921af6
20 changed files with 716 additions and 161 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.6 on 2020-06-28 19:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0155_quota_release_after_exit'),
]
operations = [
migrations.AddField(
model_name='cartposition',
name='override_tax_rate',
field=models.DecimalField(decimal_places=2, max_digits=10, null=True),
),
]

View File

@@ -448,24 +448,32 @@ class Item(LoggedModel):
return self.event.settings.show_quota_left
return self.show_quota_left
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False):
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False):
price = price if price is not None else self.default_price
if not self.tax_rule:
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
rate=Decimal('0.00'), name='')
else:
t = self.tax_rule.tax(price, base_price_is=base_price_is,
currency=currency or self.event.currency)
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
override_tax_rate=override_tax_rate, currency=currency or self.event.currency)
if include_bundled:
for b in self.bundles.all():
if b.designated_price and b.bundled_item.tax_rule_id != self.tax_rule_id:
if b.bundled_variation:
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross',
invoice_address=invoice_address,
currency=currency)
else:
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
compare_price = self.tax_rule.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
bprice = b.bundled_item.tax(b.designated_price * b.count,
invoice_address=invoice_address,
base_price_is='gross',
currency=currency)
compare_price = self.tax_rule.tax(b.designated_price * b.count,
override_tax_rate=override_tax_rate,
invoice_address=invoice_address,
currency=currency)
t.net += bprice.net - compare_price.net
t.tax += bprice.tax - compare_price.tax
t.name = "MIXED!"
@@ -673,23 +681,31 @@ class ItemVariation(models.Model):
def price(self):
return self.default_price if self.default_price is not None else self.item.default_price
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False):
def tax(self, price=None, base_price_is='auto', currency=None, include_bundled=False, override_tax_rate=None,
invoice_address=None):
price = price if price is not None else self.price
if not self.item.tax_rule:
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
rate=Decimal('0.00'), name='')
else:
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency)
t = self.item.tax_rule.tax(price, base_price_is=base_price_is, currency=currency,
override_tax_rate=override_tax_rate,
invoice_address=invoice_address)
if include_bundled:
for b in self.item.bundles.all():
if b.designated_price and b.bundled_item.tax_rule_id != self.item.tax_rule_id:
if b.bundled_variation:
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross',
currency=currency,
invoice_address=invoice_address)
else:
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
compare_price = self.item.tax_rule.tax(b.designated_price * b.count, base_price_is='gross', currency=currency)
bprice = b.bundled_item.tax(b.designated_price * b.count, base_price_is='gross',
currency=currency,
invoice_address=invoice_address)
compare_price = self.item.tax_rule.tax(b.designated_price * b.count, base_price_is='gross',
currency=currency, invoice_address=invoice_address)
t.net += bprice.net - compare_price.net
t.tax += bprice.tax - compare_price.tax
t.name = "MIXED!"

View File

@@ -1813,13 +1813,9 @@ class OrderFee(models.Model):
self.tax_rule = self.order.event.settings.tax_rate_default
if self.tax_rule:
if self.tax_rule.tax_applicable(ia):
tax = self.tax_rule.tax(self.value, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
tax = self.tax_rule.tax(self.value, base_price_is='gross', invoice_address=ia)
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
@@ -1966,13 +1962,9 @@ class OrderPosition(AbstractPosition):
except InvoiceAddress.DoesNotExist:
ia = None
if self.tax_rule:
if self.tax_rule.tax_applicable(ia):
tax = self.tax_rule.tax(self.price, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
tax = self.tax_rule.tax(self.price, invoice_address=ia, base_price_is='gross')
self.tax_rate = tax.rate
self.tax_value = tax.tax
else:
self.tax_value = Decimal('0.00')
self.tax_rate = Decimal('0.00')
@@ -2111,6 +2103,10 @@ class CartPosition(AbstractPosition):
includes_tax = models.BooleanField(
default=True
)
override_tax_rate = models.DecimalField(
max_digits=10, decimal_places=2,
null=True, blank=True
)
is_bundled = models.BooleanField(default=False)
objects = ScopedManager(organizer='event__organizer')
@@ -2127,6 +2123,8 @@ class CartPosition(AbstractPosition):
@property
def tax_rate(self):
if self.includes_tax:
if self.override_tax_rate is not None:
return self.override_tax_rate
return self.item.tax(self.price, base_price_is='gross').rate
else:
return Decimal('0.00')
@@ -2134,7 +2132,7 @@ class CartPosition(AbstractPosition):
@property
def tax_value(self):
if self.includes_tax:
return self.item.tax(self.price, base_price_is='gross').tax
return self.item.tax(self.price, override_tax_rate=self.override_tax_rate, base_price_is='gross').tax
else:
return Decimal('0.00')

View File

@@ -164,16 +164,39 @@ class TaxRule(LoggedModel):
def has_custom_rules(self):
return self.custom_rules and self.custom_rules != '[]'
def tax(self, base_price, base_price_is='auto', currency=None):
def tax_rate_for(self, invoice_address):
if not self._tax_applicable(invoice_address):
return Decimal('0.00')
if self.has_custom_rules:
rule = self.get_matching_rule(invoice_address)
if rule.get('action', 'vat') == 'vat' and rule.get('rate') is not None:
return Decimal(rule.get('rate'))
return Decimal(self.rate)
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
subtract_from_gross=Decimal('0.00')):
from .event import Event
try:
currency = currency or self.event.currency
except Event.DoesNotExist:
pass
if self.rate == Decimal('0.00'):
rate = Decimal(self.rate)
if override_tax_rate is not None:
rate = override_tax_rate
elif invoice_address:
adjust_rate = self.tax_rate_for(invoice_address)
if adjust_rate != rate:
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
base_price = normal_price.net
base_price_is = 'net'
subtract_from_gross = Decimal('0.00')
rate = adjust_rate
if rate == Decimal('0.00'):
return TaxedPrice(
net=base_price, gross=base_price, tax=Decimal('0.00'),
rate=self.rate, name=self.name
net=base_price - subtract_from_gross, gross=base_price - subtract_from_gross, tax=Decimal('0.00'),
rate=rate, name=self.name
)
if base_price_is == 'auto':
@@ -183,19 +206,22 @@ class TaxRule(LoggedModel):
base_price_is = 'net'
if base_price_is == 'gross':
gross = base_price
net = round_decimal(gross - (base_price * (1 - 100 / (100 + self.rate))),
gross = max(Decimal('0.00'), base_price - subtract_from_gross)
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
elif base_price_is == 'net':
net = base_price
gross = round_decimal((net * (1 + self.rate / 100)),
currency)
gross = round_decimal((net * (1 + rate / 100)), currency)
if subtract_from_gross:
gross -= subtract_from_gross
net = round_decimal(gross - (gross * (1 - 100 / (100 + rate))),
currency)
else:
raise ValueError('Unknown base price type: {}'.format(base_price_is))
return TaxedPrice(
net=net, gross=gross, tax=gross - net,
rate=self.rate, name=self.name
rate=rate, name=self.name
)
@property
@@ -243,7 +269,7 @@ class TaxRule(LoggedModel):
return False
def tax_applicable(self, invoice_address):
def _tax_applicable(self, invoice_address):
if self._custom_rules:
rule = self.get_matching_rule(invoice_address)
return rule.get('action', 'vat') == 'vat'

View File

@@ -907,6 +907,7 @@ class CartManager:
price=op.price.gross, expires=self._expiry, cart_id=self.cart_id,
voucher=op.voucher, addon_to=op.addon_to if op.addon_to else None,
subevent=op.subevent, includes_tax=op.includes_tax, seat=op.seat,
override_tax_rate=op.price.rate,
price_before_voucher=op.price_before_voucher.gross if op.price_before_voucher is not None else None
)
if self.event.settings.attendee_names_asked:
@@ -940,7 +941,7 @@ class CartManager:
new_cart_positions.append(CartPosition(
event=self.event, item=b.item, variation=b.variation,
price=b.price.gross, expires=self._expiry, cart_id=self.cart_id,
voucher=None, addon_to=cp,
voucher=None, addon_to=cp, override_tax_rate=b.price.rate,
subevent=b.subevent, includes_tax=b.includes_tax, is_bundled=True
))
@@ -1032,19 +1033,15 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
for pos in positions:
if not pos.item.tax_rule:
continue
charge_tax = pos.item.tax_rule.tax_applicable(invoice_address)
if pos.includes_tax and not charge_tax:
price = pos.item.tax(pos.price, base_price_is='gross').net
totaldiff += price - pos.price
pos.price = price
pos.includes_tax = False
pos.save(update_fields=['price', 'includes_tax'])
elif charge_tax and not pos.includes_tax:
price = pos.item.tax(pos.price, base_price_is='net').gross
totaldiff += price - pos.price
pos.price = price
pos.includes_tax = True
pos.save(update_fields=['price', 'includes_tax'])
rate = pos.item.tax_rule.tax_rate_for(invoice_address)
if pos.tax_rate != rate:
current_net = pos.price - pos.tax_value
new_gross = pos.item.tax(current_net, base_price_is='net', invoice_address=invoice_address).gross
pos.price = new_gross
pos.includes_tax = rate != Decimal('0.00')
pos.override_tax_rate = rate
pos.save(update_fields=['price', 'includes_tax', 'override_tax_rate'])
return totaldiff
@@ -1092,23 +1089,14 @@ def get_fees(event, request, total, invoice_address, provider, positions):
if payment_fee:
payment_fee_tax_rule = event.settings.tax_rate_default or TaxRule.zero()
if payment_fee_tax_rule.tax_applicable(invoice_address):
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross')
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_PAYMENT,
value=payment_fee,
tax_rate=payment_fee_tax.rate,
tax_value=payment_fee_tax.tax,
tax_rule=payment_fee_tax_rule
))
else:
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_PAYMENT,
value=payment_fee,
tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'),
tax_rule=payment_fee_tax_rule
))
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross', invoice_address=invoice_address)
fees.append(OrderFee(
fee_type=OrderFee.FEE_TYPE_PAYMENT,
value=payment_fee,
tax_rate=payment_fee_tax.rate,
tax_value=payment_fee_tax.tax,
tax_rule=payment_fee_tax_rule
))
return fees

View File

@@ -24,7 +24,7 @@ from pretix.base.i18n import language
from pretix.base.models import (
Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee,
)
from pretix.base.models.tax import EU_CURRENCIES
from pretix.base.models.tax import EU_COUNTRIES, EU_CURRENCIES
from pretix.base.services.tasks import TransactionAwareTask
from pretix.base.settings import GlobalSettingsObject
from pretix.base.signals import invoice_line_text, periodic_task
@@ -181,11 +181,17 @@ def build_invoice(invoice: Invoice) -> Invoice:
if reverse_charge:
if invoice.additional_text:
invoice.additional_text += "<br /><br />"
invoice.additional_text += pgettext(
"invoice",
"Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/EEC, VAT liability "
"rests with the service recipient."
)
if str(invoice.invoice_to_country) in EU_COUNTRIES:
invoice.additional_text += pgettext(
"invoice",
"Reverse Charge: According to Article 194, 196 of Council Directive 2006/112/EEC, VAT liability "
"rests with the service recipient."
)
else:
invoice.additional_text += pgettext(
"invoice",
"VAT liability rests with the service recipient."
)
invoice.reverse_charge = True
invoice.save()

View File

@@ -34,7 +34,7 @@ from pretix.base.models.orders import (
generate_secret,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.tax import TaxedPrice, TaxRule
from pretix.base.models.tax import TaxRule
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services import tickets
@@ -1263,7 +1263,8 @@ class OrderChangeManager:
self._operations.append(self.RegenerateSecretOperation(position))
def change_price(self, position: OrderPosition, price: Decimal):
price = position.item.tax(price, base_price_is='gross')
tax_rule = self._current_tax_rules().get(position.pk, position.tax_rule)
price = tax_rule.tax(price, base_price_is='gross')
if position.issued_gift_cards.exists():
raise OrderError(self.error_messages['gift_card_change'])
@@ -1279,27 +1280,38 @@ class OrderChangeManager:
self._operations.append(self.TaxRuleOperation(position_or_fee, tax_rule))
self._invoice_dirty = True
def recalculate_taxes(self):
def _current_tax_rules(self):
tax_rules = {}
for p in self._operations:
if isinstance(p, self.TaxRuleOperation):
tax_rules[p.position.pk] = p.tax_rule
elif isinstance(p, self.ItemOperation):
tax_rules[p.position.pk] = p.item.tax_rule
return tax_rules
def recalculate_taxes(self, keep='net'):
positions = self.order.positions.select_related('item', 'item__tax_rule')
ia = self._invoice_address
tax_rules = self._current_tax_rules()
for pos in positions:
if not pos.item.tax_rule:
tax_rule = tax_rules.get(pos.pk, pos.tax_rule)
if not tax_rule:
continue
if not pos.price:
continue
charge_tax = pos.item.tax_rule.tax_applicable(ia)
if pos.tax_value and not charge_tax:
net_price = pos.price - pos.tax_value
price = TaxedPrice(gross=net_price, net=net_price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
if price.gross != pos.price:
self._totaldiff += price.gross - pos.price
self._operations.append(self.PriceOperation(pos, price))
elif charge_tax and not pos.tax_value:
price = pos.item.tax(pos.price, base_price_is='net')
if price.gross != pos.price:
self._totaldiff += price.gross - pos.price
self._operations.append(self.PriceOperation(pos, price))
new_rate = tax_rule.tax_rate_for(ia)
# We use override_tax_rate to make sure .tax() doesn't get clever and re-adjusts the pricing itself
if new_rate != pos.tax_rate:
if keep == 'net':
new_tax = tax_rule.tax(pos.price - pos.tax_value, base_price_is='net', currency=self.event.currency,
override_tax_rate=new_rate)
else:
new_tax = tax_rule.tax(pos.price, base_price_is='gross', currency=self.event.currency,
override_tax_rate=new_rate)
self._totaldiff += new_tax.gross - pos.price
self._operations.append(self.PriceOperation(pos, new_tax))
def cancel_fee(self, fee: OrderFee):
self._totaldiff -= fee.value
@@ -1345,10 +1357,7 @@ class OrderChangeManager:
if price is None:
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
else:
if item.tax_rule and item.tax_rule.tax_applicable(self._invoice_address):
price = item.tax(price, base_price_is='gross')
else:
price = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
price = item.tax(price, base_price_is='gross', invoice_address=self._invoice_address)
if price is None:
raise OrderError(self.error_messages['product_invalid'])
@@ -1599,7 +1608,8 @@ class OrderChangeManager:
'new_price': op.price.gross
})
op.position.price = op.price.gross
op.position._calculate_tax()
op.position.tax_rate = op.price.rate
op.position.tax_value = op.price.tax
op.position.save()
elif isinstance(op, self.TaxRuleOperation):
if isinstance(op.position, OrderPosition):

View File

@@ -46,33 +46,27 @@ def get_price(item: Item, variation: ItemVariation = None,
price_includes_tax=True,
eu_reverse_charge=False,
)
price = tax_rule.tax(price)
price = tax_rule.tax(price, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
if force_custom_price and custom_price is not None and custom_price != "":
if custom_price_is_net:
price = tax_rule.tax(custom_price, base_price_is='net')
price = tax_rule.tax(custom_price, base_price_is='net', invoice_address=invoice_address,
subtract_from_gross=bundled_sum)
else:
price = tax_rule.tax(custom_price, base_price_is='gross')
price = tax_rule.tax(custom_price, base_price_is='gross', invoice_address=invoice_address,
subtract_from_gross=bundled_sum)
if item.free_price and custom_price is not None and custom_price != "":
if not isinstance(custom_price, Decimal):
custom_price = Decimal(str(custom_price).replace(",", "."))
if custom_price > 100000000:
raise ValueError('price_too_high')
if custom_price_is_net:
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net')
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net',
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
else:
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross')
if bundled_sum:
price = price - TaxedPrice(net=bundled_sum, gross=bundled_sum, rate=0, tax=0, name='')
if price.gross < Decimal('0.00'):
return TAXED_ZERO
if invoice_address and not tax_rule.tax_applicable(invoice_address):
price.tax = Decimal('0.00')
price.rate = Decimal('0.00')
price.gross = price.net
price.name = ''
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross',
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
price.gross = round_decimal(price.gross, item.event.currency)
price.net = round_decimal(price.net, item.event.currency)