Index: django/contrib/localflavor/es/es_province.py
===================================================================
--- django/contrib/localflavor/es/es_province.py	(revision 0)
+++ django/contrib/localflavor/es/es_province.py	(revision 0)
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+"""
+An alphabetical list of spanish provinces, including two autonomous 
+cities, for use as `choices` in a formfield.
+
+This exists in this standalone file so that it's only imported into 
+memory when explicitly needed.
+"""
+
+PROVINCE_CHOICES = (
+    ('VI', u'Álava'),
+    ('AB', u'Albacete'),
+    ('A', u'Alicante'),
+    ('AL', u'Almería'),
+    ('O', u'Asturias'),
+    ('AV', u'Ávila'),
+    ('BA', u'Badajoz'),
+    ('PM', u'Islas Baleares'),
+    ('B', u'Barcelona'),
+    ('Bu', u'Burgos'),
+    ('CC', u'Cáceres'),
+    ('CA', u'Cádiz'),
+    ('S', u'Cantabria'),
+    ('CS', u'Castellón'),
+    ('CE', u'Ceuta'),
+    ('CR', u'Ciudad Real'),
+    ('CO', u'Córdoba'),
+    ('Cu', u'Cuenca'),
+    ('GI', u'Gerona'),
+    ('GR', u'Granada'),
+    ('Gu', u'Guadalajara'),
+    ('SS', u'Guipúzcoa'),
+    ('H', u'Huelva'),
+    ('Hu', u'Huesca'),
+    ('J', u'Jaén'),
+    ('C', u'La Coruña'),            
+    ('LO', u'La Rioja'),
+    ('GC', u'Las Palmas'),
+    ('LE', u'León'),
+    ('L', u'Lérida'),
+    ('Lu', u'Lugo'),
+    ('M', u'Madrid'),
+    ('MA', u'Málaga'),
+    ('ML', u'Melilla'),
+    ('Mu', u'Murcia'),
+    ('NA', u'Navarra'),
+    ('OR', u'Orense'),
+    ('P', u'Palencia'),
+    ('PO', u'Pontevedra'),
+    ('SA', u'Salamanca'),
+    ('TF', u'Santa Cruz de Tenerife'),
+    ('SG', u'Segovia'),
+    ('SE', u'Sevilla'),
+    ('SO', u'Soria'),
+    ('T', u'Tarragona'),
+    ('TE', u'Teruel'),
+    ('TO', u'Toledo'),
+    ('V', u'Valencia'),
+    ('VA', u'Valladolid'),
+    ('BI', u'Vizcaya'),
+    ('ZA', u'Zamora'),
+    ('Z', u'Zaragoza'),
+)
Index: django/contrib/localflavor/es/__init__.py
===================================================================
Index: django/contrib/localflavor/es/forms.py
===================================================================
--- django/contrib/localflavor/es/forms.py	(revision 0)
+++ django/contrib/localflavor/es/forms.py	(revision 0)
@@ -0,0 +1,123 @@
+# -*- 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 is 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 ESSubdivisionSelect(Select):
+    """
+    A Select widget that uses a list of spanish provinces and autonomous
+    cities as its choices.
+    """
+    def __init__(self, attrs=None):
+        from es_province import PROVINCE_CHOICES
+        super(ESSubdivisionSelect, self).__init__(attrs, choices=PROVINCE_CHOICES)
+
+
+class ESIdentityCardNumberField(RegexField):
+    """
+    A form field that validates its input as an Spanish NIF, NIE or CIF identity values
+    Apart from validating the format, this also checks the checksum-like control character
+    
+    The NIF (Nmero de identificacin fiscal):
+        Assigned to Spanish citizens
+         - 8 digits representing individual (known as the DNI)
+         - 1 control character using modulus of DNI value against an alphabetical index
+        e.g. '78699688J'
+    
+    The NIE (Nmero de Identificacin de Extranjeros):
+        Assigned to foreigners
+         - "X" or "T"
+         - 7 or 8 digits representing individual (if 7 digits, 0 must be prepended)
+         - 1 control character using modulus of DNI value against an alphabetical index
+        e.g. 'X3287690R'
+        
+    The CIF (Cdigo de identificacin fiscal):
+        Assigned to corporations
+         - 1 character representing "type"
+         - 2 digits representing the province (01 - 52)
+         - 5 digits representing business registry number from the province
+         - 1 control digit or character using calculation of 7 digits & type
+        e.g. 'B38790911'
+    
+    It acepts two optional arguments:
+    nif (default True)
+        accept NIF or NIE values
+    cif (default True)
+        accept CIF values
+    """    
+    def __init__(self, nif=True, cif=True, *args, **kwargs):
+        super(ESIdentityCardNumberField, self).__init__(r'^(\d{8}((-|\s|)[A-Za-z]{1}|)|([Xx](-|\s|)(\d{7}|\d{8})((-|\s|)[A-Za-z]{1}|))|([A-Sa-s]{1}(-|\s|)\d{7}(-|\s|)[A-Za-z0-9]{1}))$',
+            max_length=None, min_length=None,
+            error_message=_('Please enter a valid NIF, NIE or CIF.'),
+            *args, **kwargs)
+        assert nif or cif, _('"nif" or "cif" must be True')
+        self.nif = nif
+        self.cif = cif
+
+    def clean(self, value):
+        super(ESIdentityCardNumberField, self).clean(value)       
+        if value in EMPTY_VALUES:
+            return u''
+        value = unicode(value).replace('-','').replace(' ','').upper()
+        ni_control_chars = 'TRWAGMYFPDXBNJZSQVHLCKE'
+        ni_control_suffix = lambda x: ni_control_chars[long(x) % 23]
+        cif_type = 'ABCDEFGHKLMNPQS'
+        
+        try:
+            if re.match(r'^\d{8}[%s]$' % ni_control_chars, value):
+                if not self.nif or value[-1] != ni_control_suffix(value[:8]):
+                    raise # invalid NIF
+            elif re.match(r'^(X|T)\d{7,8}[%s]$' % ni_control_chars, value):
+                test_val = value[1:]
+                if len(value) == 9: # 7 digit number only
+                    test_val = '0' + test_val
+                if not self.nif or test_val[-1] != ni_control_suffix(test_val[:8]):
+                    raise # invalid NIE
+            elif re.match(r'^[%s](0[1-9]|[1-4][0-9]|5[0-2])\d{5}[A-Z0-9]$' % cif_type, value):
+                digits = value[1:8]
+                # a = sum of odd digits
+                # b = sum of digits of 2 * even digits
+                # c = a + b
+                # control = 10 - last digit of c
+                a = sum([int(v) for k,v in enumerate(digits) if k%2]) # sum of odd values
+                b_list = map(lambda x: 2*int(x), [v for k,v in enumerate(digits) if k%2 == 0])
+                b = sum([sum(map(int, list(str(x)))) for x in b_list])
+                c = a + b
+                control = 10 - int(str(c)[-1])
+                control_letters = 'JABCDEFGHI'
+                # the first char determines whether to use the control value or the corresponding control_letter
+                if (not self.cif) or \
+                   (value[0] in 'KPQS' and value[-1] != control_letters[control]) or \
+                   (value[0] in 'ABEH' and value[-1] != str(control)) or \
+                   (value[0] not in 'ABEHKPQS' and value[-1] != str(control) and value[-1] != control_letters[control]):
+                   raise # invalid CIF
+            else:
+                raise # not valid format
+            return value
+        except Exception, e:
+            # produce appropriate error
+            if not self.nif:
+                raise ValidationError, _('Please enter a valid CIF.')
+            elif not self.cif:
+                raise ValidationError, _('Please enter a valid NIF or NIE.')
+            else:
+                raise ValidationError, _('Please enter a valid NIF, NIE or CIF.')
Index: tests/regressiontests/forms/localflavor.py
===================================================================
--- tests/regressiontests/forms/localflavor.py	(revision 6194)
+++ tests/regressiontests/forms/localflavor.py	(working copy)
@@ -1818,4 +1818,198 @@
 u''
 >>> f.clean(u'')
 u''
+
+# ESPostalCodeField #############################################################
+
+ESPostalCodeField validates that the data is a valid ES postal code.
+>>> from django.contrib.localflavor.es.forms import *
+>>> f = ESPostalCodeField()
+>>> f.clean('01000')
+u'01000'
+>>> f.clean('52999')
+u'52999'
+>>> f.clean('00999')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('53000')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('0A200')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('380001')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> f = ESPostalCodeField(required=False)
+>>> f.clean('01000')
+u'01000'
+>>> f.clean('52999')
+u'52999'
+>>> f.clean('00999')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('53000')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('2A200')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean('380001')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the range and format 01XXX - 52XXX.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# ESSubdivisionSelect ###############################################################
+
+ESSubdivisionSelect
+>>> w = ESSubdivisionSelect()
+>>> w.render('provinces', 'TF')
+u'<select name="provinces">\n<option value="VI">\xc1lava</option>\n<option value="AB">Albacete</option>\n<option value="A">Alicante</option>\n<option value="AL">Almer\xeda</option>\n<option value="O">Asturias</option>\n<option value="AV">\xc1vila</option>\n<option value="BA">Badajoz</option>\n<option value="PM">Islas Baleares</option>\n<option value="B">Barcelona</option>\n<option value="Bu">Burgos</option>\n<option value="CC">C\xe1ceres</option>\n<option value="CA">C\xe1diz</option>\n<option value="S">Cantabria</option>\n<option value="CS">Castell\xf3n</option>\n<option value="CE">Ceuta</option>\n<option value="CR">Ciudad Real</option>\n<option value="CO">C\xf3rdoba</option>\n<option value="Cu">Cuenca</option>\n<option value="GI">Gerona</option>\n<option value="GR">Granada</option>\n<option value="Gu">Guadalajara</option>\n<option value="SS">Guip\xfazcoa</option>\n<option value="H">Huelva</option>\n<option value="Hu">Huesca</option>\n<option value="J">Ja\xe9n</option>\n<option value="C">La Coru\xf1a</option>\n<option value="LO">La Rioja</option>\n<option value="GC">Las Palmas</option>\n<option value="LE">Le\xf3n</option>\n<option value="L">L\xe9rida</option>\n<option value="Lu">Lugo</option>\n<option value="M">Madrid</option>\n<option value="MA">M\xe1laga</option>\n<option value="ML">Melilla</option>\n<option value="Mu">Murcia</option>\n<option value="NA">Navarra</option>\n<option value="OR">Orense</option>\n<option value="P">Palencia</option>\n<option value="PO">Pontevedra</option>\n<option value="SA">Salamanca</option>\n<option value="TF" selected="selected">Santa Cruz de Tenerife</option>\n<option value="SG">Segovia</option>\n<option value="SE">Sevilla</option>\n<option value="SO">Soria</option>\n<option value="T">Tarragona</option>\n<option value="TE">Teruel</option>\n<option value="TO">Toledo</option>\n<option value="V">Valencia</option>\n<option value="VA">Valladolid</option>\n<option value="BI">Vizcaya</option>\n<option value="ZA">Zamora</option>\n<option value="Z">Zaragoza</option>\n</select>'
+
+# ESIdentityCardNumberField #############################################################
+
+ESIdentityCardNumberField
+>>> f = ESIdentityCardNumberField()
+>>> f.clean('78699688J')
+u'78699688J'
+>>> f.clean('78699688-J')
+u'78699688J'
+>>> f.clean('78699688 J')
+u'78699688J'
+>>> f.clean('78699688 j')
+u'78699688J'
+>>> f.clean('78699688T')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
+>>> f.clean('X0901797J')
+u'X0901797J'
+>>> f.clean('X-6124387-Q')
+u'X6124387Q'
+>>> f.clean('X 0012953 G')
+u'X0012953G'
+>>> f.clean('x-3287690-r')
+u'X3287690R'
+>>> f.clean('X-03287690r')
+u'X03287690R'
+>>> 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'Please enter a valid NIF, NIE or CIF.']
+>>> f.clean('B38790911')
+u'B38790911'
+>>> f.clean('B-3879091A')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
+>>> f.clean('B 38790917')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
+>>> f.clean('P-3900800-H')
+u'P3900800H'
+>>> f.clean('P 39008008')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
+>>> f.clean('C-28795565')
+u'C28795565'
+>>> f.clean('C 2879556E')
+u'C2879556E'
+>>> f.clean('C28795567')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE or 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('999999999')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f = ESIdentityCardNumberField(required=False)
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+>>> f = ESIdentityCardNumberField()
+>>> f.clean('78699688J')
+u'78699688J'
+>>> f.clean('78699688T')
+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'Please enter a valid NIF, NIE or CIF.']
+>>> f.clean('B-3879091A')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
+>>> f.clean('P 39008008')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
+>>> f.clean('C28795567')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF, NIE or CIF.']
+>>> f = ESIdentityCardNumberField(cif=False)
+>>> f.clean('78699688J')
+u'78699688J'
+>>> f.clean('X-6124387-Q')
+u'X6124387Q'
+>>> f.clean('B38790911')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid NIF or NIE.']
+>>> f = ESIdentityCardNumberField(nif=False)
+>>> f.clean('78699688J')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid CIF.']
+>>> f.clean('X-6124387-Q')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please enter a valid CIF.']
+>>> f.clean('B38790911')
+u'B38790911'
 """
