mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Fix infinite price adjustment loop when combining free prices, country-dependent tax rates, and vouchers
This commit is contained in:
@@ -174,7 +174,7 @@ class TaxRule(LoggedModel):
|
|||||||
return Decimal(self.rate)
|
return Decimal(self.rate)
|
||||||
|
|
||||||
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
|
def tax(self, base_price, base_price_is='auto', currency=None, override_tax_rate=None, invoice_address=None,
|
||||||
subtract_from_gross=Decimal('0.00')):
|
subtract_from_gross=Decimal('0.00'), gross_price_is_tax_rate: Decimal = None):
|
||||||
from .event import Event
|
from .event import Event
|
||||||
try:
|
try:
|
||||||
currency = currency or self.event.currency
|
currency = currency or self.event.currency
|
||||||
@@ -186,7 +186,9 @@ class TaxRule(LoggedModel):
|
|||||||
rate = override_tax_rate
|
rate = override_tax_rate
|
||||||
elif invoice_address:
|
elif invoice_address:
|
||||||
adjust_rate = self.tax_rate_for(invoice_address)
|
adjust_rate = self.tax_rate_for(invoice_address)
|
||||||
if adjust_rate != rate:
|
if adjust_rate == gross_price_is_tax_rate and base_price_is == 'gross':
|
||||||
|
rate = adjust_rate
|
||||||
|
elif adjust_rate != rate:
|
||||||
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
|
normal_price = self.tax(base_price, base_price_is, currency, subtract_from_gross=subtract_from_gross)
|
||||||
base_price = normal_price.net
|
base_price = normal_price.net
|
||||||
base_price_is = 'net'
|
base_price_is = 'net'
|
||||||
|
|||||||
@@ -618,8 +618,10 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
except ItemBundle.MultipleObjectsReturned:
|
except ItemBundle.MultipleObjectsReturned:
|
||||||
raise OrderError("Invalid product configuration (duplicate bundle)")
|
raise OrderError("Invalid product configuration (duplicate bundle)")
|
||||||
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
price = get_price(cp.item, cp.variation, cp.voucher, bprice, cp.subevent, custom_price_is_net=False,
|
||||||
|
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||||
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
|
pbv = get_price(cp.item, cp.variation, None, bprice, cp.subevent, custom_price_is_net=False,
|
||||||
|
custom_price_is_tax_rate=cp.override_tax_rate,
|
||||||
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
invoice_address=address, force_custom_price=True, max_discount=max_discount)
|
||||||
changed_prices[cp.pk] = bprice
|
changed_prices[cp.pk] = bprice
|
||||||
else:
|
else:
|
||||||
@@ -631,10 +633,10 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
|
|
||||||
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
price = get_price(cp.item, cp.variation, cp.voucher, cp.price, cp.subevent, custom_price_is_net=False,
|
||||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
||||||
max_discount=max_discount)
|
max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate)
|
||||||
pbv = get_price(cp.item, cp.variation, None, cp.price, cp.subevent, custom_price_is_net=False,
|
pbv = get_price(cp.item, cp.variation, None, cp.price, cp.subevent, custom_price_is_net=False,
|
||||||
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
addon_to=cp.addon_to, invoice_address=address, bundled_sum=bundled_sum,
|
||||||
max_discount=max_discount)
|
max_discount=max_discount, custom_price_is_tax_rate=cp.override_tax_rate)
|
||||||
|
|
||||||
if max_discount is not None:
|
if max_discount is not None:
|
||||||
v_budget[cp.voucher] = v_budget[cp.voucher] + current_discount - (pbv.gross - price.gross)
|
v_budget[cp.voucher] = v_budget[cp.voucher] + current_discount - (pbv.gross - price.gross)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
|||||||
def get_price(item: Item, variation: ItemVariation = None,
|
def get_price(item: Item, variation: ItemVariation = None,
|
||||||
voucher: Voucher = None, custom_price: Decimal = None,
|
voucher: Voucher = None, custom_price: Decimal = None,
|
||||||
subevent: SubEvent = None, custom_price_is_net: bool = False,
|
subevent: SubEvent = None, custom_price_is_net: bool = False,
|
||||||
|
custom_price_is_tax_rate: Decimal=None,
|
||||||
addon_to: AbstractPosition = None, invoice_address: InvoiceAddress = None,
|
addon_to: AbstractPosition = None, invoice_address: InvoiceAddress = None,
|
||||||
force_custom_price: bool = False, bundled_sum: Decimal = Decimal('0.00'),
|
force_custom_price: bool = False, bundled_sum: Decimal = Decimal('0.00'),
|
||||||
max_discount: Decimal = None, tax_rule=None) -> TaxedPrice:
|
max_discount: Decimal = None, tax_rule=None) -> TaxedPrice:
|
||||||
@@ -66,7 +67,7 @@ def get_price(item: Item, variation: ItemVariation = None,
|
|||||||
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net',
|
price = tax_rule.tax(max(custom_price, price.net), base_price_is='net',
|
||||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||||
else:
|
else:
|
||||||
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross',
|
price = tax_rule.tax(max(custom_price, price.gross), base_price_is='gross', gross_price_is_tax_rate=custom_price_is_tax_rate,
|
||||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||||
else:
|
else:
|
||||||
price = tax_rule.tax(price, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
price = tax_rule.tax(price, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||||
|
|||||||
@@ -402,6 +402,44 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
|
|||||||
cr1.refresh_from_db()
|
cr1.refresh_from_db()
|
||||||
assert cr1.price == Decimal('23.20')
|
assert cr1.price == Decimal('23.20')
|
||||||
|
|
||||||
|
def test_country_taxing_free_price_and_voucher(self):
|
||||||
|
self._enable_country_specific_taxing()
|
||||||
|
|
||||||
|
self.ticket.free_price = True
|
||||||
|
self.ticket.save()
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
cr1 = CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=23, expires=now() + timedelta(minutes=10),
|
||||||
|
voucher=self.event.vouchers.create()
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch('vat_moss.id.validate') as mock_validate:
|
||||||
|
mock_validate.return_value = ('AT', 'AT123456', 'Foo')
|
||||||
|
self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
|
||||||
|
'is_business': 'individual',
|
||||||
|
'name': 'Bar',
|
||||||
|
'street': 'Baz',
|
||||||
|
'zipcode': '12345',
|
||||||
|
'city': 'Here',
|
||||||
|
'country': 'AT',
|
||||||
|
'email': 'admin@localhost'
|
||||||
|
}, follow=True)
|
||||||
|
|
||||||
|
cr1.refresh_from_db()
|
||||||
|
assert cr1.price == Decimal('23.20')
|
||||||
|
|
||||||
|
self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
|
||||||
|
'payment': 'banktransfer'
|
||||||
|
}, follow=True)
|
||||||
|
|
||||||
|
self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||||
|
with scopes_disabled():
|
||||||
|
assert not CartPosition.objects.filter(pk=cr1.pk).exists()
|
||||||
|
o = Order.objects.last()
|
||||||
|
assert o.positions.get().price == Decimal('23.20')
|
||||||
|
|
||||||
def test_country_taxing_switch(self):
|
def test_country_taxing_switch(self):
|
||||||
self.test_country_taxing()
|
self.test_country_taxing()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user