Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
433ba66519 Cart: Fix wrong rounding being displayed 2026-01-16 15:13:03 +01:00
39 changed files with 334 additions and 845 deletions

View File

@@ -1601,7 +1601,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
order.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
cp.addon_to, cp.is_bundled, pos._voucher_discount)
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
for cp in order_positions
]
)

View File

@@ -443,7 +443,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'customer_accounts',
'customer_accounts_native',
'customer_accounts_link_by_email',
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'imprint_url',

View File

@@ -34,13 +34,14 @@
from contextlib import contextmanager
from asgiref.local import Local
from babel import localedata
from django.conf import settings
from django.utils import translation
from django.utils.formats import date_format, number_format
from django.utils.translation import gettext
from pretix.base.templatetags.money import money_filter
from i18nfield.fields import ( # noqa
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
)
@@ -50,9 +51,6 @@ from i18nfield.strings import LazyI18nString # noqa
from i18nfield.utils import I18nJSONEncoder # noqa
_active_region = Local()
class LazyDate:
def __init__(self, value):
self.value = value
@@ -88,8 +86,6 @@ class LazyCurrencyNumber:
return self.__str__()
def __str__(self):
from pretix.base.templatetags.money import money_filter
return money_filter(self.value, self.currency)
@@ -109,41 +105,14 @@ ALLOWED_LANGUAGES = dict(settings.LANGUAGES)
def get_babel_locale():
# Babel, and therefore also django-phonenumberfield, do not support our custom locales such das de_Informal
# Also, this returns best-effort region information for number formatting etc
current_language = translation.get_language()
current_region = getattr(_active_region, "value", None)
# Babel only accepts locales that exist on the system. We try combinations in the following order:
# language-languageversion-region
# language-region
# language-languageversion
# language
# fallback to system default
# fallback to english
try_locales = []
if current_language:
if "-" in current_language:
lng_parts = current_language.split("-")
if current_region:
try_locales.append(f"{lng_parts[0]}_{lng_parts[1].title()}_{current_region.upper()}")
try_locales.append(f"{lng_parts[0]}_{current_region.upper()}")
try_locales.append(f"{lng_parts[0]}_{lng_parts[1].upper()}")
try_locales.append(f"{lng_parts[0]}_{lng_parts[1].title()}")
try_locales.append(f"{lng_parts[0]}")
else:
if current_region:
try_locales.append(f"{current_language}_{current_region.upper()}")
try_locales.append(f"{current_language}")
try_locales.append(settings.LANGUAGE_CODE)
for locale in try_locales:
if localedata.exists(locale):
return locale
return "en"
babel_locale = 'en'
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
if translation.get_language():
if localedata.exists(translation.get_language()):
babel_locale = translation.get_language()
elif localedata.exists(translation.get_language()[:2]):
babel_locale = translation.get_language()[:2]
return babel_locale
def get_language_without_region(lng=None):
@@ -163,10 +132,6 @@ def get_language_without_region(lng=None):
return lng
def set_region(region):
_active_region.value = region
@contextmanager
def language(lng, region=None):
"""
@@ -178,18 +143,15 @@ def language(lng, region=None):
formatting. If you pass a ``lng`` that already contains a region, e.g. ``pt-br``, the ``region``
attribute will be ignored.
"""
lng_before = translation.get_language()
region_before = getattr(_active_region, "value", None)
_lng = translation.get_language()
lng = lng or settings.LANGUAGE_CODE
if '-' not in lng and region:
lng += '-' + region.lower()
translation.activate(lng)
_active_region.value = region
try:
yield
finally:
translation.activate(lng_before)
_active_region.value = region_before
translation.activate(_lng)
class LazyLocaleException(Exception):

View File

@@ -64,7 +64,7 @@ class PeppolIdValidator:
"0020": "[0-9]{9}",
"0201": "[0-9a-zA-Z]{6}",
"0204": "[0-9]{2,12}(-[0-9A-Z]{0,30})?-[0-9]{2}",
"0208": "[01][0-9]{9}",
"0208": "0[0-9]{9}",
"0209": ".*",
"0210": "[A-Z0-9]+",
"0211": "IT[0-9]{11}",

View File

@@ -35,7 +35,7 @@ from django.utils.translation.trans_real import (
parse_accept_lang_header,
)
from pretix.base.i18n import get_language_without_region, set_region
from pretix.base.i18n import get_language_without_region
from pretix.base.settings import global_settings_object
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
@@ -92,14 +92,10 @@ class LocaleMiddleware(MiddlewareMixin):
)
if '-' not in language and settings_holder.settings.region:
language += '-' + settings_holder.settings.region
if settings_holder.settings.region:
set_region(settings_holder.settings.region)
else:
gs = global_settings_object(request)
if '-' not in language and gs.settings.region:
language += '-' + gs.settings.region
if gs.settings.region:
set_region(gs.settings.region)
translation.activate(language)
request.LANGUAGE_CODE = get_language_without_region()

View File

@@ -37,7 +37,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
PositionInfo = namedtuple('PositionInfo',
['item_id', 'subevent_id', 'subevent_date_from', 'line_price_gross', 'addon_to',
['item_id', 'subevent_id', 'subevent_date_from', 'line_price_gross', 'is_addon_to',
'voucher_discount'])
@@ -279,42 +279,6 @@ class Discount(LoggedModel):
for idx in condition_idx_group:
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
def _addon_idx(self, positions, idx):
"""
If we have the following cart:
- Main product
- 10x Addon product 5€
- Main product
- 10x Addon product 5€
And we have a discount rule that grants "every 10th product is free", people tend to expect
- Main product
- 9x Addon product 5€
- 1x Addon product free
- Main product
- 9x Addon product 5€
- 1x Addon product free
And get confused if they get
- Main product
- 8x Addon product 5€
- 2x Addon product free
- Main product
- 10x Addon product 5€
Even if the result is the same. Therefore, we sort positions in the cart not only by price, but also by their
relative index within their addon group. This is only a heuristic and there are *still* scenarios where the more
unexpected version happens, e.g. if prices are different. We need to accept this as long as discounts work on
cart level and not on addon-group level, but this simple sorting reduces the number of support issues by making
the weird case less likely.
"""
if not positions[idx].addon_to:
return 0
return len([1 for i, p in positions.items() if i < idx and p.addon_to == positions[idx].addon_to])
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
if len(condition_idx_group) < self.condition_min_count:
return
@@ -324,8 +288,8 @@ class Discount(LoggedModel):
if self.benefit_only_apply_to_cheapest_n_matches:
# sort by line_price
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx), -idx))
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx), -idx))
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
# want to match multiples of 3
@@ -470,7 +434,7 @@ class Discount(LoggedModel):
for idx, p in positions.items():
subevent_to_idx[p.subevent_id].append(idx)
for v in subevent_to_idx.values():
v.sort(key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx)))
v.sort(key=lambda idx: positions[idx].line_price_gross)
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
# Build groups of exactly condition_min_count distinct subevents
@@ -494,7 +458,7 @@ class Discount(LoggedModel):
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
# and 2 from the end" scheme to optimize price distribution among groups
candidates = sorted(candidates, key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx)))
candidates = sorted(candidates, key=lambda idx: positions[idx].line_price_gross)
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
candidate = candidates[0]
else:

View File

@@ -141,9 +141,8 @@ class LogEntry(models.Model):
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
if log_entry_type:
sender = self.event if self.event else self.organizer
link_info = log_entry_type.get_object_link_info(self)
if is_app_active(sender, meta['plugin']):
if is_app_active(self.event, meta['plugin']):
return make_link(link_info, log_entry_type.object_link_wrapper)
else:
return make_link(link_info, log_entry_type.object_link_wrapper, is_active=False,

View File

@@ -35,7 +35,6 @@ from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import User, Voucher
from pretix.base.services.mail import SendMailException, mail, render_mail
from pretix.helpers import OF_SELF
from ...helpers.format import format_map
from ...helpers.names import build_name
@@ -186,47 +185,44 @@ class WaitingListEntry(LoggedModel):
if not free_seats:
raise WaitingListException(_('No seat with this product is currently available.'))
if self.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.'))
if '@' not in self.email:
raise WaitingListException(_('This entry is anonymized and can no longer be used.'))
with transaction.atomic():
locked_wle = WaitingListEntry.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
if locked_wle.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.'))
e = locked_wle.email
if locked_wle.name:
e += ' / ' + locked_wle.name
e = self.email
if self.name:
e += ' / ' + self.name
v = Voucher.objects.create(
event=locked_wle.event,
event=self.event,
max_usages=1,
valid_until=now() + timedelta(hours=locked_wle.event.settings.waiting_list_hours),
item=locked_wle.item,
variation=locked_wle.variation,
valid_until=now() + timedelta(hours=self.event.settings.waiting_list_hours),
item=self.item,
variation=self.variation,
tag='waiting-list',
comment=_('Automatically created from waiting list entry for {email}').format(
email=e
),
block_quota=True,
subevent=locked_wle.subevent,
subevent=self.subevent,
)
v.log_action('pretix.voucher.added', {
'item': locked_wle.item.pk,
'variation': locked_wle.variation.pk if locked_wle.variation else None,
'item': self.item.pk,
'variation': self.variation.pk if self.variation else None,
'tag': 'waiting-list',
'block_quota': True,
'valid_until': v.valid_until.isoformat(),
'max_usages': 1,
'subevent': locked_wle.subevent.pk if locked_wle.subevent else None,
'subevent': self.subevent.pk if self.subevent else None,
'source': 'waitinglist',
}, user=user, auth=auth)
v.log_action('pretix.voucher.added.waitinglist', {
'email': locked_wle.email,
'waitinglistentry': locked_wle.pk,
'email': self.email,
'waitinglistentry': self.pk,
}, user=user, auth=auth)
locked_wle.voucher = v
locked_wle.save()
self.refresh_from_db()
self.voucher = v
self.save()
with language(self.locale, self.event.settings.region):
self.send_mail(

View File

@@ -1528,7 +1528,7 @@ class CartManager:
self._sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
cp.addon_to, cp.is_bundled, cp.listed_price - cp.price_after_voucher)
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in positions
]
)

View File

@@ -121,7 +121,7 @@ class CrossSellingService:
self.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
cp.addon_to, cp.is_bundled,
bool(cp.addon_to), cp.is_bundled,
cp.listed_price - cp.price_after_voucher)
for cp in self.cartpositions
],

View File

@@ -914,7 +914,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
cp.addon_to, cp.is_bundled, cp.listed_price - cp.price_after_voucher)
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in sorted_positions
]
)

View File

@@ -174,9 +174,7 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
:param event: Event the cart belongs to
:param sales_channel: Sales channel the cart was created with
:param positions: Tuple of the form ``(item_id, subevent_id, subevent_date_from, line_price_gross, addon_to_id, is_bundled, voucher_discount)``
``addon_to_id`` does not have to be the proper ID, any identifier is okay, even ``True``/``False`` are accepted, but
a better result may be given if addons to the same main product have the same distinct value.
:param positions: Tuple of the form ``(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
@@ -198,9 +196,9 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs:
result = discount.apply({
idx: PositionInfo(item_id, subevent_id, subevent_date_from, line_price_gross, addon_to, voucher_discount)
idx: PositionInfo(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, voucher_discount)
for
idx, (item_id, subevent_id, subevent_date_from, line_price_gross, addon_to, is_bundled, voucher_discount)
idx, (item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)
in enumerate(positions)
if not is_bundled and idx not in new_prices
}, collect_potential_discounts)

View File

@@ -180,19 +180,6 @@ DEFAULTS = {
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-customer_accounts'}),
)
},
'customer_accounts_require_login_for_order_access': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require login to access order confirmation pages"),
help_text=_("If enabled, users who were logged in at the time of purchase must also log in to access their order information. "
"If a customer account is created while placing an order, the restriction only becomes active after the customer "
"account is activated."),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-customer_accounts'}),
)
},
'customer_accounts_link_by_email': {
'default': 'False',
'type': bool,

View File

@@ -26,8 +26,7 @@ from babel.numbers import format_currency
from django import template
from django.conf import settings
from django.template.defaultfilters import floatformat
from pretix.base.i18n import get_babel_locale
from django.utils import translation
register = template.Library()
@@ -60,10 +59,13 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
if hide_currency:
return floatformat(value, f"{places}g")
try:
locale = Locale(get_babel_locale())
except UnknownLocaleError:
locale = "en"
locale_parts = translation.get_language().split("-", 1)
locale = locale_parts[0]
if len(locale_parts) > 1 and len(locale_parts[1]) == 2:
try:
locale = Locale(locale_parts[0], locale_parts[1].upper())
except UnknownLocaleError:
pass
try:
return format_currency(value, arg, locale=locale)

View File

@@ -474,7 +474,6 @@ class OrganizerSettingsForm(SettingsForm):
'customer_accounts',
'customer_accounts_native',
'customer_accounts_link_by_email',
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'imprint_url',

View File

@@ -132,7 +132,6 @@
<legend>{% trans "Customer accounts" %}</legend>
{% bootstrap_field sform.customer_accounts layout="control" %}
{% bootstrap_field sform.customer_accounts_native layout="control" %}
{% bootstrap_field sform.customer_accounts_require_login_for_order_access layout="control" %}
{% bootstrap_field sform.customer_accounts_link_by_email layout="control" %}
{% bootstrap_field sform.name_scheme layout="control" %}
{% bootstrap_field sform.name_scheme_titles layout="control" %}

View File

@@ -188,8 +188,6 @@ class LicenseCheckView(StaffMemberRequiredMixin, FormView):
return None, None
try:
for k, v in pkg.metadata.items():
if k == "License-Expression":
license = v
if k == "License":
license = v
if k == "Home-page":

View File

@@ -2583,7 +2583,7 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
def get_queryset(self):
# technically, we'd also need to sort by pk since this is a paginated list, but in this case we just can't
# bear the performance cost
qs = self.request.organizer.logentry_set.filter(event=None).select_related(
qs = self.request.organizer.all_logentries().select_related(
'user', 'content_type', 'api_token', 'oauth_application', 'device'
).order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 12:12+0000\n"
"PO-Revision-Date: 2026-01-14 00:00+0000\n"
"Last-Translator: Mario Montes <mario@t3chfest.es>\n"
"PO-Revision-Date: 2026-01-06 23:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
"Language: es\n"
@@ -35588,8 +35588,7 @@ msgstr "El período de preventa para este evento ha terminado."
#: pretix/presale/views/widget.py:807
#, python-format
msgid "The booking period for this event will start on %(date)s at %(time)s."
msgstr ""
"El periodo de reserva para este evento comenzará el %(date)s a las %(time)s."
msgstr "La preventa para este evento comenzará en %(date)s a %(time)s."
#: pretix/presale/templates/pretixpresale/event/index.html:185
#: pretix/presale/templates/pretixpresale/event/seatingplan.html:23
@@ -36852,11 +36851,11 @@ msgstr ""
#: pretix/presale/views/cart.py:520
msgid "Your cart has been updated."
msgstr "Su carrito ha sido actualizado."
msgstr "Su carrito ha sido actualizada."
#: pretix/presale/views/cart.py:523 pretix/presale/views/cart.py:549
msgid "Your cart is now empty."
msgstr "Su carrito está vacio."
msgstr "Su carrito ha sido vaciada."
#: pretix/presale/views/cart.py:570
msgid ""

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 12:13+0000\n"
"PO-Revision-Date: 2026-01-14 00:00+0000\n"
"PO-Revision-Date: 2025-10-22 16:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\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.15.1\n"
"X-Generator: Weblate 5.13.3\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -738,8 +738,11 @@ msgstr "Su carrito está a punto de caducar."
#: pretix/static/pretixpresale/js/ui/cart.js:62
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Los artículos de la cesta están reservados durante un minuto."
msgstr[1] "Los artículos de la cesta están reservados durante {num} minutos."
msgstr[0] ""
"Los elementos en su carrito de compras se han reservado durante un minuto."
msgstr[1] ""
"Los elementos en su carrito de compras se han reservado durante {num} "
"minutos."
#: pretix/static/pretixpresale/js/ui/cart.js:83
msgid "Your cart has expired."
@@ -763,11 +766,11 @@ msgstr "Renovar reserva"
#: pretix/static/pretixpresale/js/ui/main.js:194
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr "El organizador retiene %(currency)s %(amount)s"
msgstr "El organizador se queda %(currency)s %(price)s"
#: pretix/static/pretixpresale/js/ui/main.js:202
msgid "You get %(currency)s %(amount)s back"
msgstr "Se le devolverá %(moneda)s %(cantidad)s"
msgstr "Obtienes %(currency)s %(price)s de vuelta"
#: pretix/static/pretixpresale/js/ui/main.js:218
msgid "Please enter the amount the organizer can keep."

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 12:12+0000\n"
"PO-Revision-Date: 2026-01-14 00:00+0000\n"
"Last-Translator: Mario Montes <mario@t3chfest.es>\n"
"PO-Revision-Date: 2025-12-15 20:00+0000\n"
"Last-Translator: sandra r <sandrarial@gestiontickets.online>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix/"
"gl/>\n"
"Language: gl\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.15.1\n"
"X-Generator: Weblate 5.14.3\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -1149,7 +1149,7 @@ msgstr "Pregunta: {name}"
#: pretix/base/settings.py:3775 pretix/base/settings.py:3828
#: pretix/base/settings.py:3849 pretix/base/settings.py:3871
msgid "Given name"
msgstr "Nome"
msgstr "Nombre"
#: pretix/base/datasync/sourcefields.py:628
#: pretix/base/datasync/sourcefields.py:638 pretix/base/settings.py:3670
@@ -18192,9 +18192,9 @@ msgstr ""
"\"{new_email}\"."
#: pretix/control/logdisplay.py:673
#, python-brace-format
#, fuzzy, python-brace-format
msgid "Your email address {email} has been confirmed."
msgstr "Tu email {email} ha sido confirmado."
msgstr "Su carrito ha sido actualizado."
#: pretix/control/logdisplay.py:685
#, fuzzy, python-brace-format
@@ -24293,7 +24293,7 @@ msgstr "Sí, aprobar la orden"
#: pretix/presale/templates/pretixpresale/event/order.html:483
#: pretix/presale/templates/pretixpresale/event/order_cancel.html:7
msgid "Cancel order"
msgstr "Cancelar o pedido"
msgstr "Cancelar a orde"
#: pretix/control/templates/pretixcontrol/order/cancel.html:12
#: pretix/control/templates/pretixcontrol/order/deny.html:11
@@ -35952,9 +35952,10 @@ msgstr ""
#: pretix/presale/forms/renderers.py:66
#: pretix/presale/templates/pretixpresale/event/fragment_voucher_form.html:14
#, fuzzy
msgctxt "form"
msgid "required"
msgstr "obrigatorio"
msgstr "expirado"
#: pretix/presale/ical.py:87 pretix/presale/ical.py:146
#: pretix/presale/ical.py:182
@@ -37538,13 +37539,14 @@ msgid "A payment of %(total)s is still pending for this order."
msgstr "Un pago de %(total)s aínda está pendente para esta orde."
#: pretix/presale/templates/pretixpresale/event/order.html:97
#, python-format
#, fuzzy, python-format
msgid "Please complete your payment before %(date)s"
msgstr "Por favor complete o seu pago antes de %(date)s"
msgstr "Por favor complete su pago antes de %(date)s"
#: pretix/presale/templates/pretixpresale/event/order.html:108
#, fuzzy
msgid "Re-try payment or choose another payment method"
msgstr "Volva tentar o pago ou escolla outro método de pago"
msgstr "Vuelva a intentar el pago o elija otro método de pago"
#: pretix/presale/templates/pretixpresale/event/order.html:126
#, fuzzy

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 12:12+0000\n"
"PO-Revision-Date: 2026-01-12 17:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"PO-Revision-Date: 2026-01-05 10:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
"Language: ja\n"
@@ -3831,7 +3831,7 @@ msgstr ""
#: pretix/base/invoicing/peppol.py:164
msgid "The Peppol participant ID is not registered on the Peppol network."
msgstr "Peppol参加者IDはPeppolネットワークに登録されていません。"
msgstr ""
#: pretix/base/invoicing/peppol.py:184
msgid "Peppol participant ID"
@@ -8281,7 +8281,7 @@ msgstr "販売されていない製品が選択されています。"
msgid ""
"Some products can no longer be purchased and have been removed from your "
"cart for the following reason: %s"
msgstr "一部の製品は購入できなくなり、次の理由でカートから削除されました:%s"
msgstr ""
#: pretix/base/services/cart.py:117
msgid ""
@@ -9400,14 +9400,20 @@ msgid "Uncategorized"
msgstr "未分類"
#: pretix/base/services/tax.py:43
#, fuzzy
#| msgid ""
#| "Your VAT ID could not be checked, as the VAT checking service of your "
#| "country is currently not available. We will therefore need to charge VAT "
#| "on your invoice. You can get the tax amount back via the VAT "
#| "reimbursement process."
msgid ""
"Your VAT ID could not be checked, as the VAT checking service of your "
"country is currently not available. We will therefore need to charge you the "
"same tax rate as if you did not enter a VAT ID."
msgstr ""
"あなたの国のVATチェックサービスが現在利用できないため、VAT IDを確認できません"
"でした。したがって、VAT IDを入力しなかった場合と同じ税率を請求する必要があり"
"ます。"
"お客様のVAT IDは確認できませんでした。お客様の国のVAT確認サービスが現在利用で"
"きないため、請求書にVATを請求する必要があります。VAT払い戻し手続きを通じて税"
"金を返金することができます。"
#: pretix/base/services/tax.py:47 pretix/base/services/tax.py:366
#: pretix/base/services/tax.py:393
@@ -9822,17 +9828,23 @@ msgid "Ask for VAT ID"
msgstr "VAT IDを尋ねる"
#: pretix/base/settings.py:632
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Only works if an invoice address is asked for. VAT ID is never required "
#| "and only requested from business customers in the following countries: "
#| "{countries}"
msgid ""
"Only works if an invoice address is asked for. VAT ID is only requested from "
"business customers in the following countries: {countries}."
msgstr ""
"請求の住所が要求された場合のみ有効です。VAT ID は、{countries} の国の法人の"
"お客様にのみ要求されます。"
"請求の住所が要求された場合のみ機能します。消費税登録番号やVAT IDは必要な"
"く、以下の国の法人顧客からのみ要求されます:{countries}"
#: pretix/base/settings.py:651
#, fuzzy
#| msgid "Require name"
msgid "Require VAT ID in"
msgstr "VAT IDを要求"
msgstr "名前を要求"
#: pretix/base/settings.py:657
msgid ""
@@ -9840,9 +9852,6 @@ msgid ""
"ID in all countries. VAT ID will be required for all business addresses in "
"the selected countries."
msgstr ""
"VAT ID はデフォルトでオプションです。これは、すべての企業に VAT ID が割り当て"
"られているわけではないためです。選択した国のすべてのビジネスアドレスには、"
"VAT IDが必要です。"
#: pretix/base/settings.py:672
msgid "Invoice address explanation"
@@ -13433,9 +13442,11 @@ msgstr ""
"リクエストのために再度有効にしてください。"
#: pretix/base/views/js_helpers.py:41
#, fuzzy
#| msgid "ID"
msgctxt "tax_id_swiss"
msgid "UID"
msgstr "UID"
msgstr "ID"
#. Translators: Only translate to French (IDE) and Italien (IDI), otherwise keep the same
#. Awareness around VAT IDs differes by EU country. For example, in Germany the VAT ID is assigned
@@ -13445,27 +13456,35 @@ msgstr "UID"
#. number (Partita IVA) and also used on domestic transactions. So someone who never purchased something international
#. for their company, might still know the value, if we call it the right way and not just "VAT ID".
#: pretix/base/views/js_helpers.py:49
#, fuzzy
#| msgid "VAT ID"
msgctxt "tax_id_italy"
msgid "VAT ID / P.IVA"
msgstr "VAT ID / P.IVA"
msgstr "消費税登録番号又はVAT ID"
#. Translators: Translate to only "P.IVA" in Italian, keep second part as-is in other languages
#: pretix/base/views/js_helpers.py:50
#, fuzzy
#| msgid "VAT ID"
msgctxt "tax_id_greece"
msgid "VAT ID / TIN"
msgstr "VAT ID / TIN"
msgstr "消費税登録番号又はVAT ID"
#. Translators: Translate to only "ΑΦΜ" in Greek
#: pretix/base/views/js_helpers.py:51
#, fuzzy
#| msgid "VAT ID"
msgctxt "tax_id_spain"
msgid "VAT ID / NIF"
msgstr "VAT ID / NIF"
msgstr "消費税登録番号又はVAT ID"
#. Translators: Translate to only "NIF" in Spanish
#: pretix/base/views/js_helpers.py:52
#, fuzzy
#| msgid "VAT ID"
msgctxt "tax_id_portugal"
msgid "VAT ID / NIF"
msgstr "VAT ID / NIF"
msgstr "消費税登録番号又はVAT ID"
#: pretix/base/views/tasks.py:185
msgid "An unexpected error has occurred, please try again later."
@@ -14002,8 +14021,6 @@ msgid ""
"Formatting is not supported, as some accounting departments process mail "
"automatically and do not handle formatted emails properly."
msgstr ""
"一部の経理部門はメールを自動的に処理し、フォーマットされたメールを適切に処理"
"しないため、フォーマットはサポートされていません。"
#: pretix/control/forms/event.py:1356
msgid ""
@@ -14452,7 +14469,7 @@ msgstr "支払い済み"
#: pretix/control/forms/filter.py:1304
msgctxt "subevent"
msgid "Date doesn't start in selected date range."
msgstr "日付は選択した日付範囲で開始されません。"
msgstr ""
#: pretix/control/forms/filter.py:1360 pretix/control/forms/filter.py:1827
msgid "Shop live and presale running"
@@ -27990,9 +28007,6 @@ msgid ""
"automatically. We recommend that you rename these in your source file to "
"avoid problems during import."
msgstr ""
"CSVファイルの複数の列は同じ名前で、自動的に名前が変更されました。インポート中"
"に問題が発生しないように、ソースファイルでこれらの名前を変更することをお勧め"
"します。"
#: pretix/control/views/modelimport.py:188
msgid "The import was successful."
@@ -29623,7 +29637,7 @@ msgstr ""
#: pretix/plugins/banktransfer/camtimport.py:33
msgid "Empty file or unknown format."
msgstr "空のファイルまたは不明な形式。"
msgstr ""
#: pretix/plugins/banktransfer/payment.py:69
msgid ""
@@ -32118,16 +32132,16 @@ msgid "Przelewy24"
msgstr "Przelewy24"
#: pretix/plugins/stripe/payment.py:417 pretix/plugins/stripe/payment.py:1836
#, fuzzy
#| msgid "Payment by bank transfer"
msgid "Pay by bank"
msgstr "Pay by bank"
msgstr "銀行振込による支払い"
#: pretix/plugins/stripe/payment.py:422
msgid ""
"Currently only available for charges in GBP and customers with UK bank "
"accounts, and in private preview for France and Germany."
msgstr ""
"現在、英国ポンドでの請求と英国の銀行口座を持つ顧客、およびフランスとドイツの"
"プライベートプレビューでのみ利用できます。"
#: pretix/plugins/stripe/payment.py:429 pretix/plugins/stripe/payment.py:1789
msgid "WeChat Pay"
@@ -32432,16 +32446,16 @@ msgstr ""
"いておいてください。"
#: pretix/plugins/stripe/payment.py:1835
#, fuzzy
#| msgid "PayPal via Stripe"
msgid "Pay by bank via Stripe"
msgstr "Stripe経由でPay by bank"
msgstr "Stripe経由でPayPal"
#: pretix/plugins/stripe/payment.py:1841
msgid ""
"Pay by bank allows you to authorize a secure Open Banking payment from your "
"banking app. Currently available only with a UK bank account."
msgstr ""
"Pay by bankを使用すると、銀行アプリから安全なオープンバンキングの支払いを承認"
"できます。現在、英国の銀行口座でのみ利用できます。"
#: pretix/plugins/stripe/payment.py:1861
msgid "PayPal via Stripe"
@@ -32460,16 +32474,22 @@ msgstr ""
"能です。アプリを準備してください。"
#: pretix/plugins/stripe/payment.py:1893
#, fuzzy
#| msgid "giropay via Stripe"
msgid "PromptPay via Stripe"
msgstr "Stripe経由でPromptPay"
msgstr "Stripe経由でgiropay"
#: pretix/plugins/stripe/payment.py:1898
#, fuzzy
#| msgid ""
#| "This payment method is available to MobilePay app users in Denmark and "
#| "Finland. Please have your app ready."
msgid ""
"This payment method is available to PromptPay users in Thailand. Please have "
"your app ready."
msgstr ""
"この支払い方法は、タイのPromptPayユーザー利用できます。アプリを準備してくだ"
"さい。"
"この支払い方法は、デンマークとフィンランドのMobilePayアプリユーザー利用可能"
"です。アプリを準備してください。"
#: pretix/plugins/stripe/payment.py:1917
msgid "TWINT via Stripe"
@@ -32772,16 +32792,22 @@ msgid "Confirm payment: %(code)s"
msgstr "支払いを確認: %(code)s"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html:32
#, fuzzy
#| msgid ""
#| "Please scan the barcode below to complete your WeChat payment. Once you "
#| "have completed your payment, you can refresh this page."
msgid ""
"Please scan the QR code below to complete your PromptPay payment. Once you "
"have completed your payment, you can refresh this page."
msgstr ""
"PromptPayの支払いを完了するには、以下のQRコードをスキャンしてください。支払い"
"が完了したら、このページを更新できます。"
"下記のバーコードをスキャンしてWeChat支払いを完了してください。支払いが完了し"
"たら、このページを更新してください。"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html:37
#, fuzzy
#| msgid "Create QR code"
msgid "PromptPay QR code"
msgstr "PromptPay QRコード"
msgstr "注文番号"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/sca_return.html:20
msgid "Confirming your payment…"
@@ -33799,7 +33825,10 @@ msgstr[0] ""
"このカテゴリから%(min_count)s個のオプションを選択する必要があります。"
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:26
#, python-format
#, fuzzy, python-format
#| msgid "You can choose %(max_count)s option from this category."
#| msgid_plural ""
#| "You can choose up to %(max_count)s options from this category."
msgid "You can choose one option from this category."
msgid_plural "You can choose up to %(max_count)s options from this category."
msgstr[0] "このカテゴリから最大 %(max_count)s のオプションを選択できます。"
@@ -35845,8 +35874,6 @@ msgid ""
"Your cart timeout was extended. Please note that some of the prices in your "
"cart changed."
msgstr ""
"カートのタイムアウトが延長されました。カートの価格の一部が変更されましたので"
"ご注意ください。"
#: pretix/presale/views/cart.py:573
msgid "Your cart timeout was extended."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 12:12+0000\n"
"PO-Revision-Date: 2026-01-12 17:00+0000\n"
"PO-Revision-Date: 2025-12-22 20:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_Informal/>\n"
@@ -4008,16 +4008,18 @@ msgstr ""
#: pretix/base/invoicing/peppol.py:164
msgid "The Peppol participant ID is not registered on the Peppol network."
msgstr "De Peppol-deelnemers-ID is niet geregistreerd op het Peppol-netwerk."
msgstr ""
#: pretix/base/invoicing/peppol.py:184
msgid "Peppol participant ID"
msgstr "Peppol-deelnemers-ID"
#: pretix/base/invoicing/peppol.py:200
#, fuzzy
#| msgid "Gift card code"
msgctxt "peppol_invoice"
msgid "Visual copy"
msgstr "Visuele kopie"
msgstr "Cadeauboncode"
#: pretix/base/invoicing/peppol.py:205
msgctxt "peppol_invoice"
@@ -8767,8 +8769,6 @@ msgid ""
"Some products can no longer be purchased and have been removed from your "
"cart for the following reason: %s"
msgstr ""
"Sommige producten kunnen niet meer gekocht worden en zijn om de volgende "
"reden uit je winkelwagen verwijderd: %s"
#: pretix/base/services/cart.py:117
msgid ""
@@ -10037,15 +10037,21 @@ msgid "Uncategorized"
msgstr "Ongecategoriseerd"
#: pretix/base/services/tax.py:43
#, fuzzy
#| msgid ""
#| "Your VAT ID could not be checked, as the VAT checking service of your "
#| "country is currently not available. We will therefore need to charge VAT "
#| "on your invoice. You can get the tax amount back via the VAT "
#| "reimbursement process."
msgid ""
"Your VAT ID could not be checked, as the VAT checking service of your "
"country is currently not available. We will therefore need to charge you the "
"same tax rate as if you did not enter a VAT ID."
msgstr ""
"Je btw-nummer kon niet worden gecontroleerd, omdat de btw-controleservice "
"van je land momenteel niet beschikbaar is. We zullen daarom hetzelfde "
"belastingtarief in rekening moeten brengen als wanneer je geen btw-nummer "
"had opgegeven."
"Je btw-nummer kan niet worden gecontroleerd omdat de btw-controledienst van "
"jouw land momenteel niet beschikbaar is. We zijn daarom genoodzaakt om de "
"btw te factureren. Je kan het belastingbedrag terugkrijgen via het btw-"
"terugbetalingsproces."
#: pretix/base/services/tax.py:47 pretix/base/services/tax.py:366
#: pretix/base/services/tax.py:393
@@ -10500,17 +10506,21 @@ msgid "Ask for VAT ID"
msgstr "Vraag om btw-nummer"
#: pretix/base/settings.py:632
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Does only work if an invoice address is asked for. VAT ID is not required."
msgid ""
"Only works if an invoice address is asked for. VAT ID is only requested from "
"business customers in the following countries: {countries}."
msgstr ""
"Werkt alleen als een factuuradres wordt gevraagd. Btw-nummer is alleen "
"vereist voor zakelijke klanten in de volgende landen: {countries}."
"Werkt alleen als een factuuradres wordt gevraagd. Btw-nummer is niet "
"verplicht."
#: pretix/base/settings.py:651
#, fuzzy
#| msgid "Require name"
msgid "Require VAT ID in"
msgstr "Btw-nummer verplicht in"
msgstr "Naam opgeven verplicht"
#: pretix/base/settings.py:657
msgid ""
@@ -10518,9 +10528,6 @@ msgid ""
"ID in all countries. VAT ID will be required for all business addresses in "
"the selected countries."
msgstr ""
"Het btw-nummer is standaard optioneel, omdat niet alle bedrijven in alle "
"landen een btw-nummer toegewezen krijgen. Het btw-nummer is vereist voor "
"alle bedrijfsadressen in de geselecteerde landen."
#: pretix/base/settings.py:672
msgid "Invoice address explanation"
@@ -14357,9 +14364,11 @@ msgstr ""
"weer aan, ten minste voor deze site, of voor 'same-origin'-verzoeken."
#: pretix/base/views/js_helpers.py:41
#, fuzzy
#| msgid "ID"
msgctxt "tax_id_swiss"
msgid "UID"
msgstr "UID"
msgstr "ID"
#. Translators: Only translate to French (IDE) and Italien (IDI), otherwise keep the same
#. Awareness around VAT IDs differes by EU country. For example, in Germany the VAT ID is assigned
@@ -14965,8 +14974,6 @@ msgid ""
"Formatting is not supported, as some accounting departments process mail "
"automatically and do not handle formatted emails properly."
msgstr ""
"Opmaak wordt niet ondersteund, omdat sommige boekhoudafdelingen e-mails "
"automatisch verwerken en opgemaakte e-mails niet goed kunnen verwerken."
#: pretix/control/forms/event.py:1356
msgid ""
@@ -14974,7 +14981,7 @@ msgid ""
"the field is empty, the mail will never be sent."
msgstr ""
"Deze e-mail zal dit aantal dagen voor het evenement start worden verstuurd. "
"Als dit veld leeg is, zal de mail nooit worden verstuurd."
"Als dit veld leeg is zal de mail nooit worden verstuurd."
#: pretix/control/forms/event.py:1360
#, fuzzy
@@ -15465,7 +15472,7 @@ msgstr "Betaald"
#: pretix/control/forms/filter.py:1304
msgctxt "subevent"
msgid "Date doesn't start in selected date range."
msgstr "De datum valt niet binnen de geselecteerde datumrange."
msgstr ""
#: pretix/control/forms/filter.py:1360 pretix/control/forms/filter.py:1827
msgid "Shop live and presale running"
@@ -29841,9 +29848,6 @@ msgid ""
"automatically. We recommend that you rename these in your source file to "
"avoid problems during import."
msgstr ""
"Meerdere kolommen van het CSV-bestand hebben dezelfde naam en zijn "
"automatisch hernoemd. We raden aan deze in je bronbestand te hernoemen om "
"problemen tijdens het importeren te voorkomen."
#: pretix/control/views/modelimport.py:188
msgid "The import was successful."
@@ -31607,7 +31611,7 @@ msgstr ""
#: pretix/plugins/banktransfer/camtimport.py:33
msgid "Empty file or unknown format."
msgstr "Leeg bestand of onbekend format."
msgstr ""
#: pretix/plugins/banktransfer/payment.py:69
msgid ""
@@ -34202,9 +34206,6 @@ msgid ""
"generate you API keys with the recommended permission level for optimal "
"usage with pretix."
msgstr ""
"Met de bovenstaande knop installeer je onze Stripe-app op je account en "
"genereer je API-sleutels met het aanbevolen machtigingsniveau voor optimaal "
"gebruik met pretix."
#: pretix/plugins/stripe/payment.py:291
msgid "Secret key"
@@ -34265,8 +34266,6 @@ msgid ""
"Some payment methods might need to be enabled in the settings of your Stripe "
"account before they work properly."
msgstr ""
"Sommige betaalmethoden moeten mogelijk eerst worden ingeschakeld in de "
"instellingen van je Stripe-account voordat ze correct werken."
#: pretix/plugins/stripe/payment.py:350 pretix/plugins/stripe/payment.py:1572
msgid "Alipay"
@@ -34327,16 +34326,16 @@ msgid "Przelewy24"
msgstr "Przelewy24"
#: pretix/plugins/stripe/payment.py:417 pretix/plugins/stripe/payment.py:1836
#, fuzzy
#| msgid "Payment by bank transfer"
msgid "Pay by bank"
msgstr "Betaling via bank"
msgstr "Betaling via bankoverschrijving"
#: pretix/plugins/stripe/payment.py:422
msgid ""
"Currently only available for charges in GBP and customers with UK bank "
"accounts, and in private preview for France and Germany."
msgstr ""
"Momenteel alleen beschikbaar voor betalingen in GBP en klanten met een "
"Britse bankrekening, en in privé-preview voor Frankrijk en Duitsland."
#: pretix/plugins/stripe/payment.py:429 pretix/plugins/stripe/payment.py:1789
msgid "WeChat Pay"
@@ -34631,8 +34630,10 @@ msgid ""
msgstr ""
#: pretix/plugins/stripe/payment.py:1816
#, fuzzy
#| msgid "WeChat Pay via Stripe"
msgid "Revolut Pay via Stripe"
msgstr "Revolut Pay via Stripe"
msgstr "WeChat Pay via Stripe"
#: pretix/plugins/stripe/payment.py:1817
msgid "Revolut Pay"
@@ -34645,24 +34646,28 @@ msgid ""
msgstr ""
#: pretix/plugins/stripe/payment.py:1835
#, fuzzy
#| msgid "Payment via Stripe"
msgid "Pay by bank via Stripe"
msgstr "Betaling door bank via Stripe"
msgstr "Betaling via Stripe"
#: pretix/plugins/stripe/payment.py:1841
msgid ""
"Pay by bank allows you to authorize a secure Open Banking payment from your "
"banking app. Currently available only with a UK bank account."
msgstr ""
"Met Betalen door bank kun je een veilige Open Banking-betaling autoriseren "
"vanuit je bankapp. Momenteel alleen beschikbaar met een Britse bankrekening."
#: pretix/plugins/stripe/payment.py:1861
#, fuzzy
#| msgid "Payment via Stripe"
msgid "PayPal via Stripe"
msgstr "PayPal via Stripe"
msgstr "Betaling via Stripe"
#: pretix/plugins/stripe/payment.py:1869
#, fuzzy
#| msgid "EPS via Stripe"
msgid "Swish via Stripe"
msgstr "Swish via Stripe"
msgstr "EPS via Stripe"
#: pretix/plugins/stripe/payment.py:1874
msgid ""
@@ -34671,20 +34676,22 @@ msgid ""
msgstr ""
#: pretix/plugins/stripe/payment.py:1893
#, fuzzy
#| msgid "giropay via Stripe"
msgid "PromptPay via Stripe"
msgstr "PromptPay via Stripe"
msgstr "giropay via Stripe"
#: pretix/plugins/stripe/payment.py:1898
msgid ""
"This payment method is available to PromptPay users in Thailand. Please have "
"your app ready."
msgstr ""
"Deze betaalmethode is beschikbaar voor PromptPay-gebruikers in Thailand. "
"Zorg ervoor dat je de app bij de hand hebt."
#: pretix/plugins/stripe/payment.py:1917
#, fuzzy
#| msgid "EPS via Stripe"
msgid "TWINT via Stripe"
msgstr "TWINT via Stripe"
msgstr "EPS via Stripe"
#: pretix/plugins/stripe/payment.py:1922
msgid ""
@@ -35003,16 +35010,22 @@ msgid "Confirm payment: %(code)s"
msgstr "Betaling bevestigen: %(code)s"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html:32
#, fuzzy
#| msgid ""
#| "Please scan the barcode below to complete your WeChat payment. Once you "
#| "have completed your payment, you can refresh this page."
msgid ""
"Please scan the QR code below to complete your PromptPay payment. Once you "
"have completed your payment, you can refresh this page."
msgstr ""
"Scan de QR-code hieronder om je PromptPay-betaling uit te voeren. Als je de "
"betaling hebt afgerond, kan je deze pagina verversen."
"Scan de QR-code hieronder om je WeChat-betaling uit te voeren. Als je de "
"betaling hebt afgerond kan je deze pagina verversen."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html:37
#, fuzzy
#| msgid "Order code"
msgid "PromptPay QR code"
msgstr "PromptPay-QR-code"
msgstr "Bestelcode"
#: pretix/plugins/stripe/templates/pretixplugins/stripe/sca_return.html:20
msgid "Confirming your payment…"
@@ -36159,7 +36172,10 @@ msgstr[0] "Je moet precies één optie kiezen uit deze categorie."
msgstr[1] "Je moet %(min_count)s opties kiezen uit deze categorie."
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:26
#, python-format
#, fuzzy, python-format
#| msgid "You can choose at most one option from this category."
#| msgid_plural ""
#| "You can choose up to %(max_count)s options from this category."
msgid "You can choose one option from this category."
msgid_plural "You can choose up to %(max_count)s options from this category."
msgstr[0] "Je kan maximaal één optie kiezen uit deze categorie."
@@ -38510,12 +38526,12 @@ msgid ""
"Your cart timeout was extended. Please note that some of the prices in your "
"cart changed."
msgstr ""
"De time-out van je winkelwagen is verlengd. Merk op dat sommige prijzen in "
"je winkelwagen gewijzigd zijn."
#: pretix/presale/views/cart.py:573
#, fuzzy
#| msgid "Your cart has been updated."
msgid "Your cart timeout was extended."
msgstr "De time-out van je winkelwagen is verlengd."
msgstr "Je winkelwagen is bijgewerkt."
#: pretix/presale/views/cart.py:588
msgid "The products have been successfully added to your cart."

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 12:12+0000\n"
"PO-Revision-Date: 2026-01-16 09:22+0000\n"
"Last-Translator: Richard Schreiber <schreiber@rami.io>\n"
"PO-Revision-Date: 2025-11-07 23:00+0000\n"
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix/"
"sv/>\n"
"Language: sv\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.15.2\n"
"X-Generator: Weblate 5.14.2\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -8993,12 +8993,10 @@ msgstr ""
"exporter."
#: pretix/base/services/invoices.py:116
#, fuzzy, python-brace-format
#, python-brace-format
msgctxt "invoice"
msgid "Please complete your payment before {expire_date}."
msgstr ""
"Om din betalning inte gick igenom, se till att uppdatera din "
"betalningsinformation innan {expire_date} via knappen nedan."
msgstr "{expire_date}."
#: pretix/base/services/invoices.py:128
#, python-brace-format
@@ -35836,7 +35834,7 @@ msgstr "Visa i backend"
#: pretix/presale/templates/pretixpresale/event/order.html:92
#, python-format
msgid "A payment of %(total)s is still pending for this order."
msgstr "Tack för din bokning på %(total)s."
msgstr "En faktura på totalt %(total)s kommer att skickas från Coeo."
#: pretix/presale/templates/pretixpresale/event/order.html:97
#, fuzzy, python-format
@@ -35845,7 +35843,7 @@ msgstr "Tack för din bokning!"
#: pretix/presale/templates/pretixpresale/event/order.html:108
msgid "Re-try payment or choose another payment method"
msgstr "Uppdatera eller ändra betalningsmetod"
msgstr "Tack"
#: pretix/presale/templates/pretixpresale/event/order.html:126
msgid ""

View File

@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 12:12+0000\n"
"PO-Revision-Date: 2026-01-12 17:00+0000\n"
"Last-Translator: chondaen12 <chondaen12@gmail.com>\n"
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix/th/>"
"\n"
"PO-Revision-Date: 2024-03-30 11:00+0000\n"
"Last-Translator: Thatthep <amaudy@gmail.com>\n"
"Language-Team: Thai <https://translate.pretix.eu/projects/pretix/pretix/th/"
">\n"
"Language: th\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.15.1\n"
"X-Generator: Weblate 5.4.3\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -29,7 +29,7 @@ msgstr "เยอรมัน"
#: pretix/_base_settings.py:89
msgid "German (informal)"
msgstr "เยอรมัน (ไม่เป็นทางการ)"
msgstr "เยอรมัน"
#: pretix/_base_settings.py:90
msgid "Arabic"
@@ -3567,7 +3567,7 @@ msgstr ""
#: pretix/base/invoicing/pdf.py:732 pretix/base/invoicing/pdf.py:740
msgctxt "invoice"
msgid "Description"
msgstr "คำอธิบาย"
msgstr ""
#: pretix/base/invoicing/pdf.py:733 pretix/base/invoicing/pdf.py:741
msgctxt "invoice"
@@ -3577,7 +3577,7 @@ msgstr ""
#: pretix/base/invoicing/pdf.py:734 pretix/base/invoicing/pdf.py:1038
msgctxt "invoice"
msgid "Tax rate"
msgstr "อัตราภาษี"
msgstr ""
#: pretix/base/invoicing/pdf.py:735
msgctxt "invoice"
@@ -3592,7 +3592,7 @@ msgstr ""
#: pretix/base/invoicing/pdf.py:742
msgctxt "invoice"
msgid "Amount"
msgstr "จำนวน"
msgstr ""
#: pretix/base/invoicing/pdf.py:869
#, python-brace-format
@@ -3624,12 +3624,12 @@ msgstr ""
#: pretix/base/invoicing/pdf.py:979
msgctxt "invoice"
msgid "Paid by gift card"
msgstr "จ่ายโดยบัตรของขวัญ"
msgstr ""
#: pretix/base/invoicing/pdf.py:984
msgctxt "invoice"
msgid "Remaining amount"
msgstr "จำนวนคงเหลือ"
msgstr ""
#: pretix/base/invoicing/pdf.py:1008
#, python-brace-format
@@ -3640,7 +3640,7 @@ msgstr ""
#: pretix/base/invoicing/pdf.py:1039
msgctxt "invoice"
msgid "Net value"
msgstr "ยอดสุทธิ"
msgstr ""
#: pretix/base/invoicing/pdf.py:1040
msgctxt "invoice"
@@ -3650,7 +3650,7 @@ msgstr ""
#: pretix/base/invoicing/pdf.py:1041
msgctxt "invoice"
msgid "Tax"
msgstr "ภาษี"
msgstr ""
#: pretix/base/invoicing/pdf.py:1071
msgctxt "invoice"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-05 12:12+0000\n"
"PO-Revision-Date: 2026-01-12 17:00+0000\n"
"PO-Revision-Date: 2025-12-22 20:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Chinese (Traditional Han script) <https://translate.pretix.eu/"
"projects/pretix/pretix/zh_Hant/>\n"
@@ -288,28 +288,39 @@ msgid "The bundled item must not have bundles on its own."
msgstr "捆綁商品不得單獨捆綁。"
#: pretix/api/serializers/item.py:235
#, fuzzy
#| msgid "The payment is too late to be accepted."
msgid "The program start must not be empty."
msgstr "程式啟動不能為空。"
msgstr "付款太晚無法接受。"
#: pretix/api/serializers/item.py:239
#, fuzzy
#| msgid "The payment is too late to be accepted."
msgid "The program end must not be empty."
msgstr "程式結束不能為空。"
msgstr "付款太晚無法接受。"
#: pretix/api/serializers/item.py:242 pretix/base/models/items.py:2321
#, fuzzy
#| msgid "The maximum date must not be before the minimum value."
msgid "The program end must not be before the program start."
msgstr "程式不能在程式開始之前結束。"
msgstr "最大日期不得早於最小值。"
#: pretix/api/serializers/item.py:247 pretix/base/models/items.py:2315
#, fuzzy
#| msgid "You can not select a subevent if your event is not an event series."
msgid "You cannot use program times on an event series."
msgstr "您不能在事件系列中使用程式時間。"
msgstr "如果你的活動不是活動系列,則無法選擇子活動。"
#: pretix/api/serializers/item.py:337
#, fuzzy
#| msgid ""
#| "Updating add-ons, bundles, or variations via PATCH/PUT is not supported. "
#| "Please use the dedicated nested endpoint."
msgid ""
"Updating add-ons, bundles, program times or variations via PATCH/PUT is not "
"supported. Please use the dedicated nested endpoint."
msgstr ""
"不支援透過PATCH/PUT更新附加件、捆綁包、程式時間或變體。 請使用專用的巢狀端"
"點。"
"不支持通過 PATCH/PUT 更新附加件、捆綁包或變體。 請使用專用的嵌套端點。"
#: pretix/api/serializers/item.py:345
msgid "Only admission products can currently be personalized."
@@ -559,8 +570,10 @@ msgid "Event series date deleted"
msgstr "活動系列日期已刪除"
#: pretix/api/webhooks.py:375
#, fuzzy
#| msgid "Product name"
msgid "Product changed"
msgstr "產品已更改"
msgstr "商品名稱"
#: pretix/api/webhooks.py:376
msgid ""
@@ -601,12 +614,16 @@ msgid "Waiting list entry received voucher"
msgstr "候補名單條目收到憑證"
#: pretix/api/webhooks.py:413
#, fuzzy
#| msgid "Voucher code"
msgid "Voucher added"
msgstr "添加了優惠券"
msgstr "優惠券代碼"
#: pretix/api/webhooks.py:417
#, fuzzy
#| msgid "Voucher assigned"
msgid "Voucher changed"
msgstr "優惠券已更改"
msgstr "已分配的優惠券"
#: pretix/api/webhooks.py:418
msgid ""

View File

@@ -271,8 +271,7 @@ class BankTransfer(BasePaymentProvider):
'request': request,
'event': self.event,
'settings': self.settings,
'code': self._code(order, force=False) if order else None,
'order': order,
'code': self._code(order) if order else None,
'details': self.settings.get('bank_details', as_type=LazyI18nString),
}
return template.render(ctx)
@@ -296,7 +295,7 @@ class BankTransfer(BasePaymentProvider):
md_nl2br = " \n"
if self.settings.get('bank_details_type') == 'sepa':
bankdetails = (
(_("Reference"), self._code(order, force=True)),
(_("Reference"), self._code(order)),
(_("Amount"), money_filter(payment.amount, self.event.currency)),
(_("Account holder"), self.settings.get('bank_details_sepa_name')),
(_("IBAN"), ibanformat(self.settings.get('bank_details_sepa_iban'))),
@@ -305,7 +304,7 @@ class BankTransfer(BasePaymentProvider):
)
else:
bankdetails = (
(_("Reference"), self._code(order, force=True)),
(_("Reference"), self._code(order)),
(_("Amount"), money_filter(payment.amount, self.event.currency)),
)
t += md_nl2br.join([f"**{k}:** {v}" for k, v in bankdetails])
@@ -318,7 +317,7 @@ class BankTransfer(BasePaymentProvider):
template = get_template('pretixplugins/banktransfer/pending.html')
ctx = {
'event': self.event,
'code': self._code(payment.order, force=True),
'code': self._code(payment.order),
'order': payment.order,
'amount': payment.amount,
'payment_info': payment.info_data,
@@ -346,25 +345,16 @@ class BankTransfer(BasePaymentProvider):
def _render_control_info(self, request, order, info_data, **extra_context):
template = get_template('pretixplugins/banktransfer/control.html')
ctx = {'request': request, 'event': self.event,
'code': self._code(order, force=True),
'code': self._code(order),
'payment_info': info_data, 'order': order,
**extra_context}
return template.render(ctx)
def _code(self, order, force=False):
def _code(self, order):
prefix = self.settings.get('prefix', default='')
li = order.invoices.last()
invoice_number = li.number if self.settings.get('include_invoice_number', as_type=bool) and li else ''
invoice_will_be_generated = (
not li and
self.settings.get('include_invoice_number', as_type=bool) and
order.event.settings.get('invoice_generate') == 'paid' and
self.requires_invoice_immediately
)
if invoice_will_be_generated and not force:
return None
code = " ".join((prefix, order.full_code, invoice_number)).strip(" ")
if self.settings.get('omit_hyphen', as_type=bool):

View File

@@ -24,13 +24,7 @@
{{ details | rich_text }}
{% if not code %}</div>{% endif %}
{% if not code and order %}
<p>
<strong>
{% trans "We will assign you a personal reference code in the next step." %}
</strong>
</p>
{% elif not code %}
{% if not code %}
<p>
<strong>
{% trans "We will assign you a personal reference code to use after you completed the order." %}

View File

@@ -19,30 +19,6 @@ $(function () {
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'abd_chart',
data: JSON.parse($("#abd-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'abt_chart',
data: JSON.parse($("#abt-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'rev_chart',
data: JSON.parse($("#rev-data").html()),

View File

@@ -24,48 +24,6 @@
<small>
{% blocktrans trimmed %}
Orders paid in multiple payments are shown with the date of their last payment.
Placed orders include all orders (pending, paid, cancelled, and expired);
paid orders include only paid orders and exclude all cancelled orders.
{% endblocktrans %}
</small>
</p>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Attendees by day" %}</h3>
</div>
<div class="panel-body">
<div id="abd_chart" class="chart"></div>
<p class="help-block">
<small>
{% blocktrans trimmed %}
Attendees in orders paid in multiple instalments are shown using the date of the
final payment. Order dates reflect when the order was first placed; attendees added
later via additional order positions still use the original order date. Attendees in
placed orders include those from all order states (pending, paid, cancelled, and
expired); attendees in paid orders include only those from paid orders and exclude
those from cancelled orders.
{% endblocktrans %}
</small>
</p>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Attendees by time" %}</h3>
</div>
<div class="panel-body">
<div id="abt_chart" class="chart"></div>
<p class="help-block">
<small>
{% blocktrans trimmed %}
Attendees in orders paid in multiple instalments are shown using the date of the
final payment. Order dates reflect when the order was first placed; attendees added
later via additional order positions still use the original order date. Attendees in
placed orders include those from all order states (pending, paid, cancelled, and
expired); attendees in paid orders include only those from paid orders and exclude
those from cancelled orders.
{% endblocktrans %}
</small>
</p>
@@ -87,19 +45,10 @@
{% endif %}
<p class="help-block">
<small>
{% if request.GET.subevent %}
{% blocktrans trimmed %}
Only fully paid orders are counted.
Orders paid in multiple payments are shown with the date of their last payment.
Revenue excludes all fees, including cancellation fees.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
Only fully paid orders are counted.
Orders paid in multiple payments are shown with the date of their last payment.
Revenue includes all fees, including cancellation fees from cancelled orders.
{% endblocktrans %}
{% endif %}
{% blocktrans trimmed %}
Only fully paid orders are counted.
Orders paid in multiple payments are shown with the date of their last payment.
{% endblocktrans %}
</small>
</p>
</div>
@@ -110,14 +59,6 @@
</div>
<div class="panel-body">
<div id="obp_chart" class="chart"></div>
<p class="help-block">
<small>
{% blocktrans trimmed %}
Placed orders include all orders (pending, paid, cancelled, and expired);
paid orders include only paid orders and exclude all cancelled orders.
{% endblocktrans %}
</small>
</p>
</div>
</div>
{% if seats %}
@@ -217,8 +158,6 @@
</div>
{% endif %}
<script type="application/json" id="obd-data">{{ obd_data|escapejson }}</script>
<script type="application/json" id="abd-data">{{ abd_data|escapejson }}</script>
<script type="application/json" id="abt-data">{{ abt_data|escapejson }}</script>
<script type="application/json" id="rev-data">{{ rev_data|escapejson }}</script>
<script type="application/json" id="obp-data">{{ obp_data|escapejson }}</script>
<script type="application/text" id="currency">{{ request.event.currency }}</script>

View File

@@ -103,10 +103,7 @@ class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView)
day = o['datetime'].astimezone(tz).date()
ordered_by_day[day] = ordered_by_day.get(day, 0) + 1
paid_by_day = {}
for o in oqs.filter(
event=self.request.event, payment_date__isnull=False,
status=Order.STATUS_PAID, all_positions__canceled=False
).distinct().values('payment_date'):
for o in oqs.filter(event=self.request.event, payment_date__isnull=False).values('payment_date'):
day = o['payment_date'].astimezone(tz).date()
paid_by_day[day] = paid_by_day.get(day, 0) + 1
@@ -128,56 +125,10 @@ class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView)
ctx['obd_data'] = json.dumps(data)
cache.set('statistics_obd_data' + ckey, ctx['obd_data'])
# Attendees by day/time
ctx['abd_data'] = cache.get('statistics_abd_data' + ckey)
ctx['abt_data'] = cache.get('statistics_abt_data' + ckey)
if not ctx['abd_data'] or not ctx['abt_data']:
opqs = OrderPosition.all.filter(order__event=self.request.event, item__admission=True).annotate(
payment_date=Subquery(op_date, output_field=DateTimeField())
)
if subevent:
opqs = opqs.filter(subevent=subevent)
ordered_by_day = {}
for p in opqs.values('order__datetime'):
day = p['order__datetime'].astimezone(tz).date()
ordered_by_day[day] = ordered_by_day.get(day, 0) + 1
paid_by_day = {}
for p in opqs.filter(payment_date__isnull=False, canceled=False, order__status=Order.STATUS_PAID).values('payment_date'):
day = p['payment_date'].astimezone(tz).date()
paid_by_day[day] = paid_by_day.get(day, 0) + 1
day_data = []
time_data = []
for d in dateutil.rrule.rrule(
dateutil.rrule.DAILY,
dtstart=min(ordered_by_day.keys()) if ordered_by_day else datetime.date.today(),
until=max(
max(ordered_by_day.keys() if paid_by_day else [datetime.date.today()]),
max(paid_by_day.keys() if paid_by_day else [datetime.date(1970, 1, 1)])
)):
d = d.date()
day_data.append({
'date': d.strftime('%Y-%m-%d'),
'ordered': ordered_by_day.get(d, 0),
'paid': paid_by_day.get(d, 0)
})
time_data.append({
'date': d.strftime('%Y-%m-%d'),
'ordered': (time_data[-1]["ordered"] if time_data else 0) + ordered_by_day.get(d, 0),
'paid': (time_data[-1]["paid"] if time_data else 0) + paid_by_day.get(d, 0)
})
ctx['abd_data'] = json.dumps(day_data)
ctx['abt_data'] = json.dumps(time_data)
cache.set('statistics_abd_data' + ckey, ctx['abd_data'])
cache.set('statistics_abt_data' + ckey, ctx['abt_data'])
# Orders by product
ctx['obp_data'] = cache.get('statistics_obp_data' + ckey)
if not ctx['obp_data']:
opqs = OrderPosition.all
opqs = OrderPosition.objects
if subevent:
opqs = opqs.filter(subevent=subevent)
num_ordered = {
@@ -190,7 +141,7 @@ class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView)
num_paid = {
p['item']: p['cnt']
for p in (opqs
.filter(order__event=self.request.event, order__status=Order.STATUS_PAID, canceled=False)
.filter(order__event=self.request.event, order__status=Order.STATUS_PAID)
.values('item')
.annotate(cnt=Count('id')).order_by())
}

View File

@@ -313,12 +313,12 @@
<input type="hidden" name="variation_{{ line.item.id }}_{{ line.variation.id }}"
value="1" />
<input type="hidden" name="price_{{ line.item.id }}_{{ line.variation.id }}"
value="{% if event.settings.display_net_prices %}{{ line.price_for_input_net }}{% else %}{{ line.price_for_input }}{% endif %}" />
value="{% if event.settings.display_net_prices %}{{ line.bundle_sum_net }}{% else %}{{ line.bundle_sum }}{% endif %}" />
{% else %}
<input type="hidden" name="item_{{ line.item.id }}"
value="1" />
<input type="hidden" name="price_{{ line.item.id }}"
value="{% if event.settings.display_net_prices %}{{ line.price_for_input_net }}{% else %}{{ line.price_for_input }}{% endif %}" />
value="{% if event.settings.display_net_prices %}{{ line.bundle_sum_net }}{% else %}{{ line.bundle_sum }}{% endif %}" />
{% endif %}
<button class="btn btn-mini btn-link {% if line.seat %}btn-invisible{% endif %}" title="{% blocktrans with item=line.item.name %}Add one more {{item}} to your cart{% endblocktrans %}" {% if line.seat %}disabled{% endif %}>
<i class="fa fa-plus" aria-hidden="true"></i>

View File

@@ -48,7 +48,7 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_scopes import scopes_disabled
from pretix.base.i18n import get_language_without_region, set_region
from pretix.base.i18n import get_language_without_region
from pretix.base.middleware import get_supported_language
from pretix.base.models import (
CartPosition, Customer, InvoiceAddress, ItemAddOn, OrderFee, Question,
@@ -209,8 +209,6 @@ class CartMixin:
pos.valid_from,
pos.valid_until,
pos.used_membership_id,
pos.gross_price_before_rounding,
pos.tax_value_before_rounding,
)
positions = []
@@ -224,8 +222,8 @@ class CartMixin:
if not hasattr(group, 'tax_rule'):
group.tax_rule = group.item.tax_rule
group.price_for_input = group.gross_price_before_rounding + sum(a.gross_price_before_rounding for a in has_addons[group.pk])
group.price_for_input_net = group.net_price_before_rounding + sum(a.net_price_before_rounding for a in has_addons[group.pk])
group.bundle_sum = group.price + sum(a.price for a in has_addons[group.pk])
group.bundle_sum_net = group.net_price + sum(a.net_price for a in has_addons[group.pk])
if answers:
group.cache_answers(all=False)
@@ -544,7 +542,6 @@ def iframe_entry_view_wrapper(view_func):
region = request.event.settings.region
if '-' not in lng and region:
lng += '-' + region.lower()
set_region(region)
# with language() is not good enough here we really need to take the role of LocaleMiddleware and modify
# global state, because template rendering might be happening lazily.

View File

@@ -42,7 +42,6 @@ import os
import re
from collections import OrderedDict, defaultdict
from decimal import Decimal
from urllib.parse import quote
from django import forms
from django.conf import settings
@@ -104,55 +103,13 @@ logger = logging.getLogger(__name__)
class OrderDetailMixin(NoSearchIndexViewMixin):
def _allow_anonymous_access(self):
return not (self.request.organizer.settings.customer_accounts and
self.request.organizer.settings.customer_accounts_require_login_for_order_access)
def verify_order_access(self):
o = self.order
if o is None:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if o is False:
login_url = eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={})
if hasattr(self.request, "event_domain") and self.request.event_domain:
next_url = quote(self.request.scheme + "://" + self.request.get_host() + self.request.get_full_path())
return redirect_to_url(f'{login_url}?next={next_url}&request_cross_domain_customer_auth=true')
else:
next_url = quote(self.request.get_full_path())
return redirect_to_url(f'{login_url}?next={next_url}')
return None
@cached_property
def order(self):
"""
Returns the order object when access is permitted, returns `False` when the
order exists but requires authentication, and returns `None` when the order
does not exist or access is denied entirely.
"""
try:
order = self.request.event.orders.filter().select_related('event').get_with_secret_check(
return self.request.event.orders.filter().select_related('event').get_with_secret_check(
code=self.kwargs['order'], received_secret=self.kwargs['secret'], tag=None,
)
if has_event_access_permission(self.request, 'can_view_orders'):
return order
if order.customer is None or not order.customer.is_verified or self._allow_anonymous_access():
return order
if not self.request.customer:
return False
if order.customer_id == self.request.customer.pk:
return order
return None
except Order.DoesNotExist:
return None
@@ -162,13 +119,6 @@ class OrderDetailMixin(NoSearchIndexViewMixin):
'secret': self.order.secret
})
def dispatch(self, request, *args, **kwargs):
resp = self.verify_order_access()
if resp:
return resp
return super().dispatch(request, *args, **kwargs)
class OrderPositionDetailMixin(NoSearchIndexViewMixin):
@cached_property
@@ -207,6 +157,8 @@ class OrderPositionDetailMixin(NoSearchIndexViewMixin):
@method_decorator(xframe_options_exempt, 'dispatch')
class OrderOpen(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs):
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if self.order.check_email_confirm_secret(kwargs.get('hash')) and not self.order.email_known_to_work:
self.order.log_action('pretix.event.order.contact.confirmed')
self.order.email_known_to_work = True
@@ -287,6 +239,8 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin,
def get(self, request, *args, **kwargs):
self.kwargs = kwargs
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if self.order.status == Order.STATUS_PENDING:
payment_to_complete = self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CREATED, process_initiated=False).first()
if payment_to_complete:
@@ -406,11 +360,8 @@ class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView):
def dispatch(self, request, *args, **kwargs):
self.request = request
self.request.pci_dss_payment_page = True
resp = self.verify_order_access()
if resp:
return resp
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if (self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED)
or self.payment.state != OrderPayment.PAYMENT_STATE_CREATED
or not self.payment.payment_provider.is_enabled
@@ -477,11 +428,8 @@ class OrderPaymentConfirm(EventViewMixin, OrderDetailMixin, TemplateView):
def dispatch(self, request, *args, **kwargs):
self.request = request
resp = self.verify_order_access()
if resp:
return resp
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED or self.order._can_be_paid() is not True:
messages.error(request, _('The payment for this order cannot be continued.'))
return redirect(self.get_order_url())
@@ -547,11 +495,8 @@ class OrderPaymentComplete(EventViewMixin, OrderDetailMixin, View):
def dispatch(self, request, *args, **kwargs):
self.request = request
resp = self.verify_order_access()
if resp:
return resp
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED or self.order._can_be_paid() is not True:
messages.error(request, _('The payment for this order cannot be continued.'))
return redirect(self.get_order_url())
@@ -596,11 +541,8 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
def dispatch(self, request, *args, **kwargs):
self.request = request
self.request.pci_dss_payment_page = True
resp = self.verify_order_access()
if resp:
return resp
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) or self.order._can_be_paid() is not True:
messages.error(request, _('The payment method for this order cannot be changed.'))
return redirect(self.get_order_url())
@@ -785,6 +727,8 @@ class OrderInvoiceCreate(EventViewMixin, OrderDetailMixin, View):
def dispatch(self, request, *args, **kwargs):
self.request = request
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
@@ -916,11 +860,8 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
def dispatch(self, request, *args, **kwargs):
self.request = request
self.kwargs = kwargs
resp = self.verify_order_access()
if resp:
return resp
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if not self.order.can_modify_answers:
messages.error(request, _('You cannot modify this order'))
return redirect(self.get_order_url())
@@ -1006,11 +947,8 @@ class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
def dispatch(self, request, *args, **kwargs):
self.request = request
self.kwargs = kwargs
resp = self.verify_order_access()
if resp:
return resp
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if not self.order.user_cancel_allowed:
messages.error(request, _('You cannot cancel this order.'))
return redirect(self.get_order_url())
@@ -1057,7 +995,14 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
def get_error_url(self):
return self.get_order_url()
def get(self, request, *args, **kwargs):
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if not self.order.user_cancel_allowed:
messages.error(request, _('You cannot cancel this order.'))
return redirect(self.get_order_url())
@@ -1348,6 +1293,9 @@ class OrderPositionDownload(OrderDownloadMixin, EventViewMixin, OrderPositionDet
class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs):
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
try:
invoice = Invoice.objects.get(
event=self.request.event,
@@ -1740,11 +1688,8 @@ class OrderChange(OrderChangeMixin, EventViewMixin, OrderDetailMixin, TemplateVi
def dispatch(self, request, *args, **kwargs):
self.request = request
self.kwargs = kwargs
resp = self.verify_order_access()
if resp:
return resp
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if not self.order.user_change_allowed:
messages.error(request, _('You cannot change this order.'))
return redirect(self.get_order_url())

View File

@@ -781,39 +781,6 @@ testcases_single_rule = [
)
),
# Distribute discounts somewhat equally over addon groups
(
(
Discount(
condition_min_count=3,
benefit_discount_matching_percent=20,
condition_apply_to_addons=True,
benefit_only_apply_to_cheapest_n_matches=1,
condition_ignore_voucher_discounted=True,
),
),
(
(2, 1, now(), Decimal('0.00'), None, False, Decimal('10.00')), # Main product
(1, 1, now(), Decimal('100.00'), 1, False, Decimal('0.00')),
(1, 1, now(), Decimal('100.00'), 1, False, Decimal('0.00')),
(1, 1, now(), Decimal('100.00'), 1, False, Decimal('0.00')),
(2, 1, now(), Decimal('0.00'), None, False, Decimal('10.00')), # Main product
(1, 1, now(), Decimal('100.00'), 2, False, Decimal('0.00')),
(1, 1, now(), Decimal('100.00'), 2, False, Decimal('0.00')),
(1, 1, now(), Decimal('100.00'), 2, False, Decimal('0.00')),
),
(
Decimal('0.00'),
Decimal('100.00'),
Decimal('100.00'),
Decimal('80.00'),
Decimal('0.00'),
Decimal('100.00'),
Decimal('100.00'),
Decimal('80.00'),
)
),
# Ignore bundled
(
(

View File

@@ -68,6 +68,9 @@ def test_urlreplace_replace_parameter():
("en", Decimal("1023"), "JPY", "¥1,023"),
("pt-pt", Decimal("10.00"), "EUR", "10,00" + NBSP + ""),
("pt-br", Decimal("10.00"), "EUR", "" + NBSP + "10,00"),
# unknown currency
("de", Decimal("1234.56"), "FOO", "1.234,56" + NBSP + "FOO"),
("de", Decimal("1234.567"), "FOO", "1.234,57" + NBSP + "FOO"),

View File

@@ -2611,11 +2611,3 @@ def test_approve_cancellation_request(client, env):
doc = BeautifulSoup(response.content.decode(), "lxml")
assert doc.select('input[name=refund-new-giftcard]')[0]['value'] == '10.00'
assert not env[2].cancellation_requests.exists()
@pytest.mark.django_db
def test_view_as_user(client, env):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/%s/%s/order/%s/%s/' % (env[0].organizer.slug, env[0].slug, env[2].code, env[2].secret))
assert response.status_code == 200
assert env[2].code in response.content.decode()

View File

@@ -19,12 +19,9 @@
# 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 pytest
from django.utils.translation import get_language
from pretix.base.i18n import (
get_babel_locale, get_language_without_region, language,
)
from pretix.base.i18n import get_language_without_region, language
from pretix.helpers.i18n import get_javascript_format, get_moment_locale
@@ -45,26 +42,22 @@ def test_get_locale():
assert get_moment_locale('en-CA') == 'en-ca'
@pytest.mark.parametrize(
["lng_in", "region_in", "lng_out", "lng_without_region_out", "babel_out"],
[
("en", None, "en", "en", "en"),
("en-us", None, "en-us", "en", "en_US"),
("en", "US", "en-us", "en", "en_US"),
("de", None, "de", "de", "de"),
("de", "US", "de-us", "de", "de"),
("de", "DE", "de-de", "de", "de_DE"),
("de-informal", "DE", "de-informal", "de-informal", "de_DE"),
("de-informal", "CH", "de-informal", "de-informal", "de_CH"),
("pt-pt", "PT", "pt-pt", "pt-pt", "pt_PT"),
("es", "MX", "es-mx", "es", "es_MX"),
("es-419", "MX", "es-419", "es-419", "es_MX"),
("zh-hans", "CN", "zh-hans", "zh-hans", "zh_Hans_CN"),
("zh-hant", "TW", "zh-hant", "zh-hant", "zh_Hant_TW"),
],
)
def test_set_region(lng_in, region_in, lng_out, lng_without_region_out, babel_out):
with language(lng_in, region_in):
assert get_language() == lng_out
assert get_language_without_region() == lng_without_region_out
assert str(get_babel_locale()) == babel_out
def test_set_region():
with language('de'):
assert get_language() == 'de'
assert get_language_without_region() == 'de'
with language('de', 'US'):
assert get_language() == 'de-us'
assert get_language_without_region() == 'de'
with language('de', 'DE'):
assert get_language() == 'de-de'
assert get_language_without_region() == 'de'
with language('de-informal', 'DE'):
assert get_language() == 'de-informal'
assert get_language_without_region() == 'de-informal'
with language('pt', 'PT'):
assert get_language() == 'pt-pt'
assert get_language_without_region() == 'pt-pt'
with language('pt-pt', 'BR'):
assert get_language() == 'pt-pt'
assert get_language_without_region() == 'pt-pt'

View File

@@ -1738,222 +1738,3 @@ class OrdersTest(BaseOrdersTest):
self.order.secret, a.pk, match.group(1))
)
assert response.status_code == 404
def test_require_login_for_order_access_disabled_unauth(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = False
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
response = self.client.get(
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_enabled_unauth(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 302
response = self.client.get(
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_disabled_auth(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = False
response = self.client.post('/%s/account/login' % (self.orga.slug), {
'email': 'john@example.org',
'password': 'foo',
})
assert response.status_code == 302
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
response = self.client.get(
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_enabled_auth(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.post('/%s/account/login' % (self.orga.slug), {
'email': 'john@example.org',
'password': 'foo',
})
assert response.status_code == 302
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
response = self.client.get(
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_accounts_disabled(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts = False
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()
def test_require_login_for_order_access_enabled_wrong_customer(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
with scopes_disabled():
customer = self.orga.customers.create(email='jill@example.org', is_verified=True)
customer.set_password("bar")
customer.save()
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.post('/%s/account/login' % (self.orga.slug), {
'email': 'jill@example.org',
'password': 'bar',
})
assert response.status_code == 302
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 404
def test_require_login_for_order_access_enabled_unverified_account(self):
self.orga.settings.customer_accounts = True
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=False)
customer.set_password("foo")
customer.save()
self.order.customer = customer
self.order.save()
self.orga.settings.customer_accounts_require_login_for_order_access = True
response = self.client.get(
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200
doc = BeautifulSoup(response.content.decode(), "lxml")
assert len(doc.select(".cart-row")) > 0
assert "pending" in doc.select(".order-details")[0].text.lower()
assert "Peter" in response.content.decode()
assert "Lukas" not in response.content.decode()