mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
Add support for custom taxation rules
This commit is contained in:
25
src/pretix/base/migrations/0083_auto_20180228_2102.py
Normal file
25
src/pretix/base/migrations/0083_auto_20180228_2102.py
Normal file
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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={
|
||||
|
||||
387
src/tests/base/test_taxrules.py
Normal file
387
src/tests/base/test_taxrules.py
Normal file
@@ -0,0 +1,387 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from django_countries.fields import Country
|
||||
|
||||
from pretix.base.models import Event, InvoiceAddress, Organizer, TaxRule
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def event():
|
||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
event = Event.objects.create(
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now()
|
||||
)
|
||||
return event
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_from_gross_price(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal('10.00'), price_includes_tax=True
|
||||
)
|
||||
tp = tr.tax(Decimal('100.00'))
|
||||
assert tp.gross == Decimal('100')
|
||||
assert tp.net == Decimal('90.91')
|
||||
assert tp.tax == Decimal('100.00') - Decimal('90.91')
|
||||
assert tp.rate == Decimal('10.00')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_from_net_price(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
tp = tr.tax(Decimal('100.00'))
|
||||
assert tp.gross == Decimal('110.00')
|
||||
assert tp.net == Decimal('100.00')
|
||||
assert tp.tax == Decimal('10.00')
|
||||
assert tp.rate == Decimal('10.00')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_no_address(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True,
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
assert not tr.is_reverse_charge(None)
|
||||
assert tr.tax_applicable(None)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_no_country(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True,
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_individual_same_country(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=False,
|
||||
country=Country('DE')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_individual_eu(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=False,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_individual_3rdc(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=False,
|
||||
country=Country('US')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_business_same_country(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('DE')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_business_eu(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_business_3rdc(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('US')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_valid_vat_id_business_same_country(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('DE'),
|
||||
vat_id='DE123456',
|
||||
vat_id_validated=True
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_valid_vat_id_business_eu(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
vat_id='AT12346',
|
||||
vat_id_validated=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_valid_vat_id_business_3rdc(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('US'),
|
||||
vat_id='US12346',
|
||||
vat_id_validated=True
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_reverse_charge_disabled(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=False, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
vat_id='AT12346',
|
||||
vat_id_validated=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_rules_override(event):
|
||||
tr = TaxRule(
|
||||
event=event, eu_reverse_charge=True, home_country=Country('DE'),
|
||||
rate=Decimal('10.00'), price_includes_tax=False,
|
||||
custom_rules=json.dumps([
|
||||
{'country': 'ZZ', 'address_type': '', 'action': 'vat'}
|
||||
])
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
vat_id='AT12346',
|
||||
vat_id_validated=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_rules_in_order(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal('10.00'), price_includes_tax=False,
|
||||
custom_rules=json.dumps([
|
||||
{'country': 'ZZ', 'address_type': '', 'action': 'vat'},
|
||||
{'country': 'ZZ', 'address_type': '', 'action': 'reverse'}
|
||||
])
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
vat_id='AT12346',
|
||||
vat_id_validated=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_rules_any_country(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal('10.00'), price_includes_tax=False,
|
||||
custom_rules=json.dumps([
|
||||
{'country': 'ZZ', 'address_type': '', 'action': 'no'},
|
||||
])
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_rules_eu_country(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal('10.00'), price_includes_tax=False,
|
||||
custom_rules=json.dumps([
|
||||
{'country': 'EU', 'address_type': '', 'action': 'no'},
|
||||
])
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('US')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_rules_specific_country(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal('10.00'), price_includes_tax=False,
|
||||
custom_rules=json.dumps([
|
||||
{'country': 'AT', 'address_type': '', 'action': 'no'},
|
||||
])
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('DE')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_rules_individual(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal('10.00'), price_includes_tax=False,
|
||||
custom_rules=json.dumps([
|
||||
{'country': 'ZZ', 'address_type': 'individual', 'action': 'no'},
|
||||
])
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=False,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('DE')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_rules_business(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal('10.00'), price_includes_tax=False,
|
||||
custom_rules=json.dumps([
|
||||
{'country': 'ZZ', 'address_type': 'business', 'action': 'no'},
|
||||
])
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
ia = InvoiceAddress(
|
||||
is_business=False,
|
||||
country=Country('DE')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_rules_vat_id(event):
|
||||
tr = TaxRule(
|
||||
event=event,
|
||||
rate=Decimal('10.00'), price_includes_tax=False,
|
||||
custom_rules=json.dumps([
|
||||
{'country': 'EU', 'address_type': 'business_vat_id', 'action': 'reverse'},
|
||||
])
|
||||
)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('AT')
|
||||
)
|
||||
assert not tr.is_reverse_charge(ia)
|
||||
assert tr.tax_applicable(ia)
|
||||
ia = InvoiceAddress(
|
||||
is_business=True,
|
||||
country=Country('DE'),
|
||||
vat_id='DE1234',
|
||||
vat_id_validated=True
|
||||
)
|
||||
assert tr.is_reverse_charge(ia)
|
||||
assert not tr.tax_applicable(ia)
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
@@ -415,6 +416,51 @@ class CheckoutTestCase(TestCase):
|
||||
ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address'))
|
||||
assert not ia.vat_id_validated
|
||||
|
||||
def test_custom_tax_rules(self):
|
||||
self.tr19.custom_rules = json.dumps([
|
||||
{'country': 'AT', 'address_type': '', 'action': 'vat'},
|
||||
{'country': 'ZZ', 'address_type': '', 'action': 'reverse'},
|
||||
])
|
||||
self.tr19.save()
|
||||
self.event.settings.invoice_address_vatid = True
|
||||
|
||||
cr1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
|
||||
with mock.patch('vat_moss.id.validate') as mock_validate:
|
||||
mock_validate.return_value = ('AT', 'AT123456', 'Foo')
|
||||
self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'is_business': 'business',
|
||||
'company': 'Foo',
|
||||
'name': 'Bar',
|
||||
'street': 'Baz',
|
||||
'zipcode': '12345',
|
||||
'city': 'Here',
|
||||
'country': 'AT',
|
||||
'vat_id': 'AT123456',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
|
||||
cr1.refresh_from_db()
|
||||
assert cr1.price == Decimal('23.00')
|
||||
|
||||
self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||
'is_business': 'business',
|
||||
'company': 'Foo',
|
||||
'name': 'Bar',
|
||||
'street': 'Baz',
|
||||
'zipcode': '12345',
|
||||
'city': 'Here',
|
||||
'country': 'DE',
|
||||
'vat_id': 'DE123456',
|
||||
'email': 'admin@localhost'
|
||||
}, follow=True)
|
||||
|
||||
cr1.refresh_from_db()
|
||||
assert cr1.price == Decimal('19.33')
|
||||
|
||||
def test_question_file_upload(self):
|
||||
q1 = Question.objects.create(
|
||||
event=self.event, question='Student ID', type=Question.TYPE_FILE,
|
||||
|
||||
Reference in New Issue
Block a user