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