diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index b207bbc57a..5f2fad27db 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -275,6 +275,7 @@ class OrganizerSettingsSerializer(SettingsSerializer): default_fields = [ 'customer_accounts', 'customer_accounts_link_by_email', + 'invoice_regenerate_allowed', 'contact_mail', 'imprint_url', 'organizer_info_text', diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 8e4a8aafcb..ab0794805d 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -1451,8 +1451,14 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet): inv = self.get_object() if inv.canceled: raise ValidationError('The invoice has already been canceled.') + if not inv.event.settings.invoice_regenerate_allowed: + raise PermissionDenied('Invoices may not be changed after they are created.') elif inv.shredded: raise PermissionDenied('The invoice file is no longer stored on the server.') + elif inv.sent_to_organizer: + raise PermissionDenied('The invoice file has already been exported.') + elif now().astimezone(self.request.event.timezone).date() - inv.date > datetime.timedelta(days=1): + raise PermissionDenied('The invoice file is too old to be regenerated.') else: inv = regenerate_invoice(inv) inv.order.log_action( diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index d0f6199461..c0c49c8357 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -323,7 +323,7 @@ def generate_invoice(order: Order, trigger_pdf=True): order=order, event=order.event, organizer=order.event.organizer, - date=timezone.now().date(), + date=timezone.now().astimezone(order.event.timezone).date(), ) invoice = build_invoice(invoice) if trigger_pdf: diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index a01c62a0e6..b6e9b4c6c7 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -744,6 +744,18 @@ DEFAULTS = { "changes made through the backend."), ) }, + 'invoice_regenerate_allowed': { + 'default': 'False', + 'type': bool, + 'form_class': forms.BooleanField, + 'serializer_class': serializers.BooleanField, + 'form_kwargs': dict( + label=_("Allow to update existing invoices"), + help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we " + "recommend to leave this option turned off and always issue a new invoice if a change needs " + "to be made."), + ) + }, 'invoice_generate_sales_channels': { 'default': json.dumps(['web']), 'type': list diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index b4b43775f4..7badbb5a34 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -286,6 +286,7 @@ class OrganizerSettingsForm(SettingsForm): auto_fields = [ 'customer_accounts', 'customer_accounts_link_by_email', + 'invoice_regenerate_allowed', 'contact_mail', 'imprint_url', 'organizer_info_text', diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index a37eb767d2..9739df9578 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -236,14 +236,16 @@ {% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %} {{ i.number }} ({{ i.date|date:"SHORT_DATE_FORMAT" }}) {% if not i.canceled %} -
- {% csrf_token %} - -
+ {% if request.event.settings.invoice_regenerate_allowed %} +
+ {% csrf_token %} + +
+ {% endif %} {% if not i.is_cancellation %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/edit.html b/src/pretix/control/templates/pretixcontrol/organizers/edit.html index d1e6a8361f..55043d7a98 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/edit.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/edit.html @@ -81,6 +81,10 @@ {% bootstrap_field sform.giftcard_expiry_years layout="control" %} {% bootstrap_field sform.giftcard_length layout="control" %} +
+ {% trans "Invoices" %} + {% bootstrap_field sform.invoice_regenerate_allowed layout="control" %} +
diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 5a8f7fb20d..de8ae6161a 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -1330,8 +1330,14 @@ class OrderInvoiceRegenerate(OrderView): except Invoice.DoesNotExist: messages.error(self.request, _('Unknown invoice.')) else: + if not inv.event.settings.invoice_regenerate_allowed: + messages.error(self.request, _('Invoices may not be changed after they are created.')) if inv.canceled: messages.error(self.request, _('The invoice has already been canceled.')) + elif inv.sent_to_organizer: + messages.error(self.request, _('The invoice file has already been exported.')) + elif now().astimezone(self.request.event.timezone).date() - inv.date > timedelta(days=1): + messages.error(self.request, _('The invoice file is too old to be regenerated.')) elif inv.shredded: messages.error(self.request, _('The invoice has been cleaned of personal data.')) else: diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index b48f059bc4..585ea2339b 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -25,6 +25,7 @@ import json from decimal import Decimal from unittest import mock +import freezegun import pytest from django.core import mail as djmail from django.core.files.base import ContentFile @@ -1112,15 +1113,17 @@ def test_invoice_detail(token_client, organizer, event, item, invoice): @pytest.mark.django_db def test_invoice_regenerate(token_client, organizer, event, invoice): + organizer.settings.invoice_regenerate_allowed = True with scopes_disabled(): InvoiceAddress.objects.filter(order=invoice.order).update(company="ACME Ltd") - resp = token_client.post('/api/v1/organizers/{}/events/{}/invoices/{}/regenerate/'.format( - organizer.slug, event.slug, invoice.number - )) - assert resp.status_code == 204 - invoice.refresh_from_db() - assert "ACME Ltd" in invoice.invoice_to + with freezegun.freeze_time("2017-12-10"): + resp = token_client.post('/api/v1/organizers/{}/events/{}/invoices/{}/regenerate/'.format( + organizer.slug, event.slug, invoice.number + )) + assert resp.status_code == 204 + invoice.refresh_from_db() + assert "ACME Ltd" in invoice.invoice_to @pytest.mark.django_db