diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 3450248264..29a9d85eaf 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -21,7 +21,8 @@ from pretix.base.models.organizer import TeamAPIToken from pretix.base.services.invoices import invoice_pdf from pretix.base.services.mail import SendMailException from pretix.base.services.orders import ( - OrderError, cancel_order, extend_order, mark_order_paid, + OrderError, cancel_order, extend_order, mark_order_expired, + mark_order_paid, ) from pretix.base.services.tickets import ( get_cachedticket_for_order, get_cachedticket_for_position, @@ -153,10 +154,8 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet): status=status.HTTP_400_BAD_REQUEST ) - order.status = Order.STATUS_EXPIRED - order.save() - order.log_action( - 'pretix.event.order.expired', + mark_order_expired( + order, user=request.user if request.user.is_authenticated else None, api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None), ) diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index a5abeef4d8..2460261953 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -128,8 +128,14 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date order_paid.send(order.event, order=order) invoice = None - if order.event.settings.get('invoice_generate') in ('True', 'paid') and invoice_qualified(order): - if not order.invoices.exists(): + if invoice_qualified(order): + invoices = order.invoices.filter(is_cancellation=False).count() + cancellations = order.invoices.filter(is_cancellation=True).count() + gen_invoice = ( + (invoices == 0 and order.event.settings.get('invoice_generate') in ('True', 'paid')) or + 0 < invoices <= cancellations + ) + if gen_invoice: invoice = generate_invoice( order, trigger_pdf=not send_mail or not order.event.settings.invoice_email_attachment @@ -231,6 +237,32 @@ def mark_order_refunded(order, user=None): return order +@transaction.atomic +def mark_order_expired(order, user=None, api_token=None): + """ + Mark this order as expired. This sets the payment status and returns the order object. + :param order: The order to change + :param user: The user that performed the change + :param api_token: The API token used to performed the change + """ + if isinstance(order, int): + order = Order.objects.get(pk=order) + if isinstance(user, int): + user = User.objects.get(pk=user) + if isinstance(api_token, int): + api_token = TeamAPIToken.objects.get(pk=api_token) + with order.event.lock(): + order.status = Order.STATUS_EXPIRED + order.save() + + order.log_action('pretix.event.order.expired', user=user, api_token=api_token) + i = order.invoices.filter(is_cancellation=False).last() + if i: + generate_cancellation(i) + + return order + + @transaction.atomic def _cancel_order(order, user=None, send_mail: bool=True, api_token=None): """ @@ -562,9 +594,7 @@ def expire_orders(sender, **kwargs): expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool) eventcache[o.event.pk] = expire if expire: - o.status = Order.STATUS_EXPIRED - o.log_action('pretix.event.order.expired') - o.save() + mark_order_expired(o) @receiver(signal=periodic_task) diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index dbda228292..57948a4c80 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -38,7 +38,7 @@ from pretix.base.services.locking import LockTimeoutException from pretix.base.services.mail import SendMailException, render_mail from pretix.base.services.orders import ( OrderChangeManager, OrderError, cancel_order, extend_order, - mark_order_paid, + mark_order_expired, mark_order_paid, ) from pretix.base.services.stats import order_overview from pretix.base.signals import register_data_exporters @@ -229,9 +229,7 @@ class OrderTransition(OrderView): self.order.log_action('pretix.event.order.unpaid', user=self.request.user) messages.success(self.request, _('The order has been marked as not paid.')) elif self.order.status == Order.STATUS_PENDING and to == 'e': - self.order.status = Order.STATUS_EXPIRED - self.order.save() - self.order.log_action('pretix.event.order.expired', user=self.request.user) + mark_order_expired(self.order, user=self.request.user) messages.success(self.request, _('The order has been marked as expired.')) elif self.order.status == Order.STATUS_PAID and to == 'r': ret = self.payment_provider.order_control_refund_perform(self.request, self.order) diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py index fcc9405a7d..5e3b7bb527 100644 --- a/src/tests/base/test_orders.py +++ b/src/tests/base/test_orders.py @@ -19,7 +19,7 @@ from pretix.base.reldate import RelativeDate, RelativeDateWrapper from pretix.base.services.invoices import generate_invoice from pretix.base.services.orders import ( OrderChangeManager, OrderError, _create_order, expire_orders, - send_download_reminders, + mark_order_paid, send_download_reminders, ) @@ -28,7 +28,8 @@ def event(): o = Organizer.objects.create(name='Dummy', slug='dummy') event = Event.objects.create( organizer=o, name='Dummy', slug='dummy', - date_from=now() + date_from=now(), + plugins='pretix.plugins.banktransfer' ) return event @@ -144,21 +145,42 @@ def test_expiry_dst(event): def test_expiring(event): o1 = Order.objects.create( code='FOO', event=event, email='dummy@dummy.test', - status=Order.STATUS_PENDING, + status=Order.STATUS_PENDING, locale='en', datetime=now(), expires=now() + timedelta(days=10), total=0, payment_provider='banktransfer' ) o2 = Order.objects.create( code='FO2', event=event, email='dummy@dummy.test', - status=Order.STATUS_PENDING, + status=Order.STATUS_PENDING, locale='en', datetime=now(), expires=now() - timedelta(days=10), - total=0, payment_provider='banktransfer' + total=12, payment_provider='banktransfer' ) + generate_invoice(o2) expire_orders(None) o1 = Order.objects.get(id=o1.id) assert o1.status == Order.STATUS_PENDING o2 = Order.objects.get(id=o2.id) assert o2.status == Order.STATUS_EXPIRED + assert o2.invoices.count() == 2 + assert o2.invoices.last().is_cancellation is True + + +@pytest.mark.django_db +def test_expiring_paid_invoice(event): + o2 = Order.objects.create( + code='FO2', event=event, email='dummy@dummy.test', + status=Order.STATUS_PENDING, locale='en', + datetime=now(), expires=now() - timedelta(days=10), + total=12, payment_provider='banktransfer' + ) + generate_invoice(o2) + expire_orders(None) + o2 = Order.objects.get(id=o2.id) + assert o2.status == Order.STATUS_EXPIRED + assert o2.invoices.count() == 2 + mark_order_paid(o2) + assert o2.invoices.count() == 3 + assert o2.invoices.last().is_cancellation is False @pytest.mark.django_db