diff --git a/doc/images/order_states.png b/doc/images/order_states.png index 8297351b93..a47808d4b1 100644 Binary files a/doc/images/order_states.png and b/doc/images/order_states.png differ diff --git a/doc/images/order_states.puml b/doc/images/order_states.puml index 9c32f0fa12..743c4207ca 100644 --- a/doc/images/order_states.puml +++ b/doc/images/order_states.puml @@ -11,6 +11,7 @@ Pending --> Paid: successful payment Pending --> Expired: automatically\nor manually\non admin action Expired --> Paid: if payment is received\nonly if quota left Expired --> Canceled +Expired --> Pending: manually\non admin action Paid --> Refunded: manually on\nadmin action\nor if an external\npayment provider\nnotifies about a\npayment refund Pending --> Canceled: on admin or\ncustomer action Paid -> Pending: manually on admin action diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index fc3a893acf..294ee9c181 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -286,8 +286,6 @@ class Order(LoggedModel): raise Quota.QuotaExceededException(error_messages['unavailable']) for quota in quotas: - # Lock the quota, so no other thread is allowed to perform sales covered by this - # quota while we're doing so. if quota.id not in quota_cache: quota_cache[quota.id] = quota quota.cached_availability = quota.availability(now_dt)[1] diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 053941c2ff..c335f304dd 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -1,6 +1,7 @@ from django import forms from django.core.exceptions import ValidationError from django.db import models +from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from pretix.base.forms import I18nModelForm @@ -15,6 +16,12 @@ class ExtendForm(I18nModelForm): 'expires': forms.DateTimeInput(attrs={'class': 'datetimepicker'}), } + def clean(self): + data = super().clean() + if data['expires'] < now(): + raise ValidationError(_('The new expiry date needs to be in the future.')) + return data + class ExporterForm(forms.Form): diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index bd5f75fdab..397b206c75 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -23,11 +23,11 @@
{% if order.status == 'n' or order.status == 'e' %} - {% endif %} - {% if order.status == 'n' %} {% trans "Extend payment term" %} + {% endif %} + {% if order.status == 'n' %} {% trans "Cancel order" %} diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 065766e255..3d8e20b330 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -442,16 +442,12 @@ class OrderExtend(OrderView): permission = 'can_change_orders' def post(self, *args, **kwargs): - if self.order.status != Order.STATUS_PENDING: - messages.error(self.request, _('This action is only allowed for pending orders.')) - return self._redirect_back() - oldvalue = self.order.expires - if self.form.is_valid(): - if oldvalue > now(): + if self.order.status == Order.STATUS_PENDING: messages.success(self.request, _('The payment term has been changed.')) self.order.log_action('pretix.event.order.expirychanged', user=self.request.user, data={ - 'expires': self.order.expires + 'expires': self.order.expires, + 'state_change': False }) self.form.save() else: @@ -460,8 +456,11 @@ class OrderExtend(OrderView): is_available = self.order._is_still_available(now_dt) if is_available is True: self.form.save() + self.order.status = Order.STATUS_PENDING + self.order.save() self.order.log_action('pretix.event.order.expirychanged', user=self.request.user, data={ - 'expires': self.order.expires + 'expires': self.order.expires, + 'state_change': True }) messages.success(self.request, _('The payment term has been changed.')) else: @@ -473,10 +472,13 @@ class OrderExtend(OrderView): else: return self.get(*args, **kwargs) - def get(self, *args, **kwargs): - if self.order.status != Order.STATUS_PENDING: + def dispatch(self, request, *args, **kwargs): + if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED): messages.error(self.request, _('This action is only allowed for pending orders.')) return self._redirect_back() + return super().dispatch(request, *kwargs, **kwargs) + + def get(self, *args, **kwargs): return render(self.request, 'pretixcontrol/order/extend.html', { 'order': self.order, 'form': self.form, diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py index f23fedffec..30f1c5587c 100644 --- a/src/tests/control/test_orders.py +++ b/src/tests/control/test_orders.py @@ -447,11 +447,11 @@ def test_order_extend_not_expired(client, env): @pytest.mark.django_db -def test_order_extend_expired_quota_left(client, env): +def test_order_extend_overdue_quota_empty(client, env): o = Order.objects.get(id=env[2].id) o.expires = now() - timedelta(days=5) o.save() - q = Quota.objects.create(event=env[0], size=3) + q = Quota.objects.create(event=env[0], size=0) q.items.add(env[3]) newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S") client.login(email='dummy@dummy.dummy', password='dummy') @@ -463,10 +463,30 @@ def test_order_extend_expired_quota_left(client, env): assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == newdate +@pytest.mark.django_db +def test_order_extend_expired_quota_left(client, env): + o = Order.objects.get(id=env[2].id) + o.expires = now() - timedelta(days=5) + o.status = Order.STATUS_EXPIRED + o.save() + q = Quota.objects.create(event=env[0], size=3) + q.items.add(env[3]) + newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S") + client.login(email='dummy@dummy.dummy', password='dummy') + response = client.post('/control/event/dummy/dummy/orders/FOO/extend', { + 'expires': newdate + }, follow=True) + assert 'alert-success' in response.rendered_content + o = Order.objects.get(id=env[2].id) + assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == newdate + assert o.status == Order.STATUS_PENDING + + @pytest.mark.django_db def test_order_extend_expired_quota_empty(client, env): o = Order.objects.get(id=env[2].id) o.expires = now() - timedelta(days=5) + o.status = Order.STATUS_EXPIRED olddate = o.expires o.save() q = Quota.objects.create(event=env[0], size=0) @@ -479,6 +499,34 @@ def test_order_extend_expired_quota_empty(client, env): assert 'alert-danger' in response.rendered_content o = Order.objects.get(id=env[2].id) assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == olddate.strftime("%Y-%m-%d %H:%M:%S") + assert o.status == Order.STATUS_EXPIRED + + +@pytest.mark.django_db +def test_order_extend_expired_quota_partial(client, env): + o = Order.objects.get(id=env[2].id) + OrderPosition.objects.create( + order=o, + item=env[3], + variation=None, + price=Decimal("14"), + attendee_name="Peter" + ) + o.expires = now() - timedelta(days=5) + o.status = Order.STATUS_EXPIRED + olddate = o.expires + o.save() + q = Quota.objects.create(event=env[0], size=1) + q.items.add(env[3]) + newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S") + client.login(email='dummy@dummy.dummy', password='dummy') + response = client.post('/control/event/dummy/dummy/orders/FOO/extend', { + 'expires': newdate + }, follow=True) + assert 'alert-danger' in response.rendered_content + o = Order.objects.get(id=env[2].id) + assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == olddate.strftime("%Y-%m-%d %H:%M:%S") + assert o.status == Order.STATUS_EXPIRED @pytest.mark.django_db