catalogmx 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- catalogmx/__init__.py +56 -0
- catalogmx/catalogs/__init__.py +5 -0
- catalogmx/catalogs/banxico/__init__.py +24 -0
- catalogmx/catalogs/banxico/banks.py +136 -0
- catalogmx/catalogs/banxico/codigos_plaza.py +287 -0
- catalogmx/catalogs/banxico/instituciones_financieras.py +338 -0
- catalogmx/catalogs/banxico/monedas_divisas.py +386 -0
- catalogmx/catalogs/banxico/udis.py +279 -0
- catalogmx/catalogs/ift/__init__.py +15 -0
- catalogmx/catalogs/ift/codigos_lada.py +426 -0
- catalogmx/catalogs/ift/operadores_moviles.py +315 -0
- catalogmx/catalogs/inegi/__init__.py +21 -0
- catalogmx/catalogs/inegi/localidades.py +207 -0
- catalogmx/catalogs/inegi/municipios.py +73 -0
- catalogmx/catalogs/inegi/municipios_completo.py +236 -0
- catalogmx/catalogs/inegi/states.py +148 -0
- catalogmx/catalogs/mexico/__init__.py +17 -0
- catalogmx/catalogs/mexico/hoy_no_circula.py +215 -0
- catalogmx/catalogs/mexico/placas_formatos.py +184 -0
- catalogmx/catalogs/mexico/salarios_minimos.py +156 -0
- catalogmx/catalogs/mexico/uma.py +207 -0
- catalogmx/catalogs/sat/__init__.py +13 -0
- catalogmx/catalogs/sat/carta_porte/__init__.py +19 -0
- catalogmx/catalogs/sat/carta_porte/aeropuertos.py +76 -0
- catalogmx/catalogs/sat/carta_porte/carreteras.py +59 -0
- catalogmx/catalogs/sat/carta_porte/config_autotransporte.py +54 -0
- catalogmx/catalogs/sat/carta_porte/material_peligroso.py +66 -0
- catalogmx/catalogs/sat/carta_porte/puertos_maritimos.py +63 -0
- catalogmx/catalogs/sat/carta_porte/tipo_embalaje.py +48 -0
- catalogmx/catalogs/sat/carta_porte/tipo_permiso.py +54 -0
- catalogmx/catalogs/sat/cfdi_4/__init__.py +42 -0
- catalogmx/catalogs/sat/cfdi_4/clave_prod_serv.py +383 -0
- catalogmx/catalogs/sat/cfdi_4/clave_unidad.py +298 -0
- catalogmx/catalogs/sat/cfdi_4/exportacion.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/forma_pago.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/impuesto.py +57 -0
- catalogmx/catalogs/sat/cfdi_4/meses.py +34 -0
- catalogmx/catalogs/sat/cfdi_4/metodo_pago.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/objeto_imp.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/periodicidad.py +34 -0
- catalogmx/catalogs/sat/cfdi_4/regimen_fiscal.py +57 -0
- catalogmx/catalogs/sat/cfdi_4/tasa_o_cuota.py +42 -0
- catalogmx/catalogs/sat/cfdi_4/tipo_comprobante.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/tipo_factor.py +34 -0
- catalogmx/catalogs/sat/cfdi_4/tipo_relacion.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/uso_cfdi.py +45 -0
- catalogmx/catalogs/sat/comercio_exterior/__init__.py +39 -0
- catalogmx/catalogs/sat/comercio_exterior/claves_pedimento.py +77 -0
- catalogmx/catalogs/sat/comercio_exterior/estados.py +122 -0
- catalogmx/catalogs/sat/comercio_exterior/incoterms.py +226 -0
- catalogmx/catalogs/sat/comercio_exterior/monedas.py +107 -0
- catalogmx/catalogs/sat/comercio_exterior/motivos_traslado.py +54 -0
- catalogmx/catalogs/sat/comercio_exterior/paises.py +88 -0
- catalogmx/catalogs/sat/comercio_exterior/registro_ident_trib.py +76 -0
- catalogmx/catalogs/sat/comercio_exterior/unidades_aduana.py +54 -0
- catalogmx/catalogs/sat/comercio_exterior/validator.py +212 -0
- catalogmx/catalogs/sat/nomina/__init__.py +19 -0
- catalogmx/catalogs/sat/nomina/banco.py +50 -0
- catalogmx/catalogs/sat/nomina/periodicidad_pago.py +48 -0
- catalogmx/catalogs/sat/nomina/riesgo_puesto.py +56 -0
- catalogmx/catalogs/sat/nomina/tipo_contrato.py +47 -0
- catalogmx/catalogs/sat/nomina/tipo_jornada.py +42 -0
- catalogmx/catalogs/sat/nomina/tipo_nomina.py +52 -0
- catalogmx/catalogs/sat/nomina/tipo_regimen.py +47 -0
- catalogmx/catalogs/sepomex/__init__.py +5 -0
- catalogmx/catalogs/sepomex/codigos_postales.py +184 -0
- catalogmx/cli.py +185 -0
- catalogmx/helpers.py +324 -0
- catalogmx/utils/text.py +55 -0
- catalogmx/validators/__init__.py +0 -0
- catalogmx/validators/clabe.py +233 -0
- catalogmx/validators/curp.py +623 -0
- catalogmx/validators/nss.py +255 -0
- catalogmx/validators/rfc.py +1004 -0
- catalogmx-0.3.0.dist-info/METADATA +644 -0
- catalogmx-0.3.0.dist-info/RECORD +81 -0
- catalogmx-0.3.0.dist-info/WHEEL +5 -0
- catalogmx-0.3.0.dist-info/entry_points.txt +2 -0
- catalogmx-0.3.0.dist-info/licenses/AUTHORS.rst +5 -0
- catalogmx-0.3.0.dist-info/licenses/LICENSE +19 -0
- catalogmx-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import datetime
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import unidecode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CURPException(Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CURPLengthError(CURPException):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CURPStructureError(CURPException):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CURPGeneral:
|
|
21
|
+
"""
|
|
22
|
+
General Functions for CURP (Clave Única de Registro de Población)
|
|
23
|
+
|
|
24
|
+
CURP is an 18-character unique identifier for people in Mexico.
|
|
25
|
+
Format: AAAA-YYMMDD-H-EE-BBB-CC
|
|
26
|
+
Where:
|
|
27
|
+
AAAA: 4 letters from name (like RFC)
|
|
28
|
+
YYMMDD: Birth date
|
|
29
|
+
H: Gender (H=Male/Hombre, M=Female/Mujer)
|
|
30
|
+
EE: State code (2 letters)
|
|
31
|
+
BBB: Internal consonants from paterno, materno, nombre
|
|
32
|
+
CC: Homoclave (2 digits/letters)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
general_regex = re.compile(
|
|
36
|
+
r"[A-Z][AEIOUX][A-Z]{2}[0-9]{2}[0-1][0-9][0-3][0-9][MH][A-Z]{2}[BCDFGHJKLMNPQRSTVWXYZ]{3}[0-9A-Z]{2}"
|
|
37
|
+
)
|
|
38
|
+
length = 18
|
|
39
|
+
|
|
40
|
+
# Mexican state codes
|
|
41
|
+
state_codes = {
|
|
42
|
+
"AGUASCALIENTES": "AS",
|
|
43
|
+
"BAJA CALIFORNIA": "BC",
|
|
44
|
+
"BAJA CALIFORNIA SUR": "BS",
|
|
45
|
+
"CAMPECHE": "CC",
|
|
46
|
+
"COAHUILA": "CL",
|
|
47
|
+
"COLIMA": "CM",
|
|
48
|
+
"CHIAPAS": "CS",
|
|
49
|
+
"CHIHUAHUA": "CH",
|
|
50
|
+
"CIUDAD DE MEXICO": "DF", # Also accepts CDMX
|
|
51
|
+
"DISTRITO FEDERAL": "DF",
|
|
52
|
+
"CDMX": "DF",
|
|
53
|
+
"DURANGO": "DG",
|
|
54
|
+
"GUANAJUATO": "GT",
|
|
55
|
+
"GUERRERO": "GR",
|
|
56
|
+
"HIDALGO": "HG",
|
|
57
|
+
"JALISCO": "JC",
|
|
58
|
+
"ESTADO DE MEXICO": "MC",
|
|
59
|
+
"MEXICO": "MC",
|
|
60
|
+
"MICHOACAN": "MN",
|
|
61
|
+
"MORELOS": "MS",
|
|
62
|
+
"NAYARIT": "NT",
|
|
63
|
+
"NUEVO LEON": "NL",
|
|
64
|
+
"OAXACA": "OC",
|
|
65
|
+
"PUEBLA": "PL",
|
|
66
|
+
"QUERETARO": "QT",
|
|
67
|
+
"QUINTANA ROO": "QR",
|
|
68
|
+
"SAN LUIS POTOSI": "SP",
|
|
69
|
+
"SINALOA": "SL",
|
|
70
|
+
"SONORA": "SR",
|
|
71
|
+
"TABASCO": "TC",
|
|
72
|
+
"TAMAULIPAS": "TS",
|
|
73
|
+
"TLAXCALA": "TL",
|
|
74
|
+
"VERACRUZ": "VZ",
|
|
75
|
+
"YUCATAN": "YN",
|
|
76
|
+
"ZACATECAS": "ZS",
|
|
77
|
+
"NACIDO EN EL EXTRANJERO": "NE", # Born abroad
|
|
78
|
+
"EXTRANJERO": "NE",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
vocales = "AEIOU"
|
|
82
|
+
consonantes = "BCDFGHJKLMNPQRSTVWXYZ"
|
|
83
|
+
|
|
84
|
+
# Lista oficial completa de palabras inconvenientes según Anexo 2 del Instructivo Normativo CURP
|
|
85
|
+
# Cuando se detectan estas palabras en las primeras 4 letras, la segunda letra se sustituye con 'X'
|
|
86
|
+
cacophonic_words = [
|
|
87
|
+
"BACA",
|
|
88
|
+
"BAKA",
|
|
89
|
+
"BUEI",
|
|
90
|
+
"BUEY",
|
|
91
|
+
"CACA",
|
|
92
|
+
"CACO",
|
|
93
|
+
"CAGA",
|
|
94
|
+
"CAGO",
|
|
95
|
+
"CAKA",
|
|
96
|
+
"KAKO",
|
|
97
|
+
"COGE",
|
|
98
|
+
"COGI",
|
|
99
|
+
"COJA",
|
|
100
|
+
"COJE",
|
|
101
|
+
"COJI",
|
|
102
|
+
"COJO",
|
|
103
|
+
"COLA",
|
|
104
|
+
"CULO",
|
|
105
|
+
"FALO",
|
|
106
|
+
"FETO",
|
|
107
|
+
"GETA",
|
|
108
|
+
"GUEI",
|
|
109
|
+
"GUEY",
|
|
110
|
+
"JETA",
|
|
111
|
+
"JOTO",
|
|
112
|
+
"KACA",
|
|
113
|
+
"KACO",
|
|
114
|
+
"KAGA",
|
|
115
|
+
"KAGO",
|
|
116
|
+
"KAKA",
|
|
117
|
+
"KAKO",
|
|
118
|
+
"KOGE",
|
|
119
|
+
"KOGI",
|
|
120
|
+
"KOJA",
|
|
121
|
+
"KOJE",
|
|
122
|
+
"KOJI",
|
|
123
|
+
"KOJO",
|
|
124
|
+
"KOLA",
|
|
125
|
+
"KULO",
|
|
126
|
+
"LILO",
|
|
127
|
+
"LOCA",
|
|
128
|
+
"LOCO",
|
|
129
|
+
"LOKA",
|
|
130
|
+
"LOKO",
|
|
131
|
+
"MAME",
|
|
132
|
+
"MAMO",
|
|
133
|
+
"MEAR",
|
|
134
|
+
"MEAS",
|
|
135
|
+
"MEON",
|
|
136
|
+
"MIAR",
|
|
137
|
+
"MION",
|
|
138
|
+
"MOCO",
|
|
139
|
+
"MOKO",
|
|
140
|
+
"MULA",
|
|
141
|
+
"MULO",
|
|
142
|
+
"NACA",
|
|
143
|
+
"NACO",
|
|
144
|
+
"PEDA",
|
|
145
|
+
"PEDO",
|
|
146
|
+
"PENE",
|
|
147
|
+
"PIPI",
|
|
148
|
+
"PITO",
|
|
149
|
+
"POPO",
|
|
150
|
+
"PUTA",
|
|
151
|
+
"PUTO",
|
|
152
|
+
"QULO",
|
|
153
|
+
"RATA",
|
|
154
|
+
"ROBA",
|
|
155
|
+
"ROBE",
|
|
156
|
+
"ROBO",
|
|
157
|
+
"RUIN",
|
|
158
|
+
"SENO",
|
|
159
|
+
"TETA",
|
|
160
|
+
"VACA",
|
|
161
|
+
"VAGA",
|
|
162
|
+
"VAGO",
|
|
163
|
+
"VAKA",
|
|
164
|
+
"VUEI",
|
|
165
|
+
"VUEY",
|
|
166
|
+
"WUEI",
|
|
167
|
+
"WUEY",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
excluded_words = [
|
|
171
|
+
"DE",
|
|
172
|
+
"LA",
|
|
173
|
+
"LAS",
|
|
174
|
+
"MC",
|
|
175
|
+
"VON",
|
|
176
|
+
"DEL",
|
|
177
|
+
"LOS",
|
|
178
|
+
"Y",
|
|
179
|
+
"MAC",
|
|
180
|
+
"VAN",
|
|
181
|
+
"MI",
|
|
182
|
+
"DA",
|
|
183
|
+
"DAS",
|
|
184
|
+
"DE",
|
|
185
|
+
"DEL",
|
|
186
|
+
"DER",
|
|
187
|
+
"DI",
|
|
188
|
+
"DIE",
|
|
189
|
+
"DD",
|
|
190
|
+
"EL",
|
|
191
|
+
"LA",
|
|
192
|
+
"LOS",
|
|
193
|
+
"LAS",
|
|
194
|
+
"LE",
|
|
195
|
+
"LES",
|
|
196
|
+
"MAC",
|
|
197
|
+
"MC",
|
|
198
|
+
"VAN",
|
|
199
|
+
"VON",
|
|
200
|
+
"Y",
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
allowed_chars = list("ABCDEFGHIJKLMNÑOPQRSTUVWXYZ")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class CURPValidator(CURPGeneral):
|
|
207
|
+
"""
|
|
208
|
+
Validates a CURP (Clave Única de Registro de Población)
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __init__(self, curp: str | None) -> None:
|
|
212
|
+
"""
|
|
213
|
+
:param curp: The CURP code to be validated
|
|
214
|
+
"""
|
|
215
|
+
self.curp = ""
|
|
216
|
+
if bool(curp) and isinstance(curp, str):
|
|
217
|
+
self.curp = curp.upper().strip()
|
|
218
|
+
|
|
219
|
+
def validate(self) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
Validates the CURP structure
|
|
222
|
+
:return: True if valid, raises exception if invalid
|
|
223
|
+
"""
|
|
224
|
+
value = self.curp.strip()
|
|
225
|
+
if len(value) != self.length:
|
|
226
|
+
raise CURPLengthError("CURP length must be 18")
|
|
227
|
+
if self.general_regex.match(value):
|
|
228
|
+
return True
|
|
229
|
+
else:
|
|
230
|
+
raise CURPStructureError("Invalid CURP structure")
|
|
231
|
+
|
|
232
|
+
def is_valid(self) -> bool:
|
|
233
|
+
"""
|
|
234
|
+
Checks if CURP is valid without raising exceptions
|
|
235
|
+
:return: True if valid, False otherwise
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
return self.validate()
|
|
239
|
+
except CURPException:
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
def validate_check_digit(self) -> bool:
|
|
243
|
+
"""
|
|
244
|
+
Valida el dígito verificador (posición 18) del CURP
|
|
245
|
+
|
|
246
|
+
:return: True si el dígito verificador es correcto, False en caso contrario
|
|
247
|
+
"""
|
|
248
|
+
if len(self.curp) != 18:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
# Obtener los primeros 17 caracteres
|
|
252
|
+
curp_17 = self.curp[:17]
|
|
253
|
+
|
|
254
|
+
# Calcular el dígito verificador esperado
|
|
255
|
+
expected_digit = CURPGenerator.calculate_check_digit(curp_17)
|
|
256
|
+
|
|
257
|
+
# Comparar con el dígito actual
|
|
258
|
+
actual_digit = self.curp[17]
|
|
259
|
+
|
|
260
|
+
return expected_digit == actual_digit
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class CURPGeneratorUtils(CURPGeneral):
|
|
264
|
+
"""
|
|
265
|
+
Utility functions for CURP generation
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def clean_name(cls, nombre: str | None) -> str:
|
|
270
|
+
"""Clean name by removing excluded words and special characters"""
|
|
271
|
+
if not nombre:
|
|
272
|
+
return ""
|
|
273
|
+
result = (
|
|
274
|
+
"".join(
|
|
275
|
+
char if char in cls.allowed_chars else unidecode.unidecode(char)
|
|
276
|
+
for char in " ".join(
|
|
277
|
+
elem for elem in nombre.split(" ") if elem.upper() not in cls.excluded_words
|
|
278
|
+
)
|
|
279
|
+
.strip()
|
|
280
|
+
.upper()
|
|
281
|
+
)
|
|
282
|
+
.strip()
|
|
283
|
+
.upper()
|
|
284
|
+
)
|
|
285
|
+
return result
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def name_adapter(name: str | None, non_strict: bool = False) -> str:
|
|
289
|
+
"""Adapt name to uppercase and strip"""
|
|
290
|
+
if isinstance(name, str):
|
|
291
|
+
return name.upper().strip()
|
|
292
|
+
elif non_strict:
|
|
293
|
+
if name is None or not name:
|
|
294
|
+
return ""
|
|
295
|
+
else:
|
|
296
|
+
raise ValueError("Name must be a string")
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
def get_first_consonant(cls, word: str) -> str:
|
|
300
|
+
"""
|
|
301
|
+
Get the first internal consonant from a word
|
|
302
|
+
(the first consonant that is not the first letter)
|
|
303
|
+
"""
|
|
304
|
+
if not word or len(word) <= 1:
|
|
305
|
+
return "X"
|
|
306
|
+
|
|
307
|
+
for char in word[1:]:
|
|
308
|
+
if char in cls.consonantes:
|
|
309
|
+
return char
|
|
310
|
+
return "X"
|
|
311
|
+
|
|
312
|
+
@classmethod
|
|
313
|
+
def get_state_code(cls, state: str | None) -> str:
|
|
314
|
+
"""
|
|
315
|
+
Get the two-letter state code from state name
|
|
316
|
+
"""
|
|
317
|
+
if not state:
|
|
318
|
+
return "NE" # Born abroad default
|
|
319
|
+
|
|
320
|
+
state_upper = state.upper().strip()
|
|
321
|
+
|
|
322
|
+
# Try exact match first
|
|
323
|
+
if state_upper in cls.state_codes:
|
|
324
|
+
return cls.state_codes[state_upper]
|
|
325
|
+
|
|
326
|
+
# Clean the state name and try again
|
|
327
|
+
state_clean = cls.clean_name(state).upper()
|
|
328
|
+
if state_clean in cls.state_codes:
|
|
329
|
+
return cls.state_codes[state_clean]
|
|
330
|
+
|
|
331
|
+
# Try to find partial match
|
|
332
|
+
for state_name, code in cls.state_codes.items():
|
|
333
|
+
if state_name in state_upper or state_upper in state_name:
|
|
334
|
+
return code
|
|
335
|
+
|
|
336
|
+
# If it's already a 2-letter code, validate and return
|
|
337
|
+
if (
|
|
338
|
+
len(state_upper) == 2
|
|
339
|
+
and state_upper[0] in cls.allowed_chars
|
|
340
|
+
and state_upper[1] in cls.allowed_chars
|
|
341
|
+
):
|
|
342
|
+
return state_upper
|
|
343
|
+
|
|
344
|
+
return "NE" # Default to born abroad
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class CURPGenerator(CURPGeneratorUtils):
|
|
348
|
+
"""
|
|
349
|
+
CURP Generator for Mexican citizens and residents
|
|
350
|
+
|
|
351
|
+
Generates an 18-character CURP based on:
|
|
352
|
+
- Personal names (paterno, materno, nombre)
|
|
353
|
+
- Birth date
|
|
354
|
+
- Gender
|
|
355
|
+
- Birth state
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def __init__(
|
|
359
|
+
self,
|
|
360
|
+
nombre: str,
|
|
361
|
+
paterno: str,
|
|
362
|
+
materno: str | None,
|
|
363
|
+
fecha_nacimiento: datetime.date,
|
|
364
|
+
sexo: str,
|
|
365
|
+
estado: str | None,
|
|
366
|
+
) -> None:
|
|
367
|
+
"""
|
|
368
|
+
Initialize CURP Generator
|
|
369
|
+
|
|
370
|
+
:param nombre: First name(s)
|
|
371
|
+
:param paterno: First surname (apellido paterno)
|
|
372
|
+
:param materno: Second surname (apellido materno) - can be empty
|
|
373
|
+
:param fecha_nacimiento: Birth date (datetime.date object)
|
|
374
|
+
:param sexo: Gender - 'H' for male (Hombre), 'M' for female (Mujer)
|
|
375
|
+
:param estado: Birth state (Mexican state name or code)
|
|
376
|
+
"""
|
|
377
|
+
if not paterno or not paterno.strip():
|
|
378
|
+
raise ValueError("Apellido paterno is required")
|
|
379
|
+
if not nombre or not nombre.strip():
|
|
380
|
+
raise ValueError("Nombre is required")
|
|
381
|
+
if not isinstance(fecha_nacimiento, datetime.date):
|
|
382
|
+
raise ValueError("fecha_nacimiento must be a datetime.date object")
|
|
383
|
+
if sexo.upper() not in ("H", "M"):
|
|
384
|
+
raise ValueError('sexo must be "H" (Hombre) or "M" (Mujer)')
|
|
385
|
+
|
|
386
|
+
self.nombre = nombre
|
|
387
|
+
self.paterno = paterno
|
|
388
|
+
self.materno = materno if materno else ""
|
|
389
|
+
self.fecha_nacimiento = fecha_nacimiento
|
|
390
|
+
self.sexo = sexo.upper()
|
|
391
|
+
self.estado = estado
|
|
392
|
+
self._curp = ""
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def nombre(self) -> str:
|
|
396
|
+
return self._nombre
|
|
397
|
+
|
|
398
|
+
@nombre.setter
|
|
399
|
+
def nombre(self, value: str) -> None:
|
|
400
|
+
self._nombre = self.name_adapter(value)
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def paterno(self) -> str:
|
|
404
|
+
return self._paterno
|
|
405
|
+
|
|
406
|
+
@paterno.setter
|
|
407
|
+
def paterno(self, value: str) -> None:
|
|
408
|
+
self._paterno = self.name_adapter(value)
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def materno(self) -> str:
|
|
412
|
+
return self._materno
|
|
413
|
+
|
|
414
|
+
@materno.setter
|
|
415
|
+
def materno(self, value: str | None) -> None:
|
|
416
|
+
self._materno = self.name_adapter(value, non_strict=True)
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def nombre_calculo(self) -> str:
|
|
420
|
+
"""Get cleaned first name"""
|
|
421
|
+
return self.clean_name(self.nombre)
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def paterno_calculo(self) -> str:
|
|
425
|
+
"""Get cleaned first surname"""
|
|
426
|
+
return self.clean_name(self.paterno)
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def materno_calculo(self) -> str:
|
|
430
|
+
"""Get cleaned second surname"""
|
|
431
|
+
return self.clean_name(self.materno) if self.materno else ""
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def nombre_iniciales(self) -> str:
|
|
435
|
+
"""
|
|
436
|
+
Get the first name to use for initials
|
|
437
|
+
Skip common first names like JOSE and MARIA in compound names
|
|
438
|
+
"""
|
|
439
|
+
if not self.nombre_calculo:
|
|
440
|
+
return self.nombre_calculo
|
|
441
|
+
|
|
442
|
+
words = self.nombre_calculo.split()
|
|
443
|
+
if len(words) > 1:
|
|
444
|
+
if words[0] in ("MARIA", "JOSE", "MA", "MA.", "J", "J."):
|
|
445
|
+
return " ".join(words[1:])
|
|
446
|
+
return self.nombre_calculo
|
|
447
|
+
|
|
448
|
+
def generate_letters(self) -> str:
|
|
449
|
+
"""
|
|
450
|
+
Generate the first 4 letters of CURP
|
|
451
|
+
|
|
452
|
+
1. First letter of paterno
|
|
453
|
+
2. First vowel of paterno (after first letter)
|
|
454
|
+
3. First letter of materno (or X if none)
|
|
455
|
+
4. First letter of nombre
|
|
456
|
+
"""
|
|
457
|
+
clave = []
|
|
458
|
+
|
|
459
|
+
# First letter of paterno
|
|
460
|
+
paterno = self.paterno_calculo
|
|
461
|
+
if not paterno:
|
|
462
|
+
raise ValueError("Apellido paterno cannot be empty")
|
|
463
|
+
|
|
464
|
+
clave.append(paterno[0])
|
|
465
|
+
|
|
466
|
+
# First vowel of paterno (after first letter)
|
|
467
|
+
vowel_found = False
|
|
468
|
+
for char in paterno[1:]:
|
|
469
|
+
if char in self.vocales:
|
|
470
|
+
clave.append(char)
|
|
471
|
+
vowel_found = True
|
|
472
|
+
break
|
|
473
|
+
|
|
474
|
+
if not vowel_found:
|
|
475
|
+
clave.append("X")
|
|
476
|
+
|
|
477
|
+
# First letter of materno (or X if none)
|
|
478
|
+
materno = self.materno_calculo
|
|
479
|
+
if materno:
|
|
480
|
+
clave.append(materno[0])
|
|
481
|
+
else:
|
|
482
|
+
clave.append("X")
|
|
483
|
+
|
|
484
|
+
# First letter of nombre
|
|
485
|
+
nombre = self.nombre_iniciales
|
|
486
|
+
if not nombre:
|
|
487
|
+
raise ValueError("Nombre cannot be empty")
|
|
488
|
+
|
|
489
|
+
clave.append(nombre[0])
|
|
490
|
+
|
|
491
|
+
result = "".join(clave)
|
|
492
|
+
|
|
493
|
+
# Check for cacophonic words and replace second character (first vowel) with 'X'
|
|
494
|
+
# Según el Instructivo Normativo CURP, Anexo 2
|
|
495
|
+
if result in self.cacophonic_words:
|
|
496
|
+
result = result[0] + "X" + result[2:]
|
|
497
|
+
|
|
498
|
+
return result
|
|
499
|
+
|
|
500
|
+
def generate_date(self) -> str:
|
|
501
|
+
"""Generate date portion in YYMMDD format"""
|
|
502
|
+
return self.fecha_nacimiento.strftime("%y%m%d")
|
|
503
|
+
|
|
504
|
+
def generate_consonants(self) -> str:
|
|
505
|
+
"""
|
|
506
|
+
Generate the 3-consonant section
|
|
507
|
+
|
|
508
|
+
1. First internal consonant of paterno
|
|
509
|
+
2. First internal consonant of materno (or X if none)
|
|
510
|
+
3. First internal consonant of nombre
|
|
511
|
+
"""
|
|
512
|
+
consonants = []
|
|
513
|
+
|
|
514
|
+
# First internal consonant of paterno
|
|
515
|
+
paterno = self.paterno_calculo
|
|
516
|
+
consonants.append(self.get_first_consonant(paterno))
|
|
517
|
+
|
|
518
|
+
# First internal consonant of materno
|
|
519
|
+
materno = self.materno_calculo
|
|
520
|
+
if materno:
|
|
521
|
+
consonants.append(self.get_first_consonant(materno))
|
|
522
|
+
else:
|
|
523
|
+
consonants.append("X")
|
|
524
|
+
|
|
525
|
+
# First internal consonant of nombre
|
|
526
|
+
nombre = self.nombre_iniciales
|
|
527
|
+
consonants.append(self.get_first_consonant(nombre))
|
|
528
|
+
|
|
529
|
+
return "".join(consonants)
|
|
530
|
+
|
|
531
|
+
def generate_homoclave(self) -> str:
|
|
532
|
+
"""
|
|
533
|
+
Generate the 2-character homoclave (positions 17-18)
|
|
534
|
+
|
|
535
|
+
IMPORTANTE: Según el Instructivo Normativo oficial:
|
|
536
|
+
- Posición 17: Diferenciador de homonimia asignado ALEATORIAMENTE por RENAPO
|
|
537
|
+
(no es calculable algorítmicamente)
|
|
538
|
+
Para nacidos antes del 2000: números 0-9
|
|
539
|
+
Para nacidos después del 2000: letras A-Z o números 0-9
|
|
540
|
+
- Posición 18: Dígito verificador calculado mediante algoritmo oficial
|
|
541
|
+
|
|
542
|
+
Este método genera valores por defecto ya que la homoclave real solo puede
|
|
543
|
+
ser asignada oficialmente por RENAPO.
|
|
544
|
+
"""
|
|
545
|
+
# Posición 17: Diferenciador (asignado por RENAPO, usamos '0' por defecto)
|
|
546
|
+
if self.fecha_nacimiento.year < 2000:
|
|
547
|
+
differentiator = "0" # Para antes del 2000: 0-9
|
|
548
|
+
else:
|
|
549
|
+
differentiator = "A" # Para después del 2000: A-Z o 0-9
|
|
550
|
+
|
|
551
|
+
# Posición 18: Dígito verificador (calculable)
|
|
552
|
+
temp_curp = (
|
|
553
|
+
self.generate_letters()
|
|
554
|
+
+ self.generate_date()
|
|
555
|
+
+ self.sexo
|
|
556
|
+
+ self.get_state_code(self.estado)
|
|
557
|
+
+ self.generate_consonants()
|
|
558
|
+
+ differentiator
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
check_digit = self.calculate_check_digit(temp_curp)
|
|
562
|
+
|
|
563
|
+
return differentiator + check_digit
|
|
564
|
+
|
|
565
|
+
@staticmethod
|
|
566
|
+
def calculate_check_digit(curp_17: str) -> str:
|
|
567
|
+
"""
|
|
568
|
+
Calcula el dígito verificador (posición 18) según el algoritmo oficial RENAPO
|
|
569
|
+
|
|
570
|
+
Algoritmo:
|
|
571
|
+
1. Diccionario de valores: "0123456789ABCDEFGHIJKLMNÑOPQRSTUVWXYZ"
|
|
572
|
+
2. Para cada carácter de los primeros 17:
|
|
573
|
+
valor = índice_en_diccionario * (18 - posición)
|
|
574
|
+
3. Suma todos los valores
|
|
575
|
+
4. dígito = 10 - (suma % 10)
|
|
576
|
+
5. Si dígito == 10, entonces dígito = 0
|
|
577
|
+
|
|
578
|
+
:param curp_17: Los primeros 17 caracteres del CURP
|
|
579
|
+
:return: Dígito verificador (0-9)
|
|
580
|
+
"""
|
|
581
|
+
if len(curp_17) != 17:
|
|
582
|
+
raise ValueError(
|
|
583
|
+
"CURP debe tener exactamente 17 caracteres para calcular dígito verificador"
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Diccionario oficial de valores
|
|
587
|
+
dictionary = "0123456789ABCDEFGHIJKLMNÑOPQRSTUVWXYZ"
|
|
588
|
+
|
|
589
|
+
suma = 0
|
|
590
|
+
for i, char in enumerate(curp_17):
|
|
591
|
+
# Obtener el índice del carácter en el diccionario
|
|
592
|
+
try:
|
|
593
|
+
char_value = dictionary.index(char)
|
|
594
|
+
except ValueError:
|
|
595
|
+
# Si el carácter no está en el diccionario, usar 0
|
|
596
|
+
char_value = 0
|
|
597
|
+
|
|
598
|
+
# Multiplicar por (18 - posición)
|
|
599
|
+
suma += char_value * (18 - i)
|
|
600
|
+
|
|
601
|
+
# Calcular dígito verificador
|
|
602
|
+
digito = 10 - (suma % 10)
|
|
603
|
+
|
|
604
|
+
# Si es 10, retornar 0
|
|
605
|
+
if digito == 10:
|
|
606
|
+
digito = 0
|
|
607
|
+
|
|
608
|
+
return str(digito)
|
|
609
|
+
|
|
610
|
+
@property
|
|
611
|
+
def curp(self) -> str:
|
|
612
|
+
"""Generate and return the complete CURP"""
|
|
613
|
+
if not self._curp:
|
|
614
|
+
letters = self.generate_letters()
|
|
615
|
+
date = self.generate_date()
|
|
616
|
+
gender = self.sexo
|
|
617
|
+
state = self.get_state_code(self.estado)
|
|
618
|
+
consonants = self.generate_consonants()
|
|
619
|
+
homoclave = self.generate_homoclave()
|
|
620
|
+
|
|
621
|
+
self._curp = letters + date + gender + state + consonants + homoclave
|
|
622
|
+
|
|
623
|
+
return self._curp
|