forked from CGM_Public/pretix_original
Fix edge case in bundle price configuration
This commit is contained in:
@@ -581,18 +581,15 @@ class Item(LoggedModel):
|
||||
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False):
|
||||
price = price if price is not None else self.default_price
|
||||
|
||||
if not self.tax_rule:
|
||||
t = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
else:
|
||||
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
|
||||
override_tax_rate=override_tax_rate, currency=currency or self.event.currency)
|
||||
|
||||
bundled_sum = Decimal('0.00')
|
||||
bundled_sum_net = Decimal('0.00')
|
||||
bundled_sum_tax = Decimal('0.00')
|
||||
if include_bundled:
|
||||
for b in self.bundles.all():
|
||||
if b.designated_price and b.bundled_item.tax_rule_id != self.tax_rule_id:
|
||||
if b.bundled_variation:
|
||||
bprice = b.bundled_variation.tax(b.designated_price * b.count, base_price_is='gross',
|
||||
bprice = b.bundled_variation.tax(b.designated_price * b.count,
|
||||
base_price_is='gross',
|
||||
invoice_address=invoice_address,
|
||||
currency=currency)
|
||||
else:
|
||||
@@ -600,17 +597,23 @@ class Item(LoggedModel):
|
||||
invoice_address=invoice_address,
|
||||
base_price_is='gross',
|
||||
currency=currency)
|
||||
if not self.tax_rule:
|
||||
compare_price = TaxedPrice(gross=b.designated_price * b.count, net=b.designated_price * b.count,
|
||||
tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
|
||||
else:
|
||||
compare_price = self.tax_rule.tax(b.designated_price * b.count,
|
||||
override_tax_rate=override_tax_rate,
|
||||
invoice_address=invoice_address,
|
||||
currency=currency)
|
||||
t.net += bprice.net - compare_price.net
|
||||
t.tax += bprice.tax - compare_price.tax
|
||||
t.name = "MIXED!"
|
||||
bundled_sum += bprice.gross
|
||||
bundled_sum_net += bprice.net
|
||||
bundled_sum_tax += bprice.tax
|
||||
|
||||
if not self.tax_rule:
|
||||
t = TaxedPrice(gross=price - bundled_sum, net=price - bundled_sum, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
else:
|
||||
t = self.tax_rule.tax(price, base_price_is=base_price_is, invoice_address=invoice_address,
|
||||
override_tax_rate=override_tax_rate, currency=currency or self.event.currency,
|
||||
subtract_from_gross=bundled_sum)
|
||||
|
||||
if bundled_sum:
|
||||
t.name = "MIXED!"
|
||||
t.gross += bundled_sum
|
||||
t.net += bundled_sum_net
|
||||
t.tax += bundled_sum_tax
|
||||
|
||||
return t
|
||||
|
||||
|
||||
@@ -2737,6 +2737,7 @@ class CartPosition(AbstractPosition):
|
||||
tax_rule=self.item.tax_rule,
|
||||
invoice_address=invoice_address,
|
||||
bundled_sum=sum([b.price_after_voucher for b in bundled_positions]),
|
||||
is_bundled=self.is_bundled,
|
||||
)
|
||||
if line_price.gross != self.line_price_gross or line_price.rate != self.tax_rate:
|
||||
self.line_price_gross = line_price.gross
|
||||
|
||||
@@ -117,7 +117,7 @@ def get_listed_price(item: Item, variation: ItemVariation = None, subevent: SubE
|
||||
|
||||
|
||||
def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, custom_price_input_is_net: bool,
|
||||
tax_rule: TaxRule, invoice_address: InvoiceAddress, bundled_sum: Decimal) -> TaxedPrice:
|
||||
tax_rule: TaxRule, invoice_address: InvoiceAddress, bundled_sum: Decimal, is_bundled=False) -> TaxedPrice:
|
||||
if not tax_rule:
|
||||
tax_rule = TaxRule(
|
||||
name='',
|
||||
@@ -135,7 +135,8 @@ def get_line_price(price_after_voucher: Decimal, custom_price_input: Decimal, cu
|
||||
price = tax_rule.tax(max(custom_price_input, price.gross), base_price_is='gross', override_tax_rate=price.rate,
|
||||
invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
else:
|
||||
price = tax_rule.tax(price_after_voucher, invoice_address=invoice_address, subtract_from_gross=bundled_sum)
|
||||
price = tax_rule.tax(price_after_voucher, invoice_address=invoice_address, subtract_from_gross=bundled_sum,
|
||||
base_price_is='gross' if is_bundled else 'auto')
|
||||
|
||||
return price
|
||||
|
||||
|
||||
152
src/tests/presale/test_bundle_prices.py
Normal file
152
src/tests/presale/test_bundle_prices.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io 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/>.
|
||||
#
|
||||
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, Item, OrderPosition, Organizer, Quota,
|
||||
)
|
||||
from pretix.base.services.orders import _perform_order
|
||||
from pretix.testutils.sessions import get_cart_session_key
|
||||
|
||||
|
||||
class BundlePricesTest(TestCase):
|
||||
# This is an end to end test in addition to what's already tested in
|
||||
# cart and checkout tests to ensure consistency
|
||||
|
||||
@scopes_disabled()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.orga = Organizer.objects.create(name='CCC', slug='ccc')
|
||||
self.event = Event.objects.create(
|
||||
organizer=self.orga, name='30C3', slug='30c3',
|
||||
date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc),
|
||||
live=True,
|
||||
plugins="pretix.plugins.banktransfer",
|
||||
sales_channels=['web', 'bar']
|
||||
)
|
||||
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'))
|
||||
self.tr7 = self.event.tax_rules.create(rate=Decimal('7.00'))
|
||||
|
||||
self.food = Item.objects.create(event=self.event, name='Food',
|
||||
default_price=5, require_bundling=True,
|
||||
tax_rule=self.tr7)
|
||||
|
||||
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
|
||||
default_price=23,
|
||||
tax_rule=self.tr19)
|
||||
|
||||
self.bundle = self.ticket.bundles.create(
|
||||
bundled_item=self.food, designated_price=Decimal('10.00'),
|
||||
)
|
||||
|
||||
self.quota_all = Quota.objects.create(event=self.event, name='All', size=None)
|
||||
self.quota_all.items.add(self.ticket)
|
||||
self.quota_all.items.add(self.food)
|
||||
|
||||
self.session_key = get_cart_session_key(self.client, self.event)
|
||||
|
||||
def test_simple_case(self):
|
||||
# Verify correct price displayed on event page
|
||||
response = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
self.assertContains(response, '23.00')
|
||||
|
||||
# Verify correct price being added to cart
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_%d' % self.ticket.id: '1'
|
||||
}, follow=True)
|
||||
with scopes_disabled():
|
||||
cp1 = CartPosition.objects.get(is_bundled=False)
|
||||
cp2 = CartPosition.objects.get(is_bundled=True)
|
||||
|
||||
assert cp1.price == Decimal('13.00')
|
||||
assert cp1.item == self.ticket
|
||||
assert cp2.price == Decimal('10.00')
|
||||
assert cp2.item == self.food
|
||||
|
||||
# Make sure cart expires
|
||||
cp1.expires = now() - datetime.timedelta(minutes=120)
|
||||
cp1.save()
|
||||
cp2.expires = now() - datetime.timedelta(minutes=120)
|
||||
cp2.save()
|
||||
|
||||
# Verify price is kept if cart expires and order is sent
|
||||
with scopes_disabled():
|
||||
_perform_order(self.event, 'manual', [cp1.pk, cp2.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
op1 = OrderPosition.objects.get(is_bundled=False)
|
||||
op2 = OrderPosition.objects.get(is_bundled=True)
|
||||
assert op1.price == Decimal('13.00')
|
||||
assert op1.item == self.ticket
|
||||
assert op1.tax_rate == Decimal('19.00')
|
||||
assert op2.price == Decimal('10.00')
|
||||
assert op2.item == self.food
|
||||
assert op2.tax_rate == Decimal('7.00')
|
||||
|
||||
def test_net_price_definitions(self):
|
||||
self.tr19.price_includes_tax = False
|
||||
self.tr19.save()
|
||||
self.tr7.price_includes_tax = False
|
||||
self.tr7.save()
|
||||
# Verify correct price displayed on event page
|
||||
response = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
self.assertContains(response, '27.37')
|
||||
|
||||
# Verify correct price displayed on event page in net mode
|
||||
self.event.settings.display_net_prices = True
|
||||
response = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
self.assertContains(response, '23.95')
|
||||
|
||||
# Verify correct price being added to cart
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_%d' % self.ticket.id: '1'
|
||||
}, follow=True)
|
||||
with scopes_disabled():
|
||||
cp1 = CartPosition.objects.get(is_bundled=False)
|
||||
cp2 = CartPosition.objects.get(is_bundled=True)
|
||||
|
||||
assert cp1.price == Decimal('17.37')
|
||||
assert cp1.item == self.ticket
|
||||
assert cp2.price == Decimal('10.00')
|
||||
assert cp2.item == self.food
|
||||
|
||||
# Make sure cart expires
|
||||
cp1.expires = now() - datetime.timedelta(minutes=120)
|
||||
cp1.save()
|
||||
cp2.expires = now() - datetime.timedelta(minutes=120)
|
||||
cp2.save()
|
||||
|
||||
# Verify price is kept if cart expires and order is sent
|
||||
with scopes_disabled():
|
||||
_perform_order(self.event, 'manual', [cp1.pk, cp2.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
op1 = OrderPosition.objects.get(is_bundled=False)
|
||||
op2 = OrderPosition.objects.get(is_bundled=True)
|
||||
assert op1.price == Decimal('17.37')
|
||||
assert op1.item == self.ticket
|
||||
assert op1.tax_rate == Decimal('19.00')
|
||||
assert op2.price == Decimal('10.00')
|
||||
assert op2.item == self.food
|
||||
assert op2.tax_rate == Decimal('7.00')
|
||||
Reference in New Issue
Block a user