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:
Raphael Michel
2025-06-30 16:47:09 +02:00
committed by GitHub
parent 090358833d
commit 14ed6982a5
34 changed files with 615 additions and 104 deletions

View File

@@ -443,19 +443,17 @@ class EventsTest(SoupTest):
assert self.event1.settings.get('payment_banktransfer__fee_abs', as_type=Decimal) == Decimal('12.23')
def test_payment_settings(self):
tr19 = self.event1.tax_rules.create(rate=Decimal('19.00'))
self.get_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug))
self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), {
'payment_term_days': '2',
'payment_term_minutes': '30',
'payment_term_mode': 'days',
'tax_rate_default': tr19.pk,
'tax_rule_payment': 'default',
})
self.event1.settings.flush()
assert self.event1.settings.get('payment_term_days', as_type=int) == 2
def test_payment_settings_last_date_payment_after_presale_end(self):
tr19 = self.event1.tax_rules.create(rate=Decimal('19.00'))
self.event1.presale_end = now()
self.event1.save(update_fields=['presale_end'])
doc = self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), {
@@ -464,15 +462,13 @@ class EventsTest(SoupTest):
'payment_term_last_1': (self.event1.presale_end - datetime.timedelta(1)).strftime('%Y-%m-%d'),
'payment_term_last_2': '0',
'payment_term_last_3': 'date_from',
'tax_rate_default': tr19.pk,
'tax_rule_payment': 'default',
})
assert doc.select('.alert-danger')
self.event1.presale_end = None
self.event1.save(update_fields=['presale_end'])
def test_payment_settings_relative_date_payment_after_presale_end(self):
with scopes_disabled():
tr19 = self.event1.tax_rules.create(rate=Decimal('19.00'))
self.event1.presale_end = self.event1.date_from - datetime.timedelta(days=5)
self.event1.save(update_fields=['presale_end'])
doc = self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), {
@@ -481,7 +477,7 @@ class EventsTest(SoupTest):
'payment_term_last_1': '',
'payment_term_last_2': '10',
'payment_term_last_3': 'date_from',
'tax_rate_default': tr19.pk,
'tax_rule_payment': 'default',
})
assert doc.select('.alert-danger')
self.event1.presale_end = None
@@ -912,7 +908,7 @@ class EventsTest(SoupTest):
def test_create_event_copy_success(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(
rate=19, name="VAT"
rate=19, name="VAT", default=True
)
q1 = self.event1.quotas.create(
name='Foo',
@@ -923,7 +919,6 @@ class EventsTest(SoupTest):
category=None, default_price=23, tax_rule=tr,
admission=True, hidden_if_available=q1
)
self.event1.settings.tax_rate_default = tr
doc = self.get_doc('/control/events/add')
doc = self.post_doc('/control/events/add', {
@@ -990,14 +985,13 @@ class EventsTest(SoupTest):
def test_create_event_clone_success(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(
rate=19, name="VAT"
rate=19, name="VAT", default=True
)
self.event1.items.create(
name='Early-bird ticket',
category=None, default_price=23, tax_rule=tr,
admission=True
)
self.event1.settings.tax_rate_default = tr
doc = self.get_doc('/control/events/add?clone=' + str(self.event1.pk))
tabletext = doc.select("form")[0].text
self.assertIn("CCC", tabletext)

View File

@@ -439,8 +439,37 @@ def test_order_cancel_paid_keep_fee(client, env):
o.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=o.total)
o.status = Order.STATUS_PAID
o.save()
tr7 = o.event.tax_rules.create(rate=Decimal('7.00'))
o.event.settings.tax_rate_default = tr7
o.event.tax_rules.create(rate=Decimal('7.00'), default=True)
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
'status': 'c',
'cancellation_fee': '6.00'
})
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
assert not o.positions.exists()
assert o.all_positions.exists()
f = o.fees.get()
assert f.fee_type == OrderFee.FEE_TYPE_CANCELLATION
assert f.value == Decimal('6.00')
assert f.tax_value == Decimal('0.00')
assert f.tax_rate == Decimal('0.00')
assert f.tax_rule is None
assert o.status == Order.STATUS_PAID
assert o.total == Decimal('6.00')
assert o.pending_sum == Decimal('-8.00')
@pytest.mark.django_db
def test_order_cancel_paid_keep_fee_taxed(client, env):
env[0].settings.tax_rule_cancellation = "default"
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
o.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=o.total)
o.status = Order.STATUS_PAID
o.save()
tr7 = o.event.tax_rules.create(rate=Decimal('7.00'), default=True)
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
@@ -462,6 +491,50 @@ def test_order_cancel_paid_keep_fee(client, env):
assert o.pending_sum == Decimal('-8.00')
@pytest.mark.django_db
def test_order_cancel_paid_keep_fee_tax_split(client, env):
env[0].settings.tax_rule_cancellation = "split"
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
o.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=o.total)
o.status = Order.STATUS_PAID
o.save()
tr7 = o.event.tax_rules.create(rate=Decimal('7.00'), default=False)
tr19 = o.event.tax_rules.create(rate=Decimal('19.00'), default=True)
op1 = o.positions.first()
op1._calculate_tax(tax_rule=tr7)
op1.save()
op2 = o.all_positions.last()
op2.canceled = False
op2._calculate_tax(tax_rule=tr19)
op2.save()
client.login(email='dummy@dummy.dummy', password='dummy')
client.get('/control/event/dummy/dummy/orders/FOO/transition?status=c')
client.post('/control/event/dummy/dummy/orders/FOO/transition', {
'status': 'c',
'cancellation_fee': '6.00'
})
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
assert not o.positions.exists()
assert o.all_positions.exists()
f = o.fees.order_by("-tax_rate")
assert len(f) == 2
assert f[0].fee_type == OrderFee.FEE_TYPE_CANCELLATION
assert f[0].value == Decimal('3.00')
assert f[0].tax_value == Decimal('0.48')
assert f[0].tax_rate == Decimal('19')
assert f[0].tax_rule == tr19
assert f[1].fee_type == OrderFee.FEE_TYPE_CANCELLATION
assert f[1].value == Decimal('3.00')
assert f[1].tax_value == Decimal('0.20')
assert f[1].tax_rate == Decimal('7')
assert f[1].tax_rule == tr7
assert o.status == Order.STATUS_PAID
assert o.total == Decimal('6.00')
assert o.pending_sum == Decimal('-8.00')
@pytest.mark.django_db
def test_order_cancel_pending_keep_fee(client, env):
with scopes_disabled():

View File

@@ -104,6 +104,7 @@ event_urls = [
"settings/tax/add",
"settings/tax/1/",
"settings/tax/1/delete",
"settings/tax/1/default",
"items/",
"items/add",
"items/1/",
@@ -318,6 +319,7 @@ event_permission_urls = [
("can_change_event_settings", "settings/tax/1/", 404, HTTP_GET),
("can_change_event_settings", "settings/tax/add", 200, HTTP_GET),
("can_change_event_settings", "settings/tax/1/delete", 404, HTTP_GET),
("can_change_event_settings", "settings/tax/1/default", 404, HTTP_POST),
("can_change_event_settings", "comment/", 405, HTTP_GET),
# Lists are currently not access-controlled
# ("can_change_items", "items/", 200),

View File

@@ -56,9 +56,22 @@ class TaxRateFormTest(SoupTest):
assert doc.select(".alert-success")
self.assertIn("VAT", doc.select("#page-wrapper table")[0].text)
with scopes_disabled():
assert self.event1.tax_rules.get(
tr = self.event1.tax_rules.get(
rate=19, price_includes_tax=True, eu_reverse_charge=False
)
assert tr.default
def test_set_default(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(rate=19, name="VAT")
tr2 = self.event1.tax_rules.create(rate=7, name="VAT", default=True)
doc = self.post_doc('/control/event/%s/%s/settings/tax/%s/default' % (self.orga1.slug, self.event1.slug, tr.id),
{})
assert doc.select(".alert-success")
tr.refresh_from_db()
assert tr.default
tr2.refresh_from_db()
assert not tr2.default
def test_update(self):
with scopes_disabled():
@@ -98,8 +111,8 @@ class TaxRateFormTest(SoupTest):
def test_delete_default_rule(self):
with scopes_disabled():
tr = self.event1.tax_rules.create(rate=19, name="VAT")
self.event1.settings.tax_rate_default = tr
tr = self.event1.tax_rules.create(rate=19, name="VAT", default=True)
self.event1.tax_rules.create(rate=7, name="V2")
doc = self.get_doc('/control/event/%s/%s/settings/tax/%s/delete' % (self.orga1.slug, self.event1.slug, tr.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
doc = self.post_doc('/control/event/%s/%s/settings/tax/%s/delete' % (self.orga1.slug, self.event1.slug, tr.id),