From aac5555f2fb333d958e838274fd18cf3ac89d914 Mon Sep 17 00:00:00 2001
From: Malcolm Tredinnick <malcolm.tredinnick@gmail.com>
Date: Sat, 20 Oct 2007 09:16:54 +0000
Subject: [PATCH] Fixed #4036 -- Added Spanish localflavor. Thanks,
 ricardojbarrios@gmail.com and oggie_rob.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@6555 bcc190cf-cafb-0310-a4f2-bffc1f526a37
---
 AUTHORS                                       |   1 +
 django/contrib/localflavor/es/__init__.py     |   0
 django/contrib/localflavor/es/es_provinces.py |  58 +++
 django/contrib/localflavor/es/es_regions.py   |  23 ++
 django/contrib/localflavor/es/forms.py        | 173 +++++++++
 tests/regressiontests/forms/localflavor/es.py | 343 ++++++++++++++++++
 tests/regressiontests/forms/tests.py          |   2 +
 7 files changed, 600 insertions(+)
 create mode 100644 django/contrib/localflavor/es/__init__.py
 create mode 100644 django/contrib/localflavor/es/es_provinces.py
 create mode 100644 django/contrib/localflavor/es/es_regions.py
 create mode 100644 django/contrib/localflavor/es/forms.py
 create mode 100644 tests/regressiontests/forms/localflavor/es.py

diff --git a/AUTHORS b/AUTHORS
index 825ba8e658..363c7df100 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -257,6 +257,7 @@ answer newbie questions, and generally made Django that much better:
     Brian Ray <http://brianray.chipy.org/>
     remco@diji.biz
     rhettg@gmail.com
+    ricardojbarrios@gmail.com
     Matt Riggott
     Henrique Romano <onaiort@gmail.com>
     Armin Ronacher
diff --git a/django/contrib/localflavor/es/__init__.py b/django/contrib/localflavor/es/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/django/contrib/localflavor/es/es_provinces.py b/django/contrib/localflavor/es/es_provinces.py
new file mode 100644
index 0000000000..9f5e12680b
--- /dev/null
+++ b/django/contrib/localflavor/es/es_provinces.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+from django.utils.translation import ugettext_lazy as _
+
+PROVINCE_CHOICES = (
+    ('01', _('Arava')),
+    ('02', _('Albacete')),
+    ('03', _('Alacant')),
+    ('04', _('Almeria')),
+    ('05', _('Avila')),
+    ('06', _('Badajoz')),
+    ('07', _('Illes Balears')),
+    ('08', _('Barcelona')),
+    ('09', _('Burgos')),
+    ('10', _('Caceres')),
+    ('11', _('Cadiz')),
+    ('12', _('Castello')),
+    ('13', _('Ciudad Real')),
+    ('14', _('Cordoba')),
+    ('15', _('A Coruna')),
+    ('16', _('Cuenca')),
+    ('17', _('Girona')),
+    ('18', _('Granada')),
+    ('19', _('Guadalajara')),
+    ('20', _('Guipuzkoa')),
+    ('21', _('Huelva')),
+    ('22', _('Huesca')),
+    ('23', _('Jaen')),
+    ('24', _('Leon')),
+    ('25', _('Lleida')),
+    ('26', _('La Rioja')),
+    ('27', _('Lugo')),
+    ('28', _('Madrid')),
+    ('29', _('Malaga')),
+    ('30', _('Murcia')),
+    ('31', _('Navarre')),
+    ('32', _('Ourense')),
+    ('33', _('Asturias')),
+    ('34', _('Palencia')),
+    ('35', _('Las Palmas')),
+    ('36', _('Pontevedra')),
+    ('37', _('Salamanca')),
+    ('38', _('Santa Cruz de Tenerife')),
+    ('39', _('Cantabria')),
+    ('40', _('Segovia')),
+    ('41', _('Seville')),
+    ('42', _('Soria')),
+    ('43', _('Tarragona')),
+    ('44', _('Teruel')),
+    ('45', _('Toledo')),
+    ('46', _('Valencia')),
+    ('47', _('Valladolid')),
+    ('48', _('Bizkaia')),
+    ('49', _('Zamora')),
+    ('50', _('Zaragoza')),
+    ('51', _('Ceuta')),
+    ('52', _('Melilla')),
+)
+
diff --git a/django/contrib/localflavor/es/es_regions.py b/django/contrib/localflavor/es/es_regions.py
new file mode 100644
index 0000000000..3c1ea0e974
--- /dev/null
+++ b/django/contrib/localflavor/es/es_regions.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+from django.utils.translation import ugettext_lazy as _
+
+REGION_CHOICES = (
+    ('AN', _('Andalusia')),
+    ('AR', _('Aragon')),
+    ('O', _('Principality of Asturias')),
+    ('IB', _('Balearic Islands')),
+    ('PV', _('Basque Country')),
+    ('CN', _('Canary Islands')),
+    ('S', _('Cantabria')),
+    ('CM', _('Castile-La Mancha')),
+    ('CL', _('Castile and Leon')),
+    ('CT', _('Catalonia')),
+    ('EX', _('Extremadura')),
+    ('GA', _('Galicia')),
+    ('LO', _('La Rioja')),
+    ('M', _('Madrid')),
+    ('MU', _('Region of Murcia')),
+    ('NA', _('Foral Community of Navarre')),
+    ('VC', _('Valencian Community')),
+)
+
diff --git a/django/contrib/localflavor/es/forms.py b/django/contrib/localflavor/es/forms.py
new file mode 100644
index 0000000000..29b41828f6
--- /dev/null
+++ b/django/contrib/localflavor/es/forms.py
@@ -0,0 +1,173 @@
+# -*- 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.
+    """
+    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,
+                error_message=_('Enter a valid postal code in the range and format 01XXX - 52XXX.'),
+                *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
+    """
+    def __init__(self, *args, **kwargs):
+        super(ESPhoneNumberField, self).__init__(r'^(6|8|9)\d{8}$',
+                max_length=None, min_length=None,
+                error_message=_('Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.'),
+                *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.
+    """
+    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'
+        if self.only_nif:
+            self.id_types = 'NIF or NIE'
+        else:
+            self.id_types = 'NIF, NIE, or CIF'
+        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=_('Please enter a valid %s.' % self.id_types),
+                *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, _('Invalid checksum for NIF.')
+        elif letter1 in self.nie_types and letter2:
+            # NIE
+            if letter2 == nif_get_checksum(number):
+                return value
+            else:
+                raise ValidationError, _('Invalid checksum for 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, _('Invalid checksum for CIF.')
+        else:
+            raise ValidationError, _('Please enter a valid %s.' % self.id_types)
+
+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
+    """
+    def __init__(self, *args, **kwargs):
+        super(ESCCCField, self).__init__(r'^\d{4}[ -]?\d{4}[ -]?\d{2}[ -]?\d{10}$',
+            max_length=None, min_length=None,
+            error_message=_('Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX.'),
+            *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, _('Invalid checksum for bank account number.')
+
+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)
+
diff --git a/tests/regressiontests/forms/localflavor/es.py b/tests/regressiontests/forms/localflavor/es.py
new file mode 100644
index 0000000000..f149aa9cbe
--- /dev/null
+++ b/tests/regressiontests/forms/localflavor/es.py
@@ -0,0 +1,343 @@
+# -*- coding: utf-8 -*-
+# Tests for the contrib/localflavor/ ES form fields.
+
+tests = r"""
+# ESPostalCodeField ##############################################################
+
+ESPostalCodeField validates that data is a five-digit spanish postal code.
+>>> from django.contrib.localflavor.es.forms import ESPostalCodeField
+>>> f = ESPostalCodeField()
+>>> f.clean('08028')
+u'08028'
+>>> f.clean('28080')
+u'28080'
+>>> f.clean('53001')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('0801')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('080001')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('00999')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('08 01')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('08A01')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f = ESPostalCodeField(required=False)
+>>> f.clean('08028')
+u'08028'
+>>> f.clean('28080')
+u'28080'
+>>> f.clean('53001')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('0801')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('080001')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('00999')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('08 01')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('08A01')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('')
+u''
+
+# ESPhoneNumberField ##############################################################
+
+ESPhoneNumberField validates that data is a nine-digit spanish phone number.
+>>> from django.contrib.localflavor.es.forms import ESPhoneNumberField
+>>> f = ESPhoneNumberField()
+>>> f.clean('650010101')
+u'650010101'
+>>> f.clean('931234567')
+u'931234567'
+>>> f.clean('800123123')
+u'800123123'
+>>> f.clean('555555555')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.']
+>>> f.clean('789789789')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.']
+>>> f.clean('99123123')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.']
+>>> f.clean('9999123123')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f = ESPhoneNumberField(required=False)
+>>> f.clean('650010101')
+u'650010101'
+>>> f.clean('931234567')
+u'931234567'
+>>> f.clean('800123123')
+u'800123123'
+>>> f.clean('555555555')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.']
+>>> f.clean('789789789')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.']
+>>> f.clean('99123123')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.']
+>>> f.clean('9999123123')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.']
+>>> f.clean('')
+u''
+
+# ESIdentityCardNumberField ##############################################################
+
+ESIdentityCardNumberField validates that data is a identification spanish code for companies or individuals (CIF, NIF or NIE).
+>>> from django.contrib.localflavor.es.forms import ESIdentityCardNumberField
+>>> f = ESIdentityCardNumberField()
+>>> f.clean('78699688J')
+'78699688J'
+>>> f.clean('78699688-J')
+'78699688J'
+>>> f.clean('78699688 J')
+'78699688J'
+>>> f.clean('78699688 j')
+'78699688J'
+>>> f.clean('78699688T')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for NIF.']
+>>> f.clean('X0901797J')
+'X0901797J'
+>>> f.clean('X-6124387-Q')
+'X6124387Q'
+>>> f.clean('X 0012953 G')
+'X0012953G'
+>>> f.clean('x-3287690-r')
+'X3287690R'
+>>> f.clean('t-03287690r')
+'T03287690R'
+>>> f.clean('X-03287690')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE, or CIF.']
+>>> f.clean('X-03287690-T')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for NIE.']
+>>> f.clean('B38790911')
+'B38790911'
+>>> f.clean('B-3879091A')
+'B3879091A'
+>>> f.clean('B 38790917')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for CIF.']
+>>> f.clean('B 38790911')
+'B38790911'
+>>> f.clean('P-3900800-H')
+'P3900800H'
+>>> f.clean('P 39008008')
+'P39008008'
+>>> f.clean('C-28795565')
+'C28795565'
+>>> f.clean('C 2879556E')
+'C2879556E'
+>>> f.clean('C28795567')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for CIF.']
+>>> f.clean('I38790911')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE, or CIF.']
+>>> f.clean('78699688-2')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE, or CIF.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f = ESIdentityCardNumberField(required=False)
+>>> f.clean('78699688J')
+'78699688J'
+>>> f.clean('78699688-J')
+'78699688J'
+>>> f.clean('78699688 J')
+'78699688J'
+>>> f.clean('78699688 j')
+'78699688J'
+>>> f.clean('78699688T')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for NIF.']
+>>> f.clean('X0901797J')
+'X0901797J'
+>>> f.clean('X-6124387-Q')
+'X6124387Q'
+>>> f.clean('X 0012953 G')
+'X0012953G'
+>>> f.clean('x-3287690-r')
+'X3287690R'
+>>> f.clean('t-03287690r')
+'T03287690R'
+>>> f.clean('X-03287690')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE, or CIF.']
+>>> f.clean('X-03287690-T')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for NIE.']
+>>> f.clean('B38790911')
+'B38790911'
+>>> f.clean('B-3879091A')
+'B3879091A'
+>>> f.clean('B 38790917')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for CIF.']
+>>> f.clean('B 38790911')
+'B38790911'
+>>> f.clean('P-3900800-H')
+'P3900800H'
+>>> f.clean('P 39008008')
+'P39008008'
+>>> f.clean('C-28795565')
+'C28795565'
+>>> f.clean('C 2879556E')
+'C2879556E'
+>>> f.clean('C28795567')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for CIF.']
+>>> f.clean('I38790911')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE, or CIF.']
+>>> f.clean('78699688-2')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE, or CIF.']
+>>> f.clean('')
+u''
+
+# ESCCCField ##############################################################
+
+ESCCCField validates that data is a spanish bank account number (codigo cuenta cliente).
+
+>>> from django.contrib.localflavor.es.forms import ESCCCField
+>>> f = ESCCCField()
+>>> f.clean('20770338793100254321')
+'20770338793100254321'
+>>> f.clean('2077 0338 79 3100254321')
+'2077 0338 79 3100254321'
+>>> f.clean('2077-0338-79-3100254321')
+'2077-0338-79-3100254321'
+>>> f.clean('2077.0338.79.3100254321')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX.']
+>>> f.clean('2077-0338-78-3100254321')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for bank account number.']
+>>> f.clean('2077-0338-89-3100254321')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for bank account number.']
+>>> f.clean('2077-03-3879-3100254321')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f = ESCCCField(required=False)
+>>> f.clean('20770338793100254321')
+'20770338793100254321'
+>>> f.clean('2077 0338 79 3100254321')
+'2077 0338 79 3100254321'
+>>> f.clean('2077-0338-79-3100254321')
+'2077-0338-79-3100254321'
+>>> f.clean('2077.0338.79.3100254321')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX.']
+>>> f.clean('2077-0338-78-3100254321')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for bank account number.']
+>>> f.clean('2077-0338-89-3100254321')
+Traceback (most recent call last):
+...
+ValidationError: [u'Invalid checksum for bank account number.']
+>>> f.clean('2077-03-3879-3100254321')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX.']
+>>> f.clean('')
+u''
+
+# ESRegionSelect ##############################################################
+
+ESRegionSelect is a Select widget that uses a list of Spain regions as its choices.
+>>> from django.contrib.localflavor.es.forms import ESRegionSelect
+>>> w = ESRegionSelect()
+>>> w.render('regions', 'CT')
+u'<select name="regions">\n<option value="AN">Andalusia</option>\n<option value="AR">Aragon</option>\n<option value="O">Principality of Asturias</option>\n<option value="IB">Balearic Islands</option>\n<option value="PV">Basque Country</option>\n<option value="CN">Canary Islands</option>\n<option value="S">Cantabria</option>\n<option value="CM">Castile-La Mancha</option>\n<option value="CL">Castile and Leon</option>\n<option value="CT" selected="selected">Catalonia</option>\n<option value="EX">Extremadura</option>\n<option value="GA">Galicia</option>\n<option value="LO">La Rioja</option>\n<option value="M">Madrid</option>\n<option value="MU">Region of Murcia</option>\n<option value="NA">Foral Community of Navarre</option>\n<option value="VC">Valencian Community</option>\n</select>'
+
+# ESProvincenSelect ##############################################################
+
+ESProvinceSelect is a Select widget that uses a list of Spain provinces as its choices.
+>>> from django.contrib.localflavor.es.forms import ESProvinceSelect
+>>> w = ESProvinceSelect()
+>>> w.render('provinces', '08')
+u'<select name="provinces">\n<option value="01">Arava</option>\n<option value="02">Albacete</option>\n<option value="03">Alacant</option>\n<option value="04">Almeria</option>\n<option value="05">Avila</option>\n<option value="06">Badajoz</option>\n<option value="07">Illes Balears</option>\n<option value="08" selected="selected">Barcelona</option>\n<option value="09">Burgos</option>\n<option value="10">Caceres</option>\n<option value="11">Cadiz</option>\n<option value="12">Castello</option>\n<option value="13">Ciudad Real</option>\n<option value="14">Cordoba</option>\n<option value="15">A Coruna</option>\n<option value="16">Cuenca</option>\n<option value="17">Girona</option>\n<option value="18">Granada</option>\n<option value="19">Guadalajara</option>\n<option value="20">Guipuzkoa</option>\n<option value="21">Huelva</option>\n<option value="22">Huesca</option>\n<option value="23">Jaen</option>\n<option value="24">Leon</option>\n<option value="25">Lleida</option>\n<option value="26">La Rioja</option>\n<option value="27">Lugo</option>\n<option value="28">Madrid</option>\n<option value="29">Malaga</option>\n<option value="30">Murcia</option>\n<option value="31">Navarre</option>\n<option value="32">Ourense</option>\n<option value="33">Asturias</option>\n<option value="34">Palencia</option>\n<option value="35">Las Palmas</option>\n<option value="36">Pontevedra</option>\n<option value="37">Salamanca</option>\n<option value="38">Santa Cruz de Tenerife</option>\n<option value="39">Cantabria</option>\n<option value="40">Segovia</option>\n<option value="41">Seville</option>\n<option value="42">Soria</option>\n<option value="43">Tarragona</option>\n<option value="44">Teruel</option>\n<option value="45">Toledo</option>\n<option value="46">Valencia</option>\n<option value="47">Valladolid</option>\n<option value="48">Bizkaia</option>\n<option value="49">Zamora</option>\n<option value="50">Zaragoza</option>\n<option value="51">Ceuta</option>\n<option value="52">Melilla</option>\n</select>'
+
+"""
+
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index a732f9fdb0..aa33386d09 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -9,6 +9,7 @@ from localflavor.ca import tests as localflavor_ca_tests
 from localflavor.ch import tests as localflavor_ch_tests
 from localflavor.cl import tests as localflavor_cl_tests
 from localflavor.de import tests as localflavor_de_tests
+from localflavor.es import tests as localflavor_es_tests
 from localflavor.fi import tests as localflavor_fi_tests
 from localflavor.fr import tests as localflavor_fr_tests
 from localflavor.generic import tests as localflavor_generic_tests
@@ -35,6 +36,7 @@ __test__ = {
     'localflavor_ch_tests': localflavor_ch_tests,
     'localflavor_cl_tests': localflavor_cl_tests,
     'localflavor_de_tests': localflavor_de_tests,
+    'localflavor_es_tests': localflavor_es_tests,
     'localflavor_fi_tests': localflavor_fi_tests,
     'localflavor_fr_tests': localflavor_fr_tests,
     'localflavor_generic_tests': localflavor_generic_tests,