catalogmx 0.3.0__py3-none-any.whl → 0.4.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 +133 -19
- catalogmx/calculators/__init__.py +113 -0
- catalogmx/calculators/costo_trabajador.py +213 -0
- catalogmx/calculators/impuestos.py +920 -0
- catalogmx/calculators/imss.py +370 -0
- catalogmx/calculators/isr.py +290 -0
- catalogmx/calculators/resico.py +154 -0
- catalogmx/catalogs/banxico/__init__.py +29 -3
- catalogmx/catalogs/banxico/cetes_sqlite.py +279 -0
- catalogmx/catalogs/banxico/inflacion_sqlite.py +302 -0
- catalogmx/catalogs/banxico/salarios_minimos_sqlite.py +295 -0
- catalogmx/catalogs/banxico/tiie_sqlite.py +279 -0
- catalogmx/catalogs/banxico/tipo_cambio_usd_sqlite.py +255 -0
- catalogmx/catalogs/banxico/udis_sqlite.py +332 -0
- catalogmx/catalogs/cnbv/__init__.py +9 -0
- catalogmx/catalogs/cnbv/sectores.py +173 -0
- catalogmx/catalogs/conapo/__init__.py +15 -0
- catalogmx/catalogs/conapo/sistema_urbano_nacional.py +50 -0
- catalogmx/catalogs/conapo/zonas_metropolitanas.py +230 -0
- catalogmx/catalogs/ift/__init__.py +1 -1
- catalogmx/catalogs/ift/codigos_lada.py +517 -313
- catalogmx/catalogs/inegi/__init__.py +17 -0
- catalogmx/catalogs/inegi/scian.py +127 -0
- catalogmx/catalogs/mexico/__init__.py +2 -0
- catalogmx/catalogs/mexico/giros_mercantiles.py +119 -0
- catalogmx/catalogs/sat/carta_porte/material_peligroso.py +5 -1
- catalogmx/catalogs/sat/cfdi_4/clave_prod_serv.py +78 -0
- catalogmx/catalogs/sat/cfdi_4/tasa_o_cuota.py +2 -1
- catalogmx/catalogs/sepomex/__init__.py +2 -1
- catalogmx/catalogs/sepomex/codigos_postales.py +30 -2
- catalogmx/catalogs/sepomex/codigos_postales_completo.py +261 -0
- catalogmx/cli.py +12 -9
- catalogmx/data/__init__.py +10 -0
- catalogmx/data/mexico_dynamic.sqlite3 +0 -0
- catalogmx/data/updater.py +362 -0
- catalogmx/generators/__init__.py +20 -0
- catalogmx/generators/identity.py +582 -0
- catalogmx/helpers.py +177 -3
- catalogmx/utils/__init__.py +29 -0
- catalogmx/utils/clabe_utils.py +417 -0
- catalogmx/utils/text.py +7 -1
- catalogmx/validators/clabe.py +52 -2
- catalogmx/validators/nss.py +32 -27
- catalogmx/validators/rfc.py +185 -52
- catalogmx-0.4.0.dist-info/METADATA +905 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/RECORD +51 -25
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/WHEEL +1 -1
- catalogmx/catalogs/banxico/udis.py +0 -279
- catalogmx-0.3.0.dist-info/METADATA +0 -644
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/entry_points.txt +0 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mexican Identity Generator
|
|
3
|
+
|
|
4
|
+
Generates complete Mexican identities with realistic data for testing.
|
|
5
|
+
Uses Faker for random data generation and catalogmx validators for
|
|
6
|
+
RFC, CURP, CLABE, and NSS generation.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from catalogmx.generators import generate_identity
|
|
10
|
+
|
|
11
|
+
# Generate a random persona física
|
|
12
|
+
identity = generate_identity()
|
|
13
|
+
|
|
14
|
+
# Generate with specific parameters
|
|
15
|
+
identity = generate_identity(
|
|
16
|
+
sexo='M',
|
|
17
|
+
estado='Jalisco',
|
|
18
|
+
min_age=25,
|
|
19
|
+
max_age=45
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
print(identity['nombre_completo'])
|
|
23
|
+
print(identity['rfc'])
|
|
24
|
+
print(identity['curp'])
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import random
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from datetime import date, timedelta
|
|
32
|
+
from typing import Literal
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from faker import Faker
|
|
36
|
+
except ImportError:
|
|
37
|
+
Faker = None # type: ignore
|
|
38
|
+
|
|
39
|
+
from catalogmx.helpers import (
|
|
40
|
+
generate_clabe,
|
|
41
|
+
generate_curp,
|
|
42
|
+
generate_rfc_persona_fisica,
|
|
43
|
+
generate_rfc_persona_moral,
|
|
44
|
+
)
|
|
45
|
+
from catalogmx.validators.nss import generate_nss
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_faker() -> Faker:
|
|
49
|
+
"""Get a Faker instance with Mexican locale."""
|
|
50
|
+
if Faker is None:
|
|
51
|
+
raise ImportError(
|
|
52
|
+
"Faker is required for identity generation. " "Install it with: pip install faker"
|
|
53
|
+
)
|
|
54
|
+
return Faker("es_MX")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Mexican states with CURP codes
|
|
58
|
+
ESTADOS_MEXICO = [
|
|
59
|
+
{"name": "AGUASCALIENTES", "code": "AS", "inegi": "01"},
|
|
60
|
+
{"name": "BAJA CALIFORNIA", "code": "BC", "inegi": "02"},
|
|
61
|
+
{"name": "BAJA CALIFORNIA SUR", "code": "BS", "inegi": "03"},
|
|
62
|
+
{"name": "CAMPECHE", "code": "CC", "inegi": "04"},
|
|
63
|
+
{"name": "CHIAPAS", "code": "CS", "inegi": "07"},
|
|
64
|
+
{"name": "CHIHUAHUA", "code": "CH", "inegi": "08"},
|
|
65
|
+
{"name": "CIUDAD DE MEXICO", "code": "DF", "inegi": "09"},
|
|
66
|
+
{"name": "COAHUILA", "code": "CL", "inegi": "05"},
|
|
67
|
+
{"name": "COLIMA", "code": "CM", "inegi": "06"},
|
|
68
|
+
{"name": "DURANGO", "code": "DG", "inegi": "10"},
|
|
69
|
+
{"name": "ESTADO DE MEXICO", "code": "MC", "inegi": "15"},
|
|
70
|
+
{"name": "GUANAJUATO", "code": "GT", "inegi": "11"},
|
|
71
|
+
{"name": "GUERRERO", "code": "GR", "inegi": "12"},
|
|
72
|
+
{"name": "HIDALGO", "code": "HG", "inegi": "13"},
|
|
73
|
+
{"name": "JALISCO", "code": "JC", "inegi": "14"},
|
|
74
|
+
{"name": "MICHOACAN", "code": "MN", "inegi": "16"},
|
|
75
|
+
{"name": "MORELOS", "code": "MS", "inegi": "17"},
|
|
76
|
+
{"name": "NAYARIT", "code": "NT", "inegi": "18"},
|
|
77
|
+
{"name": "NUEVO LEON", "code": "NL", "inegi": "19"},
|
|
78
|
+
{"name": "OAXACA", "code": "OC", "inegi": "20"},
|
|
79
|
+
{"name": "PUEBLA", "code": "PL", "inegi": "21"},
|
|
80
|
+
{"name": "QUERETARO", "code": "QT", "inegi": "22"},
|
|
81
|
+
{"name": "QUINTANA ROO", "code": "QR", "inegi": "23"},
|
|
82
|
+
{"name": "SAN LUIS POTOSI", "code": "SP", "inegi": "24"},
|
|
83
|
+
{"name": "SINALOA", "code": "SL", "inegi": "25"},
|
|
84
|
+
{"name": "SONORA", "code": "SR", "inegi": "26"},
|
|
85
|
+
{"name": "TABASCO", "code": "TC", "inegi": "27"},
|
|
86
|
+
{"name": "TAMAULIPAS", "code": "TS", "inegi": "28"},
|
|
87
|
+
{"name": "TLAXCALA", "code": "TL", "inegi": "29"},
|
|
88
|
+
{"name": "VERACRUZ", "code": "VZ", "inegi": "30"},
|
|
89
|
+
{"name": "YUCATAN", "code": "YN", "inegi": "31"},
|
|
90
|
+
{"name": "ZACATECAS", "code": "ZS", "inegi": "32"},
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# Common Mexican banks with CLABE codes
|
|
94
|
+
BANCOS_MEXICO = [
|
|
95
|
+
{"code": "002", "name": "BANAMEX"},
|
|
96
|
+
{"code": "012", "name": "BBVA MEXICO"},
|
|
97
|
+
{"code": "014", "name": "SANTANDER"},
|
|
98
|
+
{"code": "021", "name": "HSBC"},
|
|
99
|
+
{"code": "030", "name": "BAJIO"},
|
|
100
|
+
{"code": "036", "name": "INBURSA"},
|
|
101
|
+
{"code": "044", "name": "SCOTIABANK"},
|
|
102
|
+
{"code": "058", "name": "BANREGIO"},
|
|
103
|
+
{"code": "072", "name": "BANORTE"},
|
|
104
|
+
{"code": "127", "name": "AZTECA"},
|
|
105
|
+
{"code": "130", "name": "COMPARTAMOS"},
|
|
106
|
+
{"code": "137", "name": "BANCOPPEL"},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
# Regímenes fiscales for persona física
|
|
110
|
+
REGIMENES_PERSONA_FISICA = [
|
|
111
|
+
{"code": "605", "name": "Sueldos y Salarios e Ingresos Asimilados a Salarios"},
|
|
112
|
+
{"code": "606", "name": "Arrendamiento"},
|
|
113
|
+
{"code": "612", "name": "Personas Físicas con Actividades Empresariales y Profesionales"},
|
|
114
|
+
{"code": "621", "name": "Incorporación Fiscal"},
|
|
115
|
+
{
|
|
116
|
+
"code": "625",
|
|
117
|
+
"name": "Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas",
|
|
118
|
+
},
|
|
119
|
+
{"code": "626", "name": "Régimen Simplificado de Confianza"},
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class PersonaFisica:
|
|
125
|
+
"""Represents a Mexican natural person (persona física)."""
|
|
126
|
+
|
|
127
|
+
nombre: str
|
|
128
|
+
apellido_paterno: str
|
|
129
|
+
apellido_materno: str
|
|
130
|
+
fecha_nacimiento: date
|
|
131
|
+
sexo: Literal["H", "M"]
|
|
132
|
+
estado_nacimiento: str
|
|
133
|
+
estado_nacimiento_code: str
|
|
134
|
+
|
|
135
|
+
# Calculated identifiers
|
|
136
|
+
rfc: str = ""
|
|
137
|
+
curp: str = ""
|
|
138
|
+
nss: str = ""
|
|
139
|
+
|
|
140
|
+
# Banking
|
|
141
|
+
banco: dict = field(default_factory=dict)
|
|
142
|
+
clabe: str = ""
|
|
143
|
+
cuenta: str = ""
|
|
144
|
+
tarjeta_debito: str = ""
|
|
145
|
+
|
|
146
|
+
# Address
|
|
147
|
+
calle: str = ""
|
|
148
|
+
numero_exterior: str = ""
|
|
149
|
+
numero_interior: str = ""
|
|
150
|
+
colonia: str = ""
|
|
151
|
+
codigo_postal: str = ""
|
|
152
|
+
municipio: str = ""
|
|
153
|
+
estado: str = ""
|
|
154
|
+
|
|
155
|
+
# Contact
|
|
156
|
+
telefono: str = ""
|
|
157
|
+
email: str = ""
|
|
158
|
+
|
|
159
|
+
# Fiscal
|
|
160
|
+
regimen_fiscal: dict = field(default_factory=dict)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def nombre_completo(self) -> str:
|
|
164
|
+
"""Full name."""
|
|
165
|
+
return f"{self.nombre} {self.apellido_paterno} {self.apellido_materno}".strip()
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def edad(self) -> int:
|
|
169
|
+
"""Age in years."""
|
|
170
|
+
today = date.today()
|
|
171
|
+
return (
|
|
172
|
+
today.year
|
|
173
|
+
- self.fecha_nacimiento.year
|
|
174
|
+
- ((today.month, today.day) < (self.fecha_nacimiento.month, self.fecha_nacimiento.day))
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> dict:
|
|
178
|
+
"""Convert to dictionary."""
|
|
179
|
+
return {
|
|
180
|
+
"tipo": "persona_fisica",
|
|
181
|
+
"nombre": self.nombre,
|
|
182
|
+
"apellido_paterno": self.apellido_paterno,
|
|
183
|
+
"apellido_materno": self.apellido_materno,
|
|
184
|
+
"nombre_completo": self.nombre_completo,
|
|
185
|
+
"fecha_nacimiento": self.fecha_nacimiento.isoformat(),
|
|
186
|
+
"edad": self.edad,
|
|
187
|
+
"sexo": self.sexo,
|
|
188
|
+
"sexo_descripcion": "Masculino" if self.sexo == "H" else "Femenino",
|
|
189
|
+
"estado_nacimiento": self.estado_nacimiento,
|
|
190
|
+
"estado_nacimiento_code": self.estado_nacimiento_code,
|
|
191
|
+
"rfc": self.rfc,
|
|
192
|
+
"curp": self.curp,
|
|
193
|
+
"nss": self.nss,
|
|
194
|
+
"banco": self.banco,
|
|
195
|
+
"clabe": self.clabe,
|
|
196
|
+
"cuenta": self.cuenta,
|
|
197
|
+
"tarjeta_debito": self.tarjeta_debito,
|
|
198
|
+
"direccion": {
|
|
199
|
+
"calle": self.calle,
|
|
200
|
+
"numero_exterior": self.numero_exterior,
|
|
201
|
+
"numero_interior": self.numero_interior,
|
|
202
|
+
"colonia": self.colonia,
|
|
203
|
+
"codigo_postal": self.codigo_postal,
|
|
204
|
+
"municipio": self.municipio,
|
|
205
|
+
"estado": self.estado,
|
|
206
|
+
},
|
|
207
|
+
"telefono": self.telefono,
|
|
208
|
+
"email": self.email,
|
|
209
|
+
"regimen_fiscal": self.regimen_fiscal,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class PersonaMoral:
|
|
215
|
+
"""Represents a Mexican legal entity (persona moral)."""
|
|
216
|
+
|
|
217
|
+
razon_social: str
|
|
218
|
+
fecha_constitucion: date
|
|
219
|
+
|
|
220
|
+
# Calculated identifiers
|
|
221
|
+
rfc: str = ""
|
|
222
|
+
|
|
223
|
+
# Banking
|
|
224
|
+
banco: dict = field(default_factory=dict)
|
|
225
|
+
clabe: str = ""
|
|
226
|
+
cuenta: str = ""
|
|
227
|
+
|
|
228
|
+
# Address
|
|
229
|
+
calle: str = ""
|
|
230
|
+
numero_exterior: str = ""
|
|
231
|
+
numero_interior: str = ""
|
|
232
|
+
colonia: str = ""
|
|
233
|
+
codigo_postal: str = ""
|
|
234
|
+
municipio: str = ""
|
|
235
|
+
estado: str = ""
|
|
236
|
+
|
|
237
|
+
# Contact
|
|
238
|
+
telefono: str = ""
|
|
239
|
+
email: str = ""
|
|
240
|
+
|
|
241
|
+
# Fiscal
|
|
242
|
+
regimen_fiscal: dict = field(default_factory=dict)
|
|
243
|
+
|
|
244
|
+
def to_dict(self) -> dict:
|
|
245
|
+
"""Convert to dictionary."""
|
|
246
|
+
return {
|
|
247
|
+
"tipo": "persona_moral",
|
|
248
|
+
"razon_social": self.razon_social,
|
|
249
|
+
"fecha_constitucion": self.fecha_constitucion.isoformat(),
|
|
250
|
+
"rfc": self.rfc,
|
|
251
|
+
"banco": self.banco,
|
|
252
|
+
"clabe": self.clabe,
|
|
253
|
+
"cuenta": self.cuenta,
|
|
254
|
+
"direccion": {
|
|
255
|
+
"calle": self.calle,
|
|
256
|
+
"numero_exterior": self.numero_exterior,
|
|
257
|
+
"numero_interior": self.numero_interior,
|
|
258
|
+
"colonia": self.colonia,
|
|
259
|
+
"codigo_postal": self.codigo_postal,
|
|
260
|
+
"municipio": self.municipio,
|
|
261
|
+
"estado": self.estado,
|
|
262
|
+
},
|
|
263
|
+
"telefono": self.telefono,
|
|
264
|
+
"email": self.email,
|
|
265
|
+
"regimen_fiscal": self.regimen_fiscal,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class IdentityGenerator:
|
|
270
|
+
"""
|
|
271
|
+
Generator for Mexican identities.
|
|
272
|
+
|
|
273
|
+
Uses Faker for random data and catalogmx for RFC, CURP, CLABE, NSS generation.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def __init__(self, seed: int | None = None):
|
|
277
|
+
"""
|
|
278
|
+
Initialize the generator.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
seed: Optional random seed for reproducible results
|
|
282
|
+
"""
|
|
283
|
+
self.faker = _get_faker()
|
|
284
|
+
if seed is not None:
|
|
285
|
+
self.faker.seed_instance(seed)
|
|
286
|
+
random.seed(seed)
|
|
287
|
+
|
|
288
|
+
def generate_persona_fisica(
|
|
289
|
+
self,
|
|
290
|
+
sexo: Literal["H", "M"] | None = None,
|
|
291
|
+
estado: str | None = None,
|
|
292
|
+
min_age: int = 18,
|
|
293
|
+
max_age: int = 65,
|
|
294
|
+
include_banking: bool = True,
|
|
295
|
+
include_address: bool = True,
|
|
296
|
+
include_contact: bool = True,
|
|
297
|
+
) -> PersonaFisica:
|
|
298
|
+
"""
|
|
299
|
+
Generate a random Mexican persona física.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
sexo: Gender ('H' for male, 'M' for female), random if None
|
|
303
|
+
estado: Birth state name, random if None
|
|
304
|
+
min_age: Minimum age (default 18)
|
|
305
|
+
max_age: Maximum age (default 65)
|
|
306
|
+
include_banking: Include banking details
|
|
307
|
+
include_address: Include address
|
|
308
|
+
include_contact: Include contact info
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
PersonaFisica with all generated data
|
|
312
|
+
"""
|
|
313
|
+
# Random sex if not specified
|
|
314
|
+
if sexo is None:
|
|
315
|
+
sexo = random.choice(["H", "M"])
|
|
316
|
+
|
|
317
|
+
# Generate name based on sex
|
|
318
|
+
if sexo == "H":
|
|
319
|
+
nombre = self.faker.first_name_male()
|
|
320
|
+
else:
|
|
321
|
+
nombre = self.faker.first_name_female()
|
|
322
|
+
|
|
323
|
+
apellido_paterno = self.faker.last_name()
|
|
324
|
+
apellido_materno = self.faker.last_name()
|
|
325
|
+
|
|
326
|
+
# Random birth date within age range
|
|
327
|
+
today = date.today()
|
|
328
|
+
min_birth = today - timedelta(days=max_age * 365)
|
|
329
|
+
max_birth = today - timedelta(days=min_age * 365)
|
|
330
|
+
days_range = (max_birth - min_birth).days
|
|
331
|
+
fecha_nacimiento = min_birth + timedelta(days=random.randint(0, days_range))
|
|
332
|
+
|
|
333
|
+
# Random state if not specified
|
|
334
|
+
if estado:
|
|
335
|
+
estado_info = next(
|
|
336
|
+
(e for e in ESTADOS_MEXICO if e["name"].upper() == estado.upper()),
|
|
337
|
+
random.choice(ESTADOS_MEXICO),
|
|
338
|
+
)
|
|
339
|
+
else:
|
|
340
|
+
estado_info = random.choice(ESTADOS_MEXICO)
|
|
341
|
+
|
|
342
|
+
estado_nacimiento = estado_info["name"]
|
|
343
|
+
estado_nacimiento_code = estado_info["code"]
|
|
344
|
+
|
|
345
|
+
# Generate RFC
|
|
346
|
+
rfc = generate_rfc_persona_fisica(
|
|
347
|
+
nombre=nombre,
|
|
348
|
+
apellido_paterno=apellido_paterno,
|
|
349
|
+
apellido_materno=apellido_materno,
|
|
350
|
+
fecha_nacimiento=fecha_nacimiento,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Generate CURP
|
|
354
|
+
curp = generate_curp(
|
|
355
|
+
nombre=nombre,
|
|
356
|
+
apellido_paterno=apellido_paterno,
|
|
357
|
+
apellido_materno=apellido_materno,
|
|
358
|
+
fecha_nacimiento=fecha_nacimiento,
|
|
359
|
+
sexo=sexo,
|
|
360
|
+
estado=estado_nacimiento,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Generate NSS
|
|
364
|
+
subdelegation = random.randint(1, 97)
|
|
365
|
+
registration_year = random.randint(0, 99)
|
|
366
|
+
birth_year = fecha_nacimiento.year % 100
|
|
367
|
+
sequential = random.randint(0, 9999)
|
|
368
|
+
nss = generate_nss(subdelegation, registration_year, birth_year, sequential)
|
|
369
|
+
|
|
370
|
+
persona = PersonaFisica(
|
|
371
|
+
nombre=nombre,
|
|
372
|
+
apellido_paterno=apellido_paterno,
|
|
373
|
+
apellido_materno=apellido_materno,
|
|
374
|
+
fecha_nacimiento=fecha_nacimiento,
|
|
375
|
+
sexo=sexo,
|
|
376
|
+
estado_nacimiento=estado_nacimiento,
|
|
377
|
+
estado_nacimiento_code=estado_nacimiento_code,
|
|
378
|
+
rfc=rfc,
|
|
379
|
+
curp=curp,
|
|
380
|
+
nss=nss,
|
|
381
|
+
regimen_fiscal=random.choice(REGIMENES_PERSONA_FISICA),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if include_banking:
|
|
385
|
+
self._add_banking(persona)
|
|
386
|
+
|
|
387
|
+
if include_address:
|
|
388
|
+
self._add_address(persona)
|
|
389
|
+
|
|
390
|
+
if include_contact:
|
|
391
|
+
self._add_contact(persona, nombre, apellido_paterno)
|
|
392
|
+
|
|
393
|
+
return persona
|
|
394
|
+
|
|
395
|
+
def generate_persona_moral(
|
|
396
|
+
self,
|
|
397
|
+
include_banking: bool = True,
|
|
398
|
+
include_address: bool = True,
|
|
399
|
+
include_contact: bool = True,
|
|
400
|
+
) -> PersonaMoral:
|
|
401
|
+
"""
|
|
402
|
+
Generate a random Mexican persona moral (company).
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
include_banking: Include banking details
|
|
406
|
+
include_address: Include address
|
|
407
|
+
include_contact: Include contact info
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
PersonaMoral with all generated data
|
|
411
|
+
"""
|
|
412
|
+
# Generate company name
|
|
413
|
+
company_types = ["S.A. de C.V.", "S.A.P.I. de C.V.", "S. de R.L. de C.V.", "S.C."]
|
|
414
|
+
company_name = self.faker.company()
|
|
415
|
+
razon_social = f"{company_name} {random.choice(company_types)}"
|
|
416
|
+
|
|
417
|
+
# Random constitution date (1-50 years ago)
|
|
418
|
+
today = date.today()
|
|
419
|
+
min_date = today - timedelta(days=50 * 365)
|
|
420
|
+
max_date = today - timedelta(days=365)
|
|
421
|
+
days_range = (max_date - min_date).days
|
|
422
|
+
fecha_constitucion = min_date + timedelta(days=random.randint(0, days_range))
|
|
423
|
+
|
|
424
|
+
# Generate RFC
|
|
425
|
+
rfc = generate_rfc_persona_moral(
|
|
426
|
+
razon_social=razon_social,
|
|
427
|
+
fecha_constitucion=fecha_constitucion,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
persona = PersonaMoral(
|
|
431
|
+
razon_social=razon_social,
|
|
432
|
+
fecha_constitucion=fecha_constitucion,
|
|
433
|
+
rfc=rfc,
|
|
434
|
+
regimen_fiscal={"code": "601", "name": "General de Ley Personas Morales"},
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if include_banking:
|
|
438
|
+
self._add_banking(persona)
|
|
439
|
+
|
|
440
|
+
if include_address:
|
|
441
|
+
self._add_address(persona)
|
|
442
|
+
|
|
443
|
+
if include_contact:
|
|
444
|
+
# Use company name for email domain
|
|
445
|
+
domain = company_name.lower().replace(" ", "").replace(",", "")[:15]
|
|
446
|
+
persona.email = f"contacto@{domain}.com.mx"
|
|
447
|
+
persona.telefono = self._generate_phone()
|
|
448
|
+
|
|
449
|
+
return persona
|
|
450
|
+
|
|
451
|
+
def _add_banking(self, persona: PersonaFisica | PersonaMoral) -> None:
|
|
452
|
+
"""Add banking information to persona."""
|
|
453
|
+
banco = random.choice(BANCOS_MEXICO)
|
|
454
|
+
persona.banco = banco
|
|
455
|
+
|
|
456
|
+
# Generate CLABE
|
|
457
|
+
branch = str(random.randint(1, 999)).zfill(3)
|
|
458
|
+
account = str(random.randint(0, 99999999999)).zfill(11)
|
|
459
|
+
persona.clabe = generate_clabe(banco["code"], branch, account)
|
|
460
|
+
persona.cuenta = account
|
|
461
|
+
|
|
462
|
+
# Generate debit card (Visa or Mastercard)
|
|
463
|
+
card_type = random.choice(["visa", "mastercard"])
|
|
464
|
+
if card_type == "visa":
|
|
465
|
+
prefix = "4"
|
|
466
|
+
persona.tarjeta_debito = prefix + "".join(str(random.randint(0, 9)) for _ in range(15))
|
|
467
|
+
else:
|
|
468
|
+
prefix = "5" + str(random.randint(1, 5))
|
|
469
|
+
persona.tarjeta_debito = prefix + "".join(str(random.randint(0, 9)) for _ in range(14))
|
|
470
|
+
|
|
471
|
+
# Add Luhn check digit
|
|
472
|
+
persona.tarjeta_debito = self._add_luhn_checksum(persona.tarjeta_debito[:-1])
|
|
473
|
+
|
|
474
|
+
def _add_luhn_checksum(self, card_number: str) -> str:
|
|
475
|
+
"""Calculate and append Luhn checksum digit."""
|
|
476
|
+
digits = [int(d) for d in card_number]
|
|
477
|
+
odd_digits = digits[-1::-2]
|
|
478
|
+
even_digits = digits[-2::-2]
|
|
479
|
+
|
|
480
|
+
total = sum(odd_digits)
|
|
481
|
+
for d in even_digits:
|
|
482
|
+
total += sum(divmod(d * 2, 10))
|
|
483
|
+
|
|
484
|
+
check_digit = (10 - (total % 10)) % 10
|
|
485
|
+
return card_number + str(check_digit)
|
|
486
|
+
|
|
487
|
+
def _add_address(self, persona: PersonaFisica | PersonaMoral) -> None:
|
|
488
|
+
"""Add address information to persona."""
|
|
489
|
+
persona.calle = self.faker.street_name()
|
|
490
|
+
persona.numero_exterior = str(random.randint(1, 9999))
|
|
491
|
+
persona.numero_interior = "" if random.random() > 0.3 else str(random.randint(1, 50))
|
|
492
|
+
persona.colonia = self.faker.city_suffix() + " " + self.faker.last_name()
|
|
493
|
+
persona.codigo_postal = str(random.randint(1000, 99999)).zfill(5)
|
|
494
|
+
persona.municipio = self.faker.city()
|
|
495
|
+
persona.estado = random.choice(ESTADOS_MEXICO)["name"].title()
|
|
496
|
+
|
|
497
|
+
def _add_contact(self, persona: PersonaFisica, nombre: str, apellido: str) -> None:
|
|
498
|
+
"""Add contact information to persona física."""
|
|
499
|
+
persona.telefono = self._generate_phone()
|
|
500
|
+
|
|
501
|
+
# Generate email
|
|
502
|
+
email_styles = [
|
|
503
|
+
f"{nombre.lower()}_{apellido.lower()}",
|
|
504
|
+
f"{nombre.lower()}.{apellido.lower()}",
|
|
505
|
+
f"{nombre.lower()}{random.randint(1, 99)}",
|
|
506
|
+
f"{apellido.lower()}.{nombre.lower()}",
|
|
507
|
+
]
|
|
508
|
+
email_domains = ["gmail.com", "hotmail.com", "outlook.com", "yahoo.com.mx", "proton.me"]
|
|
509
|
+
|
|
510
|
+
email_user = random.choice(email_styles)
|
|
511
|
+
# Normalize email (remove accents)
|
|
512
|
+
import unicodedata
|
|
513
|
+
|
|
514
|
+
email_user = unicodedata.normalize("NFD", email_user)
|
|
515
|
+
email_user = "".join(c for c in email_user if unicodedata.category(c) != "Mn")
|
|
516
|
+
|
|
517
|
+
persona.email = f"{email_user}@{random.choice(email_domains)}"
|
|
518
|
+
|
|
519
|
+
def _generate_phone(self) -> str:
|
|
520
|
+
"""Generate a Mexican phone number (10 digits)."""
|
|
521
|
+
# Mexican area codes (LADA)
|
|
522
|
+
ladas = ["55", "33", "81", "222", "442", "477", "614", "656", "664", "744", "998"]
|
|
523
|
+
lada = random.choice(ladas)
|
|
524
|
+
|
|
525
|
+
remaining = 10 - len(lada)
|
|
526
|
+
number = "".join(str(random.randint(0, 9)) for _ in range(remaining))
|
|
527
|
+
|
|
528
|
+
return lada + number
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def generate_identity(
|
|
532
|
+
tipo: Literal["fisica", "moral"] = "fisica",
|
|
533
|
+
**kwargs,
|
|
534
|
+
) -> dict:
|
|
535
|
+
"""
|
|
536
|
+
Generate a complete Mexican identity.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
tipo: Type of identity ('fisica' or 'moral')
|
|
540
|
+
**kwargs: Additional arguments passed to generator
|
|
541
|
+
For persona física:
|
|
542
|
+
- sexo: 'H' or 'M'
|
|
543
|
+
- estado: Birth state name
|
|
544
|
+
- min_age: Minimum age (default 18)
|
|
545
|
+
- max_age: Maximum age (default 65)
|
|
546
|
+
For persona moral:
|
|
547
|
+
- No additional options
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Dictionary with complete identity data
|
|
551
|
+
|
|
552
|
+
Example:
|
|
553
|
+
>>> identity = generate_identity()
|
|
554
|
+
>>> print(identity['nombre_completo'])
|
|
555
|
+
'Juan Pérez García'
|
|
556
|
+
>>> print(identity['rfc'])
|
|
557
|
+
'PEGJ900515XXX'
|
|
558
|
+
"""
|
|
559
|
+
generator = IdentityGenerator()
|
|
560
|
+
|
|
561
|
+
if tipo == "moral":
|
|
562
|
+
return generator.generate_persona_moral(**kwargs).to_dict()
|
|
563
|
+
else:
|
|
564
|
+
return generator.generate_persona_fisica(**kwargs).to_dict()
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def generate_persona_fisica(**kwargs) -> dict:
|
|
568
|
+
"""
|
|
569
|
+
Generate a Mexican persona física identity.
|
|
570
|
+
|
|
571
|
+
Shortcut for generate_identity(tipo='fisica', **kwargs)
|
|
572
|
+
"""
|
|
573
|
+
return generate_identity(tipo="fisica", **kwargs)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def generate_persona_moral(**kwargs) -> dict:
|
|
577
|
+
"""
|
|
578
|
+
Generate a Mexican persona moral identity.
|
|
579
|
+
|
|
580
|
+
Shortcut for generate_identity(tipo='moral', **kwargs)
|
|
581
|
+
"""
|
|
582
|
+
return generate_identity(tipo="moral", **kwargs)
|