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,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.'))