From cc7f249cb84a32bae2c3dc5c74e648da31dd93d1 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 23 Jun 2023 11:30:37 +0200 Subject: [PATCH] Fix crash if a tax rule on a fee prevents sale (PRETIXEU-8MZ) --- src/pretix/base/payment.py | 8 +++-- src/pretix/base/services/orders.py | 10 ++++-- src/pretix/plugins/paypal2/views.py | 12 +++++-- src/pretix/presale/checkoutflow.py | 27 +++++++++----- src/pretix/presale/views/__init__.py | 22 ++++++++---- src/tests/presale/test_checkout.py | 53 ++++++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 23 deletions(-) diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 81fe7f3370..a79af66c8e 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -60,7 +60,7 @@ from pretix.base.channels import get_all_sales_channels from pretix.base.forms import PlaceholderValidator from pretix.base.models import ( CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment, - OrderRefund, Quota, + OrderRefund, Quota, TaxRule, ) from pretix.base.reldate import RelativeDateField, RelativeDateWrapper from pretix.base.settings import SettingsSandbox @@ -1015,7 +1015,11 @@ class FreeOrderProvider(BasePaymentProvider): cart = get_cart(request) total = get_cart_total(request) - total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)]) + try: + total += sum([f.value for f in get_fees(self.event, request, total, None, None, cart)]) + except TaxRule.SaleNotAllowed: + # ignore for now, will fail on order creation + pass return total == 0 def order_change_allowed(self, order: Order) -> bool: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 19e8719397..6521d85470 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -935,7 +935,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d raise OrderError(e.message) require_approval = any(p.requires_approval(invoice_address=address) for p in positions) - fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval) + try: + fees = _get_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval) + except TaxRule.SaleNotAllowed: + raise OrderError(error_messages['country_blocked']) total = pending_sum = sum([c.price for c in positions]) + sum([c.value for c in fees]) order = Order( @@ -968,7 +971,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d for fee in fees: fee.order = order - fee._calculate_tax() + try: + fee._calculate_tax() + except TaxRule.SaleNotAllowed: + raise OrderError(error_messages['country_blocked']) if fee.tax_rule and not fee.tax_rule.pk: fee.tax_rule = None # TODO: deprecate fee.save() diff --git a/src/pretix/plugins/paypal2/views.py b/src/pretix/plugins/paypal2/views.py index 68593ac848..7651d79a44 100644 --- a/src/pretix/plugins/paypal2/views.py +++ b/src/pretix/plugins/paypal2/views.py @@ -56,7 +56,9 @@ from django.views.generic import TemplateView from django_scopes import scopes_disabled from paypalcheckoutsdk import orders as pp_orders, payments as pp_payments -from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota +from pretix.base.models import ( + Event, Order, OrderPayment, OrderRefund, Quota, TaxRule, +) from pretix.base.payment import PaymentException from pretix.base.services.cart import add_payment_to_cart, get_fees from pretix.base.settings import GlobalSettingsObject @@ -158,8 +160,12 @@ class XHRView(View): 'info_data': {}, }] - for fee in get_fees(request.event, request, cart_total, None, simulated_payments, get_cart(request)): - cart_total += fee.value + try: + for fee in get_fees(request.event, request, cart_total, None, simulated_payments, get_cart(request)): + cart_total += fee.value + except TaxRule.SaleNotAllowed: + # ignore for now, will fail on order creation + pass total_remaining = cart_total for p in multi_use_cart_payments: diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index d80e05009d..1ba322f8d8 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -1126,13 +1126,17 @@ class PaymentStep(CartMixin, TemplateFlowStep): def _total_order_value(self): cart = get_cart(self.request) total = get_cart_total(self.request) - total += sum([ - f.value for f in get_fees( - self.request.event, self.request, total, self.invoice_address, - [p for p in self.cart_session.get('payments', []) if p.get('multi_use_supported')], - cart, - ) - ]) + try: + total += sum([ + f.value for f in get_fees( + self.request.event, self.request, total, self.invoice_address, + [p for p in self.cart_session.get('payments', []) if p.get('multi_use_supported')], + cart, + ) + ]) + except TaxRule.SaleNotAllowed: + # ignore for now, will fail on order creation + pass return Decimal(total) @cached_property @@ -1201,6 +1205,7 @@ class PaymentStep(CartMixin, TemplateFlowStep): cart = self.get_cart(payments=simulated_payments) else: cart = self.get_cart() + resp = pprov.checkout_prepare( request, cart, @@ -1283,8 +1288,12 @@ class PaymentStep(CartMixin, TemplateFlowStep): cart = get_cart(self.request) total = get_cart_total(self.request) - total += sum([f.value for f in get_fees(self.request.event, self.request, total, self.invoice_address, - self.cart_session.get('payments', []), cart)]) + try: + total += sum([f.value for f in get_fees(self.request.event, self.request, total, self.invoice_address, + self.cart_session.get('payments', []), cart)]) + except TaxRule.SaleNotAllowed: + # ignore for now, will fail on order creation + pass selected = self.current_selected_payments(total, warn=warn, total_includes_payment_fees=True) if sum(p['payment_amount'] for p in selected) != total: if warn: diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py index 6771297e1c..0c8642b980 100644 --- a/src/pretix/presale/views/__init__.py +++ b/src/pretix/presale/views/__init__.py @@ -49,7 +49,7 @@ from django_scopes import scopes_disabled from pretix.base.i18n import language from pretix.base.models import ( CartPosition, Customer, InvoiceAddress, ItemAddOn, Question, - QuestionAnswer, QuestionOption, + QuestionAnswer, QuestionOption, TaxRule, ) from pretix.base.services.cart import get_fees from pretix.base.templatetags.money import money_filter @@ -198,11 +198,15 @@ class CartMixin: if order: fees = order.fees.all() elif positions: - 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 - ) + 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 = [] @@ -389,7 +393,11 @@ def get_cart_is_free(request): pos = get_cart(request) ia = get_cart_invoice_address(request) total = get_cart_total(request) - fees = get_fees(request.event, request, total, ia, cs.get('payments', []), pos) + try: + fees = get_fees(request.event, request, total, ia, cs.get('payments', []), 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') return request._cart_free_cache diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index b9409eb7ba..14102710fa 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -456,6 +456,59 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): cr1.refresh_from_db() assert cr1.price == Decimal('23.00') + def test_custom_tax_rules_blocked_on_fee(self): + self.tr7 = self.event.tax_rules.create(rate=7) + self.tr7.custom_rules = json.dumps([ + {'country': 'AT', 'address_type': 'business_vat_id', 'action': 'reverse'}, + {'country': 'ZZ', 'address_type': '', 'action': 'block'}, + ]) + self.tr7.save() + self.event.settings.set('payment_banktransfer__enabled', True) + self.event.settings.set('payment_banktransfer__fee_percent', 20) + self.event.settings.set('payment_banktransfer__fee_reverse_calc', False) + self.event.settings.set('tax_rate_default', self.tr7) + self.event.settings.invoice_address_vatid = True + + with scopes_disabled(): + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + + self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'is_business': 'business', + 'company': 'Foo', + 'name': 'Bar', + 'street': 'Baz', + 'zipcode': '12345', + 'city': 'Here', + 'country': 'DE', + 'email': 'admin@localhost' + }, follow=True) + + with mock.patch('pretix.base.services.tax._validate_vat_id_EU') as mock_validate: + mock_validate.return_value = 'AT123456' + self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'is_business': 'business', + 'company': 'Foo', + 'name': 'Bar', + 'street': 'Baz', + 'zipcode': '1234', + 'city': 'Here', + 'country': 'AT', + 'vat_id': 'AT123456', + 'email': 'admin@localhost' + }, follow=True) + + self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), { + 'payment': 'banktransfer' + }, follow=True) + + r = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(r.content.decode(), "lxml") + assert doc.select(".alert-danger") + assert "not available in the selected country" in doc.select(".alert-danger")[0].text + def test_custom_tax_rules_blocked(self): self.tr19.custom_rules = json.dumps([ {'country': 'AT', 'address_type': 'business_vat_id', 'action': 'reverse'},