From 98c18b162f234e8ebde70978d4ab351da95ad059 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 13 Aug 2019 13:10:48 +0200 Subject: [PATCH] Order creation API: Add support for reverse charge --- doc/api/resources/orders.rst | 7 ++- src/pretix/api/serializers/order.py | 4 +- src/tests/api/test_orders.py | 86 +++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 0965e51a68..053af6cf8b 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -747,8 +747,6 @@ Creating orders * does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping module - * does not support reverse charge taxation - * does not support file upload questions You can supply the following fields of the resource: @@ -789,6 +787,9 @@ Creating orders * ``state`` * ``internal_reference`` * ``vat_id`` + * ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check + yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will + trigger reverse charge taxation. Don't forget to set ``is_business`` as well! * ``positions`` @@ -881,7 +882,7 @@ Creating orders ], "subevent": null } - ], + ] } **Example response**: diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 427789ed18..04b6f5fb50 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -49,7 +49,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer): model = InvoiceAddress fields = ('last_modified', 'is_business', 'company', 'name', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state', 'vat_id', 'vat_id_validated', 'internal_reference') - read_only_fields = ('last_modified', 'vat_id_validated') + read_only_fields = ('last_modified',) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -377,7 +377,7 @@ class OrderSerializer(I18nAwareModelSerializer): } try: ia = instance.invoice_address - if iadata.get('vat_id') != ia.vat_id: + if iadata.get('vat_id') != ia.vat_id and 'vat_id_validated' not in iadata: ia.vat_id_validated = False self.fields['invoice_address'].update(ia, iadata) except InvoiceAddress.DoesNotExist: diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index 7daaffa702..3e3151a6dc 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -2889,6 +2889,64 @@ def test_order_create_auto_pricing(token_client, organizer, event, item, quota, assert o.total == item.default_price + Decimal('0.25') +@pytest.mark.django_db +def test_order_create_auto_pricing_reverse_charge(token_client, organizer, event, item, quota, question, taxrule): + taxrule.eu_reverse_charge = True + taxrule.home_country = Country('DE') + taxrule.save() + item.tax_rule = taxrule + item.save() + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + res['invoice_address']['country'] = 'FR' + res['invoice_address']['is_business'] = True + res['invoice_address']['vat_id'] = 'FR12345' + res['invoice_address']['vat_id_validated'] = True + del res['positions'][0]['price'] + 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 == Decimal('19.33') + assert p.tax_rate == Decimal('0.00') + assert p.tax_value == Decimal('0.00') + assert o.total == Decimal('19.58') + + +@pytest.mark.django_db +def test_order_create_auto_pricing_reverse_charge_require_valid_vatid(token_client, organizer, event, item, quota, + question, taxrule): + taxrule.eu_reverse_charge = True + taxrule.home_country = Country('DE') + taxrule.save() + item.tax_rule = taxrule + item.save() + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + res['invoice_address']['country'] = 'FR' + res['invoice_address']['is_business'] = True + res['invoice_address']['vat_id'] = 'FR12345' + del res['positions'][0]['price'] + 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 == Decimal('23.00') + assert p.tax_rate == Decimal('19.00') + + @pytest.mark.django_db def test_order_create_voucher_price(token_client, organizer, event, item, quota, question): res = copy.deepcopy(ORDER_CREATE_PAYLOAD) @@ -3391,6 +3449,34 @@ def test_order_update_allowed_fields(token_client, organizer, event, order): assert order.all_logentries().get(action_type='pretix.event.order.modified') +@pytest.mark.django_db +def test_order_update_validated_vat_id(token_client, organizer, event, order): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/orders/{}/'.format( + organizer.slug, event.slug, order.code + ), format='json', data={ + 'invoice_address': { + "is_business": False, + "company": "This is my company name", + "name": "John Doe", + "name_parts": {}, + "street": "", + "state": "", + "zipcode": "", + "city": "Paris", + "country": "FR", + "internal_reference": "", + "vat_id": "FR123", + "vat_id_validated": True + } + } + ) + assert resp.status_code == 200 + order.refresh_from_db() + assert order.invoice_address.vat_id == "FR123" + assert order.invoice_address.vat_id_validated + + @pytest.mark.django_db def test_order_update_invoiceaddress_delete_create(token_client, organizer, event, order): event.settings.locales = ['de', 'en']