forked from CGM_Public/pretix_original
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:
@@ -27,7 +27,6 @@ from decimal import Decimal
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import requests
|
||||
import vat_moss.id
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from zeep import Client, Transport
|
||||
@@ -42,14 +41,142 @@ logger = logging.getLogger(__name__)
|
||||
error_messages = {
|
||||
'unavailable': _(
|
||||
'Your VAT ID could not be checked, as the VAT checking service of '
|
||||
'your country is currently not available. We will therefore '
|
||||
'need to charge VAT on your invoice. You can get the tax amount '
|
||||
'back via the VAT reimbursement process.'
|
||||
'your country is currently not available. We will therefore need to '
|
||||
'charge you the same tax rate as if you did not enter a VAT ID.'
|
||||
),
|
||||
'invalid': _('This VAT ID is not valid. Please re-check your input.'),
|
||||
'country_mismatch': _('Your VAT ID does not match the selected country.'),
|
||||
}
|
||||
|
||||
VAT_ID_PATTERNS = {
|
||||
# Patterns generated by consulting the following URLs:
|
||||
#
|
||||
# - http://en.wikipedia.org/wiki/VAT_identification_number
|
||||
# - http://ec.europa.eu/taxation_customs/vies/faq.html
|
||||
# - https://euipo.europa.eu/tunnel-web/secure/webdav/guest/document_library/Documents/COSME/VAT%20numbers%20EU.pdf
|
||||
# - http://www.skatteetaten.no/en/International-pages/Felles-innhold-benyttes-i-flere-malgrupper/Brochure/Guide-to-value-added-tax-in-Norway/?chapter=7159
|
||||
'AT': { # Austria
|
||||
'regex': '^U\\d{8}$',
|
||||
'country_code': 'AT'
|
||||
},
|
||||
'BE': { # Belgium
|
||||
'regex': '^(1|0?)\\d{9}$',
|
||||
'country_code': 'BE'
|
||||
},
|
||||
'BG': { # Bulgaria
|
||||
'regex': '^\\d{9,10}$',
|
||||
'country_code': 'BG'
|
||||
},
|
||||
'CH': { # Switzerland
|
||||
'regex': '^\\dE{9}$',
|
||||
'country_code': 'CH'
|
||||
},
|
||||
'CY': { # Cyprus
|
||||
'regex': '^\\d{8}[A-Z]$',
|
||||
'country_code': 'CY'
|
||||
},
|
||||
'CZ': { # Czech Republic
|
||||
'regex': '^\\d{8,10}$',
|
||||
'country_code': 'CZ'
|
||||
},
|
||||
'DE': { # Germany
|
||||
'regex': '^\\d{9}$',
|
||||
'country_code': 'DE'
|
||||
},
|
||||
'DK': { # Denmark
|
||||
'regex': '^\\d{8}$',
|
||||
'country_code': 'DK'
|
||||
},
|
||||
'EE': { # Estonia
|
||||
'regex': '^\\d{9}$',
|
||||
'country_code': 'EE'
|
||||
},
|
||||
'EL': { # Greece
|
||||
'regex': '^\\d{9}$',
|
||||
'country_code': 'GR'
|
||||
},
|
||||
'ES': { # Spain
|
||||
'regex': '^[A-Z0-9]\\d{7}[A-Z0-9]$',
|
||||
'country_code': 'ES'
|
||||
},
|
||||
'FI': { # Finland
|
||||
'regex': '^\\d{8}$',
|
||||
'country_code': 'FI'
|
||||
},
|
||||
'FR': { # France
|
||||
'regex': '^[A-Z0-9]{2}\\d{9}$',
|
||||
'country_code': 'FR'
|
||||
},
|
||||
'GB': { # United Kingdom
|
||||
'regex': '^(GD\\d{3}|HA\\d{3}|\\d{9}|\\d{12})$',
|
||||
'country_code': 'GB'
|
||||
},
|
||||
'HR': { # Croatia
|
||||
'regex': '^\\d{11}$',
|
||||
'country_code': 'HR'
|
||||
},
|
||||
'HU': { # Hungary
|
||||
'regex': '^\\d{8}$',
|
||||
'country_code': 'HU'
|
||||
},
|
||||
'IE': { # Ireland
|
||||
'regex': '^(\\d{7}[A-Z]{1,2}|\\d[A-Z+*]\\d{5}[A-Z])$',
|
||||
'country_code': 'IE'
|
||||
},
|
||||
'IT': { # Italy
|
||||
'regex': '^\\d{11}$',
|
||||
'country_code': 'IT'
|
||||
},
|
||||
'LT': { # Lithuania
|
||||
'regex': '^(\\d{9}|\\d{12})$',
|
||||
'country_code': 'LT'
|
||||
},
|
||||
'LU': { # Luxembourg
|
||||
'regex': '^\\d{8}$',
|
||||
'country_code': 'LU'
|
||||
},
|
||||
'LV': { # Latvia
|
||||
'regex': '^\\d{11}$',
|
||||
'country_code': 'LV'
|
||||
},
|
||||
'MT': { # Malta
|
||||
'regex': '^\\d{8}$',
|
||||
'country_code': 'MT'
|
||||
},
|
||||
'NL': { # Netherlands
|
||||
'regex': '^\\d{9}B\\d{2}$',
|
||||
'country_code': 'NL'
|
||||
},
|
||||
'NO': { # Norway
|
||||
'regex': '^\\d{9}MVA$',
|
||||
'country_code': 'NO'
|
||||
},
|
||||
'PL': { # Poland
|
||||
'regex': '^\\d{10}$',
|
||||
'country_code': 'PL'
|
||||
},
|
||||
'PT': { # Portugal
|
||||
'regex': '^\\d{9}$',
|
||||
'country_code': 'PT'
|
||||
},
|
||||
'RO': { # Romania
|
||||
'regex': '^\\d{2,10}$',
|
||||
'country_code': 'RO'
|
||||
},
|
||||
'SE': { # Sweden
|
||||
'regex': '^\\d{12}$',
|
||||
'country_code': 'SE'
|
||||
},
|
||||
'SI': { # Slovenia
|
||||
'regex': '^\\d{8}$',
|
||||
'country_code': 'SI'
|
||||
},
|
||||
'SK': { # Slovakia
|
||||
'regex': '^\\d{10}$',
|
||||
'country_code': 'SK'
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class VATIDError(Exception):
|
||||
def __init__(self, message):
|
||||
@@ -64,13 +191,57 @@ class VATIDTemporaryError(VATIDError):
|
||||
pass
|
||||
|
||||
|
||||
def normalize_vat_id(vat_id, country_code):
|
||||
"""
|
||||
Accepts a VAT ID and normaizes it, getting rid of spaces, periods, dashes
|
||||
etc and converting it to upper case.
|
||||
|
||||
Original function from https://github.com/wbond/vat_moss-python
|
||||
Copyright (c) 2015 Will Bond <will@wbond.net>
|
||||
MIT License
|
||||
"""
|
||||
if not vat_id:
|
||||
return None
|
||||
|
||||
if not isinstance(vat_id, str):
|
||||
raise TypeError('VAT ID is not a string')
|
||||
|
||||
if len(vat_id) < 3:
|
||||
raise ValueError('VAT ID must be at least three character long')
|
||||
|
||||
# Normalize the ID for simpler regexes
|
||||
vat_id = re.sub('\\s+', '', vat_id)
|
||||
vat_id = vat_id.replace('-', '')
|
||||
vat_id = vat_id.replace('.', '')
|
||||
vat_id = vat_id.upper()
|
||||
|
||||
# Clean the different shapes a number can take in Switzerland depending on purpse
|
||||
if country_code == "CH":
|
||||
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
|
||||
|
||||
# Fix people using GR prefix for Greece
|
||||
if vat_id[0:2] == "GR" and country_code == "GR":
|
||||
vat_id = "EL" + vat_id[2:]
|
||||
|
||||
# Check if we already have a valid country prefix. If not, we try to figure out if we can
|
||||
# add one, since in some countries (e.g. Italy) it's very custom to enter it without the prefix
|
||||
if vat_id[:2] in VAT_ID_PATTERNS and re.match(VAT_ID_PATTERNS[vat_id[0:2]]['regex'], vat_id[2:]):
|
||||
# Prefix set and prefix matches pattern, nothing to do
|
||||
pass
|
||||
elif re.match(VAT_ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], vat_id):
|
||||
# Prefix not set but adding it fixes pattern
|
||||
vat_id = cc_to_vat_prefix(country_code) + vat_id
|
||||
else:
|
||||
# We have no idea what this is
|
||||
pass
|
||||
|
||||
return vat_id
|
||||
|
||||
|
||||
def _validate_vat_id_NO(vat_id, country_code):
|
||||
# Inspired by vat_moss library
|
||||
if not vat_id.startswith("NO"):
|
||||
# prefix is not usually used in Norway, but expected by vat_moss library
|
||||
vat_id = "NO" + vat_id
|
||||
try:
|
||||
vat_id = vat_moss.id.normalize(vat_id)
|
||||
vat_id = normalize_vat_id(vat_id, country_code)
|
||||
except ValueError:
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
|
||||
@@ -104,7 +275,7 @@ def _validate_vat_id_NO(vat_id, country_code):
|
||||
def _validate_vat_id_EU(vat_id, country_code):
|
||||
# Inspired by vat_moss library
|
||||
try:
|
||||
vat_id = vat_moss.id.normalize(vat_id)
|
||||
vat_id = normalize_vat_id(vat_id, country_code)
|
||||
except ValueError:
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
|
||||
@@ -112,11 +283,10 @@ def _validate_vat_id_EU(vat_id, country_code):
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
|
||||
number = vat_id[2:]
|
||||
|
||||
if vat_id[:2] != cc_to_vat_prefix(country_code):
|
||||
raise VATIDFinalError(error_messages['country_mismatch'])
|
||||
|
||||
if not re.match(vat_moss.id.ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], number):
|
||||
if not re.match(VAT_ID_PATTERNS[cc_to_vat_prefix(country_code)]['regex'], number):
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
|
||||
# We are relying on the country code of the normalized VAT-ID and not the user/InvoiceAddress-provided
|
||||
@@ -175,9 +345,12 @@ def _validate_vat_id_EU(vat_id, country_code):
|
||||
|
||||
def _validate_vat_id_CH(vat_id, country_code):
|
||||
if vat_id[:3] != 'CHE':
|
||||
raise VATIDFinalError(_('Your VAT ID does not match the selected country.'))
|
||||
raise VATIDFinalError(error_messages['country_mismatch'])
|
||||
|
||||
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
|
||||
try:
|
||||
vat_id = normalize_vat_id(vat_id, country_code)
|
||||
except ValueError:
|
||||
raise VATIDFinalError(error_messages['invalid'])
|
||||
try:
|
||||
transport = Transport(
|
||||
cache=SqliteCache(os.path.join(settings.CACHE_DIR, "validate_vat_id_ch_zeep_cache.db")),
|
||||
|
||||
Reference in New Issue
Block a user