From f3221e6e76889767d67c5781db5e2e217bb18941 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 7 Nov 2017 17:52:09 +0100 Subject: [PATCH] Allow attaching invoices to emails --- src/pretix/base/models/orders.py | 8 ++-- src/pretix/base/services/invoices.py | 12 +++--- src/pretix/base/services/mail.py | 41 ++++++++++++++++--- src/pretix/base/services/orders.py | 18 +++++--- src/pretix/base/settings.py | 4 ++ src/pretix/control/forms/event.py | 11 ++++- .../pretixcontrol/event/invoicing.html | 1 + 7 files changed, 76 insertions(+), 19 deletions(-) diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 0e51d4a0a..c77d6a6f8 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -368,7 +368,7 @@ class Order(LoggedModel): def send_mail(self, subject: str, template: Union[str, LazyI18nString], context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent', - user: User=None, headers: dict=None, sender: str=None): + user: User=None, headers: dict=None, sender: str=None, invoices: list=None): """ Sends an email to the user that placed this order. Basically, this method does two things: @@ -393,7 +393,8 @@ class Order(LoggedModel): email_content = render_mail(template, context)[0] mail( recipient, subject, template, context, - self.event, self.locale, self, headers, sender + self.event, self.locale, self, headers, sender, + invoices=invoices ) except SendMailException: raise @@ -404,7 +405,8 @@ class Order(LoggedModel): data={ 'subject': subject, 'message': email_content, - 'recipient': recipient + 'recipient': recipient, + 'invoices': [i.pk for i in invoices] if invoices else [] } ) diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index 50b909715..abab4ad6c 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -158,7 +158,7 @@ def build_cancellation(invoice: Invoice): return invoice -def generate_cancellation(invoice: Invoice): +def generate_cancellation(invoice: Invoice, trigger_pdf=True): cancellation = copy.copy(invoice) cancellation.pk = None cancellation.invoice_no = None @@ -170,7 +170,8 @@ def generate_cancellation(invoice: Invoice): cancellation.save() cancellation = build_cancellation(cancellation) - invoice_pdf(cancellation.pk) + if trigger_pdf: + invoice_pdf(cancellation.pk) return cancellation @@ -183,7 +184,7 @@ def regenerate_invoice(invoice: Invoice): return invoice -def generate_invoice(order: Order): +def generate_invoice(order: Order, trigger_pdf=True): locale = order.event.settings.get('invoice_language') if locale: if locale == '__user__': @@ -197,10 +198,11 @@ def generate_invoice(order: Order): locale=locale ) invoice = build_invoice(invoice) - invoice_pdf(invoice.pk) + if trigger_pdf: + invoice_pdf(invoice.pk) if order.status in (Order.STATUS_CANCELED, Order.STATUS_REFUNDED): - generate_cancellation(invoice) + generate_cancellation(invoice, trigger_pdf) return invoice diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 4eb4b7ce7..7b24d22eb 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -4,6 +4,7 @@ from typing import Any, Dict, List, Union import bleach import cssutils import markdown +from celery import chain from django.conf import settings from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import get_template @@ -12,7 +13,8 @@ from i18nfield.strings import LazyI18nString from inlinestyler.utils import inline_css from pretix.base.i18n import language -from pretix.base.models import Event, InvoiceAddress, Order +from pretix.base.models import Event, Invoice, InvoiceAddress, Order +from pretix.base.services.invoices import invoice_pdf_task from pretix.celery_app import app from pretix.multidomain.urlreverse import build_absolute_uri @@ -33,7 +35,7 @@ class SendMailException(Exception): def mail(email: str, subject: str, template: Union[str, LazyI18nString], context: Dict[str, Any]=None, event: Event=None, locale: str=None, - order: Order=None, headers: dict=None, sender: str=None): + order: Order=None, headers: dict=None, sender: str=None, invoices: list=None): """ Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation. @@ -61,6 +63,8 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString], :param sender: Set the sender email address. If not set and ``event`` is set, the event's default will be used, otherwise the system default. + :param invoices: A list of invoices to attach to this email. + :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. """ @@ -137,15 +141,42 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString], tpl = get_template('pretixbase/email/plainwrapper.html') body_html = tpl.render(htmlctx) - return mail_send([email], subject, body_plain, body_html, sender, event.id if event else None, headers) + + send_task = mail_send_task.si( + to=[email], + subject=subject, + body=body_plain, + html=body_html, + sender=sender, + event=event.id if event else None, + headers=headers, + invoices=[i.pk for i in invoices] + ) + + if invoices: + task_chain = [invoice_pdf_task.si(i.pk).on_error(send_task) for i in invoices if not i.file] + else: + task_chain = [] + + task_chain.append(send_task) + chain(*task_chain).apply_async() @app.task -def mail_send_task(to: List[str], subject: str, body: str, html: str, sender: str, - event: int=None, headers: dict=None, bcc: List[str]=None) -> bool: +def mail_send_task(*args, to: List[str], subject: str, body: str, html: str, sender: str, + event: int=None, headers: dict=None, bcc: List[str]=None, invoices: List[int]=None) -> bool: email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers) if html is not None: email.attach_alternative(inline_css(html), "text/html") + if invoices: + invoices = Invoice.objects.filter(pk__in=invoices) + for inv in invoices: + if inv.file: + email.attach( + '{}.pdf'.format(inv.number), + inv.file.file.read(), + 'application/pdf' + ) if event: event = Event.objects.get(id=event) backend = event.get_mail_backend() diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 313bf9440..32c2dac17 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -127,9 +127,13 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date }, user=user, api_token=api_token) order_paid.send(order.event, order=order) + invoice = None if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order): if not order.invoices.exists(): - generate_invoice(order) + invoice = generate_invoice( + order, + trigger_pdf=not send_mail or not order.event.settings.invoice_email_attachment + ) if send_mail: with language(order.locale): @@ -155,7 +159,8 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date try: order.send_mail( email_subject, email_template, email_context, - 'pretix.event.order.email.order_paid', user + 'pretix.event.order.email.order_paid', user, + invoices=[invoice] if invoice and order.event.settings.invoice_email_attachment else [] ) except SendMailException: logger.exception('Order paid email could not be sent') @@ -502,9 +507,11 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str], order = _create_order(event, email, positions, now_dt, pprov, locale=locale, address=addr, meta_info=meta_info) + invoice = order.invoices.last() # Might be generated by plugin already if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order): - if not order.invoices.exists(): - generate_invoice(order) + if not invoice: + invoice = generate_invoice(order, trigger_pdf=not event.settings.invoice_email_attachment) + # send_mail will trigger PDF generation later if order.total == Decimal('0.00'): email_template = event.settings.mail_text_order_free @@ -536,7 +543,8 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str], try: order.send_mail( email_subject, email_template, email_context, - log_entry + log_entry, + invoices=[invoice] if invoice and event.settings.invoice_email_attachment else [] ) except SendMailException: logger.exception('Order received email could not be sent') diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index b91d099c7..283ffa42e 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -129,6 +129,10 @@ DEFAULTS = { 'default': '__user__', 'type': str }, + 'invoice_email_attachment': { + 'default': 'False', + 'type': bool + }, 'show_items_outside_presale_period': { 'default': 'True', 'type': bool diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index a8fb9d658..1131d803f 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -544,7 +544,16 @@ class InvoiceSettingsForm(SettingsForm): ('user', _('Automatically on user request')), ('True', _('Automatically for all created orders')), ('paid', _('Automatically on payment')), - ) + ), + help_text=_("Invoices will never be automatically generated for free orders.") + ) + invoice_email_attachment = forms.BooleanField( + label=_("Attach invoices to emails"), + help_text=_("If invocies are automatically generated for all orders, they will be attached to the order " + "confirmation mail. If they are automatically generated on payment, they will be attached to the " + "payment confirmation mail. If they are not automatically generated, they will not be attached " + "to emails."), + required=False ) invoice_renderer = forms.ChoiceField( label=_("Invoice style"), diff --git a/src/pretix/control/templates/pretixcontrol/event/invoicing.html b/src/pretix/control/templates/pretixcontrol/event/invoicing.html index 9e8877283..371db824d 100644 --- a/src/pretix/control/templates/pretixcontrol/event/invoicing.html +++ b/src/pretix/control/templates/pretixcontrol/event/invoicing.html @@ -11,6 +11,7 @@ {% bootstrap_field form.invoice_address_required layout="control" %} {% bootstrap_field form.invoice_name_required layout="control" %} {% bootstrap_field form.invoice_generate layout="control" %} + {% bootstrap_field form.invoice_email_attachment layout="control" %} {% bootstrap_field form.invoice_address_vatid layout="control" %} {% bootstrap_field form.invoice_numbers_consecutive layout="control" %} {% bootstrap_field form.invoice_numbers_prefix layout="control" %}