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

@@ -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.

View File

@@ -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):

View File

@@ -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.")
)

View File

@@ -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()))

View File

@@ -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

View File

@@ -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))

View File

@@ -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