app/django/contrib/localflavor/es/forms.py
changeset 54 03e267d67478
child 323 ff1a9aa48cfd
equal deleted inserted replaced
53:57b4279d8c4e 54:03e267d67478
       
     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     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)
       
   184