Tax rules and reverse charge (#559)

Tax rules and reverse charge
This commit is contained in:
Raphael Michel
2017-08-23 13:13:16 +03:00
committed by GitHub
parent b9ec5ea83c
commit 56338be13e
82 changed files with 2934 additions and 428 deletions

View File

@@ -9,7 +9,7 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from pytz import common_timezones, timezone
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer
from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.control.forms import ExtFileField, SlugWidget
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -58,6 +58,13 @@ class EventWizardBasicsForm(I18nModelForm):
choices=settings.LANGUAGES,
label=_("Default language"),
)
tax_rate = forms.DecimalField(
label=_("Sales tax rate"),
help_text=_("Do you need to pay sales tax on your tickets? In this case, please enter the applicable tax rate "
"here in percent. If you have a more complicated tax situation, you can add more tax rates and "
"detailled configuration later."),
required=False
)
class Meta:
model = Event
@@ -375,10 +382,12 @@ class PaymentSettingsForm(SettingsForm):
"configured above."),
required=False
)
tax_rate_default = forms.DecimalField(
label=_('Tax rate for payment fees'),
help_text=_("The tax rate that applies for additional fees you configured for single payment methods "
"(in percent)."),
tax_rate_default = forms.ModelChoiceField(
queryset=TaxRule.objects.none(),
label=_('Tax rule for payment fees'),
required=False,
help_text=_("The tax rule that applies for additional fees you configured for single payment methods. This "
"will set the tax rate and reverse charge rules, other settings of the tax rule are ignored.")
)
def clean(self):
@@ -392,6 +401,10 @@ class PaymentSettingsForm(SettingsForm):
)
return cleaned_data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['tax_rate_default'].queryset = self.obj.tax_rules.all()
class ProviderForm(SettingsForm):
"""
@@ -777,3 +790,9 @@ class CommentForm(I18nModelForm):
'class': 'helper-width-100',
}),
}
class TaxRuleForm(I18nModelForm):
class Meta:
model = TaxRule
fields = ['name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country']

View File

@@ -138,6 +138,8 @@ class ItemCreateForm(I18nModelForm):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all()
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
self.fields['tax_rule'].empty_label = _('No taxation')
self.fields['copy_from'] = forms.ModelChoiceField(
label=_("Copy product information"),
queryset=self.event.items.all(),
@@ -250,7 +252,7 @@ class ItemCreateForm(I18nModelForm):
'category',
'admission',
'default_price',
'tax_rate',
'tax_rule',
'allow_cancel'
]
@@ -259,6 +261,7 @@ class ItemUpdateForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all()
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
class Meta:
model = Item
@@ -272,7 +275,7 @@ class ItemUpdateForm(I18nModelForm):
'picture',
'default_price',
'free_price',
'tax_rate',
'tax_rule',
'available_from',
'available_until',
'require_voucher',

View File

@@ -7,7 +7,9 @@ from django.utils.timezone import now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from pretix.base.forms import I18nModelForm, PlaceholderValidator
from pretix.base.models import Item, ItemAddOn, Order, OrderPosition
from pretix.base.models import (
InvoiceAddress, Item, ItemAddOn, Order, OrderPosition,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price
@@ -66,6 +68,22 @@ class SubEventChoiceField(forms.ModelChoiceField):
p, self.instance.order.event.currency)
class OtherOperationsForm(forms.Form):
recalculate_taxes = forms.BooleanField(
label=_('Re-calculate taxes'),
required=False,
help_text=_(
'This operation re-checks if taxes should be paid to the items due to e.g. configured reverse charge rules '
'and changes the prices and tax values accordingly. This is useful e.g. after an invoice address change. '
'Use with care and only if you need to. Note that rounding differences might occur in this procedure.'
)
)
def __init__(self, *args, **kwargs):
kwargs.pop('order')
super().__init__(*args, **kwargs)
class OrderPositionAddForm(forms.Form):
do = forms.BooleanField(
label=_('Add a new product to the order'),
@@ -83,7 +101,7 @@ class OrderPositionAddForm(forms.Form):
required=False,
max_digits=10, decimal_places=2,
label=_('Gross price'),
help_text=_("Keep empty for the product's default price")
help_text=_("Including taxes, if any. Keep empty for the product's default price")
)
subevent = forms.ModelChoiceField(
SubEvent.objects.none(),
@@ -95,6 +113,12 @@ class OrderPositionAddForm(forms.Form):
def __init__(self, *args, **kwargs):
order = kwargs.pop('order')
super().__init__(*args, **kwargs)
try:
ia = order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
choices = []
for i in order.event.items.prefetch_related('variations').all():
pname = str(i.name)
@@ -103,12 +127,12 @@ class OrderPositionAddForm(forms.Form):
variations = list(i.variations.all())
if variations:
for v in variations:
p = get_price(i, v, invoice_address=ia)
choices.append(('%d-%d' % (i.pk, v.pk),
'%s %s (%s %s)' % (pname, v.value, localize(v.price),
order.event.currency)))
'%s %s (%s %s)' % (pname, v.value, p, order.event.currency)))
else:
choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(i.default_price),
order.event.currency)))
p = get_price(i, invoice_address=ia)
choices.append((str(i.pk), '%s (%s %s)' % (pname, p, order.event.currency)))
self.fields['itemvar'].choices = choices
if ItemAddOn.objects.filter(base_item__event=order.event).exists():
self.fields['addon_to'].queryset = order.positions.filter(addon_to__isnull=True).select_related(
@@ -150,6 +174,12 @@ class OrderPositionChangeForm(forms.Form):
def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance')
initial = kwargs.get('initial', {})
try:
ia = instance.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
if instance:
try:
if instance.variation:
@@ -159,7 +189,10 @@ class OrderPositionChangeForm(forms.Form):
except Item.DoesNotExist:
pass
initial['price'] = instance.price
if instance.item.tax_rule and not instance.item.tax_rule.price_includes_tax:
initial['price'] = instance.price - instance.tax_value
else:
initial['price'] = instance.price
initial['subevent'] = instance.subevent
kwargs['initial'] = initial
@@ -169,20 +202,24 @@ class OrderPositionChangeForm(forms.Form):
self.fields['subevent'].queryset = instance.order.event.subevents.all()
else:
del self.fields['subevent']
choices = []
for i in instance.order.event.items.prefetch_related('variations').all():
pname = str(i.name)
if not i.is_available():
pname += ' ({})'.format(_('inactive'))
variations = list(i.variations.all())
if variations:
for v in variations:
p = get_price(i, v, voucher=instance.voucher, subevent=instance.subevent)
p = get_price(i, v, voucher=instance.voucher, subevent=instance.subevent,
invoice_address=ia)
choices.append(('%d-%d' % (i.pk, v.pk),
'%s %s (%s %s)' % (pname, v.value, localize(p),
instance.order.event.currency)))
else:
p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent)
p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent,
invoice_address=ia)
choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(p),
instance.order.event.currency)))
self.fields['itemvar'].choices = choices

View File

@@ -146,6 +146,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.question.added': _('The question has been added.'),
'pretix.event.question.deleted': _('The question has been deleted.'),
'pretix.event.question.changed': _('The question has been modified.'),
'pretix.event.taxrule.added': _('The tax rule has been added.'),
'pretix.event.taxrule.deleted': _('The tax rule has been deleted.'),
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),
'pretix.event.settings': _('The event settings have been changed.'),
'pretix.event.tickets.settings': _('The ticket download settings have been changed.'),
'pretix.event.plugins.enabled': _('A plugin has been enabled.'),

View File

@@ -10,10 +10,10 @@
{% bootstrap_field form.invoice_address_asked layout="horizontal" %}
{% bootstrap_field form.invoice_address_required layout="horizontal" %}
{% bootstrap_field form.invoice_name_required layout="horizontal" %}
{% bootstrap_field form.invoice_generate layout="horizontal" %}
{% bootstrap_field form.invoice_address_vatid layout="horizontal" %}
{% bootstrap_field form.invoice_numbers_consecutive layout="horizontal" %}
{% bootstrap_field form.invoice_numbers_prefix layout="horizontal" %}
{% bootstrap_field form.invoice_generate layout="horizontal" %}
{% bootstrap_field form.invoice_renderer layout="horizontal" %}
{% bootstrap_field form.invoice_language layout="horizontal" %}
{% bootstrap_field form.invoice_include_free layout="horizontal" %}

View File

@@ -36,6 +36,11 @@
{% trans "Email" %}
</a>
</li>
<li {% if "event.settings.tax" in url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.tax' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Tax rules" %}
</a>
</li>
<li {% if "event.settings.invoice" == url_name %}class="active"{% endif %}>
<a href="{% url 'control:event.settings.invoice' organizer=request.event.organizer.slug event=request.event.slug %}">
{% trans "Invoicing" %}

View File

@@ -0,0 +1,26 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete tax rule" %}{% endblock %}
{% block inside %}
<legend>{% trans "Delete tax rule" %}</legend>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if possible %}
<p>{% blocktrans %}Are you sure you want to delete the tax rule <strong>{{ taxrule }}</strong>?{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans %}You cannot delete a tax rule that is in use for a product or has been in use for any existing orders.{% endblocktrans %}</p>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.settings.tax" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn
btn-default btn-cancel">
{% trans "Cancel" %}
</a>
{% if possible %}
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}
{% if rule %}
{% blocktrans with name=rule.name %}Tax rule: {{ name }}{% endblocktrans %}
{% else %}
{% trans "Tax rule" %}
{% endif %}
{% endblock %}
{% block inside %}
{% if rule %}
<legend>{% blocktrans with name=rule.name %}Tax rule: {{ name }}{% endblocktrans %}</legend>
{% else %}
<legend>{% trans "Tax rule" %}</legend>
{% endif %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.rate layout="horizontal" %}
<legend>{% trans "Advanced settings" %}</legend>
<div class="alert alert-warning">
<span class="fa fa-w 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
of taxes in your ticket shop. If in doubt, please contact a lawyer or tax consultant.
{% endblocktrans %}
<div class="clearfix"></div>
</div>
{% bootstrap_field form.price_includes_tax layout="horizontal" %}
{% bootstrap_field form.eu_reverse_charge layout="horizontal" %}
{% bootstrap_field form.home_country layout="horizontal" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% block title %}{% trans "Tax rules" %}{% endblock %}
{% block inside %}
<legend>{% trans "Tax rules" %}</legend>
{% if taxrules|length == 0 %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
You haven't created any tax rules yet.
{% endblocktrans %}
</p>
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}</a>
</div>
{% else %}
<p>
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}
</a>
</p>
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Rate" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for tr in taxrules %}
<tr>
<td>
<strong><a href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
{{ tr.name }}
</a></strong>
</td>
<td>{{ tr.rate }} %</td>
<td class="text-right">
<a href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.settings.tax.delete" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -33,6 +33,7 @@
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.location layout="horizontal" %}
{% bootstrap_field form.currency layout="horizontal" %}
{% bootstrap_field form.tax_rate layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Display settings" %}</legend>

View File

@@ -31,7 +31,7 @@
<fieldset>
<legend>{% trans "Price settings" %}</legend>
{% bootstrap_field form.default_price layout="horizontal" %}
{% bootstrap_field form.tax_rate layout="horizontal" %}
{% bootstrap_field form.tax_rule layout="horizontal" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -18,7 +18,7 @@
<fieldset>
<legend>{% trans "Price settings" %}</legend>
{% bootstrap_field form.default_price layout="horizontal" %}
{% bootstrap_field form.tax_rate layout="horizontal" %}
{% bootstrap_field form.tax_rule layout="horizontal" %}
{% bootstrap_field form.free_price layout="horizontal" %}
</fieldset>
<fieldset>

View File

@@ -103,8 +103,18 @@
{% if position.form.operation.value == "price" %}checked="checked"{% endif %}>
{% trans "Change price to" %}
{% bootstrap_field position.form.price layout='inline' %}
{% if request.event.settings.display_net_prices %}
<em>{% trans "Enter a gross price including taxes." %}</em>
{% if position.apply_tax %}
{% if position.item.tax_rule and not position.item.tax_rule.price_includes_tax %}
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %}
<strong>plus</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
{% elif position.item.tax_rule %}
{% blocktrans trimmed with rate=position.item.tax_rule.rate name=position.item.tax_rule.name %}
<strong>incl.</strong> {{ rate }}% {{ name }}
{% endblocktrans %}
{% endif %}
{% else %}
{% trans "no taxes apply" %}
{% endif %}
</label>
</div>
@@ -150,6 +160,24 @@
</div>
</div>
</div>
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">
Other operations
</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_form_errors other_form %}
{% if other_form.custom_error %}
<div class="alert alert-danger">
{{ other_form.custom_error }}
</div>
{% endif %}
{% bootstrap_field other_form.recalculate_taxes layout='horizontal' %}
</div>
</div>
</div>
<div class="form-group submit-group">
<a class="btn btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">

View File

@@ -9,6 +9,7 @@
{% endblocktrans %}
{% endblock %}
{% block content %}
{% blocktrans asvar s_taxes %}taxes{% endblocktrans %}
<h1>
{% blocktrans trimmed with code=order.code %}
Order details: {{ code }}
@@ -239,16 +240,22 @@
{% if event.settings.display_net_prices %}
<strong>{{ event.currency }} {{ line.net_price|floatformat:2 }}</strong>
{% if line.tax_rate %}
<br /><small>{% blocktrans trimmed with rate=line.tax_rate %}
<strong>plus</strong> {{ rate }}% taxes
{% endblocktrans %}</small>
<br />
<small>
{% blocktrans trimmed with rate=line.tax_rate taxname=line.tax_rule.name|default:s_taxes %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
{% else %}
<strong>{{ event.currency }} {{ line.price|floatformat:2 }}</strong>
{% if line.tax_rate %}
<br /><small>{% blocktrans trimmed with rate=line.tax_rate %}
incl. {{ rate }}% taxes
{% endblocktrans %}</small>
<br />
<small>
{% blocktrans trimmed with rate=line.tax_rate taxname=line.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
{% endif %}
</div>
@@ -265,17 +272,21 @@
<strong>{{ event.currency }} {{ order.payment_fee_net|floatformat:2 }}</strong>
{% if order.payment_fee_tax_rate %}
<br/>
<small>{% blocktrans trimmed with rate=order.payment_fee_tax_rate %}
<strong>plus</strong> {{ rate }}% taxes
{% endblocktrans %}</small>
<small>
{% blocktrans trimmed with rate=order.payment_fee_tax_rate taxname=order.payment_fee_tax_rule.name|default:s_taxes %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
{% else %}
<strong>{{ event.currency }} {{ items.payment_fee|floatformat:2 }}</strong>
{% if order.payment_fee_tax_rate %}
<br/>
<small>{% blocktrans trimmed with rate=order.payment_fee_tax_rate %}
incl. {{ rate }}% taxes
{% endblocktrans %}</small>
<small>
{% blocktrans trimmed with rate=order.payment_fee_tax_rate taxname=order.payment_fee_tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
{% endif %}
</div>
@@ -358,7 +369,20 @@
<dd>{{ order.invoice_address.country.name|default:order.invoice_address.country_old }}</dd>
{% if request.event.settings.invoice_address_vatid %}
<dt>{% trans "VAT ID" %}</dt>
<dd>{{ order.invoice_address.vat_id }}</dd>
<dd>
{{ order.invoice_address.vat_id }}
{% if order.invoice_address.vat_id_validated %}
<span class="fa fa-check" data-toggle="tooltip" title="{% blocktrans trimmed %}Valid EU VAT ID{% endblocktrans %}"></span>
{% else %}
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.checkvatid" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% csrf_token %}
<button class="btn btn-default btn-xs">
{% trans "Check" %}
</button>
</form>
{% endif %}
</dd>
{% endif %}
</dl>
</div>

View File

@@ -69,6 +69,10 @@ urlpatterns = [
url(r'^settings/invoice$', event.InvoiceSettings.as_view(), name='event.settings.invoice'),
url(r'^settings/invoice/preview$', event.InvoicePreview.as_view(), name='event.settings.invoice.preview'),
url(r'^settings/display', event.DisplaySettings.as_view(), name='event.settings.display'),
url(r'^settings/tax/$', event.TaxList.as_view(), name='event.settings.tax'),
url(r'^settings/tax/(?P<rule>\d+)/$', event.TaxUpdate.as_view(), name='event.settings.tax.edit'),
url(r'^settings/tax/add$', event.TaxCreate.as_view(), name='event.settings.tax.add'),
url(r'^settings/tax/(?P<rule>\d+)/delete$', event.TaxDelete.as_view(), name='event.settings.tax.delete'),
url(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'),
url(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'),
url(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
@@ -131,6 +135,8 @@ urlpatterns = [
url(r'^orders/(?P<code>[0-9A-Z]+)/answer/(?P<answer>[^/]+)/$',
orders.AnswerDownload.as_view(),
name='event.order.download.answer'),
url(r'^orders/(?P<code>[0-9A-Z]+)/checkvatid', orders.OrderCheckVATID.as_view(),
name='event.order.checkvatid'),
url(r'^orders/(?P<code>[0-9A-Z]+)/extend$', orders.OrderExtend.as_view(),
name='event.order.extend'),
url(r'^orders/(?P<code>[0-9A-Z]+)/contact$', orders.OrderContactChange.as_view(),

View File

@@ -9,22 +9,24 @@ from django.core.files import File
from django.core.urlresolvers import reverse
from django.db import transaction
from django.http import (
HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, JsonResponse,
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect
from django.utils import translation
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView, ListView
from django.utils.translation import ugettext, ugettext_lazy as _
from django.views.generic import DeleteView, FormView, ListView
from django.views.generic.base import TemplateView, View
from django.views.generic.detail import SingleObjectMixin
from i18nfield.strings import LazyI18nString
from pytz import timezone
from pretix.base.models import (
CachedTicket, Event, Item, ItemVariation, LogEntry, Order, RequiredAction,
Voucher,
CachedTicket, Event, Item, ItemVariation, LogEntry, Order, OrderPosition,
RequiredAction, TaxRule, Voucher,
)
from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf
@@ -32,13 +34,13 @@ from pretix.base.signals import event_live_issues, register_ticket_outputs
from pretix.control.forms.event import (
CommentForm, DisplaySettingsForm, EventSettingsForm, EventUpdateForm,
InvoiceSettingsForm, MailSettingsForm, PaymentSettingsForm, ProviderForm,
TicketSettingsForm,
TaxRuleForm, TicketSettingsForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.urls import build_absolute_uri
from pretix.presale.style import regenerate_css
from . import UpdateView
from . import CreateView, UpdateView
from ..logdisplay import OVERVIEW_BLACKLIST
@@ -787,3 +789,129 @@ class EventComment(EventPermissionRequiredMixin, View):
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug
})
class TaxList(EventPermissionRequiredMixin, ListView):
model = TaxRule
context_object_name = 'taxrules'
paginate_by = 30
template_name = 'pretixcontrol/event/tax_index.html'
permission = 'can_change_event_settings'
def get_queryset(self):
return self.request.event.tax_rules.all()
class TaxCreate(EventPermissionRequiredMixin, CreateView):
model = TaxRule
form_class = TaxRuleForm
template_name = 'pretixcontrol/event/tax_edit.html'
permission = 'can_change_event_settings'
context_object_name = 'taxrule'
def get_success_url(self) -> str:
return reverse('control:event.settings.tax', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
def get_initial(self):
return {
'name': LazyI18nString.from_gettext(ugettext('VAT'))
}
@transaction.atomic
def form_valid(self, form):
form.instance.event = self.request.event
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))
return ret
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class TaxUpdate(EventPermissionRequiredMixin, UpdateView):
model = TaxRule
form_class = TaxRuleForm
template_name = 'pretixcontrol/event/tax_edit.html'
permission = 'can_change_event_settings'
context_object_name = 'rule'
def get_object(self, queryset=None) -> TaxRule:
try:
return self.request.event.tax_rules.get(
id=self.kwargs['rule']
)
except TaxRule.DoesNotExist:
raise Http404(_("The requested tax rule does not exist."))
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
if form.has_changed():
self.object.log_action(
'pretix.event.taxrule.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
return super().form_valid(form)
def get_success_url(self) -> str:
return reverse('control:event.settings.tax', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class TaxDelete(EventPermissionRequiredMixin, DeleteView):
model = TaxRule
template_name = 'pretixcontrol/event/tax_delete.html'
permission = 'can_change_event_settings'
context_object_name = 'taxrule'
def get_object(self, queryset=None) -> TaxRule:
try:
return self.request.event.tax_rules.get(
id=self.kwargs['rule']
)
except TaxRule.DoesNotExist:
raise Http404(_("The requested tax rule does not exist."))
@transaction.atomic
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
if self.is_allowed():
self.object.log_action(action='pretix.event.taxrule.deleted', user=request.user)
self.object.delete()
messages.success(self.request, _('The selected tax rule has been deleted.'))
else:
messages.error(self.request, _('The selected tax rule can not be deleted.'))
return redirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.settings.tax', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
def is_allowed(self) -> bool:
o = self.object
return (
not self.request.event.orders.filter(payment_fee_tax_rule=o).exists()
and not OrderPosition.objects.filter(tax_rule=o, order__event=self.request.event).exists()
and not self.request.event.items.filter(tax_rule=o).exists()
and self.request.event.settings.tax_rate_default != o
)
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['possible'] = self.is_allowed()
return context

View File

@@ -775,6 +775,13 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
'item': self.object.id,
})
def get_initial(self):
initial = super().get_initial()
trs = list(self.request.event.tax_rules.all())
if len(trs) == 1:
initial['tax_rule'] = trs[0]
return initial
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))

View File

@@ -6,10 +6,11 @@ from django.http import JsonResponse
from django.shortcuts import redirect
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext, ugettext_lazy as _
from django.views import View
from django.views.generic import ListView
from formtools.wizard.views import SessionWizardView
from i18nfield.strings import LazyI18nString
from pretix.base.models import Event, Team
from pretix.control.forms.event import (
@@ -118,6 +119,12 @@ class EventWizard(SessionWizardView):
active=True
)
if basics_data['tax_rate']:
event.settings.tax_rate_default = event.tax_rules.create(
name=LazyI18nString.from_gettext(ugettext('VAT')),
rate=basics_data['tax_rate']
)
logdata = {}
for f in form_list:
logdata.update({

View File

@@ -1,8 +1,10 @@
import logging
import mimetypes
import os
from datetime import timedelta
import pytz
import vat_moss.id
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse
@@ -25,6 +27,7 @@ from pretix.base.models import (
generate_position_secret, generate_secret,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.services.export import export
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
@@ -42,12 +45,15 @@ from pretix.control.forms.filter import EventOrderFilterForm
from pretix.control.forms.orders import (
CommentForm, ExporterForm, ExtendForm, OrderContactForm, OrderLocaleForm,
OrderMailForm, OrderPositionAddForm, OrderPositionChangeForm,
OtherOperationsForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
class OrderList(EventPermissionRequiredMixin, ListView):
model = Order
@@ -144,7 +150,7 @@ class OrderDetail(OrderView):
cartpos = queryset.order_by(
'item', 'variation'
).select_related(
'item', 'variation', 'addon_to'
'item', 'variation', 'addon_to', 'tax_rule'
).prefetch_related(
'item__questions', 'answers', 'answers__question', 'checkins'
).order_by('positionid')
@@ -277,6 +283,54 @@ class OrderInvoiceCreate(OrderView):
return HttpResponseNotAllowed(['POST'])
class OrderCheckVATID(OrderView):
permission = 'can_change_orders'
def post(self, *args, **kwargs):
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
messages.error(self.request, _('No VAT ID specified.'))
return redirect(self.get_order_url())
else:
if not ia.vat_id:
messages.error(self.request, _('No VAT ID specified.'))
return redirect(self.get_order_url())
if not ia.country:
messages.error(self.request, _('No country specified.'))
return redirect(self.get_order_url())
if str(ia.country) not in EU_COUNTRIES:
messages.error(self.request, _('VAT ID could not be checked since a non-EU country has been '
'specified.'))
return redirect(self.get_order_url())
if ia.vat_id[:2] != str(ia.country):
messages.error(self.request, _('Your VAT ID does not match the selected country.'))
return redirect(self.get_order_url())
try:
result = vat_moss.id.validate(ia.vat_id)
if result:
country_code, normalized_id, company_name = result
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
except vat_moss.errors.InvalidError:
messages.error(self.request, _('This VAT ID is not valid.'))
except vat_moss.errors.WebServiceUnavailableError:
logger.exception('VAT ID checking failed for country {}'.format(ia.country))
messages.error(self.request, _('The VAT ID could not be checked, as the VAT checking service of '
'the country is currently not available.'))
else:
messages.success(self.request, _('This VAT ID is valid.'))
return redirect(self.get_order_url())
def get(self, *args, **kwargs): # NOQA
return HttpResponseNotAllowed(['POST'])
class OrderInvoiceRegenerate(OrderView):
permission = 'can_change_orders'
@@ -458,6 +512,11 @@ class OrderChange(OrderView):
return self._redirect_back()
return super().dispatch(request, *args, **kwargs)
@cached_property
def other_form(self):
return OtherOperationsForm(prefix='other', order=self.order,
data=self.request.POST if self.request.method == "POST" else None)
@cached_property
def add_form(self):
return OrderPositionAddForm(prefix='add', order=self.order,
@@ -469,14 +528,28 @@ class OrderChange(OrderView):
for p in positions:
p.form = OrderPositionChangeForm(prefix='op-{}'.format(p.pk), instance=p,
data=self.request.POST if self.request.method == "POST" else None)
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
p.apply_tax = p.item.tax_rule and p.item.tax_rule.tax_applicable(invoice_address=ia)
return positions
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['positions'] = self.positions
ctx['add_form'] = self.add_form
ctx['other_form'] = self.other_form
return ctx
def _process_other(self, ocm):
if not self.other_form.is_valid():
return False
else:
if self.other_form.cleaned_data['recalculate_taxes']:
ocm.recalculate_taxes()
return True
def _process_add(self, ocm):
if not self.add_form.is_valid():
return False
@@ -534,7 +607,7 @@ class OrderChange(OrderView):
def post(self, *args, **kwargs):
ocm = OrderChangeManager(self.order, self.request.user)
form_valid = self._process_add(ocm) and self._process_change(ocm)
form_valid = self._process_add(ocm) and self._process_change(ocm) and self._process_other(ocm)
if not form_valid:
messages.error(self.request, _('An error occured. Please see the details below.'))