forked from CGM_Public/pretix_original
Track if invoices have been sent via email (#2231)
This commit is contained in:
21
src/pretix/base/migrations/0198_invoice_sent_to_customer.py
Normal file
21
src/pretix/base/migrations/0198_invoice_sent_to_customer.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 3.2.4 on 2021-09-30 10:25
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from pytz import UTC
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0197_auto_20210914_0814'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='sent_to_customer',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, default=UTC.localize(datetime(1970, 1, 1, 0, 0, 0, 0))),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -159,6 +159,8 @@ class Invoice(models.Model):
|
|||||||
# False: The invoice wasn't sent and never will, because sending was not configured at the time of the check.
|
# False: The invoice wasn't sent and never will, because sending was not configured at the time of the check.
|
||||||
sent_to_organizer = models.BooleanField(null=True, blank=True)
|
sent_to_organizer = models.BooleanField(null=True, blank=True)
|
||||||
|
|
||||||
|
sent_to_customer = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(organizer='event__organizer')
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
|
|||||||
cancellation.payment_provider_text = ''
|
cancellation.payment_provider_text = ''
|
||||||
cancellation.file = None
|
cancellation.file = None
|
||||||
cancellation.sent_to_organizer = None
|
cancellation.sent_to_organizer = None
|
||||||
|
cancellation.sent_to_customer = None
|
||||||
with language(invoice.locale, invoice.event.settings.region):
|
with language(invoice.locale, invoice.event.settings.region):
|
||||||
cancellation.invoice_from = invoice.event.settings.get('invoice_address_from')
|
cancellation.invoice_from = invoice.event.settings.get('invoice_address_from')
|
||||||
cancellation.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
|
cancellation.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
|
||||||
@@ -346,8 +347,8 @@ def invoice_pdf_task(invoice: int):
|
|||||||
i.file.delete()
|
i.file.delete()
|
||||||
with language(i.locale, i.event.settings.region):
|
with language(i.locale, i.event.settings.region):
|
||||||
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
|
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
|
||||||
i.file.save(fname, ContentFile(fcontent))
|
i.file.save(fname, ContentFile(fcontent), save=False)
|
||||||
i.save()
|
i.save(update_fields=['file'])
|
||||||
return i.file.name
|
return i.file.name
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ from django.core.mail import (
|
|||||||
from django.core.mail.message import SafeMIMEText
|
from django.core.mail.message import SafeMIMEText
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.timezone import override
|
from django.utils.timezone import now, override
|
||||||
from django.utils.translation import gettext as _, pgettext
|
from django.utils.translation import gettext as _, pgettext
|
||||||
from django_scopes import scope, scopes_disabled
|
from django_scopes import scope, scopes_disabled
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
@@ -438,6 +438,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
|
|
||||||
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
|
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
|
||||||
|
|
||||||
|
invoices_sent = []
|
||||||
if invoices:
|
if invoices:
|
||||||
invoices = Invoice.objects.filter(pk__in=invoices)
|
invoices = Invoice.objects.filter(pk__in=invoices)
|
||||||
for inv in invoices:
|
for inv in invoices:
|
||||||
@@ -449,6 +450,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
inv.file.file.read(),
|
inv.file.file.read(),
|
||||||
'application/pdf'
|
'application/pdf'
|
||||||
)
|
)
|
||||||
|
invoices_sent.append(inv)
|
||||||
except:
|
except:
|
||||||
logger.exception('Could not attach invoice to email')
|
logger.exception('Could not attach invoice to email')
|
||||||
pass
|
pass
|
||||||
@@ -558,6 +560,10 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
|
|||||||
)
|
)
|
||||||
logger.exception('Error sending email')
|
logger.exception('Error sending email')
|
||||||
raise SendMailException('Failed to send an email to {}.'.format(to))
|
raise SendMailException('Failed to send an email to {}.'.format(to))
|
||||||
|
else:
|
||||||
|
for i in invoices_sent:
|
||||||
|
i.sent_to_customer = now()
|
||||||
|
i.save(update_fields=['sent_to_customer'])
|
||||||
|
|
||||||
|
|
||||||
def mail_send(*args, **kwargs):
|
def mail_send(*args, **kwargs):
|
||||||
|
|||||||
@@ -234,7 +234,23 @@
|
|||||||
{% for i in invoices %}
|
{% for i in invoices %}
|
||||||
<a href="{% url "control:event.invoice.download" invoice=i.pk event=request.event.slug organizer=request.event.organizer.slug %}">
|
<a href="{% url "control:event.invoice.download" invoice=i.pk event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||||
{% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %}
|
{% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %}
|
||||||
{{ i.number }}</a> ({{ i.date|date:"SHORT_DATE_FORMAT" }})
|
{{ i.number }}</a>
|
||||||
|
({{ i.date|date:"SHORT_DATE_FORMAT" }})
|
||||||
|
{% if i.sent_to_customer.year == 1970 %}
|
||||||
|
<span class="fa-stack fa-stack-small" data-toggle="tooltip" title="{% trans "We don't know if this invoice was emailed to the customer since it was created before our system tracked this information" %}">
|
||||||
|
<span class="fa fa-background fa-envelope text-muted fa-stack-1x"></span>
|
||||||
|
<span class="fa fa-question fa-stack-1x fa-stack-shifted"></span>
|
||||||
|
</span>
|
||||||
|
{% elif i.sent_to_customer %}
|
||||||
|
<span class="fa-stack fa-stack-small" data-toggle="tooltip" title="{% trans "Invoice was emailed to customer" %}">
|
||||||
|
<span class="fa fa-background fa-envelope text-muted fa-stack-1x"></span>
|
||||||
|
<span class="fa fa-check text-success fa-stack-1x fa-stack-shifted"></span>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="fa-stack fa-stack-small" data-toggle="tooltip" title="{% trans "Invoice was not yet emailed to customer" %}">
|
||||||
|
<span class="fa fa-background fa-envelope text-muted fa-stack-1x"></span>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
{% if not i.canceled %}
|
{% if not i.canceled %}
|
||||||
{% if request.event.settings.invoice_regenerate_allowed %}
|
{% if request.event.settings.invoice_regenerate_allowed %}
|
||||||
<form class="form-inline helper-display-inline" method="post"
|
<form class="form-inline helper-display-inline" method="post"
|
||||||
@@ -268,6 +284,12 @@
|
|||||||
<br/>
|
<br/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if invoices_send_link %}
|
||||||
|
<br/>
|
||||||
|
<a class="btn btn-default btn-xs" href="{{ invoices_send_link }}">
|
||||||
|
{% trans "Email invoices" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if can_generate_invoice %}
|
{% if can_generate_invoice %}
|
||||||
<br/>
|
<br/>
|
||||||
<form class="form-inline helper-display-inline" method="post"
|
<form class="form-inline helper-display-inline" method="post"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ from django.utils.formats import date_format, get_format
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _, ngettext
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
DetailView, FormView, ListView, TemplateView, View,
|
DetailView, FormView, ListView, TemplateView, View,
|
||||||
)
|
)
|
||||||
@@ -313,6 +313,27 @@ class OrderDetail(OrderView):
|
|||||||
ctx['download_buttons'] = self.download_buttons
|
ctx['download_buttons'] = self.download_buttons
|
||||||
ctx['payment_refund_sum'] = self.order.payment_refund_sum
|
ctx['payment_refund_sum'] = self.order.payment_refund_sum
|
||||||
ctx['pending_sum'] = self.order.pending_sum
|
ctx['pending_sum'] = self.order.pending_sum
|
||||||
|
|
||||||
|
unsent_invoices = [ii.pk for ii in ctx['invoices'] if not ii.sent_to_customer]
|
||||||
|
if unsent_invoices:
|
||||||
|
ctx['invoices_send_link'] = reverse('control:event.order.sendmail', kwargs={
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
'code': self.order.code
|
||||||
|
}) + '?' + urlencode({
|
||||||
|
'subject': ngettext('Your invoice', 'Your invoices', len(unsent_invoices)),
|
||||||
|
'message': ngettext(
|
||||||
|
'Hello,\n\nplease find your invoice attached to this email.\n\n'
|
||||||
|
'Your {event} team',
|
||||||
|
'Hello,\n\nplease find your invoices attached to this email.\n\n'
|
||||||
|
'Your {event} team',
|
||||||
|
len(unsent_invoices)
|
||||||
|
).format(
|
||||||
|
event="{event}",
|
||||||
|
),
|
||||||
|
'attach_invoices': unsent_invoices
|
||||||
|
}, doseq=True)
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@@ -1954,6 +1975,8 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
|||||||
kwargs['initial']['subject'] = self.request.GET.get('subject')
|
kwargs['initial']['subject'] = self.request.GET.get('subject')
|
||||||
if self.request.GET.get('message'):
|
if self.request.GET.get('message'):
|
||||||
kwargs['initial']['message'] = self.request.GET.get('message')
|
kwargs['initial']['message'] = self.request.GET.get('message')
|
||||||
|
if self.request.GET.getlist('attach_invoices'):
|
||||||
|
kwargs['initial']['attach_invoices'] = self.order.invoices.filter(pk__in=self.request.GET.getlist('attach_invoices'))
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
|
|||||||
@@ -203,6 +203,19 @@ svg.svg-icon {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fa-background {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-stack-small {
|
||||||
|
width: 1.6em;
|
||||||
|
height: 1em;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
.fa-stack-shifted {
|
||||||
|
left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
@include table-row-variant('success', lighten($brand-success, 40%));
|
@include table-row-variant('success', lighten($brand-success, 40%));
|
||||||
@include table-row-variant('info', lighten($brand-info, 30%));
|
@include table-row-variant('info', lighten($brand-info, 30%));
|
||||||
@include table-row-variant('warning', lighten($brand-warning, 40%));
|
@include table-row-variant('warning', lighten($brand-warning, 40%));
|
||||||
|
|||||||
Reference in New Issue
Block a user