diff --git a/src/pretix/base/models/discount.py b/src/pretix/base/models/discount.py index 11c6039cf8..0a2a646af9 100644 --- a/src/pretix/base/models/discount.py +++ b/src/pretix/base/models/discount.py @@ -23,6 +23,7 @@ from collections import defaultdict from decimal import Decimal from itertools import groupby +from math import ceil from typing import Dict, Optional, Tuple from django.core.exceptions import ValidationError @@ -272,7 +273,7 @@ class Discount(LoggedModel): # Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only # want to match multiples of 3 - n_groups = min(len(condition_idx_group) // self.condition_min_count, len(benefit_idx_group)) + n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches)) consume_idx = condition_idx_group[:n_groups * self.condition_min_count] benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches] else: diff --git a/src/tests/base/test_pricing_discount.py b/src/tests/base/test_pricing_discount.py index b39dddf5f9..ca8fecde7f 100644 --- a/src/tests/base/test_pricing_discount.py +++ b/src/tests/base/test_pricing_discount.py @@ -51,6 +51,11 @@ def item2(event): return event.items.create(name='Ticket II', default_price=Decimal('50.00')) +@pytest.fixture +def item3(event): + return event.items.create(name='Ticket III', default_price=Decimal('42.00')) + + @pytest.fixture def voucher(event): return event.vouchers.create() @@ -1338,3 +1343,66 @@ def test_multiple_discounts_with_benefit_condition_overlap(event, item, item2): new_prices = [p for p, d in apply_discounts(event, 'web', positions)] assert sorted(new_prices) == sorted(expected) + + +@pytest.mark.django_db +@scopes_disabled() +def test_multiple_discounts_with_same_condition(event, item, item2, item3): + # "For every 1 item1, you get three item2 for 10 % off." + "For every 1 item1, you get five item3 for 10 % off." + d1 = Discount( + event=event, + condition_min_count=1, + condition_all_products=False, + benefit_only_apply_to_cheapest_n_matches=3, + benefit_discount_matching_percent=10, + benefit_same_products=False, + position=1, + ) + d1.save() + d1.condition_limit_products.add(item) + d1.benefit_limit_products.add(item2) + + d2 = Discount( + event=event, + condition_min_count=1, + condition_all_products=False, + benefit_only_apply_to_cheapest_n_matches=5, + benefit_discount_matching_percent=10, + benefit_same_products=False, + position=2, + ) + d2.save() + d2.condition_limit_products.add(item) + d2.benefit_limit_products.add(item3) + + positions = ( + (item3.pk, None, Decimal('42.00'), False, False, Decimal('0.00')), + (item3.pk, None, Decimal('42.00'), False, False, Decimal('0.00')), + (item3.pk, None, Decimal('42.00'), False, False, Decimal('0.00')), + (item3.pk, None, Decimal('42.00'), False, False, Decimal('0.00')), + (item3.pk, None, Decimal('42.00'), False, False, Decimal('0.00')), + (item3.pk, None, Decimal('42.00'), False, False, Decimal('0.00')), + (item2.pk, None, Decimal('50.00'), False, False, Decimal('0.00')), + (item2.pk, None, Decimal('50.00'), False, False, Decimal('0.00')), + (item.pk, None, Decimal('23.00'), False, False, Decimal('0.00')), + (item.pk, None, Decimal('23.00'), False, False, Decimal('0.00')), + ) + expected = ( + # both item1 remain full price + Decimal('23.00'), + Decimal('23.00'), + # 5 item3 discounted + Decimal('37.80'), + Decimal('37.80'), + Decimal('37.80'), + Decimal('37.80'), + Decimal('37.80'), + # 2 item2 discounted + Decimal('45.00'), + Decimal('45.00'), + # 1 item3 remains untouched + Decimal('42.00'), + ) + + new_prices = [p for p, d in apply_discounts(event, 'web', positions)] + assert sorted(new_prices) == sorted(expected)