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