Compare commits

...

12 Commits

Author SHA1 Message Date
dependabot[bot]
26d6ed7072 Bump arabic-reshaper from 3.0.0 to 3.0.1
Bumps [arabic-reshaper](https://github.com/mpcabd/python-arabic-reshaper) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/mpcabd/python-arabic-reshaper/releases)
- [Commits](https://github.com/mpcabd/python-arabic-reshaper/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: arabic-reshaper
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 18:13:12 +00:00
Raphael Michel
bda27d72e7 Bump version to 2026.5.0.dev0 2026-04-28 16:48:33 +02:00
Raphael Michel
f67690bc56 Bump version to 2025.5.0.dev0 2026-04-28 16:47:51 +02:00
Raphael Michel
75c8f97080 Bump version to 2026.4.0 2026-04-28 16:47:33 +02:00
Raphael Michel
10789f097d Bump version to 2025.5.0.dev0 2026-04-28 16:39:29 +02:00
Raphael Michel
1adec102e6 Bump version to 2025.4.0 2026-04-28 16:39:24 +02:00
Raphael Michel
921fd801e5 Thumbnails: Perform color space transform before resizing (Z#23232101) (#6120) 2026-04-28 15:35:27 +02:00
Kara Engelhardt
448d2e70d5 AddOnsStep: Expand selected variants 2026-04-28 14:50:35 +02:00
Kara Engelhardt
49f692c666 AddOnsStep: Use post data as initial data if exists (Z#23232311) 2026-04-28 14:50:35 +02:00
Kara Engelhardt
2d31c62812 Allow dash character in pdf placeholders 2026-04-28 13:10:08 +02:00
Raphael Michel
08df3d828d Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6295 of 6295 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de_Informal/

powered by weblate
2026-04-28 12:20:37 +02:00
Raphael Michel
96e10bcd71 Translations: Update German
Currently translated at 100.0% (6295 of 6295 strings)

Translation: pretix/pretix
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix/de/

powered by weblate
2026-04-28 12:20:37 +02:00
9 changed files with 155 additions and 82 deletions

View File

@@ -27,7 +27,7 @@ classifiers = [
]
dependencies = [
"arabic-reshaper==3.0.0", # Support for Arabic in reportlab
"arabic-reshaper==3.0.1", # Support for Arabic in reportlab
"babel",
"BeautifulSoup4==4.14.*",
"bleach==6.3.*",

View File

@@ -19,4 +19,4 @@
# 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/>.
#
__version__ = "2026.4.0.dev0"
__version__ = "2026.5.0.dev0"

View File

@@ -923,7 +923,7 @@ class Renderer:
# We do not use str.format like in emails so we (a) can evaluate lazily and (b) can re-implement this
# 1:1 on other platforms that render PDFs through our API (libpretixprint)
return re.sub(r'\{([a-zA-Z0-9:_]+)\}', replace, text)
return re.sub(r'\{([-a-zA-Z0-9:_]+)\}', replace, text)
elif o['content'].startswith('itemmeta:'):
if op.variation_id:

View File

@@ -173,6 +173,7 @@ def create_thumbnail(source, size, formats=None):
# filesystem path, this only works because _open() uses safe_join, which accepts absolute paths if they match the
# expected base dir. For NanoCDN Files, this works because source.name is set to the storage path.
source_rb = default_storage.open(source_name, mode='rb')
source_ext = os.path.splitext(source_name)[1].lower()
image = Image.open(BytesIO(source_rb.read()), formats=formats or settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
try:
@@ -183,11 +184,14 @@ def create_thumbnail(source, size, formats=None):
frames = []
durations = []
for f in ImageSequence.Iterator(image):
if f.mode in ("P", "PA") and source_ext == '.png':
f = f.convert('RGBA')
if f.mode not in ("1", "L", "RGB", "RGBA"):
f = f.convert('RGB')
durations.append(f.info.get("duration", 1000))
frames.append(resize_image(f, size))
image_out = frames[0]
save_kwargs = {}
source_ext = os.path.splitext(source_name)[1].lower()
if source_ext == '.jpg' or source_ext == '.jpeg':
# Yields better file sizes for photos
@@ -211,10 +215,6 @@ def create_thumbnail(source, size, formats=None):
checksum = hashlib.md5(image.tobytes()).hexdigest()
name = checksum + '.' + size.replace('^', 'c') + '.' + target_ext
buffer = BytesIO()
if image_out.mode == "P" and source_ext == '.png':
image_out = image_out.convert('RGBA')
if image_out.mode not in ("1", "L", "RGB", "RGBA"):
image_out = image_out.convert('RGB')
image_out.save(fp=buffer, format=target_ext.upper(), quality=quality, **save_kwargs)
imgfile = ContentFile(buffer.getvalue())

View File

@@ -5,16 +5,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:03+0000\n"
"PO-Revision-Date: 2026-03-30 11:50+0000\n"
"PO-Revision-Date: 2026-04-28 09:22+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
">\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.17\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: pretix/_base_settings.py:87
@@ -8226,10 +8226,8 @@ msgstr ""
"2x Workshop 2"
#: pretix/base/pdf.py:383
#, fuzzy
#| msgid "List of Add-Ons"
msgid "List of Checked-In Add-Ons"
msgstr "Liste der Zusatz-Produkte"
msgstr "Liste der eingecheckten Zusatzprodukte"
#: pretix/base/pdf.py:390 pretix/control/forms/filter.py:1537
#: pretix/control/forms/filter.py:1539
@@ -9236,10 +9234,8 @@ msgid "Czech National Bank"
msgstr "Tschechische Nationalbank"
#: pretix/base/services/currencies.py:41
#, fuzzy
#| msgid "Czech National Bank"
msgid "National Bank of Poland"
msgstr "Tschechische Nationalbank"
msgstr "Nationalbank von Polen"
#: pretix/base/services/export.py:95 pretix/base/services/export.py:155
msgid ""
@@ -10330,16 +10326,12 @@ msgstr ""
"Rechnungsbetrag nicht in CZK ist."
#: pretix/base/settings.py:577 pretix/base/settings.py:586
#, fuzzy
#| msgid ""
#| "Based on Czech National Bank daily rates, whenever the invoice amount is "
#| "not in CZK."
msgid ""
"Based on National Bank of Poland daily rates, whenever the invoice amount is "
"not in PLN."
msgstr ""
"Basierend auf den Kursen der Tschechischen Nationalbank, immer wenn der "
"Rechnungsbetrag nicht in CZK ist."
"Basierend auf den Kursen der Nationalbank von Polen, immer wenn der "
"Rechnungsbetrag nicht in PLN ist."
#: pretix/base/settings.py:597
msgid "Require invoice address"
@@ -16440,10 +16432,8 @@ msgid "Allow to overbook quotas when performing this operation"
msgstr "Überbuchen von Kontingenten bei dieser Aktion erlauben"
#: pretix/control/forms/orders.py:335
#, fuzzy
#| msgid "Number of orders"
msgid "Number of products to add"
msgstr "Anzahl Bestellungen"
msgstr "Anzahl hinzuzufügender Produkt"
#: pretix/control/forms/orders.py:344
msgid "Add-on to"
@@ -16475,10 +16465,10 @@ msgstr ""
"Produktes."
#: pretix/control/forms/orders.py:441
#, fuzzy
#| msgid "You can not select the same seat multiple times."
msgid "You can not choose a seat when adding multiple products at once."
msgstr "Sie können den gleichen Sitzplatz nicht mehrfach auswählen."
msgstr ""
"Sie können keinen Sitzplatz auswählen, wenn mehrere Produkte auf einmal "
"hinzugefügt werden."
#: pretix/control/forms/orders.py:478 pretix/control/forms/orders.py:482
#: pretix/control/forms/orders.py:510 pretix/control/forms/orders.py:552
@@ -17097,7 +17087,7 @@ msgstr "Wochenendtag"
#: pretix/control/forms/subevents.py:106
msgctxt "subevent"
msgid "Skip dates that overlap with any existing date"
msgstr ""
msgstr "Überspringe Termine, die mit einem bestehenden Termin überlappen"
#: pretix/control/forms/subevents.py:109
msgctxt "subevent"
@@ -17107,6 +17097,10 @@ msgid ""
"This respects even inactive dates and works best if all dates have both a "
"start and end time."
msgstr ""
"Dies kann nützlich sein, wenn alle Termine am selben Ort stattfinden und "
"wiederholte Termine nicht in Konflikt mit existierenden Sonderterminen "
"geraten sollen. Dies berücksichtigt auch inaktive Termine und funktioniert "
"am besten, wenn alle Termine eine Start- und Endzeit haben."
#: pretix/control/forms/subevents.py:128
msgid "Keep the current values"
@@ -30276,6 +30270,8 @@ msgstr "Bitte erstelle maximal 100.000 Termine auf einmal."
#: pretix/control/views/subevents.py:966
msgid "All dates would be skipped because they conflict with existing dates."
msgstr ""
"Alle Termine würden übersprungen werden, weil sie mit existierenden Terminen "
"überlappen."
#: pretix/control/views/subevents.py:1102
#, python-brace-format

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:03+0000\n"
"PO-Revision-Date: 2026-03-30 11:50+0000\n"
"PO-Revision-Date: 2026-04-28 09:22+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix/de_Informal/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.17\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -8219,10 +8219,8 @@ msgstr ""
"2x Workshop 2"
#: pretix/base/pdf.py:383
#, fuzzy
#| msgid "List of Add-Ons"
msgid "List of Checked-In Add-Ons"
msgstr "Liste der Zusatz-Produkte"
msgstr "Liste der eingecheckten Zusatzprodukte"
#: pretix/base/pdf.py:390 pretix/control/forms/filter.py:1537
#: pretix/control/forms/filter.py:1539
@@ -9226,10 +9224,8 @@ msgid "Czech National Bank"
msgstr "Tschechische Nationalbank"
#: pretix/base/services/currencies.py:41
#, fuzzy
#| msgid "Czech National Bank"
msgid "National Bank of Poland"
msgstr "Tschechische Nationalbank"
msgstr "Nationalbank von Polen"
#: pretix/base/services/export.py:95 pretix/base/services/export.py:155
msgid ""
@@ -10318,16 +10314,12 @@ msgstr ""
"Rechnungsbetrag nicht in CZK ist."
#: pretix/base/settings.py:577 pretix/base/settings.py:586
#, fuzzy
#| msgid ""
#| "Based on Czech National Bank daily rates, whenever the invoice amount is "
#| "not in CZK."
msgid ""
"Based on National Bank of Poland daily rates, whenever the invoice amount is "
"not in PLN."
msgstr ""
"Basierend auf den Kursen der Tschechischen Nationalbank, immer wenn der "
"Rechnungsbetrag nicht in CZK ist."
"Basierend auf den Kursen der Nationalbank von Polen, immer wenn der "
"Rechnungsbetrag nicht in PLN ist."
#: pretix/base/settings.py:597
msgid "Require invoice address"
@@ -16417,10 +16409,8 @@ msgid "Allow to overbook quotas when performing this operation"
msgstr "Überbuchen von Kontingenten bei dieser Aktion erlauben"
#: pretix/control/forms/orders.py:335
#, fuzzy
#| msgid "Number of orders"
msgid "Number of products to add"
msgstr "Anzahl Bestellungen"
msgstr "Anzahl hinzuzufügender Produkt"
#: pretix/control/forms/orders.py:344
msgid "Add-on to"
@@ -16452,10 +16442,10 @@ msgstr ""
"Produktes."
#: pretix/control/forms/orders.py:441
#, fuzzy
#| msgid "You can not select the same seat multiple times."
msgid "You can not choose a seat when adding multiple products at once."
msgstr "Du kannst den gleichen Sitzplatz nicht mehrfach auswählen."
msgstr ""
"Du kannst keinen Sitzplatz auswählen, wenn mehrere Produkte auf einmal "
"hinzugefügt werden."
#: pretix/control/forms/orders.py:478 pretix/control/forms/orders.py:482
#: pretix/control/forms/orders.py:510 pretix/control/forms/orders.py:552
@@ -17075,7 +17065,7 @@ msgstr "Wochenendtag"
#: pretix/control/forms/subevents.py:106
msgctxt "subevent"
msgid "Skip dates that overlap with any existing date"
msgstr ""
msgstr "Überspringe Termine, die mit einem bestehenden Termin überlappen"
#: pretix/control/forms/subevents.py:109
msgctxt "subevent"
@@ -17085,6 +17075,10 @@ msgid ""
"This respects even inactive dates and works best if all dates have both a "
"start and end time."
msgstr ""
"Dies kann nützlich sein, wenn alle Termine am selben Ort stattfinden und "
"wiederholte Termine nicht in Konflikt mit existierenden Sonderterminen "
"geraten sollen. Dies berücksichtigt auch inaktive Termine und funktioniert "
"am besten, wenn alle Termine eine Start- und Endzeit haben."
#: pretix/control/forms/subevents.py:128
msgid "Keep the current values"
@@ -30232,6 +30226,8 @@ msgstr "Bitte erstelle maximal 100.000 Termine auf einmal."
#: pretix/control/views/subevents.py:966
msgid "All dates would be skipped because they conflict with existing dates."
msgstr ""
"Alle Termine würden übersprungen werden, weil sie mit existierenden Terminen "
"überlappen."
#: pretix/control/views/subevents.py:1102
#, python-brace-format

View File

@@ -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({

View File

@@ -95,17 +95,17 @@
<div class="col-md-2 col-sm-3 col-xs-6 availability-box">
{% if not event.settings.show_variations_expanded %}
<button type="button" data-toggle="variations" class="btn btn-default btn-block js-only"
data-label-alt="{% trans "Hide variants" %}"
aria-expanded="false" aria-controls="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations"
data-label-alt="{% if item.expand %}{% trans "Show variants" %}{% else %}{% trans "Hide variants" %}{% endif %}"
aria-expanded="{{ item.expand|yesno:"true,false" }}" aria-controls="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations"
aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend">
<i class="fa fa-angle-down collapse-indicator" aria-hidden="true"></i>
<span>{% trans "Show variants" %}</span>
<span>{% if item.expand %}{% trans "Hide variants" %}{% else %}{% trans "Show variants" %}{% endif %}</span>
</button>
{% endif %}
</div>
<div class="clearfix"></div>
</div>
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}" id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations">
<div class="variations {% if not event.settings.show_variations_expanded and not item.expand %}variations-collapsed{% endif %}" id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations">
{% for var in item.available_variations %}
<article aria-labelledby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row-fluid product-row variation"
{% if not item.free_price %}

View File

@@ -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