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,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IMSS (Instituto Mexicano del Seguro Social) Calculator for Mexico
|
|
3
|
+
Calculates employer/employee contributions and voluntary modalities (10, 40)
|
|
4
|
+
Uses centralized JSON tables from shared-data
|
|
5
|
+
|
|
6
|
+
Official sources:
|
|
7
|
+
- Ley del Seguro Social
|
|
8
|
+
- Sistema Único de Autodeterminación (SUA)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Literal, TypedDict
|
|
14
|
+
|
|
15
|
+
IMSSYear = Literal[2024, 2025, 2026]
|
|
16
|
+
ZonaSalario = Literal["general", "frontera"]
|
|
17
|
+
ClaseRiesgo = Literal[1, 2, 3, 4, 5]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CuotasIMSSResult(TypedDict):
|
|
21
|
+
"""IMSS contributions breakdown result"""
|
|
22
|
+
|
|
23
|
+
salario_diario: float
|
|
24
|
+
dias: int
|
|
25
|
+
salario_base_cotizacion: float
|
|
26
|
+
year: int
|
|
27
|
+
cuotas_patron: dict[str, float]
|
|
28
|
+
cuotas_trabajador: dict[str, float]
|
|
29
|
+
total_patron: float
|
|
30
|
+
total_trabajador: float
|
|
31
|
+
total_imss: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Modalidad40Result(TypedDict):
|
|
35
|
+
"""Modalidad 40 calculation result"""
|
|
36
|
+
|
|
37
|
+
salario_base_cotizacion: float
|
|
38
|
+
year: int
|
|
39
|
+
cuota_mensual: float
|
|
40
|
+
porcentaje_total: float
|
|
41
|
+
componentes: dict[str, float]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Modalidad10Result(TypedDict):
|
|
45
|
+
"""Modalidad 10 calculation result"""
|
|
46
|
+
|
|
47
|
+
salario_base_cotizacion: float
|
|
48
|
+
year: int
|
|
49
|
+
cuota_mensual: float
|
|
50
|
+
cuota_fija_uma: float
|
|
51
|
+
cuota_variable: float
|
|
52
|
+
porcentaje_variable: float
|
|
53
|
+
componentes: dict[str, float]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UMAInfo(TypedDict):
|
|
57
|
+
"""UMA (Unidad de Medida y Actualización) information"""
|
|
58
|
+
|
|
59
|
+
diaria: float
|
|
60
|
+
mensual: float
|
|
61
|
+
anual: float
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Load IMSS tables from centralized JSON
|
|
65
|
+
_IMSS_TABLES: dict | None = None
|
|
66
|
+
_IMSS_CATALOGS: dict | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _load_imss_tables() -> dict:
|
|
70
|
+
"""Load IMSS tables from shared JSON file"""
|
|
71
|
+
global _IMSS_TABLES
|
|
72
|
+
if _IMSS_TABLES is None:
|
|
73
|
+
json_path = (
|
|
74
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
75
|
+
/ "packages"
|
|
76
|
+
/ "shared-data"
|
|
77
|
+
/ "imss-tables.json"
|
|
78
|
+
)
|
|
79
|
+
with open(json_path, encoding="utf-8") as f:
|
|
80
|
+
_IMSS_TABLES = json.load(f)
|
|
81
|
+
return _IMSS_TABLES
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _load_imss_catalogs() -> dict:
|
|
85
|
+
"""Load IMSS catalogs from shared JSON file"""
|
|
86
|
+
global _IMSS_CATALOGS
|
|
87
|
+
if _IMSS_CATALOGS is None:
|
|
88
|
+
json_path = (
|
|
89
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
90
|
+
/ "packages"
|
|
91
|
+
/ "shared-data"
|
|
92
|
+
/ "imss-catalogs.json"
|
|
93
|
+
)
|
|
94
|
+
with open(json_path, encoding="utf-8") as f:
|
|
95
|
+
_IMSS_CATALOGS = json.load(f)
|
|
96
|
+
return _IMSS_CATALOGS
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_uma(year: IMSSYear) -> UMAInfo:
|
|
100
|
+
"""
|
|
101
|
+
Get UMA (Unidad de Medida y Actualización) values for a specific year
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
year: Year (2024, 2025, or 2026)
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
UMA values (diaria, mensual, anual)
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
>>> uma = get_uma(2026)
|
|
111
|
+
>>> print(f"UMA diaria 2026: ${uma['diaria']}")
|
|
112
|
+
UMA diaria 2026: $113.14
|
|
113
|
+
"""
|
|
114
|
+
tables = _load_imss_tables()
|
|
115
|
+
year_str = str(year)
|
|
116
|
+
uma_data = tables["uma"][year_str]
|
|
117
|
+
return UMAInfo(
|
|
118
|
+
diaria=uma_data["diaria"],
|
|
119
|
+
mensual=uma_data["mensual"],
|
|
120
|
+
anual=uma_data["anual"],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_salario_minimo(year: IMSSYear, zona: ZonaSalario = "general") -> float:
|
|
125
|
+
"""
|
|
126
|
+
Get minimum wage for a specific year and zone
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
year: Year (2024, 2025, or 2026)
|
|
130
|
+
zona: Zone type ("general" or "frontera")
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Daily minimum wage amount
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
>>> salario = get_salario_minimo(2026, "general")
|
|
137
|
+
>>> print(f"Salario mínimo general 2026: ${salario}")
|
|
138
|
+
Salario mínimo general 2026: $278.80
|
|
139
|
+
"""
|
|
140
|
+
tables = _load_imss_tables()
|
|
141
|
+
year_str = str(year)
|
|
142
|
+
return tables["salario_minimo"][year_str][zona]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def calcular_cuotas_obrero_patronales(
|
|
146
|
+
salario_diario: float,
|
|
147
|
+
dias: int = 30,
|
|
148
|
+
year: IMSSYear = 2026,
|
|
149
|
+
clase_riesgo: ClaseRiesgo = 1,
|
|
150
|
+
) -> CuotasIMSSResult:
|
|
151
|
+
"""
|
|
152
|
+
Calculate IMSS employer and employee contributions
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
salario_diario: Daily wage
|
|
156
|
+
dias: Number of days (default: 30)
|
|
157
|
+
year: Year (default: 2026)
|
|
158
|
+
clase_riesgo: Work risk class 1-5 (default: 1 - minimum risk)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Complete breakdown of IMSS contributions
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
>>> result = calcular_cuotas_obrero_patronales(500.0, 30, 2026)
|
|
165
|
+
>>> print(f"Total patrón: ${result['total_patron']:.2f}")
|
|
166
|
+
>>> print(f"Total trabajador: ${result['total_trabajador']:.2f}")
|
|
167
|
+
"""
|
|
168
|
+
tables = _load_imss_tables()
|
|
169
|
+
uma = get_uma(year)
|
|
170
|
+
cuotas = tables["cuotas_imss"]
|
|
171
|
+
|
|
172
|
+
# Calculate base salary for contributions
|
|
173
|
+
salario_base = salario_diario * dias
|
|
174
|
+
|
|
175
|
+
# Límite de 3 UMAs diario para algunas cuotas
|
|
176
|
+
uma_diaria = uma["diaria"]
|
|
177
|
+
tres_uma_mensual = uma_diaria * 3 * 30
|
|
178
|
+
|
|
179
|
+
# Initialize contribution dictionaries
|
|
180
|
+
cuotas_patron: dict[str, float] = {}
|
|
181
|
+
cuotas_trabajador: dict[str, float] = {}
|
|
182
|
+
|
|
183
|
+
# 1. Enfermedad y Maternidad
|
|
184
|
+
em = cuotas["enfermedad_maternidad"]
|
|
185
|
+
|
|
186
|
+
# Cuota fija (sobre 3 UMAs)
|
|
187
|
+
cuotas_patron["enfermedad_mat_cuota_fija"] = (
|
|
188
|
+
uma_diaria * 3 * dias * em["prestaciones_en_especie"]["patron"]
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Excedente de 3 UMAs
|
|
192
|
+
if salario_diario > tres_uma_mensual / 30:
|
|
193
|
+
excedente_base = salario_base - tres_uma_mensual
|
|
194
|
+
cuotas_patron["enfermedad_mat_excedente"] = (
|
|
195
|
+
excedente_base * em["prestaciones_en_especie_excedente"]["patron"]
|
|
196
|
+
)
|
|
197
|
+
cuotas_trabajador["enfermedad_mat_excedente"] = (
|
|
198
|
+
excedente_base * em["prestaciones_en_especie_excedente"]["trabajador"]
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
cuotas_patron["enfermedad_mat_excedente"] = 0.0
|
|
202
|
+
cuotas_trabajador["enfermedad_mat_excedente"] = 0.0
|
|
203
|
+
|
|
204
|
+
# Prestaciones en dinero
|
|
205
|
+
cuotas_patron["enfermedad_mat_dinero"] = salario_base * em["prestaciones_en_dinero"]["patron"]
|
|
206
|
+
cuotas_trabajador["enfermedad_mat_dinero"] = (
|
|
207
|
+
salario_base * em["prestaciones_en_dinero"]["trabajador"]
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Gastos médicos pensionados
|
|
211
|
+
cuotas_patron["gastos_medicos_pensionados"] = (
|
|
212
|
+
salario_base * em["gastos_medicos_pensionados"]["patron"]
|
|
213
|
+
)
|
|
214
|
+
cuotas_trabajador["gastos_medicos_pensionados"] = (
|
|
215
|
+
salario_base * em["gastos_medicos_pensionados"]["trabajador"]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# 2. Invalidez y Vida
|
|
219
|
+
iv = cuotas["invalidez_vida"]
|
|
220
|
+
cuotas_patron["invalidez_vida"] = salario_base * iv["patron"]
|
|
221
|
+
cuotas_trabajador["invalidez_vida"] = salario_base * iv["trabajador"]
|
|
222
|
+
|
|
223
|
+
# 3. Retiro, Cesantía y Vejez
|
|
224
|
+
rcv = cuotas["retiro_cesantia_vejez"]
|
|
225
|
+
cuotas_patron["retiro"] = salario_base * rcv["retiro"]["patron"]
|
|
226
|
+
cuotas_patron["cesantia_vejez"] = salario_base * rcv["cesantia_vejez"]["patron"]
|
|
227
|
+
cuotas_trabajador["cesantia_vejez"] = salario_base * rcv["cesantia_vejez"]["trabajador"]
|
|
228
|
+
|
|
229
|
+
# 4. Guarderías y Prestaciones Sociales
|
|
230
|
+
gps = cuotas["guarderias_prestaciones_sociales"]
|
|
231
|
+
cuotas_patron["guarderias"] = salario_base * gps["patron"]
|
|
232
|
+
|
|
233
|
+
# 5. Riesgos de Trabajo
|
|
234
|
+
rt = cuotas["riesgo_trabajo"]
|
|
235
|
+
prima_riesgo = rt[f"clase_{clase_riesgo}"]
|
|
236
|
+
cuotas_patron["riesgo_trabajo"] = salario_base * prima_riesgo
|
|
237
|
+
|
|
238
|
+
# Calculate totals
|
|
239
|
+
total_patron = sum(cuotas_patron.values())
|
|
240
|
+
total_trabajador = sum(cuotas_trabajador.values())
|
|
241
|
+
|
|
242
|
+
return CuotasIMSSResult(
|
|
243
|
+
salario_diario=salario_diario,
|
|
244
|
+
dias=dias,
|
|
245
|
+
salario_base_cotizacion=salario_base,
|
|
246
|
+
year=year,
|
|
247
|
+
cuotas_patron=cuotas_patron,
|
|
248
|
+
cuotas_trabajador=cuotas_trabajador,
|
|
249
|
+
total_patron=total_patron,
|
|
250
|
+
total_trabajador=total_trabajador,
|
|
251
|
+
total_imss=total_patron + total_trabajador,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def calcular_modalidad_40(
|
|
256
|
+
salario_base_cotizacion: float, year: IMSSYear = 2026
|
|
257
|
+
) -> Modalidad40Result:
|
|
258
|
+
"""
|
|
259
|
+
Calculate Modalidad 40 voluntary IMSS contributions
|
|
260
|
+
(Continuación voluntaria en el régimen obligatorio)
|
|
261
|
+
|
|
262
|
+
Modalidad 40 allows workers who left formal employment to continue
|
|
263
|
+
contributing to IMSS to increase their pension amount.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
salario_base_cotizacion: Monthly base salary (between 1 and 25 UMAs)
|
|
267
|
+
year: Year (default: 2026)
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Modalidad 40 calculation result
|
|
271
|
+
|
|
272
|
+
Examples:
|
|
273
|
+
>>> result = calcular_modalidad_40(15000, 2026)
|
|
274
|
+
>>> print(f"Cuota mensual: ${result['cuota_mensual']:.2f}")
|
|
275
|
+
"""
|
|
276
|
+
tables = _load_imss_tables()
|
|
277
|
+
uma = get_uma(year)
|
|
278
|
+
mod40 = tables["modalidad_40"]
|
|
279
|
+
|
|
280
|
+
# Validate salary limits
|
|
281
|
+
uma_mensual = uma["mensual"]
|
|
282
|
+
salario_minimo = uma_mensual * mod40["limites_salario"]["minimo_uma"]
|
|
283
|
+
salario_maximo = uma_mensual * mod40["limites_salario"]["maximo_uma"]
|
|
284
|
+
|
|
285
|
+
if salario_base_cotizacion < salario_minimo:
|
|
286
|
+
salario_base_cotizacion = salario_minimo
|
|
287
|
+
elif salario_base_cotizacion > salario_maximo:
|
|
288
|
+
salario_base_cotizacion = salario_maximo
|
|
289
|
+
|
|
290
|
+
# Calculate monthly contribution (10.47% total)
|
|
291
|
+
porcentaje_total = mod40["cuota_mensual"]["porcentaje_total"]
|
|
292
|
+
cuota_mensual = salario_base_cotizacion * porcentaje_total
|
|
293
|
+
|
|
294
|
+
# Get component breakdown
|
|
295
|
+
componentes = {
|
|
296
|
+
key: salario_base_cotizacion * value
|
|
297
|
+
for key, value in mod40["cuota_mensual"]["componentes"].items()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return Modalidad40Result(
|
|
301
|
+
salario_base_cotizacion=salario_base_cotizacion,
|
|
302
|
+
year=year,
|
|
303
|
+
cuota_mensual=cuota_mensual,
|
|
304
|
+
porcentaje_total=porcentaje_total,
|
|
305
|
+
componentes=componentes,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def calcular_modalidad_10(
|
|
310
|
+
salario_base_cotizacion: float, year: IMSSYear = 2026
|
|
311
|
+
) -> Modalidad10Result:
|
|
312
|
+
"""
|
|
313
|
+
Calculate Modalidad 10 voluntary IMSS contributions
|
|
314
|
+
(Incorporación voluntaria al régimen obligatorio - trabajadores independientes)
|
|
315
|
+
|
|
316
|
+
Modalidad 10 allows independent workers to enroll in IMSS and access
|
|
317
|
+
all social security benefits including healthcare, retirement, and more.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
salario_base_cotizacion: Monthly base salary (between 1 and 25 UMAs)
|
|
321
|
+
year: Year (default: 2026)
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Modalidad 10 calculation result
|
|
325
|
+
|
|
326
|
+
Examples:
|
|
327
|
+
>>> result = calcular_modalidad_10(10000, 2026)
|
|
328
|
+
>>> print(f"Cuota mensual: ${result['cuota_mensual']:.2f}")
|
|
329
|
+
"""
|
|
330
|
+
tables = _load_imss_tables()
|
|
331
|
+
uma = get_uma(year)
|
|
332
|
+
mod10 = tables["modalidad_10"]
|
|
333
|
+
|
|
334
|
+
# Validate salary limits
|
|
335
|
+
uma_mensual = uma["mensual"]
|
|
336
|
+
salario_minimo = uma_mensual * mod10["limites_salario"]["minimo_uma"]
|
|
337
|
+
salario_maximo = uma_mensual * mod10["limites_salario"]["maximo_uma"]
|
|
338
|
+
|
|
339
|
+
if salario_base_cotizacion < salario_minimo:
|
|
340
|
+
salario_base_cotizacion = salario_minimo
|
|
341
|
+
elif salario_base_cotizacion > salario_maximo:
|
|
342
|
+
salario_base_cotizacion = salario_maximo
|
|
343
|
+
|
|
344
|
+
# Fixed fee: 3.3 UMAs for healthcare
|
|
345
|
+
cuota_fija_uma = uma["diaria"] * mod10["cuota_mensual"]["cuota_fija_uma_factor"]
|
|
346
|
+
|
|
347
|
+
# Variable percentage: 10.47%
|
|
348
|
+
porcentaje_variable = mod10["cuota_mensual"]["porcentaje_variable"]
|
|
349
|
+
cuota_variable = salario_base_cotizacion * porcentaje_variable
|
|
350
|
+
|
|
351
|
+
# Total monthly contribution
|
|
352
|
+
cuota_mensual = cuota_fija_uma + cuota_variable
|
|
353
|
+
|
|
354
|
+
# Get component breakdown
|
|
355
|
+
componentes = {
|
|
356
|
+
"prestaciones_en_especie_fija": cuota_fija_uma,
|
|
357
|
+
}
|
|
358
|
+
for key, value in mod10["cuota_mensual"]["componentes"].items():
|
|
359
|
+
if isinstance(value, (int, float)):
|
|
360
|
+
componentes[key] = salario_base_cotizacion * value
|
|
361
|
+
|
|
362
|
+
return Modalidad10Result(
|
|
363
|
+
salario_base_cotizacion=salario_base_cotizacion,
|
|
364
|
+
year=year,
|
|
365
|
+
cuota_mensual=cuota_mensual,
|
|
366
|
+
cuota_fija_uma=cuota_fija_uma,
|
|
367
|
+
cuota_variable=cuota_variable,
|
|
368
|
+
porcentaje_variable=porcentaje_variable,
|
|
369
|
+
componentes=componentes,
|
|
370
|
+
)
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ISR (Impuesto Sobre la Renta) Calculator for Mexico
|
|
3
|
+
Supports multi-year calculations: 2024, 2025, 2026
|
|
4
|
+
Uses centralized JSON tables from shared-data
|
|
5
|
+
|
|
6
|
+
Official source: Anexo 8 RMF (Resolución Miscelánea Fiscal)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal, TypedDict
|
|
12
|
+
|
|
13
|
+
ISRYear = Literal[2024, 2025, 2026]
|
|
14
|
+
ISRPeriod = Literal["diaria", "semanal", "decenal", "quincenal", "mensual", "anual"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ISRBracket(TypedDict):
|
|
18
|
+
"""Tax bracket structure"""
|
|
19
|
+
|
|
20
|
+
limiteInferior: float
|
|
21
|
+
limiteSuperior: float | None
|
|
22
|
+
cuotaFija: float
|
|
23
|
+
tasa: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ISRSubsidyBracket(TypedDict):
|
|
27
|
+
"""Subsidy bracket for tiered system (2024)"""
|
|
28
|
+
|
|
29
|
+
desde: float
|
|
30
|
+
hasta: float | None
|
|
31
|
+
subsidio: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ISRSubsidyFlat(TypedDict):
|
|
35
|
+
"""Flat subsidy system (2025/2026)"""
|
|
36
|
+
|
|
37
|
+
amount: float
|
|
38
|
+
maxIncome: float
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ISRCalculationResult(TypedDict):
|
|
42
|
+
"""Complete ISR calculation result"""
|
|
43
|
+
|
|
44
|
+
ingresoGravable: float
|
|
45
|
+
periodo: str
|
|
46
|
+
year: int
|
|
47
|
+
ingresoMensualizado: float
|
|
48
|
+
bracket: dict
|
|
49
|
+
excedente: float
|
|
50
|
+
impuestoMarginal: float
|
|
51
|
+
isrAntesSubsidio: float
|
|
52
|
+
subsidio: float
|
|
53
|
+
isrFinal: float
|
|
54
|
+
tasaEfectiva: float
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Load ISR tables from centralized JSON
|
|
58
|
+
_ISR_TABLES: dict | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_isr_tables() -> dict:
|
|
62
|
+
"""Load ISR tables from shared JSON file"""
|
|
63
|
+
global _ISR_TABLES
|
|
64
|
+
if _ISR_TABLES is None:
|
|
65
|
+
# Path from catalogmx/calculators/isr.py -> packages/shared-data/isr-tables.json
|
|
66
|
+
# Go up: isr.py -> calculators -> catalogmx -> python -> packages -> catalogmx (root)
|
|
67
|
+
json_path = (
|
|
68
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
69
|
+
/ "packages"
|
|
70
|
+
/ "shared-data"
|
|
71
|
+
/ "isr-tables.json"
|
|
72
|
+
)
|
|
73
|
+
with open(json_path, encoding="utf-8") as f:
|
|
74
|
+
_ISR_TABLES = json.load(f)
|
|
75
|
+
return _ISR_TABLES
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Period mapping
|
|
79
|
+
PERIOD_MAPPING = {
|
|
80
|
+
"diaria": "daily",
|
|
81
|
+
"semanal": "weekly",
|
|
82
|
+
"decenal": "monthly", # No specific decenal table, use monthly
|
|
83
|
+
"quincenal": "biweekly",
|
|
84
|
+
"mensual": "monthly",
|
|
85
|
+
"anual": "annual",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Period factors for conversion
|
|
89
|
+
PERIOD_FACTORS = {
|
|
90
|
+
"diaria": 30.4,
|
|
91
|
+
"semanal": 4.33,
|
|
92
|
+
"decenal": 3.0,
|
|
93
|
+
"quincenal": 2.0,
|
|
94
|
+
"mensual": 1.0,
|
|
95
|
+
"anual": 1 / 12,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_isr_brackets(year: ISRYear, period: ISRPeriod) -> list[ISRBracket]:
|
|
100
|
+
"""
|
|
101
|
+
Get ISR tax brackets for a specific year and period
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
year: Tax year (2024, 2025, or 2026)
|
|
105
|
+
period: Payment period
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of tax brackets
|
|
109
|
+
"""
|
|
110
|
+
tables = _load_isr_tables()
|
|
111
|
+
year_str = str(year)
|
|
112
|
+
period_key = PERIOD_MAPPING[period]
|
|
113
|
+
|
|
114
|
+
brackets_data = tables["brackets"][year_str][period_key]
|
|
115
|
+
|
|
116
|
+
# Convert None to float('inf') for limiteSuperior
|
|
117
|
+
brackets = []
|
|
118
|
+
for b in brackets_data:
|
|
119
|
+
bracket = ISRBracket(
|
|
120
|
+
limiteInferior=b["limiteInferior"],
|
|
121
|
+
limiteSuperior=float("inf") if b["limiteSuperior"] is None else b["limiteSuperior"],
|
|
122
|
+
cuotaFija=b["cuotaFija"],
|
|
123
|
+
tasa=b["tasa"],
|
|
124
|
+
)
|
|
125
|
+
brackets.append(bracket)
|
|
126
|
+
|
|
127
|
+
return brackets
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def calculate_subsidy(income: float, year: ISRYear) -> float:
|
|
131
|
+
"""
|
|
132
|
+
Calculate employment subsidy (subsidio al empleo) for a given income
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
income: Monthly income amount
|
|
136
|
+
year: Tax year
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Subsidy amount
|
|
140
|
+
"""
|
|
141
|
+
tables = _load_isr_tables()
|
|
142
|
+
year_str = str(year)
|
|
143
|
+
subsidy_data = tables["subsidies"][year_str]
|
|
144
|
+
|
|
145
|
+
if subsidy_data["type"] == "tiered":
|
|
146
|
+
# 2024: tiered system
|
|
147
|
+
monthly_data = subsidy_data["monthly"]
|
|
148
|
+
for s in monthly_data:
|
|
149
|
+
hasta = float("inf") if s["hasta"] is None else s["hasta"]
|
|
150
|
+
if s["desde"] <= income <= hasta:
|
|
151
|
+
return s["subsidio"]
|
|
152
|
+
return 0.0
|
|
153
|
+
else:
|
|
154
|
+
# 2025/2026: flat system
|
|
155
|
+
flat_data = subsidy_data["monthly"]
|
|
156
|
+
if income <= flat_data["maxIncome"]:
|
|
157
|
+
return flat_data["amount"]
|
|
158
|
+
return 0.0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def calculate_isr(
|
|
162
|
+
ingreso_gravable: float, periodo: ISRPeriod = "mensual", year: ISRYear = 2026
|
|
163
|
+
) -> ISRCalculationResult:
|
|
164
|
+
"""
|
|
165
|
+
Calculate ISR (Income Tax) for Mexico
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
ingreso_gravable: Taxable income for the period
|
|
169
|
+
periodo: Payment period (default: mensual)
|
|
170
|
+
year: Tax year (default: 2026)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Complete calculation result with breakdown
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
>>> result = calculate_isr(15000, "mensual", 2026)
|
|
177
|
+
>>> print(f"ISR: ${result['isrFinal']:.2f}")
|
|
178
|
+
ISR: $1234.56
|
|
179
|
+
|
|
180
|
+
>>> result = calculate_isr(500, "diaria", 2026)
|
|
181
|
+
>>> print(f"ISR diario: ${result['isrFinal']:.2f}")
|
|
182
|
+
ISR diario: $45.67
|
|
183
|
+
"""
|
|
184
|
+
# For 2026, use period-specific tables directly
|
|
185
|
+
use_period_tables = year == 2026 and periodo in [
|
|
186
|
+
"diaria",
|
|
187
|
+
"semanal",
|
|
188
|
+
"quincenal",
|
|
189
|
+
"mensual",
|
|
190
|
+
"anual",
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
if use_period_tables:
|
|
194
|
+
# Use official tables for the period (no conversion needed)
|
|
195
|
+
brackets = get_isr_brackets(year, periodo)
|
|
196
|
+
|
|
197
|
+
# Find applicable bracket
|
|
198
|
+
bracket = None
|
|
199
|
+
for b in brackets:
|
|
200
|
+
if b["limiteInferior"] <= ingreso_gravable <= b["limiteSuperior"]:
|
|
201
|
+
bracket = b
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
if bracket is None:
|
|
205
|
+
bracket = brackets[-1] # Last bracket (highest income)
|
|
206
|
+
|
|
207
|
+
# Calculate excess over lower limit
|
|
208
|
+
excedente = ingreso_gravable - bracket["limiteInferior"]
|
|
209
|
+
|
|
210
|
+
# Calculate marginal tax
|
|
211
|
+
impuesto_marginal = excedente * (bracket["tasa"] / 100)
|
|
212
|
+
|
|
213
|
+
# Add fixed fee
|
|
214
|
+
isr_antes_subsidio = bracket["cuotaFija"] + impuesto_marginal
|
|
215
|
+
|
|
216
|
+
# Calculate subsidy (convert to monthly equivalent)
|
|
217
|
+
factor = PERIOD_FACTORS[periodo]
|
|
218
|
+
ingreso_mensual_equivalente = ingreso_gravable * factor
|
|
219
|
+
subsidio_mensual = calculate_subsidy(ingreso_mensual_equivalente, year)
|
|
220
|
+
subsidio_prorrateado = subsidio_mensual / factor
|
|
221
|
+
|
|
222
|
+
# Final ISR
|
|
223
|
+
isr_final = max(0, isr_antes_subsidio - subsidio_prorrateado)
|
|
224
|
+
|
|
225
|
+
tasa_efectiva = (isr_final / ingreso_gravable * 100) if ingreso_gravable > 0 else 0
|
|
226
|
+
|
|
227
|
+
return ISRCalculationResult(
|
|
228
|
+
ingresoGravable=ingreso_gravable,
|
|
229
|
+
periodo=periodo,
|
|
230
|
+
year=year,
|
|
231
|
+
ingresoMensualizado=ingreso_mensual_equivalente,
|
|
232
|
+
bracket=dict(bracket),
|
|
233
|
+
excedente=excedente,
|
|
234
|
+
impuestoMarginal=impuesto_marginal,
|
|
235
|
+
isrAntesSubsidio=isr_antes_subsidio,
|
|
236
|
+
subsidio=subsidio_prorrateado,
|
|
237
|
+
isrFinal=isr_final,
|
|
238
|
+
tasaEfectiva=tasa_efectiva,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# For 2024/2025 or decenal period, use factor-based conversion
|
|
242
|
+
brackets = get_isr_brackets(year, "mensual")
|
|
243
|
+
|
|
244
|
+
# Convert to monthly
|
|
245
|
+
factor = PERIOD_FACTORS[periodo]
|
|
246
|
+
ingreso_mensualizado = ingreso_gravable * factor
|
|
247
|
+
|
|
248
|
+
# Find applicable bracket
|
|
249
|
+
bracket = None
|
|
250
|
+
for b in brackets:
|
|
251
|
+
if b["limiteInferior"] <= ingreso_mensualizado <= b["limiteSuperior"]:
|
|
252
|
+
bracket = b
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
if bracket is None:
|
|
256
|
+
bracket = brackets[-1]
|
|
257
|
+
|
|
258
|
+
# Calculate excess over lower limit
|
|
259
|
+
excedente = ingreso_mensualizado - bracket["limiteInferior"]
|
|
260
|
+
|
|
261
|
+
# Calculate marginal tax
|
|
262
|
+
impuesto_marginal = excedente * (bracket["tasa"] / 100)
|
|
263
|
+
|
|
264
|
+
# Add fixed fee
|
|
265
|
+
isr_antes_subsidio = bracket["cuotaFija"] + impuesto_marginal
|
|
266
|
+
|
|
267
|
+
# Calculate subsidy
|
|
268
|
+
subsidio = calculate_subsidy(ingreso_mensualizado, year)
|
|
269
|
+
|
|
270
|
+
# Final ISR (monthly)
|
|
271
|
+
isr_final_mensual = max(0, isr_antes_subsidio - subsidio)
|
|
272
|
+
|
|
273
|
+
# Convert back to original period
|
|
274
|
+
isr_periodo = isr_final_mensual / factor
|
|
275
|
+
|
|
276
|
+
tasa_efectiva = (isr_periodo / ingreso_gravable * 100) if ingreso_gravable > 0 else 0
|
|
277
|
+
|
|
278
|
+
return ISRCalculationResult(
|
|
279
|
+
ingresoGravable=ingreso_gravable,
|
|
280
|
+
periodo=periodo,
|
|
281
|
+
year=year,
|
|
282
|
+
ingresoMensualizado=ingreso_mensualizado,
|
|
283
|
+
bracket=dict(bracket),
|
|
284
|
+
excedente=excedente,
|
|
285
|
+
impuestoMarginal=impuesto_marginal,
|
|
286
|
+
isrAntesSubsidio=isr_antes_subsidio,
|
|
287
|
+
subsidio=subsidio,
|
|
288
|
+
isrFinal=isr_periodo,
|
|
289
|
+
tasaEfectiva=tasa_efectiva,
|
|
290
|
+
)
|