mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
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:
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user