diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index ae250799cd..dc4479252a 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -309,6 +309,10 @@ class ItemVariation(models.Model): def __str__(self): return str(self.value) + @property + def price(self): + return self.default_price if self.default_price is not None else self.item.default_price + def delete(self, *args, **kwargs): super().delete(*args, **kwargs) if self.item: diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 1e4f9641fb..d8db2e22fb 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -190,6 +190,10 @@ class Order(LoggedModel): """ return '{event}-{code}'.format(event=self.event.slug.upper(), code=self.code) + @property + def changable(self): + return self.status in (Order.STATUS_PAID, Order.STATUS_PENDING) + def save(self, *args, **kwargs): if not self.code: self.assign_code() diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index ac06c849df..81d468a770 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -480,9 +480,11 @@ class OrderChangeManager: 'quota': _('The quota {name} does not have enough capacity left to perform the operation.'), 'product_invalid': _('The selected product is not active or has no price set.'), 'complete_cancel': _('This operation would leave the order empty. Please cancel the order itself instead.'), - 'not_pending': _('Only pending orders can be changed.'), + 'not_pending_or_paid': _('Only pending or paid orders can be changed.'), 'paid_to_free_exceeded': _('This operation would make the order free and therefore immediately paid, however ' 'no quota is available.'), + 'paid_price_change': _('Currently, paid orders can only be changed in a way that does not change the total ' + 'price of the order as partial payments or refunds are not yet supported.') } ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation', 'price')) PriceOperation = namedtuple('PriceOperation', ('position', 'price')) @@ -498,8 +500,7 @@ class OrderChangeManager: def change_item(self, position: OrderPosition, item: Item, variation: Optional[ItemVariation]): if (not variation and item.has_variations) or (variation and variation.item_id != item.pk): raise OrderError(self.error_messages['product_without_variation']) - price = item.default_price if variation is None else ( - variation.default_price if variation.default_price is not None else item.default_price) + price = item.default_price if variation is None else variation.price if not price: raise OrderError(self.error_messages['product_invalid']) self._totaldiff = price - position.price @@ -528,6 +529,10 @@ class OrderChangeManager: if self.order.total == Decimal('0.00') and self._totaldiff > 0: raise OrderError(self.error_messages['free_to_paid']) + def _check_paid_price_change(self): + if self.order.status == Order.STATUS_PAID and self._totaldiff != 0: + raise OrderError(self.error_messages['paid_price_change']) + def _check_paid_to_free(self): if self.order.total == 0: try: @@ -624,9 +629,10 @@ class OrderChangeManager: return with transaction.atomic(): with self.order.event.lock(): - if self.order.status != Order.STATUS_PENDING: - raise OrderError(self.error_messages['not_pending']) + if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID): + raise OrderError(self.error_messages['not_pending_or_paid']) self._check_free_to_paid() + self._check_paid_price_change() self._check_quotas() self._check_complete_cancel() self._perform_operations() diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 173f7dbe69..313f5a44e7 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.formats import localize from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ @@ -94,9 +95,12 @@ class OrderPositionChangeForm(forms.Form): variations = list(i.variations.all()) if variations: for v in variations: - choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (pname, v.value))) + choices.append(('%d-%d' % (i.pk, v.pk), + '%s – %s (%s %s)' % (pname, v.value, localize(v.price), + instance.order.event.currency))) else: - choices.append((str(i.pk), pname)) + choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(i.default_price), + instance.order.event.currency))) self.fields['itemvar'].choices = choices def clean(self): diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index 0453c4830b..c141504caa 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -144,7 +144,7 @@
- {% if order.status == "n" and request.eventperm.can_change_orders %} + {% if order.changable and request.eventperm.can_change_orders %} {% trans "Change products" %} diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index e058b9e8ea..5647125379 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -446,8 +446,8 @@ class OrderChange(OrderView): template_name = 'pretixcontrol/order/change.html' def dispatch(self, request, *args, **kwargs): - if self.order.status != Order.STATUS_PENDING: - messages.error(self.request, _('This action is only allowed for pending orders.')) + if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID): + messages.error(self.request, _('This action is only allowed for pending or paid orders.')) return self._redirect_back() return super().dispatch(request, *args, **kwargs) diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py index 187eb3515e..8bc116056c 100644 --- a/src/tests/base/test_orders.py +++ b/src/tests/base/test_orders.py @@ -140,6 +140,8 @@ class OrderChangeManagerTests(TestCase): ) self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket', tax_rate=Decimal('7.00'), default_price=Decimal('23.00'), admission=True) + self.ticket2 = Item.objects.create(event=self.event, name='Other ticket', tax_rate=Decimal('7.00'), + default_price=Decimal('23.00'), admission=True) self.shirt = Item.objects.create(event=self.event, name='T-Shirt', tax_rate=Decimal('19.00'), default_price=Decimal('12.00')) self.op1 = OrderPosition.objects.create( @@ -303,3 +305,22 @@ class OrderChangeManagerTests(TestCase): assert self.order.total == 0 assert self.order.status == Order.STATUS_PAID assert self.order.payment_provider == 'free' + + def test_change_paid_same_price(self): + self.order.status = Order.STATUS_PAID + self.order.save() + self.ocm.change_item(self.op1, self.ticket2, None) + self.ocm.commit() + self.order.refresh_from_db() + assert self.order.total == 46 + assert self.order.status == Order.STATUS_PAID + + def test_change_paid_different_price(self): + self.order.status = Order.STATUS_PAID + self.order.save() + self.ocm.change_price(self.op1, Decimal('5.00')) + with self.assertRaises(OrderError): + self.ocm.commit() + self.order.refresh_from_db() + assert self.order.total == 46 + assert self.order.status == Order.STATUS_PAID