mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
Allow to disable some e-mails depending on sales channel (#1726)
Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
@@ -928,7 +928,8 @@ Creating orders
|
|||||||
during order generation and is not respected automatically when the order changes later.)
|
during order generation and is not respected automatically when the order changes later.)
|
||||||
|
|
||||||
* ``force`` (optional). If set to ``true``, quotas will be ignored.
|
* ``force`` (optional). If set to ``true``, quotas will be ignored.
|
||||||
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order. Defaults to
|
* ``send_mail`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of
|
||||||
|
whether these emails are enabled for certain sales channels. Defaults to
|
||||||
``false``.
|
``false``.
|
||||||
|
|
||||||
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ gunicorn
|
|||||||
guid
|
guid
|
||||||
hardcoded
|
hardcoded
|
||||||
hostname
|
hostname
|
||||||
|
ics
|
||||||
idempotency
|
idempotency
|
||||||
iframe
|
iframe
|
||||||
incrementing
|
incrementing
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ Sender address
|
|||||||
we strongly recommend to use the SMTP settings below as well, otherwise your e-mails might be detected as spam
|
we strongly recommend to use the SMTP settings below as well, otherwise your e-mails might be detected as spam
|
||||||
due to the `Sender Policy Framework`_ and similar mechanisms.
|
due to the `Sender Policy Framework`_ and similar mechanisms.
|
||||||
|
|
||||||
|
Sender name
|
||||||
|
This is the name associated with the sender address. By default, this is your event name.
|
||||||
|
|
||||||
Signature
|
Signature
|
||||||
This text will be appended to all e-mails in form of a signature. This might be useful e.g. to add your contact
|
This text will be appended to all e-mails in form of a signature. This might be useful e.g. to add your contact
|
||||||
details or any legal information that needs to be included with the e-mails.
|
details or any legal information that needs to be included with the e-mails.
|
||||||
@@ -33,6 +36,15 @@ Signature
|
|||||||
Bcc address
|
Bcc address
|
||||||
This email address will receive a copy of every event-related email.
|
This email address will receive a copy of every event-related email.
|
||||||
|
|
||||||
|
Attach calendar files
|
||||||
|
With this option, every order confirmation mail will include an ics file with name, date and location of
|
||||||
|
your event. It can be imported into many digital calendars.
|
||||||
|
|
||||||
|
Sales Channels for Checkout Emails
|
||||||
|
When you are using multiple sales channel, you may want to decide that mails for order and payment confirmation
|
||||||
|
are only to be sent for some sales channels. For orders created through the default online shop, these emails
|
||||||
|
must always be send. A similar option is available for ticket download reminders.
|
||||||
|
|
||||||
E-mail design
|
E-mail design
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|||||||
39
src/pretix/base/migrations/0159_mails_by_sales_channel.py
Normal file
39
src/pretix/base/migrations/0159_mails_by_sales_channel.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 3.0.8 on 2020-07-24 09:05
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from pretix.base.channels import get_all_sales_channels
|
||||||
|
|
||||||
|
def set_sales_channels(apps, schema_editor):
|
||||||
|
# We now allow restricting some mails to certain sales channels
|
||||||
|
# The default is changing from all channels to "web" only
|
||||||
|
# Therefore, for existing events, we enable all sales channels
|
||||||
|
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
||||||
|
Event = apps.get_model('pretixbase', 'Event')
|
||||||
|
all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channels()) + "]"
|
||||||
|
batch_size = 1000
|
||||||
|
Event_SettingsStore.objects.bulk_create([
|
||||||
|
Event_SettingsStore(
|
||||||
|
object=event,
|
||||||
|
key="mail_sales_channel_placed_paid",
|
||||||
|
value=all_sales_channels)
|
||||||
|
for event in Event.objects.all()
|
||||||
|
], batch_size=batch_size)
|
||||||
|
Event_SettingsStore.objects.bulk_create([
|
||||||
|
Event_SettingsStore(
|
||||||
|
object=event,
|
||||||
|
key="mail_sales_channel_download_reminder",
|
||||||
|
value=all_sales_channels)
|
||||||
|
for event in Event.objects.all()
|
||||||
|
], batch_size=batch_size)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0158_auto_20200724_0754'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(set_sales_channels, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -1491,7 +1491,7 @@ class OrderPayment(models.Model):
|
|||||||
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
|
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
|
||||||
)
|
)
|
||||||
|
|
||||||
if send_mail:
|
if send_mail and self.order.sales_channel in self.order.event.settings.mail_sales_channel_placed_paid:
|
||||||
self._send_paid_mail(invoice, user, mail_text)
|
self._send_paid_mail(invoice, user, mail_text)
|
||||||
if self.order.event.settings.mail_send_order_paid_attendee:
|
if self.order.event.settings.mail_send_order_paid_attendee:
|
||||||
for p in self.order.positions.all():
|
for p in self.order.positions.all():
|
||||||
|
|||||||
@@ -960,11 +960,12 @@ def _perform_order(event: Event, payment_provider: str, position_ids: List[str],
|
|||||||
email_attendees = event.settings.mail_send_order_placed_attendee
|
email_attendees = event.settings.mail_send_order_placed_attendee
|
||||||
email_attendees_template = event.settings.mail_text_order_placed_attendee
|
email_attendees_template = event.settings.mail_text_order_placed_attendee
|
||||||
|
|
||||||
_order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment)
|
if sales_channel in event.settings.mail_sales_channel_placed_paid:
|
||||||
if email_attendees:
|
_order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment)
|
||||||
for p in order.positions.all():
|
if email_attendees:
|
||||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
for p in order.positions.all():
|
||||||
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry)
|
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||||
|
_order_placed_email_attendee(event, order, p, email_attendees_template, log_entry)
|
||||||
|
|
||||||
return order.id
|
return order.id
|
||||||
|
|
||||||
@@ -1056,6 +1057,9 @@ def send_download_reminders(sender, **kwargs):
|
|||||||
if days is None:
|
if days is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if o.sales_channel not in event.settings.mail_sales_channel_download_reminder:
|
||||||
|
continue
|
||||||
|
|
||||||
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
if now() < reminder_date or o.datetime > reminder_date:
|
if now() < reminder_date or o.datetime > reminder_date:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1190,6 +1190,14 @@ DEFAULTS = {
|
|||||||
"Defaults to your event name."),
|
"Defaults to your event name."),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'mail_sales_channel_placed_paid': {
|
||||||
|
'default': ['web'],
|
||||||
|
'type': list,
|
||||||
|
},
|
||||||
|
'mail_sales_channel_download_reminder': {
|
||||||
|
'default': ['web'],
|
||||||
|
'type': list,
|
||||||
|
},
|
||||||
'mail_text_signature': {
|
'mail_text_signature': {
|
||||||
'type': LazyI18nString,
|
'type': LazyI18nString,
|
||||||
'default': ""
|
'default': ""
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from urllib.parse import urlencode, urlparse
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -17,6 +15,7 @@ from i18nfield.forms import (
|
|||||||
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
|
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
|
||||||
)
|
)
|
||||||
from pytz import common_timezones, timezone
|
from pytz import common_timezones, timezone
|
||||||
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
from pretix.base.email import get_available_placeholders
|
from pretix.base.email import get_available_placeholders
|
||||||
@@ -750,6 +749,11 @@ def multimail_validate(val):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def contains_web_channel_validate(val):
|
||||||
|
if "web" not in val:
|
||||||
|
raise ValidationError(_("The online shop must be selected to receive these emails."))
|
||||||
|
|
||||||
|
|
||||||
class MailSettingsForm(SettingsForm):
|
class MailSettingsForm(SettingsForm):
|
||||||
auto_fields = [
|
auto_fields = [
|
||||||
'mail_prefix',
|
'mail_prefix',
|
||||||
@@ -758,6 +762,27 @@ class MailSettingsForm(SettingsForm):
|
|||||||
'mail_attach_ical',
|
'mail_attach_ical',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
mail_sales_channel_placed_paid = forms.MultipleChoiceField(
|
||||||
|
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
|
||||||
|
label=_('Sales channels for checkout emails'),
|
||||||
|
help_text=_('The order placed and paid emails will only be send to orders from these sales channels. '
|
||||||
|
'The online shop must be enabled.'),
|
||||||
|
widget=forms.CheckboxSelectMultiple(
|
||||||
|
attrs={'class': 'scrolling-multiple-choice'}
|
||||||
|
),
|
||||||
|
validators=[contains_web_channel_validate],
|
||||||
|
)
|
||||||
|
|
||||||
|
mail_sales_channel_download_reminder = forms.MultipleChoiceField(
|
||||||
|
choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
|
||||||
|
label=_('Sales channels'),
|
||||||
|
help_text=_('This email will only be send to orders from these sales channels. The online shop must be enabled.'),
|
||||||
|
widget=forms.CheckboxSelectMultiple(
|
||||||
|
attrs={'class': 'scrolling-multiple-choice'}
|
||||||
|
),
|
||||||
|
validators=[contains_web_channel_validate],
|
||||||
|
)
|
||||||
|
|
||||||
mail_bcc = forms.CharField(
|
mail_bcc = forms.CharField(
|
||||||
label=_("Bcc address"),
|
label=_("Bcc address"),
|
||||||
help_text=_("All emails will be sent to this address as a Bcc copy"),
|
help_text=_("All emails will be sent to this address as a Bcc copy"),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
{% bootstrap_field form.mail_text_signature layout="control" %}
|
{% bootstrap_field form.mail_text_signature layout="control" %}
|
||||||
{% bootstrap_field form.mail_bcc layout="control" %}
|
{% bootstrap_field form.mail_bcc layout="control" %}
|
||||||
{% bootstrap_field form.mail_attach_ical layout="control" %}
|
{% bootstrap_field form.mail_attach_ical layout="control" %}
|
||||||
|
{% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "E-mail design" %}</legend>
|
<legend>{% trans "E-mail design" %}</legend>
|
||||||
@@ -70,7 +71,7 @@
|
|||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="custom_mail" title=title_order_custom_mail items="mail_text_order_custom_mail" %}
|
||||||
|
|
||||||
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
|
{% blocktrans asvar title_download_tickets_reminder %}Reminder to download tickets{% endblocktrans %}
|
||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_download_tickets_reminder items="mail_days_download_reminder,mail_text_download_reminder,mail_send_download_reminder_attendee,mail_text_download_reminder_attendee,mail_sales_channel_download_reminder" exclude="mail_days_download_reminder,mail_send_download_reminder_attendee,mail_sales_channel_download_reminder" %}
|
||||||
|
|
||||||
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
|
{% blocktrans asvar title_require_approval %}Order approval process{% endblocktrans %}
|
||||||
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}
|
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="ticket_reminder" title=title_require_approval items="mail_text_order_placed_require_approval,mail_text_order_approved,mail_text_order_denied" %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import Quota
|
from pretix.base.models import Quota
|
||||||
from pretix.base.models.tax import TaxedPrice
|
from pretix.base.models.tax import TaxedPrice
|
||||||
|
|||||||
@@ -543,6 +543,13 @@ class DownloadReminderTests(TestCase):
|
|||||||
send_download_reminders(sender=self.event)
|
send_download_reminders(sender=self.event)
|
||||||
assert len(djmail.outbox) == 0
|
assert len(djmail.outbox) == 0
|
||||||
|
|
||||||
|
@classscope(attr='o')
|
||||||
|
def test_not_sent_for_disabled_sales_channel(self):
|
||||||
|
self.event.settings.mail_days_download_reminder = 2
|
||||||
|
self.event.settings.mail_sales_channel_download_reminder = []
|
||||||
|
send_download_reminders(sender=self.event)
|
||||||
|
assert len(djmail.outbox) == 0
|
||||||
|
|
||||||
|
|
||||||
class OrderCancelTests(TestCase):
|
class OrderCancelTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from unittest import mock
|
|||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core import mail as djmail
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
@@ -2510,6 +2511,33 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
|
|||||||
assert not Order.objects.last().testmode
|
assert not Order.objects.last().testmode
|
||||||
assert "0" not in Order.objects.last().code
|
assert "0" not in Order.objects.last().code
|
||||||
|
|
||||||
|
def test_receive_order_confirmation_and_paid_mail(self):
|
||||||
|
with scopes_disabled():
|
||||||
|
cp1 = CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=23, expires=now() + timedelta(minutes=10)
|
||||||
|
)
|
||||||
|
djmail.outbox = []
|
||||||
|
oid = _perform_order(self.event, 'manual', [cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||||
|
assert len(djmail.outbox) == 1
|
||||||
|
o = Order.objects.get(pk=oid)
|
||||||
|
o.payments.first().confirm()
|
||||||
|
assert len(djmail.outbox) == 2
|
||||||
|
|
||||||
|
def test_order_confirmation_and_paid_mail_not_send_on_disabled_sales_channel(self):
|
||||||
|
with scopes_disabled():
|
||||||
|
cp1 = CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=23, expires=now() + timedelta(minutes=10)
|
||||||
|
)
|
||||||
|
djmail.outbox = []
|
||||||
|
self.event.settings.mail_sales_channel_placed_paid = []
|
||||||
|
oid = _perform_order(self.event, 'manual', [cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||||
|
assert len(djmail.outbox) == 0
|
||||||
|
o = Order.objects.get(pk=oid)
|
||||||
|
o.payments.first().confirm()
|
||||||
|
assert len(djmail.outbox) == 0
|
||||||
|
|
||||||
|
|
||||||
class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
|
class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user