diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index ac5c8a3114..4953303c75 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -206,6 +206,17 @@ checkins list of objects List of **succe ├ device integer Internal ID of the device. Can be ``null``. **Deprecated**, since this ID is not otherwise used in the API and is therefore not very useful. ├ device_id integer Attribute ``device_id`` of the device. Can be ``null``. └ auto_checked_in boolean Indicates if this check-in been performed automatically by the system +print_logs list of objects List of print jobs recorded e.g. by the pretix apps +├ id integer Internal ID of the print job +├ successful boolean Whether the print job successfully resulted in a print. + This is not expected to be 100 % reliable information (since + printer feedback is never perfect) and there is no guarantee + that unsuccessful jobs will be logged. +├ device_id integer Attribute ``device_id`` of the device that recorded the print. Can be ``null``. +├ datetime datetime Time of printing +├ source string Source of print job, e.g. name of the app used. +├ type string Type of print (currently ``badge``, ``ticket``, ``certificate``, or ``other``) +└ info object Additional data with client-dependent structure. downloads list of objects List of ticket download options ├ output string Ticket output provider (e.g. ``pdf``, ``passbook``) └ url string Download URL @@ -233,6 +244,10 @@ pdf_data object Data object req The attributes ``blocked``, ``valid_from`` and ``valid_until`` have been added. +.. versionchanged:: 2024.9 + + The attribute ``print_logs`` has been added. + .. _order-payment-resource: Order payment resource @@ -399,10 +414,21 @@ List of all orders "type": "entry", "gate": null, "device": 2, + "device_id": 1, "datetime": "2017-12-25T12:45:23Z", "auto_checked_in": false } ], + "print_logs": [ + { + "id": 1, + "type": "badge", + "datetime": "2017-12-25T12:45:23Z", + "device_id": 1, + "source": "pretixSCAN", + "info": {} + } + ], "answers": [ { "question": 12, @@ -626,10 +652,22 @@ Fetching individual orders "type": "entry", "gate": null, "device": 2, + "device_id": 1, "datetime": "2017-12-25T12:45:23Z", "auto_checked_in": false } ], + "print_logs": [ + { + "id": 1, + "type": "badge", + "successful": true, + "datetime": "2017-12-25T12:45:23Z", + "device_id": 1, + "source": "pretixSCAN", + "info": {} + } + ], "answers": [ { "question": 12, @@ -1581,10 +1619,22 @@ List of all order positions "type": "entry", "gate": null, "device": 2, + "device_id": 1, "datetime": "2017-12-25T12:45:23Z", "auto_checked_in": false } ], + "print_logs": [ + { + "id": 1, + "type": "badge", + "successful": true, + "datetime": "2017-12-25T12:45:23Z", + "device_id": 1, + "source": "pretixSCAN", + "info": {} + } + ], "answers": [ { "question": 12, @@ -1695,10 +1745,22 @@ Fetching individual positions "type": "entry", "gate": null, "device": 2, + "device_id": 1, "datetime": "2017-12-25T12:45:23Z", "auto_checked_in": false } ], + "print_logs": [ + { + "id": 1, + "type": "badge", + "successful": true, + "datetime": "2017-12-25T12:45:23Z", + "device_id": 1, + "source": "pretixSCAN", + "info": {} + } + ], "answers": [ { "question": 12, @@ -1795,6 +1857,10 @@ Manipulating individual positions The endpoints to manage blocks have been added. +.. versionchanged:: 2024.9 + + The API now supports logging ticket and badge prints. + .. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/ Updates specific fields on an order position. Currently, only the following fields are supported: @@ -2054,6 +2120,59 @@ Manipulating individual positions :statuscode 401: Authentication failure :statuscode 403: The requested organizer/event does not exist **or** you have no permission to update this order position. +.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/(id)/printlog/ + + Creates a print log, stating that this ticket has been printed. + + **Example request**: + + .. sourcecode:: http + + POST /api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/printlog/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + Content-Type: application/json + + { + "datetime": "2024-09-19T13:37:00+02:00", + "source": "pretixPOS", + "type": "badge", + "info": { + "cashier": 1234 + } + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 Created + Vary: Accept + Content-Type: application/pdf + + { + "id": 1234, + "device_id": null, + "datetime": "2024-09-19T13:37:00+02:00", + "source": "pretixPOS", + "type": "badge", + "info": { + "cashier": 1234 + } + } + + :param organizer: The ``slug`` field of the organizer to create a log for + :param event: The ``slug`` field of the event to create a log for + :param id: The ``id`` field of the order position to create a log for + :statuscode 201: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource + **or** downloads are not available for this order position at this time. The response content will + contain more details. + :statuscode 404: The requested order position or download provider does not exist. + :statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few + seconds. + Changing order contents ----------------------- diff --git a/src/pretix/api/auth/devicesecurity.py b/src/pretix/api/auth/devicesecurity.py index 54f905ffac..aef2fef40f 100644 --- a/src/pretix/api/auth/devicesecurity.py +++ b/src/pretix/api/auth/devicesecurity.py @@ -77,6 +77,7 @@ class PretixScanSecurityProfile(AllowListSecurityProfile): ('GET', 'api-v1:blockedsecrets-list'), ('GET', 'api-v1:order-list'), ('GET', 'api-v1:orderposition-pdf_image'), + ('POST', 'api-v1:orderposition-printlog'), ('GET', 'api-v1:event.settings'), ('POST', 'api-v1:upload'), ('POST', 'api-v1:checkinrpc.redeem'), @@ -112,6 +113,7 @@ class PretixScanNoSyncNoSearchSecurityProfile(AllowListSecurityProfile): ('GET', 'api-v1:revokedsecrets-list'), ('GET', 'api-v1:blockedsecrets-list'), ('GET', 'api-v1:orderposition-pdf_image'), + ('POST', 'api-v1:orderposition-printlog'), ('GET', 'api-v1:event.settings'), ('POST', 'api-v1:upload'), ('POST', 'api-v1:checkinrpc.redeem'), @@ -147,6 +149,7 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile): ('GET', 'api-v1:revokedsecrets-list'), ('GET', 'api-v1:blockedsecrets-list'), ('GET', 'api-v1:orderposition-pdf_image'), + ('POST', 'api-v1:orderposition-printlog'), ('GET', 'api-v1:event.settings'), ('POST', 'api-v1:upload'), ('POST', 'api-v1:checkinrpc.redeem'), @@ -188,6 +191,7 @@ class PretixPosSecurityProfile(AllowListSecurityProfile): ('GET', 'api-v1:orderposition-list'), ('GET', 'api-v1:orderposition-answer'), ('GET', 'api-v1:orderposition-pdf_image'), + ('POST', 'api-v1:orderposition-printlog'), ('POST', 'api-v1:order-mark-canceled'), ('POST', 'api-v1:orderpayment-list'), ('POST', 'api-v1:orderrefund-list'), diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 807dd55792..53e35b4810 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -55,7 +55,7 @@ from pretix.base.models import ( ) from pretix.base.models.orders import ( BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund, - RevokedTicketSecret, + PrintLog, RevokedTicketSecret, ) from pretix.base.pdf import get_images, get_variables from pretix.base.services.cart import error_messages @@ -284,6 +284,26 @@ class CheckinSerializer(I18nAwareModelSerializer): fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type') +class PrintLogSerializer(serializers.ModelSerializer): + device_id = serializers.SlugRelatedField( + source='device', + slug_field='device_id', + read_only=True, + ) + + class Meta: + model = PrintLog + fields = ( + "id", + "successful", + "datetime", + "source", + "type", + "device_id", + "info", + ) + + class FailedCheckinSerializer(I18nAwareModelSerializer): error_reason = serializers.ChoiceField(choices=Checkin.REASONS, required=True, allow_null=False) raw_barcode = serializers.CharField(required=True, allow_null=False) @@ -476,6 +496,7 @@ class OrderPositionListSerializer(serializers.ListSerializer): class OrderPositionSerializer(I18nAwareModelSerializer): checkins = CheckinSerializer(many=True, read_only=True) + print_logs = PrintLogSerializer(many=True, read_only=True) answers = AnswerSerializer(many=True) downloads = PositionDownloadsField(source='*', read_only=True) order = serializers.SlugRelatedField(slug_field='code', read_only=True) @@ -490,7 +511,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer): fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country', 'state', 'discount', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', - 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled', + 'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use') read_only_fields = ( 'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret', @@ -577,9 +598,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer): fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country', 'state', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', - 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention', - 'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until', - 'blocked') + 'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', + 'require_attention', 'order__status', 'order__valid_if_pending', 'order__require_approval', + 'valid_from', 'valid_until', 'blocked') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1494,6 +1515,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer): pos.answers = answers pos.pseudonymization_id = "PREVIEW" pos.checkins = [] + pos.print_logs = [] pos_map[pos.positionid] = pos else: if pos.voucher: diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index 668b2e19c3..3c18e18cc9 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -62,6 +62,7 @@ from pretix.base.models import ( CachedFile, Checkin, CheckinList, Device, Event, Order, OrderPosition, Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken, ) +from pretix.base.models.orders import PrintLog from pretix.base.services.checkin import ( CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin, ) @@ -365,8 +366,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr qs = qs.prefetch_related( Prefetch( lookup='checkins', - queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]) + queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device') ), + Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), 'answers', 'answers__options', 'answers__question', Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')), Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related( @@ -378,6 +380,7 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr 'positions', OrderPosition.objects.prefetch_related( Prefetch('checkins', queryset=Checkin.objects.select_related('device')), + Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), 'item', 'variation', 'answers', 'answers__options', 'answers__question', ) ) @@ -389,8 +392,9 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr qs = qs.prefetch_related( Prefetch( lookup='checkins', - queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]) + queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device') ), + Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), 'answers', 'answers__options', 'answers__question', Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')) ).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat') diff --git a/src/pretix/api/views/media.py b/src/pretix/api/views/media.py index e70f1bf769..4954a9fe0c 100644 --- a/src/pretix/api/views/media.py +++ b/src/pretix/api/views/media.py @@ -42,6 +42,7 @@ from pretix.base.models import ( Checkin, GiftCard, GiftCardAcceptance, GiftCardTransaction, OrderPosition, ReusableMedium, ) +from pretix.base.models.orders import PrintLog from pretix.helpers import OF_SELF from pretix.helpers.dicts import merge_dicts @@ -79,6 +80,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet): 'order', 'order__event', 'order__event__organizer', 'seat', ).prefetch_related( Prefetch('checkins', queryset=Checkin.objects.select_related('device')), + Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), 'answers', 'answers__options', 'answers__question', ) ), diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index a96c373448..2c79de3780 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -57,7 +57,8 @@ from pretix.api.serializers.order import ( OrderPaymentCreateSerializer, OrderPaymentSerializer, OrderPositionSerializer, OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer, PriceCalcSerializer, - RevokedTicketSecretSerializer, SimulatedOrderSerializer, + PrintLogSerializer, RevokedTicketSecretSerializer, + SimulatedOrderSerializer, ) from pretix.api.serializers.orderchange import ( BlockNameSerializer, OrderChangeOperationSerializer, @@ -75,7 +76,7 @@ from pretix.base.models import ( TeamAPIToken, generate_secret, ) from pretix.base.models.orders import ( - BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret, + BlockedTicketSecret, PrintLog, QuestionAnswer, RevokedTicketSecret, ) from pretix.base.payment import PaymentException from pretix.base.pdf import get_images @@ -259,6 +260,7 @@ class OrderViewSetMixin: 'positions', opq.all().prefetch_related( Prefetch('checkins', queryset=Checkin.objects.select_related('device')), + Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached') )), @@ -280,6 +282,7 @@ class OrderViewSetMixin: 'positions', opq.all().prefetch_related( Prefetch('checkins', queryset=Checkin.objects.select_related('device')), + Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), 'item', 'variation', Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')), 'seat', @@ -1093,6 +1096,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet): ) qs = qs.prefetch_related( Prefetch('checkins', queryset=Checkin.objects.select_related("device")), + Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached') @@ -1136,6 +1140,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet): else: qs = qs.prefetch_related( Prefetch('checkins', queryset=Checkin.objects.select_related("device")), + Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), 'answers', 'answers__options', 'answers__question', ).select_related( 'item', 'order', 'order__event', 'order__event__organizer', 'seat' @@ -1254,6 +1259,34 @@ class OrderPositionViewSet(viewsets.ModelViewSet): ) return resp + @action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"]) + def printlog(self, request, **kwargs): + pos = self.get_object() + serializer = PrintLogSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + with transaction.atomic(): + serializer.save( + position=pos, + device=request.auth if isinstance(request.auth, Device) else None, + user=request.user if request.user.is_authenticated else None, + api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None, + oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None, + ) + + pos.order.log_action( + "pretix.event.order.print", + data={ + "position": pos.pk, + "positionid": pos.positionid, + **serializer.validated_data, + }, + auth=request.auth, + user=request.user, + ) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + @action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P[^/]+)') def pdf_image(self, request, key, **kwargs): pos = self.get_object() diff --git a/src/pretix/base/migrations/0272_printlog.py b/src/pretix/base/migrations/0272_printlog.py new file mode 100644 index 0000000000..24064f7dce --- /dev/null +++ b/src/pretix/base/migrations/0272_printlog.py @@ -0,0 +1,79 @@ +# Generated by Django 4.2.16 on 2024-09-19 10:41 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL), + ("pretixbase", "0271_itemcategory_cross_selling"), + ] + + operations = [ + migrations.CreateModel( + name="PrintLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ("datetime", models.DateTimeField(default=django.utils.timezone.now)), + ("created", models.DateTimeField(auto_now_add=True, null=True)), + ("successful", models.BooleanField(default=True)), + ("source", models.CharField(max_length=255)), + ("type", models.CharField(max_length=255)), + ("info", models.JSONField(default=dict)), + ( + "api_token", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="pretixbase.teamapitoken", + ), + ), + ( + "device", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="print_logs", + to="pretixbase.device", + ), + ), + ( + "oauth_application", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL, + ), + ), + ( + "position", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="print_logs", + to="pretixbase.orderposition", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="print_logs", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ("-datetime",), + }, + ), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index ce9a6dbc9f..74eea62271 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -3391,6 +3391,74 @@ class BlockedTicketSecret(models.Model): unique_together = (('event', 'secret'),) +class PrintLog(models.Model): + """ + A print log object is created when a ticket or badge is printed with our apps. + """ + TYPE_BADGE = 'badge' + TYPE_TICKET = 'ticket' + TYPE_CERTIFICATE = 'certificate' + TYPE_OTHER = 'other' + PRINT_TYPES = ( + (TYPE_BADGE, _('Badge')), + (TYPE_TICKET, _('Ticket')), + (TYPE_CERTIFICATE, _('Certificate')), + (TYPE_OTHER, _('Other')), + ) + + position = models.ForeignKey( + 'pretixbase.OrderPosition', + related_name='print_logs', + on_delete=models.CASCADE, + ) + successful = models.BooleanField( + default=True, + ) + + # Datetime of checkin, might be different from created if past scans are uploaded + datetime = models.DateTimeField(default=now) + + # Datetime of creation on server + created = models.DateTimeField(auto_now_add=True, null=True, blank=True) + + # Who printed? + device = models.ForeignKey('Device', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT) + user = models.ForeignKey('User', related_name='print_logs', null=True, blank=True, on_delete=models.PROTECT) + api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT) + oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT) + + # Source = Tag field with undefined values, e.g. name of app ("pretixscan") + source = models.CharField(max_length=255) + + # Type = Type of object printed ("badge", "ticket") + type = models.CharField(max_length=255, choices=PRINT_TYPES) + + info = models.JSONField(default=dict) + + objects = ScopedManager(organizer='position__order__event__organizer') + + class Meta: + ordering = (('-datetime'),) + + def __repr__(self): + return "".format( + self.position, self.datetime, self.source + ) + + def save(self, **kwargs): + super().save(**kwargs) + if self.position: + self.position.order.touch() + + def delete(self, **kwargs): + super().delete(**kwargs) + self.position.order.touch() + + @property + def is_late_upload(self): + return self.created and abs(self.created - self.datetime) > timedelta(minutes=2) + + @receiver(post_delete, sender=CachedTicket) def cachedticket_delete(sender, instance, **kwargs): if instance.file: diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 44da167da7..2399bd5c8b 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -51,6 +51,7 @@ from pretix.base.models import ( Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition, TaxRule, ) +from pretix.base.models.orders import PrintLog from pretix.base.signals import logentry_display, orderposition_blocked_display from pretix.base.templatetags.money import money_filter @@ -639,6 +640,15 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): if sender and logentry.action_type.startswith('pretix.event.checkin'): return _display_checkin(sender, logentry) + if logentry.action_type == 'pretix.event.order.print': + return _('Position #{posid} has been printed at {datetime} with type "{type}".').format( + posid=data.get('positionid'), + datetime=date_format( + dateutil.parser.parse(data["datetime"]), "SHORT_DATETIME_FORMAT" + ), + type=dict(PrintLog.PRINT_TYPES)[data["type"]], + ) + if logentry.action_type == 'pretix.control.views.checkin': # deprecated dt = dateutil.parser.parse(data.get('datetime')) diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index 3f5eb633a9..3a7a9508a9 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -123,6 +123,7 @@ TEST_ORDERPOSITION1_RES = { "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "addon_to": None, "checkins": [], + "print_logs": [], "downloads": [], "answers": [], "seat": None, @@ -160,6 +161,7 @@ TEST_ORDERPOSITION2_RES = { "secret": "sf4HZG73fU6kwddgjg2QOusFbYZwVKpK", "addon_to": None, "checkins": [], + "print_logs": [], "downloads": [], "answers": [], "seat": None, @@ -197,6 +199,7 @@ TEST_ORDERPOSITION3_RES = { "secret": "3u4ez6vrrbgb3wvezxhq446p548dt2wn", "addon_to": None, "checkins": [], + "print_logs": [], "downloads": [], "answers": [], "seat": None, @@ -467,7 +470,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a p3["addon_to"] = p1["id"] # All items - with django_assert_num_queries(23): + with django_assert_num_queries(24): resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format( organizer.slug, event.slug, clist_all.pk )) @@ -1359,7 +1362,7 @@ def test_search(token_client, organizer, event, clist, clist_all, item, other_it p1["id"] = order.positions.get(positionid=1).pk p1["item"] = item.pk - with django_assert_max_num_queries(17): + with django_assert_max_num_queries(18): resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?search=z3fsn8jyu'.format( organizer.slug, event.slug, clist_all.pk )) diff --git a/src/tests/api/test_checkinrpc.py b/src/tests/api/test_checkinrpc.py index b23795aa67..8a91bfcaf6 100644 --- a/src/tests/api/test_checkinrpc.py +++ b/src/tests/api/test_checkinrpc.py @@ -163,6 +163,7 @@ TEST_ORDERPOSITION1_RES = { "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "addon_to": None, "checkins": [], + "print_logs": [], "downloads": [], "answers": [], "seat": None, diff --git a/src/tests/api/test_giftcards.py b/src/tests/api/test_giftcards.py index 3cfc664b9f..4eeb5a0dcd 100644 --- a/src/tests/api/test_giftcards.py +++ b/src/tests/api/test_giftcards.py @@ -155,6 +155,7 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard): "addon_to": None, "subevent": None, "checkins": [], + "print_logs": [], "downloads": [], "answers": [], "tax_rule": None, diff --git a/src/tests/api/test_order_create.py b/src/tests/api/test_order_create.py index b8f7c8daff..05e319fb48 100644 --- a/src/tests/api/test_order_create.py +++ b/src/tests/api/test_order_create.py @@ -463,6 +463,7 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques 'subevent': None, 'discount': None, 'checkins': [], + 'print_logs': [], 'downloads': [], "valid_from": None, "valid_until": None, @@ -535,13 +536,13 @@ def test_order_create_positionids_addons_simulated(token_client, organizer, even 'attendee_name': 'Peter', 'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'}, 'company': None, 'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None, 'voucher': None, 'tax_rate': '0.00', 'tax_value': '0.00', 'discount': None, 'voucher_budget_use': None, - 'addon_to': None, 'subevent': None, 'checkins': [], 'downloads': [], 'answers': [], 'tax_rule': None, + 'addon_to': None, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': None, 'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None}, {'id': 0, 'order': '', 'positionid': 2, 'item': item.pk, 'variation': None, 'price': '23.00', 'attendee_name': 'Peter', 'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'}, 'company': None, 'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None, 'voucher': None, 'tax_rate': '0.00', 'tax_value': '0.00', 'discount': None, 'voucher_budget_use': None, - 'addon_to': 1, 'subevent': None, 'checkins': [], 'downloads': [], 'answers': [], 'tax_rule': None, + 'addon_to': 1, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': None, 'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None} ] diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index 39db165a37..e24eacc6a6 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -76,7 +76,7 @@ def quota(event, item): @pytest.fixture -def order(event, item, taxrule, question): +def order(event, item, device, taxrule, question): testtime = datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc) event.plugins += ",pretix.plugins.stripe" event.save() @@ -137,6 +137,13 @@ def order(event, item, taxrule, question): canceled=True, positionid=2, ) + op.print_logs.create( + device=device, + type="badge", + source="pretixpos", + info={"cashier": 1234}, + datetime=datetime.datetime(2017, 12, 1, 12, 0, 0, tzinfo=datetime.timezone.utc), + ) op.answers.create(question=question, answer='S') return o @@ -200,6 +207,19 @@ TEST_ORDERPOSITION_RES = { "addon_to": None, "pseudonymization_id": "ABCDEFGHKL", "checkins": [], + "print_logs": [ + { + "id": -1, + "device_id": -1, + "successful": True, + "datetime": "2017-12-01T12:00:00Z", + "source": "pretixpos", + "type": "badge", + "info": { + "cashier": 1234 + }, + } + ], "downloads": [], "seat": None, "company": None, @@ -321,7 +341,7 @@ TEST_ORDER_RES = { @pytest.mark.django_db -def test_order_list_filter_subevent_date(token_client, organizer, event, order, item, taxrule, subevent, question): +def test_order_list_filter_subevent_date(token_client, device, organizer, event, order, item, taxrule, subevent, question): res = copy.deepcopy(TEST_ORDER_RES) with scopes_disabled(): res["positions"][0]["id"] = order.positions.first().pk @@ -329,6 +349,9 @@ def test_order_list_filter_subevent_date(token_client, organizer, event, order, p.subevent = subevent p.save() fee = order.fees.first() + pl = p.print_logs.first() + res["positions"][0]["print_logs"][0]["id"] = pl.pk + res["positions"][0]["print_logs"][0]["device_id"] = device.device_id res["positions"][0]["item"] = item.pk res["positions"][0]["subevent"] = subevent.pk res["positions"][0]["answers"][0]["question"] = question.pk @@ -379,11 +402,13 @@ def test_order_list_filter_subevent_date(token_client, organizer, event, order, @pytest.mark.django_db -def test_order_list(token_client, organizer, event, order, item, taxrule, question): +def test_order_list(token_client, organizer, event, order, item, taxrule, question, device): res = dict(TEST_ORDER_RES) with scopes_disabled(): res["positions"][0]["id"] = order.positions.first().pk res["fees"][0]["id"] = order.fees.first().pk + res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk + res["positions"][0]["print_logs"][0]["device_id"] = device.device_id res["positions"][0]["item"] = item.pk res["positions"][0]["answers"][0]["question"] = question.pk res["last_modified"] = order.last_modified.isoformat().replace('+00:00', 'Z') @@ -497,6 +522,7 @@ def test_order_detail(token_client, organizer, event, order, item, taxrule, ques res = dict(TEST_ORDER_RES) with scopes_disabled(): res["positions"][0]["id"] = order.positions.first().pk + res["positions"][0]["print_logs"][0]["id"] = order.positions.first().print_logs.first().pk res["fees"][0]["id"] = order.fees.first().pk res["positions"][0]["item"] = item.pk res["fees"][0]["tax_rule"] = taxrule.pk @@ -968,12 +994,14 @@ def test_orderposition_list(token_client, organizer, device, event, order, item, var2 = item.variations.create(value="Children") res = copy.copy(TEST_ORDERPOSITION_RES) op = order.positions.first() - op.variation = var - op.save() - res["id"] = op.pk - res["item"] = item.pk - res["variation"] = var.pk - res["answers"][0]["question"] = question.pk + op.variation = var + op.save() + res["id"] = op.pk + res["item"] = item.pk + res["variation"] = var.pk + res["answers"][0]["question"] = question.pk + res["print_logs"][0]["id"] = op.print_logs.first().pk + res["print_logs"][0]["device_id"] = device.device_id resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(organizer.slug, event.slug)) assert resp.status_code == 200 @@ -1072,7 +1100,7 @@ def test_orderposition_list(token_client, organizer, device, event, order, item, 'gate': None, 'type': 'entry' }] - with django_assert_num_queries(15): + with django_assert_num_queries(16): resp = token_client.get( '/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug) ) @@ -1107,6 +1135,7 @@ def test_orderposition_detail(token_client, organizer, event, order, item, quest res = dict(TEST_ORDERPOSITION_RES) with scopes_disabled(): op = order.positions.first() + res["print_logs"][0]["id"] = op.print_logs.first().pk res["id"] = op.pk res["item"] = item.pk res["answers"][0]["question"] = question.pk @@ -1171,6 +1200,30 @@ def test_orderposition_delete(token_client, organizer, event, order, item, quest assert order.total == Decimal('23.25') +@pytest.mark.django_db +def test_orderposition_printlog(token_client, team, organizer, event, order, item, question): + with scopes_disabled(): + op = order.positions.first() + resp = token_client.post('/api/v1/organizers/{}/events/{}/orderpositions/{}/printlog/'.format( + organizer.slug, event.slug, op.pk + ), data={ + "datetime": "2023-09-04T12:23:45+02:00", + "source": "pretixscan", + "type": "badge", + "info": { + "cashier": 1234, + } + }, format='json') + assert resp.status_code == 201 + + with scopes_disabled(): + l = op.print_logs.get(source="pretixscan") + assert l.type == "badge" + assert l.info == {"cashier": 1234} + assert l.api_token.team == team + assert l.datetime.isoformat() == "2023-09-04T10:23:45+00:00" + + @pytest.mark.django_db def test_order_mark_paid_pending(token_client, organizer, event, order): resp = token_client.post( @@ -1920,7 +1973,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q assert not resp.data['positions'][0].get('pdf_data') # order list - with django_assert_max_num_queries(30): + with django_assert_max_num_queries(31): resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format( organizer.slug, event.slug )) @@ -1935,7 +1988,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q assert not resp.data['results'][0]['positions'][0].get('pdf_data') # position list - with django_assert_max_num_queries(33): + with django_assert_max_num_queries(34): resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/?pdf_data=true'.format( organizer.slug, event.slug )) diff --git a/src/tests/api/test_reusable_media.py b/src/tests/api/test_reusable_media.py index 0d0f564f97..c72e408c52 100644 --- a/src/tests/api/test_reusable_media.py +++ b/src/tests/api/test_reusable_media.py @@ -192,6 +192,7 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome "addon_to": None, "subevent": None, "checkins": [], + "print_logs": [], "downloads": [], "answers": [], "tax_rule": None,