diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index f742801c83..de6da4fd4d 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -75,8 +75,9 @@ positions list of objects List of order p fees list of objects List of fees included in the order total. By default, only non-canceled fees are included. ├ id integer Internal ID of the fee record -├ fee_type string Type of fee (currently ``payment``, ``passbook``, - ``other``) +├ fee_type string Type of fee (currently ``payment``, ``shipping``, + ``service``, ``cancellation``, ``insurance``, ``late``, + ``other``, ``giftcard``) ├ value money (string) Fee amount ├ description string Human-readable string with more details (can be empty) ├ internal_type string Internal string (i.e. ID of the payment provider), @@ -2244,6 +2245,9 @@ otherwise, such as splitting an order or changing fees. * ``cancel_fees``: A list of objects with the single key ``fee`` specifying an order fee ID. + * ``create_fees``: A list of objects describing new order fees with the fields ``fee_type``, ``value``, ``description``, + ``internal_type``, ``tax_rule`` + * ``recalculate_taxes``: If set to ``"keep_net"``, all taxes will be recalculated based on the tax rule and invoice address, the net price will be kept. If set to ``"keep_gross"``, the gross price will be kept. If set to ``null`` (the default) the taxes are not recalculated. @@ -2263,17 +2267,12 @@ otherwise, such as splitting an order or changing fees. Content-Type: application/json { - "cancel_positions": [ - { - "position": 12373 - } - ], "patch_positions": [ { "position": 12374, "body": { "item": 12, - "variation": None, + "variation": null, "subevent": 562, "seat": "seat-guid-2", "price": "99.99", @@ -2281,6 +2280,11 @@ otherwise, such as splitting an order or changing fees. } } ], + "cancel_positions": [ + { + "position": 12373 + } + ], "split_positions": [ { "position": 12375 @@ -2289,7 +2293,7 @@ otherwise, such as splitting an order or changing fees. "create_positions": [ { "item": 12, - "variation": None, + "variation": null, "subevent": 562, "seat": "seat-guid-2", "price": "99.99", @@ -2297,12 +2301,7 @@ otherwise, such as splitting an order or changing fees. "attendee_name": "Peter", } ], - "cancel_fees": [ - { - "fee": 49 - } - ], - "change_fees": [ + "patch_fees": [ { "fee": 51, "body": { @@ -2310,6 +2309,20 @@ otherwise, such as splitting an order or changing fees. } } ], + "cancel_fees": [ + { + "fee": 49 + } + ], + "create_fees": [ + { + "fee_type": "other", + "value": "1.50", + "description": "Example Fee", + "internal_type": "", + "tax_rule": 15 + } + ], "reissue_invoice": true, "send_email": true, "recalculate_taxes": "keep_gross" diff --git a/src/pretix/api/serializers/orderchange.py b/src/pretix/api/serializers/orderchange.py index 8424eb0620..5998f7eebd 100644 --- a/src/pretix/api/serializers/orderchange.py +++ b/src/pretix/api/serializers/orderchange.py @@ -30,7 +30,7 @@ from rest_framework.exceptions import ValidationError from pretix.api.serializers.order import ( AnswerCreateSerializer, AnswerSerializer, CompatibleCountryField, - OrderPositionCreateSerializer, + OrderFeeCreateSerializer, OrderPositionCreateSerializer, ) from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition from pretix.base.services.orders import OrderError @@ -104,6 +104,54 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize raise ValidationError(str(e)) +class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer): + order = serializers.SlugRelatedField(slug_field='code', queryset=Order.objects.none(), required=True, allow_null=False) + value = serializers.DecimalField(required=True, allow_null=False, decimal_places=2, + max_digits=13) + internal_type = serializers.CharField(required=False, default="") + + class Meta: + model = OrderFee + fields = ('order', 'fee_type', 'value', 'description', 'internal_type', 'tax_rule') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.context: + return + self.fields['order'].queryset = self.context['event'].orders.all() + self.fields['tax_rule'].queryset = self.context['event'].tax_rules.all() + if 'order' in self.context: + del self.fields['order'] + + def validate(self, data): + data = super().validate(data) + if 'order' in self.context: + data['order'] = self.context['order'] + return data + + def create(self, validated_data): + ocm = self.context['ocm'] + + try: + f = OrderFee( + order=validated_data['order'], + fee_type=validated_data['fee_type'], + value=validated_data.get('value'), + description=validated_data.get('description'), + internal_type=validated_data.get('internal_type'), + tax_rule=validated_data.get('tax_rule'), + ) + f._calculate_tax() + ocm.add_fee(f) + if self.context.get('commit', True): + ocm.commit() + return validated_data['order'].fees.order_by('-pk').first() + else: + return OrderFee() # fake to appease DRF + except OrderError as e: + raise ValidationError(str(e)) + + class OrderPositionInfoPatchSerializer(serializers.ModelSerializer): answers = AnswerSerializer(many=True) country = CompatibleCountryField(source='*') @@ -401,6 +449,9 @@ class OrderChangeOperationSerializer(serializers.Serializer): self.fields['split_positions'] = SelectPositionSerializer( many=True, required=False, context=self.context ) + self.fields['create_fees'] = OrderFeeCreateForExistingOrderSerializer( + many=True, required=False, context=self.context + ) self.fields['patch_fees'] = PatchFeeSerializer( many=True, required=False, context=self.context ) diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 8e47ba2f75..5148f29df6 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -63,7 +63,8 @@ from pretix.api.serializers.order import ( ) from pretix.api.serializers.orderchange import ( BlockNameSerializer, OrderChangeOperationSerializer, - OrderFeeChangeSerializer, OrderPositionChangeSerializer, + OrderFeeChangeSerializer, OrderFeeCreateForExistingOrderSerializer, + OrderPositionChangeSerializer, OrderPositionCreateForExistingOrderSerializer, OrderPositionInfoPatchSerializer, ) @@ -988,6 +989,12 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet): ocm.cancel_fee(r['fee']) canceled_fees.add(r['fee']) + for r in serializer.validated_data.get('create_fees', []): + pos_serializer = OrderFeeCreateForExistingOrderSerializer( + context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()}, + ) + pos_serializer.create(r) + for r in serializer.validated_data.get('patch_fees', []): if r['fee'] in canceled_fees: continue diff --git a/src/tests/api/test_order_change.py b/src/tests/api/test_order_change.py index b1c601a382..a956950f32 100644 --- a/src/tests/api/test_order_change.py +++ b/src/tests/api/test_order_change.py @@ -1797,6 +1797,13 @@ def test_order_change_cancel_and_create(token_client, organizer, event, order, q 'price': '99.99' }, ], + 'create_fees': [ + { + 'value': '5.99', + 'fee_type': 'service', + 'description': 'Service fee', + }, + ], 'cancel_fees': [ { 'fee': f.pk, @@ -1818,6 +1825,11 @@ def test_order_change_cancel_and_create(token_client, organizer, event, order, q assert p_new.price == Decimal('99.99') f.refresh_from_db() assert f.canceled + f_new = order.all_fees.get(fee_type=OrderFee.FEE_TYPE_SERVICE) + assert f_new.value == Decimal('5.99') + assert f_new.description == "Service fee" + assert f_new.internal_type == "" + assert f_new.tax_value == Decimal('0.00') @pytest.mark.django_db