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

@@ -2215,7 +2215,7 @@ class EventTest(TestCase):
is_public=True,
)
event1.meta_values.create(property=prop, value="DE")
tr7 = event1.tax_rules.create(rate=Decimal('7.00'))
tr7 = event1.tax_rules.create(rate=Decimal('7.00'), default=True)
c1 = event1.categories.create(name='Tickets')
c2 = event1.categories.create(name='Workshops')
i1 = event1.items.create(name='Foo', default_price=Decimal('13.00'), tax_rule=tr7,
@@ -2228,7 +2228,6 @@ class EventTest(TestCase):
que1 = event1.questions.create(question="Age", type="N")
que1.items.add(i1)
event1.settings.foo_setting = 23
event1.settings.tax_rate_default = tr7
cl1 = event1.checkin_lists.create(
name="All", all_products=False,
rules={
@@ -2271,7 +2270,7 @@ class EventTest(TestCase):
assert que1new.type == que1.type
assert que1new.items.get(pk=i1new.pk)
assert event2.settings.foo_setting == '23'
assert event2.settings.tax_rate_default == trnew
assert event2.cached_default_tax_rule == trnew
assert event2.checkin_lists.count() == 1
clnew = event2.checkin_lists.first()
assert [i.pk for i in clnew.limit_products.all()] == [i1new.pk]

View File

@@ -1117,7 +1117,7 @@ class OrderCancelTests(TestCase):
self.order.save()
self.order.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=48.5)
with pytest.raises(OrderError):
cancel_order(self.order.pk, cancellation_fee=50)
cancel_order(self.order.pk, cancellation_fee=Decimal("50.00"))
self.order.refresh_from_db()
assert self.order.status == Order.STATUS_PAID
assert self.order.total == 46
@@ -1131,7 +1131,7 @@ class OrderCancelTests(TestCase):
self.order.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=48.5)
self.op1.voucher = self.event.vouchers.create(item=self.ticket, redeemed=1)
self.op1.save()
cancel_order(self.order.pk, cancellation_fee=2.5)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.50"))
self.order.refresh_from_db()
assert self.order.status == Order.STATUS_PAID
self.op1.refresh_from_db()
@@ -1158,7 +1158,7 @@ class OrderCancelTests(TestCase):
self.order.payments.create(state=OrderPayment.PAYMENT_STATE_CONFIRMED, amount=48.5)
self.op1.voucher = self.event.vouchers.create(item=self.ticket, redeemed=1)
self.op1.save()
cancel_order(self.order.pk, cancellation_fee=2.5)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.50"))
self.order.refresh_from_db()
assert self.order.status == Order.STATUS_PAID
self.op1.refresh_from_db()
@@ -1172,7 +1172,7 @@ class OrderCancelTests(TestCase):
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='testdummy_partialrefund'
)
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('44.00')
@@ -1190,7 +1190,7 @@ class OrderCancelTests(TestCase):
provider='giftcard',
info='{"gift_card": %d}' % gc.pk
)
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('44.00')
@@ -1209,7 +1209,7 @@ class OrderCancelTests(TestCase):
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='testdummy_partialrefund'
)
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert gc.value == Decimal('0.00')
@@ -1224,7 +1224,7 @@ class OrderCancelTests(TestCase):
provider='testdummy_partialrefund'
)
with pytest.raises(OrderError):
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
assert gc.value == Decimal('20.00')
@classscope(attr='o')
@@ -1234,7 +1234,7 @@ class OrderCancelTests(TestCase):
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='testdummy_fullrefund'
)
cancel_order(self.order.pk, cancellation_fee=2, try_auto_refund=True)
cancel_order(self.order.pk, cancellation_fee=Decimal("2.00"), try_auto_refund=True)
assert not self.order.refunds.exists()
assert self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists()
@@ -1258,7 +1258,7 @@ class OrderChangeManagerTests(TestCase):
provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CREATED, amount=self.order.total
)
self.tr7 = self.event.tax_rules.create(rate=Decimal('7.00'))
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'))
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'), default=True)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rule=self.tr7,
default_price=Decimal('23.00'), admission=True)
self.ticket2 = Item.objects.create(event=self.event, name='Other ticket', tax_rule=self.tr7,
@@ -1868,7 +1868,6 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_payment_fee_calculation(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_abs', Decimal('0.30'))
self.ocm.change_price(self.op1, Decimal('24.00'))
@@ -1882,7 +1881,6 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_pending_free_order_stays_pending(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
self.ocm.change_price(self.op1, Decimal('0.00'))
self.ocm.change_price(self.op2, Decimal('0.00'))
self.ocm.commit()
@@ -2270,7 +2268,6 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_recalculate_country_rate(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_abs', Decimal('0.30'))
self.ocm._recalculate_total_and_payment_fee()
@@ -2303,7 +2300,6 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_recalculate_country_rate_keep_gross(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_abs', Decimal('0.30'))
self.ocm._recalculate_total_and_payment_fee()
@@ -2334,7 +2330,6 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_recalculate_reverse_charge(self):
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_abs', Decimal('0.30'))
self.ocm._recalculate_total_and_payment_fee()
@@ -2493,7 +2488,6 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_split_pending_payment_fees(self):
# Set payment fees
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_percent', Decimal('2.00'))
prov.settings.set('_fee_abs', Decimal('1.00'))
@@ -2697,7 +2691,6 @@ class OrderChangeManagerTests(TestCase):
ia = self._enable_reverse_charge()
# Set payment fees
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_percent', Decimal('2.00'))
prov.settings.set('_fee_reverse_calc', False)
@@ -2791,7 +2784,6 @@ class OrderChangeManagerTests(TestCase):
@classscope(attr='o')
def test_split_paid_payment_fees(self):
# Set payment fees
self.event.settings.set('tax_rate_default', self.tr19.pk)
prov = self.ocm._get_payment_provider()
prov.settings.set('_fee_percent', Decimal('2.00'))
prov.settings.set('_fee_abs', Decimal('1.00'))

View File

@@ -27,8 +27,11 @@ from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scope
from pretix.base.models import Event, InvoiceAddress, Organizer, TaxRule
from pretix.base.models import (
Event, InvoiceAddress, OrderFee, OrderPosition, Organizer, TaxRule,
)
from pretix.base.models.tax import TaxedPrice
from pretix.base.services.tax import split_fee_for_taxes
@pytest.fixture
@@ -962,3 +965,39 @@ def test_allow_negative(event):
price_includes_tax=True,
)
assert tr.tax(Decimal('-100.00')).gross == Decimal("-100.00")
@pytest.mark.django_db
def test_split_fees(event):
tr19 = TaxRule(rate=Decimal("19.00"), pk=1)
tr7 = TaxRule(rate=Decimal("7.00"), pk=2)
item = event.items.create(name="Budget Ticket", default_price=23)
op1 = OrderPosition(price=Decimal("11.90"), item=item)
op1._calculate_tax(tax_rule=tr19, invoice_address=InvoiceAddress())
op2 = OrderPosition(price=Decimal("10.70"), item=item)
op2._calculate_tax(tax_rule=tr7, invoice_address=InvoiceAddress())
of1 = OrderFee(value=Decimal("5.00"), fee_type=OrderFee.FEE_TYPE_SHIPPING)
of1._calculate_tax(tax_rule=tr7, invoice_address=InvoiceAddress())
# Example of a 10% service fee
assert split_fee_for_taxes([op1, op2], Decimal("2.26"), event) == [
(tr7, Decimal("1.07")),
(tr19, Decimal("1.19")),
]
# Example of a full cancellation fee
assert split_fee_for_taxes([op1, op2], Decimal("22.60"), event) == [
(tr7, Decimal("10.70")),
(tr19, Decimal("11.90")),
]
assert split_fee_for_taxes([op1, op2, of1], Decimal("27.60"), event) == [
(tr7, Decimal("15.70")),
(tr19, Decimal("11.90")),
]
# Example that rounding always is done with benefit to the highest tax rate
assert split_fee_for_taxes([op1, op2], Decimal("0.03"), event) == [
(tr7, Decimal("0.01")),
(tr19, Decimal("0.02")),
]