mirror of
https://github.com/pretix/pretix.git
synced 2026-01-14 23:02:26 +00:00
Compare commits
1 Commits
master
...
rounding-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15cdf606d0 |
@@ -1733,6 +1733,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
rounding_mode = self.context["event"].settings.tax_rounding
|
||||
changed = apply_rounding(
|
||||
rounding_mode,
|
||||
ia,
|
||||
self.context["event"].currency,
|
||||
[*pos_map.values(), *fees]
|
||||
)
|
||||
|
||||
@@ -1639,7 +1639,7 @@ def get_fees(event, request, _total_ignored_=None, invoice_address=None, payment
|
||||
if fee.tax_rule and not fee.tax_rule.pk:
|
||||
fee.tax_rule = None # TODO: deprecate
|
||||
|
||||
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees])
|
||||
apply_rounding(event.settings.tax_rounding, invoice_address, event.currency, [*positions, *fees])
|
||||
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
|
||||
|
||||
if total != 0 and payments:
|
||||
@@ -1679,7 +1679,7 @@ def get_fees(event, request, _total_ignored_=None, invoice_address=None, payment
|
||||
fees.append(pf)
|
||||
|
||||
# Re-apply rounding as grand total has changed
|
||||
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees])
|
||||
apply_rounding(event.settings.tax_rounding, invoice_address, event.currency, [*positions, *fees])
|
||||
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
|
||||
|
||||
# Re-calculate to_pay as grand total has changed
|
||||
|
||||
@@ -968,7 +968,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
|
||||
fee.tax_rule = None # TODO: deprecate
|
||||
|
||||
# Apply rounding to get final total in case no payment fees will be added
|
||||
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees])
|
||||
apply_rounding(event.settings.tax_rounding, address, event.currency, [*positions, *fees])
|
||||
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
|
||||
|
||||
payments_assigned = Decimal("0.00")
|
||||
@@ -995,7 +995,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
|
||||
p['fee'] = pf
|
||||
|
||||
# Re-apply rounding as grand total has changed
|
||||
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees])
|
||||
apply_rounding(event.settings.tax_rounding, address, event.currency, [*positions, *fees])
|
||||
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
|
||||
|
||||
# Re-calculate to_pay as grand total has changed
|
||||
@@ -1641,6 +1641,7 @@ class OrderChangeManager:
|
||||
ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until'))
|
||||
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
|
||||
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
|
||||
ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple())
|
||||
|
||||
class AddPositionResult:
|
||||
_position: Optional[OrderPosition]
|
||||
@@ -1804,6 +1805,7 @@ class OrderChangeManager:
|
||||
positions = self.order.positions.select_related('item', 'item__tax_rule')
|
||||
ia = self._invoice_address
|
||||
tax_rules = self._current_tax_rules()
|
||||
self._operations.append(self.ForceRecomputeOperation())
|
||||
|
||||
for pos in positions:
|
||||
tax_rule = tax_rules.get(pos.pk, pos.tax_rule)
|
||||
@@ -2640,6 +2642,10 @@ class OrderChangeManager:
|
||||
except BlockedTicketSecret.DoesNotExist:
|
||||
pass
|
||||
# todo: revoke list handling
|
||||
elif isinstance(op, self.ForceRecomputeOperation):
|
||||
self.order.log_action('pretix.event.order.changed.recomputed', user=self.user, auth=self.auth, data={})
|
||||
else:
|
||||
raise TypeError(f"Unknown operation {type(op)}")
|
||||
|
||||
for p in secret_dirty:
|
||||
assign_ticket_secret(
|
||||
@@ -2694,7 +2700,10 @@ class OrderChangeManager:
|
||||
fees.append(new_fee)
|
||||
|
||||
changed_by_rounding = set(apply_rounding(
|
||||
self.order.tax_rounding_mode, self.event.currency, [p for p in split_positions if not p.canceled] + fees
|
||||
self.order.tax_rounding_mode,
|
||||
self._invoice_address,
|
||||
self.event.currency,
|
||||
[p for p in split_positions if not p.canceled] + fees
|
||||
))
|
||||
split_order.total = sum([p.price for p in split_positions if not p.canceled])
|
||||
|
||||
@@ -2716,7 +2725,10 @@ class OrderChangeManager:
|
||||
fee.delete()
|
||||
|
||||
changed_by_rounding |= set(apply_rounding(
|
||||
self.order.tax_rounding_mode, self.event.currency, [p for p in split_positions if not p.canceled] + fees
|
||||
self.order.tax_rounding_mode,
|
||||
self._invoice_address,
|
||||
self.event.currency,
|
||||
[p for p in split_positions if not p.canceled] + fees
|
||||
))
|
||||
split_order.total = sum([p.price for p in split_positions if not p.canceled]) + sum([f.value for f in fees])
|
||||
|
||||
@@ -2833,7 +2845,12 @@ class OrderChangeManager:
|
||||
if fee_changed:
|
||||
fees = list(self.order.fees.all())
|
||||
|
||||
changed = apply_rounding(self.order.tax_rounding_mode, self.order.event.currency, [*positions, *fees])
|
||||
changed = apply_rounding(
|
||||
self.order.tax_rounding_mode,
|
||||
self._invoice_address,
|
||||
self.order.event.currency,
|
||||
[*positions, *fees]
|
||||
)
|
||||
for l in changed:
|
||||
if isinstance(l, OrderPosition):
|
||||
l.save(update_fields=[
|
||||
@@ -3269,8 +3286,12 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
|
||||
positions = list(order.positions.all())
|
||||
fees = list(order.fees.all())
|
||||
try:
|
||||
ia = order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
rounding_changed = set(apply_rounding(
|
||||
order.tax_rounding_mode, order.event.currency, [*positions, *[f for f in fees if f.pk != fee.pk]]
|
||||
order.tax_rounding_mode, ia, order.event.currency, [*positions, *[f for f in fees if f.pk != fee.pk]]
|
||||
))
|
||||
total_without_fee = sum(c.price for c in positions) + sum(f.value for f in fees if f.pk != fee.pk)
|
||||
pending_sum_without_fee = max(Decimal("0.00"), total_without_fee - already_paid)
|
||||
@@ -3295,7 +3316,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
fee = None
|
||||
|
||||
rounding_changed |= set(apply_rounding(
|
||||
order.tax_rounding_mode, order.event.currency, [*positions, *fees]
|
||||
order.tax_rounding_mode, ia, order.event.currency, [*positions, *fees]
|
||||
))
|
||||
for l in rounding_changed:
|
||||
if isinstance(l, OrderPosition):
|
||||
|
||||
@@ -209,7 +209,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_keep_gross"], currency: str,
|
||||
def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only_business", "sum_by_net_keep_gross"],
|
||||
invoice_address: Optional[InvoiceAddress], currency: str,
|
||||
lines: List[Union[OrderPosition, CartPosition, OrderFee]]) -> list:
|
||||
"""
|
||||
Given a list of order positions / cart positions / order fees (may be mixed), applies the given rounding mode
|
||||
@@ -224,11 +225,17 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_keep
|
||||
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"``, or ``"sum_by_net_keep_gross"``.
|
||||
:param rounding_mode: One of ``"line"``, ``"sum_by_net"``, ``"sum_by_net_only_business"``, or ``"sum_by_net_keep_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
|
||||
:return: Collection of ``lines`` members that have been changed and may need to be persisted to the database.
|
||||
"""
|
||||
if rounding_mode == "sum_by_net_only_business":
|
||||
if invoice_address and invoice_address.is_business:
|
||||
rounding_mode = "sum_by_net"
|
||||
else:
|
||||
rounding_mode = "line"
|
||||
|
||||
def _key(line):
|
||||
return (line.tax_rate, line.tax_code or "")
|
||||
|
||||
@@ -81,6 +81,7 @@ from pretix.helpers.countries import CachedCountries
|
||||
ROUNDING_MODES = (
|
||||
('line', _('Compute taxes for every line individually')),
|
||||
('sum_by_net', _('Compute taxes based on net total')),
|
||||
('sum_by_net_only_business', _('Compute taxes based on net total for business customers')),
|
||||
('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
|
||||
)
|
||||
|
||||
@@ -867,6 +867,11 @@ class TaxSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
"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_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 "
|
||||
|
||||
@@ -170,6 +170,12 @@ class OrderFeeAdded(OrderChangeLogEntryType):
|
||||
plain = _('A fee has been added')
|
||||
|
||||
|
||||
@log_entry_types.new()
|
||||
class OrderRecomputed(OrderChangeLogEntryType):
|
||||
action_type = 'pretix.event.order.changed.recomputed'
|
||||
plain = _('Taxes and rounding have been recomputed')
|
||||
|
||||
|
||||
@log_entry_types.new()
|
||||
class OrderFeeChanged(OrderChangeLogEntryType):
|
||||
action_type = 'pretix.event.order.changed.feevalue'
|
||||
|
||||
@@ -493,7 +493,18 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if cart.show_rounding_info %}
|
||||
<div class="text-muted">
|
||||
<small>
|
||||
{% icon "info-circle" %}
|
||||
{% blocktrans trimmed %}
|
||||
Since you entered a business address, your price was computed from the
|
||||
VAT-exclusive price. Due to rounding, this caused a small change to the
|
||||
total price.
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if not cart.is_ordered %}
|
||||
|
||||
@@ -167,7 +167,7 @@ class CartMixin:
|
||||
fees = []
|
||||
|
||||
if not order:
|
||||
apply_rounding(self.request.event.settings.tax_rounding, self.request.event.currency, [*lcp, *fees])
|
||||
apply_rounding(self.request.event.settings.tax_rounding, self.invoice_address, self.request.event.currency, [*lcp, *fees])
|
||||
|
||||
total = sum([c.price for c in lcp]) + sum([f.value for f in fees])
|
||||
net_total = sum(p.price - p.tax_value for p in lcp) + sum([f.net_value for f in fees])
|
||||
@@ -262,6 +262,11 @@ class CartMixin:
|
||||
'max_expiry_extend': max_expiry_extend,
|
||||
'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
|
||||
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)
|
||||
),
|
||||
'itemvarsums': itemvarsums,
|
||||
'current_selected_payments': [
|
||||
p for p in self.current_selected_payments(positions, fees, self.invoice_address)
|
||||
@@ -273,7 +278,7 @@ class CartMixin:
|
||||
raw_payments = copy.deepcopy(self.cart_session.get('payments', []))
|
||||
fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here
|
||||
|
||||
apply_rounding(self.request.event.settings.tax_rounding, self.request.event.currency, [*positions, *fees])
|
||||
apply_rounding(self.request.event.settings.tax_rounding, invoice_address, self.request.event.currency, [*positions, *fees])
|
||||
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
|
||||
|
||||
payments = []
|
||||
@@ -327,7 +332,7 @@ class CartMixin:
|
||||
fees.append(pf)
|
||||
|
||||
# Re-apply rounding as grand total has changed
|
||||
apply_rounding(self.request.event.settings.tax_rounding, self.request.event.currency, [*positions, *fees])
|
||||
apply_rounding(self.request.event.settings.tax_rounding, invoice_address, self.request.event.currency, [*positions, *fees])
|
||||
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
|
||||
|
||||
# Re-calculate to_pay as grand total has changed
|
||||
|
||||
@@ -43,7 +43,7 @@ def _validate_sample_lines(sample_lines, rounding_mode):
|
||||
(line.tax_value_includes_rounding_correction, line.price_includes_rounding_correction)
|
||||
for line in sample_lines
|
||||
]
|
||||
changed = apply_rounding(rounding_mode, "EUR", sample_lines)
|
||||
changed = apply_rounding(rounding_mode, None, "EUR", sample_lines)
|
||||
for line, original in zip(sample_lines, corrections):
|
||||
if (line.tax_value_includes_rounding_correction, line.price_includes_rounding_correction) != original:
|
||||
assert line in changed
|
||||
@@ -108,7 +108,7 @@ def test_revert_net_rounding_to_single_line(sample_lines):
|
||||
tax_rate=Decimal("19.00"),
|
||||
tax_code="S",
|
||||
)
|
||||
apply_rounding("sum_by_net", "EUR", [l])
|
||||
apply_rounding("sum_by_net", 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")
|
||||
@@ -126,7 +126,7 @@ def test_revert_net_keep_gross_rounding_to_single_line(sample_lines):
|
||||
tax_rate=Decimal("19.00"),
|
||||
tax_code="S",
|
||||
)
|
||||
apply_rounding("sum_by_net_keep_gross", "EUR", [l])
|
||||
apply_rounding("sum_by_net_keep_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")
|
||||
@@ -144,7 +144,7 @@ def test_rounding_of_impossible_gross_price(rounding_mode):
|
||||
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(rounding_mode, None, "EUR", [l])
|
||||
assert l.price == Decimal("23.01")
|
||||
assert l.price_includes_rounding_correction == Decimal("0.01")
|
||||
assert l.tax_value == Decimal("1.51")
|
||||
@@ -164,12 +164,12 @@ def test_round_down():
|
||||
assert sum(l.tax_value for l in lines) == Decimal("79.85")
|
||||
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.15")
|
||||
|
||||
apply_rounding("sum_by_net", "EUR", lines)
|
||||
apply_rounding("sum_by_net", None, "EUR", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("499.98")
|
||||
assert sum(l.tax_value for l in lines) == Decimal("79.83")
|
||||
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.15")
|
||||
|
||||
apply_rounding("sum_by_net_keep_gross", "EUR", lines)
|
||||
apply_rounding("sum_by_net_keep_gross", None, "EUR", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("500.00")
|
||||
assert sum(l.tax_value for l in lines) == Decimal("79.83")
|
||||
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.17")
|
||||
@@ -187,12 +187,12 @@ def test_round_up():
|
||||
assert sum(l.tax_value for l in lines) == Decimal("79.80")
|
||||
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.10")
|
||||
|
||||
apply_rounding("sum_by_net", "EUR", lines)
|
||||
apply_rounding("sum_by_net", None, "EUR", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("499.92")
|
||||
assert sum(l.tax_value for l in lines) == Decimal("79.82")
|
||||
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.10")
|
||||
|
||||
apply_rounding("sum_by_net_keep_gross", "EUR", lines)
|
||||
apply_rounding("sum_by_net_keep_gross", None, "EUR", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("499.90")
|
||||
assert sum(l.tax_value for l in lines) == Decimal("79.82")
|
||||
assert sum(l.price - l.tax_value for l in lines) == Decimal("420.08")
|
||||
@@ -210,12 +210,12 @@ def test_round_currency_without_decimals():
|
||||
assert sum(l.tax_value for l in lines) == Decimal("7980.00")
|
||||
assert sum(l.price - l.tax_value for l in lines) == Decimal("42010.00")
|
||||
|
||||
apply_rounding("sum_by_net", "JPY", lines)
|
||||
apply_rounding("sum_by_net", None, "JPY", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("49992.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("42010.00")
|
||||
|
||||
apply_rounding("sum_by_net_keep_gross", "JPY", lines)
|
||||
apply_rounding("sum_by_net_keep_gross", None, "JPY", lines)
|
||||
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")
|
||||
@@ -235,7 +235,7 @@ def test_do_not_touch_free(rounding_mode):
|
||||
price=Decimal("23.00"),
|
||||
)
|
||||
l2._calculate_tax(tax_rule=TaxRule(rate=Decimal("7.00")), invoice_address=InvoiceAddress())
|
||||
apply_rounding(rounding_mode, "EUR", [l1, l2])
|
||||
apply_rounding(rounding_mode, None, "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")
|
||||
@@ -245,3 +245,20 @@ def test_do_not_touch_free(rounding_mode):
|
||||
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")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_only_business():
|
||||
lines = [OrderPosition(
|
||||
price=Decimal("100.00"),
|
||||
tax_value=Decimal("15.97"),
|
||||
tax_rate=Decimal("19.00"),
|
||||
tax_code="S",
|
||||
) for _ in range(5)]
|
||||
assert sum(l.price for l in lines) == Decimal("500.00")
|
||||
|
||||
apply_rounding("sum_by_net_only_business", None, "EUR", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("500.00")
|
||||
|
||||
apply_rounding("sum_by_net_only_business", InvoiceAddress(is_business=True), "EUR", lines)
|
||||
assert sum(l.price for l in lines) == Decimal("499.98")
|
||||
|
||||
Reference in New Issue
Block a user