diff --git a/src/pretix/base/models/discount.py b/src/pretix/base/models/discount.py index 6c2848984..d6fc6ca18 100644 --- a/src/pretix/base/models/discount.py +++ b/src/pretix/base/models/discount.py @@ -304,8 +304,8 @@ class Discount(LoggedModel): # unused discount ("for each 1 ticket you buy, get 50% on 2 t-shirts", cart content: 1 ticket # but 0 t-shirts) -> 2 shirt maybe potential discount (if the 1 ticket is not consumed by a later discount) for i, idx in enumerate(condition_idx_group[ - n_groups * self.condition_min_count: - possible_applications_cond * self.condition_min_count + n_groups * self.condition_min_count: + possible_applications_cond * self.condition_min_count ]): print(i, idx) collect_potential_discounts[idx] += [ diff --git a/src/pretix/base/services/cross_selling.py b/src/pretix/base/services/cross_selling.py index e36db7844..2ed0835be 100644 --- a/src/pretix/base/services/cross_selling.py +++ b/src/pretix/base/services/cross_selling.py @@ -1,12 +1,12 @@ from collections import defaultdict from decimal import Decimal from itertools import groupby +from math import inf from typing import List from django.utils.functional import cached_property -from math import inf -from pretix.base.models import ItemCategory, CartPosition, SalesChannel +from pretix.base.models import CartPosition, ItemCategory, SalesChannel from pretix.presale.views.event import get_grouped_items @@ -163,8 +163,8 @@ class CrossSellingService: item.original_price = item.original_price or item.display_price previous_price = item.display_price new_price = ( - previous_price * ( - (Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00')) + previous_price * ( + (Decimal('100.00') - discount_rule.benefit_discount_matching_percent) / Decimal('100.00')) ) item.display_price = new_price @@ -177,4 +177,3 @@ class CrossSellingService: new_items.append(item) return new_items - diff --git a/src/pretix/base/services/pricing.py b/src/pretix/base/services/pricing.py index 1484114d7..af5dddfac 100644 --- a/src/pretix/base/services/pricing.py +++ b/src/pretix/base/services/pricing.py @@ -31,7 +31,7 @@ from pretix.base.models import ( AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, SalesChannel, Voucher, ) -from pretix.base.models.discount import PositionInfo, Discount +from pretix.base.models.discount import Discount, PositionInfo from pretix.base.models.event import Event, SubEvent from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule from pretix.base.timemachine import time_machine_now diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 6b01db956..c0bab2469 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -515,7 +515,6 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep): return request._checkoutflow_addons_applicable - def is_completed(self, request, warn=False): if getattr(self, '_completed', None) is not None: return self._completed diff --git a/src/tests/base/test_cross_selling.py b/src/tests/base/test_cross_selling.py index 02e65db1e..6d92c6a5f 100644 --- a/src/tests/base/test_cross_selling.py +++ b/src/tests/base/test_cross_selling.py @@ -28,7 +28,7 @@ import pytest from django.utils.timezone import now from django_scopes import scopes_disabled -from pretix.base.models import Discount, Event, Organizer, CartPosition +from pretix.base.models import CartPosition, Discount, Event, Organizer from pretix.base.services.cross_selling import CrossSellingService @@ -41,32 +41,50 @@ def event(): ) return event + def pattern(regex, **kwargs): return re.compile(regex), kwargs + +cond_suffix = [ + pattern(r" in the same subevent$", subevent_mode=Discount.SUBEVENT_MODE_SAME), + pattern(r" in distinct subevents$", subevent_mode=Discount.SUBEVENT_MODE_DISTINCT), +] +cond_patterns = [ + pattern(r"^Buy at least (?P\d+) of (?P.+)$", + condition_all_products=False), + pattern(r"^Buy at least (?P\d+) products$", + condition_all_products=True), + pattern(r"^Spend at least (?P\d+)\$$", + condition_all_products=True), + pattern(r"^For every (?P\d+) of (?P.+)$", + condition_all_products=False), + pattern(r"^For every (?P\d+) products$", + condition_all_products=True), +] +benefit_patterns = [ + pattern(r"^get (?P\d+)% discount on them\.$", + benefit_same_products=True), + pattern(r"^get (?P\d+)% discount on everything\.$", + benefit_same_products=True), + pattern(r"^get (?P\d+)% discount on " + r"(?P\d+) of them\.$", + benefit_same_products=True), + pattern(r"^get (?P\d+)% discount on " + r"(?P\d+) of (?P.+)\.$", + benefit_same_products=False), + pattern(r"^get (?P\d+)% discount on " + r"(?P.+)\.$", + benefit_same_products=False), +] + + def make_discount(description, event: Event): - cond_suffix = [ - pattern(r" in the same subevent$", subevent_mode=Discount.SUBEVENT_MODE_SAME), - pattern(r" in distinct subevents$", subevent_mode=Discount.SUBEVENT_MODE_DISTINCT), - ] - cond_patterns = [ - pattern(r"^Buy at least (?P\d+) of (?P.+)$", condition_all_products=False), - pattern(r"^Buy at least (?P\d+) products$", condition_all_products=True), - pattern(r"^Spend at least (?P\d+)\$$", condition_all_products=True), - pattern(r"^For every (?P\d+) of (?P.+)$", condition_all_products=False), - pattern(r"^For every (?P\d+) products$", condition_all_products=True), - ] - benefit_patterns = [ - pattern(r"^get (?P\d+)% discount on them\.$", benefit_same_products=True), - pattern(r"^get (?P\d+)% discount on everything\.$", benefit_same_products=True), - pattern(r"^get (?P\d+)% discount on (?P\d+) of them\.$", benefit_same_products=True), - pattern(r"^get (?P\d+)% discount on (?P\d+) of (?P.+)\.$", benefit_same_products=False), - pattern(r"^get (?P\d+)% discount on (?P.+)\.$", benefit_same_products=False), - ] condition, benefit = description.split(', ') d = Discount(event=event, internal_name=description) d.save() + def apply(patterns: List[Tuple[re.Pattern, dict]], input): for regex, options in patterns: m = regex.search(input) @@ -224,7 +242,8 @@ def check_cart_behaviour(event, cart_contents, recommendations): positions = [ CartPosition( item_id=event.items.get(name=item_name).pk, - subevent_id=1, line_price_gross=Decimal(regular_price), addon_to=None, is_bundled=False, listed_price=Decimal(regular_price), price_after_voucher=Decimal(regular_price) + subevent_id=1, line_price_gross=Decimal(regular_price), addon_to=None, is_bundled=False, + listed_price=Decimal(regular_price), price_after_voucher=Decimal(regular_price) ) for (item_name, regular_price, expected_discounted_price) in cart_contents ] expected_recommendations = split_table(recommendations) @@ -232,13 +251,15 @@ def check_cart_behaviour(event, cart_contents, recommendations): service = CrossSellingService(event, event.organizer.sales_channels.get(identifier='web'), positions, None) result = service.get_data() result_recommendations = [ - [str(category.name), str(item.name), str(item.original_price.gross.quantize(Decimal('0.00'))), str(item.display_price.gross.quantize(Decimal('0.00'))), str(item.order_max)] + [str(category.name), str(item.name), str(item.original_price.gross.quantize(Decimal('0.00'))), + str(item.display_price.gross.quantize(Decimal('0.00'))), str(item.order_max)] for category, items in result for item in items ] assert result_recommendations == expected_recommendations - assert [str(price) for price, discount in service._discounted_prices] == [disc for (name, reg, disc) in cart_contents] + assert [str(price) for price, discount in service._discounted_prices] == [ + expected_discounted_price for (item_name, regular_price, expected_discounted_price) in cart_contents] @scopes_disabled() @@ -250,7 +271,8 @@ def test_2f1r_discount_cross_selling(event): ) make_discount('For every 2 of Regular Ticket, get 50% discount on 1 of Reduced Ticket.', event) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -259,7 +281,8 @@ def test_2f1r_discount_cross_selling(event): Tickets Reduced Ticket 23.00 11.50 1 ''' ) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -269,7 +292,8 @@ def test_2f1r_discount_cross_selling(event): recommendations=''' Price Discounted Price Max Count ''' ) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -280,7 +304,8 @@ def test_2f1r_discount_cross_selling(event): recommendations=''' Price Discounted Price Max Count ''' ) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -291,7 +316,8 @@ def test_2f1r_discount_cross_selling(event): Tickets Reduced Ticket 23.00 11.50 2 ''' ) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -304,7 +330,9 @@ def test_2f1r_discount_cross_selling(event): Tickets Reduced Ticket 23.00 11.50 1 ''' ) - check_cart_behaviour(event, + print("The interesting part:") + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -318,7 +346,8 @@ def test_2f1r_discount_cross_selling(event): Tickets Reduced Ticket 23.00 11.50 1 ''' ) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -348,7 +377,8 @@ def test_free_drinks(event): ) make_discount('Spend at least 100$, get 100% discount on 1 of Free Drinks.', event) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -356,7 +386,8 @@ def test_free_drinks(event): recommendations=''' Price Discounted Price Max Count ''' ) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -366,7 +397,8 @@ def test_free_drinks(event): Free Drinks Free Drinks 50.00 0.00 1 ''' ) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -388,7 +420,8 @@ def test_five_tickets_one_free(event): # we don't expect a recommendation here, as in the current implementation we only recommend based on discounts # where the condition is already completely satisfied but no (or not enough) benefitting products are in the # cart yet - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -398,7 +431,8 @@ def test_five_tickets_one_free(event): recommendations=''' Price Discounted Price Max Count ''' ) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -409,7 +443,8 @@ def test_five_tickets_one_free(event): recommendations=''' Price Discounted Price Max Count ''' ) - check_cart_behaviour(event, + check_cart_behaviour( + event, cart_contents=''' Price Discounted Regular Ticket 42.00 42.00 Regular Ticket 42.00 42.00 @@ -421,4 +456,4 @@ def test_five_tickets_one_free(event): ''', recommendations=''' Price Discounted Price Max Count ''' - ) \ No newline at end of file + )