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

@@ -65,6 +65,7 @@ from pretix.base.services.cart import (
CartError, CartManager, add_payment_to_cart, error_messages, get_fees,
set_cart_addons,
)
from pretix.base.services.cross_selling import CrossSellingService
from pretix.base.services.memberships import validate_memberships_in_order
from pretix.base.services.orders import perform_order
from pretix.base.services.tasks import EventTask
@@ -93,7 +94,8 @@ from pretix.presale.views import (
CartMixin, get_cart, get_cart_is_free, get_cart_total,
)
from pretix.presale.views.cart import (
cart_session, create_empty_cart_id, get_or_create_cart_id,
_items_from_post_data, cart_session, create_empty_cart_id,
get_or_create_cart_id,
)
from pretix.presale.views.event import get_grouped_items
from pretix.presale.views.questions import QuestionsViewMixin
@@ -486,9 +488,31 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
label = pgettext_lazy('checkoutflow', 'Add-on products')
icon = 'puzzle-piece'
def _check_is_applicable(self, request):
self.request = request
# check whether addons are applicable
if get_cart(request).filter(item__addons__isnull=False).exists():
return True
# don't re-check whether cross-selling is applicable if we're already past the AddOnsStep
cur_step_identifier = request.resolver_match.kwargs.get('step')
is_past_this_step = any(step.identifier == cur_step_identifier for step in request._checkout_flow[request._checkout_flow.index(self) + 1:])
if is_past_this_step:
applicable = self.cart_session.get('_checkoutflow_addons_applicable', None)
if applicable is not None:
return applicable
# check whether cross-selling is applicable
applicable = self.cross_selling_is_applicable
self.cart_session['_checkoutflow_addons_applicable'] = applicable
return applicable
def is_applicable(self, request):
if not hasattr(request, '_checkoutflow_addons_applicable'):
request._checkoutflow_addons_applicable = get_cart(request).filter(item__addons__isnull=False).exists()
cur_step_identifier = request.resolver_match.kwargs.get('step')
request._checkoutflow_addons_applicable = self._check_is_applicable(request) or cur_step_identifier == self.identifier
return request._checkoutflow_addons_applicable
def is_completed(self, request, warn=False):
@@ -605,10 +629,21 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
formset.append(formsetentry)
return formset
@cached_property
def cross_selling_is_applicable(self):
return any(len(items) > 0 for (category, items, form_prefix) in self.cross_selling_data)
@cached_property
def cross_selling_data(self):
return CrossSellingService(
self.request.event, self.request.sales_channel, self.positions, self.request.customer
).get_data()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['forms'] = self.forms
ctx['cart'] = self.get_cart()
ctx['cross_selling_data'] = self.cross_selling_data
return ctx
def get_success_message(self, value):
@@ -687,7 +722,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
def post(self, request, *args, **kwargs):
self.request = request
data = []
addons = []
for f in self.forms:
for c in f['categories']:
try:
@@ -697,7 +732,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
return self.get(request, *args, **kwargs)
for (i, v), (c, price) in selected.items():
data.append({
addons.append({
'addon_to': f['pos'].pk,
'item': i.pk,
'variation': v.pk if v else None,
@@ -705,7 +740,9 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
'price': price,
})
return self.do(self.request.event.id, data, get_or_create_cart_id(self.request),
add_to_cart_items = _items_from_post_data(self.request, warn_if_empty=False)
return self.do(self.request.event.id, addons, add_to_cart_items, get_or_create_cart_id(self.request),
invoice_address=self.invoice_address.pk, locale=get_language(),
sales_channel=request.sales_channel.identifier, override_now_dt=time_machine_now(default=None))

View File

@@ -6,9 +6,12 @@
{% load money %}
{% load thumb %}
{% block inner %}
<p>
{% trans "For some of the products in your cart, you can choose additional options before you continue." %}
</p>
{% if forms %}
<p>
{% trans "For some of the products in your cart, you can choose additional options before you continue." %}
</p>
{% endif %}
<form class="form-horizontal" method="post" data-asynctask
data-asynctask-headline="{% trans "We're now trying to book these add-ons for you!" %}">
{% csrf_token %}
@@ -17,10 +20,12 @@
<details class="panel panel-default" open>
<summary class="panel-heading">
<h3 class="panel-title">
<span class="sr-only">{% trans "Add-ons:" %}</span>
<strong>{{ form.item.name }}{% if form.variation %}
{{ form.variation }}
{% endif %}</strong>
<span>
{% trans "Additional options for" %}
<strong>{{ form.item.name }}{% if form.variation %}
{{ form.variation }}
{% endif %}</strong>
</span>
</h3>
</summary>
<div id="cp{{ form.pos.pk }}">
@@ -46,6 +51,20 @@
</details>
{% endfor %}
</div>
{% if cross_selling_data %}
<details class="panel panel-default cross-selling" open>
<summary class="panel-heading">
<h3 class="panel-title">
{% trans "More recommendations" %}
</h3>
</summary>
<div class="panel-body">
{% include "pretixpresale/event/fragment_product_list.html" with items_by_category=cross_selling_data ev=event %}
</div>
</details>
{% endif %}
<div class="row checkout-button-row">
<div class="col-md-4 col-sm-6">
<a class="btn btn-block btn-default btn-lg"

View File

@@ -5,20 +5,31 @@
{% load thumb %}
{% load eventsignal %}
{% load rich_text %}
{% for tup in items_by_category %}
{% if tup.0 %}
<section aria-labelledby="category-{{ tup.0.id }}"{% if tup.0.description %} aria-describedby="category-info-{{ tup.0.id }}"{% endif %}>
<h3 id="category-{{ tup.0.id }}">{{ tup.0.name }}</h3>
{% if tup.0.description %}
<div id="category-info-{{ tup.0.id }}">{{ tup.0.description|localize|rich_text }}</div>
{% for tup in items_by_category %}{% with category=tup.0 items=tup.1 form_prefix=tup.2 %}
{% if category %}
<section aria-labelledby="{{ form_prefix }}category-{{ category.id }}"{% if category.description %} aria-describedby="{{ form_prefix }}category-info-{{ category.id }}"{% endif %}>
<h3 id="{{ form_prefix }}category-{{ category.id }}">{{ category.name }}
{% if category.subevent_name %}
<small class="text-muted"><i class="fa fa-calendar"></i> {{ category.subevent_name }}</small>
{% endif %}
{% if category.category_has_discount %}
<small class="text-success">
<i class="fa fa-star" aria-hidden="true"></i>
<span class="sr-only">Congratulations!</span>
{% trans "Your order qualifies for a discount" %}
</small>
{% endif %}
</h3>
{% if category.description %}
<div id="{{ form_prefix }}category-info-{{ category.id }}">{{ category.description|localize|rich_text }}</div>
{% endif %}
{% else %}
<section aria-labelledby="category-none">
<h3 id="category-none" class="sr-only">{% trans "Uncategorized items" %}</h3>
<section aria-labelledby="{{ form_prefix }}category-none">
<h3 id="{{ form_prefix }}category-none" class="sr-only">{% trans "Uncategorized items" %}</h3>
{% endif %}
{% for item in tup.1 %}
{% for item in items %}
{% if item.has_variations %}
<article aria-labelledby="item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="item-{{ item.pk }}-description"{% endif %} class="item-with-variations{% if event.settings.show_variations_expanded %} details-open{% endif %}" id="item-{{ item.pk }}">
<article aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="{{ form_prefix }}item-{{ item.pk }}-description"{% endif %} class="item-with-variations{% if event.settings.show_variations_expanded %} details-open{% endif %}" id="{{ form_prefix }}item-{{ item.pk }}">
<div class="row product-row headline">
<div class="col-md-8 col-sm-6 col-xs-12">
{% if item.picture %}
@@ -32,9 +43,9 @@
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h4 id="item-{{ item.pk }}-legend">{{ item.name }}</h4>
<h4 id="{{ form_prefix }}item-{{ item.pk }}-legend">{{ item.name }}</h4>
{% if item.description %}
<div id="item-{{ item.pk }}-description" class="product-description">
<div id="{{ form_prefix }}item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
@@ -101,14 +112,14 @@
</div>
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}">
{% for var in item.available_variations %}
<article aria-labelledby="item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row product-row variation" id="item-{{ item.pk }}-{{ var.pk }}"
<article aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row product-row variation" id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}"
{% if not item.free_price %}
data-price="{% if event.settings.display_net_prices %}{{ var.display_price.net|unlocalize }}{% else %}{{ var.display_price.gross|unlocalize }}{% endif %}"
{% endif %}>
<div class="col-md-8 col-sm-6 col-xs-12">
<h5 id="item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h5>
<h5 id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend">{{ var }}</h5>
{% if var.description %}
<div id="item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description">
<div id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description" class="variation-description">
{{ var.description|localize|rich_text }}
</div>
{% endif %}
@@ -139,11 +150,11 @@
<div class="input-group input-group-price">
<span class="input-group-addon">{{ event.currency }}</span>
<input type="number" class="form-control input-item-price"
id="price-variation-{{ item.pk }}-{{ var.pk }}"
id="{{ form_prefix }}price-variation-{{ item.pk }}-{{ var.pk }}"
{% if not ev.presale_is_running %}disabled{% endif %}
placeholder="0"
min="{% if event.settings.display_net_prices %}{{ var.display_price.net|money_numberfield:event.currency }}{% else %}{{ var.display_price.gross|money_numberfield:event.currency }}{% endif %}"
name="price_{{ item.id }}_{{ var.id }}"
name="{{ form_prefix }}price_{{ item.id }}_{{ var.id }}"
{% if var.suggested_price.gross != var.display_price.gross %}
{% if event.settings.display_net_prices %}
title="{% blocktrans trimmed with item=var.value price=var.display_price.net|money:event.currency %}Modify price for {{ item }}, at least {{ price }}{% endblocktrans %}"
@@ -200,16 +211,16 @@
data-checked-onchange="price-variation-{{ item.pk }}-{{ var.pk }}"
{% endif %}
{% if not ev.presale_is_running %}disabled{% endif %}
id="variation_{{ item.id }}_{{ var.id }}"
name="variation_{{ item.id }}_{{ var.id }}"
id="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
name="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}"
{% if var.description %} aria-describedby="item-{{ item.pk }}-{{ var.pk }}-description"{% endif %}>
{% if var.description %} aria-describedby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description"{% endif %}>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<div class="input-item-count-group">
<button type="button" data-step="-1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}"
<button type="button" data-step="-1" data-controls="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
@@ -217,10 +228,10 @@
data-checked-onchange="price-variation-{{ item.pk }}-{{ var.pk }}"
{% endif %}
max="{{ var.order_max }}"
id="variation_{{ item.id }}_{{ var.id }}"
name="variation_{{ item.id }}_{{ var.id }}"
id="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
name="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}"
aria-label="{% blocktrans with item=item.name var=var.name %}Quantity of {{ item }}, {{ var }} to order{% endblocktrans %}">
<button type="button" data-step="1" data-controls="variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}"
<button type="button" data-step="1" data-controls="{{ form_prefix }}variation_{{ item.id }}_{{ var.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>+</button>
</div>
{% endif %}
@@ -234,7 +245,7 @@
</div>
</article>
{% else %}
<article aria-labelledby="item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="item-{{ item.pk }}-description"{% endif %} class="row product-row simple" id="item-{{ item.pk }}"
<article aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-legend"{% if item.description %} aria-describedby="{{ form_prefix }}item-{{ item.pk }}-description"{% endif %} class="row product-row simple" id="{{ form_prefix }}item-{{ item.pk }}"
{% if not item.free_price %}
data-price="{% if event.settings.display_net_prices %}{{ item.display_price.net|unlocalize }}{% else %}{{ item.display_price.gross|unlocalize }}{% endif %}"
{% endif %}>
@@ -250,9 +261,9 @@
</a>
{% endif %}
<div class="product-description {% if item.picture %}with-picture{% endif %}">
<h4 id="item-{{ item.pk }}-legend">{{ item.name }}</h4>
<h4 id="{{ form_prefix }}item-{{ item.pk }}-legend">{{ item.name }}</h4>
{% if item.description %}
<div id="item-{{ item.pk }}-description" class="product-description">
<div id="{{ form_prefix }}item-{{ item.pk }}-description" class="product-description">
{{ item.description|localize|rich_text }}
</div>
{% endif %}
@@ -293,10 +304,10 @@
<label class="sr-only" for="price-item-{{ item.pk }}">{% blocktrans trimmed with item=item.name currency=event.currency %}Set price in {{ currency }} for {{ item }}{% endblocktrans %}</label>
<span class="input-group-addon" aria-hidden="true">{{ event.currency }}</span>
<input type="number" class="form-control input-item-price" placeholder="0"
id="price-item-{{ item.pk }}"
id="{{ form_prefix }}price-item-{{ item.pk }}"
{% if not ev.presale_is_running %}disabled{% endif %}
min="{% if event.settings.display_net_prices %}{{ item.display_price.net|money_numberfield:event.currency }}{% else %}{{ item.display_price.gross|money_numberfield:event.currency }}{% endif %}"
name="price_{{ item.id }}"
name="{{ form_prefix }}price_{{ item.id }}"
{% if item.suggested_price.gross != item.display_price.gross %}
{% if event.settings.display_net_prices %}
title="{% blocktrans trimmed with item=item.name price=item.display_price.net|money:event.currency %}Modify price for {{ item }}, at least {{ price }}{% endblocktrans %}"
@@ -352,15 +363,15 @@
data-checked-onchange="price-item-{{ item.pk }}"
{% endif %}
{% if not ev.presale_is_running %}disabled{% endif %}
name="item_{{ item.id }}" id="item_{{ item.id }}"
name="{{ form_prefix }}item_{{ item.id }}" id="{{ form_prefix }}item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Add {{ item }} to cart{% endblocktrans %}"
{% if item.description %} aria-describedby="item-{{ item.id }}-description"{% endif %}>
{% if item.description %} aria-describedby="{{ form_prefix }}item-{{ item.id }}-description"{% endif %}>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %}
</label>
{% else %}
<div class="input-item-count-group">
<button type="button" data-step="-1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}"
<button type="button" data-step="-1" data-controls="{{ form_prefix }}item_{{ item.id }}" class="btn btn-default input-item-count-dec" aria-label="{% trans "Decrease quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>-</button>
<input type="number" class="form-control input-item-count" placeholder="0" min="0"
{% if not ev.presale_is_running %}disabled{% endif %}
@@ -369,11 +380,11 @@
data-checked-onchange="price-item-{{ item.pk }}"
{% endif %}
max="{{ item.order_max }}"
name="item_{{ item.id }}"
id="item_{{ item.id }}"
name="{{ form_prefix }}item_{{ item.id }}"
id="{{ form_prefix }}item_{{ item.id }}"
aria-label="{% blocktrans with item=item.name %}Quantity of {{ item }} to order{% endblocktrans %}"
{% if item.description %} aria-describedby="item-{{ item.id }}-description"{% endif %}>
<button type="button" data-step="1" data-controls="item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}"
{% if item.description %} aria-describedby="{{ form_prefix }}item-{{ item.id }}-description"{% endif %}>
<button type="button" data-step="1" data-controls="{{ form_prefix }}item_{{ item.id }}" class="btn btn-default input-item-count-inc" aria-label="{% trans "Increase quantity" %}"
{% if not ev.presale_is_running %}disabled{% endif %}>+</button>
</div>
{% endif %}
@@ -386,4 +397,4 @@
{% endif %}
{% endfor %}
</section>
{% endfor %}
{% endwith %}{% endfor %}

View File

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

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,

View File

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