diff --git a/src/pretix/base/models/discount.py b/src/pretix/base/models/discount.py index 11b677b7f..c8803e91f 100644 --- a/src/pretix/base/models/discount.py +++ b/src/pretix/base/models/discount.py @@ -247,7 +247,7 @@ class Discount(LoggedModel): return False 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: return @@ -264,9 +264,9 @@ class Discount(LoggedModel): if collect_potential_discounts is not None: 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: return @@ -297,7 +297,7 @@ class Discount(LoggedModel): # but only 1 t-shirt) -> 1 shirt definitiv potential discount for idx in consume_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): @@ -308,7 +308,7 @@ class Discount(LoggedModel): possible_applications_cond * self.condition_min_count ]): 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: @@ -317,7 +317,7 @@ class Discount(LoggedModel): if collect_potential_discounts is not None: 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: 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.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: - 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: def key(idx): @@ -396,9 +396,9 @@ class Discount(LoggedModel): for subevent_id, g in candidate_groups: benefit_g = [idx for idx in benefit_candidates if positions[idx].subevent_id == subevent_id] 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: - 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: if self.condition_min_value or not self.benefit_same_products: diff --git a/src/pretix/base/services/cross_selling.py b/src/pretix/base/services/cross_selling.py index 7fd6fd8fb..e3f13068f 100644 --- a/src/pretix/base/services/cross_selling.py +++ b/src/pretix/base/services/cross_selling.py @@ -38,9 +38,9 @@ class DummyCategory: once for each subevent """ - def __init__(self, category: ItemCategory, subevent=None): + def __init__(self, category: ItemCategory, subevent): self.id = category.id - self.name = str(category.name) + (f" ({subevent})" if subevent else "") + self.name = f"{category.name} ({subevent})" self.description = category.description @@ -58,35 +58,34 @@ class CrossSellingService: (DummyCategory(category, subevent), self._prepare_items(subevent, items_qs, discount_info), f'subevent_{subevent.pk}_') - for (category, items_qs, discount_info) in self._applicable_categories for subevent in subevents + for (category, items_qs, discount_info) in self._applicable_categories(subevent.pk) ) else: result = ( (category, self._prepare_items(None, items_qs, discount_info), 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] - @property - def _applicable_categories(self): + def _applicable_categories(self, subevent_id): return [ (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') ) 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 returns a queryset of the items that should be displayed, as well as a dict giving additional information on them. - :returns: (QuerySet, dict) + :returns: (QuerySet, dict<(subevent_id, item_pk): (max_count, discount_rule)>) max_count is `inf` if the item should not be limited 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()] potential_discount_items = { item.pk: (max_count, discount_rule) - for item, max_count, discount_rule in self._potential_discounts_by_item_for_current_cart - if max_count > 0 and item.pk in my_item_pks and item.is_available() + 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() and (subevent_id == filter_subevent_id or subevent_id is None) } - return category.items.filter(pk__in=potential_discount_items), potential_discount_items @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) from ..services.pricing import apply_discounts @@ -133,26 +131,27 @@ class CrossSellingService: # - max_count for product: sum up max_counts # - 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) return ( + subevent_id, item, - sum(max_count for (item, discount_rule, max_count, i) in infos_for_item), - next(discount_rule 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 (subevent_id, item, discount_rule, max_count, i) in infos_for_item), ) 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( sorted( ( - (item, discount_rule, max_count, i) - for (discount_rule, max_count, i) in potential_discount_set.keys() + (subevent_id, item, discount_rule, max_count, i) + for (discount_rule, max_count, i, subevent_id) in potential_discount_set.keys() 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):