forked from CGM_Public/pretix_original
Allow to round taxes on order-level (#5019)
* Allow to round taxes on order-level * Rename get_cart_total * Persist rounding mode with order * Add general docs * Order creation API * Update fee algorithm * Rounding on payment method change * Round when splitting order * Fix failing tests * Add settings page * Add tests * Replace algorithm * Add test case for currency rounding * Improve order change * Update flowchart * Update discount logic (more hypothetical, we don't store rounding on cart positions atm) * Rename internal method * Fix typo * Update help text * Apply suggestions from code review Co-authored-by: luelista <weller@rami.io> * Order rounding refactor (#5571) * Add RoundingCorrectionMixin providing before-rounding-values as properties * Use gross_price_before_rounding in more places * Update doc/development/algorithms/pricing.rst Co-authored-by: Martin Gross <gross@rami.io> * Allow to override on perform_order * Rebase migration * Fix event cancellation --------- Co-authored-by: luelista <weller@rami.io> Co-authored-by: Martin Gross <gross@rami.io>
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import copy
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -50,10 +51,11 @@ from django_scopes import scopes_disabled
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.middleware import get_supported_language
|
||||
from pretix.base.models import (
|
||||
CartPosition, Customer, InvoiceAddress, ItemAddOn, Question,
|
||||
CartPosition, Customer, InvoiceAddress, ItemAddOn, OrderFee, Question,
|
||||
QuestionAnswer, QuestionOption, TaxRule,
|
||||
)
|
||||
from pretix.base.services.cart import get_fees
|
||||
from pretix.base.services.pricing import apply_rounding
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.cookies import set_cookie_without_samesite
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
@@ -147,6 +149,30 @@ class CartMixin:
|
||||
'question': value.label
|
||||
})
|
||||
|
||||
if order:
|
||||
fees = order.fees.all()
|
||||
elif lcp:
|
||||
try:
|
||||
fees = get_fees(
|
||||
event=self.request.event,
|
||||
request=self.request,
|
||||
invoice_address=self.invoice_address,
|
||||
payments=payments if payments is not None else self.cart_session.get('payments', []),
|
||||
positions=cartpos,
|
||||
)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
# ignore for now, will fail on order creation
|
||||
fees = []
|
||||
else:
|
||||
fees = []
|
||||
|
||||
if not order:
|
||||
apply_rounding(self.request.event.settings.tax_rounding, 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])
|
||||
tax_total = sum(p.tax_value for p in lcp) + sum([f.tax_value for f in fees])
|
||||
|
||||
# Group items of the same variation
|
||||
# We do this by list manipulations instead of a GROUP BY query, as
|
||||
# Django is unable to join related models in a .values() query
|
||||
@@ -177,7 +203,7 @@ class CartMixin:
|
||||
pos.subevent_id,
|
||||
pos.item_id,
|
||||
pos.variation_id,
|
||||
pos.price,
|
||||
pos.net_price if self.request.event.settings.display_net_prices else pos.price,
|
||||
(pos.voucher_id or 0),
|
||||
(pos.seat_id or 0),
|
||||
pos.valid_from,
|
||||
@@ -204,29 +230,6 @@ class CartMixin:
|
||||
group.additional_answers = pos_additional_fields.get(group.pk)
|
||||
positions.append(group)
|
||||
|
||||
total = sum(p.total for p in positions)
|
||||
net_total = sum(p.net_total for p in positions)
|
||||
tax_total = sum(p.total - p.net_total for p in positions)
|
||||
|
||||
if order:
|
||||
fees = order.fees.all()
|
||||
elif positions:
|
||||
try:
|
||||
fees = get_fees(
|
||||
self.request.event, self.request, total, self.invoice_address,
|
||||
payments if payments is not None else self.cart_session.get('payments', []),
|
||||
cartpos
|
||||
)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
# ignore for now, will fail on order creation
|
||||
fees = []
|
||||
else:
|
||||
fees = []
|
||||
|
||||
total += sum([f.value for f in fees])
|
||||
net_total += sum([f.net_value for f in fees])
|
||||
tax_total += sum([f.tax_value for f in fees])
|
||||
|
||||
try:
|
||||
first_expiry = min(p.expires for p in positions) if positions else now()
|
||||
max_expiry_extend = min((p.max_extend for p in positions if p.max_extend), default=None)
|
||||
@@ -255,20 +258,28 @@ 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),
|
||||
'current_selected_payments': [p for p in self.current_selected_payments(total) if p.get('multi_use_supported')]
|
||||
'current_selected_payments': [
|
||||
p for p in self.current_selected_payments(positions, fees, self.invoice_address)
|
||||
if p.get('multi_use_supported')
|
||||
]
|
||||
}
|
||||
|
||||
def current_selected_payments(self, total, warn=False, total_includes_payment_fees=False):
|
||||
def current_selected_payments(self, positions, fees, invoice_address, *, warn=False):
|
||||
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])
|
||||
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
|
||||
|
||||
payments = []
|
||||
total_remaining = total
|
||||
payments_assigned = Decimal("0.00")
|
||||
for p in raw_payments:
|
||||
# This algorithm of treating min/max values and fees needs to stay in sync between the following
|
||||
# places in the code base:
|
||||
# - pretix.base.services.cart.get_fees
|
||||
# - pretix.base.services.orders._get_fees
|
||||
# - pretix.presale.views.CartMixin.current_selected_payments
|
||||
if p.get('min_value') and total_remaining < Decimal(p['min_value']):
|
||||
if p.get('min_value') and total - payments_assigned < Decimal(p['min_value']):
|
||||
if warn:
|
||||
messages.warning(
|
||||
self.request,
|
||||
@@ -279,7 +290,7 @@ class CartMixin:
|
||||
self._remove_payment(p['id'])
|
||||
continue
|
||||
|
||||
to_pay = total_remaining
|
||||
to_pay = max(total - payments_assigned, Decimal("0.00"))
|
||||
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
||||
to_pay = min(to_pay, Decimal(p['max_value']))
|
||||
|
||||
@@ -288,12 +299,36 @@ class CartMixin:
|
||||
self._remove_payment(p['id'])
|
||||
continue
|
||||
|
||||
if not total_includes_payment_fees:
|
||||
fee = pprov.calculate_fee(to_pay)
|
||||
total_remaining += fee
|
||||
to_pay += fee
|
||||
else:
|
||||
fee = Decimal('0.00')
|
||||
payment_fee = pprov.calculate_fee(to_pay)
|
||||
if payment_fee:
|
||||
if self.request.event.settings.tax_rule_payment == "default":
|
||||
payment_fee_tax_rule = self.request.event.cached_default_tax_rule or TaxRule.zero()
|
||||
else:
|
||||
payment_fee_tax_rule = TaxRule.zero()
|
||||
try:
|
||||
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross', invoice_address=invoice_address)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
# Replicate behavior from elsewhere, will fail later at the order stage
|
||||
payment_fee = Decimal("0.00")
|
||||
payment_fee_tax = TaxRule.zero().tax(payment_fee)
|
||||
pf = OrderFee(
|
||||
fee_type=OrderFee.FEE_TYPE_PAYMENT,
|
||||
value=payment_fee,
|
||||
tax_rate=payment_fee_tax.rate,
|
||||
tax_value=payment_fee_tax.tax,
|
||||
tax_code=payment_fee_tax.code,
|
||||
tax_rule=payment_fee_tax_rule
|
||||
)
|
||||
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])
|
||||
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
|
||||
|
||||
# Re-calculate to_pay as grand total has changed
|
||||
to_pay = max(total - payments_assigned, Decimal("0.00"))
|
||||
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
||||
to_pay = min(to_pay, Decimal(p['max_value']))
|
||||
|
||||
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
||||
to_pay = min(to_pay, Decimal(p['max_value']))
|
||||
@@ -301,8 +336,8 @@ class CartMixin:
|
||||
p['payment_amount'] = to_pay
|
||||
p['provider_name'] = pprov.public_name
|
||||
p['pprov'] = pprov
|
||||
p['fee'] = fee
|
||||
total_remaining -= to_pay
|
||||
p['fee'] = payment_fee
|
||||
payments_assigned += to_pay
|
||||
payments.append(p)
|
||||
return payments
|
||||
|
||||
@@ -373,6 +408,21 @@ def get_cart(request):
|
||||
|
||||
|
||||
def get_cart_total(request):
|
||||
"""
|
||||
Use the following pattern instead::
|
||||
|
||||
cart = get_cart(request)
|
||||
fees = get_fees(
|
||||
event=request.event,
|
||||
request=request,
|
||||
invoice_address=cached_invoice_address(request),
|
||||
payments=None,
|
||||
positions=cart,
|
||||
)
|
||||
total = sum([c.price for c in cart]) + sum([f.value for f in fees])
|
||||
"""
|
||||
warnings.warn('get_cart_total is deprecated and will be removed in a future release',
|
||||
DeprecationWarning)
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
|
||||
if not hasattr(request, '_cart_total_cache'):
|
||||
@@ -409,13 +459,14 @@ def get_cart_is_free(request):
|
||||
cs = cart_session(request)
|
||||
pos = get_cart(request)
|
||||
ia = get_cart_invoice_address(request)
|
||||
total = get_cart_total(request)
|
||||
try:
|
||||
fees = get_fees(request.event, request, total, ia, cs.get('payments', []), pos)
|
||||
fees = get_fees(event=request.event, request=request, invoice_address=ia,
|
||||
payments=cs.get('payments', []), positions=pos)
|
||||
except TaxRule.SaleNotAllowed:
|
||||
# ignore for now, will fail on order creation
|
||||
fees = []
|
||||
request._cart_free_cache = total + sum(f.value for f in fees) == Decimal('0.00')
|
||||
|
||||
request._cart_free_cache = sum(p.price for p in pos) + sum(f.value for f in fees) == Decimal('0.00')
|
||||
return request._cart_free_cache
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user