diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 52b81ce9a2..5693777316 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -1078,6 +1078,7 @@ class CartManager: quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt) err = None new_cart_positions = [] + deleted_positions = set() err = err or self._check_min_max_per_product() @@ -1089,7 +1090,10 @@ class CartManager: if op.position.expires > self.now_dt: for q in op.position.quotas: quotas_ok[q] += 1 - op.position.addons.all().delete() + addons = op.position.addons.all() + deleted_positions |= {a.pk for a in addons} + addons.delete() + deleted_positions.add(op.position.pk) op.position.delete() elif isinstance(op, (self.AddOperation, self.ExtendOperation)): @@ -1239,20 +1243,28 @@ class CartManager: if op.seat and not op.seat.is_available(ignore_cart=op.position, sales_channel=self._sales_channel, ignore_voucher_id=op.position.voucher_id): err = err or error_messages['seat_unavailable'] - op.position.addons.all().delete() + + addons = op.position.addons.all() + deleted_positions |= {a.pk for a in addons} + deleted_positions.add(op.position.pk) + addons.delete() op.position.delete() elif available_count == 1: op.position.expires = self._expiry op.position.listed_price = op.listed_price op.position.price_after_voucher = op.price_after_voucher # op.position.price will be updated by recompute_final_prices_and_taxes() - try: - op.position.save(force_update=True, update_fields=['expires', 'listed_price', 'price_after_voucher']) - except DatabaseError: - # Best effort... The position might have been deleted in the meantime! - pass + if op.position.pk not in deleted_positions: + try: + op.position.save(force_update=True, update_fields=['expires', 'listed_price', 'price_after_voucher']) + except DatabaseError: + # Best effort... The position might have been deleted in the meantime! + pass elif available_count == 0: - op.position.addons.all().delete() + addons = op.position.addons.all() + deleted_positions |= {a.pk for a in addons} + deleted_positions.add(op.position.pk) + addons.delete() op.position.delete() else: raise AssertionError("ExtendOperation cannot affect more than one item") diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index 281db6dbe7..bde7472a68 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -2414,6 +2414,25 @@ class CartAddonTest(CartTestMixin, TestCase): assert cp2.item == self.workshop1 assert cp2.price == 0 + @classscope(attr='orga') + def test_extend_included_addon_no_longer_available(self): + self.addon1.price_included = True + self.addon1.save() + self.quota_tickets.size = 0 + self.quota_tickets.save() + cp1 = CartPosition.objects.create( + expires=now() - timedelta(minutes=10), item=self.ticket, price=Decimal('23.00'), + event=self.event, cart_id=self.session_key + ) + CartPosition.objects.create( + expires=now() - timedelta(minutes=10), item=self.workshop1, price=Decimal('0.00'), + event=self.event, cart_id=self.session_key, addon_to=cp1 + ) + self.cm.extend_expired_positions() + with self.assertRaises(CartError): + self.cm.commit() + assert CartPosition.objects.count() == 0 + @classscope(attr='orga') def test_cart_addon_remove_parent(self): self.addon1.price_included = True