diff --git a/src/pretix/base/email.py b/src/pretix/base/email.py index 50e14f98a4..feeded31fd 100644 --- a/src/pretix/base/email.py +++ b/src/pretix/base/email.py @@ -381,6 +381,14 @@ def base_placeholders(sender, **kwargs): SimpleFunctionalMailTextPlaceholder( 'currency', ['event'], lambda event: event.currency, lambda event: event.currency ), + SimpleFunctionalMailTextPlaceholder( + 'order_email', ['order'], lambda order: order.email, 'john@example.org' + ), + SimpleFunctionalMailTextPlaceholder( + 'invoice_number', ['invoice'], + lambda invoice: invoice.full_invoice_no, + f'{sender.settings.invoice_numbers_prefix or (sender.slug.upper() + "-")}00000' + ), SimpleFunctionalMailTextPlaceholder( 'refund_amount', ['event_or_subevent', 'refund_amount'], lambda event_or_subevent, refund_amount: LazyCurrencyNumber(refund_amount, event_or_subevent.currency), diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index c6a9e035a3..a9571923f0 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -325,16 +325,6 @@ class BasePaymentProvider: help_text=_('Users will not be able to choose this payment provider after the given date.'), required=False, )), - ('_invoice_text', - I18nFormField( - label=_('Text on invoices'), - help_text=_('Will be printed just below the payment figures and above the closing text on invoices. ' - 'This will only be used if the invoice is generated before the order is paid. If the ' - 'invoice is generated later, it will show a text stating that it has already been paid.'), - required=False, - widget=I18nTextarea, - widget_kwargs={'attrs': {'rows': '2'}} - )), ('_total_min', forms.DecimalField( label=_('Minimum order total'), @@ -382,6 +372,16 @@ class BasePaymentProvider: 'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'), required=False )), + ('_invoice_text', + I18nFormField( + label=_('Text on invoices'), + help_text=_('Will be printed just below the payment figures and above the closing text on invoices. ' + 'This will only be used if the invoice is generated before the order is paid. If the ' + 'invoice is generated later, it will show a text stating that it has already been paid.'), + required=False, + widget=I18nTextarea, + widget_kwargs={'attrs': {'rows': '2'}} + )), ('_restricted_countries', forms.MultipleChoiceField( label=_('Restrict to countries'), diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 4bb61f46c1..f15a7a6cfd 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -86,7 +86,6 @@ INVALID_ADDRESS = 'invalid-pretix-mail-address' class TolerantDict(dict): - # kept for backwards compatibility with plugins def __missing__(self, key): return key @@ -100,7 +99,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None, position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None, customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None, - attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None): + attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None, + plain_text_only=False, no_order_links=False): """ Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation. @@ -150,12 +150,21 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La :param attach_other_files: A list of file paths on our storage to attach. + :param plain_text_only: If set to ``True``, rendering a HTML version will be skipped. + + :param no_order_links: If set to ``True``, no link to the order confirmation page will be auto-appended. Currently + only allowed to use together with ``plain_text_only`` since HTML renderers add their own + links. + :raises MailOrderException: on obvious, immediate failures. Not raising an exception does not necessarily mean that the email has been sent, just that it has been queued by the email backend. """ if email == INVALID_ADDRESS: return + if no_order_links and not plain_text_only: + raise ValueError('If you set no_order_links, you also need to set plain_text_only.') + headers = headers or {} if auto_email: headers['X-Auto-Response-Suppress'] = 'OOF, NRN, AutoReply, RN' @@ -179,7 +188,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La }) renderer = ClassicMailRenderer(None, organizer) content_plain = body_plain = render_mail(template, context) - subject = format_map(str(subject), context) + subject = str(subject).format_map(TolerantDict(context)) sender = ( sender or (event.settings.get('mail_from') if event else None) or @@ -244,7 +253,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La if order and order.testmode: subject = "[TESTMODE] " + subject - if order and position: + if order and position and not no_order_links: body_plain += _( "You are receiving this email because someone placed an order for {event} for you." ).format(event=event.name) @@ -260,7 +269,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La } ) ) - elif order: + elif order and not no_order_links: body_plain += _( "You are receiving this email because you placed an order for {event}." ).format(event=event.name) @@ -280,7 +289,9 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La with override(timezone): try: - if 'position' in inspect.signature(renderer.render).parameters: + if plain_text_only: + body_html = None + elif 'position' in inspect.signature(renderer.render).parameters: body_html = renderer.render(content_plain, signature, raw_subject, order, position) else: # Backwards compatibility diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py index 86d1fcadcd..e856ec02c2 100644 --- a/src/pretix/plugins/banktransfer/payment.py +++ b/src/pretix/plugins/banktransfer/payment.py @@ -41,6 +41,7 @@ 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,8 +50,13 @@ 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.i18n import language from pretix.base.models import Order, OrderPayment, OrderRefund from pretix.base.payment import BasePaymentProvider +from pretix.base.services.mail import SendMailException, mail, render_mail +from pretix.helpers.format import format_map from pretix.plugins.banktransfer.templatetags.ibanformat import ibanformat @@ -81,7 +87,8 @@ class BankTransfer(BasePaymentProvider): )), ('bank_details_sepa_name', forms.CharField( label=_('Name of account holder'), - help_text=_('Please note: special characters other than letters, numbers, and some punctuation can cause problems with some banks.'), + help_text=_( + 'Please note: special characters other than letters, numbers, and some punctuation can cause problems with some banks.'), widget=forms.TextInput( attrs={ 'data-display-dependency': '#id_payment_banktransfer_bank_details_type_0', @@ -166,12 +173,15 @@ class BankTransfer(BasePaymentProvider): help_text=_('This text will be shown on the order confirmation page for pending orders in addition to ' 'the standard text.'), widget=I18nTextarea, + widget_kwargs={'attrs': { + 'rows': '2', + }}, required=False, )), ('refund_iban_blocklist', forms.CharField( label=_('IBAN blocklist for refunds'), required=False, - widget=forms.Textarea, + widget=forms.Textarea(attrs={'rows': 4}), help_text=_('Put one IBAN or IBAN prefix per line. The system will not attempt to send refunds to any ' 'of these IBANs. Useful e.g. if you receive a lot of "forwarded payments" by a third-party payment ' 'provider. You can also list country codes such as "GB" if you never want to send refunds to ' @@ -194,7 +204,54 @@ class BankTransfer(BasePaymentProvider): @property def settings_form_fields(self): - d = OrderedDict(list(super().settings_form_fields.items()) + list(BankTransfer.form_fields().items())) + phs = [ + '{%s}' % p + for p in sorted(get_available_placeholders(self.event, ['event', 'order', 'invoice']).keys()) + ] + phs_ht = _('Available placeholders: {list}').format( + list=', '.join(phs) + ) + 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 + )), + ]) + + d = OrderedDict(list(super().settings_form_fields.items()) + list(BankTransfer.form_fields().items()) + list(more_fields.items())) + d.move_to_end('invoice_immediately', last=False) d.move_to_end('bank_details', last=False) d.move_to_end('bank_details_sepa_bank', last=False) d.move_to_end('bank_details_sepa_bic', last=False) @@ -207,7 +264,10 @@ class BankTransfer(BasePaymentProvider): def settings_form_clean(self, cleaned_data): if cleaned_data.get('payment_banktransfer_bank_details_type') == 'sepa': - for f in ('bank_details_sepa_name', 'bank_details_sepa_bank', 'bank_details_sepa_bic', 'bank_details_sepa_iban'): + for f in ( + 'bank_details_sepa_name', 'bank_details_sepa_bank', 'bank_details_sepa_bic', + 'bank_details_sepa_iban' + ): if not cleaned_data.get('payment_banktransfer_%s' % f): raise ValidationError( {'payment_banktransfer_%s' % f: _('Please fill out your bank account details.')}) @@ -217,10 +277,45 @@ 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) + )) + ) + + @property + def payment_form_fields(self) -> dict: + if self._invoice_email_asked: + return { + 'send_invoice': forms.BooleanField( + label=_('Please send my invoice directly to our accounting department'), + required=False, + ), + 'send_invoice_to': forms.EmailField( + label=_('Invoice recipient e-mail'), + 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, @@ -229,16 +324,97 @@ class BankTransfer(BasePaymentProvider): return template.render(ctx) def checkout_prepare(self, request, total): - return True + 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=order.invoice_address) + 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) def payment_prepare(self, request: HttpRequest, payment: OrderPayment): - return True + return self.checkout_prepare(request, payment.amount) def payment_is_valid_session(self, request): return True def checkout_confirm_render(self, request, order=None): - return self.payment_form_render(request, order=order) + 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) def order_pending_mail_render(self, order, payment) -> str: template = get_template('pretixplugins/banktransfer/email/order_pending.txt') @@ -355,6 +531,8 @@ class BankTransfer(BasePaymentProvider): d = obj.info_data d['reference'] = '█' d['payer'] = '█' + if 'send_invoice_to' in d: + d['send_invoice_to'] = '█' d['_shredded'] = True obj.info = json.dumps(d) obj.save(update_fields=['info']) diff --git a/src/pretix/plugins/banktransfer/signals.py b/src/pretix/plugins/banktransfer/signals.py index 6c2affb68c..8bc8dddbe6 100644 --- a/src/pretix/plugins/banktransfer/signals.py +++ b/src/pretix/plugins/banktransfer/signals.py @@ -22,11 +22,13 @@ 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 _ +from django.utils.translation import gettext_lazy as _, gettext_noop +from i18nfield.strings import LazyI18nString -from pretix.base.signals import register_payment_providers +from pretix.base.signals import logentry_display, register_payment_providers from pretix.control.signals import html_head, nav_event, nav_organizer +from ...base.settings import settings_hierarkey from .payment import BankTransfer @@ -113,3 +115,30 @@ def html_head_presale(sender, request=None, **kwargs): return template.render({}) else: return "" + + +@receiver(signal=logentry_display) +def pretixcontrol_logentry_display(sender, logentry, **kwargs): + plains = { + 'pretix.plugins.banktransfer.order.email.invoice': _('The invoice was sent to the designated email address.'), + } + if logentry.action_type in plains: + return plains[logentry.action_type] + + +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""")) +) diff --git a/src/pretix/plugins/banktransfer/tasks.py b/src/pretix/plugins/banktransfer/tasks.py index 73718dfbaa..cfe6f5b863 100644 --- a/src/pretix/plugins/banktransfer/tasks.py +++ b/src/pretix/plugins/banktransfer/tasks.py @@ -206,6 +206,7 @@ def _handle_transaction(trans: BankTransaction, matches: tuple, event: Event = N ).last() p.info_data = { + **p.info_data, 'reference': trans.reference, 'date': trans.date_parsed.isoformat() if trans.date_parsed else trans.date, 'payer': trans.payer, diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_confirm.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_confirm.html new file mode 100644 index 0000000000..7c2ac0e882 --- /dev/null +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_confirm.html @@ -0,0 +1,39 @@ +{% load i18n %} +{% load ibanformat %} +{% load bootstrap3 %} + +
{% blocktrans trimmed %} + After completing your purchase, we will ask you to transfer the money to the following + bank account, using a personal reference code: +{% endblocktrans %}
+ +{% if settings.bank_details_type == "sepa" %} ++ + {% trans "We will assign you a personal reference code to use after you completed the order." %} + +
+{% endif %} +{% if request.session.payment_banktransfer_send_invoice and request.session.payment_banktransfer_send_invoice_to %} ++ {% blocktrans trimmed with recipient=request.session.payment_banktransfer_send_invoice_to %} + We will send a copy of your invoice directly to {{ recipient }}. + {% endblocktrans %} +
+{% endif %} diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html index 3767c880d8..b39c04acd7 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/checkout_payment_form.html @@ -1,5 +1,6 @@ {% load i18n %} {% load ibanformat %} +{% load bootstrap3 %}{% blocktrans trimmed %} After completing your purchase, we will ask you to transfer the money to the following @@ -29,3 +30,8 @@
{% endif %} +{% if form.fields %} +