fix cross selling recommendations for SUBEVENT_MODE_SAME discounts

This commit is contained in:
Mira Weller
2024-10-01 18:14:46 +02:00
parent fd6ae65f23
commit d5e480b7fd
2 changed files with 30 additions and 31 deletions

View File

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

View File

@@ -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):