Skip to content

Commit b02fee9

Browse files
committed
Fixed #210 -- Added VAT identifcation number validator
1 parent 0d7a9cf commit b02fee9

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

localflavor/generic/validators.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,84 @@ def __call__(self, value):
270270
value = re.compile(r'[^\d]+').sub('', value)
271271
if not checksums.ean(value):
272272
raise ValidationError(self.message, code='invalid')
273+
274+
275+
VATIN_PATTERN_MAP = {
276+
'AT': r'^ATU\d{8}$',
277+
'BE': r'^BE0?\d{9}$',
278+
'BG': r'^BG\d{9,10}$',
279+
'HR': r'^HR\d{11}$',
280+
'CY': r'^CY\d{8}[A-Z]$',
281+
'CZ': r'^CZ\d{8,10}$',
282+
'DE': r'^DE\d{9}$',
283+
'DK': r'^DK\d{8}$',
284+
'EE': r'^EE\d{9}$',
285+
'EL': r'^EL\d{9}$',
286+
'ES': r'^ES[A-Z0-9]\d{7}[A-Z0-9]$',
287+
'FI': r'^FI\d{8}$',
288+
'FR': r'^FR[A-HJ-NP-Z0-9][A-HJ-NP-Z0-9]\d{9}$',
289+
'GB': r'^(GB(GD|HA)\d{3}|GB\d{9}|GB\d{12})$',
290+
'HU': r'^HU\d{8}$',
291+
'IE': r'^IE\d[A-Z0-9\+\*]\d{5}[A-Z]{1,2}$',
292+
'IT': r'^IT\d{11}$',
293+
'LT': r'^LT(\d{9}|\d{12})$',
294+
'LU': r'^LU\d{8}$',
295+
'LV': r'^LV\d{11}$',
296+
'MT': r'^MT\d{8}$',
297+
'NL': r'^NL\d{9}B\d{2}$',
298+
'PL': r'^PL\d{10}$',
299+
'PT': r'^PT\d{9}$',
300+
'RO': r'^RO\d{2,10}$',
301+
'SE': r'^SE\d{10}01$',
302+
'SI': r'^SI\d{8}$',
303+
'SK': r'^SK\d{10}$',
304+
}
305+
"""
306+
Map of country codes and regular expressions.
307+
308+
See https://en.wikipedia.org/wiki/VAT_identification_number
309+
"""
310+
311+
VATIN_COUNTRY_CODE_LENGTH = 2
312+
"""
313+
Length of the country code prefix of a VAT identification number.
314+
315+
Codes are two letter ISO 3166-1 alpha-2 codes except for Greece that uses
316+
ISO 639-1.
317+
"""
318+
319+
320+
class VATINValidator:
321+
"""
322+
A validator for VAT identification numbers.
323+
324+
Currently only supports European VIES VAT identification numbers.
325+
326+
See See https://en.wikipedia.org/wiki/VAT_identification_number
327+
"""
328+
messages = {
329+
'country_code': _('%(country_code)s is not a valid country code.'),
330+
'vatin': _('%(vatin)s is not a valid VAT identification number.'),
331+
}
332+
333+
def __call__(self, value):
334+
country_code, number = self.clean(value)
335+
try:
336+
match = re.match(VATIN_PATTERN_MAP[country_code], value)
337+
if not match:
338+
raise ValidationError(
339+
self.messages['vatin'],
340+
code='vatin',
341+
params={'vatin': value}
342+
)
343+
344+
except KeyError:
345+
raise ValidationError(
346+
self.messages['country_code'],
347+
code='country_code',
348+
params={'country_code': country_code}
349+
)
350+
351+
def clean(self, value):
352+
"""Return tuple of country code and number."""
353+
return value[:VATIN_COUNTRY_CODE_LENGTH], value[VATIN_COUNTRY_CODE_LENGTH:]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import unittest
2+
3+
from django.core.exceptions import ValidationError
4+
5+
from localflavor.generic import validators
6+
7+
8+
class TestVATINValidator(unittest.TestCase):
9+
validator = validators.VATINValidator()
10+
11+
VALID_VATIN = 'DE284754038'
12+
13+
def test_valid_vatin(self):
14+
self.validator(self.VALID_VATIN)
15+
16+
def test_invalid_vatin(self):
17+
with self.assertRaises(ValidationError) as cm:
18+
self.validator('DE99999999')
19+
e = cm.exception
20+
self.assertIn("DE99999999 is not a valid VAT identification number.", e.messages)
21+
22+
def test_invalid_country_code(self):
23+
with self.assertRaises(ValidationError) as cm:
24+
self.validator('XX99999999')
25+
e = cm.exception
26+
self.assertIn("XX is not a valid country code.", e.messages)

0 commit comments

Comments
 (0)