Compare commits

...

6 Commits

Author SHA1 Message Date
Mira Weller
4619901a37 docs: update fee_type options 2025-02-25 14:16:32 +01:00
Mira Weller
ee921a6331 docs: None -> null 2025-02-25 14:04:27 +01:00
Mira Weller
3310e9670b Add create_fees to example 2025-02-25 13:57:33 +01:00
Mira Weller
93570d42c7 Consistent order of examples 2025-02-25 13:57:01 +01:00
Mira Weller
598527073c Fix example in docs 2025-02-25 13:22:59 +01:00
Raphael Michel
d011651565 API: Allow to add a fee to an order (#4806) 2025-02-10 17:24:46 +01:00
4 changed files with 100 additions and 17 deletions

View File

@@ -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 fees list of objects List of fees included in the order total. By default, only
non-canceled fees are included. non-canceled fees are included.
├ id integer Internal ID of the fee record ├ id integer Internal ID of the fee record
├ fee_type string Type of fee (currently ``payment``, ``passbook``, ├ fee_type string Type of fee (currently ``payment``, ``shipping``,
``other``) ``service``, ``cancellation``, ``insurance``, ``late``,
``other``, ``giftcard``)
├ value money (string) Fee amount ├ value money (string) Fee amount
├ description string Human-readable string with more details (can be empty) ├ description string Human-readable string with more details (can be empty)
├ internal_type string Internal string (i.e. ID of the payment provider), ├ 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. * ``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 * ``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`` 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. (the default) the taxes are not recalculated.
@@ -2263,17 +2267,12 @@ otherwise, such as splitting an order or changing fees.
Content-Type: application/json Content-Type: application/json
{ {
"cancel_positions": [
{
"position": 12373
}
],
"patch_positions": [ "patch_positions": [
{ {
"position": 12374, "position": 12374,
"body": { "body": {
"item": 12, "item": 12,
"variation": None, "variation": null,
"subevent": 562, "subevent": 562,
"seat": "seat-guid-2", "seat": "seat-guid-2",
"price": "99.99", "price": "99.99",
@@ -2281,6 +2280,11 @@ otherwise, such as splitting an order or changing fees.
} }
} }
], ],
"cancel_positions": [
{
"position": 12373
}
],
"split_positions": [ "split_positions": [
{ {
"position": 12375 "position": 12375
@@ -2289,7 +2293,7 @@ otherwise, such as splitting an order or changing fees.
"create_positions": [ "create_positions": [
{ {
"item": 12, "item": 12,
"variation": None, "variation": null,
"subevent": 562, "subevent": 562,
"seat": "seat-guid-2", "seat": "seat-guid-2",
"price": "99.99", "price": "99.99",
@@ -2297,12 +2301,7 @@ otherwise, such as splitting an order or changing fees.
"attendee_name": "Peter", "attendee_name": "Peter",
} }
], ],
"cancel_fees": [ "patch_fees": [
{
"fee": 49
}
],
"change_fees": [
{ {
"fee": 51, "fee": 51,
"body": { "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, "reissue_invoice": true,
"send_email": true, "send_email": true,
"recalculate_taxes": "keep_gross" "recalculate_taxes": "keep_gross"

View File

@@ -30,7 +30,7 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.order import ( from pretix.api.serializers.order import (
AnswerCreateSerializer, AnswerSerializer, CompatibleCountryField, AnswerCreateSerializer, AnswerSerializer, CompatibleCountryField,
OrderPositionCreateSerializer, OrderFeeCreateSerializer, OrderPositionCreateSerializer,
) )
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
from pretix.base.services.orders import OrderError from pretix.base.services.orders import OrderError
@@ -104,6 +104,54 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
raise ValidationError(str(e)) 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): class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
answers = AnswerSerializer(many=True) answers = AnswerSerializer(many=True)
country = CompatibleCountryField(source='*') country = CompatibleCountryField(source='*')
@@ -401,6 +449,9 @@ class OrderChangeOperationSerializer(serializers.Serializer):
self.fields['split_positions'] = SelectPositionSerializer( self.fields['split_positions'] = SelectPositionSerializer(
many=True, required=False, context=self.context many=True, required=False, context=self.context
) )
self.fields['create_fees'] = OrderFeeCreateForExistingOrderSerializer(
many=True, required=False, context=self.context
)
self.fields['patch_fees'] = PatchFeeSerializer( self.fields['patch_fees'] = PatchFeeSerializer(
many=True, required=False, context=self.context many=True, required=False, context=self.context
) )

View File

@@ -63,7 +63,8 @@ from pretix.api.serializers.order import (
) )
from pretix.api.serializers.orderchange import ( from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer, BlockNameSerializer, OrderChangeOperationSerializer,
OrderFeeChangeSerializer, OrderPositionChangeSerializer, OrderFeeChangeSerializer, OrderFeeCreateForExistingOrderSerializer,
OrderPositionChangeSerializer,
OrderPositionCreateForExistingOrderSerializer, OrderPositionCreateForExistingOrderSerializer,
OrderPositionInfoPatchSerializer, OrderPositionInfoPatchSerializer,
) )
@@ -988,6 +989,12 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
ocm.cancel_fee(r['fee']) ocm.cancel_fee(r['fee'])
canceled_fees.add(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', []): for r in serializer.validated_data.get('patch_fees', []):
if r['fee'] in canceled_fees: if r['fee'] in canceled_fees:
continue continue

View File

@@ -1797,6 +1797,13 @@ def test_order_change_cancel_and_create(token_client, organizer, event, order, q
'price': '99.99' 'price': '99.99'
}, },
], ],
'create_fees': [
{
'value': '5.99',
'fee_type': 'service',
'description': 'Service fee',
},
],
'cancel_fees': [ 'cancel_fees': [
{ {
'fee': f.pk, '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') assert p_new.price == Decimal('99.99')
f.refresh_from_db() f.refresh_from_db()
assert f.canceled 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 @pytest.mark.django_db