mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
fix cross selling recommendations for SUBEVENT_MODE_SAME discounts
This commit is contained in:
@@ -247,7 +247,7 @@ class Discount(LoggedModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts):
|
def _apply_min_value(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
|
||||||
if self.condition_min_value and sum(positions[idx].line_price_gross for idx in condition_idx_group) < self.condition_min_value:
|
if self.condition_min_value and sum(positions[idx].line_price_gross for idx in condition_idx_group) < self.condition_min_value:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -264,9 +264,9 @@ class Discount(LoggedModel):
|
|||||||
|
|
||||||
if collect_potential_discounts is not None:
|
if collect_potential_discounts is not None:
|
||||||
for idx in condition_idx_group:
|
for idx in condition_idx_group:
|
||||||
collect_potential_discounts[idx] = [(self, inf, -1)]
|
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
|
||||||
|
|
||||||
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts):
|
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
|
||||||
if len(condition_idx_group) < self.condition_min_count:
|
if len(condition_idx_group) < self.condition_min_count:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ class Discount(LoggedModel):
|
|||||||
# but only 1 t-shirt) -> 1 shirt definitiv potential discount
|
# but only 1 t-shirt) -> 1 shirt definitiv potential discount
|
||||||
for idx in consume_idx:
|
for idx in consume_idx:
|
||||||
collect_potential_discounts[idx] = [
|
collect_potential_discounts[idx] = [
|
||||||
(self, n_groups * self.benefit_only_apply_to_cheapest_n_matches - len(benefit_idx_group), -1)
|
(self, n_groups * self.benefit_only_apply_to_cheapest_n_matches - len(benefit_idx_group), -1, subevent_id)
|
||||||
]
|
]
|
||||||
|
|
||||||
if possible_applications_cond * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
|
if possible_applications_cond * self.benefit_only_apply_to_cheapest_n_matches > len(benefit_idx_group):
|
||||||
@@ -308,7 +308,7 @@ class Discount(LoggedModel):
|
|||||||
possible_applications_cond * self.condition_min_count
|
possible_applications_cond * self.condition_min_count
|
||||||
]):
|
]):
|
||||||
collect_potential_discounts[idx] += [
|
collect_potential_discounts[idx] += [
|
||||||
(self, self.benefit_only_apply_to_cheapest_n_matches, i // self.condition_min_count)
|
(self, self.benefit_only_apply_to_cheapest_n_matches, i // self.condition_min_count, subevent_id)
|
||||||
]
|
]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -317,7 +317,7 @@ class Discount(LoggedModel):
|
|||||||
|
|
||||||
if collect_potential_discounts is not None:
|
if collect_potential_discounts is not None:
|
||||||
for idx in consume_idx:
|
for idx in consume_idx:
|
||||||
collect_potential_discounts[idx] = [(self, inf, -1)]
|
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
|
||||||
|
|
||||||
for idx in benefit_idx:
|
for idx in benefit_idx:
|
||||||
previous_price = positions[idx].line_price_gross
|
previous_price = positions[idx].line_price_gross
|
||||||
@@ -379,9 +379,9 @@ class Discount(LoggedModel):
|
|||||||
|
|
||||||
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
|
if self.subevent_mode == self.SUBEVENT_MODE_MIXED: # also applies to non-series events
|
||||||
if self.condition_min_count:
|
if self.condition_min_count:
|
||||||
self._apply_min_count(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts)
|
self._apply_min_count(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
|
||||||
else:
|
else:
|
||||||
self._apply_min_value(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts)
|
self._apply_min_value(positions, condition_candidates, benefit_candidates, result, collect_potential_discounts, None)
|
||||||
|
|
||||||
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
|
elif self.subevent_mode == self.SUBEVENT_MODE_SAME:
|
||||||
def key(idx):
|
def key(idx):
|
||||||
@@ -396,9 +396,9 @@ class Discount(LoggedModel):
|
|||||||
for subevent_id, g in candidate_groups:
|
for subevent_id, g in candidate_groups:
|
||||||
benefit_g = [idx for idx in benefit_candidates if positions[idx].subevent_id == subevent_id]
|
benefit_g = [idx for idx in benefit_candidates if positions[idx].subevent_id == subevent_id]
|
||||||
if self.condition_min_count:
|
if self.condition_min_count:
|
||||||
self._apply_min_count(positions, g, benefit_g, result, collect_potential_discounts)
|
self._apply_min_count(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
|
||||||
else:
|
else:
|
||||||
self._apply_min_value(positions, g, benefit_g, result, collect_potential_discounts)
|
self._apply_min_value(positions, g, benefit_g, result, collect_potential_discounts, subevent_id)
|
||||||
|
|
||||||
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
|
elif self.subevent_mode == self.SUBEVENT_MODE_DISTINCT:
|
||||||
if self.condition_min_value or not self.benefit_same_products:
|
if self.condition_min_value or not self.benefit_same_products:
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ class DummyCategory:
|
|||||||
once for each subevent
|
once for each subevent
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, category: ItemCategory, subevent=None):
|
def __init__(self, category: ItemCategory, subevent):
|
||||||
self.id = category.id
|
self.id = category.id
|
||||||
self.name = str(category.name) + (f" ({subevent})" if subevent else "")
|
self.name = f"{category.name} ({subevent})"
|
||||||
self.description = category.description
|
self.description = category.description
|
||||||
|
|
||||||
|
|
||||||
@@ -58,35 +58,34 @@ class CrossSellingService:
|
|||||||
(DummyCategory(category, subevent),
|
(DummyCategory(category, subevent),
|
||||||
self._prepare_items(subevent, items_qs, discount_info),
|
self._prepare_items(subevent, items_qs, discount_info),
|
||||||
f'subevent_{subevent.pk}_')
|
f'subevent_{subevent.pk}_')
|
||||||
for (category, items_qs, discount_info) in self._applicable_categories
|
|
||||||
for subevent in subevents
|
for subevent in subevents
|
||||||
|
for (category, items_qs, discount_info) in self._applicable_categories(subevent.pk)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = (
|
result = (
|
||||||
(category,
|
(category,
|
||||||
self._prepare_items(None, items_qs, discount_info),
|
self._prepare_items(None, items_qs, discount_info),
|
||||||
None)
|
None)
|
||||||
for (category, items_qs, discount_info) in self._applicable_categories
|
for (category, items_qs, discount_info) in self._applicable_categories(0)
|
||||||
)
|
)
|
||||||
return [(category, items, form_prefix) for (category, items, form_prefix) in result if len(items) > 0]
|
return [(category, items, form_prefix) for (category, items, form_prefix) in result if len(items) > 0]
|
||||||
|
|
||||||
@property
|
def _applicable_categories(self, subevent_id):
|
||||||
def _applicable_categories(self):
|
|
||||||
return [
|
return [
|
||||||
(c, products_qs, discount_info) for (c, products_qs, discount_info) in
|
(c, products_qs, discount_info) for (c, products_qs, discount_info) in
|
||||||
(
|
(
|
||||||
(c, *self._get_visible_items_for_category(c))
|
(c, *self._get_visible_items_for_category(subevent_id, c))
|
||||||
for c in self.event.categories.filter(cross_selling_mode__isnull=False).prefetch_related('items')
|
for c in self.event.categories.filter(cross_selling_mode__isnull=False).prefetch_related('items')
|
||||||
)
|
)
|
||||||
if products_qs is not None
|
if products_qs is not None
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_visible_items_for_category(self, category: ItemCategory):
|
def _get_visible_items_for_category(self, filter_subevent_id, category: ItemCategory):
|
||||||
"""
|
"""
|
||||||
If this category should be visible in the cross-selling step for a given cart and sales_channel, this method
|
If this category should be visible in the cross-selling step for a given cart and sales_channel, this method
|
||||||
returns a queryset of the items that should be displayed, as well as a dict giving additional information on them.
|
returns a queryset of the items that should be displayed, as well as a dict giving additional information on them.
|
||||||
|
|
||||||
:returns: (QuerySet<Item>, dict<item_pk: (max_count, discount_rule)>)
|
:returns: (QuerySet<Item>, dict<(subevent_id, item_pk): (max_count, discount_rule)>)
|
||||||
max_count is `inf` if the item should not be limited
|
max_count is `inf` if the item should not be limited
|
||||||
discount_rule is None if the item will not be discounted
|
discount_rule is None if the item will not be discounted
|
||||||
"""
|
"""
|
||||||
@@ -101,14 +100,13 @@ class CrossSellingService:
|
|||||||
my_item_pks = [item.id for item in category.items.all()]
|
my_item_pks = [item.id for item in category.items.all()]
|
||||||
potential_discount_items = {
|
potential_discount_items = {
|
||||||
item.pk: (max_count, discount_rule)
|
item.pk: (max_count, discount_rule)
|
||||||
for item, max_count, discount_rule in self._potential_discounts_by_item_for_current_cart
|
for subevent_id, item, max_count, discount_rule in self._potential_discounts_by_subevent_and_item_for_current_cart
|
||||||
if max_count > 0 and item.pk in my_item_pks and item.is_available()
|
if max_count > 0 and item.pk in my_item_pks and item.is_available() and (subevent_id == filter_subevent_id or subevent_id is None)
|
||||||
}
|
}
|
||||||
|
|
||||||
return category.items.filter(pk__in=potential_discount_items), potential_discount_items
|
return category.items.filter(pk__in=potential_discount_items), potential_discount_items
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def _potential_discounts_by_item_for_current_cart(self):
|
def _potential_discounts_by_subevent_and_item_for_current_cart(self):
|
||||||
potential_discounts_by_cartpos = defaultdict(list)
|
potential_discounts_by_cartpos = defaultdict(list)
|
||||||
|
|
||||||
from ..services.pricing import apply_discounts
|
from ..services.pricing import apply_discounts
|
||||||
@@ -133,26 +131,27 @@ class CrossSellingService:
|
|||||||
# - max_count for product: sum up max_counts
|
# - max_count for product: sum up max_counts
|
||||||
# - discount_rule for product: take first discount_rule
|
# - discount_rule for product: take first discount_rule
|
||||||
|
|
||||||
def discount_info(item, infos_for_item):
|
def discount_info(subevent_id, item, infos_for_item):
|
||||||
infos_for_item = list(infos_for_item)
|
infos_for_item = list(infos_for_item)
|
||||||
return (
|
return (
|
||||||
|
subevent_id,
|
||||||
item,
|
item,
|
||||||
sum(max_count for (item, discount_rule, max_count, i) in infos_for_item),
|
sum(max_count for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
|
||||||
next(discount_rule for (item, discount_rule, max_count, i) in infos_for_item)
|
next(discount_rule for (subevent_id, item, discount_rule, max_count, i) in infos_for_item),
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
discount_info(item, infos_for_item) for item, infos_for_item in
|
discount_info(subevent_id, item, infos_for_item) for (subevent_id, item), infos_for_item in
|
||||||
groupby(
|
groupby(
|
||||||
sorted(
|
sorted(
|
||||||
(
|
(
|
||||||
(item, discount_rule, max_count, i)
|
(subevent_id, item, discount_rule, max_count, i)
|
||||||
for (discount_rule, max_count, i) in potential_discount_set.keys()
|
for (discount_rule, max_count, i, subevent_id) in potential_discount_set.keys()
|
||||||
for item in discount_rule.benefit_limit_products.all()
|
for item in discount_rule.benefit_limit_products.all()
|
||||||
),
|
),
|
||||||
key=lambda tup: tup[0].pk
|
key=lambda tup: (tup[0], tup[1].pk)
|
||||||
),
|
),
|
||||||
lambda tup: tup[0])
|
lambda tup: (tup[0], tup[1]))
|
||||||
]
|
]
|
||||||
|
|
||||||
def _prepare_items(self, subevent, items_qs, discount_info):
|
def _prepare_items(self, subevent, items_qs, discount_info):
|
||||||
|
|||||||
Reference in New Issue
Block a user