diff --git a/doc/development/algorithms/pricing.rst b/doc/development/algorithms/pricing.rst index f64790e12..9e565dfd0 100644 --- a/doc/development/algorithms/pricing.rst +++ b/doc/development/algorithms/pricing.rst @@ -286,6 +286,11 @@ gross prices stay the same. **This is less confusing to consumers and the end result is still compliant to EN 16931, so we recommend this for e-invoicing when (primarily) consumers are involved.** +Some gross prices **cannot** stay the same. For example, it is impossible to represent €15.00 incl. 19%, since a net +price of €12.60 would lead to €14.99 gross and a net price of €12.61 would lead to €15.01 gross. Since this algorithm +is supposed to be consumer-friendly, it has a bias for choosing €14.99 in this case, even if €15.01 would be a little +less of a difference. + The main downside is that it might be confusing when selling to business customers, since the prices of the identical tickets appear to be different. Full computation for the example above: diff --git a/src/pretix/base/services/pricing.py b/src/pretix/base/services/pricing.py index 3921a1b13..c7ddfcacd 100644 --- a/src/pretix/base/services/pricing.py +++ b/src/pretix/base/services/pricing.py @@ -284,12 +284,27 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_keep # e.g. 99.99 at 19% is impossible since 84.03 + 19% = 100.00 and 84.02 + 19% = 99.98 target_gross_total = round_decimal((target_net_total * (1 + tax_rate / 100)), currency) + if target_net_total and target_gross_total > gross_total: + target_net_total -= min((target_gross_total - gross_total), len(lines) * minimum_unit) + try_target_gross_total = round_decimal((target_net_total * (1 + tax_rate / 100)), currency) + if try_target_gross_total <= gross_total: + target_gross_total = try_target_gross_total + diff_gross = target_gross_total - gross_total diff_net = target_net_total - net_total diff_gross_sgn = -1 if diff_gross < 0 else 1 diff_net_sgn = -1 if diff_net < 0 else 1 for l in lines: - if diff_gross: + if diff_gross and diff_net: + apply_diff = diff_gross_sgn * minimum_unit + l.price = l.gross_price_before_rounding + apply_diff + l.price_includes_rounding_correction = apply_diff + l.tax_value = l.tax_value_before_rounding + l.tax_value_includes_rounding_correction = Decimal("0.00") + changed.append(l) + diff_gross -= apply_diff + diff_net -= apply_diff + elif diff_gross: apply_diff = diff_gross_sgn * minimum_unit l.price = l.gross_price_before_rounding + apply_diff l.price_includes_rounding_correction = apply_diff @@ -312,6 +327,11 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_keep l.tax_value_includes_rounding_correction = Decimal("0.00") changed.append(l) + # Double-check that result is consistent in computing gross from net + new_net_total = sum(l.price - l.tax_value for l in lines) + new_gross_total = sum(l.price for l in lines) + assert new_gross_total == round_decimal((new_net_total * (1 + tax_rate / 100)), currency) + elif rounding_mode == "line": for l in lines: if l.price_includes_rounding_correction or l.tax_value_includes_rounding_correction: diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index b04c378ce..48f9a0adf 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -871,7 +871,8 @@ class TaxSettingsForm(EventSettingsValidationMixin, SettingsForm): "Recommended for e-invoicing when you primarily sell to consumers. " "The gross or net price of some products may be changed automatically to ensure correct " "rounding of the order total. The system attempts to keep gross prices as configured whenever " - "possible. Gross prices may still change if they are impossible to derive from a rounded net price." + "possible. Gross prices may still change if they are impossible to derive from a rounded net price, " + "but the system will prefer rounding them down instead of up." ), } self.fields["tax_rounding"].choices = ( diff --git a/src/tests/base/test_pricing_rounding.py b/src/tests/base/test_pricing_rounding.py index ffe9eaea1..89469ece3 100644 --- a/src/tests/base/test_pricing_rounding.py +++ b/src/tests/base/test_pricing_rounding.py @@ -134,17 +134,12 @@ def test_revert_net_keep_gross_rounding_to_single_line(sample_lines): assert l.tax_rate == Decimal("19.00") -@pytest.mark.django_db -@pytest.mark.parametrize("rounding_mode", [ - "sum_by_net", - "sum_by_net_keep_gross", -]) -def test_rounding_of_impossible_gross_price(rounding_mode): +def test_rounding_of_impossible_gross_price(): l = OrderPosition( price=Decimal("23.00"), ) l._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress()) - apply_rounding(rounding_mode, "EUR", [l]) + apply_rounding("sum_by_net", "EUR", [l]) assert l.price == Decimal("23.01") assert l.price_includes_rounding_correction == Decimal("0.01") assert l.tax_value == Decimal("1.51") @@ -152,6 +147,19 @@ def test_rounding_of_impossible_gross_price(rounding_mode): assert l.tax_rate == Decimal("7.00") +def test_rounding_of_impossible_gross_price_keep_gross(): + l = OrderPosition( + price=Decimal("23.00"), + ) + l._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress()) + apply_rounding("sum_by_net_keep_gross", "EUR", [l]) + assert l.price == Decimal("22.99") + assert l.price_includes_rounding_correction == Decimal("-0.01") + assert l.tax_value == Decimal("1.50") + assert l.tax_value_includes_rounding_correction == Decimal("0.00") + assert l.tax_rate == Decimal("7.00") + + @pytest.mark.django_db def test_round_down(): lines = [OrderPosition( @@ -236,10 +244,8 @@ def test_do_not_touch_free(rounding_mode): ) 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.price == Decimal("23.01") or l2.price == Decimal("22.99") + assert l2.price_includes_rounding_correction != Decimal("0.00") or l2.tax_value_includes_rounding_correction != Decimal("0.00") assert l2.tax_rate == Decimal("7.00") assert l1.price == Decimal("0.00") assert l1.price_includes_rounding_correction == Decimal("0.00")