diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 897986150d..1b1c3cbce4 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1195,7 +1195,20 @@ DEFAULTS = { help_text=_("If you ask for a phone number, explain why you do so and what you will use the phone number for.") ) }, - + 'show_checkin_number_user': { + 'default': 'False', + 'type': bool, + 'serializer_class': serializers.BooleanField, + 'form_class': forms.BooleanField, + 'form_kwargs': dict( + label=_("Show number of check-ins to customer"), + help_text=_('With this option enabled, your customers will be able how many times they entered ' + 'the event. This is usually not necessary, but might be useful in combination with tickets ' + 'that are usable a specific number of times, so customers can see how many times they have ' + 'already been used. Exits or failed scans will not be counted, and the user will not see ' + 'the different check-in lists.'), + ) + }, 'ticket_download': { 'default': 'False', 'type': bool, diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 746b3268d8..87a1ee2479 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -523,6 +523,7 @@ class EventSettingsForm(SettingsForm): 'last_order_modification_date', 'allow_modifications_after_checkin', 'checkout_show_copy_answers_button', + 'show_checkin_number_user', 'primary_color', 'theme_color_success', 'theme_color_danger', diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index 09624f4822..1c2784f653 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -221,6 +221,7 @@ {% bootstrap_field sform.show_items_outside_presale_period layout="control" %} {% bootstrap_field sform.last_order_modification_date layout="control" %} {% bootstrap_field sform.allow_modifications_after_checkin layout="control" %} + {% bootstrap_field sform.show_checkin_number_user layout="control" %}
{% trans "Display" %} diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html index 0a39fabfc5..d7063ffc22 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html @@ -31,6 +31,8 @@

{% if line.seat or line.voucher or line.subevent or line.used_membership%}
+ {% elif event.settings.show_checkin_number_user and line.checkin_count %} +
{% endif %} {% if line.seat %}
@@ -94,8 +96,25 @@
{% endif %} + {% if event.settings.show_checkin_number_user and line.checkin_count %} +
+
{% trans "Usage:" context "ticket_checkins" %}
+
+ + + {% blocktrans trimmed count count=line.checkin_count %} + This ticket has been used once. + {% plural %} + This ticket has been used {{ count }} times. + {% endblocktrans %} + +
+
+ {% endif %} {% if line.seat or line.voucher or line.subevent or line.used_membership%}
+ {% elif event.settings.show_checkin_number_user and line.checkin_count %} +
{% endif %} {% if line.issued_gift_cards.exists %} diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index 4e149fca64..e3435a6d25 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -47,7 +47,7 @@ from django.contrib import messages from django.core.exceptions import ValidationError from django.core.files import File from django.db import transaction -from django.db.models import Exists, OuterRef, Q, Sum +from django.db.models import Count, Exists, OuterRef, Q, Subquery, Sum from django.http import ( FileResponse, Http404, HttpResponseRedirect, JsonResponse, ) @@ -60,7 +60,8 @@ from django.views.decorators.clickjacking import xframe_options_exempt from django.views.generic import TemplateView, View from pretix.base.models import ( - CachedTicket, GiftCard, Invoice, Order, OrderPosition, Quota, TaxRule, + CachedTicket, Checkin, GiftCard, Invoice, Order, OrderPosition, Quota, + TaxRule, ) from pretix.base.models.orders import ( CachedCombinedTicket, InvoiceAddress, OrderFee, OrderPayment, OrderRefund, @@ -124,12 +125,13 @@ class OrderDetailMixin(NoSearchIndexViewMixin): class OrderPositionDetailMixin(NoSearchIndexViewMixin): @cached_property def position(self): - p = OrderPosition.objects.filter( + qs = OrderPosition.objects.filter( order__event=self.request.event, addon_to__isnull=True, order__code=self.kwargs['order'], positionid=self.kwargs['position'] - ).select_related('order', 'order__event').first() + ).select_related('order', 'order__event') + p = qs.first() if p: if p.web_secret.lower() == self.kwargs['secret'].lower(): return p @@ -229,9 +231,21 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TicketPageMixin, def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) + + qs = self.order.positions.prefetch_related('issued_gift_cards').select_related('tax_rule') + if self.request.event.settings.show_checkin_number_user: + qs = qs.annotate( + checkin_count=Subquery( + Checkin.objects.filter( + successful=True, type=Checkin.TYPE_ENTRY, position_id=OuterRef('pk') + ).order_by().values('position').annotate(c=Count('*')).values('c') + ) + ) + ctx['cart'] = self.get_cart( - answers=True, downloads=ctx['can_download'], - queryset=self.order.positions.prefetch_related('issued_gift_cards').select_related('tax_rule'), + answers=True, + downloads=ctx['can_download'], + queryset=qs, order=self.order ) ctx['tickets_with_download'] = [p for p in ctx['cart']['positions'] if p.generate_ticket] @@ -328,14 +342,23 @@ class OrderPositionDetails(EventViewMixin, OrderPositionDetailMixin, CartMixin, return buttons def get_context_data(self, **kwargs): + qs = self.order.positions.select_related('tax_rule').filter( + Q(pk=self.position.pk) | Q(addon_to__id=self.position.pk) + ) + if self.request.event.settings.show_checkin_number_user: + qs = qs.annotate( + checkin_count=Subquery( + Checkin.objects.filter( + successful=True, type=Checkin.TYPE_ENTRY, position_id=OuterRef('pk') + ).order_by().values('position').annotate(c=Count('*')).values('c') + ) + ) ctx = super().get_context_data(**kwargs) ctx['can_download_multi'] = False ctx['position'] = self.position ctx['cart'] = self.get_cart( answers=True, downloads=ctx['can_download'], - queryset=self.order.positions.select_related('tax_rule').filter( - Q(pk=self.position.pk) | Q(addon_to__id=self.position.pk) - ), + queryset=qs, order=self.order ) ctx['tickets_with_download'] = [p for p in ctx['cart']['positions'] if p.generate_ticket]