Add codification of tax rates (#4372)

* draft

* .

* Rebase migration

* Update src/pretix/base/models/tax.py

Co-authored-by: Mira <weller@rami.io>

* Test, isort, flake, migration rebase

* carry data & API

* Fix failing tests

* docs fixes

* Improve validation

* Tests

* More fixes

---------

Co-authored-by: Mira <weller@rami.io>
This commit is contained in:
Raphael Michel
2024-12-13 12:04:38 +01:00
committed by GitHub
parent a4385c8b6e
commit 53f129d5d3
36 changed files with 818 additions and 124 deletions

View File

@@ -63,6 +63,7 @@ from pretix.base.forms import (
)
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.models.tax import TAX_CODE_LISTS
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin
from pretix.base.settings import (
@@ -1504,6 +1505,11 @@ class TaxRuleLineForm(I18nForm):
('require_approval', _('Order requires approval')),
],
)
code = forms.ChoiceField(
label=_("Tax code"),
choices=[("", _("Default tax code")), *TAX_CODE_LISTS],
required=False,
)
rate = forms.DecimalField(
label=_('Deviating tax rate'),
max_digits=10, decimal_places=2,
@@ -1518,6 +1524,43 @@ class TaxRuleLineForm(I18nForm):
})
)
def __init__(self, *args, **kwargs):
self.parent_form = kwargs.pop("parent_form")
super().__init__(*args, **kwargs)
def clean(self):
d = super().clean()
parent_code = self.parent_form.cleaned_data.get("code")
parent_rate = self.parent_form.cleaned_data.get("rate")
code = d.get("code") or parent_code
rate = d.get("rate")
if rate is None:
rate = parent_rate
if d.get("action") in ("reverse", "no", "block") and d.get("rate"):
raise ValidationError(_("A combination of this calculation mode with a non-zero tax rate does not make sense."))
if d.get("action") == "reverse" and d.get("code") and code != "AE":
# Reverse charge but code is not reverse charge -- this is the one case we ignore if the "default code"
# is used because it is the one scenario we can auto-fix
raise ValidationError(_("This combination of calculation mode and tax code does not make sense."))
if d.get("action") == "no" and code and code.split("/")[0] in ("S", "AE", "L", "M", "B"):
# No VAT but code indicates VAT
raise ValidationError(_("This combination of calculation mode and tax code does not make sense."))
if d.get("action") == "vat" and code and rate != Decimal("0.00") and code.split("/")[0] in ("O", "E", "Z", "G", "K", "AE"):
# VAT, but code indicates exempt
raise ValidationError(_("A combination of this tax code with a non-zero tax rate does not make sense."))
if d.get("action") == "vat" and code and rate == Decimal("0.00") and code.split("/")[0] in ("S", "L", "M", "B"):
# no VAT, but code indicates non-exempt
raise ValidationError(_("A combination of this tax code with a zero tax rate does not make sense."))
return d
class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet):
# compatibility shim for django-i18nfield library
@@ -1529,8 +1572,16 @@ class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet):
super().__init__(*args, **kwargs)
class BaseTaxRuleLineFormSet(I18nBaseFormSet):
def __init__(self, *args, **kwargs):
self.parent_form = kwargs.pop('parent_form')
super().__init__(*args, **kwargs)
self.form_kwargs['parent_form'] = self.parent_form
TaxRuleLineFormSet = formset_factory(
TaxRuleLineForm, formset=I18nBaseFormSet,
TaxRuleLineForm, formset=BaseTaxRuleLineFormSet,
can_order=True, can_delete=True, extra=0
)
@@ -1538,7 +1589,16 @@ TaxRuleLineFormSet = formset_factory(
class TaxRuleForm(I18nModelForm):
class Meta:
model = TaxRule
fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes']
fields = [
'name',
'rate',
'price_includes_tax',
'code',
'eu_reverse_charge',
'home_country',
'internal_name',
'keep_gross_if_rate_changes'
]
class WidgetCodeForm(forms.Form):