| | 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 | |