mirror of
https://github.com/pretix/pretix.git
synced 2026-05-08 15:44:02 +00:00
implement cross_selling_visibility "always" and "products"
This commit is contained in:
@@ -441,7 +441,11 @@ class ItemCategorySerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ItemCategory
|
model = ItemCategory
|
||||||
fields = ('id', 'name', 'internal_name', 'description', 'position', 'is_addon', 'cross_selling_mode', 'cross_selling_condition')
|
fields = (
|
||||||
|
'id', 'name', 'internal_name', 'description', 'position',
|
||||||
|
'is_addon', 'cross_selling_mode',
|
||||||
|
'cross_selling_condition', 'cross_selling_match_products'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
class QuestionOptionSerializer(I18nAwareModelSerializer):
|
||||||
|
|||||||
@@ -142,6 +142,18 @@ class ItemCategory(LoggedModel):
|
|||||||
verbose_name_plural = _("Product categories")
|
verbose_name_plural = _("Product categories")
|
||||||
ordering = ('position', 'id')
|
ordering = ('position', 'id')
|
||||||
|
|
||||||
|
def cross_sell_visible(self, cart_positions):
|
||||||
|
if self.cross_selling_mode is None:
|
||||||
|
return False
|
||||||
|
if self.cross_selling_condition == 'always':
|
||||||
|
return True
|
||||||
|
if self.cross_selling_condition == 'products':
|
||||||
|
match = set(match.pk for match in self.cross_selling_match_products.only('pk')) # TODO prefetch this
|
||||||
|
return any(pos.item.pk in match for pos in cart_positions)
|
||||||
|
if self.cross_selling_condition == 'discounts':
|
||||||
|
# TODO not sure how to do this yet
|
||||||
|
return False
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
name = self.internal_name or self.name
|
name = self.internal_name or self.name
|
||||||
if self.is_addon:
|
if self.is_addon:
|
||||||
@@ -295,7 +307,7 @@ class SubEventItemVariation(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
def filter_available(qs, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
||||||
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
# Channel can currently be a SalesChannel or a str, since we need that compatibility, but a SalesChannel
|
||||||
# makes the query SIGNIFICANTLY faster
|
# makes the query SIGNIFICANTLY faster
|
||||||
from .organizer import SalesChannel
|
from .organizer import SalesChannel
|
||||||
@@ -316,6 +328,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
|||||||
|
|
||||||
if not allow_addons:
|
if not allow_addons:
|
||||||
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
||||||
|
if not allow_cross_sell:
|
||||||
|
q &= Q(Q(category__isnull=True) | ~Q(category__cross_selling_mode='only'))
|
||||||
|
|
||||||
if voucher:
|
if voucher:
|
||||||
if voucher.item_id:
|
if voucher.item_id:
|
||||||
@@ -329,8 +343,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
|||||||
|
|
||||||
|
|
||||||
class ItemQuerySet(models.QuerySet):
|
class ItemQuerySet(models.QuerySet):
|
||||||
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
||||||
return filter_available(self, channel, voucher, allow_addons)
|
return filter_available(self, channel, voucher, allow_addons, allow_cross_sell)
|
||||||
|
|
||||||
|
|
||||||
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
|
class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__):
|
||||||
@@ -338,8 +352,8 @@ class ItemQuerySetManager(ScopedManager(organizer='event__organizer').__class__)
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self._queryset_class = ItemQuerySet
|
self._queryset_class = ItemQuerySet
|
||||||
|
|
||||||
def filter_available(self, channel='web', voucher=None, allow_addons=False):
|
def filter_available(self, channel='web', voucher=None, allow_addons=False, allow_cross_sell=False):
|
||||||
return filter_available(self.get_queryset(), channel, voucher, allow_addons)
|
return filter_available(self.get_queryset(), channel, voucher, allow_addons, allow_cross_sell)
|
||||||
|
|
||||||
|
|
||||||
class Item(LoggedModel):
|
class Item(LoggedModel):
|
||||||
|
|||||||
@@ -1610,7 +1610,7 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
|||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
|
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: List[dict], cart_id: str=None, locale='en',
|
||||||
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
Assigns addons to eligible products in a user's cart, adding and removing the addon products as necessary to
|
Assigns addons to eligible products in a user's cart, adding and removing the addon products as necessary to
|
||||||
@@ -1635,6 +1635,7 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
|
|||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
|
||||||
cm.set_addons(addons)
|
cm.set_addons(addons)
|
||||||
|
cm.add_new_items(add_to_cart_items)
|
||||||
cm.commit()
|
cm.commit()
|
||||||
except LockTimeoutException:
|
except LockTimeoutException:
|
||||||
self.retry()
|
self.retry()
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ from urllib.parse import urlencode
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Max
|
from django.core.files.uploadedfile import UploadedFile
|
||||||
|
from django.db.models import Max, Q
|
||||||
from django.forms import ChoiceField, RadioSelect
|
from django.forms import ChoiceField, RadioSelect
|
||||||
from django.forms.formsets import DELETION_FIELD_NAME
|
from django.forms.formsets import DELETION_FIELD_NAME
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -121,7 +122,12 @@ class CategoryForm(I18nModelForm):
|
|||||||
'data-display-dependency': '#id_cross_selling_condition_1'
|
'data-display-dependency': '#id_cross_selling_condition_1'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.fields['cross_selling_match_products'].queryset = self.event.items.all()
|
self.fields['cross_selling_match_products'].queryset = self.event.items.filter(
|
||||||
|
# don't show products which are only visible in addon/cross-sell step themselves
|
||||||
|
Q(category__isnull=True) | Q(
|
||||||
|
Q(category__is_addon=False) & Q(Q(category__cross_selling_mode='both') | Q(category__cross_selling_mode__isnull=True))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
d = super().clean()
|
d = super().clean()
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ from pretix.presale.views import (
|
|||||||
CartMixin, get_cart, get_cart_is_free, get_cart_total,
|
CartMixin, get_cart, get_cart_is_free, get_cart_total,
|
||||||
)
|
)
|
||||||
from pretix.presale.views.cart import (
|
from pretix.presale.views.cart import (
|
||||||
cart_session, create_empty_cart_id, get_or_create_cart_id,
|
cart_session, create_empty_cart_id, get_or_create_cart_id, _items_from_post_data,
|
||||||
)
|
)
|
||||||
from pretix.presale.views.event import get_grouped_items
|
from pretix.presale.views.event import get_grouped_items
|
||||||
from pretix.presale.views.questions import QuestionsViewMixin
|
from pretix.presale.views.questions import QuestionsViewMixin
|
||||||
@@ -488,9 +488,17 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
|
|
||||||
def is_applicable(self, request):
|
def is_applicable(self, request):
|
||||||
if not hasattr(request, '_checkoutflow_addons_applicable'):
|
if not hasattr(request, '_checkoutflow_addons_applicable'):
|
||||||
request._checkoutflow_addons_applicable = get_cart(request).filter(item__addons__isnull=False).exists()
|
cart = get_cart(request)
|
||||||
|
self.request = request
|
||||||
|
request._checkoutflow_addons_applicable = (cart.filter(item__addons__isnull=False).exists()
|
||||||
|
or any(self.cross_selling_applicable_rules))
|
||||||
return request._checkoutflow_addons_applicable
|
return request._checkoutflow_addons_applicable
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def cross_selling_applicable_rules(self):
|
||||||
|
cart = self.get_cart(self.request)
|
||||||
|
return [c for c in self.request.event.categories.filter(cross_selling_mode__isnull=False) if c.cross_sell_visible(cart['positions'])]
|
||||||
|
|
||||||
def is_completed(self, request, warn=False):
|
def is_completed(self, request, warn=False):
|
||||||
if getattr(self, '_completed', None) is not None:
|
if getattr(self, '_completed', None) is not None:
|
||||||
return self._completed
|
return self._completed
|
||||||
@@ -605,9 +613,48 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
formset.append(formsetentry)
|
formset.append(formsetentry)
|
||||||
return formset
|
return formset
|
||||||
|
|
||||||
|
def get_cross_selling_data(self, ctx):
|
||||||
|
class DummyCategory:
|
||||||
|
def __init__(self, rule, subevent=None):
|
||||||
|
self.id = rule.id
|
||||||
|
self.name = rule.name + (f" ({subevent})" if subevent else "")
|
||||||
|
self.description = rule.description
|
||||||
|
|
||||||
|
if self.event.has_subevents:
|
||||||
|
return [
|
||||||
|
(DummyCategory(rule, subevent), self._items_from_rule(rule, subevent), f'subevent_{subevent.pk}_')
|
||||||
|
for rule in self.cross_selling_applicable_rules
|
||||||
|
for subevent in set(pos.subevent for pos in ctx['cart']['positions'])
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
return [
|
||||||
|
(rule, self._items_from_rule(rule, None))
|
||||||
|
for rule in self.cross_selling_applicable_rules
|
||||||
|
]
|
||||||
|
|
||||||
|
def _items_from_rule(self, rule, subevent):
|
||||||
|
items, _btn = get_grouped_items(
|
||||||
|
self.request.event,
|
||||||
|
subevent=subevent,
|
||||||
|
voucher=None,
|
||||||
|
channel=self.request.sales_channel.identifier,
|
||||||
|
base_qs=rule.items.all(),
|
||||||
|
allow_addons=True,
|
||||||
|
allow_cross_sell=True,
|
||||||
|
memberships=(
|
||||||
|
self.request.customer.usable_memberships(
|
||||||
|
for_event=self.request.event, #p.subevent or self.request.event,
|
||||||
|
testmode=self.request.event.testmode
|
||||||
|
)
|
||||||
|
if self.request.customer else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return items
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['forms'] = self.forms
|
ctx['forms'] = self.forms
|
||||||
|
ctx['cross_selling_data'] = self.get_cross_selling_data(ctx)
|
||||||
ctx['cart'] = self.get_cart()
|
ctx['cart'] = self.get_cart()
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
@@ -687,7 +734,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.request = request
|
self.request = request
|
||||||
data = []
|
addons = []
|
||||||
for f in self.forms:
|
for f in self.forms:
|
||||||
for c in f['categories']:
|
for c in f['categories']:
|
||||||
try:
|
try:
|
||||||
@@ -697,7 +744,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
return self.get(request, *args, **kwargs)
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
for (i, v), (c, price) in selected.items():
|
for (i, v), (c, price) in selected.items():
|
||||||
data.append({
|
addons.append({
|
||||||
'addon_to': f['pos'].pk,
|
'addon_to': f['pos'].pk,
|
||||||
'item': i.pk,
|
'item': i.pk,
|
||||||
'variation': v.pk if v else None,
|
'variation': v.pk if v else None,
|
||||||
@@ -705,7 +752,9 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
|||||||
'price': price,
|
'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)
|
||||||
|
|
||||||
|
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(),
|
invoice_address=self.invoice_address.pk, locale=get_language(),
|
||||||
sales_channel=request.sales_channel.identifier, override_now_dt=time_machine_now(default=None))
|
sales_channel=request.sales_channel.identifier, override_now_dt=time_machine_now(default=None))
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
</details>
|
</details>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include "pretixpresale/event/fragment_product_list.html" with items_by_category=cross_selling_data ev=event %}
|
||||||
|
|
||||||
<div class="row checkout-button-row">
|
<div class="row checkout-button-row">
|
||||||
<div class="col-md-4 col-sm-6">
|
<div class="col-md-4 col-sm-6">
|
||||||
<a class="btn btn-block btn-default btn-lg"
|
<a class="btn btn-block btn-default btn-lg"
|
||||||
|
|||||||
@@ -151,112 +151,113 @@ class CartActionMixin:
|
|||||||
except InvoiceAddress.DoesNotExist:
|
except InvoiceAddress.DoesNotExist:
|
||||||
return InvoiceAddress()
|
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
|
|
||||||
|
|
||||||
subevent = None
|
def _item_from_post_value(request, key, value, voucher=None, voucher_ignore_if_redeemed=False):
|
||||||
if key.startswith('subevent_'):
|
if value.strip() == '' or '_' not in key:
|
||||||
try:
|
return
|
||||||
parts = key.split('_', 2)
|
|
||||||
subevent = int(parts[1])
|
|
||||||
key = parts[2]
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
elif 'subevent' in self.request.POST:
|
|
||||||
try:
|
|
||||||
subevent = int(self.request.POST.get('subevent'))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
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:]), "")
|
|
||||||
|
|
||||||
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.'))
|
|
||||||
|
|
||||||
|
subevent = None
|
||||||
|
if key.startswith('subevent_'):
|
||||||
try:
|
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:
|
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
|
pass
|
||||||
|
|
||||||
items = []
|
if not key.startswith('item_') and not key.startswith('variation_') and not key.startswith('seat_'):
|
||||||
if 'raw' in self.request.POST:
|
return
|
||||||
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 len(items) == 0:
|
parts = key.split("_")
|
||||||
messages.warning(self.request, _('You did not select any products.'))
|
price = request.POST.get('price_' + "_".join(parts[1:]), "")
|
||||||
return []
|
|
||||||
return items
|
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):
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
messages.warning(request, _('You did not select any products.'))
|
||||||
|
return []
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
@@ -542,7 +543,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
|||||||
cs = cart_session(request)
|
cs = cart_session(request)
|
||||||
widget_data = cs.get('widget_data', {})
|
widget_data = cs.get('widget_data', {})
|
||||||
|
|
||||||
items = self._items_from_post_data()
|
items = _items_from_post_data(self.request)
|
||||||
if items:
|
if items:
|
||||||
return self.do(self.request.event.id, items, cart_id, translation.get_language(),
|
return self.do(self.request.event.id, items, cart_id, translation.get_language(),
|
||||||
self.invoice_address.pk, widget_data, self.request.sales_channel.identifier,
|
self.invoice_address.pk, widget_data, self.request.sales_channel.identifier,
|
||||||
|
|||||||
@@ -109,7 +109,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,
|
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
||||||
ignore_hide_sold_out_for_item_ids=None):
|
ignore_hide_sold_out_for_item_ids=None):
|
||||||
base_qs_set = base_qs is not None
|
base_qs_set = base_qs is not None
|
||||||
@@ -191,7 +192,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
|
'category', 'tax_rule', # for re-grouping
|
||||||
'hidden_if_available',
|
'hidden_if_available',
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
|
|||||||
Reference in New Issue
Block a user