diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 56c2ad354..383cab539 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -928,7 +928,8 @@ Creating orders during order generation and is not respected automatically when the order changes later.) * ``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``. If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually diff --git a/doc/spelling_wordlist.txt b/doc/spelling_wordlist.txt index ba0b0918d..67f0f50b0 100644 --- a/doc/spelling_wordlist.txt +++ b/doc/spelling_wordlist.txt @@ -47,6 +47,7 @@ gunicorn guid hardcoded hostname +ics idempotency iframe incrementing diff --git a/doc/user/events/email.rst b/doc/user/events/email.rst index 69deafeb9..01225a112 100644 --- a/doc/user/events/email.rst +++ b/doc/user/events/email.rst @@ -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 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 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. @@ -33,6 +36,15 @@ Signature Bcc address 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 ------------- diff --git a/src/pretix/base/migrations/0159_mails_by_sales_channel.py b/src/pretix/base/migrations/0159_mails_by_sales_channel.py new file mode 100644 index 000000000..efab29c1b --- /dev/null +++ b/src/pretix/base/migrations/0159_mails_by_sales_channel.py @@ -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), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 4b62a0c9d..8d64eac1d 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1491,7 +1491,7 @@ class OrderPayment(models.Model): 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) if self.order.event.settings.mail_send_order_paid_attendee: for p in self.order.positions.all(): diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index a895dd0a8..bfc618ef5 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -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_template = event.settings.mail_text_order_placed_attendee - _order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment) - if email_attendees: - for p in order.positions.all(): - 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) + if sales_channel in event.settings.mail_sales_channel_placed_paid: + _order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment) + if email_attendees: + for p in order.positions.all(): + 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 @@ -1056,6 +1057,9 @@ def send_download_reminders(sender, **kwargs): if days is None: 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) if now() < reminder_date or o.datetime > reminder_date: continue diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index a73b8823c..cadd41c9f 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1190,6 +1190,14 @@ DEFAULTS = { "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': { 'type': LazyI18nString, 'default': "" diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 350063623..1861ef1c0 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -1,5 +1,3 @@ -from urllib.parse import urlencode, urlparse - from django import forms from django.conf import settings from django.core.exceptions import ValidationError @@ -17,6 +15,7 @@ from i18nfield.forms import ( I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput, ) from pytz import common_timezones, timezone +from urllib.parse import urlencode, urlparse from pretix.base.channels import get_all_sales_channels from pretix.base.email import get_available_placeholders @@ -750,6 +749,11 @@ def multimail_validate(val): 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): auto_fields = [ 'mail_prefix', @@ -758,6 +762,27 @@ class MailSettingsForm(SettingsForm): '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( label=_("Bcc address"), help_text=_("All emails will be sent to this address as a Bcc copy"), diff --git a/src/pretix/control/templates/pretixcontrol/event/mail.html b/src/pretix/control/templates/pretixcontrol/event/mail.html index 0988ebacc..0ea2e13dd 100644 --- a/src/pretix/control/templates/pretixcontrol/event/mail.html +++ b/src/pretix/control/templates/pretixcontrol/event/mail.html @@ -17,6 +17,7 @@ {% bootstrap_field form.mail_text_signature layout="control" %} {% bootstrap_field form.mail_bcc layout="control" %} {% bootstrap_field form.mail_attach_ical layout="control" %} + {% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
{% trans "E-mail design" %} @@ -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" %} {% 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 %} {% 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" %} diff --git a/src/pretix/presale/forms/order.py b/src/pretix/presale/forms/order.py index b68a8c0eb..adaee75c1 100644 --- a/src/pretix/presale/forms/order.py +++ b/src/pretix/presale/forms/order.py @@ -1,7 +1,7 @@ from decimal import Decimal 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.tax import TaxedPrice diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py index da0a39d33..f99314def 100644 --- a/src/tests/base/test_orders.py +++ b/src/tests/base/test_orders.py @@ -543,6 +543,13 @@ class DownloadReminderTests(TestCase): send_download_reminders(sender=self.event) 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): def setUp(self): diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index bd3bdb472..86371f892 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -8,6 +8,7 @@ from unittest import mock from bs4 import BeautifulSoup from django.conf import settings +from django.core import mail as djmail from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from django.utils.crypto import get_random_string @@ -2510,6 +2511,33 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): assert not Order.objects.last().testmode 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):