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,920 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tax Calculators: IVA, IEPS, Retenciones, and Impuestos Locales
|
|
3
|
+
Provides methods for calculating taxes according to Mexican tax laws
|
|
4
|
+
|
|
5
|
+
Official sources:
|
|
6
|
+
- Ley del IVA (Value Added Tax Law)
|
|
7
|
+
- Ley del IEPS (Special Production and Services Tax Law)
|
|
8
|
+
- Ley del ISR (Income Tax Law - for withholdings)
|
|
9
|
+
- State and Municipal Tax Laws
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Literal, TypedDict
|
|
16
|
+
|
|
17
|
+
# Type aliases
|
|
18
|
+
IVATipoTasa = Literal["general", "frontera", "tasa_cero"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IVATasa(TypedDict):
|
|
22
|
+
"""IVA tax rate structure"""
|
|
23
|
+
|
|
24
|
+
tipo: str
|
|
25
|
+
tasa: float
|
|
26
|
+
vigencia_inicio: str
|
|
27
|
+
vigencia_fin: str | None
|
|
28
|
+
descripcion: str
|
|
29
|
+
aplica_en: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class IVACalculationResult(TypedDict):
|
|
33
|
+
"""IVA calculation result"""
|
|
34
|
+
|
|
35
|
+
base: float
|
|
36
|
+
tasa: float
|
|
37
|
+
iva: float
|
|
38
|
+
total_con_iva: float
|
|
39
|
+
tipo_tasa: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class IEPSTasa(TypedDict):
|
|
43
|
+
"""IEPS tax rate structure"""
|
|
44
|
+
|
|
45
|
+
subcategoria: str
|
|
46
|
+
tasa: float
|
|
47
|
+
tipo: str
|
|
48
|
+
vigencia_inicio: str
|
|
49
|
+
vigencia_fin: str | None
|
|
50
|
+
descripcion: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IEPSCategoria(TypedDict):
|
|
54
|
+
"""IEPS category structure"""
|
|
55
|
+
|
|
56
|
+
categoria: str
|
|
57
|
+
descripcion: str
|
|
58
|
+
tasas: list[IEPSTasa]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class IEPSCalculationResult(TypedDict):
|
|
62
|
+
"""IEPS calculation result"""
|
|
63
|
+
|
|
64
|
+
base: float
|
|
65
|
+
tasa: float
|
|
66
|
+
ieps: float
|
|
67
|
+
tipo_calculo: str
|
|
68
|
+
unidad: str | None
|
|
69
|
+
cantidad: float | None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RetencionISR(TypedDict):
|
|
73
|
+
"""ISR withholding structure"""
|
|
74
|
+
|
|
75
|
+
concepto: str
|
|
76
|
+
tasa: float | str
|
|
77
|
+
descripcion: str
|
|
78
|
+
base: str
|
|
79
|
+
articulo: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RetencionIVA(TypedDict):
|
|
83
|
+
"""IVA withholding structure"""
|
|
84
|
+
|
|
85
|
+
concepto: str
|
|
86
|
+
tasa: float
|
|
87
|
+
descripcion: str
|
|
88
|
+
base: str
|
|
89
|
+
articulo: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class RetencionCalculationResult(TypedDict):
|
|
93
|
+
"""Withholding calculation result"""
|
|
94
|
+
|
|
95
|
+
concepto: str
|
|
96
|
+
base: float
|
|
97
|
+
tasa: float
|
|
98
|
+
retencion: float
|
|
99
|
+
impuesto_base: float | None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ImpuestoEstatal(TypedDict):
|
|
103
|
+
"""State tax structure"""
|
|
104
|
+
|
|
105
|
+
estado: str
|
|
106
|
+
cve_estado: str
|
|
107
|
+
tasa: float
|
|
108
|
+
base: str
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class IVAData(TypedDict):
|
|
112
|
+
"""IVA data file structure"""
|
|
113
|
+
|
|
114
|
+
metadata: dict
|
|
115
|
+
tasas: list[IVATasa]
|
|
116
|
+
exenciones: list
|
|
117
|
+
tasa_cero_productos: list
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class RetencionesData(TypedDict):
|
|
121
|
+
"""Retenciones data file structure"""
|
|
122
|
+
|
|
123
|
+
metadata: dict
|
|
124
|
+
isr_retenciones: list[RetencionISR]
|
|
125
|
+
iva_retenciones: list[RetencionIVA]
|
|
126
|
+
retenciones_definitivas: list
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ImpuestosLocalesData(TypedDict):
|
|
130
|
+
"""Impuestos Locales data file structure"""
|
|
131
|
+
|
|
132
|
+
metadata: dict
|
|
133
|
+
impuesto_nomina: list[ImpuestoEstatal]
|
|
134
|
+
impuesto_hospedaje: list[ImpuestoEstatal]
|
|
135
|
+
otros_impuestos_estatales: list
|
|
136
|
+
predial: dict
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class IVACalculator:
|
|
140
|
+
"""
|
|
141
|
+
IVA (Value Added Tax) Calculator
|
|
142
|
+
|
|
143
|
+
Calculates IVA according to Mexican tax law with support for:
|
|
144
|
+
- General rate (16%)
|
|
145
|
+
- Border rate (8%)
|
|
146
|
+
- Zero rate (0%)
|
|
147
|
+
- Historical rates
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
_data: IVAData | None = None
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def _load_data(cls) -> None:
|
|
154
|
+
"""Lazy load IVA data from JSON file"""
|
|
155
|
+
if cls._data is not None:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# Path from catalogmx/calculators/impuestos.py -> shared-data/sat/impuestos/iva_tasas.json
|
|
159
|
+
json_path = (
|
|
160
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
161
|
+
/ "packages"
|
|
162
|
+
/ "shared-data"
|
|
163
|
+
/ "sat"
|
|
164
|
+
/ "impuestos"
|
|
165
|
+
/ "iva_tasas.json"
|
|
166
|
+
)
|
|
167
|
+
with open(json_path, encoding="utf-8") as f:
|
|
168
|
+
cls._data = json.load(f)
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def get_tasa_vigente(
|
|
172
|
+
cls,
|
|
173
|
+
fecha: str | datetime | None = None,
|
|
174
|
+
tipo_tasa: IVATipoTasa = "general",
|
|
175
|
+
) -> IVATasa | None:
|
|
176
|
+
"""
|
|
177
|
+
Get the current IVA rate for a specific date and type
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
fecha: Date in ISO format (YYYY-MM-DD) or datetime object (default: today)
|
|
181
|
+
tipo_tasa: Rate type (general, frontera, tasa_cero)
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
IVA rate object or None if not found
|
|
185
|
+
|
|
186
|
+
Examples:
|
|
187
|
+
>>> tasa = IVACalculator.get_tasa_vigente()
|
|
188
|
+
>>> print(f"Tasa actual: {tasa['tasa']}%")
|
|
189
|
+
Tasa actual: 16.0%
|
|
190
|
+
|
|
191
|
+
>>> tasa = IVACalculator.get_tasa_vigente("2024-01-01", "frontera")
|
|
192
|
+
>>> print(f"Tasa frontera: {tasa['tasa']}%")
|
|
193
|
+
Tasa frontera: 8.0%
|
|
194
|
+
"""
|
|
195
|
+
cls._load_data()
|
|
196
|
+
|
|
197
|
+
# Convert fecha to datetime
|
|
198
|
+
if fecha is None:
|
|
199
|
+
fecha_date = datetime.now()
|
|
200
|
+
elif isinstance(fecha, str):
|
|
201
|
+
fecha_date = datetime.fromisoformat(fecha)
|
|
202
|
+
else:
|
|
203
|
+
fecha_date = fecha
|
|
204
|
+
|
|
205
|
+
# Filter by rate type
|
|
206
|
+
tasas_del_tipo = [t for t in cls._data["tasas"] if t["tipo"] == tipo_tasa]
|
|
207
|
+
|
|
208
|
+
# Find applicable rate
|
|
209
|
+
for tasa in tasas_del_tipo:
|
|
210
|
+
inicio_date = datetime.fromisoformat(tasa["vigencia_inicio"])
|
|
211
|
+
fin_date = (
|
|
212
|
+
datetime.fromisoformat(tasa["vigencia_fin"]) if tasa["vigencia_fin"] else None
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
despues_inicio = fecha_date >= inicio_date
|
|
216
|
+
antes_fin = not fin_date or fecha_date <= fin_date
|
|
217
|
+
|
|
218
|
+
if despues_inicio and antes_fin:
|
|
219
|
+
return tasa
|
|
220
|
+
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def calcular(
|
|
225
|
+
cls,
|
|
226
|
+
base: float,
|
|
227
|
+
tipo_tasa: IVATipoTasa = "general",
|
|
228
|
+
fecha: str | datetime | None = None,
|
|
229
|
+
) -> IVACalculationResult:
|
|
230
|
+
"""
|
|
231
|
+
Calculate IVA for a base amount
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
base: Base amount without IVA
|
|
235
|
+
tipo_tasa: Rate type (general, frontera, tasa_cero)
|
|
236
|
+
fecha: Date to determine applicable rate (default: today)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Complete calculation result with breakdown
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
ValueError: If no applicable rate is found
|
|
243
|
+
|
|
244
|
+
Examples:
|
|
245
|
+
>>> result = IVACalculator.calcular(1000)
|
|
246
|
+
>>> print(f"IVA: ${result['iva']:.2f}")
|
|
247
|
+
IVA: $160.00
|
|
248
|
+
|
|
249
|
+
>>> result = IVACalculator.calcular(1000, "frontera")
|
|
250
|
+
>>> print(f"IVA frontera: ${result['iva']:.2f}")
|
|
251
|
+
IVA frontera: $80.00
|
|
252
|
+
"""
|
|
253
|
+
tasa_obj = cls.get_tasa_vigente(fecha, tipo_tasa)
|
|
254
|
+
|
|
255
|
+
if not tasa_obj:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
f"No se encontró tasa de IVA vigente para tipo {tipo_tasa} y fecha {fecha}"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
tasa = tasa_obj["tasa"]
|
|
261
|
+
iva = base * (tasa / 100)
|
|
262
|
+
total_con_iva = base + iva
|
|
263
|
+
|
|
264
|
+
return IVACalculationResult(
|
|
265
|
+
base=base,
|
|
266
|
+
tasa=tasa,
|
|
267
|
+
iva=iva,
|
|
268
|
+
total_con_iva=total_con_iva,
|
|
269
|
+
tipo_tasa=tipo_tasa,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def calcular_incluido(
|
|
274
|
+
cls,
|
|
275
|
+
total_con_iva: float,
|
|
276
|
+
tipo_tasa: IVATipoTasa = "general",
|
|
277
|
+
fecha: str | datetime | None = None,
|
|
278
|
+
) -> IVACalculationResult:
|
|
279
|
+
"""
|
|
280
|
+
Calculate IVA already included in a total amount
|
|
281
|
+
(Break down IVA when price includes IVA)
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
total_con_iva: Total amount that includes IVA
|
|
285
|
+
tipo_tasa: Rate type (general, frontera, tasa_cero)
|
|
286
|
+
fecha: Date to determine applicable rate (default: today)
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Complete calculation result with breakdown
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
ValueError: If no applicable rate is found
|
|
293
|
+
|
|
294
|
+
Examples:
|
|
295
|
+
>>> result = IVACalculator.calcular_incluido(1160)
|
|
296
|
+
>>> print(f"Base: ${result['base']:.2f}, IVA: ${result['iva']:.2f}")
|
|
297
|
+
Base: $1000.00, IVA: $160.00
|
|
298
|
+
"""
|
|
299
|
+
tasa_obj = cls.get_tasa_vigente(fecha, tipo_tasa)
|
|
300
|
+
|
|
301
|
+
if not tasa_obj:
|
|
302
|
+
raise ValueError("No se encontró tasa de IVA vigente")
|
|
303
|
+
|
|
304
|
+
tasa = tasa_obj["tasa"]
|
|
305
|
+
base = total_con_iva / (1 + tasa / 100)
|
|
306
|
+
iva = total_con_iva - base
|
|
307
|
+
|
|
308
|
+
return IVACalculationResult(
|
|
309
|
+
base=base,
|
|
310
|
+
tasa=tasa,
|
|
311
|
+
iva=iva,
|
|
312
|
+
total_con_iva=total_con_iva,
|
|
313
|
+
tipo_tasa=tipo_tasa,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
@classmethod
|
|
317
|
+
def get_all_tasas(cls) -> list[IVATasa]:
|
|
318
|
+
"""
|
|
319
|
+
Get all historical IVA rates
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
List of all IVA rates (current and historical)
|
|
323
|
+
|
|
324
|
+
Examples:
|
|
325
|
+
>>> tasas = IVACalculator.get_all_tasas()
|
|
326
|
+
>>> print(f"Total tasas: {len(tasas)}")
|
|
327
|
+
Total tasas: 7
|
|
328
|
+
"""
|
|
329
|
+
cls._load_data()
|
|
330
|
+
return cls._data["tasas"].copy()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class IEPSCalculator:
|
|
334
|
+
"""
|
|
335
|
+
IEPS (Special Production and Services Tax) Calculator
|
|
336
|
+
|
|
337
|
+
Calculates IEPS for:
|
|
338
|
+
- Alcoholic beverages (ad valorem)
|
|
339
|
+
- Tobacco products (ad valorem + fixed quota)
|
|
340
|
+
- Fuels (fixed quota)
|
|
341
|
+
- Sugary drinks (fixed quota)
|
|
342
|
+
- High-calorie foods (ad valorem)
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
_data: list[IEPSCategoria] | None = None
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
def _load_data(cls) -> None:
|
|
349
|
+
"""Lazy load IEPS data from JSON file"""
|
|
350
|
+
if cls._data is not None:
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
json_path = (
|
|
354
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
355
|
+
/ "packages"
|
|
356
|
+
/ "shared-data"
|
|
357
|
+
/ "sat"
|
|
358
|
+
/ "impuestos"
|
|
359
|
+
/ "ieps_tasas.json"
|
|
360
|
+
)
|
|
361
|
+
with open(json_path, encoding="utf-8") as f:
|
|
362
|
+
cls._data = json.load(f)
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def get_categoria(cls, categoria: str) -> IEPSCategoria | None:
|
|
366
|
+
"""
|
|
367
|
+
Get an IEPS category by name
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
categoria: Category name (e.g., "bebidas_alcoholicas", "tabacos")
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
IEPS category or None if not found
|
|
374
|
+
|
|
375
|
+
Examples:
|
|
376
|
+
>>> cat = IEPSCalculator.get_categoria("bebidas_alcoholicas")
|
|
377
|
+
>>> print(cat["descripcion"])
|
|
378
|
+
Bebidas con contenido alcohólico
|
|
379
|
+
"""
|
|
380
|
+
cls._load_data()
|
|
381
|
+
return next((c for c in cls._data if c["categoria"] == categoria), None)
|
|
382
|
+
|
|
383
|
+
@classmethod
|
|
384
|
+
def calcular_ad_valorem(cls, base: float, tasa: float) -> IEPSCalculationResult:
|
|
385
|
+
"""
|
|
386
|
+
Calculate IEPS ad-valorem (percentage on value)
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
base: Base value
|
|
390
|
+
tasa: IEPS rate as percentage
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Calculation result
|
|
394
|
+
|
|
395
|
+
Examples:
|
|
396
|
+
>>> result = IEPSCalculator.calcular_ad_valorem(1000, 26.5)
|
|
397
|
+
>>> print(f"IEPS: ${result['ieps']:.2f}")
|
|
398
|
+
IEPS: $265.00
|
|
399
|
+
"""
|
|
400
|
+
ieps = base * (tasa / 100)
|
|
401
|
+
|
|
402
|
+
return IEPSCalculationResult(
|
|
403
|
+
base=base,
|
|
404
|
+
tasa=tasa,
|
|
405
|
+
ieps=ieps,
|
|
406
|
+
tipo_calculo="ad_valorem",
|
|
407
|
+
unidad=None,
|
|
408
|
+
cantidad=None,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
@classmethod
|
|
412
|
+
def calcular_cuota_fija(
|
|
413
|
+
cls, cantidad: float, cuota_por_unidad: float, unidad: str = "litro"
|
|
414
|
+
) -> IEPSCalculationResult:
|
|
415
|
+
"""
|
|
416
|
+
Calculate IEPS by fixed quota (quantity * rate)
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
cantidad: Number of units
|
|
420
|
+
cuota_por_unidad: Fixed quota per unit
|
|
421
|
+
unidad: Unit of measure (default: "litro")
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Calculation result
|
|
425
|
+
|
|
426
|
+
Examples:
|
|
427
|
+
>>> result = IEPSCalculator.calcular_cuota_fija(100, 1.0, "litro")
|
|
428
|
+
>>> print(f"IEPS: ${result['ieps']:.2f}")
|
|
429
|
+
IEPS: $100.00
|
|
430
|
+
"""
|
|
431
|
+
ieps = cantidad * cuota_por_unidad
|
|
432
|
+
|
|
433
|
+
return IEPSCalculationResult(
|
|
434
|
+
base=cantidad,
|
|
435
|
+
tasa=cuota_por_unidad,
|
|
436
|
+
ieps=ieps,
|
|
437
|
+
tipo_calculo="cuota_fija",
|
|
438
|
+
unidad=unidad,
|
|
439
|
+
cantidad=cantidad,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
@classmethod
|
|
443
|
+
def calcular_bebidas_alcoholicas(
|
|
444
|
+
cls, valor: float, grados_alcohol: float
|
|
445
|
+
) -> IEPSCalculationResult:
|
|
446
|
+
"""
|
|
447
|
+
Calculate IEPS for alcoholic beverages based on alcohol content
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
valor: Product value
|
|
451
|
+
grados_alcohol: Alcohol content in degrees (GL)
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Calculation result
|
|
455
|
+
|
|
456
|
+
Examples:
|
|
457
|
+
>>> result = IEPSCalculator.calcular_bebidas_alcoholicas(1000, 5)
|
|
458
|
+
>>> print(f"IEPS cerveza: ${result['ieps']:.2f}")
|
|
459
|
+
IEPS cerveza: $265.00
|
|
460
|
+
|
|
461
|
+
>>> result = IEPSCalculator.calcular_bebidas_alcoholicas(1000, 40)
|
|
462
|
+
>>> print(f"IEPS licor: ${result['ieps']:.2f}")
|
|
463
|
+
IEPS licor: $530.00
|
|
464
|
+
"""
|
|
465
|
+
if grados_alcohol <= 14:
|
|
466
|
+
tasa = 26.5 # Beer and drinks up to 14°
|
|
467
|
+
elif grados_alcohol <= 20:
|
|
468
|
+
tasa = 30.0 # Drinks from 14° to 20°
|
|
469
|
+
else:
|
|
470
|
+
tasa = 53.0 # Drinks over 20°
|
|
471
|
+
|
|
472
|
+
return cls.calcular_ad_valorem(valor, tasa)
|
|
473
|
+
|
|
474
|
+
@classmethod
|
|
475
|
+
def calcular_cigarros(cls, valor: float, numero_cigarros: int) -> IEPSCalculationResult:
|
|
476
|
+
"""
|
|
477
|
+
Calculate IEPS for cigarettes (ad valorem + fixed quota)
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
valor: Sale value
|
|
481
|
+
numero_cigarros: Number of cigarettes
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Calculation result
|
|
485
|
+
|
|
486
|
+
Examples:
|
|
487
|
+
>>> result = IEPSCalculator.calcular_cigarros(100, 20)
|
|
488
|
+
>>> print(f"IEPS cigarros: ${result['ieps']:.2f}")
|
|
489
|
+
IEPS cigarros: $170.16
|
|
490
|
+
"""
|
|
491
|
+
# IEPS = 160% of value + $0.5080 per cigarette
|
|
492
|
+
ieps_ad_valorem = valor * (160 / 100)
|
|
493
|
+
ieps_cuota_fija = numero_cigarros * 0.508
|
|
494
|
+
ieps_total = ieps_ad_valorem + ieps_cuota_fija
|
|
495
|
+
|
|
496
|
+
return IEPSCalculationResult(
|
|
497
|
+
base=valor,
|
|
498
|
+
tasa=160,
|
|
499
|
+
ieps=ieps_total,
|
|
500
|
+
tipo_calculo="ad_valorem",
|
|
501
|
+
unidad=None,
|
|
502
|
+
cantidad=float(numero_cigarros),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
@classmethod
|
|
506
|
+
def get_all_categorias(cls) -> list[IEPSCategoria]:
|
|
507
|
+
"""
|
|
508
|
+
Get all IEPS categories
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
List of all IEPS categories
|
|
512
|
+
|
|
513
|
+
Examples:
|
|
514
|
+
>>> categorias = IEPSCalculator.get_all_categorias()
|
|
515
|
+
>>> print(f"Total categorías: {len(categorias)}")
|
|
516
|
+
Total categorías: 7
|
|
517
|
+
"""
|
|
518
|
+
cls._load_data()
|
|
519
|
+
return cls._data.copy()
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class RetencionCalculator:
|
|
523
|
+
"""
|
|
524
|
+
Tax Withholding Calculator (ISR and IVA)
|
|
525
|
+
|
|
526
|
+
Calculates withholdings for:
|
|
527
|
+
- Professional fees (10% ISR, 2/3 IVA)
|
|
528
|
+
- Rent (10% ISR, 2/3 IVA)
|
|
529
|
+
- Freight (4% ISR, 100% IVA)
|
|
530
|
+
- Other concepts
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
_data: RetencionesData | None = None
|
|
534
|
+
|
|
535
|
+
@classmethod
|
|
536
|
+
def _load_data(cls) -> None:
|
|
537
|
+
"""Lazy load retenciones data from JSON file"""
|
|
538
|
+
if cls._data is not None:
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
json_path = (
|
|
542
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
543
|
+
/ "packages"
|
|
544
|
+
/ "shared-data"
|
|
545
|
+
/ "sat"
|
|
546
|
+
/ "impuestos"
|
|
547
|
+
/ "retenciones.json"
|
|
548
|
+
)
|
|
549
|
+
with open(json_path, encoding="utf-8") as f:
|
|
550
|
+
cls._data = json.load(f)
|
|
551
|
+
|
|
552
|
+
@classmethod
|
|
553
|
+
def get_retencion_isr(cls, concepto: str) -> RetencionISR | None:
|
|
554
|
+
"""
|
|
555
|
+
Get ISR withholding information by concept
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
concepto: Withholding concept (e.g., "honorarios", "arrendamiento")
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
ISR withholding info or None if not found
|
|
562
|
+
|
|
563
|
+
Examples:
|
|
564
|
+
>>> ret = RetencionCalculator.get_retencion_isr("honorarios")
|
|
565
|
+
>>> print(f"Tasa: {ret['tasa']}%")
|
|
566
|
+
Tasa: 10.0%
|
|
567
|
+
"""
|
|
568
|
+
cls._load_data()
|
|
569
|
+
return next(
|
|
570
|
+
(r for r in cls._data["isr_retenciones"] if r["concepto"] == concepto),
|
|
571
|
+
None,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
@classmethod
|
|
575
|
+
def get_retencion_iva(cls, concepto: str) -> RetencionIVA | None:
|
|
576
|
+
"""
|
|
577
|
+
Get IVA withholding information by concept
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
concepto: Withholding concept
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
IVA withholding info or None if not found
|
|
584
|
+
|
|
585
|
+
Examples:
|
|
586
|
+
>>> ret = RetencionCalculator.get_retencion_iva("servicios_profesionales")
|
|
587
|
+
>>> print(f"Tasa: {ret['tasa']}%")
|
|
588
|
+
Tasa: 66.67%
|
|
589
|
+
"""
|
|
590
|
+
cls._load_data()
|
|
591
|
+
return next(
|
|
592
|
+
(r for r in cls._data["iva_retenciones"] if r["concepto"] == concepto),
|
|
593
|
+
None,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
@classmethod
|
|
597
|
+
def calcular_retencion_isr(cls, base: float, concepto: str) -> RetencionCalculationResult:
|
|
598
|
+
"""
|
|
599
|
+
Calculate ISR withholding
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
base: Base amount for withholding
|
|
603
|
+
concepto: Withholding concept
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
Calculation result
|
|
607
|
+
|
|
608
|
+
Raises:
|
|
609
|
+
ValueError: If concept not found or rate is variable
|
|
610
|
+
|
|
611
|
+
Examples:
|
|
612
|
+
>>> result = RetencionCalculator.calcular_retencion_isr(10000, "honorarios")
|
|
613
|
+
>>> print(f"Retención: ${result['retencion']:.2f}")
|
|
614
|
+
Retención: $1000.00
|
|
615
|
+
"""
|
|
616
|
+
retencion = cls.get_retencion_isr(concepto)
|
|
617
|
+
|
|
618
|
+
if not retencion:
|
|
619
|
+
raise ValueError(f"No se encontró retención ISR para concepto: {concepto}")
|
|
620
|
+
|
|
621
|
+
if isinstance(retencion["tasa"], str):
|
|
622
|
+
raise ValueError(f"La tasa de {concepto} es variable. Use método específico.")
|
|
623
|
+
|
|
624
|
+
monto_retencion = base * (retencion["tasa"] / 100)
|
|
625
|
+
|
|
626
|
+
return RetencionCalculationResult(
|
|
627
|
+
concepto=retencion["concepto"],
|
|
628
|
+
base=base,
|
|
629
|
+
tasa=retencion["tasa"],
|
|
630
|
+
retencion=monto_retencion,
|
|
631
|
+
impuesto_base=None,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
@classmethod
|
|
635
|
+
def calcular_retencion_iva(
|
|
636
|
+
cls, iva_trasladado: float, concepto: str
|
|
637
|
+
) -> RetencionCalculationResult:
|
|
638
|
+
"""
|
|
639
|
+
Calculate IVA withholding (typically 2/3 parts)
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
iva_trasladado: IVA amount transferred
|
|
643
|
+
concepto: Withholding concept
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
Calculation result
|
|
647
|
+
|
|
648
|
+
Raises:
|
|
649
|
+
ValueError: If concept not found
|
|
650
|
+
|
|
651
|
+
Examples:
|
|
652
|
+
>>> result = RetencionCalculator.calcular_retencion_iva(160, "servicios_profesionales")
|
|
653
|
+
>>> print(f"Retención IVA: ${result['retencion']:.2f}")
|
|
654
|
+
Retención IVA: $106.67
|
|
655
|
+
"""
|
|
656
|
+
retencion = cls.get_retencion_iva(concepto)
|
|
657
|
+
|
|
658
|
+
if not retencion:
|
|
659
|
+
raise ValueError(f"No se encontró retención IVA para concepto: {concepto}")
|
|
660
|
+
|
|
661
|
+
monto_retencion = iva_trasladado * (retencion["tasa"] / 100)
|
|
662
|
+
|
|
663
|
+
return RetencionCalculationResult(
|
|
664
|
+
concepto=retencion["concepto"],
|
|
665
|
+
base=iva_trasladado,
|
|
666
|
+
tasa=retencion["tasa"],
|
|
667
|
+
retencion=monto_retencion,
|
|
668
|
+
impuesto_base=iva_trasladado,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
@classmethod
|
|
672
|
+
def calcular_honorarios(cls, monto_sin_iva: float) -> RetencionCalculationResult:
|
|
673
|
+
"""
|
|
674
|
+
Calculate withholding for professional fees (10% ISR)
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
monto_sin_iva: Fee amount without IVA
|
|
678
|
+
|
|
679
|
+
Returns:
|
|
680
|
+
Calculation result
|
|
681
|
+
|
|
682
|
+
Examples:
|
|
683
|
+
>>> result = RetencionCalculator.calcular_honorarios(10000)
|
|
684
|
+
>>> print(f"Retención honorarios: ${result['retencion']:.2f}")
|
|
685
|
+
Retención honorarios: $1000.00
|
|
686
|
+
"""
|
|
687
|
+
return cls.calcular_retencion_isr(monto_sin_iva, "honorarios")
|
|
688
|
+
|
|
689
|
+
@classmethod
|
|
690
|
+
def calcular_arrendamiento(cls, monto_sin_iva: float) -> RetencionCalculationResult:
|
|
691
|
+
"""
|
|
692
|
+
Calculate withholding for rent (10% ISR)
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
monto_sin_iva: Rent amount without IVA
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
Calculation result
|
|
699
|
+
|
|
700
|
+
Examples:
|
|
701
|
+
>>> result = RetencionCalculator.calcular_arrendamiento(15000)
|
|
702
|
+
>>> print(f"Retención arrendamiento: ${result['retencion']:.2f}")
|
|
703
|
+
Retención arrendamiento: $1500.00
|
|
704
|
+
"""
|
|
705
|
+
return cls.calcular_retencion_isr(monto_sin_iva, "arrendamiento")
|
|
706
|
+
|
|
707
|
+
@classmethod
|
|
708
|
+
def calcular_fletes(cls, monto_sin_iva: float) -> RetencionCalculationResult:
|
|
709
|
+
"""
|
|
710
|
+
Calculate withholding for freight (4% ISR)
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
monto_sin_iva: Freight amount without IVA
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
Calculation result
|
|
717
|
+
|
|
718
|
+
Examples:
|
|
719
|
+
>>> result = RetencionCalculator.calcular_fletes(10000)
|
|
720
|
+
>>> print(f"Retención fletes: ${result['retencion']:.2f}")
|
|
721
|
+
Retención fletes: $400.00
|
|
722
|
+
"""
|
|
723
|
+
return cls.calcular_retencion_isr(monto_sin_iva, "fletes")
|
|
724
|
+
|
|
725
|
+
@classmethod
|
|
726
|
+
def get_all_retenciones_isr(cls) -> list[RetencionISR]:
|
|
727
|
+
"""
|
|
728
|
+
Get all ISR withholding concepts
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
List of all ISR withholdings
|
|
732
|
+
|
|
733
|
+
Examples:
|
|
734
|
+
>>> retenciones = RetencionCalculator.get_all_retenciones_isr()
|
|
735
|
+
>>> print(f"Total retenciones ISR: {len(retenciones)}")
|
|
736
|
+
Total retenciones ISR: 9
|
|
737
|
+
"""
|
|
738
|
+
cls._load_data()
|
|
739
|
+
return cls._data["isr_retenciones"].copy()
|
|
740
|
+
|
|
741
|
+
@classmethod
|
|
742
|
+
def get_all_retenciones_iva(cls) -> list[RetencionIVA]:
|
|
743
|
+
"""
|
|
744
|
+
Get all IVA withholding concepts
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
List of all IVA withholdings
|
|
748
|
+
|
|
749
|
+
Examples:
|
|
750
|
+
>>> retenciones = RetencionCalculator.get_all_retenciones_iva()
|
|
751
|
+
>>> print(f"Total retenciones IVA: {len(retenciones)}")
|
|
752
|
+
Total retenciones IVA: 4
|
|
753
|
+
"""
|
|
754
|
+
cls._load_data()
|
|
755
|
+
return cls._data["iva_retenciones"].copy()
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
class ImpuestosLocalesCalculator:
|
|
759
|
+
"""
|
|
760
|
+
State and Municipal Tax Calculator
|
|
761
|
+
|
|
762
|
+
Calculates local taxes:
|
|
763
|
+
- Payroll tax (varies by state, 2-3%)
|
|
764
|
+
- Lodging tax (varies by state, 2-5%)
|
|
765
|
+
"""
|
|
766
|
+
|
|
767
|
+
_data: ImpuestosLocalesData | None = None
|
|
768
|
+
|
|
769
|
+
@classmethod
|
|
770
|
+
def _load_data(cls) -> None:
|
|
771
|
+
"""Lazy load impuestos locales data from JSON file"""
|
|
772
|
+
if cls._data is not None:
|
|
773
|
+
return
|
|
774
|
+
|
|
775
|
+
json_path = (
|
|
776
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
777
|
+
/ "packages"
|
|
778
|
+
/ "shared-data"
|
|
779
|
+
/ "sat"
|
|
780
|
+
/ "impuestos"
|
|
781
|
+
/ "impuestos_locales.json"
|
|
782
|
+
)
|
|
783
|
+
with open(json_path, encoding="utf-8") as f:
|
|
784
|
+
cls._data = json.load(f)
|
|
785
|
+
|
|
786
|
+
@classmethod
|
|
787
|
+
def get_impuesto_nomina(cls, cve_estado: str) -> ImpuestoEstatal | None:
|
|
788
|
+
"""
|
|
789
|
+
Get payroll tax rate for a state
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
cve_estado: State code (01-32)
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
State tax info or None if not found
|
|
796
|
+
|
|
797
|
+
Examples:
|
|
798
|
+
>>> impuesto = ImpuestosLocalesCalculator.get_impuesto_nomina("09")
|
|
799
|
+
>>> print(f"{impuesto['estado']}: {impuesto['tasa']}%")
|
|
800
|
+
Ciudad de México: 3.0%
|
|
801
|
+
"""
|
|
802
|
+
cls._load_data()
|
|
803
|
+
cve_padded = cve_estado.zfill(2)
|
|
804
|
+
return next(
|
|
805
|
+
(i for i in cls._data["impuesto_nomina"] if i["cve_estado"] == cve_padded),
|
|
806
|
+
None,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
@classmethod
|
|
810
|
+
def calcular_impuesto_nomina(cls, total_percepciones: float, cve_estado: str) -> float:
|
|
811
|
+
"""
|
|
812
|
+
Calculate payroll tax
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
total_percepciones: Total payroll for the period
|
|
816
|
+
cve_estado: State code
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
Tax amount
|
|
820
|
+
|
|
821
|
+
Raises:
|
|
822
|
+
ValueError: If state code not found
|
|
823
|
+
|
|
824
|
+
Examples:
|
|
825
|
+
>>> impuesto = ImpuestosLocalesCalculator.calcular_impuesto_nomina(100000, "09")
|
|
826
|
+
>>> print(f"Impuesto sobre nómina CDMX: ${impuesto:.2f}")
|
|
827
|
+
Impuesto sobre nómina CDMX: $3000.00
|
|
828
|
+
"""
|
|
829
|
+
impuesto = cls.get_impuesto_nomina(cve_estado)
|
|
830
|
+
|
|
831
|
+
if not impuesto:
|
|
832
|
+
raise ValueError(
|
|
833
|
+
f"No se encontró tasa de impuesto sobre nómina para estado: {cve_estado}"
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
return total_percepciones * (impuesto["tasa"] / 100)
|
|
837
|
+
|
|
838
|
+
@classmethod
|
|
839
|
+
def get_impuesto_hospedaje(cls, cve_estado: str) -> ImpuestoEstatal | None:
|
|
840
|
+
"""
|
|
841
|
+
Get lodging tax rate for a state
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
cve_estado: State code (01-32)
|
|
845
|
+
|
|
846
|
+
Returns:
|
|
847
|
+
State tax info or None if not found
|
|
848
|
+
|
|
849
|
+
Examples:
|
|
850
|
+
>>> impuesto = ImpuestosLocalesCalculator.get_impuesto_hospedaje("23")
|
|
851
|
+
>>> print(f"{impuesto['estado']}: {impuesto['tasa']}%")
|
|
852
|
+
Quintana Roo: 3.0%
|
|
853
|
+
"""
|
|
854
|
+
cls._load_data()
|
|
855
|
+
cve_padded = cve_estado.zfill(2)
|
|
856
|
+
return next(
|
|
857
|
+
(i for i in cls._data["impuesto_hospedaje"] if i["cve_estado"] == cve_padded),
|
|
858
|
+
None,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
@classmethod
|
|
862
|
+
def calcular_impuesto_hospedaje(cls, monto_hospedaje: float, cve_estado: str) -> float:
|
|
863
|
+
"""
|
|
864
|
+
Calculate lodging tax
|
|
865
|
+
|
|
866
|
+
Args:
|
|
867
|
+
monto_hospedaje: Lodging service amount
|
|
868
|
+
cve_estado: State code
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
Tax amount
|
|
872
|
+
|
|
873
|
+
Raises:
|
|
874
|
+
ValueError: If state code not found
|
|
875
|
+
|
|
876
|
+
Examples:
|
|
877
|
+
>>> impuesto = ImpuestosLocalesCalculator.calcular_impuesto_hospedaje(5000, "23")
|
|
878
|
+
>>> print(f"Impuesto sobre hospedaje: ${impuesto:.2f}")
|
|
879
|
+
Impuesto sobre hospedaje: $150.00
|
|
880
|
+
"""
|
|
881
|
+
impuesto = cls.get_impuesto_hospedaje(cve_estado)
|
|
882
|
+
|
|
883
|
+
if not impuesto:
|
|
884
|
+
raise ValueError(
|
|
885
|
+
f"No se encontró tasa de impuesto sobre hospedaje para estado: {cve_estado}"
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
return monto_hospedaje * (impuesto["tasa"] / 100)
|
|
889
|
+
|
|
890
|
+
@classmethod
|
|
891
|
+
def get_all_impuestos_nomina(cls) -> list[ImpuestoEstatal]:
|
|
892
|
+
"""
|
|
893
|
+
Get all payroll tax rates
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
List of all state payroll taxes
|
|
897
|
+
|
|
898
|
+
Examples:
|
|
899
|
+
>>> impuestos = ImpuestosLocalesCalculator.get_all_impuestos_nomina()
|
|
900
|
+
>>> print(f"Total estados: {len(impuestos)}")
|
|
901
|
+
Total estados: 32
|
|
902
|
+
"""
|
|
903
|
+
cls._load_data()
|
|
904
|
+
return cls._data["impuesto_nomina"].copy()
|
|
905
|
+
|
|
906
|
+
@classmethod
|
|
907
|
+
def get_all_impuestos_hospedaje(cls) -> list[ImpuestoEstatal]:
|
|
908
|
+
"""
|
|
909
|
+
Get all lodging tax rates
|
|
910
|
+
|
|
911
|
+
Returns:
|
|
912
|
+
List of all state lodging taxes
|
|
913
|
+
|
|
914
|
+
Examples:
|
|
915
|
+
>>> impuestos = ImpuestosLocalesCalculator.get_all_impuestos_hospedaje()
|
|
916
|
+
>>> print(f"Total estados: {len(impuestos)}")
|
|
917
|
+
Total estados: 32
|
|
918
|
+
"""
|
|
919
|
+
cls._load_data()
|
|
920
|
+
return cls._data["impuesto_hospedaje"].copy()
|