mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
New data model for default tax rule and new options for cancellation fees (#4962)
* New data model for default tax rule * Remove misleading empty label when field is not optional * Allow to split cancellation fee * Fix API and tests * Update migration * Update src/tests/api/test_taxrules.py Co-authored-by: luelista <weller@rami.io> * Update src/tests/api/test_taxrules.py Co-authored-by: luelista <weller@rami.io> * Review note * Update src/pretix/base/models/tax.py Co-authored-by: luelista <weller@rami.io> * Flip API behaviour for default * Fix failing tests * Fix failing test * Split migration --------- Co-authored-by: luelista <weller@rami.io>
This commit is contained in:
@@ -761,6 +761,7 @@ class CancelSettingsForm(SettingsForm):
|
||||
'change_allow_user_addons',
|
||||
'change_allow_user_if_checked_in',
|
||||
'change_allow_attendee',
|
||||
'tax_rule_cancellation',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -783,14 +784,8 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
'payment_term_accept_late',
|
||||
'payment_pending_hidden',
|
||||
'payment_explanation',
|
||||
'tax_rule_payment',
|
||||
]
|
||||
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_payment_term_days(self):
|
||||
value = self.cleaned_data.get('payment_term_days')
|
||||
@@ -804,10 +799,6 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
raise ValidationError(_("This field is required."))
|
||||
return value
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['tax_rate_default'].queryset = self.obj.tax_rules.all()
|
||||
|
||||
|
||||
class ProviderForm(SettingsForm):
|
||||
"""
|
||||
|
||||
@@ -406,7 +406,6 @@ class ItemCreateForm(I18nModelForm):
|
||||
|
||||
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
|
||||
change_decimal_field(self.fields['default_price'], self.instance.event.currency)
|
||||
self.fields['tax_rule'].empty_label = _('No taxation')
|
||||
self.fields['copy_from'] = forms.ModelChoiceField(
|
||||
label=_("Copy product information"),
|
||||
queryset=self.event.items.all(),
|
||||
@@ -416,6 +415,8 @@ class ItemCreateForm(I18nModelForm):
|
||||
)
|
||||
if self.event.tax_rules.exists():
|
||||
self.fields['tax_rule'].required = True
|
||||
else:
|
||||
self.fields['tax_rule'].empty_label = _('No taxation')
|
||||
|
||||
if not self.event.has_subevents:
|
||||
choices = [
|
||||
|
||||
@@ -174,8 +174,7 @@ class CancelForm(forms.Form):
|
||||
label=_('Keep a cancellation fee of'),
|
||||
help_text=_('If you keep a fee, all positions within this order will be canceled and the order will be reduced '
|
||||
'to a cancellation fee. Payment and shipping fees will be canceled as well, so include them '
|
||||
'in your cancellation fee if you want to keep them. Please always enter a gross value, '
|
||||
'tax will be calculated automatically.'),
|
||||
'in your cancellation fee if you want to keep them.'),
|
||||
)
|
||||
cancel_invoice = forms.BooleanField(
|
||||
label=_('Generate cancellation for invoice'),
|
||||
@@ -200,6 +199,19 @@ class CancelForm(forms.Form):
|
||||
self.fields['cancellation_fee'].max_value = self.instance.total
|
||||
if not self.instance.invoices.exists():
|
||||
del self.fields['cancel_invoice']
|
||||
if self.instance.event.settings.tax_rule_cancellation == 'split':
|
||||
self.fields['cancellation_fee'].help_text = str(self.fields['cancellation_fee'].help_text) + ' ' + _(
|
||||
'Please enter a gross amount. As per your event settings, the taxes will be split the same way as the '
|
||||
'order positions.'
|
||||
)
|
||||
elif self.instance.event.settings.tax_rule_cancellation == 'default':
|
||||
self.fields['cancellation_fee'].help_text = str(self.fields['cancellation_fee'].help_text) + ' ' + _(
|
||||
'Please enter a gross amount. As per your event settings, the default tax rate will be charged.'
|
||||
)
|
||||
elif self.instance.event.settings.tax_rule_cancellation == 'none':
|
||||
self.fields['cancellation_fee'].help_text = str(self.fields['cancellation_fee'].help_text) + ' ' + _(
|
||||
'As per your event settings, no tax will be charged.'
|
||||
)
|
||||
|
||||
def clean_cancellation_fee(self):
|
||||
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
{% bootstrap_field form.cancel_allow_user_paid_keep layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_paid_keep_percentage layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_paid_keep_fees layout="control" %}
|
||||
{% bootstrap_field form.tax_rule_cancellation layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_paid_until layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_paid_adjust_fees layout="control" %}
|
||||
<div data-display-dependency="#id_cancel_allow_user_paid_adjust_fees">
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced" %}</legend>
|
||||
{% bootstrap_form_errors form layout="control" %}
|
||||
{% bootstrap_field form.tax_rate_default layout="control" %}
|
||||
{% bootstrap_field form.tax_rule_payment layout="control" %}
|
||||
{% bootstrap_field form.payment_explanation layout="control" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,10 @@
|
||||
{% 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>
|
||||
<p>{% blocktrans %}You cannot delete a tax rule that is in use for a product, has been in use for any existing orders, or is the default tax rule of the event.{% 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">
|
||||
<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 %}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Default" %}</th>
|
||||
<th>{% trans "Rate" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
@@ -36,6 +37,22 @@
|
||||
{{ tr.internal_name|default:tr.name }}
|
||||
</a></strong>
|
||||
</td>
|
||||
<td>
|
||||
{% if tr.default %}
|
||||
<span class="text-success">
|
||||
<span class="fa fa-check"></span>
|
||||
{% trans "Default" %}
|
||||
</span>
|
||||
{% else %}
|
||||
<form class="form-inline" method="post"
|
||||
action="{% url "control:event.settings.tax.default" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default btn-sm">
|
||||
{% trans "Make default" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if tr.price_includes_tax %}
|
||||
{% blocktrans with rate=tr.rate%}incl. {{ rate }} %{% endblocktrans %}
|
||||
|
||||
@@ -288,6 +288,7 @@ urlpatterns = [
|
||||
re_path(r'^settings/tax/(?P<rule>\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<rule>\d+)/delete$', event.TaxDelete.as_view(), name='event.settings.tax.delete'),
|
||||
re_path(r'^settings/tax/(?P<rule>\d+)/default$', event.TaxDefault.as_view(), name='event.settings.tax.default'),
|
||||
re_path(r'^settings/widget$', event.WidgetSettings.as_view(), name='event.settings.widget'),
|
||||
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
|
||||
re_path(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'),
|
||||
|
||||
@@ -68,7 +68,7 @@ from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _, gettext_noop
|
||||
from django.views.generic import FormView, ListView
|
||||
from django.views.generic import DetailView, FormView, ListView
|
||||
from django.views.generic.base import TemplateView, View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from i18nfield.strings import LazyI18nString
|
||||
@@ -1274,6 +1274,8 @@ class TaxCreate(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
if not self.request.event.tax_rules.exists():
|
||||
form.instance.default = True
|
||||
form.instance.event = self.request.event
|
||||
form.instance.custom_rules = json.dumps([
|
||||
f.cleaned_data for f in self.formset.ordered_forms if f not in self.formset.deleted_forms
|
||||
@@ -1354,6 +1356,50 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class TaxDefault(EventSettingsViewMixin, EventPermissionRequiredMixin, DetailView):
|
||||
model = TaxRule
|
||||
permission = 'can_change_event_settings'
|
||||
|
||||
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."))
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
obj = self.get_object()
|
||||
if not obj.default:
|
||||
for tr in self.request.event.tax_rules.filter(default=True):
|
||||
tr.log_action(
|
||||
'pretix.event.taxrule.changed', user=self.request.user, data={
|
||||
'default': False,
|
||||
}
|
||||
)
|
||||
tr.default = False
|
||||
tr.save(update_fields=['default'])
|
||||
obj.log_action(
|
||||
'pretix.event.taxrule.changed', user=self.request.user, data={
|
||||
'default': True,
|
||||
}
|
||||
)
|
||||
obj.default = True
|
||||
obj.save(update_fields=['default'])
|
||||
return redirect(self.get_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,
|
||||
})
|
||||
|
||||
|
||||
class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, CompatDeleteView):
|
||||
model = TaxRule
|
||||
template_name = 'pretixcontrol/event/tax_delete.html'
|
||||
|
||||
@@ -181,8 +181,9 @@ class EventWizard(SafeSessionWizardView):
|
||||
initial['location'] = self.clone_from.location
|
||||
initial['timezone'] = self.clone_from.settings.timezone
|
||||
initial['locale'] = self.clone_from.settings.locale
|
||||
if self.clone_from.settings.tax_rate_default:
|
||||
initial['tax_rate'] = self.clone_from.settings.tax_rate_default.rate
|
||||
tax_rule = self.clone_from.cached_default_tax_rule
|
||||
if tax_rule:
|
||||
initial['tax_rate'] = tax_rule.rate
|
||||
if 'organizer' in self.request.GET:
|
||||
if step == 'foundation':
|
||||
try:
|
||||
@@ -325,10 +326,17 @@ class EventWizard(SafeSessionWizardView):
|
||||
event.set_defaults()
|
||||
|
||||
if basics_data['tax_rate'] is not None:
|
||||
if not event.settings.tax_rate_default or event.settings.tax_rate_default.rate != basics_data['tax_rate']:
|
||||
event.settings.tax_rate_default = event.tax_rules.create(
|
||||
if self.clone_from:
|
||||
default_tax_rule = self.clone_from.cached_default_tax_rule
|
||||
elif copy_data and copy_data['copy_from_event']:
|
||||
default_tax_rule = from_event.cached_default_tax_rule
|
||||
else:
|
||||
default_tax_rule = None
|
||||
if not default_tax_rule or default_tax_rule.rate != basics_data['tax_rate']:
|
||||
event.tax_rules.create(
|
||||
name=LazyI18nString.from_gettext(gettext('VAT')),
|
||||
rate=basics_data['tax_rate']
|
||||
rate=basics_data['tax_rate'],
|
||||
default=not default_tax_rule,
|
||||
)
|
||||
|
||||
event.settings.set('timezone', basics_data['timezone'])
|
||||
|
||||
Reference in New Issue
Block a user