forked from CGM_Public/pretix_original
Self-service order change: Respect Item.max/min_per_order (Z#23122195) (#3319)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -1442,6 +1442,16 @@ class OrderChangeManager:
|
|||||||
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
|
'seat_forbidden': gettext_lazy('The selected product does not allow to select a seat.'),
|
||||||
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
|
'tax_rule_country_blocked': gettext_lazy('The selected country is blocked by your tax rule.'),
|
||||||
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
|
'gift_card_change': gettext_lazy('You cannot change the price of a position that has been used to issue a gift card.'),
|
||||||
|
'max_items_per_product': ngettext_lazy(
|
||||||
|
"You cannot select more than %(max)s item of the product %(product)s.",
|
||||||
|
"You cannot select more than %(max)s items of the product %(product)s.",
|
||||||
|
"max"
|
||||||
|
),
|
||||||
|
'min_items_per_product': ngettext_lazy(
|
||||||
|
"You need to select at least %(min)s item of the product %(product)s.",
|
||||||
|
"You need to select at least %(min)s items of the product %(product)s.",
|
||||||
|
"min"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
ItemOperation = namedtuple('ItemOperation', ('position', 'item', 'variation'))
|
||||||
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
SubeventOperation = namedtuple('SubeventOperation', ('position', 'subevent'))
|
||||||
@@ -1744,6 +1754,11 @@ class OrderChangeManager:
|
|||||||
if self._operations:
|
if self._operations:
|
||||||
raise ValueError("Setting addons should be the first/only operation")
|
raise ValueError("Setting addons should be the first/only operation")
|
||||||
|
|
||||||
|
# Prepare containers for min/max check of products
|
||||||
|
item_counts = Counter()
|
||||||
|
for p in self.order.positions.all():
|
||||||
|
item_counts[p.item] += 1
|
||||||
|
|
||||||
# Prepare various containers to hold data later
|
# Prepare various containers to hold data later
|
||||||
current_addons = defaultdict(lambda: defaultdict(list)) # OrderPos -> currently attached add-ons
|
current_addons = defaultdict(lambda: defaultdict(list)) # OrderPos -> currently attached add-ons
|
||||||
input_addons = defaultdict(Counter) # OrderPos -> final desired set of add-ons
|
input_addons = defaultdict(Counter) # OrderPos -> final desired set of add-ons
|
||||||
@@ -1880,6 +1895,7 @@ class OrderChangeManager:
|
|||||||
item=item, variation=variation, price=price,
|
item=item, variation=variation, price=price,
|
||||||
addon_to=op, subevent=op.subevent, seat=None,
|
addon_to=op, subevent=op.subevent, seat=None,
|
||||||
)
|
)
|
||||||
|
item_counts[item] += 1
|
||||||
|
|
||||||
# Check constraints on the add-on combinations
|
# Check constraints on the add-on combinations
|
||||||
for op in toplevel_op:
|
for op in toplevel_op:
|
||||||
@@ -1929,6 +1945,27 @@ class OrderChangeManager:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.cancel(a)
|
self.cancel(a)
|
||||||
|
item_counts[a.item] -= 1
|
||||||
|
|
||||||
|
for item, count in item_counts.items():
|
||||||
|
if count == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.max_per_order and count > item.max_per_order:
|
||||||
|
raise OrderError(
|
||||||
|
self.error_messages['max_items_per_product'] % {
|
||||||
|
'max': item.max_per_order,
|
||||||
|
'product': item.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.min_per_order and count < item.min_per_order:
|
||||||
|
raise OrderError(
|
||||||
|
self.error_messages['min_items_per_product'] % {
|
||||||
|
'min': item.min_per_order,
|
||||||
|
'product': item.name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def _check_seats(self):
|
def _check_seats(self):
|
||||||
for seat, diff in self._seatdiff.items():
|
for seat, diff in self._seatdiff.items():
|
||||||
|
|||||||
@@ -1293,6 +1293,75 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
|||||||
)
|
)
|
||||||
assert 'alert-danger' in response.content.decode()
|
assert 'alert-danger' in response.content.decode()
|
||||||
|
|
||||||
|
def test_max_per_order_enforced(self):
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||||
|
{
|
||||||
|
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '2'
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
assert 'alert-danger' in response.content.decode()
|
||||||
|
|
||||||
|
self.workshop2.max_per_order = 2
|
||||||
|
self.workshop2.save()
|
||||||
|
self.iao.multi_allowed = True
|
||||||
|
self.iao.max_count = 10
|
||||||
|
self.iao.save()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||||
|
{
|
||||||
|
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '2'
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
assert 'alert-danger' not in response.content.decode()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||||
|
{
|
||||||
|
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '3'
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
assert 'alert-danger' in response.content.decode()
|
||||||
|
|
||||||
|
def test_min_per_order_enforced(self):
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||||
|
{
|
||||||
|
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '2'
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
assert 'alert-danger' in response.content.decode()
|
||||||
|
|
||||||
|
self.workshop2.min_per_order = 2
|
||||||
|
self.workshop2.save()
|
||||||
|
self.iao.multi_allowed = True
|
||||||
|
self.iao.max_count = 10
|
||||||
|
self.iao.save()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||||
|
{
|
||||||
|
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '2'
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
print(response.content.decode())
|
||||||
|
assert 'alert-danger' not in response.content.decode()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||||
|
{
|
||||||
|
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
|
||||||
|
},
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
assert 'alert-danger' in response.content.decode()
|
||||||
|
|
||||||
def test_allow_user_price_gte(self):
|
def test_allow_user_price_gte(self):
|
||||||
self.event.settings.change_allow_user_price = 'gte'
|
self.event.settings.change_allow_user_price = 'gte'
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
|
|||||||
Reference in New Issue
Block a user