|
1 # -*- coding: utf-8 -*- |
|
2 """ |
|
3 Spanish-specific Form helpers |
|
4 """ |
|
5 |
|
6 from django.newforms import ValidationError |
|
7 from django.newforms.fields import RegexField, Select, EMPTY_VALUES |
|
8 from django.utils.translation import ugettext as _ |
|
9 import re |
|
10 |
|
11 class ESPostalCodeField(RegexField): |
|
12 """ |
|
13 A form field that validates its input as a spanish postal code. |
|
14 |
|
15 Spanish postal code is a five digits string, with two first digits |
|
16 between 01 and 52, assigned to provinces code. |
|
17 """ |
|
18 default_error_messages = { |
|
19 'invalid': _('Enter a valid postal code in the range and format 01XXX - 52XXX.'), |
|
20 } |
|
21 |
|
22 def __init__(self, *args, **kwargs): |
|
23 super(ESPostalCodeField, self).__init__( |
|
24 r'^(0[1-9]|[1-4][0-9]|5[0-2])\d{3}$', |
|
25 max_length=None, min_length=None, *args, **kwargs) |
|
26 |
|
27 class ESPhoneNumberField(RegexField): |
|
28 """ |
|
29 A form field that validates its input as a Spanish phone number. |
|
30 Information numbers are ommited. |
|
31 |
|
32 Spanish phone numbers are nine digit numbers, where first digit is 6 (for |
|
33 cell phones), 8 (for special phones), or 9 (for landlines and special |
|
34 phones) |
|
35 |
|
36 TODO: accept and strip characters like dot, hyphen... in phone number |
|
37 """ |
|
38 default_error_messages = { |
|
39 'invalid': _('Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.'), |
|
40 } |
|
41 |
|
42 def __init__(self, *args, **kwargs): |
|
43 super(ESPhoneNumberField, self).__init__(r'^(6|8|9)\d{8}$', |
|
44 max_length=None, min_length=None, *args, **kwargs) |
|
45 |
|
46 class ESIdentityCardNumberField(RegexField): |
|
47 """ |
|
48 Spanish NIF/NIE/CIF (Fiscal Identification Number) code. |
|
49 |
|
50 Validates three diferent formats: |
|
51 |
|
52 NIF (individuals): 12345678A |
|
53 CIF (companies): A12345678 |
|
54 NIE (foreigners): X12345678A |
|
55 |
|
56 according to a couple of simple checksum algorithms. |
|
57 |
|
58 Value can include a space or hyphen separator between number and letters. |
|
59 Number length is not checked for NIF (or NIE), old values start with a 1, |
|
60 and future values can contain digits greater than 8. The CIF control digit |
|
61 can be a number or a letter depending on company type. Algorithm is not |
|
62 public, and different authors have different opinions on which ones allows |
|
63 letters, so both validations are assumed true for all types. |
|
64 """ |
|
65 default_error_messages = { |
|
66 'invalid': _('Please enter a valid NIF, NIE, or CIF.'), |
|
67 'invalid_only_nif': _('Please enter a valid NIF or NIE.'), |
|
68 'invalid_nif': _('Invalid checksum for NIF.'), |
|
69 'invalid_nie': _('Invalid checksum for NIE.'), |
|
70 'invalid_cif': _('Invalid checksum for CIF.'), |
|
71 } |
|
72 |
|
73 def __init__(self, only_nif=False, *args, **kwargs): |
|
74 self.only_nif = only_nif |
|
75 self.nif_control = 'TRWAGMYFPDXBNJZSQVHLCKE' |
|
76 self.cif_control = 'JABCDEFGHI' |
|
77 self.cif_types = 'ABCDEFGHKLMNPQS' |
|
78 self.nie_types = 'XT' |
|
79 super(ESIdentityCardNumberField, self).__init__(r'^([%s]?)[ -]?(\d+)[ -]?([%s]?)$' % (self.cif_types + self.nie_types + self.cif_types.lower() + self.nie_types.lower(), self.nif_control + self.nif_control.lower()), |
|
80 max_length=None, min_length=None, |
|
81 error_message=self.default_error_messages['invalid%s' % (self.only_nif and '_only_nif' or '')], |
|
82 *args, **kwargs) |
|
83 |
|
84 def clean(self, value): |
|
85 super(ESIdentityCardNumberField, self).clean(value) |
|
86 if value in EMPTY_VALUES: |
|
87 return u'' |
|
88 nif_get_checksum = lambda d: self.nif_control[int(d)%23] |
|
89 |
|
90 value = value.upper().replace(' ', '').replace('-', '') |
|
91 m = re.match(r'^([%s]?)[ -]?(\d+)[ -]?([%s]?)$' % (self.cif_types + self.nie_types, self.nif_control), value) |
|
92 letter1, number, letter2 = m.groups() |
|
93 |
|
94 if not letter1 and letter2: |
|
95 # NIF |
|
96 if letter2 == nif_get_checksum(number): |
|
97 return value |
|
98 else: |
|
99 raise ValidationError, self.error_messages['invalid_nif'] |
|
100 elif letter1 in self.nie_types and letter2: |
|
101 # NIE |
|
102 if letter2 == nif_get_checksum(number): |
|
103 return value |
|
104 else: |
|
105 raise ValidationError, self.error_messages['invalid_nie'] |
|
106 elif not self.only_nif and letter1 in self.cif_types and len(number) in [7, 8]: |
|
107 # CIF |
|
108 if not letter2: |
|
109 number, letter2 = number[:-1], int(number[-1]) |
|
110 checksum = cif_get_checksum(number) |
|
111 if letter2 in [checksum, self.cif_control[checksum]]: |
|
112 return value |
|
113 else: |
|
114 raise ValidationError, self.error_messages['invalid_cif'] |
|
115 else: |
|
116 raise ValidationError, self.error_messages['invalid'] |
|
117 |
|
118 class ESCCCField(RegexField): |
|
119 """ |
|
120 A form field that validates its input as a Spanish bank account or CCC |
|
121 (Codigo Cuenta Cliente). |
|
122 |
|
123 Spanish CCC is in format EEEE-OOOO-CC-AAAAAAAAAA where: |
|
124 |
|
125 E = entity |
|
126 O = office |
|
127 C = checksum |
|
128 A = account |
|
129 |
|
130 It's also valid to use a space as delimiter, or to use no delimiter. |
|
131 |
|
132 First checksum digit validates entity and office, and last one |
|
133 validates account. Validation is done multiplying every digit of 10 |
|
134 digit value (with leading 0 if necessary) by number in its position in |
|
135 string 1, 2, 4, 8, 5, 10, 9, 7, 3, 6. Sum resulting numbers and extract |
|
136 it from 11. Result is checksum except when 10 then is 1, or when 11 |
|
137 then is 0. |
|
138 |
|
139 TODO: allow IBAN validation too |
|
140 """ |
|
141 default_error_messages = { |
|
142 'invalid': _('Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX.'), |
|
143 'checksum': _('Invalid checksum for bank account number.'), |
|
144 } |
|
145 |
|
146 def __init__(self, *args, **kwargs): |
|
147 super(ESCCCField, self).__init__(r'^\d{4}[ -]?\d{4}[ -]?\d{2}[ -]?\d{10}$', |
|
148 max_length=None, min_length=None, *args, **kwargs) |
|
149 |
|
150 def clean(self, value): |
|
151 super(ESCCCField, self).clean(value) |
|
152 if value in EMPTY_VALUES: |
|
153 return u'' |
|
154 control_str = [1, 2, 4, 8, 5, 10, 9, 7, 3, 6] |
|
155 m = re.match(r'^(\d{4})[ -]?(\d{4})[ -]?(\d{2})[ -]?(\d{10})$', value) |
|
156 entity, office, checksum, account = m.groups() |
|
157 get_checksum = lambda d: str(11 - sum([int(digit) * int(control) for digit, control in zip(d, control_str)]) % 11).replace('10', '1').replace('11', '0') |
|
158 if get_checksum('00' + entity + office) + get_checksum(account) == checksum: |
|
159 return value |
|
160 else: |
|
161 raise ValidationError, self.error_messages['checksum'] |
|
162 |
|
163 class ESRegionSelect(Select): |
|
164 """ |
|
165 A Select widget that uses a list of spanish regions as its choices. |
|
166 """ |
|
167 def __init__(self, attrs=None): |
|
168 from es_regions import REGION_CHOICES |
|
169 super(ESRegionSelect, self).__init__(attrs, choices=REGION_CHOICES) |
|
170 |
|
171 class ESProvinceSelect(Select): |
|
172 """ |
|
173 A Select widget that uses a list of spanish provinces as its choices. |
|
174 """ |
|
175 def __init__(self, attrs=None): |
|
176 from es_provinces import PROVINCE_CHOICES |
|
177 super(ESProvinceSelect, self).__init__(attrs, choices=PROVINCE_CHOICES) |
|
178 |
|
179 |
|
180 def cif_get_checksum(number): |
|
181 s1 = sum([int(digit) for pos, digit in enumerate(number) if int(pos) % 2]) |
|
182 s2 = sum([sum([int(unit) for unit in str(int(digit) * 2)]) for pos, digit in enumerate(number) if not int(pos) % 2]) |
|
183 return 10 - ((s1 + s2) % 10) |
|
184 |