From 0cc2155aa5f2ab533cdfd7b92d693ada6e107194 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 13 Aug 2025 09:58:51 +0200 Subject: [PATCH] Add settings page --- src/pretix/base/settings.py | 13 +- src/pretix/control/forms/event.py | 79 +++++++++++- src/pretix/control/navigation.py | 2 +- .../pretixcontrol/event/invoicing.html | 1 - .../pretixcontrol/event/settings.html | 1 - .../templates/pretixcontrol/event/tax.html | 120 ++++++++++++++++++ .../pretixcontrol/event/tax_index.html | 77 ----------- src/pretix/control/urls.py | 2 +- src/pretix/control/views/event.py | 33 +++-- 9 files changed, 227 insertions(+), 101 deletions(-) create mode 100644 src/pretix/control/templates/pretixcontrol/event/tax.html delete mode 100644 src/pretix/control/templates/pretixcontrol/event/tax_index.html diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 526f1c563e..173a586aa1 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -78,9 +78,9 @@ from pretix.control.forms import ( from pretix.helpers.countries import CachedCountries ROUNDING_MODES = ( - ('line', _('Rounding every line individually')), - ('sum_by_net', _('Rounding by order total, keeping net prices stable')), - ('sum_by_gross', _('Rounding by order total, keeping gross prices stable')), + ('line', _('Round taxes for every line individually')), + ('sum_by_net', _('Round taxes by order total, keeping net prices stable')), + ('sum_by_gross', _('Round taxes by order total, keeping gross prices stable')), ) @@ -330,7 +330,7 @@ DEFAULTS = { 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, 'form_kwargs': dict( - label=_("Show net prices instead of gross prices in the product list (not recommended!)"), + label=_("Show net prices instead of gross prices in the product list"), help_text=_("Independent of your choice, the cart will show gross prices as this is the price that needs to be " "paid."), @@ -480,6 +480,11 @@ DEFAULTS = { label=_("Rounding of taxes"), widget=forms.RadioSelect, choices=ROUNDING_MODES, + help_text=_( + "Note that if you transfer your sales data from pretix to an external system for tax reporting, you " + "need to make sure to account for possible rounding differences if your external system rounds " + "differently than pretix." + ) ), 'serializer_kwargs': dict( choices=ROUNDING_MODES, diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index ab42d57df7..bafbd62f33 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -68,7 +68,7 @@ from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from pretix.base.services.placeholders import FormPlaceholderMixin from pretix.base.settings import ( COUNTRIES_WITH_STATE_IN_ADDRESS, DEFAULTS, PERSON_NAME_SCHEMES, - PERSON_NAME_TITLE_GROUPS, validate_event_settings, + PERSON_NAME_TITLE_GROUPS, ROUNDING_MODES, validate_event_settings, ) from pretix.base.validators import multimail_validate from pretix.control.forms import ( @@ -541,7 +541,6 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett 'show_date_to', 'show_times', 'show_items_outside_presale_period', - 'display_net_prices', 'hide_prices_from_attendees', 'presale_start_show_date', 'locales', @@ -799,6 +798,76 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm): return value +class DisplayNetPricesBooleanSelect(forms.RadioSelect): + def __init__(self, attrs=None): + choices = ( + ("false", format_html( + '{}
{}', + _("Prices including tax"), + _("Recommended if you sell tickets at least partly to consumers.") + )), + ("true", format_html( + '{}
{}', + _("Prices excluding tax"), + _("Recommended only if you sell tickets primarily to business customers.") + )), + ) + super().__init__(attrs, choices) + + def format_value(self, value): + try: + return { + True: "true", + False: "false", + "true": "true", + "false": "false", + }[value] + except KeyError: + return "unknown" + + def value_from_datadict(self, data, files, name): + value = data.get(name) + return { + True: True, + "True": True, + "False": False, + False: False, + "true": True, + "false": False, + }.get(value) + + +class TaxSettingsForm(EventSettingsValidationMixin, SettingsForm): + auto_fields = [ + 'display_net_prices', + 'tax_rounding', + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["display_net_prices"].label = _("Prices shown to customer") + self.fields["display_net_prices"].widget = DisplayNetPricesBooleanSelect() + help_text = { + "line": _("Recommended when e-invoicing is not required. Each product will be sold with the advertised " + "net and gross price. However, in orders of more than one product, the total tax amount " + "can differ from when it would be computed from the order total."), + "sum_by_net": _("Recommended for e-invoicing in Europe when you primarily sell to business customers and " + "show prices to customers excluding tax. " + "For orders of more than one product, the gross price of some products may be changed " + "automatically to ensure correct rounding of the order total, while the net prices " + "stay as configured. This may cause the actual payment amount to differ from buying the " + "products individually."), + "sum_by_gross": _("Recommended for e-invoicing in Europe when you primarily sell to consumers. " + "For an order of more than one product, the net price of some products may be changed " + "automatically to ensure correct rounding of the order total, while the gross prices " + "stay as configured."), + } + self.fields["tax_rounding"].choices = ( + (k, format_html('{}
{}', v, help_text.get(k, ""))) + for k, v in ROUNDING_MODES + ) + + class ProviderForm(SettingsForm): """ This is a SettingsForm, but if fields are set to required=True, validation @@ -881,7 +950,6 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm): 'invoice_logo_image', 'invoice_renderer_highlight_order_code', 'invoice_renderer_font', - 'tax_rounding', ] invoice_generate_sales_channels = forms.MultipleChoiceField( @@ -1511,7 +1579,10 @@ class TaxRuleLineForm(I18nForm): rate = forms.DecimalField( label=_('Deviating tax rate'), max_digits=10, decimal_places=2, - required=False + required=False, + widget=forms.NumberInput(attrs={ + 'placeholder': _('Deviating tax rate'), + }) ) invoice_text = I18nFormField( label=_('Text on invoice'), diff --git a/src/pretix/control/navigation.py b/src/pretix/control/navigation.py index cf8e43a798..db1304230b 100644 --- a/src/pretix/control/navigation.py +++ b/src/pretix/control/navigation.py @@ -86,7 +86,7 @@ def get_event_navigation(request: HttpRequest): 'active': url.url_name == 'event.settings.mail', }, { - 'label': _('Tax rules'), + 'label': _('Taxes'), 'url': reverse('control:event.settings.tax', kwargs={ 'event': request.event.slug, 'organizer': request.event.organizer.slug, diff --git a/src/pretix/control/templates/pretixcontrol/event/invoicing.html b/src/pretix/control/templates/pretixcontrol/event/invoicing.html index 74bd1a9650..a3f8b89965 100644 --- a/src/pretix/control/templates/pretixcontrol/event/invoicing.html +++ b/src/pretix/control/templates/pretixcontrol/event/invoicing.html @@ -21,7 +21,6 @@ {% bootstrap_field form.invoice_numbers_prefix layout="control" %} {% bootstrap_field form.invoice_numbers_prefix_cancellations layout="control" %} {% bootstrap_field form.invoice_numbers_counter_length layout="control" %} - {% bootstrap_field form.tax_rounding layout="control" %}
{% trans "Address form" %} diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index 3a90aef7ca..8d9a46f732 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -243,7 +243,6 @@ {% bootstrap_field sform.show_times layout="control" %}

{% trans "Product list" %}

{% bootstrap_field sform.show_quota_left layout="control" %} - {% bootstrap_field sform.display_net_prices layout="control" %} {% bootstrap_field sform.show_variations_expanded layout="control" %} {% bootstrap_field sform.hide_sold_out layout="control" %} diff --git a/src/pretix/control/templates/pretixcontrol/event/tax.html b/src/pretix/control/templates/pretixcontrol/event/tax.html new file mode 100644 index 0000000000..9a58c75405 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/event/tax.html @@ -0,0 +1,120 @@ +{% extends "pretixcontrol/event/settings_base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Taxes" %}{% endblock %} +{% block inside %} +

{% trans "Taxes" %}

+ {% bootstrap_form_errors form layout="control" %} +
+ {% trans "Tax rules" %} +

+ {% blocktrans trimmed %} + Tax rules define different taxation scenarios that can then be assigned to the individual products. + Each tax rule contains a default tax rate and can optionally contain additional rules that depend + on the customer's country and type. + {% endblocktrans %} +

+ + {% if taxrules|length == 0 %} +
+

+ {% blocktrans trimmed %} + You haven't created any tax rules yet. + {% endblocktrans %} +

+ + {% trans "Create a new tax rule" %} +
+ {% else %} +
+ + + + + + + + + + + + {% for tr in taxrules %} + + + + + + + + {% endfor %} + + + + + + +
{% trans "Name" %}{% trans "Default" %}{% trans "Usage" %}{% trans "Rate" %}
+ + {{ tr.internal_name|default:tr.name }} + + + {% if tr.default %} + + + {% trans "Default" %} + + {% else %} +
+ {% csrf_token %} + +
+ {% endif %} +
+ {% blocktrans trimmed count count=tr.c_items %} + {{ count }} product + {% plural %} + {{ count }} products + {% endblocktrans %} + + {% if tr.price_includes_tax %} + {% blocktrans with rate=tr.rate %}incl. {{ rate }} %{% endblocktrans %} + {% else %} + {% blocktrans with rate=tr.rate %}excl. {{ rate }} %{% endblocktrans %} + {% endif %} + {% if tr.has_custom_rules %} +
{% trans "with custom rules" %} + {% elif tr.eu_reverse_charge %} +
{% trans "reverse charge enabled" %} + {% endif %} +
+ + +
+ {% trans "Create a new tax rule" %} + +
+
+ {% endif %} +
+
+ {% csrf_token %} +
+ {% trans "Tax settings" %} + {% bootstrap_field form.tax_rounding layout="control" %} + {% bootstrap_field form.display_net_prices layout="control" %} +
+
+ +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/event/tax_index.html b/src/pretix/control/templates/pretixcontrol/event/tax_index.html deleted file mode 100644 index 19123adb1b..0000000000 --- a/src/pretix/control/templates/pretixcontrol/event/tax_index.html +++ /dev/null @@ -1,77 +0,0 @@ -{% extends "pretixcontrol/event/settings_base.html" %} -{% load i18n %} -{% block title %}{% trans "Tax rules" %}{% endblock %} -{% block inside %} -

{% trans "Tax rules" %}

- {% if taxrules|length == 0 %} -
-

- {% blocktrans trimmed %} - You haven't created any tax rules yet. - {% endblocktrans %} -

- - {% trans "Create a new tax rule" %} -
- {% else %} -

- {% trans "Create a new tax rule" %} - -

-
- - - - - - - - - - - {% for tr in taxrules %} - - - - - - - {% endfor %} - -
{% trans "Name" %}{% trans "Default" %}{% trans "Rate" %}
- - {{ tr.internal_name|default:tr.name }} - - - {% if tr.default %} - - - {% trans "Default" %} - - {% else %} -
- {% csrf_token %} - -
- {% endif %} -
- {% if tr.price_includes_tax %} - {% blocktrans with rate=tr.rate%}incl. {{ rate }} %{% endblocktrans %} - {% else %} - {% blocktrans with rate=tr.rate%}excl. {{ rate }} %{% endblocktrans %} - {% endif %} - {% if tr.eu_reverse_charge %} - ({% trans "reverse charge enabled" %}) - {% endif %} - - - -
-
- {% endif %} - {% include "pretixcontrol/pagination.html" %} -{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index f14286706e..9d1368ea6f 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -286,7 +286,7 @@ urlpatterns = [ re_path(r'^settings/invoice$', event.InvoiceSettings.as_view(), name='event.settings.invoice'), re_path(r'^settings/invoice/preview$', event.InvoicePreview.as_view(), name='event.settings.invoice.preview'), re_path(r'^settings/display', event.DisplaySettings.as_view(), name='event.settings.display'), - re_path(r'^settings/tax/$', event.TaxList.as_view(), name='event.settings.tax'), + re_path(r'^settings/tax/$', event.TaxSettings.as_view(), name='event.settings.tax'), re_path(r'^settings/tax/(?P\d+)/$', event.TaxUpdate.as_view(), name='event.settings.tax.edit'), re_path(r'^settings/tax/add$', event.TaxCreate.as_view(), name='event.settings.tax.add'), re_path(r'^settings/tax/(?P\d+)/delete$', event.TaxDelete.as_view(), name='event.settings.tax.delete'), diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index a5d6fc408e..e777fa59b9 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -54,7 +54,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.core.files import File from django.db import transaction -from django.db.models import ProtectedError +from django.db.models import Count, ProtectedError from django.forms import inlineformset_factory from django.http import ( Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, @@ -87,7 +87,7 @@ from pretix.control.forms.event import ( EventFooterLinkFormset, EventMetaValueForm, EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, ItemMetaPropertyForm, MailSettingsForm, PaymentSettingsForm, ProviderForm, QuickSetupForm, - QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet, + QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet, TaxSettingsForm, TicketSettingsForm, WidgetCodeForm, ) from pretix.control.permissions import EventPermissionRequiredMixin @@ -620,6 +620,25 @@ class PaymentSettings(EventSettingsViewMixin, EventSettingsFormView): return context +class TaxSettings(EventSettingsViewMixin, EventSettingsFormView): + template_name = 'pretixcontrol/event/tax.html' + form_class = TaxSettingsForm + permission = 'can_change_event_settings' + + def get_success_url(self) -> str: + return reverse('control:event.settings.tax', kwargs={ + 'organizer': self.request.organizer.slug, + 'event': self.request.event.slug, + }) + + def get_context_data(self, *args, **kwargs) -> dict: + context = super().get_context_data(*args, **kwargs) + context['taxrules'] = self.request.event.tax_rules.annotate( + c_items=Count("item") + ).all() + return context + + class InvoiceSettings(EventSettingsViewMixin, EventSettingsFormView): model = Event form_class = InvoiceSettingsForm @@ -1219,16 +1238,6 @@ class EventComment(EventPermissionRequiredMixin, View): }) -class TaxList(EventSettingsViewMixin, EventPermissionRequiredMixin, PaginationMixin, ListView): - model = TaxRule - context_object_name = 'taxrules' - 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(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView): model = TaxRule form_class = TaxRuleForm