diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 601e4154b6..d27f5c7d22 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -1736,7 +1736,7 @@ class OrderChangeManager: if position.issued_gift_cards.exists(): raise OrderError(self.error_messages['gift_card_change']) - self._totaldiff_guesstimate += price.gross - position.price + self._totaldiff_guesstimate += price.gross - position.price + position.price_includes_rounding_correction if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00') or position.price != Decimal('0.00'): self._invoice_dirty = True @@ -2362,10 +2362,15 @@ class OrderChangeManager: 'new_price': op.price.gross }) position.price = op.price.gross + position.price_includes_rounding_correction = Decimal("0.00") position.tax_rate = op.price.rate position.tax_value = op.price.tax + position.tax_value_includes_rounding_correction = Decimal("0.00") position.tax_code = op.price.code - position.save(update_fields=['price', 'tax_rate', 'tax_value', 'tax_code']) + position.save(update_fields=[ + 'price', 'price_includes_rounding_correction', 'tax_rate', 'tax_value', + 'tax_value_includes_rounding_correction', 'tax_code' + ]) elif isinstance(op, self.TaxRuleOperation): if isinstance(op.position, OrderPosition): position = position_cache.setdefault(op.position.pk, op.position) diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py index ec69a92cec..f962a883fd 100644 --- a/src/tests/base/test_orders.py +++ b/src/tests/base/test_orders.py @@ -1568,6 +1568,63 @@ class OrderChangeManagerTests(TestCase): assert round_decimal(self.op1.price * (1 - 100 / (100 + self.op1.tax_rate))) == self.op1.tax_value assert self.order.total == self.op1.price + self.op2.price + @classscope(attr='o') + def test_change_price_with_rounding_change_impossible(self): + # Order starts with 2*100€ tickets, but rounding corrects it to 199€. Then, the user tries to force both prices + # to 100€. No luck. + self.order.status = Order.STATUS_PAID + self.order.tax_rounding_mode = "sum_by_net" + self.order.save() + self.op1.price = Decimal("100.00") + self.op1._calculate_tax(tax_rule=self.tr19) + self.op1.save() + self.op2.price = Decimal("100.00") + self.op2._calculate_tax(tax_rule=self.tr19) + self.op2.save() + self.order.refresh_from_db() + self.ocm.regenerate_secret(self.op1) + self.ocm.commit() # Force re-rounding + self.order.refresh_from_db() + self.ocm = OrderChangeManager(self.order, None) + assert self.order.total == Decimal("199.99") + + self.ocm.change_price(self.op1, Decimal('100.00')) + self.ocm.change_price(self.op2, Decimal('100.00')) + self.ocm.commit() + self.op1.refresh_from_db() + self.op2.refresh_from_db() + self.order.refresh_from_db() + assert self.order.total == Decimal("199.99") + assert self.op1.price == Decimal('99.99') + assert self.op2.price == Decimal('100.00') + + @classscope(attr='o') + def test_change_price_with_rounding_change_autocorrected(self): + self.order.status = Order.STATUS_PAID + self.order.tax_rounding_mode = "sum_by_net" + self.order.save() + self.op1.price = Decimal("0.00") + self.op1._calculate_tax(tax_rule=self.tr19) + self.op1.save() + self.op2.price = Decimal("100.00") + self.op2._calculate_tax(tax_rule=self.tr19) + self.op2.save() + self.order.refresh_from_db() + self.ocm.regenerate_secret(self.op1) + self.ocm.commit() # Force re-rounding + self.order.refresh_from_db() + self.ocm = OrderChangeManager(self.order, None) + assert self.order.total == Decimal("100.00") + + self.ocm.change_price(self.op1, Decimal('100.00')) + self.ocm.commit() + self.op1.refresh_from_db() + self.op2.refresh_from_db() + self.order.refresh_from_db() + assert self.order.total == Decimal("199.99") + assert self.op1.price == Decimal('99.99') + assert self.op2.price == Decimal('100.00') + @classscope(attr='o') def test_change_price_net_success(self): self.tr7.price_includes_tax = False diff --git a/src/tests/base/test_pricing_rounding.py b/src/tests/base/test_pricing_rounding.py index e0764a2d40..7f46140825 100644 --- a/src/tests/base/test_pricing_rounding.py +++ b/src/tests/base/test_pricing_rounding.py @@ -219,3 +219,29 @@ def test_round_currency_without_decimals(): assert sum(l.price for l in lines) == Decimal("49990.00") assert sum(l.tax_value for l in lines) == Decimal("7982.00") assert sum(l.price - l.tax_value for l in lines) == Decimal("42008.00") + + +@pytest.mark.django_db +@pytest.mark.parametrize("rounding_mode", [ + "sum_by_net", + "sum_by_net_keep_gross", +]) +def test_do_not_touch_free(rounding_mode): + l1 = OrderPosition( + price=Decimal("0.00"), + ) + l1._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress()) + l2 = OrderPosition( + price=Decimal("23.00"), + ) + l2._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress()) + apply_rounding(rounding_mode, "EUR", [l1, l2]) + assert l2.price == Decimal("23.01") + assert l2.price_includes_rounding_correction == Decimal("0.01") + assert l2.tax_value == Decimal("1.51") + assert l2.tax_value_includes_rounding_correction == Decimal("0.01") + assert l2.tax_rate == Decimal("7.00") + assert l1.price == Decimal("0.00") + assert l1.price_includes_rounding_correction == Decimal("0.00") + assert l1.tax_value == Decimal("0.00") + assert l1.tax_value_includes_rounding_correction == Decimal("0.00")