diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 325e24c7ce..58acf3d4f5 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -806,6 +806,7 @@ class EventSettingsSerializer(SettingsSerializer): 'invoice_reissue_after_modify', 'invoice_include_free', 'invoice_generate', + 'invoice_generate_only_business', 'invoice_period', 'invoice_numbers_consecutive', 'invoice_numbers_prefix', diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 51d463047a..2b5a7f82bd 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -2031,7 +2031,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet): else: order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id) c = generate_cancellation(inv) - if inv.order.status != Order.STATUS_CANCELED: + if invoice_qualified(order): inv = generate_invoice(order) else: inv = c diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index 6db88be3ee..d76c48c992 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -521,9 +521,20 @@ def invoice_pdf_task(invoice: int): def invoice_qualified(order: Order): - if order.total == Decimal('0.00') or order.require_approval or \ - order.sales_channel.identifier not in order.event.settings.get('invoice_generate_sales_channels'): + if order.total == Decimal('0.00'): return False + if order.require_approval: + return False + if order.sales_channel.identifier not in order.event.settings.invoice_generate_sales_channels: + return False + if order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED): + return False + if order.event.settings.invoice_generate_only_business: + try: + ia = order.invoice_address + return ia.is_business + except InvoiceAddress.DoesNotExist: + return False return True diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 540ab5c3a6..e1ad944b2d 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1225,6 +1225,15 @@ DEFAULTS = { 'default': json.dumps(['web']), 'type': list }, + 'invoice_generate_only_business': { + 'default': 'False', + 'type': bool, + 'form_class': forms.BooleanField, + 'serializer_class': serializers.BooleanField, + 'form_kwargs': dict( + label=_("Only issue invoices to business customers"), + ) + }, 'invoice_address_from': { 'default': '', 'type': str, diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index b04c378ce7..917b4de1c0 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -939,6 +939,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm): 'invoice_show_payments', 'invoice_reissue_after_modify', 'invoice_generate', + 'invoice_generate_only_business', 'invoice_period', 'invoice_attendee_name', 'invoice_event_location', diff --git a/src/pretix/control/templates/pretixcontrol/event/invoicing.html b/src/pretix/control/templates/pretixcontrol/event/invoicing.html index c2fe36d82a..0c51a8e185 100644 --- a/src/pretix/control/templates/pretixcontrol/event/invoicing.html +++ b/src/pretix/control/templates/pretixcontrol/event/invoicing.html @@ -12,6 +12,7 @@ {% trans "Invoice generation" %} {% bootstrap_field form.invoice_generate layout="control" %} {% bootstrap_field form.invoice_generate_sales_channels layout="control" %} + {% bootstrap_field form.invoice_generate_only_business layout="control" %} {% bootstrap_field form.invoice_email_attachment layout="control" %} {% bootstrap_field form.invoice_email_organizer layout="control" %} {% bootstrap_field form.invoice_language layout="control" %} diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index 2a1cada254..9ae065d0ad 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -353,7 +353,7 @@ data-toggle="tooltip" title="{% trans 'Generate a cancellation document for this invoice and create a new invoice with a new invoice number.' %}" {% endif %}> - {% if order.status == "c" %} + {% if order.status == "c" or not invoice_qualified %} {% trans "Generate cancellation" %} {% else %} {% trans "Cancel and reissue" %} diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 7f758d0dd0..77fe326849 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -509,9 +509,10 @@ class OrderView(EventPermissionRequiredMixin, DetailView): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) - ctx['can_generate_invoice'] = invoice_qualified(self.order) and ( + ctx['invoice_qualified'] = invoice_qualified(self.order) + ctx['can_generate_invoice'] = ctx['invoice_qualified'] and ( self.request.event.settings.invoice_generate in ('admin', 'user', 'paid', 'user_paid', 'True') - ) and self.order.status in (Order.STATUS_PAID, Order.STATUS_PENDING) and ( + ) and ( not self.order.invoices.exists() or self.order.invoices.filter(is_cancellation=True).count() >= self.order.invoices.filter(is_cancellation=False).count() ) @@ -1742,14 +1743,15 @@ class OrderInvoiceReissue(OrderView): messages.error(self.request, _('The invoice has been cleaned of personal data.')) else: c = generate_cancellation(inv) - if order.status not in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED): + if invoice_qualified(order): inv = generate_invoice(order) + messages.success(self.request, _('The invoice has been reissued.')) else: inv = c + messages.success(self.request, _('The invoice has been canceled.')) order.log_action('pretix.event.order.invoice.reissued', user=self.request.user, data={ 'invoice': inv.pk }) - messages.success(self.request, _('The invoice has been reissued.')) return redirect(self.get_order_url()) def get(self, *args, **kwargs): # NOQA diff --git a/src/tests/base/test_invoices.py b/src/tests/base/test_invoices.py index a3032ce805..6d8ee71f6b 100644 --- a/src/tests/base/test_invoices.py +++ b/src/tests/base/test_invoices.py @@ -612,6 +612,25 @@ def test_sales_channels_qualify(env): assert invoice_qualified(order) is False +@pytest.mark.django_db +def test_business_only(env): + event, order = env + event.settings.set('invoice_generate', 'admin') + event.settings.set('invoice_generate_only_business', True) + order.total = Decimal('42.00') + + ia = InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street', is_business=True, + zipcode='12345', city='London', country_old='England', country='', order=order) + + assert invoice_qualified(order) is True + + ia.is_business = False + ia.save() + + # Order with default Sales Channel (web) + assert invoice_qualified(order) is False + + def test_addon_aware_groupby(): def is_addon(item): is_addon, id, price = item