diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 881c7746be..c48b68b14a 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -737,8 +737,6 @@ Creating orders * does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping module - * does not send order confirmations via email - * does not support reverse charge taxation * does not support file upload questions @@ -809,6 +807,8 @@ Creating orders * ``tax_rule`` * ``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 + ``false``. If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually to incrementing integers starting with ``1``. Then, you can reference one of these diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 6a24eb5d41..8ad44c3c36 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -552,11 +552,13 @@ class OrderCreateSerializer(I18nAwareModelSerializer): consume_carts = serializers.ListField(child=serializers.CharField(), required=False) force = serializers.BooleanField(default=False, required=False) payment_date = serializers.DateTimeField(required=False, allow_null=True) + send_mail = serializers.BooleanField(default=False, required=False) class Meta: model = Order fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel', - 'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', 'force') + 'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts', + 'force', 'send_mail') def validate_payment_provider(self, pp): if pp not in self.context['event'].get_payment_providers(): @@ -632,6 +634,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer): payment_info = validated_data.pop('payment_info', '{}') payment_date = validated_data.pop('payment_date', now()) force = validated_data.pop('force', False) + self._send_mail = validated_data.pop('send_mail', False) if 'invoice_address' in validated_data: iadata = validated_data.pop('invoice_address') diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index c43ed98a53..3545f8b232 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -41,7 +41,8 @@ from pretix.base.services.invoices import ( ) from pretix.base.services.mail import SendMailException from pretix.base.services.orders import ( - OrderChangeManager, OrderError, approve_order, cancel_order, deny_order, + OrderChangeManager, OrderError, _order_placed_email, + _order_placed_email_attendee, approve_order, cancel_order, deny_order, extend_order, mark_order_expired, mark_order_refunded, ) from pretix.base.services.pricing import get_price @@ -431,6 +432,7 @@ class OrderViewSet(viewsets.ModelViewSet): serializer.is_valid(raise_exception=True) with transaction.atomic(): self.perform_create(serializer) + send_mail = serializer._send_mail order = serializer.instance serializer = OrderSerializer(order, context=serializer.context) @@ -445,8 +447,42 @@ class OrderViewSet(viewsets.ModelViewSet): (order.event.settings.get('invoice_generate') == 'True') or (order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID) ) and not order.invoices.last() + invoice = None if gen_invoice: - generate_invoice(order, trigger_pdf=True) + invoice = generate_invoice(order, trigger_pdf=True) + + if send_mail: + payment = order.payments.last() + free_flow = ( + payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and + not order.require_approval and payment.provider == "free" + ) + if free_flow: + email_template = request.event.settings.mail_text_order_free + log_entry = 'pretix.event.order.email.order_free' + email_attendees = request.event.settings.mail_send_order_free_attendee + email_attendees_template = request.event.settings.mail_text_order_free_attendee + else: + email_template = request.event.settings.mail_text_order_placed + log_entry = 'pretix.event.order.email.order_placed' + email_attendees = request.event.settings.mail_send_order_placed_attendee + email_attendees_template = request.event.settings.mail_text_order_placed_attendee + + _order_placed_email( + request.event, order, payment.payment_provider if payment else None, 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(request.event, order, p, email_attendees_template, log_entry) + + if not free_flow and order.status == Order.STATUS_PAID and payment: + payment._send_paid_mail(invoice, None, '') + if self.request.event.settings.mail_send_order_paid_attendee: + for p in order.positions.all(): + if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email: + payment._send_paid_mail_attendee(p, None) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index d7490b217f..ac3b93e28c 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -2753,6 +2753,78 @@ def test_order_create_with_seat_consumed_from_cart(token_client, organizer, even assert p.seat == seat +@pytest.mark.django_db +def test_order_create_send_no_emails(token_client, organizer, event, item, quota, question): + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + djmail.outbox = [] + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + assert len(djmail.outbox) == 0 + + +@pytest.mark.django_db +def test_order_create_send_emails(token_client, organizer, event, item, quota, question): + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + res['send_mail'] = True + djmail.outbox = [] + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].subject == "Your order: {}".format(resp.data['code']) + + +@pytest.mark.django_db +def test_order_create_send_emails_free(token_client, organizer, event, item, quota, question): + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['price'] = '0.00' + res['payment_provider'] = 'free' + del res['fees'] + res['positions'][0]['answers'][0]['question'] = question.pk + res['send_mail'] = True + djmail.outbox = [] + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + print(resp.data) + assert resp.status_code == 201 + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].subject == "Your order: {}".format(resp.data['code']) + + +@pytest.mark.django_db +def test_order_create_send_emails_paid(token_client, organizer, event, item, quota, question): + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + res['send_mail'] = True + res['status'] = 'p' + djmail.outbox = [] + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + assert len(djmail.outbox) == 2 + assert djmail.outbox[0].subject == "Your order: {}".format(resp.data['code']) + assert djmail.outbox[1].subject == "Payment received for your order: {}".format(resp.data['code']) + + REFUND_CREATE_PAYLOAD = { "state": "created", "provider": "manual",