mirror of
https://github.com/pretix/pretix.git
synced 2026-11-13 01:47:07 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
043c4fa752 | ||
|
|
9bd9e470dd | ||
|
|
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,8 @@ 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", "sum_by_net_business_else_gross"],
|
||||
invoice_address: Optional[InvoiceAddress], currency: str,
|
||||
lines: List[Union[OrderPosition, CartPosition, OrderFee]]) -> list:
|
||||
"""
|
||||
@@ -227,7 +228,11 @@ 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"``,
|
||||
``"sum_by_gross"``, or ``"sum_by_net_business_else_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
|
||||
@@ -238,6 +243,11 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only
|
||||
rounding_mode = "sum_by_net"
|
||||
else:
|
||||
rounding_mode = "line"
|
||||
elif rounding_mode == "sum_by_net_business_else_gross":
|
||||
if invoice_address and invoice_address.is_business:
|
||||
rounding_mode = "sum_by_net"
|
||||
else:
|
||||
rounding_mode = "sum_by_gross"
|
||||
|
||||
def _key(line):
|
||||
return (line.tax_rate, line.tax_code or "")
|
||||
@@ -321,6 +331,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.gross_price_before_rounding)))
|
||||
net_total = sum(
|
||||
l.gross_price_before_rounding - l.tax_value_before_rounding
|
||||
for l in lines
|
||||
)
|
||||
gross_total = sum(l.gross_price_before_rounding 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.gross_price_before_rounding
|
||||
l.price_includes_rounding_correction = Decimal("0.00")
|
||||
l.tax_value = l.tax_value_before_rounding - 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.gross_price_before_rounding
|
||||
l.price_includes_rounding_correction = Decimal("0.00")
|
||||
l.tax_value = l.tax_value_before_rounding
|
||||
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:
|
||||
|
||||
@@ -81,9 +81,11 @@ from pretix.helpers.countries import CachedCountries, pycountry_add
|
||||
ROUNDING_MODES = (
|
||||
('line', _('Compute taxes for every line individually')),
|
||||
('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_gross', _('Compute taxes based on gross total')),
|
||||
('sum_by_net_business_else_gross', _('For business customers, compute taxes based on net total. For individuals, compute taxes based on gross total.')),
|
||||
('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_net_only_business',
|
||||
_('For business customers, compute taxes based on net total. For individuals, use line-based rounding')),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -974,26 +974,41 @@ class TaxSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
self.fields["display_net_prices"].widget = DisplayNetPricesBooleanSelect()
|
||||
help_text = {
|
||||
"line": _(
|
||||
"Recommended when e-invoicing is not required. Each product will be sold with the advertised "
|
||||
"net and gross price. However, in orders of more than one product, the total tax amount "
|
||||
"can differ from when it would be computed from the order total."
|
||||
"Each product will be sold with the advertised net and gross price. Net and gross prices stay stable on "
|
||||
"order splits. However, in orders of more than one product, the total tax amount can differ from when "
|
||||
"it would be computed from the order total."
|
||||
),
|
||||
"sum_by_net": _(
|
||||
"Recommended for e-invoicing when you primarily sell to business customers and "
|
||||
"show prices to customers excluding tax. "
|
||||
"show prices to customers excluding tax. Created e-invoices are fully compliant with EN 16931. "
|
||||
"The gross price of some products may be changed to ensure correct rounding, while the net "
|
||||
"prices will be kept as configured. This may cause the actual payment amount to differ."
|
||||
),
|
||||
"sum_by_net_only_business": _(
|
||||
"Same as above, but only applied to business customers. Line-based rounding will be used for consumers. "
|
||||
"Recommended when e-invoicing is only used for business customers and consumers do not receive "
|
||||
"invoices. This can cause the payment amount to change when the invoice address is changed."
|
||||
"sum_by_gross": _(
|
||||
"Recommended for e-invoicing when you primarily sell to consumers or show prices to customers including tax. "
|
||||
"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."
|
||||
),
|
||||
"sum_by_net_keep_gross": _(
|
||||
"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."
|
||||
"sum_by_net_business_else_gross": _(
|
||||
"Recommended if you sell to business customers who care about e-invoices compliant to EN 16931 and to "
|
||||
"consumers who care about advertised prices at the same time. However, this can cause the payment amount "
|
||||
"to change when the invoice address is changed."
|
||||
),
|
||||
"sum_by_net_keep_gross": format_html(
|
||||
"<span class='label label-default'>{}</span> {}",
|
||||
_('Not recommended'),
|
||||
_(
|
||||
"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_net_only_business": format_html(
|
||||
"<span class='label label-default'>{}</span> {}",
|
||||
_('Not recommended'),
|
||||
"This can cause the payment amount to change when the invoice address is changed."
|
||||
),
|
||||
}
|
||||
self.fields["tax_rounding"].choices = (
|
||||
|
||||
@@ -287,7 +287,7 @@ class CartMixin:
|
||||
'is_ordered': bool(order),
|
||||
'itemcount': sum(c.count for c in positions if not c.addon_to),
|
||||
'show_rounding_info': (
|
||||
self.request.event.settings.tax_rounding == "sum_by_net_only_business" and
|
||||
self.request.event.settings.tax_rounding in ("sum_by_net_only_business", "sum_by_net_business_else_gross") and
|
||||
not self.request.event.settings.display_net_prices and
|
||||
sum(c.price_includes_rounding_correction for c in positions) + sum(f.price_includes_rounding_correction for f in fees)
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
@@ -262,3 +312,9 @@ def test_only_business():
|
||||
|
||||
apply_rounding("sum_by_net_only_business", InvoiceAddress(is_business=True), "EUR", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("499.98")
|
||||
|
||||
apply_rounding("sum_by_net_business_else_gross", None, "EUR", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("500.00")
|
||||
|
||||
apply_rounding("sum_by_net_business_else_gross", InvoiceAddress(is_business=True), "EUR", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("499.98")
|
||||
Reference in new issue
Block a user