diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 93cef60999..7237bd3b27 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1442,11 +1442,13 @@ class AbstractPosition(models.Model): lines = [r.strip() for r in lines if r] return '\n'.join(lines).strip() - def requires_approval(self): + def requires_approval(self, invoice_address=None): if self.item.require_approval: return True if self.variation and self.variation.require_approval: return True + if self.item.tax_rule and self.item.tax_rule._require_approval(invoice_address): + return True return False diff --git a/src/pretix/base/models/tax.py b/src/pretix/base/models/tax.py index bc5ef661a5..5da308dd34 100644 --- a/src/pretix/base/models/tax.py +++ b/src/pretix/base/models/tax.py @@ -211,7 +211,7 @@ class TaxRule(LoggedModel): rule = self.get_matching_rule(invoice_address) if rule.get('action', 'vat') == 'block': raise self.SaleNotAllowed() - if rule.get('action', 'vat') == 'vat' and rule.get('rate') is not None: + if rule.get('action', 'vat') in ('vat', 'require_approval') and rule.get('rate') is not None: return Decimal(rule.get('rate')) return Decimal(self.rate) @@ -337,12 +337,19 @@ class TaxRule(LoggedModel): return False + def _require_approval(self, invoice_address): + if self._custom_rules: + rule = self.get_matching_rule(invoice_address) + if rule.get('action', 'vat') == 'require_approval': + return True + return False + def _tax_applicable(self, invoice_address): if self._custom_rules: rule = self.get_matching_rule(invoice_address) if rule.get('action', 'vat') == 'block': raise self.SaleNotAllowed() - return rule.get('action', 'vat') == 'vat' + return rule.get('action', 'vat') in ('vat', 'require_approval') if not self.eu_reverse_charge: # No reverse charge rules? Always apply VAT! diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index dbd90925b8..ec0fe47053 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -856,7 +856,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d total=total, testmode=True if sales_channel.testmode_supported and event.testmode else False, meta_info=json.dumps(meta_info or {}), - require_approval=any(p.requires_approval() for p in positions), + require_approval=any(p.requires_approval(invoice_address=address) for p in positions), sales_channel=sales_channel.identifier, customer=customer, ) @@ -2071,7 +2071,7 @@ class OrderChangeManager: split_order.code = None split_order.datetime = now() split_order.secret = generate_secret() - split_order.require_approval = self.order.require_approval and any(p.requires_approval() for p in split_positions) + split_order.require_approval = self.order.require_approval and any(p.requires_approval(invoice_address=self._invoice_address) for p in split_positions) split_order.save() split_order.log_action('pretix.event.order.changed.split_from', user=self.user, auth=self.auth, data={ 'original_order': self.order.code diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 8e9ea3875f..d62eca48a0 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -1215,6 +1215,7 @@ class TaxRuleLineForm(I18nForm): ('reverse', _('Reverse charge')), ('no', _('No VAT')), ('block', _('Sale not allowed')), + ('require_approval', _('Order requires approval')), ], ) rate = forms.DecimalField( diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index c79beb46f3..32a7ea659d 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -490,6 +490,7 @@ class OrganizerCreate(CreateView): organizer=form.instance, name=_('Administrators'), all_events=True, can_create_events=True, can_change_teams=True, can_manage_gift_cards=True, can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True, + can_manage_customers=True, can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True ) t.members.add(self.request.user) diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index f19cb26615..b2e66a02c4 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -1161,7 +1161,7 @@ class PaymentStep(CartMixin, TemplateFlowStep): self.request = request for cartpos in get_cart(self.request): - if cartpos.requires_approval(): + if cartpos.requires_approval(invoice_address=self.invoice_address): if 'payment' in self.cart_session: del self.cart_session['payment'] return False @@ -1206,7 +1206,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): if self.payment_provider: ctx['payment'] = self.payment_provider.checkout_confirm_render(self.request) ctx['payment_provider'] = self.payment_provider - ctx['require_approval'] = any(cp.requires_approval() for cp in ctx['cart']['positions']) + ctx['require_approval'] = any(cp.requires_approval(invoice_address=self.invoice_address) for cp in ctx['cart']['positions']) ctx['addr'] = self.invoice_address ctx['confirm_messages'] = self.confirm_messages ctx['cart_session'] = self.cart_session diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index 6513122555..9b8fb24890 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -448,6 +448,38 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): cr1.refresh_from_db() assert cr1.price == Decimal('19.33') + def test_custom_tax_rules_require_approval(self): + self.tr19.custom_rules = json.dumps([ + {'country': 'AT', 'address_type': 'business_vat_id', 'action': 'reverse'}, + {'country': 'ZZ', 'address_type': '', 'action': 'require_approval'}, + ]) + self.tr19.save() + self.event.settings.invoice_address_vatid = True + + with scopes_disabled(): + cr1 = 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) + + self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + with scopes_disabled(): + assert not CartPosition.objects.filter(pk=cr1.pk).exists() + o = Order.objects.last() + assert o.require_approval + assert o.positions.first().tax_rate == Decimal('19.00') + def _test_country_taxing(self): self._enable_country_specific_taxing()