mirror of
https://github.com/pretix/pretix.git
synced 2026-05-08 15:44:02 +00:00
Pluggable invoice transmission methods (#5020)
* Flexible invoice transmission
* UI work
* Add peppol and output
* API support
* Profile integration
* Simplify form for individuals
* Remove sent_to_customer usage
* more steps
* Revert "Bank transfer: Allow to send the invoice direclty to the accounting department (#2975)"
This reverts commit cea6c340be.
* minor fixes
* Fixes after rebase
* update stati
* Backend view
* Transmit and show status
* status, retransmission
* API retransmission
* More fields
* API docs
* Plugin docs
* Update migration
* Add missing license headers
* Remove dead code, fix current tests
* Run isort
* Update regex
* Rebase migration
* Fix migration
* Add tests, fix bugs
* Rebase migration
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Apply suggestion from @luelista
Co-authored-by: luelista <weller@rami.io>
* Make migration reversible
* Add TransmissionType.enforce_transmission
* Fix registries API usage after rebase
* Remove code I forgot to delete
* Update transmission status display depending on type
* Add testmode_supported
* Update src/pretix/static/pretixbase/js/addressform.js
Co-authored-by: luelista <weller@rami.io>
* Update src/pretix/static/pretixbase/js/addressform.js
Co-authored-by: luelista <weller@rami.io>
* Update src/pretix/static/pretixbase/js/addressform.js
Co-authored-by: luelista <weller@rami.io>
* New mechanism for non-required invoice forms
* Update src/pretix/base/invoicing/transmission.py
Co-authored-by: luelista <weller@rami.io>
* Declare testmode_supported for email
* Make transmission_email_other an implementation detail
* Fix failing tests and add new ones
* Update src/pretix/base/services/invoices.py
Co-authored-by: luelista <weller@rami.io>
* Add emails to email history
* Fix comma error
* More generic default email text
* Cleanup
* Remove "email invoices" button and refine logic
* Rebase migration
* Fix edge case
---------
Co-authored-by: luelista <weller@rami.io>
This commit is contained in:
@@ -40,7 +40,6 @@ from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from i18nfield.fields import I18nFormField, I18nTextarea
|
||||
from i18nfield.forms import I18nTextInput
|
||||
@@ -49,15 +48,9 @@ from localflavor.generic.forms import BICFormField, IBANFormField
|
||||
from localflavor.generic.validators import IBANValidator
|
||||
from text_unidecode import unidecode
|
||||
|
||||
from pretix.base.email import get_available_placeholders, get_email_context
|
||||
from pretix.base.forms import PlaceholderValidator
|
||||
from pretix.base.forms.widgets import format_placeholders_help_text
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import InvoiceAddress, Order, OrderPayment, OrderRefund
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.plugins.banktransfer.templatetags.ibanformat import ibanformat
|
||||
from pretix.presale.views.cart import cart_session
|
||||
|
||||
@@ -208,48 +201,6 @@ class BankTransfer(BasePaymentProvider):
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
placeholders = get_available_placeholders(self.event, ['event', 'order', 'invoice'])
|
||||
phs_ht = format_placeholders_help_text(placeholders, self.event)
|
||||
|
||||
phs = ['{%s}' % p for p in placeholders]
|
||||
more_fields = OrderedDict([
|
||||
('invoice_email',
|
||||
forms.BooleanField(
|
||||
label=_('Allow users to enter an additional email address that the invoice will be sent to.'),
|
||||
help_text=_(
|
||||
'This requires that the invoice creation settings allow the invoice to be created right after '
|
||||
'the payment method was chosen. Only the invoice will be sent to this email address, subsequent '
|
||||
'invoice corrections will not be sent automatically. Only the invoice will be sent, no additional '
|
||||
'information.'
|
||||
),
|
||||
required=False,
|
||||
)),
|
||||
('invoice_email_subject',
|
||||
I18nFormField(
|
||||
label=_('Invoice email subject'),
|
||||
widget=I18nTextInput,
|
||||
widget_kwargs={'attrs': {
|
||||
'data-display-dependency': '#id_payment_banktransfer_invoice_email',
|
||||
'data-required-if': '#id_payment_banktransfer_invoice_email',
|
||||
}},
|
||||
validators=[PlaceholderValidator(phs)],
|
||||
help_text=phs_ht,
|
||||
required=False
|
||||
)),
|
||||
('invoice_email_text',
|
||||
I18nFormField(
|
||||
label=_('Invoice email text'),
|
||||
widget=I18nTextarea,
|
||||
widget_kwargs={'attrs': {
|
||||
'rows': '8',
|
||||
'data-display-dependency': '#id_payment_banktransfer_invoice_email',
|
||||
'data-required-if': '#id_payment_banktransfer_invoice_email',
|
||||
}},
|
||||
validators=[PlaceholderValidator(phs)],
|
||||
help_text=phs_ht,
|
||||
required=False
|
||||
)),
|
||||
])
|
||||
more_fields_first = OrderedDict([
|
||||
('_restricted_business',
|
||||
forms.BooleanField(
|
||||
@@ -263,8 +214,7 @@ class BankTransfer(BasePaymentProvider):
|
||||
d = OrderedDict(
|
||||
list(super().settings_form_fields.items()) +
|
||||
list(more_fields_first.items()) +
|
||||
list(BankTransfer.form_fields().items()) +
|
||||
list(more_fields.items())
|
||||
list(BankTransfer.form_fields().items())
|
||||
)
|
||||
d.move_to_end('invoice_immediately', last=False)
|
||||
d.move_to_end('bank_details', last=False)
|
||||
@@ -292,16 +242,6 @@ class BankTransfer(BasePaymentProvider):
|
||||
{'payment_banktransfer_bank_details': _('Please enter your bank account details.')})
|
||||
return cleaned_data
|
||||
|
||||
@cached_property
|
||||
def _invoice_email_asked(self):
|
||||
return (
|
||||
self.settings.get('invoice_email', as_type=bool) and
|
||||
(self.event.settings.invoice_generate == 'True' or (
|
||||
self.event.settings.invoice_generate == 'paid' and
|
||||
self.settings.get('invoice_immediately', as_type=bool)
|
||||
))
|
||||
)
|
||||
|
||||
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
||||
def get_invoice_address():
|
||||
if not hasattr(request, '_checkout_flow_invoice_address'):
|
||||
@@ -324,35 +264,10 @@ class BankTransfer(BasePaymentProvider):
|
||||
|
||||
return super().is_allowed(request, total)
|
||||
|
||||
@property
|
||||
def payment_form_fields(self) -> dict:
|
||||
if self._invoice_email_asked:
|
||||
return {
|
||||
'send_invoice': forms.BooleanField(
|
||||
label=_('Please additionally send my invoice directly to our accounting department'),
|
||||
required=False,
|
||||
),
|
||||
'send_invoice_to': forms.EmailField(
|
||||
label=_('Invoice recipient email'),
|
||||
required=False,
|
||||
help_text=_('The invoice recipient will receive an email which includes the invoice and your email '
|
||||
'address so they know who placed this order.'),
|
||||
widget=forms.EmailInput(
|
||||
attrs={
|
||||
'data-display-dependency': '#id_payment_banktransfer-send_invoice',
|
||||
}
|
||||
)
|
||||
),
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
|
||||
def payment_form_render(self, request, total=None, order=None) -> str:
|
||||
template = get_template('pretixplugins/banktransfer/checkout_payment_form.html')
|
||||
form = self.payment_form(request)
|
||||
ctx = {
|
||||
'request': request,
|
||||
'form': form,
|
||||
'event': self.event,
|
||||
'settings': self.settings,
|
||||
'code': self._code(order) if order else None,
|
||||
@@ -361,97 +276,16 @@ class BankTransfer(BasePaymentProvider):
|
||||
return template.render(ctx)
|
||||
|
||||
def checkout_prepare(self, request, total):
|
||||
form = self.payment_form(request)
|
||||
if form.is_valid():
|
||||
for k, v in form.cleaned_data.items():
|
||||
request.session['payment_%s_%s' % (self.identifier, k)] = v
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def send_invoice_to_alternate_email(self, order, invoice, email):
|
||||
"""
|
||||
Sends an email to the alternate invoice address.
|
||||
"""
|
||||
with language(order.locale, self.event.settings.region):
|
||||
context = get_email_context(event=self.event,
|
||||
order=order,
|
||||
invoice=invoice,
|
||||
event_or_subevent=self.event,
|
||||
invoice_address=getattr(order, 'invoice_address', None) or InvoiceAddress())
|
||||
template = self.settings.get('invoice_email_text', as_type=LazyI18nString)
|
||||
subject = self.settings.get('invoice_email_subject', as_type=LazyI18nString)
|
||||
|
||||
try:
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
email,
|
||||
subject,
|
||||
template,
|
||||
context=context,
|
||||
event=self.event,
|
||||
locale=order.locale,
|
||||
order=order,
|
||||
invoices=[invoice],
|
||||
attach_tickets=False,
|
||||
auto_email=True,
|
||||
attach_ical=False,
|
||||
plain_text_only=True,
|
||||
no_order_links=True,
|
||||
)
|
||||
except SendMailException:
|
||||
raise
|
||||
else:
|
||||
order.log_action(
|
||||
'pretix.plugins.banktransfer.order.email.invoice',
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': None,
|
||||
'recipient': email,
|
||||
'invoices': [invoice.pk],
|
||||
'attach_tickets': False,
|
||||
'attach_ical': False,
|
||||
}
|
||||
)
|
||||
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
|
||||
send_invoice = (
|
||||
self._invoice_email_asked and
|
||||
request and
|
||||
request.session.get('payment_%s_%s' % (self.identifier, 'send_invoice')) and
|
||||
request.session.get('payment_%s_%s' % (self.identifier, 'send_invoice_to'))
|
||||
)
|
||||
if send_invoice:
|
||||
recipient = request.session.get('payment_%s_%s' % (self.identifier, 'send_invoice_to'))
|
||||
payment.info_data = {
|
||||
'send_invoice_to': recipient,
|
||||
}
|
||||
payment.save(update_fields=['info'])
|
||||
i = payment.order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
self.send_invoice_to_alternate_email(payment.order, i, recipient)
|
||||
if request:
|
||||
request.session.pop('payment_%s_%s' % (self.identifier, 'send_invoice'), None)
|
||||
request.session.pop('payment_%s_%s' % (self.identifier, 'send_invoice_to'), None)
|
||||
return True
|
||||
|
||||
def payment_prepare(self, request: HttpRequest, payment: OrderPayment):
|
||||
return self.checkout_prepare(request, payment.amount)
|
||||
return True
|
||||
|
||||
def payment_is_valid_session(self, request):
|
||||
return True
|
||||
|
||||
def checkout_confirm_render(self, request, order=None):
|
||||
template = get_template('pretixplugins/banktransfer/checkout_confirm.html')
|
||||
ctx = {
|
||||
'request': request,
|
||||
'event': self.event,
|
||||
'settings': self.settings,
|
||||
'code': self._code(order) if order else None,
|
||||
'details': self.settings.get('bank_details', as_type=LazyI18nString),
|
||||
}
|
||||
return template.render(ctx)
|
||||
return self.payment_form_render(request, order=order)
|
||||
|
||||
def order_pending_mail_render(self, order, payment) -> str:
|
||||
t = gettext("Please transfer the full amount to the following bank account:")
|
||||
@@ -537,7 +371,6 @@ class BankTransfer(BasePaymentProvider):
|
||||
'pending_description': self.settings.get('pending_description', as_type=LazyI18nString),
|
||||
'details': self.settings.get('bank_details', as_type=LazyI18nString),
|
||||
'has_invoices': payment.order.invoices.exists(),
|
||||
'invoice_email_enabled': self.settings.get('invoice_email', as_type=bool),
|
||||
}
|
||||
ctx['any_barcodes'] = ctx['swiss_qrbill'] or ctx['eu_barcodes']
|
||||
return template.render(ctx, request=request)
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.translation import gettext_lazy as _, gettext_noop
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.control.signals import html_head, nav_event, nav_organizer
|
||||
@@ -31,7 +30,6 @@ from pretix.control.signals import html_head, nav_event, nav_organizer
|
||||
from ...base.logentrytypes import (
|
||||
ClearDataShredderMixin, OrderLogEntryType, log_entry_types,
|
||||
)
|
||||
from ...base.settings import settings_hierarkey
|
||||
from .payment import BankTransfer
|
||||
|
||||
|
||||
@@ -122,23 +120,6 @@ def html_head_presale(sender, request=None, **kwargs):
|
||||
|
||||
@log_entry_types.new()
|
||||
class BanktransferOrderEmailInvoiceLogEntryType(OrderLogEntryType, ClearDataShredderMixin):
|
||||
# For backwards-compatibility only
|
||||
action_type = 'pretix.plugins.banktransfer.order.email.invoice'
|
||||
plain = _('The invoice was sent to the designated email address.')
|
||||
|
||||
|
||||
settings_hierarkey.add_default(
|
||||
'payment_banktransfer_invoice_email_subject',
|
||||
default_type=LazyI18nString,
|
||||
value=LazyI18nString.from_gettext(gettext_noop("Invoice {invoice_number}"))
|
||||
)
|
||||
settings_hierarkey.add_default(
|
||||
'payment_banktransfer_invoice_email_text',
|
||||
default_type=LazyI18nString,
|
||||
value=LazyI18nString.from_gettext(gettext_noop("""Hello,
|
||||
|
||||
you receive this message because an order for {event} was placed by {order_email} and we have been asked to forward the invoice to you.
|
||||
|
||||
Best regards,
|
||||
|
||||
Your {event} team""")) # noqa: W291
|
||||
)
|
||||
|
||||
@@ -38,10 +38,3 @@
|
||||
reference code.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% if request.session.payment_banktransfer_send_invoice and request.session.payment_banktransfer_send_invoice_to %}
|
||||
<p>
|
||||
{% blocktrans trimmed with recipient=request.session.payment_banktransfer_send_invoice_to %}
|
||||
We will send a copy of your invoice directly to {{ recipient }}.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% load ibanformat %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% if details or code %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
@@ -38,8 +37,3 @@
|
||||
reference code.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% if form.fields %}
|
||||
<div class="col-md-12">
|
||||
{% bootstrap_form form layout='inline' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -133,42 +133,4 @@ SCT
|
||||
</div>
|
||||
{% if swiss_qrbill %}
|
||||
<link rel="stylesheet" href="{% static "pretixplugins/banktransfer/swisscross.css" %}">
|
||||
{% endif %}
|
||||
|
||||
{% if invoice_email_enabled and has_invoices %}
|
||||
{% if payment_info.send_invoice_to %}
|
||||
<p>
|
||||
{% blocktrans trimmed with recipient=payment_info.send_invoice_to %}
|
||||
At your request, we sent the invoice directly to {{ recipient }}.
|
||||
{% endblocktrans %}
|
||||
<a data-toggle="collapse" data-target="#payment_banktransfer_send_invoice_to" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
{% trans "Send again or somewhere else" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<form id="payment_banktransfer_send_invoice_to" method="post"
|
||||
action="{% eventurl event "plugins:banktransfer:mail_invoice" order=order.code secret=order.secret %}"
|
||||
{% if payment_info.send_invoice_to %}class="collapse"{% endif %}>
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
To send the invoice directly to your accounting department, please enter their email address:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-9 col-xs-12">
|
||||
<label for="mail_invoice_email" class="sr-only">{% trans "Invoice recipient email" %}:</label>
|
||||
<input type="email" name="email" id="mail_invoice_email" class="form-control" value="" required
|
||||
placeholder="{% trans "Email address" %}" />
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-12">
|
||||
<button class="btn btn-default btn-block">
|
||||
<span class="fa fa-envelope-o" aria-hidden="true"></span>
|
||||
{% trans "Send invoice via email" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -19,19 +19,13 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django.urls import include, re_path
|
||||
from django.urls import re_path
|
||||
|
||||
from pretix.api.urls import orga_router
|
||||
from pretix.plugins.banktransfer.api import BankImportJobViewSet
|
||||
|
||||
from . import views
|
||||
|
||||
event_patterns = [
|
||||
re_path(r'^banktransfer/', include([
|
||||
re_path(r'^(?P<order>[^/][^w]+)/(?P<secret>[A-Za-z0-9]+)/mail-invoice/$', views.SendInvoiceMailView.as_view(), name='mail_invoice'),
|
||||
])),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^control/organizer/(?P<organizer>[^/]+)/banktransfer/import/',
|
||||
views.OrganizerImportView.as_view(),
|
||||
|
||||
@@ -44,18 +44,14 @@ from typing import Set
|
||||
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Q, QuerySet
|
||||
from django.http import FileResponse, Http404, JsonResponse
|
||||
from django.http import FileResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.generic import DetailView, FormView, ListView, View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from localflavor.generic.forms import BICFormField, IBANFormField
|
||||
@@ -79,8 +75,6 @@ from pretix.plugins.banktransfer.refund_export import (
|
||||
build_sepa_xml, get_refund_export_csv,
|
||||
)
|
||||
from pretix.plugins.banktransfer.tasks import process_banktransfers
|
||||
from pretix.presale.views import EventViewMixin
|
||||
from pretix.presale.views.order import OrderDetailMixin
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.banktransfer')
|
||||
|
||||
@@ -892,42 +886,3 @@ class OrganizerSepaXMLExportView(OrganizerPermissionRequiredMixin, OrganizerDeta
|
||||
organizer=self.request.organizer,
|
||||
pk=self.kwargs.get('id')
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class SendInvoiceMailView(EventViewMixin, OrderDetailMixin, View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
if not self.order:
|
||||
raise Http404(_('Unknown order code or not authorized to access this order.'))
|
||||
try:
|
||||
validate_email(request.POST['email'])
|
||||
except ValidationError:
|
||||
messages.error(request, _('Please enter a valid email address.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
last_payment = self.order.payments.last()
|
||||
if (not last_payment
|
||||
or last_payment.provider != BankTransfer.identifier
|
||||
or last_payment.state != OrderPayment.PAYMENT_STATE_CREATED):
|
||||
messages.error(request, _('No pending bank transfer payment found. Maybe the order has been paid already?'))
|
||||
return redirect(self.get_order_url())
|
||||
if not last_payment.payment_provider.settings.get('invoice_email', as_type=bool):
|
||||
messages.error(request, _('Sending invoices via email is disabled by the event organizer.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
last_invoice = self.order.invoices.last()
|
||||
if not last_invoice:
|
||||
messages.error(request, _('No invoice found, please request an invoice first.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
provider = last_payment.payment_provider
|
||||
provider.send_invoice_to_alternate_email(self.order, last_invoice, request.POST['email'])
|
||||
|
||||
last_payment.info_data = {
|
||||
**last_payment.info_data,
|
||||
'send_invoice_to': request.POST['email'],
|
||||
}
|
||||
last_payment.save(update_fields=['info'])
|
||||
|
||||
messages.success(request, _('Sending the latest invoice via email to {email}.').format(email=request.POST['email']))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
Reference in New Issue
Block a user