Django

Code

root/django/trunk/django/contrib/localflavor/es/forms.py

Revision 8405, 7.3 kB (checked in by russellm, 2 weeks ago)

Fixed #8229: Added handling for a special case in the validation of Spanish ID numbers. Thanks to Marc Garcia for the patch.

  • Property svn:eol-style set to native
Line 
1 # -*- coding: utf-8 -*-
2 """
3 Spanish-specific Form helpers
4 """
5
6 from django.forms import ValidationError
7 from django.forms.fields import RegexField, Select, EMPTY_VALUES
8 from django.utils.translation import ugettext_lazy 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)) % 10
Note: See TracBrowser for help on using the browser.