forked from CGM_Public/pretix_original
Cross selling (#4185)
Product categories can now be marked as "cross-selling categories", causing them to appear in the add-on checkout step as additional recommendations, depending on their cross-selling visibility (always, only if certain products are already in the cart, or only if they qualify for a discount according to discount rules). --------- Co-authored-by: Raphael Michel <michel@rami.io> Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -318,14 +318,14 @@ def cart_exists(request):
|
||||
|
||||
def get_cart(request):
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
qqs = request.event.questions.all()
|
||||
qqs = qqs.filter(ask_during_checkin=False, hidden=False)
|
||||
|
||||
if not hasattr(request, '_cart_cache'):
|
||||
cart_id = get_or_create_cart_id(request, create=False)
|
||||
if not cart_id:
|
||||
request._cart_cache = CartPosition.objects.none()
|
||||
else:
|
||||
qqs = request.event.questions.all()
|
||||
qqs = qqs.filter(ask_during_checkin=False, hidden=False)
|
||||
request._cart_cache = CartPosition.objects.filter(
|
||||
cart_id=cart_id, event=request.event
|
||||
).annotate(
|
||||
|
||||
@@ -151,104 +151,114 @@ class CartActionMixin:
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
return InvoiceAddress()
|
||||
|
||||
def _item_from_post_value(self, key, value, voucher=None, voucher_ignore_if_redeemed=False):
|
||||
if value.strip() == '' or '_' not in key:
|
||||
return
|
||||
|
||||
if not key.startswith('item_') and not key.startswith('variation_') and not key.startswith('seat_'):
|
||||
return
|
||||
|
||||
parts = key.split("_")
|
||||
price = self.request.POST.get('price_' + "_".join(parts[1:]), "")
|
||||
subevent = None
|
||||
if 'subevent' in self.request.POST:
|
||||
try:
|
||||
subevent = int(self.request.POST.get('subevent'))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if key.startswith('seat_'):
|
||||
try:
|
||||
return {
|
||||
'item': int(parts[1]),
|
||||
'variation': int(parts[2]) if len(parts) > 2 else None,
|
||||
'count': 1,
|
||||
'seat': value,
|
||||
'price': price,
|
||||
'voucher': voucher,
|
||||
'voucher_ignore_if_redeemed': voucher_ignore_if_redeemed,
|
||||
'subevent': subevent
|
||||
}
|
||||
except ValueError:
|
||||
raise CartError(_('Please enter numbers only.'))
|
||||
def _item_from_post_value(request, key, value, voucher=None, voucher_ignore_if_redeemed=False):
|
||||
if value.strip() == '' or '_' not in key:
|
||||
return
|
||||
|
||||
subevent = None
|
||||
if key.startswith('subevent_'):
|
||||
try:
|
||||
amount = int(value)
|
||||
parts = key.split('_', 2)
|
||||
subevent = int(parts[1])
|
||||
key = parts[2]
|
||||
except ValueError:
|
||||
pass
|
||||
elif 'subevent' in request.POST:
|
||||
try:
|
||||
subevent = int(request.POST.get('subevent'))
|
||||
except ValueError:
|
||||
raise CartError(_('Please enter numbers only.'))
|
||||
if amount < 0:
|
||||
raise CartError(_('Please enter positive numbers only.'))
|
||||
elif amount == 0:
|
||||
return
|
||||
|
||||
if key.startswith('item_'):
|
||||
try:
|
||||
return {
|
||||
'item': int(parts[1]),
|
||||
'variation': None,
|
||||
'count': amount,
|
||||
'price': price,
|
||||
'voucher': voucher,
|
||||
'voucher_ignore_if_redeemed': voucher_ignore_if_redeemed,
|
||||
'subevent': subevent
|
||||
}
|
||||
except ValueError:
|
||||
raise CartError(_('Please enter numbers only.'))
|
||||
elif key.startswith('variation_'):
|
||||
try:
|
||||
return {
|
||||
'item': int(parts[1]),
|
||||
'variation': int(parts[2]),
|
||||
'count': amount,
|
||||
'price': price,
|
||||
'voucher': voucher,
|
||||
'voucher_ignore_if_redeemed': voucher_ignore_if_redeemed,
|
||||
'subevent': subevent
|
||||
}
|
||||
except ValueError:
|
||||
raise CartError(_('Please enter numbers only.'))
|
||||
|
||||
def _items_from_post_data(self):
|
||||
"""
|
||||
Parses the POST data and returns a list of dictionaries
|
||||
"""
|
||||
|
||||
# Compatibility patch that makes the frontend code a lot easier
|
||||
req_items = list(self.request.POST.lists())
|
||||
if '_voucher_item' in self.request.POST and '_voucher_code' in self.request.POST:
|
||||
req_items.append((
|
||||
'%s' % self.request.POST['_voucher_item'], ('1',)
|
||||
))
|
||||
pass
|
||||
|
||||
items = []
|
||||
if 'raw' in self.request.POST:
|
||||
items += json.loads(self.request.POST.get("raw"))
|
||||
for key, values in req_items:
|
||||
for value in values:
|
||||
try:
|
||||
item = self._item_from_post_value(key, value, self.request.POST.get('_voucher_code'),
|
||||
voucher_ignore_if_redeemed=self.request.POST.get('_voucher_ignore_if_redeemed') == 'on')
|
||||
except CartError as e:
|
||||
messages.error(self.request, str(e))
|
||||
return
|
||||
if item:
|
||||
items.append(item)
|
||||
if not key.startswith('item_') and not key.startswith('variation_') and not key.startswith('seat_'):
|
||||
return
|
||||
|
||||
if len(items) == 0:
|
||||
messages.warning(self.request, _('You did not select any products.'))
|
||||
return []
|
||||
return items
|
||||
parts = key.split("_")
|
||||
price = request.POST.get('price_' + "_".join(parts[1:]), "")
|
||||
|
||||
if key.startswith('seat_'):
|
||||
try:
|
||||
return {
|
||||
'item': int(parts[1]),
|
||||
'variation': int(parts[2]) if len(parts) > 2 else None,
|
||||
'count': 1,
|
||||
'seat': value,
|
||||
'price': price,
|
||||
'voucher': voucher,
|
||||
'voucher_ignore_if_redeemed': voucher_ignore_if_redeemed,
|
||||
'subevent': subevent
|
||||
}
|
||||
except ValueError:
|
||||
raise CartError(_('Please enter numbers only.'))
|
||||
|
||||
try:
|
||||
amount = int(value)
|
||||
except ValueError:
|
||||
raise CartError(_('Please enter numbers only.'))
|
||||
if amount < 0:
|
||||
raise CartError(_('Please enter positive numbers only.'))
|
||||
elif amount == 0:
|
||||
return
|
||||
|
||||
if key.startswith('item_'):
|
||||
try:
|
||||
return {
|
||||
'item': int(parts[1]),
|
||||
'variation': None,
|
||||
'count': amount,
|
||||
'price': price,
|
||||
'voucher': voucher,
|
||||
'voucher_ignore_if_redeemed': voucher_ignore_if_redeemed,
|
||||
'subevent': subevent
|
||||
}
|
||||
except ValueError:
|
||||
raise CartError(_('Please enter numbers only.'))
|
||||
elif key.startswith('variation_'):
|
||||
try:
|
||||
return {
|
||||
'item': int(parts[1]),
|
||||
'variation': int(parts[2]),
|
||||
'count': amount,
|
||||
'price': price,
|
||||
'voucher': voucher,
|
||||
'voucher_ignore_if_redeemed': voucher_ignore_if_redeemed,
|
||||
'subevent': subevent
|
||||
}
|
||||
except ValueError:
|
||||
raise CartError(_('Please enter numbers only.'))
|
||||
|
||||
|
||||
def _items_from_post_data(request, warn_if_empty=True):
|
||||
"""
|
||||
Parses the POST data and returns a list of dictionaries
|
||||
"""
|
||||
|
||||
# Compatibility patch that makes the frontend code a lot easier
|
||||
req_items = list(request.POST.lists())
|
||||
if '_voucher_item' in request.POST and '_voucher_code' in request.POST:
|
||||
req_items.append((
|
||||
'%s' % request.POST['_voucher_item'], ('1',)
|
||||
))
|
||||
pass
|
||||
|
||||
items = []
|
||||
if 'raw' in request.POST:
|
||||
items += json.loads(request.POST.get("raw"))
|
||||
for key, values in req_items:
|
||||
for value in values:
|
||||
try:
|
||||
item = _item_from_post_value(request, key, value, request.POST.get('_voucher_code'),
|
||||
voucher_ignore_if_redeemed=request.POST.get('_voucher_ignore_if_redeemed') == 'on')
|
||||
except CartError as e:
|
||||
messages.error(request, str(e))
|
||||
return
|
||||
if item:
|
||||
items.append(item)
|
||||
|
||||
if len(items) == 0 and warn_if_empty:
|
||||
messages.warning(request, _('You did not select any products.'))
|
||||
return []
|
||||
return items
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@@ -534,7 +544,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
cs = cart_session(request)
|
||||
widget_data = cs.get('widget_data', {})
|
||||
|
||||
items = self._items_from_post_data()
|
||||
items = _items_from_post_data(self.request)
|
||||
if items:
|
||||
return self.do(self.request.event.id, items, cart_id, translation.get_language(),
|
||||
self.invoice_address.pk, widget_data, self.request.sales_channel.identifier,
|
||||
|
||||
@@ -111,7 +111,8 @@ def item_group_by_category(items):
|
||||
)
|
||||
|
||||
|
||||
def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None, allow_addons=False,
|
||||
def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None,
|
||||
allow_addons=False, allow_cross_sell=False,
|
||||
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
||||
ignore_hide_sold_out_for_item_ids=None):
|
||||
base_qs_set = base_qs is not None
|
||||
@@ -193,7 +194,9 @@ def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=No
|
||||
)
|
||||
)
|
||||
|
||||
items = base_qs.using(settings.DATABASE_REPLICA).filter_available(channel=channel.identifier, voucher=voucher, allow_addons=allow_addons).select_related(
|
||||
items = base_qs.using(settings.DATABASE_REPLICA).filter_available(
|
||||
channel=channel.identifier, voucher=voucher, allow_addons=allow_addons, allow_cross_sell=allow_cross_sell
|
||||
).select_related(
|
||||
'category', 'tax_rule', # for re-grouping
|
||||
'hidden_if_available',
|
||||
).prefetch_related(
|
||||
|
||||
Reference in New Issue
Block a user