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 <schreiber@rami.io>

* Update src/pretix/control/templates/pretixcontrol/checkin/reset.html

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/templates/pretixcontrol/checkin/reset.html

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Update src/pretix/control/templates/pretixcontrol/checkin/lists.html

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2025-07-18 10:02:18 +02:00
committed by GitHub
parent 200d520535
commit 423f0cbb90
9 changed files with 139 additions and 3 deletions

View File

@@ -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.")
)

View File

@@ -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

View File

@@ -83,6 +83,13 @@
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-tablet"></i> {% trans "Connected devices" %}</a>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.reset" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default">
<span class="fa fa-repeat"></span>
{% trans "Reset check-in" %}
</a>
{% endif %}
</p>
<div class="table-responsive">
<table class="table table-hover table-quotas">

View File

@@ -0,0 +1,50 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Reset check-in" %}{% endblock %}
{% block inside %}
<h1>{% trans "Reset check-in" %}</h1>
<form action="" method="post" class="" data-asynctask>
{% csrf_token %}
<p>
{% blocktrans trimmed %}
With this feature, you can reset the entire check-in state of the event.
This will delete all check-in records as well as all records of printed tickets or badges.
We recommend to use this feature after testing your hardware setup but only before your
event started, and you admitted any real attendees or printed any real badges or tickets.
{% endblocktrans %}
</p>
<p class="alert alert-danger">
{% blocktrans trimmed count count=checkins %}
This will permanently delete <strong>1 check-in</strong>.
{% plural %}
This will permanently delete <strong>{{ count }} check-ins</strong>.
{% endblocktrans %}
{% blocktrans trimmed count count=printlogs %}
Additionally, <strong>1 print log</strong> will be deleted.
{% plural %}
Additionally, <strong>{{ count }} print logs</strong> will be deleted.
{% endblocktrans %}
<br>
<strong>
{% trans "This cannot be reverted!" %}
</strong>
</p>
<p>
{% blocktrans trimmed %}
The deleted entries will still show up in the "Order history" section, but for all other
purposes the system will behave as if they never existed.
{% endblocktrans %}
</p>
{% bootstrap_form form layout="inline" %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.checkinlists" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Proceed with reset" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -500,6 +500,18 @@
{% eventsignal event "pretix.control.signals.order_position_buttons" order=order position=line request=request %}
</div>
{% endif %}
{% if staff_session %}
<div class="admin-only print-logs">
{% for pl in line.print_logs.all %}
<span class="fa fa-print"></span>
{{ pl.datetime|date:"SHORT_DATETIME_FORMAT" }}
{{ pl.get_type_display }}
({{ pl.source }}{% if pl.device %}, #{{ pl.device.device_id }}{% endif %})
{% if not pl.successful %}<span class="fa fa-warning fa-fw"></span>{% endif %}
<br>
{% endfor %}
</div>
{% endif %}
{% if line.issued_gift_cards %}
<dl>
{% for gc in line.issued_gift_cards.all %}

View File

@@ -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<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
re_path(r'^checkinlists/(?P<list>\d+)/simulator$', checkin.CheckInListSimulator.as_view(), name='event.orders.checkinlists.simulator'),

View File

@@ -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()

View File

@@ -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 = []

View File

@@ -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;