mirror of
https://github.com/pretix/pretix.git
synced 2026-10-28 23:39:52 +00:00
Compare commits
1
Commits
master
...
sum-by-gross
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ef26595f5 |
No files matched your search
@@ -284,7 +284,10 @@ In this algorithm, the tax value and gross total are computed from the sum of th
|
||||
of some of the tickets will be changed automatically by the minimum currency unit (e.g. €0.01) such that the resulting
|
||||
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 a bit 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.**
|
||||
|
||||
However, confusion for consumers can still arise when a price cannot be represented by a net price.
|
||||
For example, you cannot sell a single ticket for 15.00 with a 19 % tax rate, because 12.60 * 1.19 = 14.99 and 12.61 * 1.19 = 15.01, so a total gross price of 15.00 is impossible to reach and the system will round to a different gross price.
|
||||
|
||||
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:
|
||||
@@ -299,3 +302,26 @@ Full computation for the example above:
|
||||
Ticket E 19 % 84.03 15.97 100.00
|
||||
Sum 420.17 79.83 500.00
|
||||
========== ========== ============================= ============================== =============
|
||||
|
||||
Compute taxes based on gross total
|
||||
""""""""""""""""""""""""""""""""""
|
||||
|
||||
Algorithm identifier: ``sum_by_gross``
|
||||
|
||||
In this algorithm, the net prices of some the individual lines will be modified such that the end values will be consistent with a computation from the gross sum.
|
||||
This will lead to different net prices to be shown for tickets of the same type, but the gross price will stay always the same.
|
||||
|
||||
**This is the most sensible algorithm when dealing mostly with consumer or when advertising gross prices, and you still need to issue e-invoices. Technically, it is incompliant with EN 16931, but at least for Germany, the Federal Ministry of Finance (BMF) allows to use it until end of 2027 in lieu of a better solution.**
|
||||
|
||||
For the example above:
|
||||
|
||||
========== ========== ============================= ============================== =============
|
||||
Product Tax rate Net price Tax Gross price
|
||||
========== ========== ============================= ============================== =============
|
||||
Ticket A 19 % 84.04 (incl. 0.01 rounding) 15.96 (incl. -0.01 rounding) 100.00
|
||||
Ticket B 19 % 84.04 (incl. 0.01 rounding) 15.96 (incl. -0.01 rounding) 100.00
|
||||
Ticket C 19 % 84.03 15.97 100.00
|
||||
Ticket D 19 % 84.03 15.97 100.00
|
||||
Ticket E 19 % 84.03 15.97 100.00
|
||||
Sum 420.17 79.83 500.00
|
||||
========== ========== ============================= ============================== =============
|
||||
@@ -211,7 +211,7 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
|
||||
return [new_prices.get(idx, (p[3], None)) for idx, p in enumerate(positions)]
|
||||
|
||||
|
||||
def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only_business", "sum_by_net_keep_gross"],
|
||||
def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only_business", "sum_by_net_keep_gross", "sum_by_gross"],
|
||||
invoice_address: Optional[InvoiceAddress], currency: str,
|
||||
lines: List[Union[OrderPosition, CartPosition, OrderFee]]) -> list:
|
||||
"""
|
||||
@@ -227,7 +227,10 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only
|
||||
When rounding mode is set to ``"sum_by_net"``, the gross prices and tax values of the individual lines will be
|
||||
adjusted such that the per-taxrate/taxcode subtotal is rounded correctly. The net prices will stay constant.
|
||||
|
||||
:param rounding_mode: One of ``"line"``, ``"sum_by_net"``, ``"sum_by_net_only_business"``, or ``"sum_by_net_keep_gross"``.
|
||||
When rounding mode is set to ``"sum_by_gross"``, the net prices and tax values of the individual lines will be
|
||||
adjusted such that the per-taxrate/taxcode subtotal is rounded correctly. The gross prices will stay constant.
|
||||
|
||||
:param rounding_mode: One of ``"line"``, ``"sum_by_net"``, ``"sum_by_net_only_business"``, ``"sum_by_net_keep_gross"``, or ``"sum_by_gross"``.
|
||||
:param invoice_address: The invoice address, or ``None``
|
||||
:param currency: Currency that will be used to determine rounding precision
|
||||
:param lines: List of order/cart contents
|
||||
@@ -321,6 +324,34 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only
|
||||
l.tax_value_includes_rounding_correction = Decimal("0.00")
|
||||
changed.append(l)
|
||||
|
||||
elif rounding_mode == "sum_by_gross":
|
||||
for (tax_rate, tax_code), lines in groupby(sorted(lines, key=_key), key=_key):
|
||||
lines = list(sorted(lines, key=lambda l: -(l.price - l.price_includes_rounding_correction)))
|
||||
net_total = sum(
|
||||
l.price - l.price_includes_rounding_correction - l.tax_value + l.tax_value_includes_rounding_correction
|
||||
for l in lines
|
||||
)
|
||||
gross_total = sum(l.price - l.price_includes_rounding_correction for l in lines)
|
||||
target_net_total = round_decimal(gross_total - (gross_total * (1 - 100 / (100 + tax_rate))), currency)
|
||||
|
||||
diff = target_net_total - net_total
|
||||
diff_sgn = -1 if diff < 0 else 1
|
||||
for l in lines:
|
||||
if diff:
|
||||
apply_diff = diff_sgn * minimum_unit
|
||||
l.price = l.price - l.price_includes_rounding_correction
|
||||
l.price_includes_rounding_correction = Decimal("0.00")
|
||||
l.tax_value = l.tax_value - l.tax_value_includes_rounding_correction - apply_diff
|
||||
l.tax_value_includes_rounding_correction = -apply_diff
|
||||
diff -= apply_diff
|
||||
changed.append(l)
|
||||
elif l.price_includes_rounding_correction or l.tax_value_includes_rounding_correction:
|
||||
l.price = l.price - l.price_includes_rounding_correction
|
||||
l.price_includes_rounding_correction = Decimal("0.00")
|
||||
l.tax_value = l.tax_value - l.tax_value_includes_rounding_correction
|
||||
l.tax_value_includes_rounding_correction = Decimal("0.00")
|
||||
changed.append(l)
|
||||
|
||||
elif rounding_mode == "line":
|
||||
for l in lines:
|
||||
if l.price_includes_rounding_correction or l.tax_value_includes_rounding_correction:
|
||||
|
||||
@@ -83,7 +83,7 @@ ROUNDING_MODES = (
|
||||
('sum_by_net', _('Compute taxes based on net total')),
|
||||
('sum_by_net_only_business', _('For business customers, compute taxes based on net total. For individuals, use line-based rounding')),
|
||||
('sum_by_net_keep_gross', _('Compute taxes based on net total with stable gross prices')),
|
||||
# We could also have sum_by_gross, but we're not aware of any use-cases for it
|
||||
('sum_by_gross', _('Compute taxes based on gross total')),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -990,11 +990,18 @@ class TaxSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
"invoices. This can cause the payment amount to change when the invoice address is changed."
|
||||
),
|
||||
"sum_by_net_keep_gross": _(
|
||||
"Recommended for e-invoicing when you primarily sell to consumers. "
|
||||
"Recommended for e-invoicing when you primarily sell to consumers and need invoices compliant with EN 16931. "
|
||||
"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."
|
||||
),
|
||||
"sum_by_gross": _(
|
||||
"Recommended for e-invoicing when you primarily sell to consumers or advertise gross prices. "
|
||||
"The net price of some products may be changed to ensure correct rounding, while the gross "
|
||||
"prices will be kept as configured. This actual payment amount always stays as expected. This is "
|
||||
"technically not compliant with European e-invoicing standards, but e.g. allowed within Germany at "
|
||||
"least until end of 2027."
|
||||
),
|
||||
}
|
||||
self.fields["tax_rounding"].choices = (
|
||||
(k, format_html('{}<br><span class="text-muted">{}</span>', v, help_text.get(k, "")))
|
||||
|
||||
@@ -73,6 +73,14 @@ def _validate_sample_lines(sample_lines, rounding_mode):
|
||||
assert sum(line.price for line in sample_lines) == Decimal("500.00")
|
||||
assert sum(line.tax_value for line in sample_lines) == Decimal("79.83")
|
||||
assert sum(line.price - line.tax_value for line in sample_lines) == Decimal("420.17")
|
||||
elif rounding_mode == "sum_by_gross":
|
||||
for line in sample_lines:
|
||||
assert line.price == Decimal("100.00")
|
||||
# net price may vary
|
||||
assert line.tax_rate == Decimal("19.00")
|
||||
assert sum(line.price for line in sample_lines) == Decimal("500.00")
|
||||
assert sum(line.tax_value for line in sample_lines) == Decimal("79.83")
|
||||
assert sum(line.price - line.tax_value for line in sample_lines) == Decimal("420.17")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -86,16 +94,22 @@ def test_simple_case_by_net(sample_lines):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_simple_case_by_gross(sample_lines):
|
||||
def test_simple_case_net_by_gross(sample_lines):
|
||||
_validate_sample_lines(sample_lines, "sum_by_net_keep_gross")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_simple_case_by_gross(sample_lines):
|
||||
_validate_sample_lines(sample_lines, "sum_by_gross")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_simple_case_switch_rounding(sample_lines):
|
||||
_validate_sample_lines(sample_lines, "sum_by_net")
|
||||
_validate_sample_lines(sample_lines, "sum_by_net_keep_gross")
|
||||
_validate_sample_lines(sample_lines, "line")
|
||||
_validate_sample_lines(sample_lines, "sum_by_net")
|
||||
_validate_sample_lines(sample_lines, "sum_by_gross")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -134,6 +148,24 @@ def test_revert_net_keep_gross_rounding_to_single_line(sample_lines):
|
||||
assert l.tax_rate == Decimal("19.00")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_revert_gross_rounding_to_single_line(sample_lines):
|
||||
l = OrderPosition(
|
||||
price=Decimal("100.00"),
|
||||
price_includes_rounding_correction=Decimal("0.00"),
|
||||
tax_value=Decimal("15.96"),
|
||||
tax_value_includes_rounding_correction=Decimal("-0.01"),
|
||||
tax_rate=Decimal("19.00"),
|
||||
tax_code="S",
|
||||
)
|
||||
apply_rounding("sum_by_gross", None, "EUR", [l])
|
||||
assert l.price == Decimal("100.00")
|
||||
assert l.price_includes_rounding_correction == Decimal("0.00")
|
||||
assert l.tax_value == Decimal("15.97")
|
||||
assert l.tax_value_includes_rounding_correction == Decimal("0.00")
|
||||
assert l.tax_rate == Decimal("19.00")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("rounding_mode", [
|
||||
"sum_by_net",
|
||||
@@ -152,6 +184,24 @@ def test_rounding_of_impossible_gross_price(rounding_mode):
|
||||
assert l.tax_rate == Decimal("7.00")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("rounding_mode", [
|
||||
"sum_by_gross",
|
||||
"line",
|
||||
])
|
||||
def test_rounding_of_impossible_gross_price_possible_when_not_rounding_by_net(rounding_mode):
|
||||
l = OrderPosition(
|
||||
price=Decimal("23.00"),
|
||||
)
|
||||
l._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress())
|
||||
apply_rounding(rounding_mode, None, "EUR", [l])
|
||||
assert l.price == Decimal("23.00")
|
||||
assert l.price_includes_rounding_correction == Decimal("0.00")
|
||||
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(
|
||||
|
||||
Reference in new issue
Block a user