diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 6dfe1582da..0fed42568b 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -554,6 +554,10 @@ class Quota(LoggedModel): return CartPosition.objects.filter( Q(expires__gte=now()) & + ~Q( + Q(voucher__isnull=False) & Q(voucher__block_quota=True) + & Q(Q(voucher__valid_until__isnull=True) | Q(voucher__valid_until__gte=now())) + ) & self._position_lookup ).distinct().count() diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 8f40dc93d5..f4949f1af0 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -222,8 +222,13 @@ def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]): quota_ok = True - if not cp.voucher or not (cp.voucher.allow_ignore_quota or cp.voucher.block_quota): + ignore_all_quotas = cp.expires >= dt or ( + cp.voucher and (cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None))) + + if not ignore_all_quotas: for quota in quotas: + if cp.voucher and cp.voucher.block_quota and cp.voucher.quota_id == quota.pk: + continue avail = quota.availability() if avail[0] != Quota.AVAILABILITY_OK: # This quota is sold out/currently unavailable, so do not sell this at all diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index df0978b493..4fe5f2a396 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -240,6 +240,44 @@ class QuotaTestCase(BaseQuotaTestCase): block_quota=True) self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1)) + def test_blocking_voucher_in_cart(self): + self.quota.items.add(self.item1) + v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() + timedelta(days=5), + block_quota=True) + CartPosition.objects.create(event=self.event, item=self.item1, price=2, + expires=now() + timedelta(days=3), voucher=v) + self.assertEqual(self.quota.count_blocking_vouchers(), 1) + self.assertEqual(self.quota.count_in_cart(), 0) + self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1)) + + def test_blocking_voucher_in_cart_inifinitely_valid(self): + self.quota.items.add(self.item1) + v = Voucher.objects.create(quota=self.quota, event=self.event, block_quota=True) + CartPosition.objects.create(event=self.event, item=self.item1, price=2, + expires=now() + timedelta(days=3), voucher=v) + self.assertEqual(self.quota.count_blocking_vouchers(), 1) + self.assertEqual(self.quota.count_in_cart(), 0) + self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1)) + + def test_blocking_expired_voucher_in_cart(self): + self.quota.items.add(self.item1) + v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() - timedelta(days=5), + block_quota=True) + CartPosition.objects.create(event=self.event, item=self.item1, price=2, + expires=now() + timedelta(days=3), voucher=v) + self.assertEqual(self.quota.count_blocking_vouchers(), 0) + self.assertEqual(self.quota.count_in_cart(), 1) + self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1)) + + def test_nonblocking_voucher_in_cart(self): + self.quota.items.add(self.item1) + v = Voucher.objects.create(quota=self.quota, event=self.event) + CartPosition.objects.create(event=self.event, item=self.item1, price=2, + expires=now() + timedelta(days=3), voucher=v) + self.assertEqual(self.quota.count_blocking_vouchers(), 0) + self.assertEqual(self.quota.count_in_cart(), 1) + self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1)) + class OrderTestCase(BaseQuotaTestCase): def setUp(self): diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index 222583e8ab..5ea5d1350b 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -600,6 +600,15 @@ class CartTest(CartTestMixin, TestCase): objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event)) self.assertEqual(len(objs), 0) + def test_voucher_quota_other_quota_full(self): + quota2 = self.event.quotas.create(name='Test', size=0) + quota2.variations.add(self.shirt_red) + v = Voucher.objects.create(quota=self.quota_shirts, event=self.event) + self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), { + 'variation_%d_%d_voucher' % (self.shirt.id, self.shirt_red.id): v.code, + }, follow=True) + self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 0) + def test_hide_without_voucher(self): v = Voucher.objects.create(item=self.shirt, event=self.event) self.shirt.hide_without_voucher = True diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index 9831eaa8c7..1dddeb9bee 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -409,6 +409,24 @@ class CheckoutTestCase(TestCase): self.assertEqual(Order.objects.count(), 1) self.assertEqual(OrderPosition.objects.count(), 1) + def test_voucher_block_quota_other_quota_full(self): + self.quota_tickets.size = 0 + self.quota_tickets.save() + q2 = self.event.quotas.create(name='Testquota', size=0) + q2.items.add(self.ticket) + v = Voucher.objects.create(quota=self.quota_tickets, price=Decimal('12.00'), event=self.event, + valid_until=now() - timedelta(days=2), block_quota=True) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=12, expires=now() + timedelta(minutes=10), voucher=v + ) + self._set_session('payment', 'banktransfer') + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertEqual(len(doc.select(".alert-danger")), 1) + self.assertFalse(Order.objects.exists()) + def test_voucher_double(self): self.quota_tickets.size = 2 self.quota_tickets.save() @@ -536,3 +554,60 @@ class CheckoutTestCase(TestCase): doc = BeautifulSoup(response.rendered_content) self.assertGreaterEqual(len(doc.select(".alert-danger")), 1) self.assertFalse(CartPosition.objects.filter(id=cr1.id).exists()) + + def test_confirm_expired_with_blocking_voucher_unavailable(self): + self.quota_tickets.size = 0 + self.quota_tickets.save() + v = Voucher.objects.create(quota=self.quota_tickets, event=self.event, block_quota=True) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, voucher=v, + price=23, expires=now() - timedelta(minutes=10) + ) + self._set_session('payment', 'banktransfer') + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertEqual(len(doc.select(".thank-you")), 1) + + def test_confirm_expired_with_non_blocking_voucher_unavailable(self): + self.quota_tickets.size = 0 + self.quota_tickets.save() + v = Voucher.objects.create(quota=self.quota_tickets, event=self.event) + cr1 = CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, voucher=v, + price=23, expires=now() - timedelta(minutes=10) + ) + self._set_session('payment', 'banktransfer') + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertGreaterEqual(len(doc.select(".alert-danger")), 1) + self.assertFalse(CartPosition.objects.filter(id=cr1.id).exists()) + + def test_confirm_not_expired_with_blocking_voucher_unavailable(self): + self.quota_tickets.size = 0 + self.quota_tickets.save() + v = Voucher.objects.create(quota=self.quota_tickets, event=self.event, block_quota=True) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, voucher=v, + price=23, expires=now() + timedelta(minutes=10) + ) + self._set_session('payment', 'banktransfer') + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertEqual(len(doc.select(".thank-you")), 1) + + def test_confirm_not_expired_with_non_blocking_voucher_unavailable(self): + self.quota_tickets.size = 0 + self.quota_tickets.save() + v = Voucher.objects.create(quota=self.quota_tickets, event=self.event) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, voucher=v, + price=23, expires=now() + timedelta(minutes=10) + ) + self._set_session('payment', 'banktransfer') + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content) + self.assertEqual(len(doc.select(".thank-you")), 1)