diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py
index b6e970073..3ba6a2722 100644
--- a/src/pretix/api/serializers/event.py
+++ b/src/pretix/api/serializers/event.py
@@ -684,8 +684,9 @@ class EventSettingsSerializer(SettingsSerializer):
'locales',
'locale',
'region',
- 'last_order_modification_date',
+ 'allow_modifications',
'allow_modifications_after_checkin',
+ 'last_order_modification_date',
'show_quota_left',
'waiting_list_enabled',
'waiting_list_auto_disable',
diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py
index e08b1d63d..ed014b264 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -850,6 +850,9 @@ class Order(LockModel, LoggedModel):
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
return False
+ if self.event.settings.allow_modifications not in ("order", "attendee"):
+ return False
+
modify_deadline = self.modify_deadline
if modify_deadline is not None and now() > modify_deadline:
return False
@@ -2513,6 +2516,43 @@ class OrderPosition(AbstractPosition):
reasons[b] = b
return reasons
+ @property
+ def can_modify_answers(self) -> bool:
+ """
+ ``True`` if the user can change the question answers / attendee names that are
+ related to the position. This checks order status and modification deadlines. It also
+ returns ``False`` if there are no questions that can be answered.
+ """
+ from .checkin import Checkin
+
+ if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
+ return False
+
+ if self.event.settings.allow_modifications != "attendee":
+ return False
+
+ modify_deadline = self.order.modify_deadline
+ if modify_deadline is not None and now() > modify_deadline:
+ return False
+
+ positions = list(
+ self.order.positions.all().annotate(
+ has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
+ ).select_related('item').prefetch_related('item__questions')
+ )
+ if not self.event.settings.allow_modifications_after_checkin:
+ for cp in positions:
+ if cp.has_checkin:
+ return False
+
+ ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
+ for cp in positions:
+ if cp.pk == self.pk or cp.addon_to_id == self.pk:
+ if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
+ return True
+
+ return False # nothing there to modify
+
@classmethod
def transform_cart_positions(cls, cp: List, order) -> list:
from . import Voucher
diff --git a/src/pretix/base/services/placeholders.py b/src/pretix/base/services/placeholders.py
index 60acbdcad..8ab1a42e6 100644
--- a/src/pretix/base/services/placeholders.py
+++ b/src/pretix/base/services/placeholders.py
@@ -337,6 +337,40 @@ def base_placeholders(sender, **kwargs):
}
),
),
+ SimpleFunctionalTextPlaceholder(
+ 'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
+ event,
+ 'presale:event.order.position.modify', kwargs={
+ 'order': position.order.code,
+ 'secret': position.web_secret,
+ 'position': position.positionid
+ }
+ ), lambda event: build_absolute_uri(
+ event,
+ 'presale:event.order.position.modify', kwargs={
+ 'order': 'F8VVL',
+ 'secret': '6zzjnumtsx136ddy',
+ 'position': '123',
+ }
+ ),
+ ),
+ SimpleFunctionalTextPlaceholder(
+ 'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
+ event,
+ 'presale:event.order.position.change', kwargs={
+ 'order': position.order.code,
+ 'secret': position.web_secret,
+ 'position': position.positionid
+ }
+ ), lambda event: build_absolute_uri(
+ event,
+ 'presale:event.order.position.change', kwargs={
+ 'order': 'F8VVL',
+ 'secret': '6zzjnumtsx136ddy',
+ 'position': '123'
+ }
+ ),
+ ),
SimpleFunctionalTextPlaceholder(
'order_modification_deadline_date_and_time', ['order', 'event'],
lambda order, event:
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index 5665f7377..2217ad104 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -1653,6 +1653,28 @@ DEFAULTS = {
"calendar.")
)
},
+ 'allow_modifications': {
+ 'default': 'order',
+ 'type': str,
+ 'form_class': forms.ChoiceField,
+ 'serializer_class': serializers.ChoiceField,
+ 'serializer_kwargs': dict(
+ choices=(
+ ('no', _('No modifications after order was submitted')),
+ ('order', _('Only the person who ordered can make changes')),
+ ('attendee', _('Both the attendee and the person who ordered can make changes')),
+ )
+ ),
+ 'form_kwargs': dict(
+ label=_("Allow customers to modify their information"),
+ widget=forms.RadioSelect,
+ choices=(
+ ('no', _('No modifications after order was submitted')),
+ ('order', _('Only the person who ordered can make changes')),
+ ('attendee', _('Both the attendee and the person who ordered can make changes')),
+ )
+ ),
+ },
'allow_modifications_after_checkin': {
'default': 'False',
'type': bool,
@@ -1660,6 +1682,8 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Allow customers to modify their information after they checked in."),
+ help_text=_("By default, no more modifications are possible for an order as soon as one of the tickets "
+ "in the order has been checked in.")
)
},
'last_order_modification_date': {
diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py
index c9a0ec925..a0ca8d304 100644
--- a/src/pretix/control/forms/event.py
+++ b/src/pretix/control/forms/event.py
@@ -580,6 +580,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'banner_text',
'banner_text_bottom',
'order_email_asked_twice',
+ 'allow_modifications',
'last_order_modification_date',
'allow_modifications_after_checkin',
'checkout_show_copy_answers_button',
diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html
index 50f6c438b..db89dc996 100644
--- a/src/pretix/control/templates/pretixcontrol/event/settings.html
+++ b/src/pretix/control/templates/pretixcontrol/event/settings.html
@@ -113,10 +113,17 @@
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
-
{% trans "Other settings" %}
+ {% trans "Form settings" %}
{% bootstrap_field sform.name_scheme layout="control" %}
{% bootstrap_field sform.name_scheme_titles layout="control" %}
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
+
+ {% trans "Changes to existing orders" %}
+ {% bootstrap_field sform.allow_modifications layout="control" %}
+
+ {% bootstrap_field sform.last_order_modification_date layout="control" %}
+ {% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
+