Compare commits

...

18 Commits

Author SHA1 Message Date
Martin Gross
af28785fb9 Stripe: iDEAL -> iDEAL | Wero rebrand 2026-03-12 13:37:35 +01:00
Martin Gross
54e4957e89 Stripe: Update list of supported payment methods 2026-03-12 13:37:06 +01:00
Richard Schreiber
f3597f1a44 Fix orderlist export with no events (#5936) 2026-03-11 08:08:41 +01:00
Raphael Michel
2e01887e79 Invoice address: Special validation for Belgium (Z#23224796) (#5970)
* Invoice address: Special validation for Belgium (Z#23224796)

* Update src/pretix/base/invoicing/peppol.py

Co-authored-by: pajowu <engelhardt@pretix.eu>

---------

Co-authored-by: pajowu <engelhardt@pretix.eu>
2026-03-10 09:57:44 +01:00
Raphael Michel
5a7e7fbde3 Event lists: Show sales channels (Z#23225483) (#5967) 2026-03-10 09:56:29 +01:00
Raphael Michel
7b296107c5 Invoice address: Fix broken autofill for Peppol ID (Z#23224796) (#5971)
* Invoice address: Fix broken autofill for Peppol ID (Z#23224796)

* Fix wrong prefix
2026-03-10 09:54:54 +01:00
Raphael Michel
4f449ce6b4 Mail: Handle all rendering in mail.py, return values for log (#5895)
* Mail: Handle all rendering in mail.py, return values for log

* Apply suggestions from code review
2026-03-10 09:53:09 +01:00
Raphael Michel
e6ea8fb5bf Error pages: Load event theme if available (Z#23224853) (#5972) 2026-03-09 20:11:01 +01:00
Raphael Michel
547910beec Voucher CSV download: Do not output "any product" (Z#23224795) (#5969) 2026-03-09 18:26:54 +01:00
Raphael Michel
eef1560ede Order modification: Remove warning when invoice is not yet generated (Z#23226423) (#5966) 2026-03-09 18:16:37 +01:00
Raphael Michel
3d68bbb619 Order change manager: Recalculate tax of zero-valued positions (Z#23223874) (#5938) 2026-03-09 18:13:14 +01:00
Raphael Michel
dc4556d428 PDF editor: add file size to label (Z#23226663) (#5965) 2026-03-09 18:10:57 +01:00
Raphael Michel
5099fa16e0 Fix incorrect type annotation 2026-03-09 17:48:38 +01:00
Kara Engelhardt
f3fb1e66dc Fix waiting list availability calculation if WL vouchers have seats (Z#23226856) 2026-03-09 17:18:47 +02:00
Ruud Hendrickx
99e9690d48 Translations: Update Dutch (Belgium)
Currently translated at 71.3% (4465 of 6257 strings)

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

powered by weblate
2026-03-09 14:24:17 +01:00
Hijiri Umemoto
e63e82e854 Translations: Update Japanese
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-09 14:24:17 +01:00
argonimos
c662e627d5 Translations: Update German
Currently translated at 100.0% (6257 of 6257 strings)

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

powered by weblate
2026-03-09 14:24:17 +01:00
Mie Frydensbjerg
f2121c7853 Translations: Update Danish
Currently translated at 44.7% (2800 of 6257 strings)

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

powered by weblate
2026-03-09 14:24:17 +01:00
37 changed files with 393 additions and 241 deletions

View File

@@ -315,8 +315,9 @@ class OrderListExporter(MultiSheetListExporter):
for id, vn in payment_methods:
headers.append(_('Paid by {method}').format(method=vn))
# get meta_data labels from first cached event
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
if self.event_object_cache:
# get meta_data labels from first cached event if any
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
yield headers
full_fee_sum_cache = {
@@ -503,8 +504,9 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('External customer ID'))
headers.append(_('Payment providers'))
# get meta_data labels from first cached event
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
if self.event_object_cache:
# get meta_data labels from first cached event if any
headers += next(iter(self.event_object_cache.values())).meta_data.keys()
yield headers
yield self.ProgressSetTotal(total=qs.count())
@@ -707,9 +709,9 @@ class OrderListExporter(MultiSheetListExporter):
_('Position order link')
]
# get meta_data labels from first cached event
meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys()
if has_subevents:
# get meta_data labels from first cached event
meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys()
headers += meta_data_labels
yield headers

View File

@@ -1415,6 +1415,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not data.get(r):
raise ValidationError({r: _("This field is required for the selected type of invoice transmission.")})
transmission_type.validate_invoice_address_data(data)
self.instance.transmission_type = transmission_type.identifier
self.instance.transmission_info = transmission_type.form_data_to_transmission_info(data)
elif transmission_type.is_exclusive(self.event, data.get("country"), data.get("is_business")):

View File

@@ -33,8 +33,7 @@ from pretix.base.invoicing.transmission import (
transmission_types,
)
from pretix.base.models import Invoice, InvoiceAddress
from pretix.base.services.mail import mail, render_mail
from pretix.helpers.format import format_map
from pretix.base.services.mail import mail
@transmission_types.new()
@@ -134,9 +133,7 @@ class EmailTransmissionProvider(TransmissionProvider):
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
# Do not set to completed because that is done by the email sending task
subject = format_map(subject, context)
email_content = render_mail(template, context)
mail(
outgoing_mail = mail(
[recipient],
subject,
template,
@@ -151,19 +148,10 @@ class EmailTransmissionProvider(TransmissionProvider):
plain_text_only=True,
no_order_links=True,
)
invoice.order.log_action(
'pretix.event.order.email.invoice',
user=None,
auth=None,
data={
'subject': subject,
'message': email_content,
'position': None,
'recipient': recipient,
'invoices': [invoice.pk],
'attach_tickets': False,
'attach_ical': False,
'attach_other_files': [],
'attach_cached_files': [],
}
)
if outgoing_mail:
invoice.order.log_action(
'pretix.event.order.email.invoice',
user=None,
auth=None,
data=outgoing_mail.log_data()
)

View File

@@ -204,6 +204,12 @@ class PeppolTransmissionType(TransmissionType):
}
return base | {"transmission_peppol_participant_id"}
def validate_invoice_address_data(self, address_data: dict):
# Special case Belgium: If a Belgian business ID is used as Peppol ID, it should match the VAT ID
if address_data.get("transmission_peppol_participant_id").startswith("0208:") and address_data.get("vat_id"):
if address_data["vat_id"].removeprefix("BE") != address_data["transmission_peppol_participant_id"].removeprefix("0208:"):
raise ValidationError({"transmission_peppol_participant_id": _("The Peppol participant ID does not match your VAT ID.")})
def pdf_watermark(self) -> str:
return pgettext("peppol_invoice", "Visual copy")

View File

@@ -24,7 +24,7 @@ from typing import Optional
from django.utils.translation import gettext_lazy as _
from django_countries.fields import Country
from pretix.base.models import Invoice, InvoiceAddress
from pretix.base.models import Invoice
from pretix.base.signals import EventPluginRegistry, Registry
@@ -89,7 +89,7 @@ class TransmissionType:
def invoice_address_form_fields_visible(self, country: Country, is_business: bool) -> set:
return set(self.invoice_address_form_fields.keys())
def validate_address(self, ia: InvoiceAddress):
def validate_invoice_address_data(self, address_data: dict):
pass
@property

View File

@@ -220,3 +220,20 @@ class OutgoingMail(models.Model):
error_log_action_type = 'pretix.email.error'
log_target = None
return log_target, error_log_action_type
def log_data(self):
return {
"subject": self.subject,
"message": self.body_plain,
"to": self.to,
"cc": self.cc,
"bcc": self.bcc,
"invoices": [i.pk for i in self.should_attach_invoices.all()],
"attach_tickets": self.should_attach_tickets,
"attach_ical": self.should_attach_ical,
"attach_other_files": self.should_attach_other_files,
"attach_cached_files": [cf.filename for cf in self.should_attach_cached_files.all()],
"position": self.orderposition.positionid if self.orderposition else None,
}

View File

@@ -87,7 +87,6 @@ from pretix.base.timemachine import time_machine_now
from ...helpers import OF_SELF
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import FormattedString, format_map
from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var
from ._transactions import (
@@ -1167,7 +1166,7 @@ class Order(LockModel, LoggedModel):
only be attached for this position and child positions, the link will only point to the
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import mail, render_mail
from pretix.base.services.mail import mail
if not self.email and not (position and position.attendee_email):
return
@@ -1177,32 +1176,20 @@ class Order(LockModel, LoggedModel):
if position and position.attendee_email:
recipient = position.attendee_email
email_content = render_mail(template, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
outgoing_mail = mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
invoices=invoices, attach_tickets=attach_tickets,
position=position, auto_email=auto_email, attach_ical=attach_ical,
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
)
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'position': position.positionid if position else None,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
if outgoing_mail:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
def resend_link(self, user=None, auth=None):
with language(self.locale, self.event.settings.region):
@@ -2900,17 +2887,14 @@ class OrderPosition(AbstractPosition):
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
:param attach_ical: Attach relevant ICS files
"""
from pretix.base.services.mail import mail, render_mail
from pretix.base.services.mail import mail
if not self.attendee_email:
return
with language(self.order.locale, self.order.event.settings.region):
recipient = self.attendee_email
email_content = render_mail(template, context)
if not isinstance(subject, FormattedString):
subject = format_map(subject, context)
mail(
outgoing_mail = mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
@@ -2919,21 +2903,13 @@ class OrderPosition(AbstractPosition):
attach_ical=attach_ical,
attach_other_files=attach_other_files,
)
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'invoices': [i.pk for i in invoices] if invoices else [],
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': attach_other_files,
'attach_cached_files': [],
}
)
if outgoing_mail:
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
def resend_link(self, user=None, auth=None):

View File

@@ -34,10 +34,9 @@ from phonenumber_field.modelfields import PhoneNumberField
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 mail, render_mail
from pretix.base.services.mail import mail
from pretix.helpers import OF_SELF
from ...helpers.format import format_map
from ...helpers.names import build_name
from .base import LoggedModel
from .event import Event, SubEvent
@@ -181,10 +180,11 @@ class WaitingListEntry(LoggedModel):
block_quota=True,
item_id=self.item_id,
subevent_id=self.subevent_id,
waitinglistentries__isnull=False
waitinglistentries__isnull=False,
seat__isnull=True
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
if not free_seats:
if free_seats < 1:
raise WaitingListException(_('No seat with this product is currently available.'))
if '@' not in self.email:
@@ -272,9 +272,7 @@ class WaitingListEntry(LoggedModel):
with language(self.locale, self.event.settings.region):
recipient = self.email
email_content = render_mail(template, context)
subject = format_map(subject, context)
mail(
outgoing_mail = mail(
recipient, subject, template, context,
self.event,
self.locale,
@@ -284,18 +282,13 @@ class WaitingListEntry(LoggedModel):
attach_other_files=attach_other_files,
attach_cached_files=attach_cached_files,
)
self.log_action(
log_entry_type,
user=user,
auth=auth,
data={
'subject': subject,
'message': email_content,
'recipient': recipient,
'attach_other_files': attach_other_files,
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
}
)
if outgoing_mail:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
)
@staticmethod
def clean_itemvar(event, item, variation):

View File

@@ -1295,6 +1295,7 @@ class ManualPayment(BasePaymentProvider):
def format_map(self, order, payment):
return {
# Possible placeholder injection, we should make sure to never include user-controlled variables here
'order': order.code,
'amount': payment.amount,
'currency': self.event.currency,

View File

@@ -45,7 +45,6 @@ from pretix.base.services.tax import split_fee_for_taxes
from pretix.base.templatetags.money import money_filter
from pretix.celery_app import app
from pretix.helpers import OF_SELF
from pretix.helpers.format import format_map
logger = logging.getLogger(__name__)
@@ -55,7 +54,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
mail(
wle.email,
format_map(subject, email_context),
str(subject),
message,
email_context,
wle.event,
@@ -73,9 +72,8 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
order=order, position_or_address=ia, event=order.event)
real_subject = format_map(subject, email_context)
order.send_mail(
real_subject, message, email_context,
subject, message, email_context,
'pretix.event.order.email.event_canceled',
user,
)
@@ -85,14 +83,13 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
real_subject = format_map(subject, email_context)
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
event=order.event,
refund_amount=refund_amount,
position_or_address=p,
order=order, position=p)
order.send_mail(
real_subject, message, email_context,
subject, message, email_context,
'pretix.event.order.email.event_canceled',
position=p,
user=user

View File

@@ -149,13 +149,13 @@ def prefix_subject(settings_holder, subject, highlight=False):
return subject
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString], template: Union[str, LazyI18nString],
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None,
plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None,
sensitive: bool=False):
sensitive: bool=False) -> Optional[OutgoingMail]:
"""
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
@@ -335,14 +335,26 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
should_attach_other_files=attach_other_files or [],
sensitive=sensitive,
)
m._prefetched_objects_cache = {}
if invoices and not position:
m.should_attach_invoices.add(*invoices)
# Hack: For logging, we'll later make a `should_attach_invoices.all()` call. We can prevent a useless
# DB query by filling the cache
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = invoices
else:
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = Invoice.objects.none()
if attach_cached_files:
cf_list = []
for cf in attach_cached_files:
if not isinstance(cf, CachedFile):
m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf))
else:
m.should_attach_cached_files.add(cf)
cf = CachedFile.objects.get(pk=cf)
m.should_attach_cached_files.add(cf)
cf_list.append(cf)
# Hack: For logging, we'll later make a `should_attach_cached_files.all()` call. We can prevent a useless
# DB query by filling the cache
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = cf_list
else:
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = CachedFile.objects.none()
send_task = mail_send_task.si(
outgoing_mail=m.id
@@ -364,6 +376,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
lambda: chain(*task_chain).apply_async()
)
return m
class CustomEmail(EmailMultiAlternatives):
def _create_mime_attachment(self, content, mimetype):

View File

@@ -1799,8 +1799,6 @@ class OrderChangeManager:
tax_rule = tax_rules.get(pos.pk, pos.tax_rule)
if not tax_rule:
continue
if not pos.price:
continue
try:
new_rate = tax_rule.tax_rate_for(ia)
@@ -1817,7 +1815,9 @@ class OrderChangeManager:
override_tax_rate=new_rate, override_tax_code=new_code)
self._totaldiff_guesstimate += new_tax.gross - pos.price
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
self._invoice_dirty = True
if pos.price:
# We do not consider the invoice dirty if only 0€-valued taxes are changed
self._invoice_dirty = True
def cancel_fee(self, fee: OrderFee):
self._totaldiff_guesstimate -= fee.value

View File

@@ -39,7 +39,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
with language(event.settings.locale):
email_context = get_email_context(event=event, name=r.get('name') or '',
voucher_list=[v.code for v in voucher_list])
mail(
outgoing_mail = mail(
r['email'],
subject,
LazyI18nString(message),
@@ -60,8 +60,8 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
data={
'recipient': r['email'],
'name': r.get('name'),
'subject': subject,
'message': message,
'subject': outgoing_mail.subject,
'message': outgoing_mail.body_plain,
},
save=False
))

View File

@@ -363,7 +363,7 @@ class EmailAddressShredder(BaseDataShredder):
le.save(update_fields=['data', 'shredded'])
else:
shred_log_fields(le, banlist=[
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email', 'bcc', 'cc',
])

View File

@@ -12,6 +12,9 @@
<meta charset="utf-8">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %}
{% if css_theme %}
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
{% endif %}
</head>
<body>
<div class="container">

View File

@@ -423,7 +423,7 @@ def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optio
raise ValueError(f"Invalid timeframe '{frame}'")
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[datetime], Optional[datetime]]:
"""
Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes
where the first element ist the first possible datetime within the timeframe and the second

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load urlreplace %}
{% load bootstrap3 %}
{% load static %}
{% block title %}{% trans "Events" %}{% endblock %}
{% block content %}
<h1>{% trans "Events" %}</h1>
@@ -74,6 +75,7 @@
<a href="?{% url_replace request 'ordering' 'organizer' %}"><i class="fa fa-caret-up"></i></a>
</th>
{% endif %}
<th>{% trans "Sales channels" %}</th>
<th>
{% trans "Start date" %}
<a href="?{% url_replace request 'ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
@@ -108,6 +110,21 @@
{% endfor %}
</td>
{% if not hide_orga %}<td>{{ e.organizer }}</td>{% endif %}
<td>
{% for c in e.organizer.sales_channels.all %}
{% if e.all_sales_channels or c in e.limit_sales_channels.all %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% else %}
<span class="fa fa-fw"></span>
{% endif %}
{% endfor %}
</td>
<td class="event-date-col">
{% if e.has_subevents %}
<span class="fa fa-fw- fa-calendar"></span>

View File

@@ -24,7 +24,9 @@
{% if log.display %}
<br/><span class="fa fa-fw fa-comment-o"></span> {{ log.display }}
{% endif %}
{% if log.parsed_data.recipient %}
{% if log.parsed_data.to %}
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.to|join:", " }}
{% elif log.parsed_data.recipient %} {# legacy #}
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.recipient }}
{% endif %}
</p>

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load static %}
{% block inner %}
<h1>
{% blocktrans with name=request.organizer.name %}Organizer: {{ name }}{% endblocktrans %}
@@ -62,6 +63,7 @@
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Sales channels" %}</th>
<th>
{% trans "Start date" %}
/
@@ -77,10 +79,30 @@
<td>
<strong><a
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
<br><small>{{ e.slug }}</small>
{% for k, v in e.meta_data.items %}
{% if v %}
<small class="text-muted">&middot; {{ k }}: {{ v }}</small>
<br>
<small>
{{ e.slug }}
</small>
<small class="text-muted">
{% for k, v in e.meta_data.items %}
{% if v %}
&middot; {{ k }}: {{ v }}
{% endif %}
{% endfor %}
</small>
</td>
<td>
{% for c in sales_channels %}
{% if e.all_sales_channels or c in e.limit_sales_channels.all %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% else %}
<span class="fa fa-fw"></span>
{% endif %}
{% endfor %}
</td>

View File

@@ -264,12 +264,17 @@
The paper size will match the PDF.
{% endblocktrans %}
</p>
<p>
<p class="text-center">
<span class="btn btn-default fileinput-button background-button btn-block">
<i class="fa fa-upload"></i>
<span>{% trans "Upload PDF as background" %}</span>
<input id="fileupload" type="file" name="background" accept="application/pdf">
</span>
<small class="text-muted">
{% blocktrans trimmed with size=maxfilesize|filesizeformat %}
max. {{ size }}, smaller is better
{% endblocktrans %}
</small>
</p>
<p class="text-center">
<a class="btn btn-link background-download-button" href="{{ pdf }}" target="_blank">

View File

@@ -67,7 +67,12 @@ class EventList(PaginationMixin, ListView):
def get_queryset(self):
qs = self.request.user.get_events_with_any_permission(self.request).prefetch_related(
'organizer', '_settings_objects', 'organizer___settings_objects', 'organizer__meta_properties',
'organizer',
'organizer__sales_channels',
'_settings_objects',
'organizer___settings_objects',
'organizer__meta_properties',
'limit_sales_channels',
Prefetch(
'meta_values',
EventMetaValue.objects.select_related('property'),

View File

@@ -2421,9 +2421,9 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
with language(order.locale, self.request.event.settings.region):
email_context = get_email_context(event=order.event, order=order)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
email_subject = format_map(form.cleaned_data['subject'], email_context)
email_content = render_mail(email_template, email_context)
self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(order.event, escape(email_subject), highlight=True)
@@ -2485,9 +2485,9 @@ class OrderPositionSendMail(OrderSendMail):
with language(position.order.locale, self.request.event.settings.region):
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
self.preview_output = {
'subject': mark_safe(_('Subject: {subject}').format(
subject=prefix_subject(position.order.event, escape(email_subject), highlight=True))

View File

@@ -207,6 +207,7 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
'organizer').prefetch_related(
'organizer', '_settings_objects', 'organizer___settings_objects',
'organizer__meta_properties',
'limit_sales_channels',
Prefetch(
'meta_values',
EventMetaValue.objects.select_related('property'),
@@ -237,6 +238,7 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
self.filter_form['meta_{}'.format(p.name)] for p in
self.organizer.meta_properties.filter(filter_allowed=True)
]
ctx['sales_channels'] = self.request.organizer.sales_channels.all()
return ctx

View File

@@ -292,6 +292,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
ctx['layout'] = json.dumps(self.get_current_layout())
ctx['title'] = self.title
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
ctx['maxfilesize'] = self.maxfilesize
return ctx

View File

@@ -131,7 +131,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
elif v.quota:
prod = _('Any product in quota "{quota}"').format(quota=str(v.quota.name))
else:
prod = _('Any product')
prod = ""
row = [
v.code,
v.valid_until.isoformat() if v.valid_until else "",

View File

@@ -280,11 +280,12 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
block_quota=True,
item_id=wle.item_id,
subevent=wle.subevent_id,
waitinglistentries__isnull=False
waitinglistentries__isnull=False,
seat__isnull=True
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
wle.availability = (
Quota.AVAILABILITY_GONE if free_seats == 0 else wle.availability[0],
Quota.AVAILABILITY_GONE if free_seats < 1 else wle.availability[0],
min(free_seats, wle.availability[1]) if wle.availability[1] is not None else free_seats,
)

View File

@@ -4,16 +4,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-02-19 22:00+0000\n"
"PO-Revision-Date: 2026-03-05 20:00+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/da/"
">\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix/"
"da/>\n"
"Language: da\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\n"
"X-Generator: Weblate 5.16.1\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -34285,7 +34285,7 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/voucher.html:293
#, python-format
msgid "minimum amount to order: %(num)s"
msgstr ""
msgstr "Minimumsbestilling: %(num)s"
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:76
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:160

View File

@@ -5,8 +5,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-02-24 12:07+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"PO-Revision-Date: 2026-03-07 23:00+0000\n"
"Last-Translator: argonimos <jonas@pfeiffer-wagner.de>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
"Language: de\n"
@@ -14,7 +14,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\n"
"X-Generator: Weblate 5.16.2\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: pretix/_base_settings.py:87
@@ -3845,7 +3845,7 @@ msgstr "Restbetrag"
#, python-brace-format
msgctxt "invoice"
msgid "Invoice period: {daterange}"
msgstr "Rechungsperiode: {daterange}"
msgstr "Rechnungsperiode: {daterange}"
#: pretix/base/invoicing/pdf.py:1039
msgctxt "invoice"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-02 10:00+0000\n"
"PO-Revision-Date: 2026-03-09 12:52+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.16.1\n"
"X-Generator: Weblate 5.16.2\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -11448,7 +11448,7 @@ msgstr ""
"{event}のご注文が完了しました。無料製品のみのご注文のため、\n"
"お支払いは不要です。\n"
"\n"
"注文の詳細の変更やステータス確認は、以下のURLから行えます\n"
"注文の詳細の変更やステータス確認は、以下のURLから行えます\n"
"{url}\n"
"\n"
"よろしくお願いいたします。\n"
@@ -11750,7 +11750,7 @@ msgstr ""
"{event}のご注文のお支払いを受け取りました。\n"
"\n"
"残念ながら、受け取った金額は必要な全額よりも少ないです。\n"
"したがって、追加の**{pending_sum}**の支払いが不足しているため、\n"
"したがって、追加の **{pending_sum}** の支払いが不足しているため、\n"
"ご注文は未払いと見なされます。\n"
"\n"
"お支払い情報やご注文の状況は、以下のURLでご確認いただけます。\n"
@@ -18428,7 +18428,7 @@ msgid ""
"Do you really want to grant the application <strong>%(application)s</strong> "
"access to your pretix account?"
msgstr ""
"本当にアプリケーション<strong>%(application)s</strong>にPretixアカウントへの"
"本当にアプリケーション<strong>%(application)s</strong>にpretixアカウントへの"
"アクセスを許可しますか?"
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html:24
@@ -24692,7 +24692,7 @@ msgstr "顧客履歴"
#: pretix/control/templates/pretixcontrol/organizers/customer_anonymize.html:11
#, python-format
msgid "Anonymize customer #%(id)s"
msgstr "顧客のID #%(id)s を匿名化"
msgstr "顧客 #%(id)s を匿名化"
#: pretix/control/templates/pretixcontrol/organizers/customer_anonymize.html:16
msgid "Are you sure you want to anonymize this customer account?"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-04 16:57+0000\n"
"PO-Revision-Date: 2026-03-09 12:52+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (Belgium) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_BE/>\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.1\n"
"X-Generator: Weblate 5.16.2\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -26981,6 +26981,10 @@ msgid ""
"the affected data in your legislation, e.g. for reasons of taxation. In many "
"countries, you need to keep some data in the live system in case of an audit."
msgstr ""
"Het is uw eigen verantwoordelijkheid om te controleren of u de gegevens "
"volgens uw wetgeving mag verwijderen, bijvoorbeeld om fiscale redenen. In "
"veel landen moet u bepaalde gegevens in het livesysteem bewaren voor het "
"geval er een audit plaatsvindt."
#: pretix/control/templates/pretixcontrol/shredder/index.html:32
msgid ""
@@ -26988,81 +26992,87 @@ msgid ""
"to store it offline. Some kinds of data (such as some payment information) "
"as well as historical log data cannot be downloaded at the moment."
msgstr ""
"U kunt voor de meeste categorieën de gegevens gedeeltelijk downloaden om ze "
"offline op te slaan. Sommige soorten gegevens (bijvoorbeeld sommige "
"betalingsinformatie) en historische loggegevens kunnen momenteel niet worden "
"gedownload."
#: pretix/control/templates/pretixcontrol/shredder/index.html:46
msgid "Data selection"
msgstr ""
msgstr "Gegevensselectie"
#: pretix/control/templates/pretixcontrol/shredder/index.html:63
msgid ""
"We recommend not to remove this data because you might need it in case of a "
"tax audit."
msgstr ""
"We raden aan om deze gegevens niet te verwijderen, omdat u ze mogelijk nodig "
"hebt bij een belastingaudit."
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:10
msgctxt "subevent"
msgid "Create multiple dates"
msgstr ""
msgstr "Meerdere datums aanmaken"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:35
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:146
msgid "Repetition rule"
msgstr ""
msgstr "Regel voor herhaling"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:81
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:192
#, python-format
msgid "Repeat every %(interval)s %(freq)s, starting at %(start)s."
msgstr ""
msgstr "Herhaal ieder(e) %(interval)s %(freq)s, beginnend op %(start)s."
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:258
msgctxt "subevent"
msgid "Preview"
msgstr ""
msgstr "Voorbeeldweergave"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:265
msgctxt "subevent"
msgid "Times"
msgstr ""
msgstr "Tijden"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:339
msgid "Start of first slot"
msgstr ""
msgstr "Begin van eerste tijdsslot"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:345
msgid "End of time slots"
msgstr ""
msgstr "Einde van tijdsslots"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:351
msgid "Length of slots"
msgstr ""
msgstr "Lengte van tijdsslots"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:360
msgid "Break between slots"
msgstr ""
msgstr "Pauze tussen tijdsslots"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:370
msgid "Create"
msgstr ""
msgstr "Aanmaken"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:377
msgid "Add a single time slot"
msgstr ""
msgstr "Eén tijdsslot toevoegen"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:379
msgid "Add many time slots"
msgstr ""
msgstr "Meerdere tijdsslots toevoegen"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:481
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:266
#: pretix/control/templates/pretixcontrol/subevents/detail.html:124
msgid "Add a new quota"
msgstr ""
msgstr "Nieuw quotum toevoegen"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:485
#: pretix/control/templates/pretixcontrol/subevents/detail.html:128
msgid "Product settings"
msgstr ""
msgstr "Productinstellingen"
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:487
#: pretix/control/templates/pretixcontrol/subevents/detail.html:130
@@ -27070,6 +27080,8 @@ msgid ""
"These settings are optional, if you leave them empty, the default values "
"from the product settings will be used."
msgstr ""
"Deze instellingen zijn optioneel. Als u deze instellingen leeg laat, zullen "
"de standaardwaarden uit de productinstellingen worden gebruikt."
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:523
#: pretix/control/templates/pretixcontrol/subevents/detail.html:166

View File

@@ -38,7 +38,7 @@ var pretixpaypal = {
credit: gettext('PayPal Credit'),
card: gettext('Credit Card'),
paylater: gettext('PayPal Pay Later'),
ideal: gettext('iDEAL'),
ideal: gettext('iDEAL | Wero'),
sepa: gettext('SEPA Direct Debit'),
bancontact: gettext('Bancontact'),
giropay: gettext('giropay'),

View File

@@ -38,13 +38,10 @@ from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, Checkin, Event, InvoiceAddress, Order, User,
)
from pretix.base.models import Checkin, Event, InvoiceAddress, Order, User
from pretix.base.services.mail import mail
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
from pretix.helpers.format import format_map
def _chunks(lst, n):
@@ -64,7 +61,6 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
user = User.objects.get(pk=user) if user else None
subject = LazyI18nString(subject)
message = LazyI18nString(message)
attachments_for_log = [cf.filename for cf in CachedFile.objects.filter(pk__in=attachments)] if attachments else []
def _send_to_order(o):
send_to_order = recipients in ('both', 'orders')
@@ -122,7 +118,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
mail(
outgoing_mail = mail(
p.attendee_email,
subject,
message,
@@ -135,25 +131,17 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
attach_ical=attach_ical,
attach_cached_files=attachments
)
o.log_action(
'pretix.plugins.sendmail.order.email.sent.attendee',
user=user,
data={
'position': p.positionid,
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': p.attendee_email,
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': [],
'attach_cached_files': attachments_for_log,
}
)
if outgoing_mail:
o.log_action(
'pretix.plugins.sendmail.order.email.sent.attendee',
user=user,
data=outgoing_mail.log_data(),
)
if send_to_order and o.email:
with language(o.locale, event.settings.region):
email_context = get_email_context(event=event, order=o, invoice_address=ia)
mail(
outgoing_mail = mail(
o.email,
subject,
message,
@@ -165,19 +153,12 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
attach_ical=attach_ical,
attach_cached_files=attachments,
)
o.log_action(
'pretix.plugins.sendmail.order.email.sent',
user=user,
data={
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': o.email,
'attach_tickets': attach_tickets,
'attach_ical': attach_ical,
'attach_other_files': [],
'attach_cached_files': attachments_for_log,
}
)
if outgoing_mail:
o.log_action(
'pretix.plugins.sendmail.order.email.sent',
user=user,
data=outgoing_mail.log_data(),
)
for chunk in _chunks(objects, 1000):
orders = Order.objects.filter(pk__in=chunk, event=event)

View File

@@ -92,70 +92,89 @@ logger = logging.getLogger('pretix.plugins.stripe')
# State of the payment methods
#
# Source: https://stripe.com/docs/payments/payment-methods/overview
# Last Update: 2023-12-20
# Last Update: 2026-06-12
#
# pretix Staff: Do not forget to enable/"On by default" newly added payment methods in
# Stripe's managed payment methods configuration for the Stripe/pretix.eu connect platform.
#
# The categorization and order of payment methods is based on the list of the managed payment method configration
# in the Stripe Dashboard.
# Cards
# - Credit and Debit Cards: ✓
# - Apple, Google Pay: ✓
# * Cartes Bancaires: ✓
# * Korean cards: ✓
# * Japan installments: ✗
# * JCB: ✓
# * Meses sin intereses: ✗
#
# Bank debits
# - ACH Debit: ✗
# - Canadian PADs: ✗
# - BACS Direct Debit: ✗
# - SEPA Direct Debit: ✓
# - BECS Direct Debit: ✗
# Wallets
# - Apple: ✓ (Cards)
# - Google Pay: ✓ (Cards)
# - Link: ✓ (PaymentRequestButton/Cards)
# - Alipay: ✓
# - Stablecoins and Crypto: ✗
# - Kakao Pay: ✗
# - Naver Pay: ✗
# - MB Way: ✗
# - Satis Pay: ✗
# - WeChat Pay: ✓
# - PAYCO: ✗
# - PayPal: ✓ (No settings UI yet; incompatible with Connect+Direct Charges)
# - Samsung pay: ✗
# - MobilePay: ✓
# - Revolut Pay: ✓
# - Amazon Pay: ✗
# - PayPay: ✗
# - GrabPay: ✗
# - Cash App Pay: ✗
# - Secure Remote Commerce: ✗
#
# Bank redirects
# - Bancontact: ✓
# - BLIK: ✗
# - EPS: ✓
# - giropay: (deprecated)
# - iDEAL: ✓
# - P24: ✓
# - Sofort: (deprecated)
# - FPX:
# - PayNow: ✗
# - UPI: ✗
# - Netbanking: ✗
# - Przelewy24: ✓
# - BLIK: ✗
# - Pay By Bank:
# - TWINT: ✓
# - Wero: ✓ (No settings UI yet)
#
# Bank transfers
# - ACH Bank Transfer: ✗
# - SEPA Bank Transfer: ✗
# - UK Bank Transfer: ✗
# - Multibanco: ✗
# - Furikomi (Japan): ✗
# - Mexico Bank Transfer: ✗
# - giropay: ✗ (deprecated)
# - Sofort: ✗ (deprecated)
#
# Buy now, pay later
# - Affirm:
# - Afterpay/Clearpay: ✗
# - Billie:
# - Klarna: ✓
# - Afterpay/Clearpay: ✗
# - Zip: ✗
# - Alma: ✗
# - Affirm: ✓
#
# Bank debits
# - SEPA Direct Debit: ✓
# - ACH Direct Debit: ✗
# - Australian BECS Direct Debit: ✗
# - Canadian pre-authorized debits: ✗
# - BACS Direct Debit: ✗
# - FPX: ✗
# - NZ BECS Direct Debit: ✗
#
# Bank transfers
# - Bank Transfer: ✗
#
# Vouchers
# - Multibanco: ✓
# - Boleto: ✗
# - Konbini: ✗
# - OXXO: ✗
#
# Real-time payments
# - Swish: ✓
# - UPI: ✗
# - Pix: ✗
# - PayTo: ✗
# - PayNow: ✗
# - PromptPay: ✓
# - Pix: ✗
#
# Vouchers
# - Konbini: ✗
# - OXXO: ✗
# - Boleto: ✗
#
# Wallets
# - Apple Pay: ✓ (Cards)
# - Google Pay: ✓ (Cards)
# - Secure Remote Commerce: ✗
# - Link: ✓ (PaymentRequestButton)
# - Cash App Pay: ✗
# - PayPal: ✓ (No settings UI yet)
# - MobilePay: ✓
# - Alipay: ✓
# - WeChat Pay: ✓
# - GrabPay: ✓
class StripeSettingsHolder(BasePaymentProvider):
@@ -1550,7 +1569,7 @@ class StripeGiropay(StripeRedirectWithAccountNamePaymentIntentMethod):
class StripeIdeal(StripeRedirectMethod):
identifier = 'stripe_ideal'
verbose_name = _('iDEAL via Stripe')
public_name = _('iDEAL')
public_name = _('iDEAL | Wero')
method = 'ideal'
explanation = _(
'iDEAL is an online payment method available to customers of Dutch banks. Please keep your online '

View File

@@ -13,7 +13,7 @@
{% csrf_token %}
<div class="panel-group" id="questions_accordion">
{% if invoice_address_asked or event.settings.invoice_name_required %}
{% if invoice_address_asked and not request.GET.generate_invoice == "true" and not event.settings.invoice_reissue_after_modify %}
{% if invoice_address_asked and not request.GET.generate_invoice == "true" and not invoice_generation_selfservice %}
<div class="alert alert-info">
{% blocktrans trimmed %}
Modifying your invoice address will not automatically generate a new invoice.

View File

@@ -909,6 +909,21 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(
**kwargs,
)
ctx['invoice_generation_selfservice'] = (
self.request.event.settings.invoice_reissue_after_modify or
(
can_generate_invoice(self.request.event, self.order, ignore_payments=True) and
not self.order.invoices.exists()
)
)
return ctx
def dispatch(self, request, *args, **kwargs):
self.request = request
self.kwargs = kwargs

View File

@@ -173,8 +173,8 @@ $(function () {
if (!dependents.transmission_peppol_participant_id.val()) {
const fill_peppol_id = function () {
const vatId = dependents.vat_id.val();
if (vatId && vatId.startsWith("BE") && dependents.transmission_type.val() === "peppol" && autofill_peppol_id) {
dependents.transmission_peppol_participant_id.val("0201:" + vatId.substring(2))
if (vatId && vatId.startsWith("BE") && dependents.transmission_type.val() === "peppol") {
dependents.transmission_peppol_participant_id.val("0208:" + vatId.substring(2))
}
}
dependents.vat_id.add(dependents.transmission_type).on("change", fill_peppol_id);

View File

@@ -29,6 +29,8 @@ from pretix.base.models import (
Event, Item, ItemVariation, Organizer, Quota, Team, User, Voucher,
WaitingListEntry,
)
from pretix.base.models.seating import Seat, SeatingPlan
from pretix.base.models.waitinglist import WaitingListException
from pretix.control.views.dashboards import waitinglist_widgets
@@ -55,11 +57,11 @@ def env():
WaitingListEntry.objects.create(
event=event, item=item1, email='success@example.org', voucher=v
)
v = Voucher.objects.create(item=item1, event=event, block_quota=True, redeemed=0, valid_until=now() - timedelta(days=5))
v = Voucher.objects.create(item=item2, event=event, block_quota=True, redeemed=0, valid_until=now() - timedelta(days=5))
WaitingListEntry.objects.create(
event=event, item=item2, email='expired@example.org', voucher=v
)
v = Voucher.objects.create(item=item1, event=event, block_quota=True, redeemed=0, valid_until=now() + timedelta(days=5))
v = Voucher.objects.create(item=item2, event=event, block_quota=True, redeemed=0, valid_until=now() + timedelta(days=5))
WaitingListEntry.objects.create(
event=event, item=item2, email='valid@example.org', voucher=v
)
@@ -345,5 +347,75 @@ def test_dashboard(client, env):
quota.items.add(env['item1'])
w = waitinglist_widgets(env['event'])
assert '1' in w[0]['content']
assert '2' in w[0]['content']
assert '5' in w[1]['content']
@pytest.mark.django_db
def test_waitinglist_seat_calc(client, env):
item = env['item1']
event = env['event']
wle = env['wle']
SeatingPlan.objects.create(
name="Plan", organizer=event.organizer, layout="{}"
)
event.seat_category_mappings.create(
layout_category='Stalls', product=item
)
for i in range(2):
event.seats.create(seat_number=f"A{i}", product=item, seat_guid=f"A{i}")
quota = Quota.objects.create(event=event, size=10)
quota.items.add(item)
client.login(email='dummy@dummy.dummy', password='dummy')
# Calculated availability should not be more than number of available seats
response = client.get('/control/event/dummy/dummy/waitinglist/')
assert len(response.context['entries']) == 5
for entry in response.context['entries']:
assert entry.availability == (Quota.AVAILABILITY_OK, 2)
# Sending out a voucher reduces availability by 1
with scopes_disabled():
wle.send_voucher()
voucher = wle.voucher
assert voucher
response = client.get('/control/event/dummy/dummy/waitinglist/')
assert len(response.context['entries']) == 4
for entry in response.context['entries']:
assert entry.availability == (Quota.AVAILABILITY_OK, 1)
# Assigning a seat to a voucher does not decrease availability further
with scopes_disabled():
voucher.seat = Seat.objects.get(seat_guid="A0")
voucher.save()
response = client.get('/control/event/dummy/dummy/waitinglist/')
assert len(response.context['entries']) == 4
for entry in response.context['entries']:
assert entry.availability == (Quota.AVAILABILITY_OK, 1)
with scopes_disabled():
wle2 = WaitingListEntry.objects.filter(item=item, voucher__isnull=True).first()
wle2.send_voucher()
# Overbooking is handled correctly
# Regression test for calculation that used `not free_seats` instead of `free_seats < 1`
with scopes_disabled():
# Block seat
seat = Seat.objects.get(seat_guid="A1")
seat.blocked = True
seat.save()
response = client.get('/control/event/dummy/dummy/waitinglist/')
assert len(response.context['entries']) == 3
for entry in response.context['entries']:
assert entry.availability == (Quota.AVAILABILITY_GONE, -1)
with scopes_disabled(), pytest.raises(WaitingListException):
wle3 = WaitingListEntry.objects.filter(item=item, voucher__isnull=True).first()
wle3.send_voucher()