diff --git a/src/pretix/base/migrations/0233_ignore_from_quota_while_blocked.py b/src/pretix/base/migrations/0233_ignore_from_quota_while_blocked.py new file mode 100644 index 000000000..8f75cbd95 --- /dev/null +++ b/src/pretix/base/migrations/0233_ignore_from_quota_while_blocked.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.17 on 2023-02-14 10:35 + +import django.core.serializers.json +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0232_exchangerate'), + ] + + operations = [ + migrations.AddField( + model_name='orderposition', + name='ignore_from_quota_while_blocked', + field=models.BooleanField(default=False), + ), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index be6f430a1..642d263f2 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -2212,6 +2212,9 @@ class OrderPosition(AbstractPosition): description by a plugin. If the position is not blocked, the value must be ``None``, not an empty list. :type blocked: list + :param ignore_from_quota_while_blocked: Ignore this order position from quota, as long as ``blocked`` is set. Only + to be used carefully by specific plugins. + :type ignore_from_quota_while_blocked: boolean :param valid_from: The ticket will not be considered valid before this date. If the value is ``None``, no check on ticket level is made. :type valid_from: datetime @@ -2257,6 +2260,7 @@ class OrderPosition(AbstractPosition): canceled = models.BooleanField(default=False) blocked = models.JSONField(null=True, blank=True) + ignore_from_quota_while_blocked = models.BooleanField(default=False) valid_from = models.DateTimeField( verbose_name=_("Valid from"), null=True, diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 468bd5838..c34666a6a 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -1437,8 +1437,8 @@ class OrderChangeManager: RegenerateSecretOperation = namedtuple('RegenerateSecretOperation', ('position',)) ChangeValidFromOperation = namedtuple('ChangeValidFromOperation', ('position', 'valid_from')) ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until')) - AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name')) - RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name')) + AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked')) + RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked')) def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True): self.order = order @@ -1548,11 +1548,11 @@ class OrderChangeManager: def change_valid_until(self, position: OrderPosition, new_value: datetime): self._operations.append(self.ChangeValidUntilOperation(position, new_value)) - def add_block(self, position: OrderPosition, block_name: str): - self._operations.append(self.AddBlockOperation(position, block_name)) + def add_block(self, position: OrderPosition, block_name: str, ignore_from_quota_while_blocked=None): + self._operations.append(self.AddBlockOperation(position, block_name, ignore_from_quota_while_blocked)) - def remove_block(self, position: OrderPosition, block_name: str): - self._operations.append(self.RemoveBlockOperation(position, block_name)) + def remove_block(self, position: OrderPosition, block_name: str, ignore_from_quota_while_blocked=None): + self._operations.append(self.RemoveBlockOperation(position, block_name, ignore_from_quota_while_blocked)) def change_price(self, position: OrderPosition, price: Decimal): tax_rule = self._current_tax_rules().get(position.pk, position.tax_rule) or TaxRule.zero() @@ -2258,7 +2258,9 @@ class OrderChangeManager: op.position.blocked = op.position.blocked + [op.block_name] else: op.position.blocked = [op.block_name] - op.position.save(update_fields=['blocked']) + if op.ignore_from_quota_while_blocked is not None: + op.position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked + op.position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked']) if op.position.blocked: op.position.blocked_secrets.update_or_create( event=self.event, @@ -2278,7 +2280,9 @@ class OrderChangeManager: op.position.blocked = [b for b in op.position.blocked if b != op.block_name] if not op.position.blocked: op.position.blocked = None - op.position.save(update_fields=['blocked']) + if op.ignore_from_quota_while_blocked is not None: + op.position.ignore_from_quota_while_blocked = op.ignore_from_quota_while_blocked + op.position.save(update_fields=['blocked', 'ignore_from_quota_while_blocked']) if not op.position.blocked: try: bs = op.position.blocked_secrets.get(secret=op.position.secret) diff --git a/src/pretix/base/services/quotas.py b/src/pretix/base/services/quotas.py index 02ead7ca2..2a116a69a 100644 --- a/src/pretix/base/services/quotas.py +++ b/src/pretix/base/services/quotas.py @@ -295,6 +295,8 @@ class QuotaAvailability: Q(item_id__in={i['item_id'] for i in q_items if i['quota_id'] in quota_ids}) ) | Q( variation_id__in={i['itemvariation_id'] for i in q_vars if i['quota_id'] in quota_ids}) + ).filter( + ~Q(Q(ignore_from_quota_while_blocked=True) & Q(blocked__isnull=False)) ).order_by() if any(q.release_after_exit for q in quotas): op_lookup = op_lookup.annotate( diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index 205abe8b1..2c41c090b 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -483,6 +483,22 @@ class QuotaTestCase(BaseQuotaTestCase): with self.assertNumQueries(1): self.assertEqual(self.var1.check_quotas(_cache=cache, count_waitinglist=False), (Quota.AVAILABILITY_OK, 1)) + @classscope(attr='o') + def test_ignore_if_blocked(self): + q1 = self.event.quotas.create(name="Q1", size=50) + q1.items.add(self.item1) + + # Create orders + order = Order.objects.create(event=self.event, status=Order.STATUS_PAID, + expires=now() + timedelta(days=3), + total=6) + OrderPosition.objects.create(order=order, item=self.item1, price=2) + OrderPosition.objects.create(order=order, item=self.item1, price=2, blocked=["foo"]) + OrderPosition.objects.create(order=order, item=self.item1, price=2, blocked=["foo"], ignore_from_quota_while_blocked=True) + OrderPosition.objects.create(order=order, item=self.item1, price=2, ignore_from_quota_while_blocked=True) + + self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 47)) + @classscope(attr='o') def test_subevent_isolation(self): self.event.has_subevents = True