Compare commits

...

1 Commits

Author SHA1 Message Date
Raphael Michel
df656d1580 Avoid showing empty add-on step (Z#23167007)
Do not show add-on step if none of the add-on items is available. This
is not a perfect solution, there are still cases where an empty add-on
step will show up, e.g.:

- Products disabled only for a specific subevent
- Sold-out add-ons combined with the "hide sold-out products" flag
2024-09-27 13:44:42 +02:00
3 changed files with 86 additions and 35 deletions

View File

@@ -313,38 +313,9 @@ class EventMixin:
items=GroupConcat('pk', delimiter=',')
).values('items')
q_variation = (
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
& Q(item__active=True)
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
& Q(item__require_bundling=False)
& Q(quotas__pk=OuterRef('pk'))
)
if isinstance(channel, str):
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels__identifier=channel))
else:
q_variation &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
q_variation &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels=channel))
if voucher:
if voucher.variation_id:
q_variation &= Q(pk=voucher.variation_id)
elif voucher.item_id:
q_variation &= Q(item_id=voucher.item_id)
elif voucher.quota_id:
q_variation &= Q(quotas__in=[voucher.quota_id])
if not voucher or not voucher.show_hidden_items:
q_variation &= Q(hide_without_voucher=False)
q_variation &= Q(item__hide_without_voucher=False)
sq_active_variation = ItemVariation.objects.filter(q_variation).order_by().values_list('quotas__pk').annotate(
sq_active_variation = ItemVariation.objects.filter_available(channel=channel, voucher=voucher).filter(
Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
quota_base_qs = Quota.objects.using(settings.DATABASE_REPLICA).filter(

View File

@@ -303,6 +303,48 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
return qs.filter(q)
def filter_variations_available(qs, channel='web', voucher=None, allow_addons=False):
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
# makes the query SIGNIFICANTLY faster
from .organizer import SalesChannel
assert isinstance(channel, (SalesChannel, str))
q = (
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
& Q(item__active=True)
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
& Q(item__require_bundling=False)
)
if isinstance(channel, str):
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
q &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels__identifier=channel))
else:
q &= Q(Q(all_sales_channels=True) | Q(limit_sales_channels=channel))
q &= Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels=channel))
if not allow_addons:
q &= Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
if voucher:
if voucher.variation_id:
q &= Q(pk=voucher.variation_id)
elif voucher.item_id:
q &= Q(item_id=voucher.item_id)
elif voucher.quota_id:
q &= Q(quotas__in=[voucher.quota_id])
if not voucher or not voucher.show_hidden_items:
q &= Q(hide_without_voucher=False)
q &= Q(item__hide_without_voucher=False)
return qs.filter(q)
class ItemQuerySet(models.QuerySet):
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_available(self, channel, voucher, allow_addons)
@@ -317,6 +359,20 @@ class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__)
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
class ItemVariationQuerySet(models.QuerySet):
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_variations_available(self, channel, voucher, allow_addons)
class ItemVariationQuerySetManager(ScopedManager(organizer='item__event__organizer').__class__):
def __init__(self):
super().__init__()
self._queryset_class = ItemVariationQuerySet
def filter_available(self, channel='web', voucher=None, allow_addons=False):
return filter_variations_available(self.get_queryset(), channel, voucher, allow_addons)
class Item(LoggedModel):
"""
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
@@ -1199,7 +1255,7 @@ class ItemVariation(models.Model):
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
)
objects = ScopedManager(organizer='item__event__organizer')
objects = ItemVariationQuerySetManager()
class Meta:
verbose_name = _("Product variation")

View File

@@ -56,7 +56,7 @@ from django.views.generic.base import TemplateResponseMixin
from django_scopes import scopes_disabled
from pretix.base.models import Customer, Membership, Order
from pretix.base.models.items import Question
from pretix.base.models.items import ItemAddOn, ItemVariation, Question
from pretix.base.models.orders import (
InvoiceAddress, OrderPayment, QuestionAnswer,
)
@@ -486,9 +486,33 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
label = pgettext_lazy('checkoutflow', 'Add-on products')
icon = 'puzzle-piece'
def _is_applicable(self, request):
categories = set(ItemAddOn.objects.filter(
base_item_id__in=get_cart(request).values_list("item_id", flat=True)
).values_list("addon_category_id", flat=True))
if not categories:
return False
has_available_addons = (
self.event.items.filter_available(
channel=request.sales_channel,
allow_addons=True
).filter(
variations__isnull=True,
category__in=categories,
).exists() or ItemVariation.objects.filter_available(
channel=request.sales_channel,
allow_addons=True
).filter(
item__event=self.event,
item__category__in=categories,
)
)
return has_available_addons
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()
request._checkoutflow_addons_applicable = self._is_applicable(request)
return request._checkoutflow_addons_applicable
def is_completed(self, request, warn=False):