diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 980b728612..76c4886246 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -37,6 +37,7 @@ import uuid from collections import defaultdict from decimal import Decimal +from django import forms from django.conf import settings from django.contrib import messages from django.core.cache import caches @@ -69,6 +70,7 @@ from pretix.base.services.cart import ( from pretix.base.services.cross_selling import CrossSellingService from pretix.base.services.memberships import validate_memberships_in_order from pretix.base.services.orders import perform_order +from pretix.base.services.pricing import get_price from pretix.base.services.tasks import EventTask from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.signals import validate_cart_addons @@ -529,6 +531,48 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep): self._completed = True return True + def _get_initial_val_price(self, current_addon_products, cartpos, item, variation): + val = None + price = None + + if self.request.POST: + if variation: + field = f'cp_{cartpos.pk}_variation_{item.pk}_{variation.pk}' + else: + field = f'cp_{cartpos.pk}_item_{item.pk}' + + try: + val = int(self.request.POST.get(field) or '0') + except ValueError: + pass + if val and item.free_price: + custom_price = forms.DecimalField(localize=True).to_python(self.request.POST.get(f'{field}_price') or '0') + price = get_price( + item, variation, voucher=cartpos.voucher, custom_price=custom_price, subevent=cartpos.subevent, + custom_price_is_net=self.event.settings.display_net_prices, + invoice_address=self.invoice_address, + ) + else: + price = variation.suggested_price if variation else item.suggested_price + + else: + current_products = current_addon_products[item.pk, variation.pk if variation else None] + val = len(current_products) + if current_products and item.free_price: + a = current_products[0] + price = TaxedPrice( + net=a.price - a.tax_value, + gross=a.price, + tax=a.tax_value, + name=a.item.tax_rule.name if a.item.tax_rule else "", + rate=a.tax_rate, + code=a.item.tax_rule.code if a.item.tax_rule else None, + ) + else: + price = variation.suggested_price if variation else item.suggested_price + + return val, price + @cached_property def forms(self): """ @@ -587,34 +631,10 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep): if i.has_variations: for v in i.available_variations: - v.initial = len(current_addon_products[i.pk, v.pk]) - if v.initial and i.free_price: - a = current_addon_products[i.pk, v.pk][0] - v.initial_price = TaxedPrice( - net=a.price - a.tax_value, - gross=a.price, - tax=a.tax_value, - name=a.item.tax_rule.name if a.item.tax_rule else "", - rate=a.tax_rate, - code=a.item.tax_rule.code if a.item.tax_rule else None, - ) - else: - v.initial_price = v.suggested_price + v.initial, v.initial_price = self._get_initial_val_price(current_addon_products, cartpos, i, v) i.expand = any(v.initial for v in i.available_variations) else: - i.initial = len(current_addon_products[i.pk, None]) - if i.initial and i.free_price: - a = current_addon_products[i.pk, None][0] - i.initial_price = TaxedPrice( - net=a.price - a.tax_value, - gross=a.price, - tax=a.tax_value, - name=a.item.tax_rule.name if a.item.tax_rule else "", - rate=a.tax_rate, - code=a.item.tax_rule.code if a.item.tax_rule else None, - ) - else: - i.initial_price = i.suggested_price + i.initial, i.initial_price = self._get_initial_val_price(current_addon_products, cartpos, i, None) if items: formsetentry['categories'].append({ diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index 5de5bcc582..15f99f030e 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -99,6 +99,15 @@ class BaseCheckoutTestCase: self.workshopquota.variations.add(self.workshop2a) self.workshopquota.variations.add(self.workshop2b) + self.parkingcat = ItemCategory.objects.create(name="Parking", is_addon=True, event=self.event) + self.parkingquota = Quota.objects.create(event=self.event, name='Parking', size=5) + self.parking1 = Item.objects.create(event=self.event, name='Premium Parking', + category=self.parkingcat, default_price=Decimal('15.00')) + self.parking2 = Item.objects.create(event=self.event, name='Standard Parking', + category=self.parkingcat, default_price=Decimal('5.00')) + self.parkingquota.items.add(self.parking1) + self.parkingquota.items.add(self.parking2) + def _set_session(self, key, value): session = self.client.session session['carts'][get_cart_session_key(self.client, self.event)][key] = value @@ -4202,6 +4211,58 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase): assert '35.29' in response.content.decode() assert '10.08' in response.content.decode() + def test_set_addons_invalid_initial(self): + self.event.settings.locales = ['de', 'en'] + self.event.settings.locale = 'de' + with scopes_disabled(): + ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat, min_count=1) + ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.parkingcat, min_count=1) + cp1 = CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() - timedelta(minutes=10) + ) + self.workshop1.free_price = True + self.workshop1.save() + self.workshop2.free_price = True + self.workshop2.save() + + ws1_val = 'cp_{}_item_{}'.format(cp1.pk, self.workshop1.pk) + ws1_price = 'cp_{}_item_{}_price'.format(cp1.pk, self.workshop1.pk) + ws2a_val = 'cp_{}_variation_{}_{}'.format(cp1.pk, self.workshop2.pk, self.workshop2a.pk) + ws2a_price = 'cp_{}_variation_{}_{}_price'.format(cp1.pk, self.workshop2.pk, self.workshop2a.pk) + p1_val = 'cp_{}_item_{}'.format(cp1.pk, self.parking1.pk) + p2_val = 'cp_{}_item_{}'.format(cp1.pk, self.parking2.pk) + + response = self.client.post('/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug), { + ws1_val: '1', + ws2a_val: '1', + }) + assert response.status_code == 200 + with scopes_disabled(): + assert cp1.addons.count() == 0 + doc = BeautifulSoup(response.text, 'lxml') + assert doc.find('input', {'name': ws1_val}).attrs.get('checked') + assert doc.find('input', {'name': ws2a_val}).attrs.get('checked') + assert not doc.find('input', {'name': p1_val}).attrs.get('checked') + assert not doc.find('input', {'name': p2_val}).attrs.get('checked') + + response = self.client.post('/%s/%s/checkout/addons/' % (self.orga.slug, self.event.slug), { + ws1_val: '1', + ws1_price: '222,22', + ws2a_val: '1', + ws2a_price: '333.33', + }) + assert response.status_code == 200 + with scopes_disabled(): + assert cp1.addons.count() == 0 + doc = BeautifulSoup(response.text, 'lxml') + assert doc.find('input', {'name': ws1_val}).attrs.get('checked') + assert doc.find('input', {'name': ws1_price}).attrs.get('value') in ['222.22', '222,22'] + assert doc.find('input', {'name': ws2a_val}).attrs.get('checked') + assert doc.find('input', {'name': ws2a_price}).attrs.get('value') in ['333.33', '333,33'] + assert not doc.find('input', {'name': p1_val}).attrs.get('checked') + assert not doc.find('input', {'name': p2_val}).attrs.get('checked') + def test_confirm_subevent_presale_not_yet(self): with scopes_disabled(): self.event.has_subevents = True