Allow attaching invoices to emails

This commit is contained in:
Raphael Michel
2017-11-07 17:52:09 +01:00
parent 7649fa11d3
commit f3221e6e76
7 changed files with 76 additions and 19 deletions

View File

@@ -368,7 +368,7 @@ class Order(LoggedModel):
def send_mail(self, subject: str, template: Union[str, LazyI18nString], def send_mail(self, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent', 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: 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] email_content = render_mail(template, context)[0]
mail( mail(
recipient, subject, template, context, recipient, subject, template, context,
self.event, self.locale, self, headers, sender self.event, self.locale, self, headers, sender,
invoices=invoices
) )
except SendMailException: except SendMailException:
raise raise
@@ -404,7 +405,8 @@ class Order(LoggedModel):
data={ data={
'subject': subject, 'subject': subject,
'message': email_content, 'message': email_content,
'recipient': recipient 'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else []
} }
) )

View File

@@ -158,7 +158,7 @@ def build_cancellation(invoice: Invoice):
return invoice return invoice
def generate_cancellation(invoice: Invoice): def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation = copy.copy(invoice) cancellation = copy.copy(invoice)
cancellation.pk = None cancellation.pk = None
cancellation.invoice_no = None cancellation.invoice_no = None
@@ -170,7 +170,8 @@ def generate_cancellation(invoice: Invoice):
cancellation.save() cancellation.save()
cancellation = build_cancellation(cancellation) cancellation = build_cancellation(cancellation)
invoice_pdf(cancellation.pk) if trigger_pdf:
invoice_pdf(cancellation.pk)
return cancellation return cancellation
@@ -183,7 +184,7 @@ def regenerate_invoice(invoice: Invoice):
return invoice return invoice
def generate_invoice(order: Order): def generate_invoice(order: Order, trigger_pdf=True):
locale = order.event.settings.get('invoice_language') locale = order.event.settings.get('invoice_language')
if locale: if locale:
if locale == '__user__': if locale == '__user__':
@@ -197,10 +198,11 @@ def generate_invoice(order: Order):
locale=locale locale=locale
) )
invoice = build_invoice(invoice) 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): if order.status in (Order.STATUS_CANCELED, Order.STATUS_REFUNDED):
generate_cancellation(invoice) generate_cancellation(invoice, trigger_pdf)
return invoice return invoice

View File

@@ -4,6 +4,7 @@ from typing import Any, Dict, List, Union
import bleach import bleach
import cssutils import cssutils
import markdown import markdown
from celery import chain
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import get_template from django.template.loader import get_template
@@ -12,7 +13,8 @@ from i18nfield.strings import LazyI18nString
from inlinestyler.utils import inline_css from inlinestyler.utils import inline_css
from pretix.base.i18n import language 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.celery_app import app
from pretix.multidomain.urlreverse import build_absolute_uri 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], def mail(email: str, subject: str, template: Union[str, LazyI18nString],
context: Dict[str, Any]=None, event: Event=None, locale: str=None, 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. 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, :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. 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 :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. 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') tpl = get_template('pretixbase/email/plainwrapper.html')
body_html = tpl.render(htmlctx) 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 @app.task
def mail_send_task(to: List[str], subject: str, body: str, html: str, sender: str, 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) -> bool: 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) email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
if html is not None: if html is not None:
email.attach_alternative(inline_css(html), "text/html") 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: if event:
event = Event.objects.get(id=event) event = Event.objects.get(id=event)
backend = event.get_mail_backend() backend = event.get_mail_backend()

View File

@@ -127,9 +127,13 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
}, user=user, api_token=api_token) }, user=user, api_token=api_token)
order_paid.send(order.event, order=order) order_paid.send(order.event, order=order)
invoice = None
if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order): if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order):
if not order.invoices.exists(): 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: if send_mail:
with language(order.locale): with language(order.locale):
@@ -155,7 +159,8 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
try: try:
order.send_mail( order.send_mail(
email_subject, email_template, email_context, 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: except SendMailException:
logger.exception('Order paid email could not be sent') 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, order = _create_order(event, email, positions, now_dt, pprov,
locale=locale, address=addr, meta_info=meta_info) 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 event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
if not order.invoices.exists(): if not invoice:
generate_invoice(order) 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'): if order.total == Decimal('0.00'):
email_template = event.settings.mail_text_order_free 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: try:
order.send_mail( order.send_mail(
email_subject, email_template, email_context, email_subject, email_template, email_context,
log_entry log_entry,
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else []
) )
except SendMailException: except SendMailException:
logger.exception('Order received email could not be sent') logger.exception('Order received email could not be sent')

View File

@@ -129,6 +129,10 @@ DEFAULTS = {
'default': '__user__', 'default': '__user__',
'type': str 'type': str
}, },
'invoice_email_attachment': {
'default': 'False',
'type': bool
},
'show_items_outside_presale_period': { 'show_items_outside_presale_period': {
'default': 'True', 'default': 'True',
'type': bool 'type': bool

View File

@@ -544,7 +544,16 @@ class InvoiceSettingsForm(SettingsForm):
('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')),
) ),
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( invoice_renderer = forms.ChoiceField(
label=_("Invoice style"), label=_("Invoice style"),

View File

@@ -11,6 +11,7 @@
{% bootstrap_field form.invoice_address_required layout="control" %} {% bootstrap_field form.invoice_address_required layout="control" %}
{% bootstrap_field form.invoice_name_required layout="control" %} {% bootstrap_field form.invoice_name_required layout="control" %}
{% bootstrap_field form.invoice_generate 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_address_vatid layout="control" %}
{% bootstrap_field form.invoice_numbers_consecutive layout="control" %} {% bootstrap_field form.invoice_numbers_consecutive layout="control" %}
{% bootstrap_field form.invoice_numbers_prefix layout="control" %} {% bootstrap_field form.invoice_numbers_prefix layout="control" %}