diff -r 57b4279d8c4e -r 03e267d67478 app/django/contrib/localflavor/es/forms.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/django/contrib/localflavor/es/forms.py Fri Jul 18 18:22:23 2008 +0000 @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +""" +Spanish-specific Form helpers +""" + +from django.newforms import ValidationError +from django.newforms.fields import RegexField, Select, EMPTY_VALUES +from django.utils.translation import ugettext as _ +import re + +class ESPostalCodeField(RegexField): + """ + A form field that validates its input as a spanish postal code. + + Spanish postal code is a five digits string, with two first digits + between 01 and 52, assigned to provinces code. + """ + default_error_messages = { + 'invalid': _('Enter a valid postal code in the range and format 01XXX - 52XXX.'), + } + + def __init__(self, *args, **kwargs): + super(ESPostalCodeField, self).__init__( + r'^(0[1-9]|[1-4][0-9]|5[0-2])\d{3}$', + max_length=None, min_length=None, *args, **kwargs) + +class ESPhoneNumberField(RegexField): + """ + A form field that validates its input as a Spanish phone number. + Information numbers are ommited. + + Spanish phone numbers are nine digit numbers, where first digit is 6 (for + cell phones), 8 (for special phones), or 9 (for landlines and special + phones) + + TODO: accept and strip characters like dot, hyphen... in phone number + """ + default_error_messages = { + 'invalid': _('Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.'), + } + + def __init__(self, *args, **kwargs): + super(ESPhoneNumberField, self).__init__(r'^(6|8|9)\d{8}$', + max_length=None, min_length=None, *args, **kwargs) + +class ESIdentityCardNumberField(RegexField): + """ + Spanish NIF/NIE/CIF (Fiscal Identification Number) code. + + Validates three diferent formats: + + NIF (individuals): 12345678A + CIF (companies): A12345678 + NIE (foreigners): X12345678A + + according to a couple of simple checksum algorithms. + + Value can include a space or hyphen separator between number and letters. + Number length is not checked for NIF (or NIE), old values start with a 1, + and future values can contain digits greater than 8. The CIF control digit + can be a number or a letter depending on company type. Algorithm is not + public, and different authors have different opinions on which ones allows + letters, so both validations are assumed true for all types. + """ + default_error_messages = { + 'invalid': _('Please enter a valid NIF, NIE, or CIF.'), + 'invalid_only_nif': _('Please enter a valid NIF or NIE.'), + 'invalid_nif': _('Invalid checksum for NIF.'), + 'invalid_nie': _('Invalid checksum for NIE.'), + 'invalid_cif': _('Invalid checksum for CIF.'), + } + + def __init__(self, only_nif=False, *args, **kwargs): + self.only_nif = only_nif + self.nif_control = 'TRWAGMYFPDXBNJZSQVHLCKE' + self.cif_control = 'JABCDEFGHI' + self.cif_types = 'ABCDEFGHKLMNPQS' + self.nie_types = 'XT' + 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()), + max_length=None, min_length=None, + error_message=self.default_error_messages['invalid%s' % (self.only_nif and '_only_nif' or '')], + *args, **kwargs) + + def clean(self, value): + super(ESIdentityCardNumberField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + nif_get_checksum = lambda d: self.nif_control[int(d)%23] + + value = value.upper().replace(' ', '').replace('-', '') + m = re.match(r'^([%s]?)[ -]?(\d+)[ -]?([%s]?)$' % (self.cif_types + self.nie_types, self.nif_control), value) + letter1, number, letter2 = m.groups() + + if not letter1 and letter2: + # NIF + if letter2 == nif_get_checksum(number): + return value + else: + raise ValidationError, self.error_messages['invalid_nif'] + elif letter1 in self.nie_types and letter2: + # NIE + if letter2 == nif_get_checksum(number): + return value + else: + raise ValidationError, self.error_messages['invalid_nie'] + elif not self.only_nif and letter1 in self.cif_types and len(number) in [7, 8]: + # CIF + if not letter2: + number, letter2 = number[:-1], int(number[-1]) + checksum = cif_get_checksum(number) + if letter2 in [checksum, self.cif_control[checksum]]: + return value + else: + raise ValidationError, self.error_messages['invalid_cif'] + else: + raise ValidationError, self.error_messages['invalid'] + +class ESCCCField(RegexField): + """ + A form field that validates its input as a Spanish bank account or CCC + (Codigo Cuenta Cliente). + + Spanish CCC is in format EEEE-OOOO-CC-AAAAAAAAAA where: + + E = entity + O = office + C = checksum + A = account + + It's also valid to use a space as delimiter, or to use no delimiter. + + First checksum digit validates entity and office, and last one + validates account. Validation is done multiplying every digit of 10 + digit value (with leading 0 if necessary) by number in its position in + string 1, 2, 4, 8, 5, 10, 9, 7, 3, 6. Sum resulting numbers and extract + it from 11. Result is checksum except when 10 then is 1, or when 11 + then is 0. + + TODO: allow IBAN validation too + """ + default_error_messages = { + 'invalid': _('Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX.'), + 'checksum': _('Invalid checksum for bank account number.'), + } + + def __init__(self, *args, **kwargs): + super(ESCCCField, self).__init__(r'^\d{4}[ -]?\d{4}[ -]?\d{2}[ -]?\d{10}$', + max_length=None, min_length=None, *args, **kwargs) + + def clean(self, value): + super(ESCCCField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + control_str = [1, 2, 4, 8, 5, 10, 9, 7, 3, 6] + m = re.match(r'^(\d{4})[ -]?(\d{4})[ -]?(\d{2})[ -]?(\d{10})$', value) + entity, office, checksum, account = m.groups() + 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') + if get_checksum('00' + entity + office) + get_checksum(account) == checksum: + return value + else: + raise ValidationError, self.error_messages['checksum'] + +class ESRegionSelect(Select): + """ + A Select widget that uses a list of spanish regions as its choices. + """ + def __init__(self, attrs=None): + from es_regions import REGION_CHOICES + super(ESRegionSelect, self).__init__(attrs, choices=REGION_CHOICES) + +class ESProvinceSelect(Select): + """ + A Select widget that uses a list of spanish provinces as its choices. + """ + def __init__(self, attrs=None): + from es_provinces import PROVINCE_CHOICES + super(ESProvinceSelect, self).__init__(attrs, choices=PROVINCE_CHOICES) + + +def cif_get_checksum(number): + s1 = sum([int(digit) for pos, digit in enumerate(number) if int(pos) % 2]) + s2 = sum([sum([int(unit) for unit in str(int(digit) * 2)]) for pos, digit in enumerate(number) if not int(pos) % 2]) + return 10 - ((s1 + s2) % 10) +