From 15c2c39ac4643b61812e61fa83ff3f2e77e5e1f3 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 13 Jan 2026 16:50:58 +0100 Subject: [PATCH] Invoices: Allow issuing invoices only to businesses In situations where every invoice has a significant accounting cost and consumers usually do not need invoices, this can save a lot of money or effort. --- src/pretix/api/serializers/event.py | 1 + src/pretix/api/views/order.py | 2 +- src/pretix/base/services/invoices.py | 15 +++++++++++++-- src/pretix/base/settings.py | 9 +++++++++ src/pretix/control/forms/event.py | 1 + .../pretixcontrol/event/invoicing.html | 1 + src/pretix/control/views/orders.py | 2 +- src/tests/base/test_invoices.py | 19 +++++++++++++++++++ 8 files changed, 46 insertions(+), 4 deletions(-) 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 6f42727bc4..b7ad44ac7a 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/views/orders.py b/src/pretix/control/views/orders.py index 7f758d0dd0..27371a058e 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -1742,7 +1742,7 @@ 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) else: inv = c 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