|
| 1 | +# -*- coding: utf-8 -*- |
1 | 2 | """ |
2 | 3 | Mexican-specific form helpers. |
3 | 4 | """ |
| 5 | +import re |
4 | 6 |
|
5 | | -from django.forms.fields import Select |
| 7 | +from django.forms import ValidationError |
| 8 | +from django.forms.fields import Select, RegexField |
| 9 | +from django.utils.translation import ugettext_lazy as _ |
| 10 | +from django.core.validators import EMPTY_VALUES |
| 11 | +from django.contrib.localflavor.mx.mx_states import STATE_CHOICES |
| 12 | + |
| 13 | +DATE_RE = r'\d{2}((01|03|05|07|08|10|12)(0[1-9]|[12]\d|3[01])|02(0[1-9]|[12]\d)|(04|06|09|11)(0[1-9]|[12]\d|30))' |
| 14 | + |
| 15 | +""" |
| 16 | +This is the list of inconvenient words according to the `Anexo IV` of the |
| 17 | +document described in the next link: |
| 18 | + http://www.sisi.org.mx/jspsi/documentos/2005/seguimiento/06101/0610100162005_065.doc |
| 19 | +""" |
| 20 | + |
| 21 | +RFC_INCONVENIENT_WORDS = [ |
| 22 | + u'BUEI', u'BUEY', u'CACA', u'CACO', u'CAGA', u'CAGO', u'CAKA', u'CAKO', |
| 23 | + u'COGE', u'COJA', u'COJE', u'COJI', u'COJO', u'CULO', u'FETO', u'GUEY', |
| 24 | + u'JOTO', u'KACA', u'KACO', u'KAGA', u'KAGO', u'KOGE', u'KOJO', u'KAKA', |
| 25 | + u'KULO', u'MAME', u'MAMO', u'MEAR', u'MEAS', u'MEON', u'MION', u'MOCO', |
| 26 | + u'MULA', u'PEDA', u'PEDO', u'PENE', u'PUTA', u'PUTO', u'QULO', u'RATA', |
| 27 | + u'RUIN', |
| 28 | +] |
| 29 | + |
| 30 | +""" |
| 31 | +This is the list of inconvenient words according to the `Anexo 2` of the |
| 32 | +document described in the next link: |
| 33 | + http://portal.veracruz.gob.mx/pls/portal/url/ITEM/444112558A57C6E0E040A8C02E00695C |
| 34 | +""" |
| 35 | +CURP_INCONVENIENT_WORDS = [ |
| 36 | + u'BACA', u'BAKA', u'BUEI', u'BUEY', u'CACA', u'CACO', u'CAGA', u'CAGO', |
| 37 | + u'CAKA', u'CAKO', u'COGE', u'COGI', u'COJA', u'COJE', u'COJI', u'COJO', |
| 38 | + u'COLA', u'CULO', u'FALO', u'FETO', u'GETA', u'GUEI', u'GUEY', u'JETA', |
| 39 | + u'JOTO', u'KACA', u'KACO', u'KAGA', u'KAGO', u'KAKA', u'KAKO', u'KOGE', |
| 40 | + u'KOGI', u'KOJA', u'KOJE', u'KOJI', u'KOJO', u'KOLA', u'KULO', u'LILO', |
| 41 | + u'LOCA', u'LOCO', u'LOKA', u'LOKO', u'MAME', u'MAMO', u'MEAR', u'MEAS', |
| 42 | + u'MEON', u'MIAR', u'MION', u'MOCO', u'MOKO', u'MULA', u'MULO', u'NACA', |
| 43 | + u'NACO', u'PEDA', u'PEDO', u'PENE', u'PIPI', u'PITO', u'POPO', u'PUTA', |
| 44 | + u'PUTO', u'QULO', u'RATA', u'ROBA', u'ROBE', u'ROBO', u'RUIN', u'SENO', |
| 45 | + u'TETA', u'VACA', u'VAGA', u'VAGO', u'VAKA', u'VUEI', u'VUEY', u'WUEI', |
| 46 | + u'WUEY', |
| 47 | +] |
6 | 48 |
|
7 | 49 | class MXStateSelect(Select): |
8 | 50 | """ |
9 | 51 | A Select widget that uses a list of Mexican states as its choices. |
10 | 52 | """ |
11 | 53 | def __init__(self, attrs=None): |
12 | | - from mx_states import STATE_CHOICES |
13 | 54 | super(MXStateSelect, self).__init__(attrs, choices=STATE_CHOICES) |
14 | 55 |
|
| 56 | + |
| 57 | +class MXZipCodeField(RegexField): |
| 58 | + """ |
| 59 | + A form field that accepts a Mexican Zip Code. |
| 60 | +
|
| 61 | + More info about this: |
| 62 | + http://en.wikipedia.org/wiki/List_of_postal_codes_in_Mexico |
| 63 | + """ |
| 64 | + default_error_messages = { |
| 65 | + 'invalid': _(u'Enter a valid zip code in the format XXXXX.'), |
| 66 | + } |
| 67 | + |
| 68 | + def __init__(self, *args, **kwargs): |
| 69 | + zip_code_re = ur'^(0[1-9]|[1][0-6]|[2-9]\d)(\d{3})$' |
| 70 | + super(MXZipCodeField, self).__init__(zip_code_re, *args, **kwargs) |
| 71 | + |
| 72 | + |
| 73 | +class MXRFCField(RegexField): |
| 74 | + """ |
| 75 | + A form field that validates a Mexican *Registro Federal de Contribuyentes* |
| 76 | + for either `Persona física` or `Persona moral`. |
| 77 | +
|
| 78 | + The Persona física RFC string is integrated by a juxtaposition of |
| 79 | + characters following the next pattern: |
| 80 | +
|
| 81 | + ===== ====== =========================================== |
| 82 | + Index Format Accepted Characters |
| 83 | + ===== ====== =========================================== |
| 84 | + 1 X Any letter |
| 85 | + 2 X Any vowel |
| 86 | + 3-4 XX Any letter |
| 87 | + 5-10 YYMMDD Any valid date |
| 88 | + 11-12 XX Any letter or number between 0 and 9 |
| 89 | + 13 X Any digit between 0 and 9 or the letter *A* |
| 90 | + ===== ====== =========================================== |
| 91 | +
|
| 92 | + The Persona moral RFC string is integrated by a juxtaposition of |
| 93 | + characters following the next pattern: |
| 94 | +
|
| 95 | + ===== ====== ============================================ |
| 96 | + Index Format Accepted Characters |
| 97 | + ===== ====== ============================================ |
| 98 | + 1-3 XXX Any letter including *&* and *Ñ* chars |
| 99 | + 4-9 YYMMDD Any valid date |
| 100 | + 10-11 XX Any letter or number between 0 and 9 |
| 101 | + 12 X Any number between 0 and 9 or the letter *A* |
| 102 | + ===== ====== ============================================ |
| 103 | +
|
| 104 | + More info about this: |
| 105 | + http://es.wikipedia.org/wiki/Registro_Federal_de_Contribuyentes_(M%C3%A9xico) |
| 106 | + """ |
| 107 | + default_error_messages = { |
| 108 | + 'invalid': _('Enter a valid RFC.'), |
| 109 | + 'invalid_checksum': _('Invalid checksum for RFC.'), |
| 110 | + } |
| 111 | + |
| 112 | + def __init__(self, min_length=9, max_length=13, *args, **kwargs): |
| 113 | + rfc_re = re.compile(ur'^([A-Z&Ññ]{3}|[A-Z][AEIOU][A-Z]{2})%s([A-Z0-9]{2}[0-9A])?$' % DATE_RE, |
| 114 | + re.IGNORECASE) |
| 115 | + super(MXRFCField, self).__init__(rfc_re, min_length=min_length, |
| 116 | + max_length=max_length, *args, **kwargs) |
| 117 | + |
| 118 | + def clean(self, value): |
| 119 | + value = super(MXRFCField, self).clean(value) |
| 120 | + if value in EMPTY_VALUES: |
| 121 | + return u'' |
| 122 | + value = value.upper() |
| 123 | + if self._has_homoclave(value): |
| 124 | + if not value[-1] == self._checksum(value[:-1]): |
| 125 | + raise ValidationError(self.default_error_messages['invalid_checksum']) |
| 126 | + if self._has_inconvenient_word(value): |
| 127 | + raise ValidationError(self.default_error_messages['invalid']) |
| 128 | + return value |
| 129 | + |
| 130 | + def _has_homoclave(self, rfc): |
| 131 | + """ |
| 132 | + This check is done due to the existance of RFCs without a *homoclave* |
| 133 | + since the current algorithm to calculate it had not been created for |
| 134 | + the first RFCs ever in Mexico. |
| 135 | + """ |
| 136 | + rfc_without_homoclave_re = re.compile(ur'^[A-Z&Ññ]{3,4}%s$' % DATE_RE, |
| 137 | + re.IGNORECASE) |
| 138 | + return not rfc_without_homoclave_re.match(rfc) |
| 139 | + |
| 140 | + def _checksum(self, rfc): |
| 141 | + """ |
| 142 | + More info about this procedure: |
| 143 | + www.sisi.org.mx/jspsi/documentos/2005/seguimiento/06101/0610100162005_065.doc |
| 144 | + """ |
| 145 | + chars = u'0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ-Ñ' |
| 146 | + if len(rfc) is 11: |
| 147 | + rfc = '-' + rfc |
| 148 | + |
| 149 | + sum_ = sum(i * chars.index(c) for i, c in zip(reversed(xrange(14)), rfc)) |
| 150 | + checksum = 11 - sum_ % 11 |
| 151 | + |
| 152 | + if checksum == 10: |
| 153 | + return u'A' |
| 154 | + elif checksum == 11: |
| 155 | + return u'0' |
| 156 | + |
| 157 | + return unicode(checksum) |
| 158 | + |
| 159 | + def _has_inconvenient_word(self, rfc): |
| 160 | + first_four = rfc[:4] |
| 161 | + return first_four in RFC_INCONVENIENT_WORDS |
| 162 | + |
| 163 | + |
| 164 | +class MXCURPField(RegexField): |
| 165 | + """ |
| 166 | + A field that validates a Mexican Clave Única de Registro de Población. |
| 167 | +
|
| 168 | + The CURP is integrated by a juxtaposition of characters following the next |
| 169 | + pattern: |
| 170 | +
|
| 171 | + ===== ====== =================================================== |
| 172 | + Index Format Accepted Characters |
| 173 | + ===== ====== =================================================== |
| 174 | + 1 X Any letter |
| 175 | + 2 X Any vowel |
| 176 | + 3-4 XX Any letter |
| 177 | + 5-10 YYMMDD Any valid date |
| 178 | + 11 X Either `H` or `M`, depending on the person's gender |
| 179 | + 12-13 XX Any valid acronym for a state in Mexico |
| 180 | + 14-16 XXX Any consonant |
| 181 | + 17 X Any number between 0 and 9 or any letter |
| 182 | + 18 X Any number between 0 and 9 |
| 183 | + ===== ====== =================================================== |
| 184 | +
|
| 185 | + More info about this: |
| 186 | + http://www.condusef.gob.mx/index.php/clave-unica-de-registro-de-poblacion-curp |
| 187 | + """ |
| 188 | + default_error_messages = { |
| 189 | + 'invalid': _('Enter a valid CURP.'), |
| 190 | + 'invalid_checksum': _(u'Invalid checksum for CURP.'), |
| 191 | + } |
| 192 | + |
| 193 | + def __init__(self, min_length=18, max_length=18, *args, **kwargs): |
| 194 | + states_re = r'(AS|BC|BS|CC|CL|CM|CS|CH|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)' |
| 195 | + consonants_re = r'[B-DF-HJ-NP-TV-Z]' |
| 196 | + curp_re = (ur'^[A-Z][AEIOU][A-Z]{2}%s[HM]%s%s{3}[0-9A-Z]\d$' % |
| 197 | + (DATE_RE, states_re, consonants_re)) |
| 198 | + curp_re = re.compile(curp_re, re.IGNORECASE) |
| 199 | + super(MXCURPField, self).__init__(curp_re, min_length=min_length, |
| 200 | + max_length=max_length, *args, **kwargs) |
| 201 | + |
| 202 | + def clean(self, value): |
| 203 | + value = super(MXCURPField, self).clean(value) |
| 204 | + if value in EMPTY_VALUES: |
| 205 | + return u'' |
| 206 | + value = value.upper() |
| 207 | + if value[-1] != self._checksum(value[:-1]): |
| 208 | + raise ValidationError(self.default_error_messages['invalid_checksum']) |
| 209 | + if self._has_inconvenient_word(value): |
| 210 | + raise ValidationError(self.default_error_messages['invalid']) |
| 211 | + return value |
| 212 | + |
| 213 | + def _checksum(self, value): |
| 214 | + chars = u'0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ' |
| 215 | + |
| 216 | + s = sum(i * chars.index(c) for i, c in zip(reversed(xrange(19)), value)) |
| 217 | + checksum = 10 - s % 10 |
| 218 | + |
| 219 | + if checksum == 10: |
| 220 | + return u'0' |
| 221 | + return unicode(checksum) |
| 222 | + |
| 223 | + def _has_inconvenient_word(self, curp): |
| 224 | + first_four = curp[:4] |
| 225 | + return first_four in CURP_INCONVENIENT_WORDS |
0 commit comments