# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-2021 rami.io GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # import datetime import mimetypes import os from decimal import Decimal import django_filters import pytz from django.db import transaction from django.db.models import ( Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects, ) from django.db.models.functions import Coalesce, Concat from django.http import FileResponse, HttpResponse from django.shortcuts import get_object_or_404 from django.utils.timezone import make_aware, now from django.utils.translation import gettext as _ from django_filters.rest_framework import DjangoFilterBackend, FilterSet from django_scopes import scopes_disabled from PIL import Image from rest_framework import serializers, status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import ( APIException, NotFound, PermissionDenied, ValidationError, ) from rest_framework.mixins import CreateModelMixin from rest_framework.response import Response from pretix.api.models import OAuthAccessToken from pretix.api.pagination import TotalOrderingFilter from pretix.api.serializers.order import ( BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer, OrderPaymentSerializer, OrderPositionSerializer, OrderRefundCreateSerializer, OrderRefundSerializer, OrderSerializer, PriceCalcSerializer, RevokedTicketSecretSerializer, SimulatedOrderSerializer, ) from pretix.api.serializers.orderchange import ( BlockNameSerializer, OrderChangeOperationSerializer, OrderFeeChangeSerializer, OrderPositionChangeSerializer, OrderPositionCreateForExistingOrderSerializer, OrderPositionInfoPatchSerializer, ) from pretix.api.views import RichOrderingFilter from pretix.base.i18n import language from pretix.base.models import ( CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue, Invoice, InvoiceAddress, ItemMetaValue, ItemVariation, ItemVariationMetaValue, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule, TeamAPIToken, generate_secret, ) from pretix.base.models.orders import ( BlockedTicketSecret, QuestionAnswer, RevokedTicketSecret, ) from pretix.base.payment import PaymentException from pretix.base.pdf import get_images from pretix.base.secrets import assign_ticket_secret from pretix.base.services import tickets from pretix.base.services.invoices import ( generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified, regenerate_invoice, ) from pretix.base.services.mail import SendMailException from pretix.base.services.orders import ( OrderChangeManager, OrderError, _order_placed_email, _order_placed_email_attendee, approve_order, cancel_order, deny_order, extend_order, mark_order_expired, mark_order_refunded, reactivate_order, ) from pretix.base.services.pricing import get_price from pretix.base.services.tickets import generate from pretix.base.signals import ( order_modified, order_paid, order_placed, register_ticket_outputs, ) from pretix.base.templatetags.money import money_filter from pretix.control.signals import order_search_filter_q with scopes_disabled(): class OrderFilter(FilterSet): email = django_filters.CharFilter(field_name='email', lookup_expr='iexact') code = django_filters.CharFilter(field_name='code', lookup_expr='iexact') status = django_filters.CharFilter(field_name='status', lookup_expr='iexact') modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte') created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte') subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs') subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs') search = django_filters.CharFilter(method='search_qs') item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id', distinct=True) variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True) subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True) class Meta: model = Order fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval'] @scopes_disabled() def subevent_after_qs(self, qs, name, value): qs = qs.filter( pk__in=Subquery( OrderPosition.all.filter( subevent_id__in=SubEvent.objects.filter( Q(date_to__gt=value) | Q(date_from__gt=value, date_to__isnull=True), event=self.request.event ).values_list('id'), ).values_list('order_id') ) ) return qs def subevent_before_qs(self, qs, name, value): qs = qs.filter( pk__in=Subquery( OrderPosition.all.filter( subevent_id__in=SubEvent.objects.filter( Q(date_from__lt=value), event=self.request.event ).values_list('id'), ).values_list('order_id') ) ) return qs def search_qs(self, qs, name, value): u = value if "-" in value: code = (Q(event__slug__icontains=u.rsplit("-", 1)[0]) & Q(code__icontains=Order.normalize_code(u.rsplit("-", 1)[1]))) else: code = Q(code__icontains=Order.normalize_code(u)) matching_invoices = Invoice.objects.filter( Q(invoice_no__iexact=u) | Q(invoice_no__iexact=u.zfill(5)) | Q(full_invoice_no__iexact=u) ).values_list('order_id', flat=True) matching_positions = OrderPosition.objects.filter( Q(order=OuterRef('pk')) & Q( Q(attendee_name_cached__icontains=u) | Q(attendee_email__icontains=u) | Q(secret__istartswith=u) # | Q(voucher__code__icontains=u) # temporarily removed since it caused bad query performance on postgres ) ).values('id') mainq = ( code | Q(email__icontains=u) | Q(invoice_address__name_cached__icontains=u) | Q(invoice_address__company__icontains=u) | Q(pk__in=matching_invoices) | Q(comment__icontains=u) | Q(has_pos=True) ) for recv, q in order_search_filter_q.send(sender=getattr(self, 'event', None), query=u): mainq = mainq | q return qs.annotate(has_pos=Exists(matching_positions)).filter( mainq ) class OrderViewSet(viewsets.ModelViewSet): serializer_class = OrderSerializer queryset = Order.objects.none() filter_backends = (DjangoFilterBackend, TotalOrderingFilter) ordering = ('datetime',) ordering_fields = ('datetime', 'code', 'status', 'last_modified') filterset_class = OrderFilter lookup_field = 'code' permission = 'can_view_orders' write_permission = 'can_change_orders' def get_serializer_context(self): ctx = super().get_serializer_context() ctx['event'] = self.request.event ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true' ctx['exclude'] = self.request.query_params.getlist('exclude') ctx['include'] = self.request.query_params.getlist('include') return ctx def get_queryset(self): qs = self.request.event.orders if 'fees' not in self.request.GET.getlist('exclude'): if self.request.query_params.get('include_canceled_fees', 'false') == 'true': fqs = OrderFee.all else: fqs = OrderFee.objects qs = qs.prefetch_related(Prefetch('fees', queryset=fqs.all())) if 'payments' not in self.request.GET.getlist('exclude'): qs = qs.prefetch_related('payments') if 'refunds' not in self.request.GET.getlist('exclude'): qs = qs.prefetch_related('refunds', 'refunds__payment') if 'invoice_address' not in self.request.GET.getlist('exclude'): qs = qs.select_related('invoice_address') qs = qs.prefetch_related(self._positions_prefetch(self.request)) return qs def _positions_prefetch(self, request): if request.query_params.get('include_canceled_positions', 'false') == 'true': opq = OrderPosition.all else: opq = OrderPosition.objects if request.query_params.get('pdf_data', 'false') == 'true': prefetch_related_objects([request.organizer], 'meta_properties') prefetch_related_objects( [request.event], Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'), to_attr='meta_values_cached'), 'questions', 'item_meta_properties', ) return Prefetch( 'positions', opq.all().prefetch_related( Prefetch('checkins', queryset=Checkin.objects.all()), Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached') )), Prefetch('variation', queryset=ItemVariation.objects.prefetch_related( Prefetch('meta_values', ItemVariationMetaValue.objects.select_related('property'), to_attr='meta_values_cached') )), 'answers', 'answers__options', 'answers__question', 'item__category', 'addon_to__answers', 'addon_to__answers__options', 'addon_to__answers__question', Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related( Prefetch('meta_values', to_attr='meta_values_cached', queryset=SubEventMetaValue.objects.select_related('property')) )), Prefetch('addons', opq.select_related('item', 'variation', 'seat')), 'linked_media', ).select_related('seat', 'addon_to', 'addon_to__seat') ) else: return Prefetch( 'positions', opq.all().prefetch_related( Prefetch('checkins', queryset=Checkin.objects.all()), 'item', 'variation', Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')), 'seat', ) ) def _get_output_provider(self, identifier): responses = register_ticket_outputs.send(self.request.event) for receiver, response in responses: prov = response(self.request.event) if prov.identifier == identifier: return prov raise NotFound('Unknown output provider.') @scopes_disabled() # we are sure enough that get_queryset() is correct, so we save some perforamnce def list(self, request, **kwargs): date = serializers.DateTimeField().to_representation(now()) queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) resp = self.get_paginated_response(serializer.data) resp['X-Page-Generated'] = date return resp serializer = self.get_serializer(queryset, many=True) return Response(serializer.data, headers={'X-Page-Generated': date}) @action(detail=True, url_name='download', url_path='download/(?P[^/]+)') def download(self, request, output, **kwargs): provider = self._get_output_provider(output) order = self.get_object() if order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED): raise PermissionDenied("Downloads are not available for canceled or expired orders.") if order.status == Order.STATUS_PENDING and not (order.valid_if_pending or request.event.settings.ticket_download_pending): raise PermissionDenied("Downloads are not available for pending orders.") ct = CachedCombinedTicket.objects.filter( order=order, provider=provider.identifier, file__isnull=False ).last() if not ct or not ct.file: generate.apply_async(args=('order', order.pk, provider.identifier)) raise RetryException() else: if ct.type == 'text/uri-list': resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list') return resp else: resp = FileResponse(ct.file.file, content_type=ct.type) resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format( self.request.event.slug.upper(), order.code, provider.identifier, ct.extension ) return resp @action(detail=True, methods=['POST']) def mark_paid(self, request, **kwargs): order = self.get_object() send_mail = request.data.get('send_email', True) if request.data else True if order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED): ps = order.pending_sum try: p = order.payments.get( state__in=(OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED), provider='manual', amount=ps ) except OrderPayment.DoesNotExist: for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED)): try: with transaction.atomic(): p.payment_provider.cancel_payment(p) order.log_action('pretix.event.order.payment.canceled', { 'local_id': p.local_id, 'provider': p.provider, }, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) except PaymentException as e: order.log_action( 'pretix.event.order.payment.canceled.failed', { 'local_id': p.local_id, 'provider': p.provider, 'error': str(e) }, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth ) p = order.payments.create( state=OrderPayment.PAYMENT_STATE_CREATED, provider='manual', amount=ps, fee=None ) try: p.confirm(auth=self.request.auth, user=self.request.user if request.user.is_authenticated else None, send_mail=send_mail, count_waitinglist=False) except Quota.QuotaExceededException as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) except PaymentException as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) except SendMailException: pass return self.retrieve(request, [], **kwargs) return Response( {'detail': 'The order is not pending or expired.'}, status=status.HTTP_400_BAD_REQUEST ) @action(detail=True, methods=['POST']) def mark_canceled(self, request, **kwargs): send_mail = request.data.get('send_email', True) if request.data else True comment = request.data.get('comment', None) cancellation_fee = request.data.get('cancellation_fee', None) if cancellation_fee: try: cancellation_fee = float(Decimal(cancellation_fee)) except: cancellation_fee = None order = self.get_object() if not order.cancel_allowed(): return Response( {'detail': 'The order is not allowed to be canceled.'}, status=status.HTTP_400_BAD_REQUEST ) try: cancel_order( order, user=request.user if request.user.is_authenticated else None, api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None, device=request.auth if isinstance(request.auth, Device) else None, oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None, send_mail=send_mail, email_comment=comment, cancellation_fee=cancellation_fee ) except OrderError as e: return Response( {'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST ) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def reactivate(self, request, **kwargs): order = self.get_object() if order.status != Order.STATUS_CANCELED: return Response( {'detail': 'The order is not allowed to be reactivated.'}, status=status.HTTP_400_BAD_REQUEST ) try: reactivate_order( order, user=request.user if request.user.is_authenticated else None, auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None, ) except OrderError as e: return Response( {'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST ) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def approve(self, request, **kwargs): send_mail = request.data.get('send_email', True) if request.data else True order = self.get_object() try: approve_order( order, user=request.user if request.user.is_authenticated else None, auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None, send_mail=send_mail, ) except Quota.QuotaExceededException as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) except OrderError as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def deny(self, request, **kwargs): send_mail = request.data.get('send_email', True) if request.data else True comment = request.data.get('comment', '') order = self.get_object() try: deny_order( order, user=request.user if request.user.is_authenticated else None, auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None, send_mail=send_mail, comment=comment, ) except OrderError as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def mark_pending(self, request, **kwargs): order = self.get_object() if order.status != Order.STATUS_PAID: return Response( {'detail': 'The order is not paid.'}, status=status.HTTP_400_BAD_REQUEST ) order.status = Order.STATUS_PENDING order.save(update_fields=['status']) order.log_action( 'pretix.event.order.unpaid', user=request.user if request.user.is_authenticated else None, auth=request.auth, ) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def mark_expired(self, request, **kwargs): order = self.get_object() if order.status != Order.STATUS_PENDING: return Response( {'detail': 'The order is not pending.'}, status=status.HTTP_400_BAD_REQUEST ) mark_order_expired( order, user=request.user if request.user.is_authenticated else None, auth=request.auth, ) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def mark_refunded(self, request, **kwargs): order = self.get_object() if order.status != Order.STATUS_PAID: return Response( {'detail': 'The order is not paid.'}, status=status.HTTP_400_BAD_REQUEST ) mark_order_refunded( order, user=request.user if request.user.is_authenticated else None, auth=(request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken, Device)) else None), ) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def create_invoice(self, request, **kwargs): order = self.get_object() has_inv = order.invoices.exists() and not ( order.status in (Order.STATUS_PAID, Order.STATUS_PENDING) and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count() ) if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(order): return Response( {'detail': _('You cannot generate an invoice for this order.')}, status=status.HTTP_400_BAD_REQUEST ) elif has_inv: return Response( {'detail': _('An invoice for this order already exists.')}, status=status.HTTP_400_BAD_REQUEST ) inv = generate_invoice(order) order.log_action( 'pretix.event.order.invoice.generated', user=self.request.user, auth=self.request.auth, data={ 'invoice': inv.pk } ) return Response( InvoiceSerializer(inv).data, status=status.HTTP_201_CREATED ) @action(detail=True, methods=['POST']) def resend_link(self, request, **kwargs): order = self.get_object() if not order.email: return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST) try: order.resend_link(user=self.request.user, auth=self.request.auth) except SendMailException: return Response({'detail': _('There was an error sending the mail. Please try again later.')}, status=status.HTTP_503_SERVICE_UNAVAILABLE) return Response( status=status.HTTP_204_NO_CONTENT ) @action(detail=True, methods=['POST']) @transaction.atomic def regenerate_secrets(self, request, **kwargs): order = self.get_object() order.secret = generate_secret() for op in order.all_positions.all(): assign_ticket_secret( request.event, op, force_invalidate=True, save=True ) order.save(update_fields=['secret']) CachedTicket.objects.filter(order_position__order=order).delete() CachedCombinedTicket.objects.filter(order=order).delete() tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'order': order.pk}) order.log_action( 'pretix.event.order.secret.changed', user=self.request.user, auth=self.request.auth, ) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def extend(self, request, **kwargs): new_date = request.data.get('expires', None) force = request.data.get('force', False) if not new_date: return Response( {'detail': 'New date is missing.'}, status=status.HTTP_400_BAD_REQUEST ) df = serializers.DateField() try: new_date = df.to_internal_value(new_date) except: return Response( {'detail': 'New date is invalid.'}, status=status.HTTP_400_BAD_REQUEST ) tz = pytz.timezone(self.request.event.settings.timezone) new_date = make_aware(datetime.datetime.combine( new_date, datetime.time(hour=23, minute=59, second=59) ), tz) order = self.get_object() try: extend_order( order, new_date=new_date, force=force, user=request.user if request.user.is_authenticated else None, auth=request.auth, ) return self.retrieve(request, [], **kwargs) except OrderError as e: return Response( {'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST ) def create(self, request, *args, **kwargs): if 'send_mail' in request.data and 'send_email' not in request.data: request.data['send_email'] = request.data['send_mail'] serializer = OrderCreateSerializer(data=request.data, context=self.get_serializer_context()) serializer.is_valid(raise_exception=True) with transaction.atomic(): try: self.perform_create(serializer) except TaxRule.SaleNotAllowed: raise ValidationError(_('One of the selected products is not available in the selected country.')) send_mail = serializer._send_mail order = serializer.instance if not order.pk: # Simulation -- exit here serializer = SimulatedOrderSerializer(order, context=serializer.context) return Response(serializer.data, status=status.HTTP_201_CREATED) order.log_action( 'pretix.event.order.placed', user=request.user if request.user.is_authenticated else None, auth=request.auth, ) with language(order.locale, self.request.event.settings.region): payment = order.payments.last() order_placed.send(self.request.event, order=order) if order.status == Order.STATUS_PAID: order_paid.send(self.request.event, order=order) order.log_action( 'pretix.event.order.paid', { 'provider': payment.provider if payment else None, 'info': {}, 'date': now().isoformat(), 'force': False }, user=request.user if request.user.is_authenticated else None, auth=request.auth, ) gen_invoice = invoice_qualified(order) and ( (order.event.settings.get('invoice_generate') == 'True') or (order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID) ) and not order.invoices.last() invoice = None if gen_invoice: invoice = generate_invoice(order, trigger_pdf=True) # Refresh serializer only after running signals prefetch_related_objects([order], self._positions_prefetch(request)) serializer = OrderSerializer(order, context=serializer.context) if send_mail: free_flow = ( payment and order.total == Decimal('0.00') and order.status == Order.STATUS_PAID and not order.require_approval and payment.provider in ("free", "boxoffice") ) if order.require_approval: email_template = request.event.settings.mail_text_order_placed_require_approval subject_template = request.event.settings.mail_subject_order_placed_require_approval log_entry = 'pretix.event.order.email.order_placed_require_approval' email_attendees = False elif free_flow: email_template = request.event.settings.mail_text_order_free subject_template = request.event.settings.mail_subject_order_free log_entry = 'pretix.event.order.email.order_free' email_attendees = request.event.settings.mail_send_order_free_attendee email_attendees_template = request.event.settings.mail_text_order_free_attendee subject_attendees_template = request.event.settings.mail_subject_order_free_attendee else: email_template = request.event.settings.mail_text_order_placed subject_template = request.event.settings.mail_subject_order_placed log_entry = 'pretix.event.order.email.order_placed' email_attendees = request.event.settings.mail_send_order_placed_attendee email_attendees_template = request.event.settings.mail_text_order_placed_attendee subject_attendees_template = request.event.settings.mail_subject_order_placed_attendee _order_placed_email( request.event, order, email_template, subject_template, log_entry, invoice, [payment] if payment else [], is_free=free_flow ) if email_attendees: for p in order.positions.all(): if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email: _order_placed_email_attendee(request.event, order, p, email_attendees_template, subject_attendees_template, log_entry, is_free=free_flow) if not free_flow and order.status == Order.STATUS_PAID and payment: payment._send_paid_mail(invoice, None, '') if self.request.event.settings.mail_send_order_paid_attendee: for p in order.positions.all(): if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email: payment._send_paid_mail_attendee(p, None) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def update(self, request, *args, **kwargs): partial = kwargs.get('partial', False) if not partial: return Response( {"detail": "Method \"PUT\" not allowed."}, status=status.HTTP_405_METHOD_NOT_ALLOWED, ) return super().update(request, *args, **kwargs) def perform_update(self, serializer): with transaction.atomic(): if 'comment' in self.request.data and serializer.instance.comment != self.request.data.get('comment'): serializer.instance.log_action( 'pretix.event.order.comment', user=self.request.user, auth=self.request.auth, data={ 'new_comment': self.request.data.get('comment') } ) if 'custom_followup_at' in self.request.data and serializer.instance.custom_followup_at != self.request.data.get('custom_followup_at'): serializer.instance.log_action( 'pretix.event.order.custom_followup_at', user=self.request.user, auth=self.request.auth, data={ 'new_custom_followup_at': self.request.data.get('custom_followup_at') } ) if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'): serializer.instance.log_action( 'pretix.event.order.checkin_attention', user=self.request.user, auth=self.request.auth, data={ 'new_value': self.request.data.get('checkin_attention') } ) if 'valid_if_pending' in self.request.data and serializer.instance.valid_if_pending != self.request.data.get('valid_if_pending'): serializer.instance.log_action( 'pretix.event.order.valid_if_pending', user=self.request.user, auth=self.request.auth, data={ 'new_value': self.request.data.get('valid_if_pending') } ) if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'): serializer.instance.email_known_to_work = False serializer.instance.log_action( 'pretix.event.order.contact.changed', user=self.request.user, auth=self.request.auth, data={ 'old_email': serializer.instance.email, 'new_email': self.request.data.get('email'), } ) if 'phone' in self.request.data and serializer.instance.phone != self.request.data.get('phone'): serializer.instance.log_action( 'pretix.event.order.phone.changed', user=self.request.user, auth=self.request.auth, data={ 'old_phone': serializer.instance.phone, 'new_phone': self.request.data.get('phone'), } ) if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'): serializer.instance.log_action( 'pretix.event.order.locale.changed', user=self.request.user, auth=self.request.auth, data={ 'old_locale': serializer.instance.locale, 'new_locale': self.request.data.get('locale'), } ) if 'invoice_address' in self.request.data: serializer.instance.log_action( 'pretix.event.order.modified', user=self.request.user, auth=self.request.auth, data={ 'invoice_data': self.request.data.get('invoice_address'), } ) serializer.save() tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.event.pk, 'order': serializer.instance.pk}) if 'invoice_address' in self.request.data: order_modified.send(sender=serializer.instance.event, order=serializer.instance) def perform_create(self, serializer): serializer.save() def perform_destroy(self, instance): if not instance.testmode: raise PermissionDenied('Only test mode orders can be deleted.') with transaction.atomic(): self.get_object().gracefully_delete(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) @action(detail=True, methods=['POST']) def change(self, request, **kwargs): order = self.get_object() serializer = OrderChangeOperationSerializer( context={'order': order, **self.get_serializer_context()}, data=request.data, ) serializer.is_valid(raise_exception=True) try: ocm = OrderChangeManager( order=order, user=self.request.user if self.request.user.is_authenticated else None, auth=request.auth, notify=serializer.validated_data.get('send_email', False), reissue_invoice=serializer.validated_data.get('reissue_invoice', True), ) canceled_positions = set() for r in serializer.validated_data.get('cancel_positions', []): ocm.cancel(r['position']) canceled_positions.add(r['position']) for r in serializer.validated_data.get('patch_positions', []): if r['position'] in canceled_positions: continue pos_serializer = OrderPositionChangeSerializer( context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()}, partial=True, ) pos_serializer.update(r['position'], r['body']) for r in serializer.validated_data.get('split_positions', []): if r['position'] in canceled_positions: continue ocm.split(r['position']) for r in serializer.validated_data.get('create_positions', []): pos_serializer = OrderPositionCreateForExistingOrderSerializer( context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()}, ) pos_serializer.create(r) canceled_fees = set() for r in serializer.validated_data.get('cancel_fees', []): ocm.cancel_fee(r['fee']) canceled_fees.add(r['fee']) for r in serializer.validated_data.get('patch_fees', []): if r['fee'] in canceled_fees: continue pos_serializer = OrderFeeChangeSerializer( context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()}, ) pos_serializer.update(r['fee'], r['body']) if serializer.validated_data.get('recalculate_taxes') == 'keep_net': ocm.recalculate_taxes(keep='net') elif serializer.validated_data.get('recalculate_taxes') == 'keep_gross': ocm.recalculate_taxes(keep='gross') ocm.commit() except OrderError as e: raise ValidationError(str(e)) order.refresh_from_db() serializer = OrderSerializer( instance=order, context=self.get_serializer_context(), ) return Response(serializer.data) with scopes_disabled(): class OrderPositionFilter(FilterSet): order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact') has_checkin = django_filters.rest_framework.BooleanFilter(method='has_checkin_qs') attendee_name = django_filters.CharFilter(method='attendee_name_qs') search = django_filters.CharFilter(method='search_qs') def search_qs(self, queryset, name, value): return queryset.filter( Q(secret__istartswith=value) | Q(attendee_name_cached__icontains=value) | Q(addon_to__attendee_name_cached__icontains=value) | Q(attendee_email__icontains=value) | Q(addon_to__attendee_email__icontains=value) | Q(order__code__istartswith=value) | Q(order__invoice_address__name_cached__icontains=value) | Q(order__email__icontains=value) ) def has_checkin_qs(self, queryset, name, value): return queryset.alias(ce=Exists(Checkin.objects.filter(position=OuterRef('pk')))).filter(ce=value) def attendee_name_qs(self, queryset, name, value): return queryset.filter(Q(attendee_name_cached__iexact=value) | Q(addon_to__attendee_name_cached__iexact=value)) class Meta: model = OrderPosition fields = { 'item': ['exact', 'in'], 'variation': ['exact', 'in'], 'secret': ['exact'], 'order__status': ['exact', 'in'], 'addon_to': ['exact', 'in'], 'subevent': ['exact', 'in'], 'pseudonymization_id': ['exact'], 'voucher__code': ['exact'], 'voucher': ['exact'], } class OrderPositionViewSet(viewsets.ModelViewSet): serializer_class = OrderPositionSerializer queryset = OrderPosition.all.none() filter_backends = (DjangoFilterBackend, RichOrderingFilter) ordering = ('order__datetime', 'positionid') ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',) filterset_class = OrderPositionFilter permission = 'can_view_orders' write_permission = 'can_change_orders' ordering_custom = { 'attendee_name': { '_order': F('display_name').asc(nulls_first=True), 'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached') }, '-attendee_name': { '_order': F('display_name').asc(nulls_last=True), 'display_name': Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached') }, } def get_serializer_context(self): ctx = super().get_serializer_context() ctx['event'] = self.request.event ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false') == 'true' return ctx def get_queryset(self): if self.request.query_params.get('include_canceled_positions', 'false') == 'true': qs = OrderPosition.all else: qs = OrderPosition.objects qs = qs.filter(order__event=self.request.event) if self.request.query_params.get('pdf_data', 'false') == 'true': prefetch_related_objects([self.request.organizer], 'meta_properties') prefetch_related_objects( [self.request.event], Prefetch('meta_values', queryset=EventMetaValue.objects.select_related('property'), to_attr='meta_values_cached'), 'questions', 'item_meta_properties', ) qs = qs.prefetch_related( Prefetch('checkins', queryset=Checkin.objects.all()), Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached') )), 'variation', 'answers', 'answers__options', 'answers__question', 'item__category', 'addon_to__answers', 'addon_to__answers__options', 'addon_to__answers__question', Prefetch('addons', qs.select_related('item', 'variation')), Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related( Prefetch('meta_values', to_attr='meta_values_cached', queryset=SubEventMetaValue.objects.select_related('property')) )), 'linked_media', Prefetch('order', self.request.event.orders.select_related('invoice_address').prefetch_related( Prefetch( 'positions', qs.prefetch_related( Prefetch('checkins', queryset=Checkin.objects.all()), Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached') )), Prefetch('variation', queryset=self.request.event.items.prefetch_related( Prefetch('meta_values', ItemVariationMetaValue.objects.select_related('property'), to_attr='meta_values_cached') )), 'answers', 'answers__options', 'answers__question', 'item__category', Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related( Prefetch('meta_values', to_attr='meta_values_cached', queryset=SubEventMetaValue.objects.select_related('property')) )), Prefetch('addons', qs.select_related('item', 'variation', 'seat')) ).select_related('addon_to', 'seat', 'addon_to__seat') ) )) ).select_related( 'addon_to', 'seat', 'addon_to__seat' ) else: qs = qs.prefetch_related( Prefetch('checkins', queryset=Checkin.objects.all()), 'answers', 'answers__options', 'answers__question', ).select_related( 'item', 'order', 'order__event', 'order__event__organizer', 'seat' ) return qs def _get_output_provider(self, identifier): responses = register_ticket_outputs.send(self.request.event) for receiver, response in responses: prov = response(self.request.event) if prov.identifier == identifier: return prov raise NotFound('Unknown output provider.') @action(detail=True, methods=['POST'], url_name='price_calc') def price_calc(self, request, *args, **kwargs): """ This calculates the price assuming a change of product or subevent. This endpoint is deliberately not documented and considered a private API, only to be used by pretix' web interface. Sample input: { "item": 2, "variation": null, "subevent": 3, "tax_rule": 4, } Sample output: { "gross": "2.34", "gross_formatted": "2,34", "net": "2.34", "tax": "0.00", "rate": "0.00", "name": "VAT" } """ serializer = PriceCalcSerializer(data=request.data, event=request.event) serializer.is_valid(raise_exception=True) data = serializer.validated_data pos = self.get_object() try: ia = pos.order.invoice_address except InvoiceAddress.DoesNotExist: ia = InvoiceAddress() kwargs = { 'item': pos.item, 'variation': pos.variation, 'voucher': pos.voucher, 'subevent': pos.subevent, 'addon_to': pos.addon_to, 'invoice_address': ia, } if data.get('item'): item = data.get('item') kwargs['item'] = item if item.has_variations: variation = data.get('variation') or pos.variation if not variation: raise ValidationError('No variation given') if variation.item != item: raise ValidationError('Variation does not belong to item') kwargs['variation'] = variation else: variation = None kwargs['variation'] = None if pos.voucher and not pos.voucher.applies_to(item, variation): kwargs['voucher'] = None if data.get('subevent'): kwargs['subevent'] = data.get('subevent') if data.get('tax_rule'): kwargs['tax_rule'] = data.get('tax_rule') price = get_price(**kwargs) tr = kwargs.get('tax_rule', kwargs.get('item').tax_rule) with language(data.get('locale') or self.request.event.settings.locale, self.request.event.settings.region): return Response({ 'gross': price.gross, 'gross_formatted': money_filter(price.gross, self.request.event.currency, hide_currency=True), 'net': price.net, 'rate': price.rate, 'name': str(price.name), 'tax': price.tax, 'tax_rule': tr.pk if tr else None, }) @action(detail=True, url_name='answer', url_path=r'answer/(?P\d+)') def answer(self, request, **kwargs): pos = self.get_object() answer = get_object_or_404( QuestionAnswer, orderposition=self.get_object(), question_id=kwargs.get('question') ) if not answer.file: raise NotFound() ftype, ignored = mimetypes.guess_type(answer.file.name) resp = FileResponse(answer.file, content_type=ftype or 'application/binary') resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format( self.request.event.slug.upper(), pos.order.code, pos.positionid, os.path.basename(answer.file.name).split('.', 1)[1] ) return resp @action(detail=True, url_name='pdf_image', url_path=r'pdf_image/(?P[^/]+)') def pdf_image(self, request, key, **kwargs): pos = self.get_object() image_vars = get_images(request.event) if key not in image_vars: raise NotFound('Unknown key') image_file = image_vars[key]['evaluate'](pos, pos.order, pos.subevent or self.request.event) if image_file is None: raise NotFound('No image available') if getattr(image_file, 'name', ''): ftype, ignored = mimetypes.guess_type(image_file.name) extension = os.path.basename(image_file.name).split('.')[-1] else: img = Image.open(image_file) ftype = Image.MIME[img.format] extensions = { 'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png' } extension = extensions.get(img.format, 'bin') if hasattr(image_file, 'seek'): image_file.seek(0) resp = FileResponse(image_file, content_type=ftype or 'application/binary') resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format( self.request.event.slug.upper(), pos.order.code, pos.positionid, key, extension, ) return resp @action(detail=True, url_name='download', url_path='download/(?P[^/]+)') def download(self, request, output, **kwargs): provider = self._get_output_provider(output) pos = self.get_object() if pos.order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED): raise PermissionDenied("Downloads are not available for canceled or expired orders.") if pos.order.status == Order.STATUS_PENDING and not (pos.order.valid_if_pending or request.event.settings.ticket_download_pending): raise PermissionDenied("Downloads are not available for pending orders.") if not pos.generate_ticket: raise PermissionDenied("Downloads are not enabled for this product.") ct = CachedTicket.objects.filter( order_position=pos, provider=provider.identifier, file__isnull=False ).last() if not ct or not ct.file: generate.apply_async(args=('orderposition', pos.pk, provider.identifier)) raise RetryException() else: if ct.type == 'text/uri-list': resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list') return resp else: resp = FileResponse(ct.file.file, content_type=ct.type) resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format( self.request.event.slug.upper(), pos.order.code, pos.positionid, provider.identifier, ct.extension ) return resp @action(detail=True, methods=['POST']) def regenerate_secrets(self, request, **kwargs): instance = self.get_object() try: ocm = OrderChangeManager( instance.order, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth, notify=False, reissue_invoice=False, ) ocm.regenerate_secret(instance) ocm.commit() except OrderError as e: raise ValidationError(str(e)) except Quota.QuotaExceededException as e: raise ValidationError(str(e)) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def add_block(self, request, **kwargs): serializer = BlockNameSerializer( data=request.data, context=self.get_serializer_context(), ) serializer.is_valid(raise_exception=True) instance = self.get_object() try: ocm = OrderChangeManager( instance.order, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth, notify=False, reissue_invoice=False, ) ocm.add_block(instance, serializer.validated_data['name']) ocm.commit() except OrderError as e: raise ValidationError(str(e)) except Quota.QuotaExceededException as e: raise ValidationError(str(e)) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def remove_block(self, request, **kwargs): serializer = BlockNameSerializer( data=request.data, context=self.get_serializer_context(), ) serializer.is_valid(raise_exception=True) instance = self.get_object() try: ocm = OrderChangeManager( instance.order, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth, notify=False, reissue_invoice=False, ) ocm.remove_block(instance, serializer.validated_data['name']) ocm.commit() except OrderError as e: raise ValidationError(str(e)) except Quota.QuotaExceededException as e: raise ValidationError(str(e)) return self.retrieve(request, [], **kwargs) def perform_destroy(self, instance): try: ocm = OrderChangeManager( instance.order, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth, notify=False ) ocm.cancel(instance) ocm.commit() except OrderError as e: raise ValidationError(str(e)) except Quota.QuotaExceededException as e: raise ValidationError(str(e)) def create(self, request, *args, **kwargs): with transaction.atomic(): serializer = OrderPositionCreateForExistingOrderSerializer( data=request.data, context=self.get_serializer_context(), ) serializer.is_valid(raise_exception=True) order = serializer.validated_data['order'] ocm = OrderChangeManager( order=order, user=self.request.user if self.request.user.is_authenticated else None, auth=request.auth, notify=False, reissue_invoice=False, ) serializer.context['ocm'] = ocm serializer.save() # Fields that can be easily patched after the position was added old_data = OrderPositionInfoPatchSerializer(instance=serializer.instance, context=self.get_serializer_context()).data serializer = OrderPositionInfoPatchSerializer( instance=serializer.instance, context=self.get_serializer_context(), partial=True, data=request.data ) serializer.is_valid(raise_exception=True) serializer.save() new_data = serializer.data if old_data != new_data: log_data = self.request.data if 'answers' in log_data: for a in new_data['answers']: log_data[f'question_{a["question"]}'] = a["answer"] log_data.pop('answers', None) serializer.instance.order.log_action( 'pretix.event.order.modified', user=self.request.user, auth=self.request.auth, data={ 'data': [ dict( position=serializer.instance.pk, **log_data ) ] } ) tickets.invalidate_cache.apply_async( kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk}) order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order) return Response( OrderPositionSerializer(serializer.instance, context=self.get_serializer_context()).data, status=status.HTTP_201_CREATED, ) def update(self, request, *args, **kwargs): partial = kwargs.get('partial', False) if not partial: return Response( {"detail": "Method \"PUT\" not allowed."}, status=status.HTTP_405_METHOD_NOT_ALLOWED, ) with transaction.atomic(): instance = self.get_object() ocm = OrderChangeManager( order=instance.order, user=self.request.user if self.request.user.is_authenticated else None, auth=request.auth, notify=False, reissue_invoice=False, ) # Field that need to go through OrderChangeManager serializer = OrderPositionChangeSerializer( instance=instance, context={'ocm': ocm, **self.get_serializer_context()}, partial=True, data=request.data ) serializer.is_valid(raise_exception=True) serializer.save() # Fields that can be easily patched old_data = OrderPositionInfoPatchSerializer(instance=instance, context=self.get_serializer_context()).data serializer = OrderPositionInfoPatchSerializer( instance=instance, context=self.get_serializer_context(), partial=True, data=request.data ) serializer.is_valid(raise_exception=True) serializer.save() new_data = serializer.data if old_data != new_data: log_data = self.request.data if 'answers' in log_data: for a in new_data['answers']: log_data[f'question_{a["question"]}'] = a["answer"] log_data.pop('answers', None) serializer.instance.order.log_action( 'pretix.event.order.modified', user=self.request.user, auth=self.request.auth, data={ 'data': [ dict( position=serializer.instance.pk, **log_data ) ] } ) tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk}) order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order) return Response(self.get_serializer_class()(instance=serializer.instance, context=self.get_serializer_context()).data) class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): serializer_class = OrderPaymentSerializer queryset = OrderPayment.objects.none() permission = 'can_view_orders' write_permission = 'can_change_orders' lookup_field = 'local_id' def get_serializer_context(self): ctx = super().get_serializer_context() ctx['order'] = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event) ctx['event'] = self.request.event return ctx def get_queryset(self): order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event) return order.payments.all() def create(self, request, *args, **kwargs): send_mail = request.data.get('send_email', True) if request.data else True serializer = OrderPaymentCreateSerializer(data=request.data, context=self.get_serializer_context()) serializer.is_valid(raise_exception=True) with transaction.atomic(): mark_confirmed = False if serializer.validated_data['state'] == OrderPayment.PAYMENT_STATE_CONFIRMED: serializer.validated_data['state'] = OrderPayment.PAYMENT_STATE_PENDING mark_confirmed = True self.perform_create(serializer) r = serializer.instance if mark_confirmed: try: r.confirm( user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth, count_waitinglist=False, force=request.data.get('force', False), send_mail=send_mail, ) except Quota.QuotaExceededException: pass except SendMailException: pass serializer = OrderPaymentSerializer(r, context=serializer.context) r.order.log_action( 'pretix.event.order.payment.started', { 'local_id': r.local_id, 'provider': r.provider, }, user=request.user if request.user.is_authenticated else None, auth=request.auth ) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() @action(detail=True, methods=['POST']) def confirm(self, request, **kwargs): payment = self.get_object() force = request.data.get('force', False) send_mail = request.data.get('send_email', True) if request.data else True if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED): return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST) try: payment.confirm(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth, count_waitinglist=False, send_mail=send_mail, force=force) except Quota.QuotaExceededException as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) except PaymentException as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) except SendMailException: pass return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def refund(self, request, **kwargs): payment = self.get_object() amount = serializers.DecimalField(max_digits=13, decimal_places=2).to_internal_value( request.data.get('amount', str(payment.amount)) ) if 'mark_refunded' in request.data: mark_refunded = request.data.get('mark_refunded', False) else: mark_refunded = request.data.get('mark_canceled', False) if payment.state != OrderPayment.PAYMENT_STATE_CONFIRMED: return Response({'detail': 'Invalid state of payment.'}, status=status.HTTP_400_BAD_REQUEST) full_refund_possible = payment.payment_provider.payment_refund_supported(payment) partial_refund_possible = payment.payment_provider.payment_partial_refund_supported(payment) available_amount = payment.amount - payment.refunded_amount if amount <= 0: return Response({'amount': ['Invalid refund amount.']}, status=status.HTTP_400_BAD_REQUEST) if amount > available_amount: return Response( {'amount': ['Invalid refund amount, only {} are available to refund.'.format(available_amount)]}, status=status.HTTP_400_BAD_REQUEST) if amount != payment.amount and not partial_refund_possible: return Response({'amount': ['Partial refund not available for this payment method.']}, status=status.HTTP_400_BAD_REQUEST) if amount == payment.amount and not full_refund_possible: return Response({'amount': ['Full refund not available for this payment method.']}, status=status.HTTP_400_BAD_REQUEST) r = payment.order.refunds.create( payment=payment, source=OrderRefund.REFUND_SOURCE_ADMIN, state=OrderRefund.REFUND_STATE_CREATED, amount=amount, provider=payment.provider, info='{}', ) payment.order.log_action('pretix.event.order.refund.created', { 'local_id': r.local_id, 'provider': r.provider, }, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) try: r.payment_provider.execute_refund(r) except PaymentException as e: r.state = OrderRefund.REFUND_STATE_FAILED r.save() payment.order.log_action('pretix.event.order.refund.failed', { 'local_id': r.local_id, 'provider': r.provider, 'error': str(e) }) return Response({'detail': 'External error: {}'.format(str(e))}, status=status.HTTP_400_BAD_REQUEST) else: if payment.order.pending_sum > 0: if mark_refunded: mark_order_refunded(payment.order, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) else: payment.order.status = Order.STATUS_PENDING payment.order.set_expires( now(), payment.order.event.subevents.filter( id__in=payment.order.positions.values_list('subevent_id', flat=True)) ) payment.order.save(update_fields=['status', 'expires']) return Response(OrderRefundSerializer(r).data, status=status.HTTP_200_OK) @action(detail=True, methods=['POST']) def cancel(self, request, **kwargs): payment = self.get_object() if payment.state not in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED): return Response({'detail': 'Invalid state of payment'}, status=status.HTTP_400_BAD_REQUEST) try: with transaction.atomic(): payment.payment_provider.cancel_payment(payment) payment.order.log_action('pretix.event.order.payment.canceled', { 'local_id': payment.local_id, 'provider': payment.provider, }, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) except PaymentException as e: return Response({'detail': 'External error: {}'.format(str(e))}, status=status.HTTP_400_BAD_REQUEST) return self.retrieve(request, [], **kwargs) class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): serializer_class = OrderRefundSerializer queryset = OrderRefund.objects.none() permission = 'can_view_orders' write_permission = 'can_change_orders' lookup_field = 'local_id' def get_queryset(self): order = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event) return order.refunds.all() @action(detail=True, methods=['POST']) def cancel(self, request, **kwargs): refund = self.get_object() if refund.state not in (OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_EXTERNAL): return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST) with transaction.atomic(): refund.state = OrderRefund.REFUND_STATE_CANCELED refund.save() refund.order.log_action('pretix.event.order.refund.canceled', { 'local_id': refund.local_id, 'provider': refund.provider, }, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def process(self, request, **kwargs): refund = self.get_object() if refund.state != OrderRefund.REFUND_STATE_EXTERNAL: return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST) refund.done(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) if 'mark_refunded' in request.data: mark_refunded = request.data.get('mark_refunded', False) else: mark_refunded = request.data.get('mark_canceled', False) if mark_refunded: mark_order_refunded(refund.order, user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) elif not (refund.order.status == Order.STATUS_PAID and refund.order.pending_sum <= 0): refund.order.status = Order.STATUS_PENDING refund.order.set_expires( now(), refund.order.event.subevents.filter( id__in=refund.order.positions.values_list('subevent_id', flat=True)) ) refund.order.save(update_fields=['status', 'expires']) return self.retrieve(request, [], **kwargs) @action(detail=True, methods=['POST']) def done(self, request, **kwargs): refund = self.get_object() if refund.state not in (OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT): return Response({'detail': 'Invalid state of refund'}, status=status.HTTP_400_BAD_REQUEST) refund.done(user=self.request.user if self.request.user.is_authenticated else None, auth=self.request.auth) return self.retrieve(request, [], **kwargs) def get_serializer_context(self): ctx = super().get_serializer_context() ctx['order'] = get_object_or_404(Order, code=self.kwargs['order'], event=self.request.event) return ctx def create(self, request, *args, **kwargs): if 'mark_refunded' in request.data: mark_refunded = request.data.pop('mark_refunded', False) else: mark_refunded = request.data.pop('mark_canceled', False) mark_pending = request.data.pop('mark_pending', False) serializer = OrderRefundCreateSerializer(data=request.data, context=self.get_serializer_context()) serializer.is_valid(raise_exception=True) with transaction.atomic(): self.perform_create(serializer) r = serializer.instance serializer = OrderRefundSerializer(r, context=serializer.context) r.order.log_action( 'pretix.event.order.refund.created', { 'local_id': r.local_id, 'provider': r.provider, }, user=request.user if request.user.is_authenticated else None, auth=request.auth ) if r.state in (OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_CANCELED, OrderRefund.REFUND_STATE_FAILED): r.order.log_action( f'pretix.event.order.refund.{r.state}', { 'local_id': r.local_id, 'provider': r.provider, }, user=request.user if request.user.is_authenticated else None, auth=request.auth ) if mark_refunded: try: mark_order_refunded( r.order, user=request.user if request.user.is_authenticated else None, auth=(request.auth if request.auth else None), ) except OrderError as e: raise ValidationError(str(e)) elif mark_pending: if r.order.status == Order.STATUS_PAID and r.order.pending_sum > 0: r.order.status = Order.STATUS_PENDING r.order.set_expires( now(), r.order.event.subevents.filter( id__in=r.order.positions.values_list('subevent_id', flat=True)) ) r.order.save(update_fields=['status', 'expires']) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() with scopes_disabled(): class InvoiceFilter(FilterSet): refers = django_filters.CharFilter(method='refers_qs') number = django_filters.CharFilter(method='nr_qs') order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact') def refers_qs(self, queryset, name, value): return queryset.annotate( refers_nr=Concat('refers__prefix', 'refers__invoice_no') ).filter(refers_nr__iexact=value) def nr_qs(self, queryset, name, value): return queryset.filter(nr__iexact=value) class Meta: model = Invoice fields = ['order', 'number', 'is_cancellation', 'refers', 'locale'] class RetryException(APIException): status_code = 409 default_detail = 'The requested resource is not ready, please retry later.' default_code = 'retry_later' class InvoiceViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = InvoiceSerializer queryset = Invoice.objects.none() filter_backends = (DjangoFilterBackend, TotalOrderingFilter) ordering = ('nr',) ordering_fields = ('nr', 'date') filterset_class = InvoiceFilter permission = 'can_view_orders' lookup_url_kwarg = 'number' lookup_field = 'nr' write_permission = 'can_change_orders' def get_queryset(self): return self.request.event.invoices.prefetch_related('lines').select_related('order', 'refers').annotate( nr=Concat('prefix', 'invoice_no') ) @action(detail=True, ) def download(self, request, **kwargs): invoice = self.get_object() if not invoice.file: invoice_pdf(invoice.pk) invoice.refresh_from_db() if invoice.shredded: raise PermissionDenied('The invoice file is no longer stored on the server.') if not invoice.file: raise RetryException() resp = FileResponse(invoice.file.file, content_type='application/pdf') resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number) return resp @action(detail=True, methods=['POST']) def regenerate(self, request, **kwarts): inv = self.get_object() if inv.canceled: raise ValidationError('The invoice has already been canceled.') if not inv.event.settings.invoice_regenerate_allowed: raise PermissionDenied('Invoices may not be changed after they are created.') elif inv.shredded: raise PermissionDenied('The invoice file is no longer stored on the server.') elif inv.sent_to_organizer: raise PermissionDenied('The invoice file has already been exported.') elif now().astimezone(self.request.event.timezone).date() - inv.date > datetime.timedelta(days=1): raise PermissionDenied('The invoice file is too old to be regenerated.') else: inv = regenerate_invoice(inv) inv.order.log_action( 'pretix.event.order.invoice.regenerated', data={ 'invoice': inv.pk }, user=self.request.user, auth=self.request.auth, ) return Response(status=204) @action(detail=True, methods=['POST']) def reissue(self, request, **kwarts): inv = self.get_object() if inv.canceled: raise ValidationError('The invoice has already been canceled.') elif inv.shredded: raise PermissionDenied('The invoice file is no longer stored on the server.') else: c = generate_cancellation(inv) if inv.order.status != Order.STATUS_CANCELED: inv = generate_invoice(inv.order) else: inv = c inv.order.log_action( 'pretix.event.order.invoice.reissued', data={ 'invoice': inv.pk }, user=self.request.user, auth=self.request.auth, ) return Response(status=204) with scopes_disabled(): class RevokedSecretFilter(FilterSet): created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte') class Meta: model = RevokedTicketSecret fields = [] class RevokedSecretViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = RevokedTicketSecretSerializer queryset = RevokedTicketSecret.objects.none() filter_backends = (DjangoFilterBackend, TotalOrderingFilter) ordering = ('-created',) ordering_fields = ('created', 'secret') filterset_class = RevokedSecretFilter permission = 'can_view_orders' write_permission = 'can_change_orders' def get_queryset(self): return RevokedTicketSecret.objects.filter(event=self.request.event) with scopes_disabled(): class BlockedSecretFilter(FilterSet): updated_since = django_filters.IsoDateTimeFilter(field_name='updated', lookup_expr='gte') class Meta: model = BlockedTicketSecret fields = ['blocked'] class BlockedSecretViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = BlockedTicketSecretSerializer queryset = BlockedTicketSecret.objects.none() filter_backends = (DjangoFilterBackend, TotalOrderingFilter) ordering = ('-updated', '-pk') filterset_class = BlockedSecretFilter permission = 'can_view_orders' write_permission = 'can_change_orders' def get_queryset(self): return BlockedTicketSecret.objects.filter(event=self.request.event)