diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 863ca76cd2..e8be424cf5 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -146,6 +146,10 @@ error_messages = { 'race_condition': gettext_lazy("This order was changed by someone else simultaneously. Please check if your " "changes are still accurate and try again."), 'empty': gettext_lazy("Your cart is empty."), + 'max_items': ngettext_lazy( + "You cannot select more than %s item per order.", + "You cannot select more than %s items per order." + ), 'max_items_per_product': ngettext_lazy( "You cannot select more than %(max)s item of the product %(product)s. We removed the surplus items from your cart.", "You cannot select more than %(max)s items of the product %(product)s. We removed the surplus items from your cart.", @@ -763,6 +767,11 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti shared_lock_objects=[event] ) + # Check maximum order size + limit = min(int(event.settings.max_items_per_order), settings.PRETIX_MAX_ORDER_SIZE) + if sum(1 for cp in sorted_positions if not cp.addon_to) > limit: + err = err or (error_messages['max_items'] % limit) + # Check availability for i, cp in enumerate(sorted_positions): if cp.pk in deleted_positions: diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index d2e66a9ef5..17d43e9d91 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -3625,6 +3625,47 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase): self.assertEqual(Order.objects.count(), 1) self.assertEqual(OrderPosition.objects.count(), 1) + def test_max_items_per_order_failed(self): + self.event.settings.max_items_per_order = 2 + self.ticket.save() + with scopes_disabled(): + ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat) + cp = CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10), + ) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.workshop1, addon_to=cp, + price=12, expires=now() + timedelta(minutes=10), + ) + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10), + ) + to_delete = CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10), + ) + self._set_payment() + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.content.decode(), "lxml") + with scopes_disabled(): + self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key).count(), 4) + self.assertEqual(len(doc.select(".alert-danger")), 1) + self.assertFalse(Order.objects.exists()) + + with scopes_disabled(): + to_delete.delete() + self.client.get('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug)) # required for session['shown_total'] + + response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.content.decode(), "lxml") + with scopes_disabled(): + self.assertEqual(len(doc.select(".thank-you")), 1) + self.assertEqual(Order.objects.count(), 1) + self.assertEqual(OrderPosition.objects.count(), 3) + def test_subevent_confirm_expired_partial(self): self.event.has_subevents = True self.event.save()