Compare commits

..

7 Commits

Author SHA1 Message Date
Mira Weller
833966a4cd Use unique column names in order position export for invoice vs. attendee company name (Z#23215261) 2025-11-18 11:08:22 +01:00
Raphael Michel
d8d56ff020 Disable switching currency when orders exist (fixes #2047) 2025-11-17 17:09:17 +01:00
Raphael Michel
44b3647689 Accounting report: Allow subclasses to skip tables (#5616) 2025-11-17 17:09:06 +01:00
Richard Schreiber
818bb76e89 Fix calendar before-date to check for events (#5608) 2025-11-17 16:39:20 +01:00
Raphael Michel
8c01cad06b Stripe: Use unified wording for redirect announcement (#5613) 2025-11-17 16:20:53 +01:00
Raphael Michel
86ca7c4440 Order page: Do not show download deadline if download is disabled (fixes #3144) (#5630) 2025-11-17 15:42:20 +01:00
Richard Schreiber
d7b6856322 Fix not allowing program times on event series (API/copy) (#5595)
* Fix not allowing program times on event series (API/copy)

* Return 400 when reading endpoint in event series

* add docs program times not available on event series

* fix isort
2025-11-17 15:36:53 +01:00
18 changed files with 90 additions and 79 deletions

View File

@@ -5,6 +5,7 @@ Resource description
--------------------
Program times for products (items) that can be set in addition to event times, e.g. to display seperate schedules within an event.
Note that ``program_times`` are not available for items inside event series.
The program times resource contains the following public fields:
.. rst-class:: rest-resource-table

View File

@@ -142,6 +142,7 @@ variations list of objects A list with o
program_times list of objects A list with one object for each program time of this item.
Can be empty. Only writable during creation,
use separate endpoint to modify this later.
Not available for items in event series.
├ id integer Internal ID of the variation
├ value multi-lingual string The "name" of the variation
├ default_price money (string) The price set directly for this variation or ``null``
@@ -243,6 +244,8 @@ Also note that ``variations``, ``bundles``, ``addons`` and ``program_times`` ar
bundles, add-ons and program times please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
with nested ``variations``, ``bundles``, ``addons`` and/or ``program_times``.
``program_times`` is not available to items in event series.
Endpoints
---------

View File

@@ -241,6 +241,12 @@ class ItemProgramTimeSerializer(serializers.ModelSerializer):
if start > end:
raise ValidationError(_("The program end must not be before the program start."))
event = self.context['event']
if event.has_subevents:
raise ValidationError({
_("You cannot use program times on an event series.")
})
return data

View File

@@ -40,7 +40,7 @@ from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.response import Response
from pretix.api.pagination import TotalOrderingFilter
@@ -293,6 +293,8 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
def get_queryset(self):
if self.request.event.has_subevents:
raise ValidationError('You cannot use program times on an event series.')
return self.item.program_times.all()
def get_serializer_context(self):

View File

@@ -610,7 +610,7 @@ class OrderListExporter(MultiSheetListExporter):
headers.append(_('Attendee name') + ': ' + str(label))
headers += [
_('Attendee email'),
_('Company'),
_('Attendee company'),
_('Address'),
_('ZIP code'),
_('City'),
@@ -650,7 +650,7 @@ class OrderListExporter(MultiSheetListExporter):
options[q.pk].append(o)
headers.append(str(q.question))
headers += [
_('Company'),
_('Invoice address company'),
_('Invoice address name'),
]
if name_scheme and len(name_scheme['fields']) > 1:

View File

@@ -990,10 +990,11 @@ class Event(EventMixin, LoggedModel):
ia.bundled_variation = variation_map[ia.bundled_variation.pk]
ia.save(force_insert=True)
for ipt in ItemProgramTime.objects.filter(item__event=other).prefetch_related('item'):
ipt.pk = None
ipt.item = item_map[ipt.item.pk]
ipt.save(force_insert=True)
if not self.has_subevents and not other.has_subevents:
for ipt in ItemProgramTime.objects.filter(item__event=other).prefetch_related('item'):
ipt.pk = None
ipt.item = item_map[ipt.item.pk]
ipt.save(force_insert=True)
quota_map = {}
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):

View File

@@ -2311,6 +2311,8 @@ class ItemProgramTime(models.Model):
end = models.DateTimeField(verbose_name=_("End"))
def clean(self):
if self.item.event.has_subevents:
raise ValidationError(_("You cannot use program times on an event series."))
self.clean_start_end(start=self.start, end=self.end)
super().clean()

View File

@@ -374,6 +374,13 @@ class EventUpdateForm(I18nModelForm):
super().__init__(*args, **kwargs)
if not self.change_slug:
self.fields['slug'].widget.attrs['readonly'] = 'readonly'
if self.instance.orders.exists():
self.fields['currency'].disabled = True
self.fields['currency'].help_text = _(
'The currency cannot be changed because orders already exist.'
)
self.fields['location'].widget.attrs['rows'] = '3'
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center\nHeidelberg, Germany'
@@ -1860,11 +1867,7 @@ class QuickSetupForm(I18nForm):
self.fields['payment_banktransfer_bank_details'].required = False
for f in self.fields.values():
if 'data-required-if' in f.widget.attrs:
f.widget.attrs['data-required-if'] += ",#id_payment_banktransfer__enabled"
self.fields['payment_banktransfer_bank_details'].widget.attrs["data-required-if"] = (
"#id_payment_banktransfer_bank_details_type_1,#id_payment_banktransfer__enabled"
)
del f.widget.attrs['data-required-if']
def clean(self):
cleaned_data = super().clean()

View File

@@ -858,50 +858,58 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
for c in currencies:
c_head = f" [{c}]" if len(currencies) > 1 else ""
story += [
Spacer(0, 3 * mm),
FontFallbackParagraph(_("Orders") + c_head, style_h2),
Spacer(0, 3 * mm),
*self._table_transactions(form_data, c),
]
s = self._table_transactions(form_data, c)
if s:
story += [
Spacer(0, 3 * mm),
FontFallbackParagraph(_("Orders") + c_head, style_h2),
Spacer(0, 3 * mm),
*s
]
for c in currencies:
c_head = f" [{c}]" if len(currencies) > 1 else ""
story += [
Spacer(0, 8 * mm),
FontFallbackParagraph(_("Payments") + c_head, style_h2),
Spacer(0, 3 * mm),
*self._table_payments(form_data, c),
]
s = self._table_payments(form_data, c)
if s:
story += [
Spacer(0, 8 * mm),
FontFallbackParagraph(_("Payments") + c_head, style_h2),
Spacer(0, 3 * mm),
*s
]
for c in currencies:
c_head = f" [{c}]" if len(currencies) > 1 else ""
story += [
Spacer(0, 8 * mm),
KeepTogether(
[
FontFallbackParagraph(_("Open items") + c_head, style_h2),
Spacer(0, 3 * mm),
*self._table_open_items(form_data, c),
]
),
]
s = self._table_open_items(form_data, c)
if s:
story += [
Spacer(0, 8 * mm),
KeepTogether(
[
FontFallbackParagraph(_("Open items") + c_head, style_h2),
Spacer(0, 3 * mm),
*s
]
),
]
if (
self.is_multievent
and self.events.count() == self.organizer.events.count()
):
for c in currencies:
c_head = f" [{c}]" if len(currencies) > 1 else ""
story += [
Spacer(0, 8 * mm),
KeepTogether(
[
FontFallbackParagraph(_("Gift cards") + c_head, style_h2),
Spacer(0, 3 * mm),
*self._table_gift_cards(form_data, c),
]
),
]
s = self._table_gift_cards(form_data, c)
if s:
story += [
Spacer(0, 8 * mm),
KeepTogether(
[
FontFallbackParagraph(_("Gift cards") + c_head, style_h2),
Spacer(0, 3 * mm),
*s,
]
),
]
doc.build(story, canvasmaker=self.canvas_class(doc))
f.seek(0)

View File

@@ -23,7 +23,7 @@
{% else %}
<p>{% blocktrans trimmed %}
After you submitted your order, we will redirect you to the payment service provider to complete your payment.
You will then be redirected back here to get your tickets.
You will then be redirected back here.
{% endblocktrans %}</p>
<dl class="dl-horizontal">
<dt>{% trans "Payment method" %}</dt>

View File

@@ -6,5 +6,5 @@
{% bootstrap_form form layout='horizontal' %}
<p class="help-block">{% blocktrans trimmed %}
After you submitted your order, we will redirect you to the payment service provider to complete your payment.
You will then be redirected back here to get your tickets.
You will then be redirected back here.
{% endblocktrans %}</p>

View File

@@ -11,8 +11,8 @@
<p class="help-block">
{% blocktrans trimmed %}
After you submitted your order, we will redirect you to the payment service provider to complete your
payment. You will then be redirected back here to get your tickets.
After you submitted your order, we will redirect you to the payment service provider to complete your payment.
You will then be redirected back here.
{% endblocktrans %}
</p>

View File

@@ -4,5 +4,5 @@
{% endif %}
<p class="help-block">{% blocktrans trimmed %}
After you submitted your order, we will redirect you to the payment service provider to complete your payment.
You will then be redirected back here to get your tickets.
You will then be redirected back here.
{% endblocktrans %}</p>

View File

@@ -88,7 +88,7 @@
</p>
{% endif %}
</div>
{% elif not can_download and plugins_allow_ticket_download and ticket_download_date %}
{% elif not can_download and plugins_allow_ticket_download and ticket_download_date and event.settings.ticket_download %}
{% if order.status == 'p' %}
<div class="alert alert-info info-download">
{% blocktrans trimmed with date=ticket_download_date|date:"SHORT_DATE_FORMAT" %}

View File

@@ -689,7 +689,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
self._set_month_year()
tz = self.request.event.timezone
_, ndays = calendar.monthrange(self.year, self.month)
before = datetime(self.year, self.month, 1, 0, 0, 0, tzinfo=tz) - timedelta(days=1)
before = datetime(self.year, self.month, 1, 23, 59, 59, tzinfo=tz) - timedelta(days=1)
after = datetime(self.year, self.month, ndays, 0, 0, 0, tzinfo=tz) + timedelta(days=1)
if self.request.event.settings.event_calendar_future_only:
@@ -750,7 +750,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
tz = self.request.event.timezone
week = isoweek.Week(self.year, self.week)
before = datetime(
week.monday().year, week.monday().month, week.monday().day, 0, 0, 0, tzinfo=tz
week.monday().year, week.monday().month, week.monday().day, 23, 59, 59, tzinfo=tz
) - timedelta(days=1)
after = datetime(
week.sunday().year, week.sunday().month, week.sunday().day, 0, 0, 0, tzinfo=tz

View File

@@ -768,7 +768,7 @@ class CalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
raise Http404()
tz = get_current_timezone()
before = datetime(self.year, self.month, 1, 0, 0, 0, tzinfo=tz) - timedelta(days=1)
before = datetime(self.year, self.month, 1, 23, 59, 59, tzinfo=tz) - timedelta(days=1)
after = datetime(self.year, self.month, ndays, 0, 0, 0, tzinfo=tz) + timedelta(days=1)
ctx['date'] = date(self.year, self.month, 1)
@@ -854,7 +854,7 @@ class WeekCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
tz = get_current_timezone()
week = isoweek.Week(self.year, self.week)
before = datetime(
week.monday().year, week.monday().month, week.monday().day, 0, 0, 0, tzinfo=tz,
week.monday().year, week.monday().month, week.monday().day, 23, 59, 59, tzinfo=tz,
) - timedelta(days=1)
after = datetime(
week.sunday().year, week.sunday().month, week.sunday().day, 0, 0, 0, tzinfo=tz,
@@ -1001,7 +1001,7 @@ class DayCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
tz = get_current_timezone()
before = datetime(
self.date.year, self.date.month, self.date.day, 0, 0, 0, tzinfo=tz,
self.date.year, self.date.month, self.date.day, 23, 59, 59, tzinfo=tz,
) - timedelta(days=1)
after = datetime(
self.date.year, self.date.month, self.date.day, 0, 0, 0, tzinfo=tz,

View File

@@ -415,24 +415,16 @@ var form_handlers = function (el) {
el.find("input[data-required-if], select[data-required-if], textarea[data-required-if]").each(function () {
var dependent = $(this),
dependencies = $($(this).attr("data-required-if")),
dependency = $($(this).attr("data-required-if")),
update = function (ev) {
var enabled = true;
dependencies.each(function () {
var dependency = $(this);
var e = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val();
enabled = enabled && e;
});
var enabled = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val();
dependent.prop('required', enabled).closest('.form-group').toggleClass('required', enabled).find('.optional').stop().animate({
'opacity': enabled ? 0 : 1
}, ev ? 500 : 1);
};
update();
dependencies.each(function () {
var dependency = $(this);
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
});
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
});
el.find("div.scrolling-choice:not(.no-search)").each(function () {

View File

@@ -514,17 +514,13 @@ $(function () {
$("input[data-required-if], select[data-required-if], textarea[data-required-if]").each(function () {
var dependent = $(this),
dependentLabel = $("label[for="+this.id+"]"),
dependencies = $($(this).attr("data-required-if")),
dependency = $($(this).attr("data-required-if")),
update = function (ev) {
var enabled = true;
dependencies.each(function () {
var dependency = $(this);
var e = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val();
enabled = enabled && e;
});
var enabled = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val();
if (!dependent.is("[data-no-required-attr]")) {
dependent.prop('required', enabled);
}
dependent.closest('.form-group').toggleClass('required', enabled);
if (enabled) {
dependentLabel.append('<i class="label-required">' + gettext('required') + '</i>');
}
@@ -533,11 +529,8 @@ $(function () {
}
};
update();
dependencies.each(function () {
var dependency = $(this);
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
});
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("change", update);
dependency.closest('.form-group').find('input[name=' + dependency.attr("name") + ']').on("dp.change", update);
});
$("input[data-display-dependency], div[data-display-dependency], select[data-display-dependency], textarea[data-display-dependency]").each(function () {