mirror of
https://github.com/pretix/pretix.git
synced 2026-09-13 16:14:40 +00:00
Show (some) discounts in product list
Discounts are currently only shown in the cart, after products have been selected. This was not a big problem since we invented the discount machinery specifically for group discounts, which can only be applied in the cart. However it *is* possible to define a Discount that applies to a group of one. Then, one would expect that the discounted price would be shown already on the front page of the shop. Back then, I decided against this for complexity and performance concerns, but after looking into it, it's not actually that bad. On the user side, I think this is a game changer for the discount feature. Now, I could use discount rules also to implement early-bird pricing without duplicating producs/variations, or I can do a christmas discount on all subevents in december without modifying all individual subevents.
This commit is contained in:
@@ -510,7 +510,9 @@ class Item(LoggedModel):
|
|||||||
verbose_name=_("Free price input"),
|
verbose_name=_("Free price input"),
|
||||||
help_text=_("If this option is active, your users can choose the price themselves. The price configured above "
|
help_text=_("If this option is active, your users can choose the price themselves. The price configured above "
|
||||||
"is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect "
|
"is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect "
|
||||||
"additional donations for your event.")
|
"additional donations for your event. We recommend against combining this feature with automatic "
|
||||||
|
"discounts since discounts are applied as the last step of price computation, which means that the "
|
||||||
|
"price entered by the customer will be modified again.")
|
||||||
)
|
)
|
||||||
free_price_suggestion = models.DecimalField(
|
free_price_suggestion = models.DecimalField(
|
||||||
verbose_name=_("Suggested price"),
|
verbose_name=_("Suggested price"),
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
#
|
#
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from decimal import Decimal
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
@@ -29,11 +30,13 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.db.models.lookups import Exact
|
from django.db.models.lookups import Exact
|
||||||
|
|
||||||
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
ItemVariation, Quota, SalesChannel, SeatCategoryMapping,
|
Discount, Event, Item, ItemVariation, Quota, SalesChannel,
|
||||||
|
SeatCategoryMapping, SubEvent, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.models.items import (
|
from pretix.base.models.items import (
|
||||||
Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
||||||
)
|
)
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.timemachine import time_machine_now
|
from pretix.base.timemachine import time_machine_now
|
||||||
@@ -54,6 +57,35 @@ def item_group_by_category(items):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _single_item_discounts(event: Event, sales_channel: Union[str, SalesChannel],
|
||||||
|
subevent: SubEvent=None, voucher: Voucher=None, is_addons=False) -> List[Discount]:
|
||||||
|
discount_qs = event.discounts.filter(
|
||||||
|
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
|
||||||
|
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
|
||||||
|
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=sales_channel),
|
||||||
|
active=True,
|
||||||
|
# Only discounts that can be applied before we know the full cart
|
||||||
|
benefit_same_products=True,
|
||||||
|
benefit_only_apply_to_cheapest_n_matches__isnull=True,
|
||||||
|
condition_min_value=Decimal("0.00"),
|
||||||
|
condition_min_count=1,
|
||||||
|
).prefetch_related('condition_limit_products').order_by('position', 'pk')
|
||||||
|
|
||||||
|
if subevent:
|
||||||
|
discount_qs = discount_qs.filter(
|
||||||
|
Q(subevent_date_from__isnull=True) | Q(subevent_date_from__lte=subevent.date_from),
|
||||||
|
Q(subevent_date_until__isnull=True) | Q(subevent_date_until__gte=subevent.date_from),
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_addons:
|
||||||
|
discount_qs = discount_qs.filter(condition_apply_to_addons=True, benefit_apply_to_addons=True)
|
||||||
|
|
||||||
|
if voucher and voucher.price_mode != "none":
|
||||||
|
discount_qs = discount_qs.filter(condition_ignore_voucher_discounted=False, benefit_ignore_voucher_discounted=False)
|
||||||
|
|
||||||
|
return list(discount_qs)
|
||||||
|
|
||||||
|
|
||||||
def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None,
|
def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None,
|
||||||
allow_addons=False, allow_cross_sell=False,
|
allow_addons=False, allow_cross_sell=False,
|
||||||
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
|
||||||
@@ -190,6 +222,18 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
|
|||||||
if filter_categories:
|
if filter_categories:
|
||||||
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
|
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
|
||||||
|
|
||||||
|
# We pre-computate discounts that do not rely on specific combinations in the cart.
|
||||||
|
# This is not the same order of operations that is applied in the cart, so there could be some differences
|
||||||
|
# when it comes to tax rate handling, but we don't have that information in the product list anyway, so
|
||||||
|
# that is acceptable.
|
||||||
|
discounts = _single_item_discounts(
|
||||||
|
event=event,
|
||||||
|
sales_channel=channel,
|
||||||
|
voucher=voucher,
|
||||||
|
subevent=subevent,
|
||||||
|
is_addons=allow_addons,
|
||||||
|
)
|
||||||
|
|
||||||
display_add_to_cart = False
|
display_add_to_cart = False
|
||||||
quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
|
quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
|
||||||
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
|
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
|
||||||
@@ -272,6 +316,10 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
|
|||||||
if resp:
|
if resp:
|
||||||
item.description += ("<br/>" if item.description else "") + resp
|
item.description += ("<br/>" if item.description else "") + resp
|
||||||
|
|
||||||
|
matching_discounts = [
|
||||||
|
d for d in discounts if d.condition_all_products or item in d.condition_limit_products.all()
|
||||||
|
]
|
||||||
|
|
||||||
if not item.has_variations:
|
if not item.has_variations:
|
||||||
item._remove = False
|
item._remove = False
|
||||||
if not bool(item._subevent_quotas):
|
if not bool(item._subevent_quotas):
|
||||||
@@ -299,30 +347,51 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
|
|||||||
max_per_order
|
max_per_order
|
||||||
)
|
)
|
||||||
|
|
||||||
original_price = item_price_override.get(item.pk, item.default_price)
|
configured_price = item_price_override.get(item.pk, item.default_price)
|
||||||
voucher_reduced = False
|
voucher_reduced = False
|
||||||
if voucher:
|
if voucher:
|
||||||
price = voucher.calculate_price(original_price)
|
price = voucher.calculate_price(configured_price)
|
||||||
voucher_reduced = price < original_price
|
voucher_reduced = price < configured_price
|
||||||
include_bundled = not voucher.all_bundles_included
|
include_bundled = not voucher.all_bundles_included
|
||||||
else:
|
else:
|
||||||
price = original_price
|
price = configured_price
|
||||||
include_bundled = True
|
include_bundled = True
|
||||||
|
|
||||||
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
|
if matching_discounts and not item.free_price:
|
||||||
|
# Discounts and free prices are a non-recommended combination that behaves unintuitively, so we can
|
||||||
|
# accept it not being handled here.
|
||||||
|
discount = matching_discounts[0] # First matching discount rule always wins
|
||||||
|
# First handle taxes because discount is always computed on gross
|
||||||
|
taxed_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||||
|
price_without_bundles = taxed_price.gross - sum(b.count * b.designated_price for b in item.bundles.all())
|
||||||
|
discounted_price = round_decimal(
|
||||||
|
taxed_price.gross - price_without_bundles * discount.benefit_discount_matching_percent / Decimal('100.00'),
|
||||||
|
event.currency,
|
||||||
|
)
|
||||||
|
item.display_price = item.tax(discounted_price, currency=event.currency, include_bundled=include_bundled,
|
||||||
|
base_price_is='gross')
|
||||||
|
else:
|
||||||
|
discount = None
|
||||||
|
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||||
|
|
||||||
if item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
if item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||||
item.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency, include_bundled=include_bundled)
|
item.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency, include_bundled=include_bundled)
|
||||||
else:
|
else:
|
||||||
item.suggested_price = item.display_price
|
item.suggested_price = item.display_price
|
||||||
|
|
||||||
if price != original_price:
|
if voucher_reduced or (discount and not item.original_price):
|
||||||
item.original_price = item.tax(original_price, currency=event.currency, include_bundled=True)
|
# If a voucher is used, we use the non-voucher price as the "original price", even if a different
|
||||||
else:
|
# original price is set, to highlight the voucher's impact. We also use the configured price as
|
||||||
item.original_price = (
|
# original price if a discount is applied and no explicit original price is set
|
||||||
item.tax(item.original_price, currency=event.currency, include_bundled=True,
|
item.original_price = item.tax(configured_price, currency=event.currency, include_bundled=True)
|
||||||
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
elif item.original_price:
|
||||||
if item.original_price else None
|
item.original_price = item.tax(
|
||||||
|
item.original_price, currency=event.currency, include_bundled=True,
|
||||||
|
base_price_is='net' if event.settings.display_net_prices else 'gross' # backwards-compat
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
item.original_price = None
|
||||||
|
|
||||||
if not display_add_to_cart:
|
if not display_add_to_cart:
|
||||||
display_add_to_cart = not item.requires_seat and item.order_max > 0
|
display_add_to_cart = not item.requires_seat and item.order_max > 0
|
||||||
else:
|
else:
|
||||||
@@ -352,35 +421,53 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
|
|||||||
max_per_order
|
max_per_order
|
||||||
)
|
)
|
||||||
|
|
||||||
original_price = var_price_override.get(var.pk, var.price)
|
configured_price = var_price_override.get(var.pk, var.price)
|
||||||
voucher_reduced = False
|
voucher_reduced = False
|
||||||
if voucher:
|
if voucher:
|
||||||
price = voucher.calculate_price(original_price)
|
price = voucher.calculate_price(configured_price)
|
||||||
voucher_reduced = price < original_price
|
voucher_reduced = price < configured_price
|
||||||
include_bundled = not voucher.all_bundles_included
|
include_bundled = not voucher.all_bundles_included
|
||||||
else:
|
else:
|
||||||
price = original_price
|
price = configured_price
|
||||||
include_bundled = True
|
include_bundled = True
|
||||||
|
|
||||||
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
|
if matching_discounts and not item.free_price:
|
||||||
|
# Discounts and free prices are a non-recommended combination that behaves unintuitively, so we can
|
||||||
|
# accept it not being handled here.
|
||||||
|
discount = matching_discounts[0] # First matching discount rule always wins
|
||||||
|
# First handle taxes because discount is always computed on gross
|
||||||
|
taxed_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||||
|
price_without_bundles = taxed_price.gross - sum(b.count * b.designated_price for b in item.bundles.all())
|
||||||
|
discounted_price = round_decimal(
|
||||||
|
taxed_price.gross - price_without_bundles * discount.benefit_discount_matching_percent / Decimal('100.00'),
|
||||||
|
event.currency,
|
||||||
|
)
|
||||||
|
var.display_price = var.tax(discounted_price, currency=event.currency,
|
||||||
|
include_bundled=include_bundled,
|
||||||
|
base_price_is='gross')
|
||||||
|
else:
|
||||||
|
discount = None
|
||||||
|
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
|
||||||
|
|
||||||
if item.free_price and var.free_price_suggestion is not None and not voucher_reduced:
|
if item.free_price and var.free_price_suggestion is not None and not voucher_reduced:
|
||||||
var.suggested_price = item.tax(max(price, var.free_price_suggestion), currency=event.currency,
|
var.suggested_price = var.tax(max(price, var.free_price_suggestion), currency=event.currency,
|
||||||
include_bundled=include_bundled)
|
include_bundled=include_bundled)
|
||||||
elif item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
elif item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
|
||||||
var.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency,
|
var.suggested_price = var.tax(max(price, item.free_price_suggestion), currency=event.currency,
|
||||||
include_bundled=include_bundled)
|
include_bundled=include_bundled)
|
||||||
else:
|
else:
|
||||||
var.suggested_price = var.display_price
|
var.suggested_price = var.display_price
|
||||||
|
|
||||||
if price != original_price:
|
if voucher_reduced or (discount and not item.original_price and not var.original_price):
|
||||||
var.original_price = var.tax(original_price, currency=event.currency, include_bundled=True)
|
var.original_price = var.tax(configured_price, currency=event.currency, include_bundled=True)
|
||||||
|
elif item.original_price or var.original_price:
|
||||||
|
var.original_price = var.tax(
|
||||||
|
var.original_price or item.original_price, currency=event.currency,
|
||||||
|
include_bundled=True,
|
||||||
|
base_price_is='net' if event.settings.display_net_prices else 'gross' # backwards-compat
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
var.original_price = (
|
var.original_price = None
|
||||||
var.tax(var.original_price or item.original_price, currency=event.currency,
|
|
||||||
include_bundled=True,
|
|
||||||
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
|
|
||||||
) if var.original_price or item.original_price else None
|
|
||||||
|
|
||||||
var.current_unavailability_reason = _get_variant_unavailability_reason(var, has_voucher=voucher, subevent=subevent)
|
var.current_unavailability_reason = _get_variant_unavailability_reason(var, has_voucher=voucher, subevent=subevent)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
#
|
||||||
|
# This file is part of pretix (Community Edition).
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||||
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||||
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||||
|
#
|
||||||
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||||
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||||
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||||
|
# this file, see <https://pretix.eu/about/en/license>.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
|
# <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django_scopes import scope
|
||||||
|
|
||||||
|
from pretix.base.models import Event, Organizer
|
||||||
|
from pretix.presale.productlist import prepare_item_list_for_shop
|
||||||
|
|
||||||
|
# Tests for prepare_item_list_for_shop are really incomplete since historically, most features are
|
||||||
|
# tested on the test_event or test_widget layer. We'll slowly add new tests here to test closer to the
|
||||||
|
# source.
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def event():
|
||||||
|
o = Organizer.objects.create(name='MRMCD', slug='mrmcd')
|
||||||
|
e = Event.objects.create(
|
||||||
|
organizer=o, name='MRMCD2015', slug='2015',
|
||||||
|
date_from=now(), live=True
|
||||||
|
)
|
||||||
|
with scope(organizer=o):
|
||||||
|
yield e
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def quota(event):
|
||||||
|
return event.quotas.create(name="Tickets", size=500)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def item(event, quota):
|
||||||
|
i = event.items.create(name="Ticket", default_price=Decimal("42.00"))
|
||||||
|
quota.items.add(i)
|
||||||
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def variation(event, quota):
|
||||||
|
i = event.items.create(name="Ticket with variants", default_price=Decimal("99.00"))
|
||||||
|
v = i.variations.create(value="Default", default_price=Decimal("42.00"))
|
||||||
|
quota.items.add(i)
|
||||||
|
quota.variations.add(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def discount(event):
|
||||||
|
return event.discounts.create(
|
||||||
|
internal_name="Early-Bird-Discount",
|
||||||
|
all_sales_channels=True,
|
||||||
|
available_from=now() - timedelta(days=2),
|
||||||
|
available_until=now() + timedelta(days=2),
|
||||||
|
condition_all_products=True,
|
||||||
|
condition_min_count=1,
|
||||||
|
benefit_discount_matching_percent=Decimal("10.00"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def channel(event):
|
||||||
|
return event.organizer.sales_channels.get(identifier="web")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_default_price(event, item, variation, channel):
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("42")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("42")
|
||||||
|
|
||||||
|
|
||||||
|
def _test_no_discount(event, channel):
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("42")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("42")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discount_applied(event, item, variation, channel, discount):
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("37.80")
|
||||||
|
assert items[0].original_price.gross == Decimal("42.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
|
||||||
|
assert items[1].available_variations[0].original_price.gross == Decimal("42.00")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discount_for_groups_ignored(event, item, variation, channel, discount):
|
||||||
|
discount.condition_min_count = 2
|
||||||
|
discount.save()
|
||||||
|
_test_no_discount(event, channel)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discount_original_price_kept(event, item, variation, channel, discount):
|
||||||
|
item.original_price = Decimal("46.00")
|
||||||
|
item.save()
|
||||||
|
variation.original_price = Decimal("46.00")
|
||||||
|
variation.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("37.80")
|
||||||
|
assert items[0].original_price.gross == Decimal("46.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
|
||||||
|
assert items[1].available_variations[0].original_price.gross == Decimal("46.00")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discount_out_of_timeframe(event, item, variation, channel, discount):
|
||||||
|
discount.available_from = now() + timedelta(days=2)
|
||||||
|
discount.save()
|
||||||
|
_test_no_discount(event, channel)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discount_wrong_channel(event, item, variation, channel, discount):
|
||||||
|
discount.all_sales_channels = False
|
||||||
|
discount.save()
|
||||||
|
_test_no_discount(event, channel)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discount_benefits_other_products_ignored(event, item, variation, channel, discount):
|
||||||
|
discount.benefit_same_products = False
|
||||||
|
discount.save()
|
||||||
|
_test_no_discount(event, channel)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discounts_for_addons(event, item, variation, channel, discount):
|
||||||
|
discount.condition_apply_to_addons = False
|
||||||
|
discount.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel, allow_addons=True)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("42.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("42.00")
|
||||||
|
|
||||||
|
discount.condition_apply_to_addons = True
|
||||||
|
discount.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel, allow_addons=True)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("37.80")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discounts_with_voucher(event, item, variation, channel, discount):
|
||||||
|
voucher = event.vouchers.create(code="FOO", price_mode="subtract", value=Decimal("10.00"))
|
||||||
|
voucher2 = event.vouchers.create(code="BAR")
|
||||||
|
discount.condition_ignore_voucher_discounted = True
|
||||||
|
discount.save()
|
||||||
|
item.original_price = Decimal("46.00")
|
||||||
|
item.save()
|
||||||
|
variation.item.original_price = Decimal("46.00")
|
||||||
|
variation.item.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel, voucher=voucher)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("32.00")
|
||||||
|
assert items[0].original_price.gross == Decimal("42.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("32.00")
|
||||||
|
assert items[1].available_variations[0].original_price.gross == Decimal("42.00")
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel, voucher=voucher2)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("37.80")
|
||||||
|
assert items[0].original_price.gross == Decimal("46.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
|
||||||
|
assert items[1].available_variations[0].original_price.gross == Decimal("46.00")
|
||||||
|
|
||||||
|
discount.condition_ignore_voucher_discounted = False
|
||||||
|
discount.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel, voucher=voucher)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("28.80")
|
||||||
|
assert items[0].original_price.gross == Decimal("42.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("28.80")
|
||||||
|
assert items[1].available_variations[0].original_price.gross == Decimal("42.00")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discounts_for_subevent_timeframe(event, quota, item, variation, channel, discount):
|
||||||
|
event.has_subevents = True
|
||||||
|
event.save()
|
||||||
|
se = event.subevents.create(
|
||||||
|
name="Foobar", date_from=datetime(2028, 12, 27, 10, 0, 0, tzinfo=UTC)
|
||||||
|
)
|
||||||
|
quota.subevent = se
|
||||||
|
quota.save()
|
||||||
|
|
||||||
|
discount.subevent_date_from = se.date_from + timedelta(days=1)
|
||||||
|
discount.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel, subevent=se)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("42.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("42.00")
|
||||||
|
|
||||||
|
discount.subevent_date_from = se.date_from - timedelta(days=1)
|
||||||
|
discount.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel, subevent=se)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("37.80")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discounts_for_products(event, quota, item, variation, channel, discount):
|
||||||
|
discount.condition_all_products = False
|
||||||
|
discount.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("42.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("42.00")
|
||||||
|
|
||||||
|
discount.condition_limit_products.add(item)
|
||||||
|
discount.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("37.80")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discounts_for_products_tax_additive(event, quota, item, variation, channel, discount):
|
||||||
|
tr = event.tax_rules.create(rate=Decimal("19.00"), price_includes_tax=False)
|
||||||
|
item.tax_rule = tr
|
||||||
|
item.save()
|
||||||
|
variation.item.tax_rule = tr
|
||||||
|
variation.item.save()
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("44.98")
|
||||||
|
assert items[0].original_price.gross == Decimal("49.98")
|
||||||
|
assert items[0].original_price.net == Decimal("42.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("44.98")
|
||||||
|
assert items[1].available_variations[0].original_price.gross == Decimal("49.98")
|
||||||
|
assert items[1].available_variations[0].original_price.net == Decimal("42.00")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_discounts_for_products_tax_additive_bundle_included(event, quota, item, variation, channel, discount):
|
||||||
|
tr = event.tax_rules.create(rate=Decimal("19.00"), price_includes_tax=False)
|
||||||
|
b = event.items.create(name="Bundled product", default_price=Decimal("10.00"), tax_rule=tr, require_bundling=True)
|
||||||
|
quota.items.add(b)
|
||||||
|
item.tax_rule = tr
|
||||||
|
item.save()
|
||||||
|
variation.item.tax_rule = tr
|
||||||
|
variation.item.save()
|
||||||
|
item.bundles.create(bundled_item=b, count=2, designated_price=Decimal("5.00"))
|
||||||
|
items, _ = prepare_item_list_for_shop(event, channel=channel)
|
||||||
|
assert len(items) == 2
|
||||||
|
assert items[0].display_price.gross == Decimal("45.98")
|
||||||
|
assert items[0].original_price.gross == Decimal("49.98")
|
||||||
|
assert items[0].original_price.net == Decimal("42.00")
|
||||||
|
assert items[1].available_variations[0].display_price.gross == Decimal("45.98")
|
||||||
|
assert items[1].available_variations[0].original_price.gross == Decimal("49.98")
|
||||||
|
assert items[1].available_variations[0].original_price.net == Decimal("42.00")
|
||||||
Reference in New Issue
Block a user