Fix infinite price adjustment loop when combining free prices, country-dependent tax rates, and vouchers

This commit is contained in:
Raphael Michel
2020-10-12 12:15:13 +02:00
parent 16cf3cec76
commit afc1013d69
4 changed files with 48 additions and 5 deletions

View File

@@ -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'

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()