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" %}
-
+
{% 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" %}
+
+
+
+ {% 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 %}
+
+