From 9d6ff20191ece3b9cf5cf4cba457d3bee9107240 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 12 Aug 2019 15:30:58 +0200 Subject: [PATCH] Order creation API: Allow to auto-calculate prices --- doc/api/resources/orders.rst | 4 +- src/pretix/api/serializers/order.py | 78 +++++++++++++++++++---------- src/tests/api/test_orders.py | 20 +++++++- 3 files changed, 72 insertions(+), 30 deletions(-) diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index c48b68b14a..4a617019d9 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -726,7 +726,7 @@ Creating orders * does not validate any requirements related to add-on products - * does not check or calculate prices but believes any prices you send + * does not check prices but believes any prices you send * does not support the redemption of vouchers @@ -785,7 +785,7 @@ Creating orders * ``positionid`` (optional, see below) * ``item`` * ``variation`` - * ``price`` + * ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product) * ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.) * ``attendee_name`` **or** ``attendee_name_parts`` * ``attendee_email`` diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 8ad44c3c36..6906b0d59f 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -21,6 +21,7 @@ from pretix.base.models.orders import ( CartPosition, OrderFee, OrderPayment, OrderRefund, ) from pretix.base.pdf import get_variables +from pretix.base.services.pricing import get_price from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS from pretix.base.signals import register_ticket_outputs @@ -457,6 +458,8 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer): secret = serializers.CharField(required=False) attendee_name = serializers.CharField(required=False, allow_null=True) seat = serializers.CharField(required=False, allow_null=True) + price = serializers.DecimalField(required=False, allow_null=True, decimal_places=2, + max_digits=10) class Meta: model = OrderPosition @@ -716,38 +719,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer): validated_data['locale'] = self.context['event'].settings.locale order = Order(event=self.context['event'], **validated_data) order.set_expires(subevents=[p.get('subevent') for p in positions_data]) - order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00')) order.meta_info = "{}" + order.total = Decimal('0.00') order.save() - if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID: - order.status = Order.STATUS_PAID - order.save() - order.payments.create( - amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED, - payment_date=now() - ) - elif payment_provider == "free" and order.total != Decimal('0.00'): - raise ValidationError('You cannot use the "free" payment provider for non-free orders.') - elif validated_data.get('status') == Order.STATUS_PAID: - order.payments.create( - amount=order.total, - provider=payment_provider, - info=payment_info, - payment_date=payment_date, - state=OrderPayment.PAYMENT_STATE_CONFIRMED - ) - elif payment_provider: - order.payments.create( - amount=order.total, - provider=payment_provider, - info=payment_info, - state=OrderPayment.PAYMENT_STATE_CREATED - ) - if ia: ia.order = order ia.save() + pos_map = {} for pos_data in positions_data: answers_data = pos_data.pop('answers', []) @@ -759,9 +738,25 @@ class OrderCreateSerializer(I18nAwareModelSerializer): } pos = OrderPosition(**pos_data) pos.order = order - pos._calculate_tax() if addon_to: pos.addon_to = pos_map[addon_to] + + if pos.price is None: + price = get_price( + item=pos.item, + variation=pos.variation, + voucher=pos.voucher, + custom_price=None, + subevent=pos.subevent, + addon_to=pos.addon_to, + invoice_address=ia, + ) + pos.price = price.gross + pos.tax_rate = price.rate + pos.tax_value = price.tax + pos.tax_rule = pos.item.tax_rule + else: + pos._calculate_tax() pos.save() pos_map[pos.positionid] = pos for answ_data in answers_data: @@ -771,12 +766,41 @@ class OrderCreateSerializer(I18nAwareModelSerializer): for cp in delete_cps: cp.delete() + for fee_data in fees_data: f = OrderFee(**fee_data) f.order = order f._calculate_tax() f.save() + order.total = sum([p.price for p in order.positions.all()]) + sum([f.value for f in order.fees.all()]) + order.save(update_fields=['total']) + + if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID: + order.status = Order.STATUS_PAID + order.save() + order.payments.create( + amount=order.total, provider='free', state=OrderPayment.PAYMENT_STATE_CONFIRMED, + payment_date=now() + ) + elif payment_provider == "free" and order.total != Decimal('0.00'): + raise ValidationError('You cannot use the "free" payment provider for non-free orders.') + elif validated_data.get('status') == Order.STATUS_PAID: + order.payments.create( + amount=order.total, + provider=payment_provider, + info=payment_info, + payment_date=payment_date, + state=OrderPayment.PAYMENT_STATE_CONFIRMED + ) + elif payment_provider: + order.payments.create( + amount=order.total, + provider=payment_provider, + info=payment_info, + state=OrderPayment.PAYMENT_STATE_CREATED + ) + return order diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index ac3b93e28c..313fc0e16d 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -2800,7 +2800,6 @@ def test_order_create_send_emails_free(token_client, organizer, event, item, quo organizer.slug, event.slug ), format='json', data=res ) - print(resp.data) assert resp.status_code == 201 assert len(djmail.outbox) == 1 assert djmail.outbox[0].subject == "Your order: {}".format(resp.data['code']) @@ -2825,6 +2824,25 @@ def test_order_create_send_emails_paid(token_client, organizer, event, item, quo assert djmail.outbox[1].subject == "Payment received for your order: {}".format(resp.data['code']) +@pytest.mark.django_db +def test_order_create_auto_pricing(token_client, organizer, event, item, quota, question): + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + del res['positions'][0]['price'] + djmail.outbox = [] + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + with scopes_disabled(): + o = Order.objects.get(code=resp.data['code']) + p = o.positions.first() + assert p.price == item.default_price + assert o.total == item.default_price + Decimal('0.25') + + REFUND_CREATE_PAYLOAD = { "state": "created", "provider": "manual",