Repository: pretix/pretix

Issue:
Manual check-in of attendees
It is useful e.g. for statistical reasons to be able to mark participant as checked-in manually after the event, e.g. when check-in was done offline with a paper list.

I imagine the best way to do this would be to have checkboxes in the first column of the view at "Orders" -> "Check-ins" in the event admin interface that allow to bulk-change the check-in status of attendees.


Discussion:
+1 
Hey, 

I'd like to give this a shot. I'll let you know when I come up with something :)

Resolving patch:
diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py
--- a/src/pretix/control/logdisplay.py
+++ b/src/pretix/control/logdisplay.py
@@ -1,8 +1,11 @@
 import json
 from decimal import Decimal
 
+import dateutil.parser
+import pytz
 from django.dispatch import receiver
 from django.utils import formats
+from django.utils.formats import date_format
 from django.utils.translation import pgettext_lazy, ugettext_lazy as _
 from i18nfield.strings import LazyI18nString
 
@@ -200,6 +203,21 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
     if logentry.action_type.startswith('pretix.event.tickets.provider.'):
         return _('The settings of a ticket output provider have been changed.')
 
+    if logentry.action_type == 'pretix.control.views.checkin':
+        dt = dateutil.parser.parse(data.get('datetime'))
+        tz = pytz.timezone(sender.settings.timezone)
+        dt_formatted = date_format(dt.astimezone(tz), "SHORT_DATETIME_FORMAT")
+
+        if data.get('first'):
+            return _('Position #{posid} has been checked in manually at {datetime}.').format(
+                posid=data.get('positionid'),
+                datetime=dt_formatted
+            )
+        return _('Position #{posid} has been checked in again at {datetime}.').format(
+            posid=data.get('positionid'),
+            datetime=dt_formatted
+        )
+
     if logentry.action_type == 'pretix.team.member.added':
         return _('{user} has been added to the team.').format(user=data.get('email'))
 
diff --git a/src/pretix/control/views/checkin.py b/src/pretix/control/views/checkin.py
--- a/src/pretix/control/views/checkin.py
+++ b/src/pretix/control/views/checkin.py
@@ -1,8 +1,13 @@
+from django.contrib import messages
+from django.core.urlresolvers import reverse
 from django.db.models import F, Prefetch, Q
 from django.db.models.functions import Coalesce
+from django.shortcuts import redirect
+from django.utils.timezone import now
+from django.utils.translation import ugettext_lazy as _
 from django.views.generic import ListView
 
-from pretix.base.models import Checkin, Item, OrderPosition
+from pretix.base.models import Checkin, Item, Order, OrderPosition
 from pretix.control.permissions import EventPermissionRequiredMixin
 
 
@@ -69,6 +74,31 @@ def get_context_data(self, **kwargs):
                            or "subevent" in self.request.GET)
         return ctx
 
+    def post(self, request, *args, **kwargs):
+        positions = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').filter(
+            order__event=self.request.event,
+            pk__in=request.POST.getlist('checkin')
+        )
+
+        for op in positions:
+            created = False
+            if op.order.status == Order.STATUS_PAID:
+                ci, created = Checkin.objects.get_or_create(position=op, defaults={
+                    'datetime': now(),
+                })
+            op.order.log_action('pretix.control.views.checkin', data={
+                'position': op.id,
+                'positionid': op.positionid,
+                'first': created,
+                'datetime': now()
+            }, user=request.user)
+
+        messages.success(request, _('The selected tickets have been marked as checked in.'))
+        return redirect(reverse('control:event.orders.checkins', kwargs={
+            'event': self.request.event.slug,
+            'organizer': self.request.event.organizer.slug
+        }) + '?' + request.GET.urlencode())
+
     @staticmethod
     def get_ordering_keys_mappings():
         return {


Test patch:
diff --git a/src/tests/control/test_checkins.py b/src/tests/control/test_checkins.py
--- a/src/tests/control/test_checkins.py
+++ b/src/tests/control/test_checkins.py
@@ -5,8 +5,8 @@
 from django.utils.timezone import now
 
 from pretix.base.models import (
-    Checkin, Event, Item, ItemAddOn, ItemCategory, Order, OrderPosition,
-    Organizer, Team, User,
+    Checkin, Event, Item, ItemAddOn, ItemCategory, LogEntry, Order,
+    OrderPosition, Organizer, Team, User,
 )
 from pretix.control.views.dashboards import checkin_widget
 
@@ -269,6 +269,19 @@ def test_checkins_list_mixed(client, checkin_list_env, query, expected):
     assert item_keys == expected
 
 
+@pytest.mark.django_db
+def test_manual_checkins(client, checkin_list_env):
+    client.login(email='dummy@dummy.dummy', password='dummy')
+    assert not checkin_list_env[5][3].checkins.exists()
+    client.post('/control/event/dummy/dummy/checkins/', {
+        'checkin': [checkin_list_env[5][3].pk]
+    })
+    assert checkin_list_env[5][3].checkins.exists()
+    assert LogEntry.objects.filter(
+        action_type='pretix.control.views.checkin', object_id=checkin_list_env[5][3].order.pk
+    ).exists()
+
+
 @pytest.fixture
 def checkin_list_with_addon_env():
     # permission