Invoice address: Improve VAT ID input (#5647)

* Remove unmaintained depdendency vat_moss

* VAT ID normalization: Auto-add country codes

* VAT ID: County-specific labels

* Invoice address: Allow to set VAT ID as required per country

* Fix failing tests

* Update src/pretix/base/settings.py

Co-authored-by: luelista <weller@rami.io>

* Review fixes

---------

Co-authored-by: luelista <weller@rami.io>
This commit is contained in:
Raphael Michel
2025-12-03 16:48:19 +01:00
committed by GitHub
parent 051eb78312
commit 5a1bcae085
13 changed files with 383 additions and 36 deletions

View File

@@ -34,7 +34,7 @@ def test_no_invoice_address(client):
'data': [],
'state': {'label': 'State', 'required': False, 'visible': False},
'street': {'required': 'if_any'},
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@@ -44,7 +44,7 @@ def test_no_invoice_address(client):
'data': [],
'state': {'label': 'State', 'required': False, 'visible': False},
'street': {'required': 'if_any'},
'vat_id': {'required': False, 'visible': False},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': False},
'zipcode': {'required': False}
}
@@ -98,7 +98,7 @@ def test_provider_only_email_available(client, event):
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': False},
'transmission_types': [{'code': 'email', 'name': 'Email'}],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@@ -123,7 +123,7 @@ def test_provider_italy_sdi_not_enforced_when_optional(client, event):
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': True},
'transmission_types': [{'code': 'it_sdi', 'name': 'Exchange System (SdI)'}],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID / P.IVA', 'required': False, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@@ -148,7 +148,7 @@ def test_provider_italy_sdi_enforced_individual(client, event):
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': True},
'transmission_types': [{'code': 'it_sdi', 'name': 'Exchange System (SdI)'}],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID / P.IVA', 'required': False, 'visible': True},
'zipcode': {'required': True}
}
@@ -174,11 +174,37 @@ def test_provider_italy_sdi_enforced_business(client, event):
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': True},
'transmission_types': [{'code': 'it_sdi', 'name': 'Exchange System (SdI)'}],
'vat_id': {'required': True, 'visible': True},
'vat_id': {'helptext_visible': False, 'label': 'VAT ID / P.IVA', 'required': True, 'visible': True},
'zipcode': {'required': True}
}
@pytest.mark.django_db
def test_vat_id_enforced(client, event):
response = client.get(
'/js_helpers/address_form/?country=GR&invoice=true&organizer=org&event=ev'
'&is_business=business'
)
assert response.status_code == 200
d = response.json()
del d['data']
assert d == {
'city': {'required': 'if_any'},
'state': {'label': 'State', 'required': False, 'visible': False},
'street': {'required': 'if_any'},
'transmission_email_address': {'required': False, 'visible': False},
'transmission_email_other': {'required': False, 'visible': False},
'transmission_it_sdi_codice_fiscale': {'required': False, 'visible': False},
'transmission_it_sdi_pec': {'required': False, 'visible': False},
'transmission_it_sdi_recipient_code': {'required': False, 'visible': False},
'transmission_peppol_participant_id': {'required': False, 'visible': False},
'transmission_type': {'visible': True},
'transmission_types': [{'code': 'email', 'name': 'Email'}, {'code': 'peppol', 'name': 'Peppol'}],
'vat_id': {'helptext_visible': False, 'label': 'VAT ID / TIN', 'required': True, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@pytest.mark.django_db
def test_email_peppol_choice(client, event):
response = client.get(
@@ -203,7 +229,7 @@ def test_email_peppol_choice(client, event):
{'code': 'email', 'name': 'Email'},
{'code': 'peppol', 'name': 'Peppol'},
],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': True},
'zipcode': {'required': 'if_any'}
}
@@ -229,6 +255,6 @@ def test_email_peppol_choice(client, event):
{'code': 'email', 'name': 'Email'},
{'code': 'peppol', 'name': 'Peppol'},
],
'vat_id': {'required': False, 'visible': True},
'vat_id': {'helptext_visible': True, 'label': 'VAT ID', 'required': False, 'visible': True},
'zipcode': {'required': True}
}

View File

@@ -24,7 +24,7 @@ import responses
from requests import Timeout
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
VATIDFinalError, VATIDTemporaryError, normalize_vat_id, validate_vat_id,
)
@@ -51,6 +51,18 @@ def test_eu_country_mismatch():
validate_vat_id('AT12345', 'DE')
@responses.activate
def test_normalize():
assert normalize_vat_id('AT U 12345678', 'AT') == 'ATU12345678'
assert normalize_vat_id('U12345678', 'AT') == 'ATU12345678'
assert normalize_vat_id('IT.123.456.789.00', 'IT') == 'IT12345678900'
assert normalize_vat_id('12345678900', 'IT') == 'IT12345678900'
assert normalize_vat_id('123456789MVA', 'NO') == "NO123456789MVA"
assert normalize_vat_id('CHE 123456789 MWST', 'CH') == "CHE123456789"
# Bad combination is left for validation
assert normalize_vat_id('ATU12345678', 'IT') == 'ATU12345678'
@responses.activate
def test_eu_server_down():
def _callback(request):

View File

@@ -411,6 +411,69 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
with scopes_disabled():
ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address'))
assert ia.vat_id == "AT123456"
assert not ia.vat_id_validated
def test_reverse_charge_vatid_required(self):
self.event.settings.invoice_address_vatid = True
self.event.settings.invoice_address_vatid_required_countries = ["AT"]
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
resp = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
assert 'has-error' in resp.content.decode()
def test_reverse_charge_vatid_check_unavailable_but_required(self):
self.tr19.eu_reverse_charge = True
self.tr19.home_country = Country('DE')
self.tr19.save()
self.event.settings.invoice_address_vatid = True
self.event.settings.invoice_address_vatid_required_countries = ["AT"]
with scopes_disabled():
cr1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
with mock.patch('pretix.base.services.tax._validate_vat_id_EU') as mock_validate:
def raiser(*args, **kwargs):
raise VATIDTemporaryError('temp')
mock_validate.side_effect = raiser
self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name': 'Bar',
'street': 'Baz',
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
assert cr1.price == Decimal('23.00')
with scopes_disabled():
ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address'))
assert ia.vat_id == "AT123456"
assert not ia.vat_id_validated
def test_reverse_charge_keep_gross(self):
@@ -448,6 +511,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
with scopes_disabled():
ia = InvoiceAddress.objects.get(pk=self.client.session['carts'][self.session_key].get('invoice_address'))
assert ia.vat_id == "AT123456"
assert ia.vat_id_validated
def test_custom_tax_rules(self):
@@ -1452,7 +1516,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'transmission_type': 'it_sdi',
'vat_id': '',
}, follow=True)
assert "This field is required for the selected type" in response.content.decode()
assert "This field is required" in response.content.decode()
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
@@ -1468,6 +1532,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'state': 'MI',
'email': 'admin@localhost',
'transmission_type': 'email',
'vat_id': 'IT01234567890',
}, follow=True)
assert "must be used for this country" in response.content.decode()