1
|
|
|
import re |
2
|
|
|
|
3
|
|
|
from django.core.exceptions import ValidationError |
4
|
|
|
from django.utils.encoding import python_2_unicode_compatible |
5
|
|
|
from django.utils.functional import cached_property |
6
|
|
|
from django.utils.translation import ugettext |
7
|
|
|
from suds import WebFault |
8
|
|
|
from suds.client import Client |
9
|
|
|
|
10
|
|
|
from vies import VIES_WSDL_URL, logger |
11
|
|
|
|
12
|
|
|
|
13
|
|
|
def dk_format(v): |
14
|
|
|
return '%s %s %s %s' % (v[:4], v[4:6], v[6:8], v[8:10]) |
15
|
|
|
|
16
|
|
|
|
17
|
|
|
def gb_format(v): |
18
|
|
|
if len(v) == 11: |
19
|
|
|
return '%s %s %s' % (v[:5], v[5:9], v[9:11]) |
20
|
|
|
if len(v) == 14: |
21
|
|
|
return '%s %s' % (gb_format(v[:11]), v[11:14]) |
22
|
|
|
return v |
23
|
|
|
|
24
|
|
|
|
25
|
|
|
def fr_format(v): |
26
|
|
|
return '%s %s' % (v[:4], v[4:]) |
27
|
|
|
|
28
|
|
|
|
29
|
|
|
VIES_OPTIONS = { |
30
|
|
|
'AT': ('Austria', re.compile(r'^ATU\d{8}$')), |
31
|
|
|
'BE': ('Belgium', re.compile(r'^BE0?\d{9}$')), |
32
|
|
|
'BG': ('Bulgaria', re.compile(r'^BG\d{9,10}$')), |
33
|
|
|
'HR': ('Croatia', re.compile(r'^HR\d{11}$')), |
34
|
|
|
'CY': ('Cyprus', re.compile(r'^CY\d{8}[A-Z]$')), |
35
|
|
|
'CZ': ('Czech Republic', re.compile(r'^CZ\d{8,10}$')), |
36
|
|
|
'DE': ('Germany', re.compile(r'^DE\d{9}$')), |
37
|
|
|
'DK': ('Denmark', re.compile(r'^DK\d{8}$'), dk_format), |
38
|
|
|
'EE': ('Estonia', re.compile(r'^EE\d{9}$')), |
39
|
|
|
'EL': ('Greece', re.compile(r'^EL\d{9}$')), |
40
|
|
|
'ES': ('Spain', re.compile(r'^ES[A-Z0-9]\d{7}[A-Z0-9]$')), |
41
|
|
|
'FI': ('Finland', re.compile(r'^FI\d{8}$')), |
42
|
|
|
'FR': ('France', re.compile(r'^FR[A-HJ-NP-Z0-9][A-HJ-NP-Z0-9]\d{9}$'), fr_format), |
43
|
|
|
'GB': ('United Kingdom', re.compile(r'^(GB(GD|HA)\d{3}|GB\d{9}|GB\d{12})$'), gb_format), |
44
|
|
|
'HU': ('Hungary', re.compile(r'^HU\d{8}$')), |
45
|
|
|
'IE': ('Ireland', re.compile(r'^IE\d[A-Z0-9\+\*]\d{5}[A-Z]{1,2}$')), |
46
|
|
|
'IT': ('Italy', re.compile(r'^IT\d{11}$')), |
47
|
|
|
'LT': ('Lithuania', re.compile(r'^LT(\d{9}|\d{12})$')), |
48
|
|
|
'LU': ('Luxembourg', re.compile(r'^LU\d{8}$')), |
49
|
|
|
'LV': ('Latvia', re.compile(r'^LV\d{11}$')), |
50
|
|
|
'MT': ('Malta', re.compile(r'^MT\d{8}$')), |
51
|
|
|
'NL': ('The Netherlands', re.compile(r'^NL\d{9}B\d{2}$')), |
52
|
|
|
'PL': ('Poland', re.compile(r'^PL\d{10}$')), |
53
|
|
|
'PT': ('Portugal', re.compile(r'^PT\d{9}$')), |
54
|
|
|
'RO': ('Romania', re.compile(r'^RO\d{2,10}$')), |
55
|
|
|
'SE': ('Sweden', re.compile(r'^SE\d{10}01$')), |
56
|
|
|
'SI': ('Slovenia', re.compile(r'^SI\d{8}$')), |
57
|
|
|
'SK': ('Slovakia', re.compile(r'^SK\d{10}$')), |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
VIES_COUNTRY_CHOICES = sorted( |
61
|
|
|
(('', '--'),) + |
62
|
|
|
tuple( |
63
|
|
|
(key, key) |
64
|
|
|
for key, value in VIES_OPTIONS.items()) |
65
|
|
|
) |
66
|
|
|
|
67
|
|
|
MEMBER_COUNTRY_CODES = VIES_OPTIONS.keys() |
68
|
|
|
|
69
|
|
|
|
70
|
|
|
@python_2_unicode_compatible |
71
|
|
|
class VATIN(object): |
72
|
|
|
"""Object wrapper for the european VAT Identification Number.""" |
73
|
|
|
|
74
|
|
|
def __init__(self, country_code, number): |
75
|
|
|
self.country_code = country_code |
76
|
|
|
self.number = number |
77
|
|
|
|
78
|
|
|
def __str__(self): |
79
|
|
|
unformated_number = "{country_code}{number}".format( |
80
|
|
|
country_code=self.country_code, |
81
|
|
|
number=self.number, |
82
|
|
|
) |
83
|
|
|
|
84
|
|
|
country = VIES_OPTIONS.get(self.country_code, {}) |
85
|
|
|
if len(country) == 3: |
86
|
|
|
return country[2](unformated_number) |
87
|
|
|
return unformated_number |
88
|
|
|
|
89
|
|
|
def __repr__(self): |
90
|
|
|
return "<VATIN {}>".format(self.__str__()) |
91
|
|
|
|
92
|
|
|
def get_country_code(self): |
93
|
|
|
return self._country_code |
94
|
|
|
|
95
|
|
|
def set_country_code(self, value): |
96
|
|
|
self._country_code = value.upper() |
97
|
|
|
|
98
|
|
|
country_code = property(get_country_code, set_country_code) |
99
|
|
|
|
100
|
|
|
def get_number(self): |
101
|
|
|
return self._number |
102
|
|
|
|
103
|
|
|
def set_number(self, value): |
104
|
|
|
self._number = value.upper().replace(' ', '') |
105
|
|
|
|
106
|
|
|
number = property(get_number, set_number) |
107
|
|
|
|
108
|
|
|
@cached_property |
109
|
|
|
def data(self): |
110
|
|
|
"""VIES API response data.""" |
111
|
|
|
client = Client(VIES_WSDL_URL) |
112
|
|
|
try: |
113
|
|
|
return client.service.checkVat( |
114
|
|
|
self.country_code, |
115
|
|
|
self.number |
116
|
|
|
) |
117
|
|
|
except WebFault as e: |
118
|
|
|
logger.exception(e) |
119
|
|
|
raise |
120
|
|
|
|
121
|
|
|
def is_valid(self): |
122
|
|
|
try: |
123
|
|
|
self.verify() |
124
|
|
|
self.validate() |
125
|
|
|
except ValidationError: |
126
|
|
|
return False |
127
|
|
|
else: |
128
|
|
|
return True |
129
|
|
|
|
130
|
|
|
def verify_country_code(self): |
131
|
|
|
if not re.match(r'^[a-zA-Z]', self.country_code): |
132
|
|
|
msg = ugettext('%s is not a valid ISO_3166-1 country code.') |
133
|
|
|
raise ValidationError(msg % self.country_code) |
134
|
|
|
elif self.country_code not in MEMBER_COUNTRY_CODES: |
135
|
|
|
msg = ugettext('%s is not a european member state.') |
136
|
|
|
raise ValidationError(msg % self.country_code) |
137
|
|
|
|
138
|
|
|
def verify_regex(self): |
139
|
|
|
country = dict(map( |
140
|
|
|
lambda x, y: (x, y), ('country', 'validator', 'formatter'), |
141
|
|
|
VIES_OPTIONS[self.country_code] |
142
|
|
|
)) |
143
|
|
|
if not country['validator'].match("%s%s" % (self.country_code, self.number)): |
144
|
|
|
msg = ugettext("%s does not match the countries VAT ID specifications.") |
145
|
|
|
raise ValidationError(msg % self) |
146
|
|
|
|
147
|
|
|
def verify(self): |
148
|
|
|
self.verify_country_code() |
149
|
|
|
self.verify_regex() |
150
|
|
|
|
151
|
|
|
def validate(self): |
152
|
|
|
if not self.data.valid: |
153
|
|
|
msg = ugettext("%s is not a valid VATIN.") |
154
|
|
|
raise ValidationError(msg % self) |
155
|
|
|
|
156
|
|
|
@classmethod |
157
|
|
|
def from_str(cls, value): |
158
|
|
|
"""Return a VATIN object by given string.""" |
159
|
|
|
return cls(value[:2].strip(), value[2:].strip()) |
160
|
|
|
|