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:
Mira
2024-10-14 14:39:49 +02:00
committed by GitHub
parent 7607cc5d2f
commit 359df1f51e
24 changed files with 1737 additions and 218 deletions

View File

@@ -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,