@@ -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 :]
0 commit comments