mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Price rounding: Make sum_by_net_keep_gross even more consumer-friendly (Z#23220106)
The rounding mode sum_by_net_keep_gross is supposed to be the consumer-friendly algorithm. However, some consumer-friendly prices are still impossible, such as 15.00 incl. 19%. Previously, the system would round to 15.01, because it's the gross price derived from the closest net price. With this PR, it rounds to 14.99 instead, because consumers like paying less more than they like paying more.
This commit is contained in:
@@ -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.**
|
**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.
|
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:
|
Full computation for the example above:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
# 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)
|
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_gross = target_gross_total - gross_total
|
||||||
diff_net = target_net_total - net_total
|
diff_net = target_net_total - net_total
|
||||||
diff_gross_sgn = -1 if diff_gross < 0 else 1
|
diff_gross_sgn = -1 if diff_gross < 0 else 1
|
||||||
diff_net_sgn = -1 if diff_net < 0 else 1
|
diff_net_sgn = -1 if diff_net < 0 else 1
|
||||||
for l in lines:
|
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
|
apply_diff = diff_gross_sgn * minimum_unit
|
||||||
l.price = l.gross_price_before_rounding + apply_diff
|
l.price = l.gross_price_before_rounding + apply_diff
|
||||||
l.price_includes_rounding_correction = 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")
|
l.tax_value_includes_rounding_correction = Decimal("0.00")
|
||||||
changed.append(l)
|
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":
|
elif rounding_mode == "line":
|
||||||
for l in lines:
|
for l in lines:
|
||||||
if l.price_includes_rounding_correction or l.tax_value_includes_rounding_correction:
|
if l.price_includes_rounding_correction or l.tax_value_includes_rounding_correction:
|
||||||
|
|||||||
@@ -871,7 +871,8 @@ class TaxSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
"Recommended for e-invoicing when you primarily sell to consumers. "
|
"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 "
|
"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 "
|
"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 = (
|
self.fields["tax_rounding"].choices = (
|
||||||
|
|||||||
@@ -134,17 +134,12 @@ def test_revert_net_keep_gross_rounding_to_single_line(sample_lines):
|
|||||||
assert l.tax_rate == Decimal("19.00")
|
assert l.tax_rate == Decimal("19.00")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
def test_rounding_of_impossible_gross_price():
|
||||||
@pytest.mark.parametrize("rounding_mode", [
|
|
||||||
"sum_by_net",
|
|
||||||
"sum_by_net_keep_gross",
|
|
||||||
])
|
|
||||||
def test_rounding_of_impossible_gross_price(rounding_mode):
|
|
||||||
l = OrderPosition(
|
l = OrderPosition(
|
||||||
price=Decimal("23.00"),
|
price=Decimal("23.00"),
|
||||||
)
|
)
|
||||||
l._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress())
|
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 == Decimal("23.01")
|
||||||
assert l.price_includes_rounding_correction == Decimal("0.01")
|
assert l.price_includes_rounding_correction == Decimal("0.01")
|
||||||
assert l.tax_value == Decimal("1.51")
|
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")
|
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
|
@pytest.mark.django_db
|
||||||
def test_round_down():
|
def test_round_down():
|
||||||
lines = [OrderPosition(
|
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())
|
l2._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress())
|
||||||
apply_rounding(rounding_mode, "EUR", [l1, l2])
|
apply_rounding(rounding_mode, "EUR", [l1, l2])
|
||||||
assert l2.price == Decimal("23.01")
|
assert l2.price == Decimal("23.01") or l2.price == Decimal("22.99")
|
||||||
assert l2.price_includes_rounding_correction == Decimal("0.01")
|
assert l2.price_includes_rounding_correction != Decimal("0.00") or l2.tax_value_includes_rounding_correction != Decimal("0.00")
|
||||||
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 l2.tax_rate == Decimal("7.00")
|
||||||
assert l1.price == Decimal("0.00")
|
assert l1.price == Decimal("0.00")
|
||||||
assert l1.price_includes_rounding_correction == Decimal("0.00")
|
assert l1.price_includes_rounding_correction == Decimal("0.00")
|
||||||
|
|||||||
Reference in New Issue
Block a user