mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Allow selecting the same add-on multiple times (#1717)
This commit is contained in:
20
src/pretix/base/migrations/0157_auto_20200712_0932.py
Normal file
20
src/pretix/base/migrations/0157_auto_20200712_0932.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.0.6 on 2020-07-12 09:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.helpers.countries
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0156_cartposition_override_tax_rate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='itemaddon',
|
||||
name='multi_allowed',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -840,6 +840,10 @@ class ItemAddOn(models.Model):
|
||||
help_text=_('If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost '
|
||||
'money individually.')
|
||||
)
|
||||
multi_allowed = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Allow the same product to be selected multiple times'),
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
|
||||
@@ -96,6 +96,7 @@ error_messages = {
|
||||
'addon_max_count': _('You can select at most %(max)s add-ons from the category %(cat)s for the product %(base)s.'),
|
||||
'addon_min_count': _('You need to select at least %(min)s add-ons from the category %(cat)s for the '
|
||||
'product %(base)s.'),
|
||||
'addon_no_multi': _('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'),
|
||||
'addon_only': _('One of the products you selected can only be bought as an add-on to another project.'),
|
||||
'bundled_only': _('One of the products you selected can only be bought part of a bundle.'),
|
||||
'seat_required': _('You need to select a specific seat.'),
|
||||
@@ -605,9 +606,9 @@ class CartManager:
|
||||
)
|
||||
|
||||
# Prepare various containers to hold data later
|
||||
current_addons = defaultdict(dict) # CartPos -> currently attached add-ons
|
||||
input_addons = defaultdict(set) # CartPos -> add-ons according to input
|
||||
selected_addons = defaultdict(set) # CartPos -> final desired set of add-ons
|
||||
current_addons = defaultdict(lambda: defaultdict(list)) # CartPos -> currently attached add-ons
|
||||
input_addons = defaultdict(Counter) # CartPos -> final desired set of add-ons
|
||||
selected_addons = defaultdict(Counter) # CartPos, ItemAddOn -> final desired set of add-ons
|
||||
cpcache = {} # CartPos.pk -> CartPos
|
||||
quota_diff = Counter() # Quota -> Number of usages
|
||||
operations = []
|
||||
@@ -624,11 +625,9 @@ class CartManager:
|
||||
available_categories[cp.pk] = {iao.addon_category_id for iao in cp.item.addons.all()}
|
||||
price_included[cp.pk] = {iao.addon_category_id: iao.price_included for iao in cp.item.addons.all()}
|
||||
cpcache[cp.pk] = cp
|
||||
current_addons[cp] = {
|
||||
(a.item_id, a.variation_id): a
|
||||
for a in cp.addons.all()
|
||||
if not a.is_bundled
|
||||
}
|
||||
for a in cp.addons.all():
|
||||
if not a.is_bundled:
|
||||
current_addons[cp][a.item_id, a.variation_id].append(a)
|
||||
|
||||
# Create operations, perform various checks
|
||||
for a in addons:
|
||||
@@ -655,25 +654,31 @@ class CartManager:
|
||||
if not quotas:
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
# Every item can be attached to very CartPosition at most once
|
||||
if a['item'] in ([_a[0] for _a in input_addons[cp.id]]):
|
||||
if (a['item'], a['variation']) in input_addons[cp.id]:
|
||||
raise CartError(error_messages['addon_duplicate_item'])
|
||||
|
||||
input_addons[cp.id].add((a['item'], a['variation']))
|
||||
selected_addons[cp.id, item.category_id].add((a['item'], a['variation']))
|
||||
input_addons[cp.id][a['item'], a['variation']] = a.get('count', 1)
|
||||
selected_addons[cp.id, item.category_id][a['item'], a['variation']] = a.get('count', 1)
|
||||
|
||||
if (a['item'], a['variation']) not in current_addons[cp]:
|
||||
if price_included[cp.pk].get(item.category_id):
|
||||
price = TAXED_ZERO
|
||||
else:
|
||||
price = self._get_price(item, variation, None, a.get('price'), cp.subevent)
|
||||
|
||||
# Fix positions with wrong price (TODO: happens out-of-cartmanager-transaction and therefore a little hacky)
|
||||
for ca in current_addons[cp][a['item'], a['variation']]:
|
||||
if ca.price != price.gross:
|
||||
ca.price = price.gross
|
||||
ca.save(update_fields=['price'])
|
||||
|
||||
if a.get('count', 1) > len(current_addons[cp][a['item'], a['variation']]):
|
||||
# This add-on is new, add it to the cart
|
||||
for quota in quotas:
|
||||
quota_diff[quota] += 1
|
||||
|
||||
if price_included[cp.pk].get(item.category_id):
|
||||
price = TAXED_ZERO
|
||||
else:
|
||||
price = self._get_price(item, variation, None, None, cp.subevent)
|
||||
quota_diff[quota] += a.get('count', 1) - len(current_addons[cp][a['item'], a['variation']])
|
||||
|
||||
op = self.AddOperation(
|
||||
count=1, item=item, variation=variation, price=price, voucher=None, quotas=quotas,
|
||||
count=a.get('count', 1) - len(current_addons[cp][a['item'], a['variation']]),
|
||||
item=item, variation=variation, price=price, voucher=None, quotas=quotas,
|
||||
addon_to=cp, subevent=cp.subevent, includes_tax=bool(price.rate), bundled=[], seat=None,
|
||||
price_before_voucher=None
|
||||
)
|
||||
@@ -685,7 +690,10 @@ class CartManager:
|
||||
item = cp.item
|
||||
for iao in item.addons.all():
|
||||
selected = selected_addons[cp.id, iao.addon_category_id]
|
||||
if len(selected) > iao.max_count:
|
||||
n_per_i = Counter()
|
||||
for (i, v), c in selected.items():
|
||||
n_per_i[i] += c
|
||||
if sum(selected.values()) > iao.max_count:
|
||||
# TODO: Proper i18n
|
||||
# TODO: Proper pluralization
|
||||
raise CartError(
|
||||
@@ -696,7 +704,7 @@ class CartManager:
|
||||
'cat': str(iao.addon_category.name),
|
||||
}
|
||||
)
|
||||
elif len(selected) < iao.min_count:
|
||||
elif sum(selected.values()) < iao.min_count:
|
||||
# TODO: Proper i18n
|
||||
# TODO: Proper pluralization
|
||||
raise CartError(
|
||||
@@ -707,28 +715,39 @@ class CartManager:
|
||||
'cat': str(iao.addon_category.name),
|
||||
}
|
||||
)
|
||||
elif any(v > 1 for v in n_per_i.values()) and not iao.multi_allowed:
|
||||
raise CartError(
|
||||
error_messages['addon_no_multi'],
|
||||
{
|
||||
'base': str(item.name),
|
||||
'cat': str(iao.addon_category.name),
|
||||
}
|
||||
)
|
||||
validate_cart_addons.send(
|
||||
sender=self.event,
|
||||
addons={
|
||||
(self._items_cache[s[0]], self._variations_cache[s[1]] if s[1] else None)
|
||||
for s in selected
|
||||
(self._items_cache[s[0]], self._variations_cache[s[1]] if s[1] else None): c
|
||||
for s, c in selected.items() if c > 0
|
||||
},
|
||||
base_position=cp,
|
||||
iao=iao
|
||||
)
|
||||
|
||||
# Detect removed add-ons and create RemoveOperations
|
||||
for cp, al in current_addons.items():
|
||||
for cp, al in list(current_addons.items()):
|
||||
for k, v in al.items():
|
||||
if k not in input_addons[cp.id]:
|
||||
if v.expires > self.now_dt:
|
||||
quotas = list(v.quotas)
|
||||
input_num = input_addons[cp.id].get(k, 0)
|
||||
current_num = len(current_addons[cp].get(k, []))
|
||||
if input_num < current_num:
|
||||
for a in current_addons[cp][k][:current_num - input_num]:
|
||||
if a.expires > self.now_dt:
|
||||
quotas = list(a.quotas)
|
||||
|
||||
for quota in quotas:
|
||||
quota_diff[quota] -= 1
|
||||
for quota in quotas:
|
||||
quota_diff[quota] -= 1
|
||||
|
||||
op = self.RemoveOperation(position=v)
|
||||
operations.append(op)
|
||||
op = self.RemoveOperation(position=a)
|
||||
operations.append(op)
|
||||
|
||||
self._quota_diff.update(quota_diff)
|
||||
self._operations += operations
|
||||
|
||||
@@ -309,7 +309,7 @@ validate_cart_addons = EventPluginSignal(
|
||||
"""
|
||||
This signal is sent when a user tries to select a combination of addons. In contrast to
|
||||
``validate_cart``, this is executed before the cart is actually modified. You are passed
|
||||
an argument ``addons`` containing a set of ``(item, variation or None)`` tuples as well
|
||||
an argument ``addons`` containing a dict of ``(item, variation or None) → count`` tuples as well
|
||||
as the ``ItemAddOn`` object as the argument ``iao`` and the base cart position as
|
||||
``base_position``.
|
||||
The response of receivers will be ignored, but you can raise a CartError with an
|
||||
|
||||
Reference in New Issue
Block a user