From 423f0cbb9071d5fae4dd26eaa35cb4eb69d40eb3 Mon Sep 17 00:00:00 2001
From: Raphael Michel
Date: Fri, 18 Jul 2025 10:02:18 +0200
Subject: [PATCH] Add button to reset entire check-in stack (Z#23188730)
(#5312)
* Show print logs to admins
* Add button to reset entire check-in stack (Z#23188730)
* isort
* Update src/pretix/control/templates/pretixcontrol/checkin/reset.html
Co-authored-by: Richard Schreiber
* Update src/pretix/control/templates/pretixcontrol/checkin/reset.html
Co-authored-by: Richard Schreiber
* Update src/pretix/control/templates/pretixcontrol/checkin/reset.html
Co-authored-by: Richard Schreiber
* Update src/pretix/control/templates/pretixcontrol/checkin/lists.html
Co-authored-by: Richard Schreiber
---------
Co-authored-by: Richard Schreiber
---
src/pretix/control/forms/checkin.py | 6 ++
src/pretix/control/logdisplay.py | 1 +
.../pretixcontrol/checkin/lists.html | 7 +++
.../pretixcontrol/checkin/reset.html | 50 ++++++++++++++++
.../templates/pretixcontrol/order/index.html | 12 ++++
src/pretix/control/urls.py | 1 +
src/pretix/control/views/checkin.py | 59 ++++++++++++++++++-
src/pretix/control/views/orders.py | 2 +
.../static/pretixcontrol/scss/main.scss | 4 ++
9 files changed, 139 insertions(+), 3 deletions(-)
create mode 100644 src/pretix/control/templates/pretixcontrol/checkin/reset.html
diff --git a/src/pretix/control/forms/checkin.py b/src/pretix/control/forms/checkin.py
index 207a3d78c5..32d4021e3a 100644
--- a/src/pretix/control/forms/checkin.py
+++ b/src/pretix/control/forms/checkin.py
@@ -215,3 +215,9 @@ class CheckinListSimulatorForm(forms.Form):
)
self.fields['gate'].widget.choices = self.fields['gate'].choices
self.fields['gate'].label = _('Gate')
+
+
+class CheckinResetForm(forms.Form):
+ ok = forms.BooleanField(
+ label=_("I am sure that the check-in state of the entire event should be reset.")
+ )
diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py
index 58001cb368..11f1321abe 100644
--- a/src/pretix/control/logdisplay.py
+++ b/src/pretix/control/logdisplay.py
@@ -722,6 +722,7 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.giftcards.transaction.manual': _('A manual transaction has been performed.'),
'pretix.team.token.created': _('The token "{name}" has been created.'),
'pretix.team.token.deleted': _('The token "{name}" has been revoked.'),
+ 'pretix.event.checkin.reset': _('The check-in and print log state has been reset.')
})
class CoreLogEntryType(LogEntryType):
pass
diff --git a/src/pretix/control/templates/pretixcontrol/checkin/lists.html b/src/pretix/control/templates/pretixcontrol/checkin/lists.html
index 4a96b051ff..220458244c 100644
--- a/src/pretix/control/templates/pretixcontrol/checkin/lists.html
+++ b/src/pretix/control/templates/pretixcontrol/checkin/lists.html
@@ -83,6 +83,13 @@
{% trans "Connected devices" %}
{% endif %}
+ {% if "can_change_orders" in request.eventpermset %}
+
+
+ {% trans "Reset check-in" %}
+
+ {% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/checkin/reset.html b/src/pretix/control/templates/pretixcontrol/checkin/reset.html
new file mode 100644
index 0000000000..7ced9ce74d
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/checkin/reset.html
@@ -0,0 +1,50 @@
+{% extends "pretixcontrol/items/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block title %}{% trans "Reset check-in" %}{% endblock %}
+{% block inside %}
+ {% trans "Reset check-in" %}
+
+{% endblock %}
diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html
index 972b34e7bd..5f6dbe02d1 100644
--- a/src/pretix/control/templates/pretixcontrol/order/index.html
+++ b/src/pretix/control/templates/pretixcontrol/order/index.html
@@ -500,6 +500,18 @@
{% eventsignal event "pretix.control.signals.order_position_buttons" order=order position=line request=request %}
{% endif %}
+ {% if staff_session %}
+
+ {% for pl in line.print_logs.all %}
+
+ {{ pl.datetime|date:"SHORT_DATETIME_FORMAT" }}
+ {{ pl.get_type_display }}
+ ({{ pl.source }}{% if pl.device %}, #{{ pl.device.device_id }}{% endif %})
+ {% if not pl.successful %}{% endif %}
+
+ {% endfor %}
+
+ {% endif %}
{% if line.issued_gift_cards %}
{% for gc in line.issued_gift_cards.all %}
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py
index b7a0d0a753..cd6d39a3a3 100644
--- a/src/pretix/control/urls.py
+++ b/src/pretix/control/urls.py
@@ -464,6 +464,7 @@ urlpatterns = [
re_path(r'^checkins/$', checkin.CheckinListView.as_view(), name='event.orders.checkins'),
re_path(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
+ re_path(r'^checkinlists/reset$', checkin.CheckInResetView.as_view(), name='event.orders.checkinlists.reset'),
re_path(r'^checkinlists/select2$', typeahead.checkinlist_select2, name='event.orders.checkinlists.select2'),
re_path(r'^checkinlists/(?P\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
re_path(r'^checkinlists/(?P\d+)/simulator$', checkin.CheckInListSimulator.as_view(), name='event.orders.checkinlists.simulator'),
diff --git a/src/pretix/control/views/checkin.py b/src/pretix/control/views/checkin.py
index d24ed7a50a..26ff3e4887 100644
--- a/src/pretix/control/views/checkin.py
+++ b/src/pretix/control/views/checkin.py
@@ -50,15 +50,16 @@ from i18nfield.strings import LazyI18nString
from pretix.api.views.checkin import _redeem_process
from pretix.base.media import MEDIA_TYPES
-from pretix.base.models import Checkin, Order, OrderPosition
+from pretix.base.models import Checkin, LogEntry, Order, OrderPosition
from pretix.base.models.checkin import CheckinList
+from pretix.base.models.orders import PrintLog
from pretix.base.services.checkin import (
LazyRuleVars, _logic_annotate_for_graphic_explain,
)
from pretix.base.signals import checkin_created
-from pretix.base.views.tasks import AsyncPostView
+from pretix.base.views.tasks import AsyncFormView, AsyncPostView
from pretix.control.forms.checkin import (
- CheckinListForm, CheckinListSimulatorForm,
+ CheckinListForm, CheckinListSimulatorForm, CheckinResetForm,
)
from pretix.control.forms.filter import (
CheckinFilterForm, CheckinListAttendeeFilterForm, CheckinListFilterForm,
@@ -570,3 +571,55 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
for q in self.result["questions"]:
q["question"] = LazyI18nString(q["question"])
return self.get(self.request, self.args, self.kwargs)
+
+
+class CheckInResetView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncFormView):
+ form_class = CheckinResetForm
+ permission = "can_change_orders"
+ template_name = "pretixcontrol/checkin/reset.html"
+
+ def get_error_url(self, *args):
+ return reverse(
+ "control:event.orders.checkinlists",
+ kwargs={
+ "event": self.request.event.slug,
+ "organizer": self.request.organizer.slug,
+ },
+ )
+
+ def get_success_url(self, *args):
+ return reverse(
+ "control:event.orders.checkinlists",
+ kwargs={
+ "event": self.request.event.slug,
+ "organizer": self.request.organizer.slug,
+ },
+ )
+
+ def get_context_data(self, **kwargs):
+ ctx = super().get_context_data(**kwargs)
+ ctx['checkins'] = Checkin.all.filter(list__event=self.request.event).count()
+ ctx['printlogs'] = PrintLog.objects.filter(position__order__event=self.request.event).count()
+ return ctx
+
+ def async_form_valid(self, task, form):
+ with transaction.atomic():
+ qs = Checkin.all.filter(list__event=self.request.event).select_related("position", "position__order")
+ logentries = []
+ for ci in qs:
+ if ci.position:
+ logentries.append(ci.position.order.log_action('pretix.event.checkin.reverted', data={
+ 'position': ci.position.id,
+ 'positionid': ci.position.positionid,
+ 'list': ci.list_id,
+ 'web': True
+ }, user=self.request.user, save=False))
+
+ Order.objects.filter(pk__in=qs.values_list("position__order_id", flat=True)).update(last_modified=now())
+ qs.delete()
+ LogEntry.objects.bulk_create(logentries)
+
+ pl = PrintLog.objects.filter(position__order__event=self.request.event)
+ pl.delete()
+ self.request.event.log_action('pretix.event.checkin.reset', user=self.request.user)
+ self.request.event.cache.clear()
diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py
index e1658b7873..65c77ef227 100644
--- a/src/pretix/control/views/orders.py
+++ b/src/pretix/control/views/orders.py
@@ -83,6 +83,7 @@ from pretix.base.models import (
)
from pretix.base.models.orders import (
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
+ PrintLog,
)
from pretix.base.models.tax import ask_for_vat_id
from pretix.base.payment import PaymentException
@@ -597,6 +598,7 @@ class OrderDetail(OrderView):
'item__questions', 'issued_gift_cards', 'owned_gift_cards', 'linked_media',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),
Prefetch('all_checkins', queryset=Checkin.all.select_related('list').order_by('datetime')),
+ Prefetch('print_logs', queryset=PrintLog.objects.select_related('device').order_by('datetime')),
).order_by('positionid')
positions = []
diff --git a/src/pretix/static/pretixcontrol/scss/main.scss b/src/pretix/static/pretixcontrol/scss/main.scss
index be9756f395..41a72dee01 100644
--- a/src/pretix/static/pretixcontrol/scss/main.scss
+++ b/src/pretix/static/pretixcontrol/scss/main.scss
@@ -636,6 +636,10 @@ details summary {
.position-buttons {
padding-left: 20px;
}
+.print-logs {
+ padding-left: 20px;
+ font-size: $font-size-small;
+}
.pos-canceled * {
color: $brand-danger;