Add support for custom taxation rules

This commit is contained in:
Raphael Michel
2018-02-28 23:03:25 +01:00
parent d8d00a7e26
commit 578c1ecfaf
8 changed files with 644 additions and 4 deletions

View File

@@ -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

View File

@@ -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" %}
<legend>{% trans "Advanced settings" %}</legend>
<div class="alert alert-warning">
<span class="fa fa-w fa-legal fa-4x pull-left"></span>
<span class="fa fa-fw fa-legal fa-4x pull-left"></span>
{% blocktrans trimmed with docs="https://docs.pretix.eu/en/latest/user/events/taxes.html" %}
These settings are intended for advanced users. See the <a href="{{ docs }}">documentation</a>
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" %}
<legend>{% trans "Custom taxation rules" %}</legend>
<div class="alert alert-warning">
<span class="fa fa-fw fa-exclamation-circle fa-4x pull-left"></span>
{% 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 %}
<div class="clearfix"></div>
</div>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
{% bootstrap_form_errors form %}
<div class="row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field form.country layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field form.address_type layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field form.action layout='inline' form_group_class="" %}
</div>
<div class="col-sm-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-sm-4">
{% bootstrap_field formset.empty_form.country layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field formset.empty_form.address_type layout='inline' form_group_class="" %}
</div>
<div class="col-sm-3">
{% bootstrap_field formset.empty_form.action layout='inline' form_group_class="" %}
</div>
<div class="col-sm-2 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new rule" %}</button>
</p>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -1,3 +1,4 @@
import json
import re
from collections import OrderedDict
from datetime import timedelta
@@ -40,8 +41,8 @@ from pretix.base.templatetags.money import money_filter
from pretix.control.forms.event import (
CommentForm, DisplaySettingsForm, EventDeleteForm, EventMetaValueForm,
EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm,
WidgetCodeForm,
PaymentSettingsForm, ProviderForm, TaxRuleForm, TaxRuleLineFormSet,
TicketSettingsForm, WidgetCodeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import nav_event_settings
@@ -952,9 +953,30 @@ class TaxCreate(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView
'name': LazyI18nString.from_gettext(ugettext('VAT'))
}
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid() and self.formset.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
@cached_property
def formset(self):
return TaxRuleLineFormSet(
data=self.request.POST if self.request.method == "POST" else None,
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset
return ctx
@transaction.atomic
def form_valid(self, form):
form.instance.event = self.request.event
form.instance.custom_rules = json.dumps([
f.cleaned_data for f in self.formset if f not in self.formset.deleted_forms
])
messages.success(self.request, _('The new tax rule has been created.'))
ret = super().form_valid(form)
form.instance.log_action('pretix.event.taxrule.added', user=self.request.user, data=dict(form.cleaned_data))
@@ -980,9 +1002,32 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
except TaxRule.DoesNotExist:
raise Http404(_("The requested tax rule does not exist."))
def post(self, request, *args, **kwargs):
self.object = self.get_object(self.get_queryset())
form = self.get_form()
if form.is_valid() and self.formset.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
@cached_property
def formset(self):
return TaxRuleLineFormSet(
data=self.request.POST if self.request.method == "POST" else None,
initial=json.loads(self.object.custom_rules) if self.object.custom_rules else []
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset
return ctx
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
form.instance.custom_rules = json.dumps([
f.cleaned_data for f in self.formset if f not in self.formset.deleted_forms
])
if form.has_changed():
self.object.log_action(
'pretix.event.taxrule.changed', user=self.request.user, data={