Allow to create invoices before bank transfer runs (#1734)

Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
Felix Rindt
2020-08-04 10:53:59 +02:00
committed by GitHub
parent 9b367cb28b
commit 1c8699662d
8 changed files with 55 additions and 17 deletions

View File

@@ -126,6 +126,8 @@ The provider class
.. autoattribute:: test_mode_message .. autoattribute:: test_mode_message
.. autoattribute:: requires_invoice_immediately
Additional views Additional views
---------------- ----------------

View File

@@ -168,13 +168,23 @@ class BasePaymentProvider:
@property @property
def abort_pending_allowed(self) -> bool: 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 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 aborting a pending payment can never happen, it just hides the frontend button
to avoid users accidentally committing double payments. to avoid users accidentally committing double payments.
""" """
return False 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 @property
def settings_form_fields(self) -> dict: def settings_form_fields(self) -> dict:
""" """
@@ -770,7 +780,7 @@ class BasePaymentProvider:
def matching_id(self, payment: OrderPayment): 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. source. This should return the main transaction ID for your API.
:param payment: The payment in question. :param payment: The payment in question.

View File

@@ -933,8 +933,9 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
pass pass
invoice = order.invoices.last() # Might be generated by plugin already invoice = order.invoices.last() # Might be generated by plugin already
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order): if not invoice and invoice_qualified(order):
if not invoice: if event.settings.get('invoice_generate') == 'True' or (
event.settings.get('invoice_generate') == 'paid' and payment.payment_provider.requires_invoice_immediately):
invoice = generate_invoice( invoice = generate_invoice(
order, order,
trigger_pdf=not event.settings.invoice_email_attachment or not order.email 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 self.reissue_invoice and self._invoice_dirty:
if i: if i:
self._invoices.append(generate_cancellation(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)) self._invoices.append(generate_invoice(self.order))
def _check_complete_cancel(self): def _check_complete_cancel(self):

View File

@@ -525,7 +525,7 @@ DEFAULTS = {
('admin', _('Only manually in admin panel')), ('admin', _('Only manually in admin panel')),
('user', _('Automatically on user request')), ('user', _('Automatically on user request')),
('True', _('Automatically for all created orders')), ('True', _('Automatically for all created orders')),
('paid', _('Automatically on payment')), ('paid', _('Automatically on payment or when required by payment method')),
), ),
), ),
'form_kwargs': dict( 'form_kwargs': dict(
@@ -536,7 +536,7 @@ DEFAULTS = {
('admin', _('Only manually in admin panel')), ('admin', _('Only manually in admin panel')),
('user', _('Automatically on user request')), ('user', _('Automatically on user request')),
('True', _('Automatically for all created orders')), ('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.") help_text=_("Invoices will never be automatically generated for free orders.")
) )

View File

@@ -98,6 +98,12 @@ class BankTransfer(BasePaymentProvider):
}}, }},
required=False 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( ('public_name', I18nFormField(
label=_('Payment method name'), label=_('Payment method name'),
widget=I18nTextInput, 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 ' return _('In test mode, you can just manually mark this order as paid in the backend after it has been '
'created.') 'created.')
@property
def requires_invoice_immediately(self):
return self.settings.get('invoice_immediately', False, as_type=bool)
@property @property
def settings_form_fields(self): def settings_form_fields(self):
d = OrderedDict(list(super().settings_form_fields.items()) + list(BankTransfer.form_fields().items())) d = OrderedDict(list(super().settings_form_fields.items()) + list(BankTransfer.form_fields().items()))

View File

@@ -141,7 +141,7 @@ def get_checkout_flow(event):
# Sort by priority # Sort by priority
flow.sort(key=lambda p: p.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 last = None
for step in flow: for step in flow:
step._previous = last step._previous = last

View File

@@ -189,10 +189,10 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
[p.generate_ticket for p in ctx['cart']['positions']].count(True) > 1 [p.generate_ticket for p in ctx['cart']['positions']].count(True) > 1
) )
ctx['invoices'] = list(self.order.invoices.all()) 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 ctx['can_generate_invoice']:
if not self.order.payments.exclude( 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: ).exists() and self.order.status == Order.STATUS_PENDING:
ctx['generate_invoice_requires'] = 'payment' ctx['generate_invoice_requires'] = 'payment'
ctx['url'] = build_absolute_uri( ctx['url'] = build_absolute_uri(
@@ -388,6 +388,14 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
try: 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) resp = self.payment.payment_provider.execute_payment(request, self.payment)
except PaymentException as e: except PaymentException as e:
messages.error(request, str(e)) messages.error(request, str(e))

View File

@@ -2,19 +2,19 @@ import configparser
import logging import logging
import os import os
import sys import sys
from urllib.parse import urlparse from urllib.parse import urlparse
import django.conf.locale
from django.utils.crypto import get_random_string
from kombu import Queue from kombu import Queue
from pkg_resources import iter_entry_points
from pycountry import currencies 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 . import __version__
from django.contrib.messages import constants as messages # NOQA
from django.utils.translation import gettext_lazy as _ # NOQA
config = configparser.RawConfigParser() config = configparser.RawConfigParser()
if 'PRETIX_CONFIG_FILE' in os.environ: if 'PRETIX_CONFIG_FILE' in os.environ:
config.read_file(open(os.environ.get('PRETIX_CONFIG_FILE'), encoding='utf-8')) 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')): if config.has_option('sentry', 'dsn') and not any(c in sys.argv for c in ('shell', 'shell_scoped', 'shell_plus')):
import sentry_sdk import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration 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 from .sentry import PretixSentryIntegration, setup_custom_filters
SENTRY_ENABLED = True SENTRY_ENABLED = True