diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 233c4ce524..6cf6937d48 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -61,9 +61,10 @@ invoice_address object Invoice address └ vat_id_validated string ``true``, if the VAT ID has been validated against the EU VAT service and validation was successful. This only happens in rare cases. -positions list of objects List of non-canceled order positions (see below) -fees list of objects List of non-canceled fees included in the order total - (i.e. payment fees) +positions list of objects List of order positions (see below). By default, only + non-canceled positions are included. +fees list of objects List of fees included in the order total. By default, only + non-canceled fees are included. ├ fee_type string Type of fee (currently ``payment``, ``passbook``, ``other``) ├ value money (string) Fee amount @@ -72,7 +73,8 @@ fees list of objects List of non-can can be empty ├ tax_rate decimal (string) VAT rate applied for this fee ├ tax_value money (string) VAT included in this fee -└ tax_rule integer The ID of the used tax rule (or ``null``) +├ tax_rule integer The ID of the used tax rule (or ``null``) +└ canceled boolean Whether or not this fee has been canceled. downloads list of objects List of ticket download options for order-wise ticket downloading. This might be a multi-page PDF or a ZIP file of tickets for outputs that do not support @@ -145,6 +147,10 @@ last_modified datetime Last modificati The ``invoice_address.state`` and ``url`` attributes have been added. When creating orders through the API, vouchers are now supported and many fields are now optional. +.. versionchanged:: 3.5 + + The ``order.fees.canceled`` attribute has been added. + .. _order-position-resource: @@ -159,6 +165,8 @@ Field Type Description id integer Internal ID of the order position order string Order code of the order the position belongs to positionid integer Number of the position within the order +canceled boolean Whether or not this position has been canceled. Note that + by default, only non-canceled positions are shown. item integer ID of the purchased item variation integer ID of the purchased variation (or ``null``) price money (string) Price of this position @@ -224,6 +232,10 @@ pdf_data object Data object req The ``url`` of a ticket ``download`` can now also return a ``text/uri-list`` instead of a file. See :ref:`order-position-ticket-download` for details. +.. versionchanged:: 3.5 + + The attribute ``canceled`` has been added. + .. _order-payment-resource: Order payment resource @@ -290,6 +302,10 @@ List of all orders Filtering for emails or order codes is now case-insensitive. +.. versionchanged:: 3.5 + + The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added. + .. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/ Returns a list of all orders within a given event. @@ -355,6 +371,7 @@ List of all orders "id": 23442, "order": "ABC12", "positionid": 1, + "canceled": false, "item": 1345, "variation": null, "price": "23.00", @@ -427,6 +444,9 @@ List of all orders :query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false`` :query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field ``require_approval`` will be returned. + :query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this + only affects position-level cancellations, not fully-canceled orders. + :query include_canceled_fees: If set to ``true``, the output will contain cancaled order fees. :query string email: Only return orders created with the given email address :query string locale: Only return orders with the given customer locale :query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only @@ -444,6 +464,10 @@ List of all orders Fetching individual orders -------------------------- +.. versionchanged:: 3.5 + + The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added. + .. http:get:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/ Returns information on one order, identified by its order code. @@ -503,6 +527,7 @@ Fetching individual orders "id": 23442, "order": "ABC12", "positionid": 1, + "canceled": false, "item": 1345, "variation": null, "price": "23.00", @@ -568,6 +593,9 @@ Fetching individual orders :param organizer: The ``slug`` field of the organizer to fetch :param event: The ``slug`` field of the event to fetch :param code: The ``code`` field of the order to fetch + :query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this + only affects position-level cancellations, not fully-canceled orders. + :query include_canceled_fees: If set to ``true``, the output will contain cancaled order fees. :statuscode 200: no error :statuscode 401: Authentication failure :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. @@ -1313,8 +1341,9 @@ List of all order positions The value ``auto_checked_in`` has been added to the ``checkins``-attribute. +.. versionchanged:: 3.5 -.. note:: Individually canceled order positions are currently not visible via the API at all. + The ``include_canceled_positions`` and ``include_canceled_fees`` query parameters have been added. .. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/ @@ -1345,6 +1374,7 @@ List of all order positions "id": 23442, "order": "ABC12", "positionid": 1, + "canceled": false, "item": 1345, "variation": null, "price": "23.00", @@ -1414,6 +1444,8 @@ List of all order positions comma-separated IDs. :query string voucher: Only return positions with a specific voucher. :query string voucher__code: Only return positions with a specific voucher code. + :query include_canceled_positions: If set to ``true``, the output will contain canceled order positions. Note that this + only affects position-level cancellations, not fully-canceled orders. :param organizer: The ``slug`` field of the organizer to fetch :param event: The ``slug`` field of the event to fetch :statuscode 200: no error @@ -1447,6 +1479,7 @@ Fetching individual positions "id": 23442, "order": "ABC12", "positionid": 1, + "canceled": false, "item": 1345, "variation": null, "price": "23.00", @@ -1491,6 +1524,7 @@ Fetching individual positions :param organizer: The ``slug`` field of the organizer to fetch :param event: The ``slug`` field of the event to fetch :param id: The ``id`` field of the order position to fetch + :query include_canceled_positions: If set to ``true``, canceled positions may be returned (otherwise, they return 404). :statuscode 200: no error :statuscode 401: Authentication failure :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index bb77475506..178ad4789f 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -204,7 +204,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer): model = OrderPosition fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', - 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat') + 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -284,7 +284,7 @@ class OrderPaymentDateField(serializers.DateField): class OrderFeeSerializer(I18nAwareModelSerializer): class Meta: model = OrderFee - fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule') + fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule', 'canceled') class PaymentURLField(serializers.URLField): diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index f855ae7d23..915e8aed5c 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -30,8 +30,8 @@ from pretix.api.serializers.order import ( from pretix.base.i18n import language from pretix.base.models import ( CachedCombinedTicket, CachedTicket, Device, Event, Invoice, InvoiceAddress, - Order, OrderPayment, OrderPosition, OrderRefund, Quota, TeamAPIToken, - generate_position_secret, generate_secret, + Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, Quota, + TeamAPIToken, generate_position_secret, generate_secret, ) from pretix.base.payment import PaymentException from pretix.base.services import tickets @@ -82,20 +82,29 @@ class OrderViewSet(viewsets.ModelViewSet): return ctx def get_queryset(self): + if self.request.query_params.get('include_canceled_fees', 'false') == 'true': + fqs = OrderFee.all + else: + fqs = OrderFee.objects qs = self.request.event.orders.prefetch_related( - 'fees', 'payments', 'refunds', 'refunds__payment' + Prefetch('fees', queryset=fqs.all()), + 'payments', 'refunds', 'refunds__payment' ).select_related( 'invoice_address' ) + if self.request.query_params.get('include_canceled_positions', 'false') == 'true': + opq = OrderPosition.all + else: + opq = OrderPosition.objects if self.request.query_params.get('pdf_data', 'false') == 'true': qs = qs.prefetch_related( Prefetch( 'positions', - OrderPosition.objects.all().prefetch_related( + opq.all().prefetch_related( 'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'item__category', 'addon_to', 'seat', - Prefetch('addons', OrderPosition.objects.select_related('item', 'variation', 'seat')) + Prefetch('addons', opq.select_related('item', 'variation', 'seat')) ) ) ) @@ -103,7 +112,7 @@ class OrderViewSet(viewsets.ModelViewSet): qs = qs.prefetch_related( Prefetch( 'positions', - OrderPosition.objects.all().prefetch_related( + opq.all().prefetch_related( 'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat', ) ) @@ -654,11 +663,16 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS } def get_queryset(self): - qs = OrderPosition.objects.filter(order__event=self.request.event) + 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': qs = qs.prefetch_related( 'checkins', 'answers', 'answers__options', 'answers__question', - Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')), + Prefetch('addons', qs.select_related('item', 'variation')), Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related( Prefetch( 'event', @@ -666,7 +680,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS ), Prefetch( 'positions', - OrderPosition.objects.prefetch_related( + qs.prefetch_related( 'checkins', 'item', 'variation', 'answers', 'answers__options', 'answers__question', ) ) @@ -676,7 +690,7 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS ) else: qs = qs.prefetch_related( - 'checkins', 'answers', 'answers__options', 'answers__question' + 'checkins', 'answers', 'answers__options', 'answers__question', ).select_related( 'item', 'order', 'order__event', 'order__event__organizer', 'seat' ) diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index e36174bc65..42575d8959 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -114,6 +114,8 @@ def order(event, item, taxrule, question): ) o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'), tax_value=Decimal('0.05'), tax_rule=taxrule) + o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'), + tax_value=Decimal('0.05'), tax_rule=taxrule, canceled=True) InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'), vat_id="DE123", vat_id_validated=True) op = OrderPosition.objects.create( @@ -174,7 +176,8 @@ TEST_ORDERPOSITION_RES = { "option_identifiers": [] } ], - "subevent": None + "subevent": None, + "canceled": False, } TEST_PAYMENTS_RES = [ { @@ -226,6 +229,7 @@ TEST_ORDER_RES = { "sales_channel": "web", "fees": [ { + "canceled": False, "fee_type": "payment", "value": "0.25", "description": "", @@ -318,6 +322,22 @@ def test_order_list(token_client, organizer, event, order, item, taxrule, questi )) assert [] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?include_canceled_positions=false'.format(organizer.slug, event.slug)) + assert resp.status_code == 200 + assert len(resp.data['results'][0]['positions']) == 1 + + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?include_canceled_positions=true'.format(organizer.slug, event.slug)) + assert resp.status_code == 200 + assert len(resp.data['results'][0]['positions']) == 2 + + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?include_canceled_fees=false'.format(organizer.slug, event.slug)) + assert resp.status_code == 200 + assert len(resp.data['results'][0]['fees']) == 1 + + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?include_canceled_fees=true'.format(organizer.slug, event.slug)) + assert resp.status_code == 200 + assert len(resp.data['results'][0]['fees']) == 2 + @pytest.mark.django_db def test_order_detail(token_client, organizer, event, order, item, taxrule, question): @@ -354,6 +374,16 @@ def test_order_detail(token_client, organizer, event, order, item, taxrule, ques assert len(resp.data['downloads']) == 1 assert len(resp.data['positions'][0]['downloads']) == 1 + assert len(resp.data['positions']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/?include_canceled_positions=true'.format(organizer.slug, event.slug, order.code)) + assert resp.status_code == 200 + assert len(resp.data['positions']) == 2 + + assert len(resp.data['fees']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/?include_canceled_fees=true'.format(organizer.slug, event.slug, order.code)) + assert resp.status_code == 200 + assert len(resp.data['fees']) == 2 + @pytest.mark.django_db def test_payment_list(token_client, organizer, event, order): @@ -743,6 +773,13 @@ def test_orderposition_list(token_client, organizer, event, order, item, subeven subevent.pk + 1)) assert [] == resp.data['results'] + resp = token_client.get( + '/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=false'.format(organizer.slug, event.slug)) + assert len(resp.data['results']) == 1 + resp = token_client.get( + '/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=true'.format(organizer.slug, event.slug)) + assert len(resp.data['results']) == 2 + @pytest.mark.django_db def test_orderposition_detail(token_client, organizer, event, order, item, question): @@ -766,12 +803,15 @@ def test_orderposition_detail(token_client, organizer, event, order, item, quest @pytest.mark.django_db -def test_orderposition_detail_no_canceled(token_client, organizer, event, order, item, question): +def test_orderposition_detail_canceled(token_client, organizer, event, order, item, question): with scopes_disabled(): op = order.all_positions.filter(canceled=True).first() resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(organizer.slug, event.slug, op.pk)) assert resp.status_code == 404 + resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/{}/?include_canceled_positions=true'.format( + organizer.slug, event.slug, op.pk)) + assert resp.status_code == 200 @pytest.mark.django_db