Index: contrib/localflavor/es/es_province.py
===================================================================
--- contrib/localflavor/es/es_province.py	(revision 0)
+++ 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: contrib/localflavor/es/__init__.py
===================================================================
Index: contrib/localflavor/es/test.py
===================================================================
--- contrib/localflavor/es/test.py	(revision 0)
+++ contrib/localflavor/es/test.py	(revision 0)
@@ -0,0 +1,208 @@
+"""
+# ESPostalCodeField #############################################################
+
+ESPostalCodeField validates that the data is a valid ES postal code.
+>>> from django.contrib.localflavor.es.forms import ESPostalCodeField
+>>> 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 format XXXXX.']
+>>> f.clean('53000')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the format XXXXX.']
+>>> f.clean('2A200')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the format XXXXX.']
+>>> f.clean('380001')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the format XXXXX.']
+>>> 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 format XXXXX.']
+>>> f.clean('53000')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the format XXXXX.']
+>>> f.clean('2A200')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the format XXXXX.']
+>>> f.clean('380001')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid postal code in the format XXXXX.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# ESSubdivisionSelect ###############################################################
+
+ESSubdivisionSelect
+>>> from django.contrib.localflavor.es.forms import 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
+>>> from django.contrib.localflavor.es.forms import ESIdentityCardNumberField
+>>> f = ESIdentityCardNumberField()
+>>> f.clean('78699688')
+u'78699688J'
+>>> 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"The NIF you entered isn't valid. It should be 78699688J"]
+>>> 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')
+u'X03287690R'
+>>> f.clean('X-03287690-T')
+Traceback (most recent call last):
+...
+ValidationError: [u"The NIE you entered isn't valid. It should be X03287690R"]
+>>> f.clean('B38790911')
+u'B38790911'
+>>> f.clean('B-3879091A')
+Traceback (most recent call last):
+...
+ValidationError: [u"The CIF you entered isn't valid. It should be B38790911"]
+>>> f.clean('B 38790917')
+Traceback (most recent call last):
+...
+ValidationError: [u"The CIF you entered isn't valid. It should be B38790911"]
+>>> f.clean('P-3900800-H')
+u'P3900800H'
+>>> f.clean('P 39008008')
+Traceback (most recent call last):
+...
+ValidationError: [u"The CIF you entered isn't valid. It should be P3900800H"]
+>>> f.clean('C-28795565')
+u'C28795565'
+>>> f.clean('C 2879556E')
+u'C2879556E'
+>>> f.clean('C28795567')
+Traceback (most recent call last):
+...
+ValidationError: [u"The CIF you entered isn't valid. It should be C2879556E or C28795565"]
+>>> f.clean('I38790911')
+Traceback (most recent call last):
+...
+ValidationError: [u"The CIF you entered isn't valid. It must start with A,B,C,D,E,F,G,H,K,L,M,N,P,Q or S"]
+>>> f.clean('78699688-2')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please, enter a valid CIF, NIF or NIE.']
+>>> f.clean('999999999')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please, enter a valid CIF, NIF or NIE.']
+>>> 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(show_correct=False)
+>>> f.clean('78699688J')
+u'78699688J'
+>>> f.clean('78699688T')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please, enter a valid NIF.']
+>>> f.clean('X-03287690-T')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please, enter a valid NIE.']
+>>> f.clean('B-3879091A')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please, enter a valid CIF.']
+>>> f.clean('P 39008008')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please, enter a valid CIF.']
+>>> f.clean('C28795567')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please, enter a valid CIF.']
+>>> f = ESIdentityCardNumberField(calc_control=False)
+>>> f.clean('78699688J')
+u'78699688J'
+>>> f.clean('78699688')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please, enter a valid NIF.']
+>>> f.clean('X-03287690')
+Traceback (most recent call last):
+...
+ValidationError: [u'Please, enter a valid NIE.']
+>>> 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'
+"""
Index: contrib/localflavor/es/forms.py
===================================================================
--- contrib/localflavor/es/forms.py	(revision 0)
+++ contrib/localflavor/es/forms.py	(revision 0)
@@ -0,0 +1,137 @@
+"""
+Spanish-specific Form helpers
+"""
+
+from django.newforms import ValidationError
+from django.newforms.fields import RegexField, Select, EMPTY_VALUES
+from django.utils.translation import gettext
+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'^([1-4]\d|0[^0]|5[012])\d{3}$',
+            max_length=None, min_length=None,
+            error_message=gettext(u'Enter a valid postal code in the format XXXXX.'),
+            *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, CIF or NIE
+    
+    It acepts four new arguments:
+    nif
+        accept NIF or NIE
+    cif
+        accept CIF
+    show_correct
+        show the correct value if it's not valid
+    calc_control
+        calculate the control character of NIF and NIE if it's not present
+    """    
+    def _nif(self, dni):
+        return "TRWAGMYFPDXBNJZSQVHLCKE"[long(dni)%23]
+
+    def __init__(self, nif=True, cif=True, show_correct=True, calc_control=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=gettext(u'Please, enter a valid CIF, NIF or NIE.'),
+            *args, **kwargs)
+        self.show_correct = show_correct
+        self.calc_control = calc_control
+        self.nif = nif
+        self.cif = cif
+        # What raise if nif and cif are false?
+
+    def clean(self, value):
+        super(ESIdentityCardNumberField, self).clean(value)       
+        if value in EMPTY_VALUES:
+            return u''
+        value = value.replace('-','').replace(' ','').upper()
+        fchar = value[0]
+        lchar = value[-1]
+        nif_error_msg = gettext(u'Please, enter a valid NIF.')
+        nie_error_msg = gettext(u'Please, enter a valid NIE.')
+        nifnie_error_msg = gettext(u'Please, enter a valid NIF or NIE.')
+        cif_error_msg = gettext(u'Please, enter a valid CIF.')
+        if fchar.isdigit(): # Validate or calculate NIF
+            if not self.nif:
+                raise ValidationError(cif_error_msg)      
+            if not self.calc_control and len(value) == 8:
+                raise ValidationError(nif_error_msg)
+            dni = value[:8]
+            nif = '%s%s' % (dni, self._nif(dni))
+            if lchar.isalpha() and value != nif:
+                if self.show_correct:
+                    raise ValidationError(gettext(u'The NIF you entered isn\'t valid. It should be %(nif)s' % {'nif': nif}))
+                else:
+                    raise ValidationError(nif_error_msg)
+            return u'%s' % nif
+        elif fchar == 'X': # Validate or calculate NIE
+            if not self.nif:
+                raise ValidationError(cif_error_msg)
+            if not self.calc_control and not lchar.isalpha():
+                raise ValidationError(nie_error_msg)
+            dni = value[1:]
+            if lchar.isalpha():
+                dni = dni[:-1]
+            nie = 'X%s%s' % (dni, self._nif(dni))
+            if lchar.isalpha() and value != nie:
+                if self.show_correct:
+                    raise ValidationError(gettext(u'The NIE you entered isn\'t valid. It should be %(nie)s' % {'nie': nie}))
+                else:
+                    raise ValidationError(nie_error_msg)
+            return u'%s' % nie
+        else: # Validate CIF     
+            if not self.cif:
+                raise ValidationError(nifnie_error_msg)       
+            if not fchar in 'ABCDEFGHKLMNPQS':
+                raise ValidationError(gettext(u'The CIF you entered isn\'t valid. It must start with A,B,C,D,E,F,G,H,K,L,M,N,P,Q or S'))
+            digits = value[1:8]
+            a = int(digits[1]) + int(digits[3]) + int(digits[5])
+            b = 0
+            for d in [digits[0], digits[2], digits[4], digits[6]]:
+                v= str(int(d) * 2)
+                if len(v) == 2:
+                    v = int(v[0]) + int(v[1])
+                b = b + int(v)
+            control = 10 - int(str(a + b)[-1])
+            if fchar in 'KPQS':
+                cif = '%s%s%s' % (fchar, digits, "JABCDEFGHI"[control])
+                if value != cif:
+                    if self.show_correct:
+                        raise ValidationError(gettext(u'The CIF you entered isn\'t valid. It should be %(cif)s' % {'cif': cif}))
+                    else:
+                        raise ValidationError(cif_error_msg)
+            elif fchar in 'ABEH':
+                cif = '%s%s%s' % (fchar, digits, control)
+                if value != cif:
+                    if self.show_correct:
+                        raise ValidationError(gettext(u'The CIF you entered isn\'t valid. It should be %(cif)s' % {'cif': cif}))
+                    else:
+                        raise ValidationError(cif_error_msg)
+            else:
+                cif1 = '%s%s%s' % (fchar, digits, "JABCDEFGHI"[control])
+                cif2 = '%s%s%s' % (fchar, digits, control)
+                if value != cif1 and value != cif2:
+                    if self.show_correct:
+                        raise ValidationError(gettext(u'The CIF you entered isn\'t valid. It should be %(cif1)s or %(cif2)s' % {'cif1': cif1, 'cif2': cif2}))
+                    else:
+                        raise ValidationError(cif_error_msg)
+            return u'%s' % value
