summaryrefslogtreecommitdiffstats
path: root/webapp/django/contrib/localflavor/es/forms.py
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/django/contrib/localflavor/es/forms.py')
-rw-r--r--webapp/django/contrib/localflavor/es/forms.py184
1 files changed, 184 insertions, 0 deletions
diff --git a/webapp/django/contrib/localflavor/es/forms.py b/webapp/django/contrib/localflavor/es/forms.py
new file mode 100644
index 0000000000..145bf90768
--- /dev/null
+++ b/webapp/django/contrib/localflavor/es/forms.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+"""
+Spanish-specific Form helpers
+"""
+
+from django.forms import ValidationError
+from django.forms.fields import RegexField, Select, EMPTY_VALUES
+from django.utils.translation import ugettext_lazy 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)) % 10
+