diff --git a/doc/api/resources/taxrules.rst b/doc/api/resources/taxrules.rst index 2ba9d2284c..d9f2221f88 100644 --- a/doc/api/resources/taxrules.rst +++ b/doc/api/resources/taxrules.rst @@ -4,7 +4,8 @@ Tax rules Resource description -------------------- -Tax rules specify how tax should be calculated for specific products. +Tax rules specify how tax should be calculated for specific products. Custom taxation rulesets are currently to +available via the API. .. rst-class:: rest-resource-table diff --git a/src/pretix/base/migrations/0083_auto_20180228_2102.py b/src/pretix/base/migrations/0083_auto_20180228_2102.py new file mode 100644 index 0000000000..70bfb85dc8 --- /dev/null +++ b/src/pretix/base/migrations/0083_auto_20180228_2102.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-02-28 21:02 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0082_auto_20180222_0938'), + ] + + operations = [ + migrations.AddField( + model_name='taxrule', + name='custom_rules', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='orderfee', + name='fee_type', + field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('service', 'Service fee'), ('other', 'Other fees')], max_length=100), + ), + ] diff --git a/src/pretix/base/models/tax.py b/src/pretix/base/models/tax.py index 349abf6a40..c6f408f5b3 100644 --- a/src/pretix/base/models/tax.py +++ b/src/pretix/base/models/tax.py @@ -1,3 +1,4 @@ +import json from decimal import Decimal from django.db import models @@ -88,6 +89,7 @@ class TaxRule(LoggedModel): help_text=_('Your country of residence. This is the country the EU reverse charge rule will not apply in, ' 'if configured above.'), ) + custom_rules = models.TextField(blank=True, null=True) def allow_delete(self): from pretix.base.models.orders import OrderFee, OrderPosition @@ -151,7 +153,27 @@ class TaxRule(LoggedModel): rate=self.rate, name=self.name ) + def get_matching_rule(self, invoice_address): + rules = json.loads(self.custom_rules) + for r in rules: + if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES: + continue + if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country): + continue + if r['address_type'] == 'individual' and invoice_address.is_business: + continue + if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business: + continue + if r['address_type'] == 'business_vat_id' and (not invoice_address.vat_id or not invoice_address.vat_id_validated): + continue + return r + return {'action': 'vat'} + def is_reverse_charge(self, invoice_address): + if self.custom_rules: + rule = self.get_matching_rule(invoice_address) + return rule['action'] == 'reverse' + if not self.eu_reverse_charge: return False @@ -170,6 +192,10 @@ class TaxRule(LoggedModel): return False def tax_applicable(self, invoice_address): + if self.custom_rules: + rule = self.get_matching_rule(invoice_address) + return rule.get('action', 'vat') == 'vat' + if not self.eu_reverse_charge: # No reverse charge rules? Always apply VAT! return True diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 046909636d..a47d6254f6 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -4,8 +4,11 @@ from django.contrib.auth.hashers import check_password from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.db.models import Q +from django.forms import formset_factory from django.utils.timezone import get_current_timezone_name from django.utils.translation import pgettext_lazy, ugettext_lazy as _ +from django_countries import Countries +from django_countries.fields import LazyTypedChoiceField from i18nfield.forms import I18nFormField, I18nTextarea from pytz import common_timezones, timezone @@ -907,6 +910,43 @@ class CommentForm(I18nModelForm): } +class CountriesAndEU(Countries): + override = { + 'ZZ': _('Any country'), + 'EU': _('European Union') + } + first = ['ZZ', 'EU'] + + +class TaxRuleLineForm(forms.Form): + country = LazyTypedChoiceField( + choices=CountriesAndEU(), + required=False + ) + address_type = forms.ChoiceField( + choices=[ + ('', _('Any customer')), + ('individual', _('Individual')), + ('business', _('Business')), + ('business_vat_id', _('Business with valid VAT ID')), + ], + required=False + ) + action = forms.ChoiceField( + choices=[ + ('vat', _('Charge VAT')), + ('reverse', _('Reverse charge')), + ('no', _('No VAT')), + ], + ) + + +TaxRuleLineFormSet = formset_factory( + TaxRuleLineForm, + can_order=False, can_delete=True, extra=0 +) + + class TaxRuleForm(I18nModelForm): class Meta: model = TaxRule diff --git a/src/pretix/control/templates/pretixcontrol/event/tax_edit.html b/src/pretix/control/templates/pretixcontrol/event/tax_edit.html index 4b8639a173..b6379b4c05 100644 --- a/src/pretix/control/templates/pretixcontrol/event/tax_edit.html +++ b/src/pretix/control/templates/pretixcontrol/event/tax_edit.html @@ -1,5 +1,6 @@ {% extends "pretixcontrol/event/settings_base.html" %} {% load i18n %} +{% load formset_tags %} {% load bootstrap3 %} {% block title %} {% if rule %} @@ -21,7 +22,7 @@ {% bootstrap_field form.rate addon_after="%" layout="control" %} {% trans "Advanced settings" %}
- + {% blocktrans trimmed with docs="https://docs.pretix.eu/en/latest/user/events/taxes.html" %} These settings are intended for advanced users. See the documentation for more information. Note that we are not responsible for the correct handling @@ -32,6 +33,75 @@ {% bootstrap_field form.price_includes_tax layout="control" %} {% bootstrap_field form.eu_reverse_charge layout="control" %} {% bootstrap_field form.home_country layout="control" %} + {% trans "Custom taxation rules" %} +
+ + {% blocktrans trimmed %} + These settings are intended for professional users with very specific taxation situations. + If you create any rule here, the reverse charge settings above will be ignored. The rules will be + checked in order and once the first rule matches the order, it will be used and all further rules will + be ignored. If no rule matches, tax will be charged. + {% endblocktrans %} +
+
+ +
+ {{ formset.management_form }} + {% bootstrap_formset_errors formset %} +
+ {% for form in formset %} + {% bootstrap_form_errors form %} +
+
+ {{ form.id }} + {% bootstrap_field form.DELETE form_group_class="" layout="inline" %} +
+
+ {% bootstrap_field form.country layout='inline' form_group_class="" %} +
+
+ {% bootstrap_field form.address_type layout='inline' form_group_class="" %} +
+
+ {% bootstrap_field form.action layout='inline' form_group_class="" %} +
+
+ +
+
+ {% endfor %} +
+ +

+ +

+
+ +