AddOnsStep: Use post data as initial data if exists (Z#23232311)

This commit is contained in:
Kara Engelhardt
2026-04-27 13:50:33 +02:00
committed by pajowu
parent 2d31c62812
commit 49f692c666
2 changed files with 107 additions and 26 deletions

View File

@@ -37,6 +37,7 @@ import uuid
from collections import defaultdict from collections import defaultdict
from decimal import Decimal from decimal import Decimal
from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.cache import caches 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.cross_selling import CrossSellingService
from pretix.base.services.memberships import validate_memberships_in_order from pretix.base.services.memberships import validate_memberships_in_order
from pretix.base.services.orders import perform_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.services.tasks import EventTask
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import validate_cart_addons from pretix.base.signals import validate_cart_addons
@@ -529,6 +531,48 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
self._completed = True self._completed = True
return 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 @cached_property
def forms(self): def forms(self):
""" """
@@ -587,34 +631,10 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
if i.has_variations: if i.has_variations:
for v in i.available_variations: for v in i.available_variations:
v.initial = len(current_addon_products[i.pk, v.pk]) v.initial, v.initial_price = self._get_initial_val_price(current_addon_products, cartpos, i, v)
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
i.expand = any(v.initial for v in i.available_variations) i.expand = any(v.initial for v in i.available_variations)
else: else:
i.initial = len(current_addon_products[i.pk, None]) i.initial, i.initial_price = self._get_initial_val_price(current_addon_products, cartpos, i, 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
if items: if items:
formsetentry['categories'].append({ formsetentry['categories'].append({

View File

@@ -99,6 +99,15 @@ class BaseCheckoutTestCase:
self.workshopquota.variations.add(self.workshop2a) self.workshopquota.variations.add(self.workshop2a)
self.workshopquota.variations.add(self.workshop2b) 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): def _set_session(self, key, value):
session = self.client.session session = self.client.session
session['carts'][get_cart_session_key(self.client, self.event)][key] = value 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 '35.29' in response.content.decode()
assert '10.08' 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): def test_confirm_subevent_presale_not_yet(self):
with scopes_disabled(): with scopes_disabled():
self.event.has_subevents = True self.event.has_subevents = True