From 5667866c9d6de1384d02371bdfa8871c3f941073 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 8 Aug 2016 18:50:16 +0200 Subject: [PATCH 1/2] Make vouchers reusable upon cancelation of an Order Refs #165. --- src/pretix/base/models/vouchers.py | 4 +++- src/pretix/base/services/orders.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py index 0920718d56..104d2088a7 100644 --- a/src/pretix/base/models/vouchers.py +++ b/src/pretix/base/models/vouchers.py @@ -162,10 +162,12 @@ class Voucher(LoggedModel): def is_ordered(self) -> int: """ - Returns whether an order position exists that uses this voucher. + Returns whether a non-canceled order position exists that uses this voucher. """ return OrderPosition.objects.filter( voucher=self + ).exclude( + order__status=Order.STATUS_CANCELLED ).exists() def is_in_cart(self) -> int: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index f4949f1af0..935d4023d3 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -152,6 +152,11 @@ def cancel_order(order, user=None): if i: generate_cancellation(i) + for position in order.positions.all(): + if position.voucher: + position.voucher.redeemed = False + position.voucher.save() + return order From 87a77989e7a69949413cfe0746965696426ee480 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Sun, 21 Aug 2016 16:07:31 +0200 Subject: [PATCH 2/2] Disallow cancelling an expired order as per discussion on #165 --- src/pretix/base/models/vouchers.py | 2 +- src/pretix/base/services/orders.py | 2 +- src/pretix/presale/views/order.py | 2 +- src/tests/base/test_models.py | 24 +++++++++++++++++++++++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/pretix/base/models/vouchers.py b/src/pretix/base/models/vouchers.py index 104d2088a7..8c5a72760d 100644 --- a/src/pretix/base/models/vouchers.py +++ b/src/pretix/base/models/vouchers.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from .base import LoggedModel from .event import Event from .items import Item, ItemVariation, Quota -from .orders import CartPosition, OrderPosition +from .orders import CartPosition, Order, OrderPosition def generate_code(): diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 935d4023d3..ea6a5a3869 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -142,7 +142,7 @@ def cancel_order(order, user=None): if isinstance(user, int): user = User.objects.get(pk=user) with order.event.lock(): - if order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED): + if order.status != Order.STATUS_PENDING: raise OrderError(_('You cannot cancel this order.')) order.status = Order.STATUS_CANCELLED order.save() diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index ebe5ff7c3c..b26c534575 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -295,7 +295,7 @@ class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView): self.kwargs = kwargs if not self.order: raise Http404(_('Unknown order code or not authorized to access this order.')) - if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) or not self.order.can_user_cancel: + if self.order.status != Order.STATUS_PENDING or not self.order.can_user_cancel: messages.error(request, _('You cannot cancel this order.')) return redirect(self.get_order_url()) return super().dispatch(request, *args, **kwargs) diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index 4fe5f2a396..518b59aaf6 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -10,7 +10,9 @@ from pretix.base.models import ( CachedFile, CartPosition, Event, Item, ItemCategory, ItemVariation, Order, OrderPosition, Organizer, Question, Quota, User, Voucher, ) -from pretix.base.services.orders import mark_order_paid +from pretix.base.services.orders import ( + OrderError, cancel_order, mark_order_paid, perform_order, +) class UserTestCase(TestCase): @@ -278,6 +280,26 @@ class QuotaTestCase(BaseQuotaTestCase): self.assertEqual(self.quota.count_in_cart(), 1) self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1)) + def test_voucher_reuse(self): + self.quota.items.add(self.item1) + v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() + timedelta(days=5)) + + # use a voucher normally + cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price, + expires=now() + timedelta(days=3), voucher=v) + order = perform_order(event=self.event.id, payment_provider='free', positions=[cart.id]) + + # assert that the voucher cannot be reused + cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price, + expires=now() + timedelta(days=3), voucher=v) + self.assertRaises(OrderError, perform_order, event=self.event.id, payment_provider='free', positions=[cart.id]) + + # assert that the voucher can be re-used after cancelling the successful order + cancel_order(order) + cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price, + expires=now() + timedelta(days=3), voucher=v) + perform_order(event=self.event.id, payment_provider='free', positions=[cart.id]) + class OrderTestCase(BaseQuotaTestCase): def setUp(self):