diff --git a/doc/development/api/payment.rst b/doc/development/api/payment.rst index a060eaed2..8dbafa0a2 100644 --- a/doc/development/api/payment.rst +++ b/doc/development/api/payment.rst @@ -126,6 +126,8 @@ The provider class .. autoattribute:: test_mode_message + .. autoattribute:: requires_invoice_immediately + Additional views ---------------- diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 9ea4e1f61..093fc7094 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -168,13 +168,23 @@ class BasePaymentProvider: @property def abort_pending_allowed(self) -> bool: """ - Whether or not a user can abort a payment in pending start to switch to another + Whether or not a user can abort a payment in pending state to switch to another payment method. This returns ``False`` by default which is no guarantee that aborting a pending payment can never happen, it just hides the frontend button to avoid users accidentally committing double payments. """ return False + @property + def requires_invoice_immediately(self): + """ + Return whether this payment method requires an invoice to exist for an order, even though the event + is configured to only create invoices for paid orders. + By default this is False, but it might be overwritten for e.g. bank transfer. + `execute_payment` is called after the invoice is created. + """ + return False + @property def settings_form_fields(self) -> dict: """ @@ -770,7 +780,7 @@ class BasePaymentProvider: def matching_id(self, payment: OrderPayment): """ - Will be called to get an ID for a matching this payment when comparing pretix records with records of an external + Will be called to get an ID for matching this payment when comparing pretix records with records of an external source. This should return the main transaction ID for your API. :param payment: The payment in question. diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index bfc618ef5..1841cd8ec 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -933,8 +933,9 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str], pass invoice = order.invoices.last() # Might be generated by plugin already - if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order): - if not invoice: + if not invoice and invoice_qualified(order): + if event.settings.get('invoice_generate') == 'True' or ( + event.settings.get('invoice_generate') == 'paid' and payment.payment_provider.requires_invoice_immediately): invoice = generate_invoice( order, trigger_pdf=not event.settings.invoice_email_attachment or not order.email @@ -1876,7 +1877,11 @@ class OrderChangeManager: if self.reissue_invoice and self._invoice_dirty: if i: self._invoices.append(generate_cancellation(i)) - if (i or self.event.settings.invoice_generate == 'True') and invoice_qualified(self.order): + if invoice_qualified(self.order) and \ + (i or + self.event.settings.invoice_generate == 'True' or ( + self.open_payment is not None and self.event.settings.invoice_generate == 'paid' and + self.open_payment.payment_provider.requires_invoice_immediately)): self._invoices.append(generate_invoice(self.order)) def _check_complete_cancel(self): diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 08b47c8f4..7f3da187e 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -525,7 +525,7 @@ DEFAULTS = { ('admin', _('Only manually in admin panel')), ('user', _('Automatically on user request')), ('True', _('Automatically for all created orders')), - ('paid', _('Automatically on payment')), + ('paid', _('Automatically on payment or when required by payment method')), ), ), 'form_kwargs': dict( @@ -536,7 +536,7 @@ DEFAULTS = { ('admin', _('Only manually in admin panel')), ('user', _('Automatically on user request')), ('True', _('Automatically for all created orders')), - ('paid', _('Automatically on payment')), + ('paid', _('Automatically on payment or when required by payment method')), ), help_text=_("Invoices will never be automatically generated for free orders.") ) diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py index b6a7d8d4e..21659f6c6 100644 --- a/src/pretix/plugins/banktransfer/payment.py +++ b/src/pretix/plugins/banktransfer/payment.py @@ -98,6 +98,12 @@ class BankTransfer(BasePaymentProvider): }}, required=False )), + ('invoice_immediately', + forms.BooleanField( + label=_('Create an invoice for orders using bank transfer immediately if the event is otherwise ' + 'configured to create invoices after payment is completed.'), + required=False, + )), ('public_name', I18nFormField( label=_('Payment method name'), widget=I18nTextInput, @@ -119,6 +125,10 @@ class BankTransfer(BasePaymentProvider): return _('In test mode, you can just manually mark this order as paid in the backend after it has been ' 'created.') + @property + def requires_invoice_immediately(self): + return self.settings.get('invoice_immediately', False, as_type=bool) + @property def settings_form_fields(self): d = OrderedDict(list(super().settings_form_fields.items()) + list(BankTransfer.form_fields().items())) diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index adc770a70..00cef0c08 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -141,7 +141,7 @@ def get_checkout_flow(event): # Sort by priority flow.sort(key=lambda p: p.priority) - # Create a double-linked-list for esasy forwards/backwards traversal + # Create a double-linked-list for easy forwards/backwards traversal last = None for step in flow: step._previous = last diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index 4e8fdc501..5f0f5be27 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -189,10 +189,10 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin, [p.generate_ticket for p in ctx['cart']['positions']].count(True) > 1 ) ctx['invoices'] = list(self.order.invoices.all()) - ctx['can_generate_invoice'] = can_generate_invoice(self.request.event, self.order, True) + ctx['can_generate_invoice'] = can_generate_invoice(self.request.event, self.order, ignore_payments=True) if ctx['can_generate_invoice']: if not self.order.payments.exclude( - state__in=[OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED] + state__in=[OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED] ).exists() and self.order.status == Order.STATUS_PENDING: ctx['generate_invoice_requires'] = 'payment' ctx['url'] = build_absolute_uri( @@ -388,6 +388,14 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView): def post(self, request, *args, **kwargs): try: + if not self.order.invoices.exists() and invoice_qualified(self.order): + if self.request.event.settings.get('invoice_generate') == 'True' or ( + self.request.event.settings.get('invoice_generate') == 'paid' and self.payment.payment_provider.requires_invoice_immediately): + i = generate_invoice(self.order) + self.order.log_action('pretix.event.order.invoice.generated', data={ + 'invoice': i.pk + }) + messages.success(self.request, _('An invoice has been generated.')) resp = self.payment.payment_provider.execute_payment(request, self.payment) except PaymentException as e: messages.error(request, str(e)) diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 49bfe609d..46245303d 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -2,19 +2,19 @@ import configparser import logging import os import sys - from urllib.parse import urlparse +import django.conf.locale +from django.utils.crypto import get_random_string from kombu import Queue +from pkg_resources import iter_entry_points from pycountry import currencies -import django.conf.locale -from django.contrib.messages import constants as messages # NOQA -from django.utils.crypto import get_random_string -from django.utils.translation import gettext_lazy as _ # NOQA -from pkg_resources import iter_entry_points from . import __version__ +from django.contrib.messages import constants as messages # NOQA +from django.utils.translation import gettext_lazy as _ # NOQA + config = configparser.RawConfigParser() if 'PRETIX_CONFIG_FILE' in os.environ: config.read_file(open(os.environ.get('PRETIX_CONFIG_FILE'), encoding='utf-8')) @@ -637,7 +637,10 @@ SENTRY_ENABLED = False if config.has_option('sentry', 'dsn') and not any(c in sys.argv for c in ('shell', 'shell_scoped', 'shell_plus')): import sentry_sdk from sentry_sdk.integrations.celery import CeleryIntegration - from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger + from sentry_sdk.integrations.logging import ( + LoggingIntegration, ignore_logger, + ) + from .sentry import PretixSentryIntegration, setup_custom_filters SENTRY_ENABLED = True