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,76 @@
|
|
|
1
|
+
"""Catálogo c_RegistroIdentTribReceptor - Tipos de Registro de Identificación Tributaria"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RegistroIdentTribCatalog:
|
|
9
|
+
"""Catálogo de tipos de registro tributario del receptor extranjero"""
|
|
10
|
+
|
|
11
|
+
_data: list[dict] | None = None
|
|
12
|
+
_tipo_by_code: dict[str, dict] | None = None
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def _load_data(cls) -> None:
|
|
16
|
+
"""Carga los datos del catálogo desde el archivo JSON compartido"""
|
|
17
|
+
if cls._data is None:
|
|
18
|
+
current_file = Path(__file__)
|
|
19
|
+
shared_data_path = (
|
|
20
|
+
current_file.parent.parent.parent.parent.parent.parent
|
|
21
|
+
/ "shared-data"
|
|
22
|
+
/ "sat"
|
|
23
|
+
/ "comercio_exterior"
|
|
24
|
+
/ "registro_ident_trib.json"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
with open(shared_data_path, encoding="utf-8") as f:
|
|
28
|
+
data = json.load(f)
|
|
29
|
+
# Handle both list and dict formats
|
|
30
|
+
cls._data = data if isinstance(data, list) else data.get("tipos_registro", data)
|
|
31
|
+
|
|
32
|
+
cls._tipo_by_code = {item["code"]: item for item in cls._data}
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_tipo(cls, code: str) -> dict | None:
|
|
36
|
+
"""Obtiene un tipo de registro por su código"""
|
|
37
|
+
cls._load_data()
|
|
38
|
+
return cls._tipo_by_code.get(code)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def is_valid(cls, code: str) -> bool:
|
|
42
|
+
"""Verifica si un código de tipo es válido"""
|
|
43
|
+
return cls.get_tipo(code) is not None
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def validate_tax_id(cls, tipo_registro: str, num_reg_id_trib: str) -> dict:
|
|
47
|
+
"""
|
|
48
|
+
Valida un número de identificación tributaria según su tipo
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
tipo_registro: Código del tipo de registro
|
|
52
|
+
num_reg_id_trib: Número de identificación tributaria
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict con 'valid' (bool) y 'errors' (list)
|
|
56
|
+
"""
|
|
57
|
+
tipo = cls.get_tipo(tipo_registro)
|
|
58
|
+
if not tipo:
|
|
59
|
+
return {"valid": False, "errors": ["Tipo de registro no válido"]}
|
|
60
|
+
|
|
61
|
+
errors = []
|
|
62
|
+
|
|
63
|
+
# Validar formato si está definido
|
|
64
|
+
format_pattern = tipo.get("format_pattern")
|
|
65
|
+
if format_pattern:
|
|
66
|
+
if not re.match(format_pattern, num_reg_id_trib):
|
|
67
|
+
format_desc = tipo.get("format_description", "formato no válido")
|
|
68
|
+
errors.append(f"Formato incorrecto. Esperado: {format_desc}")
|
|
69
|
+
|
|
70
|
+
return {"valid": len(errors) == 0, "errors": errors}
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def get_all(cls) -> list[dict]:
|
|
74
|
+
"""Retorna todos los tipos de registro tributario"""
|
|
75
|
+
cls._load_data()
|
|
76
|
+
return cls._data.copy()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Catálogo c_UnidadAduana - Unidades de Medida Aduanera"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UnidadAduanaCatalog:
|
|
8
|
+
"""Catálogo de unidades de medida reconocidas por aduanas"""
|
|
9
|
+
|
|
10
|
+
_data: list[dict] | None = None
|
|
11
|
+
_unidad_by_code: dict[str, dict] | None = None
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def _load_data(cls) -> None:
|
|
15
|
+
"""Carga los datos del catálogo desde el archivo JSON compartido"""
|
|
16
|
+
if cls._data is None:
|
|
17
|
+
current_file = Path(__file__)
|
|
18
|
+
shared_data_path = (
|
|
19
|
+
current_file.parent.parent.parent.parent.parent.parent
|
|
20
|
+
/ "shared-data"
|
|
21
|
+
/ "sat"
|
|
22
|
+
/ "comercio_exterior"
|
|
23
|
+
/ "unidades_aduana.json"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
with open(shared_data_path, encoding="utf-8") as f:
|
|
27
|
+
data = json.load(f)
|
|
28
|
+
# Handle both list and dict formats
|
|
29
|
+
cls._data = data if isinstance(data, list) else data.get("unidades", data)
|
|
30
|
+
|
|
31
|
+
cls._unidad_by_code = {item["code"]: item for item in cls._data}
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_unidad(cls, code: str) -> dict | None:
|
|
35
|
+
"""Obtiene una unidad de medida por su código"""
|
|
36
|
+
cls._load_data()
|
|
37
|
+
return cls._unidad_by_code.get(code)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def is_valid(cls, code: str) -> bool:
|
|
41
|
+
"""Verifica si un código de unidad es válido"""
|
|
42
|
+
return cls.get_unidad(code) is not None
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_by_type(cls, unit_type: str) -> list[dict]:
|
|
46
|
+
"""Obtiene unidades por tipo (weight, volume, length, area, unit, container)"""
|
|
47
|
+
cls._load_data()
|
|
48
|
+
return [item for item in cls._data if item.get("type") == unit_type]
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def get_all(cls) -> list[dict]:
|
|
52
|
+
"""Retorna todas las unidades de medida aduanera"""
|
|
53
|
+
cls._load_data()
|
|
54
|
+
return cls._data.copy()
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validador completo para CFDI con Complemento de Comercio Exterior 2.0
|
|
3
|
+
|
|
4
|
+
Integra todos los catálogos para validación completa de un CFDI con
|
|
5
|
+
el complemento de comercio exterior.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .claves_pedimento import ClavePedimentoCatalog
|
|
9
|
+
from .estados import EstadoCatalog
|
|
10
|
+
from .incoterms import IncotermsValidator
|
|
11
|
+
from .monedas import MonedaCatalog
|
|
12
|
+
from .motivos_traslado import MotivoTrasladoCatalog
|
|
13
|
+
from .paises import PaisCatalog
|
|
14
|
+
from .registro_ident_trib import RegistroIdentTribCatalog
|
|
15
|
+
from .unidades_aduana import UnidadAduanaCatalog
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ComercioExteriorValidator:
|
|
19
|
+
"""Validador completo para CFDI con Complemento Comercio Exterior 2.0"""
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def validate(cls, cfdi_ce: dict) -> dict:
|
|
23
|
+
"""
|
|
24
|
+
Valida un CFDI completo con Complemento de Comercio Exterior
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
cfdi_ce: Dict con todos los campos del CFDI y complemento
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dict con 'valid' (bool), 'errors' (list), 'warnings' (list)
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> cfdi_ce = {
|
|
34
|
+
... 'tipo_comprobante': 'I',
|
|
35
|
+
... 'incoterm': 'CIF',
|
|
36
|
+
... 'clave_pedimento': 'A1',
|
|
37
|
+
... 'moneda': 'USD',
|
|
38
|
+
... 'tipo_cambio_usd': 1.0,
|
|
39
|
+
... 'total_usd': 50000.00,
|
|
40
|
+
... 'mercancias': [...]
|
|
41
|
+
... }
|
|
42
|
+
>>> result = ComercioExteriorValidator.validate(cfdi_ce)
|
|
43
|
+
>>> if not result['valid']:
|
|
44
|
+
... for error in result['errors']:
|
|
45
|
+
... print(f"Error: {error}")
|
|
46
|
+
"""
|
|
47
|
+
errors = []
|
|
48
|
+
warnings = []
|
|
49
|
+
|
|
50
|
+
# 1. Validar INCOTERM
|
|
51
|
+
incoterm = cfdi_ce.get("incoterm")
|
|
52
|
+
if not incoterm:
|
|
53
|
+
errors.append("INCOTERM es obligatorio")
|
|
54
|
+
elif not IncotermsValidator.is_valid(incoterm):
|
|
55
|
+
errors.append(f"INCOTERM {incoterm} no válido")
|
|
56
|
+
|
|
57
|
+
# 2. Validar Clave de Pedimento
|
|
58
|
+
clave_pedimento = cfdi_ce.get("clave_pedimento")
|
|
59
|
+
if not clave_pedimento:
|
|
60
|
+
errors.append("ClavePedimento es obligatoria")
|
|
61
|
+
elif not ClavePedimentoCatalog.is_valid(clave_pedimento):
|
|
62
|
+
errors.append(f"ClavePedimento {clave_pedimento} no válida")
|
|
63
|
+
|
|
64
|
+
# 3. Validar Moneda y conversión USD
|
|
65
|
+
moneda_result = MonedaCatalog.validate_conversion_usd(
|
|
66
|
+
{
|
|
67
|
+
"moneda": cfdi_ce.get("moneda"),
|
|
68
|
+
"total": cfdi_ce.get("total"),
|
|
69
|
+
"tipo_cambio_usd": cfdi_ce.get("tipo_cambio_usd"),
|
|
70
|
+
"total_usd": cfdi_ce.get("total_usd"),
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
errors.extend(moneda_result["errors"])
|
|
74
|
+
|
|
75
|
+
# 4. Validar Mercancías
|
|
76
|
+
mercancias = cfdi_ce.get("mercancias", [])
|
|
77
|
+
if not mercancias:
|
|
78
|
+
errors.append("Debe incluir al menos una mercancía")
|
|
79
|
+
else:
|
|
80
|
+
for i, merc in enumerate(mercancias):
|
|
81
|
+
merc_errors = cls._validate_mercancia(merc, i)
|
|
82
|
+
errors.extend(merc_errors)
|
|
83
|
+
|
|
84
|
+
# 5. Validar Receptor (dirección extranjera)
|
|
85
|
+
receptor = cfdi_ce.get("receptor", {})
|
|
86
|
+
if receptor:
|
|
87
|
+
receptor_result = cls._validate_receptor(receptor)
|
|
88
|
+
errors.extend(receptor_result["errors"])
|
|
89
|
+
|
|
90
|
+
# 6. Validar Motivo Traslado (solo si tipo comprobante = T)
|
|
91
|
+
tipo_comprobante = cfdi_ce.get("tipo_comprobante")
|
|
92
|
+
if tipo_comprobante == "T":
|
|
93
|
+
motivo_traslado = cfdi_ce.get("motivo_traslado")
|
|
94
|
+
if not motivo_traslado:
|
|
95
|
+
errors.append("MotivoTraslado es obligatorio para CFDI tipo T")
|
|
96
|
+
elif not MotivoTrasladoCatalog.is_valid(motivo_traslado):
|
|
97
|
+
errors.append(f"MotivoTraslado {motivo_traslado} no válido")
|
|
98
|
+
elif MotivoTrasladoCatalog.requires_propietario(motivo_traslado):
|
|
99
|
+
propietarios = cfdi_ce.get("propietarios", [])
|
|
100
|
+
if not propietarios:
|
|
101
|
+
errors.append("MotivoTraslado 05 requiere al menos un Propietario")
|
|
102
|
+
|
|
103
|
+
# 7. Certificado de Origen (opcional pero validar si presente)
|
|
104
|
+
certificado_origen = cfdi_ce.get("certificado_origen")
|
|
105
|
+
if certificado_origen and certificado_origen not in ["0", "1"]:
|
|
106
|
+
errors.append("CertificadoOrigen debe ser 0 o 1")
|
|
107
|
+
|
|
108
|
+
return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings}
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def _validate_mercancia(cls, mercancia: dict, index: int) -> list[str]:
|
|
112
|
+
"""Valida una mercancía individual"""
|
|
113
|
+
errors = []
|
|
114
|
+
prefix = f"Mercancía[{index}]"
|
|
115
|
+
|
|
116
|
+
# Fracción arancelaria (omitida por ahora, requiere TIGIE/SQLite)
|
|
117
|
+
fraccion = mercancia.get("fraccion_arancelaria")
|
|
118
|
+
if not fraccion:
|
|
119
|
+
errors.append(f"{prefix}: FraccionArancelaria es obligatoria")
|
|
120
|
+
elif len(fraccion) not in [8, 10]:
|
|
121
|
+
errors.append(f"{prefix}: FraccionArancelaria debe tener 8 o 10 dígitos")
|
|
122
|
+
|
|
123
|
+
# Unidad de medida aduanera
|
|
124
|
+
unidad_aduana = mercancia.get("unidad_aduana")
|
|
125
|
+
if not unidad_aduana:
|
|
126
|
+
errors.append(f"{prefix}: UnidadAduana es obligatoria")
|
|
127
|
+
elif not UnidadAduanaCatalog.is_valid(unidad_aduana):
|
|
128
|
+
errors.append(f"{prefix}: UnidadAduana {unidad_aduana} no válida")
|
|
129
|
+
|
|
130
|
+
# Cantidad
|
|
131
|
+
cantidad = mercancia.get("cantidad_aduana")
|
|
132
|
+
if not cantidad or cantidad <= 0:
|
|
133
|
+
errors.append(f"{prefix}: CantidadAduana debe ser mayor a 0")
|
|
134
|
+
|
|
135
|
+
# Valor unitario
|
|
136
|
+
valor_unitario = mercancia.get("valor_unitario_aduana")
|
|
137
|
+
if not valor_unitario or valor_unitario <= 0:
|
|
138
|
+
errors.append(f"{prefix}: ValorUnitarioAduana debe ser mayor a 0")
|
|
139
|
+
|
|
140
|
+
# País de origen
|
|
141
|
+
pais_origen = mercancia.get("pais_origen")
|
|
142
|
+
if not pais_origen:
|
|
143
|
+
errors.append(f"{prefix}: PaisOrigen es obligatorio")
|
|
144
|
+
elif not PaisCatalog.is_valid(pais_origen):
|
|
145
|
+
errors.append(f"{prefix}: PaisOrigen {pais_origen} no válido")
|
|
146
|
+
|
|
147
|
+
return errors
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def _validate_receptor(cls, receptor: dict) -> dict:
|
|
151
|
+
"""Valida los datos del receptor extranjero"""
|
|
152
|
+
errors = []
|
|
153
|
+
|
|
154
|
+
# Validar país
|
|
155
|
+
pais = receptor.get("pais")
|
|
156
|
+
if not pais:
|
|
157
|
+
errors.append("Receptor.Pais es obligatorio")
|
|
158
|
+
elif not PaisCatalog.is_valid(pais):
|
|
159
|
+
errors.append(f"Receptor.Pais {pais} no válido")
|
|
160
|
+
|
|
161
|
+
# Validar estado (obligatorio para USA/CAN)
|
|
162
|
+
if pais:
|
|
163
|
+
address_result = EstadoCatalog.validate_foreign_address(
|
|
164
|
+
{"pais": pais, "estado": receptor.get("estado", "")}
|
|
165
|
+
)
|
|
166
|
+
errors.extend(address_result["errors"])
|
|
167
|
+
|
|
168
|
+
# Validar tipo y número de identificación tributaria
|
|
169
|
+
tipo_reg = receptor.get("tipo_registro_trib")
|
|
170
|
+
num_reg = receptor.get("num_reg_id_trib")
|
|
171
|
+
|
|
172
|
+
if tipo_reg and num_reg:
|
|
173
|
+
tax_id_result = RegistroIdentTribCatalog.validate_tax_id(tipo_reg, num_reg)
|
|
174
|
+
errors.extend(tax_id_result["errors"])
|
|
175
|
+
|
|
176
|
+
return {"errors": errors}
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def validate_quick(cls, field: str, value: str) -> bool:
|
|
180
|
+
"""
|
|
181
|
+
Validación rápida de un campo individual
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
field: Nombre del campo (incoterm, clave_pedimento, etc.)
|
|
185
|
+
value: Valor a validar
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True si el valor es válido
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> ComercioExteriorValidator.validate_quick('incoterm', 'FOB')
|
|
192
|
+
True
|
|
193
|
+
>>> ComercioExteriorValidator.validate_quick('clave_pedimento', 'A1')
|
|
194
|
+
True
|
|
195
|
+
"""
|
|
196
|
+
validators = {
|
|
197
|
+
"incoterm": IncotermsValidator.is_valid,
|
|
198
|
+
"clave_pedimento": ClavePedimentoCatalog.is_valid,
|
|
199
|
+
"unidad_aduana": UnidadAduanaCatalog.is_valid,
|
|
200
|
+
"motivo_traslado": MotivoTrasladoCatalog.is_valid,
|
|
201
|
+
"tipo_registro_trib": RegistroIdentTribCatalog.is_valid,
|
|
202
|
+
"moneda": MonedaCatalog.is_valid,
|
|
203
|
+
"pais": PaisCatalog.is_valid,
|
|
204
|
+
"estado_usa": lambda v: EstadoCatalog.is_valid(v, "USA"),
|
|
205
|
+
"provincia_canada": lambda v: EstadoCatalog.is_valid(v, "CAN"),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
validator = validators.get(field)
|
|
209
|
+
if not validator:
|
|
210
|
+
raise ValueError(f"Campo {field} no soportado para validación")
|
|
211
|
+
|
|
212
|
+
return validator(value)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Catálogos SAT Nómina 1.2"""
|
|
2
|
+
|
|
3
|
+
from .banco import BancoCatalog
|
|
4
|
+
from .periodicidad_pago import PeriodicidadPagoCatalog
|
|
5
|
+
from .riesgo_puesto import RiesgoPuestoCatalog
|
|
6
|
+
from .tipo_contrato import TipoContratoCatalog
|
|
7
|
+
from .tipo_jornada import TipoJornadaCatalog
|
|
8
|
+
from .tipo_nomina import TipoNominaCatalog
|
|
9
|
+
from .tipo_regimen import TipoRegimenCatalog
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"TipoNominaCatalog",
|
|
13
|
+
"TipoContratoCatalog",
|
|
14
|
+
"TipoJornadaCatalog",
|
|
15
|
+
"TipoRegimenCatalog",
|
|
16
|
+
"PeriodicidadPagoCatalog",
|
|
17
|
+
"RiesgoPuestoCatalog",
|
|
18
|
+
"BancoCatalog",
|
|
19
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Catálogo c_Banco"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BancoCatalog:
|
|
8
|
+
_data: list[dict] | None = None
|
|
9
|
+
_by_code: dict[str, dict] | None = None
|
|
10
|
+
_by_name: dict[str, dict] | None = None
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def _load_data(cls) -> None:
|
|
14
|
+
if cls._data is None:
|
|
15
|
+
path = (
|
|
16
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
17
|
+
/ "shared-data"
|
|
18
|
+
/ "sat"
|
|
19
|
+
/ "nomina_1.2"
|
|
20
|
+
/ "banco.json"
|
|
21
|
+
)
|
|
22
|
+
with open(path, encoding="utf-8") as f:
|
|
23
|
+
data = json.load(f)
|
|
24
|
+
# Handle both list and dict formats
|
|
25
|
+
cls._data = data if isinstance(data, list) else data.get("bancos", data)
|
|
26
|
+
cls._by_code = {item["code"]: item for item in cls._data}
|
|
27
|
+
cls._by_name = {item["name"]: item for item in cls._data}
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_banco(cls, code: str) -> dict | None:
|
|
31
|
+
"""Obtiene banco por código"""
|
|
32
|
+
cls._load_data()
|
|
33
|
+
return cls._by_code.get(code)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_by_name(cls, name: str) -> dict | None:
|
|
37
|
+
"""Obtiene banco por nombre corto"""
|
|
38
|
+
cls._load_data()
|
|
39
|
+
return cls._by_name.get(name)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def is_valid(cls, code: str) -> bool:
|
|
43
|
+
"""Verifica si un código de banco es válido"""
|
|
44
|
+
return cls.get_banco(code) is not None
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def get_all(cls) -> list[dict]:
|
|
48
|
+
"""Obtiene todos los bancos"""
|
|
49
|
+
cls._load_data()
|
|
50
|
+
return cls._data.copy()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Catálogo c_PeriodicidadPago"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PeriodicidadPagoCatalog:
|
|
8
|
+
_data: list[dict] | None = None
|
|
9
|
+
_by_code: dict[str, dict] | None = None
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def _load_data(cls) -> None:
|
|
13
|
+
if cls._data is None:
|
|
14
|
+
path = (
|
|
15
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
16
|
+
/ "shared-data"
|
|
17
|
+
/ "sat"
|
|
18
|
+
/ "nomina_1.2"
|
|
19
|
+
/ "periodicidad_pago.json"
|
|
20
|
+
)
|
|
21
|
+
with open(path, encoding="utf-8") as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
# Handle both list and dict formats
|
|
24
|
+
cls._data = data if isinstance(data, list) else data.get("periodicidades", data)
|
|
25
|
+
cls._by_code = {item["code"]: item for item in cls._data}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_periodicidad(cls, code: str) -> dict | None:
|
|
29
|
+
"""Obtiene periodicidad de pago por código"""
|
|
30
|
+
cls._load_data()
|
|
31
|
+
return cls._by_code.get(code)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def is_valid(cls, code: str) -> bool:
|
|
35
|
+
"""Verifica si un código de periodicidad es válido"""
|
|
36
|
+
return cls.get_periodicidad(code) is not None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_all(cls) -> list[dict]:
|
|
40
|
+
"""Obtiene todas las periodicidades"""
|
|
41
|
+
cls._load_data()
|
|
42
|
+
return cls._data.copy()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_days(cls, code: str) -> int | None:
|
|
46
|
+
"""Obtiene el número de días de la periodicidad"""
|
|
47
|
+
periodicidad = cls.get_periodicidad(code)
|
|
48
|
+
return periodicidad.get("days") if periodicidad else None
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Catálogo c_RiesgoPuesto"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RiesgoPuestoCatalog:
|
|
8
|
+
_data: list[dict] | None = None
|
|
9
|
+
_by_code: dict[str, dict] | None = None
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def _load_data(cls) -> None:
|
|
13
|
+
if cls._data is None:
|
|
14
|
+
path = (
|
|
15
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
16
|
+
/ "shared-data"
|
|
17
|
+
/ "sat"
|
|
18
|
+
/ "nomina_1.2"
|
|
19
|
+
/ "riesgo_puesto.json"
|
|
20
|
+
)
|
|
21
|
+
with open(path, encoding="utf-8") as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
# Handle both list and dict formats
|
|
24
|
+
cls._data = data if isinstance(data, list) else data.get("riesgos", data)
|
|
25
|
+
cls._by_code = {item["code"]: item for item in cls._data}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_riesgo(cls, code: str) -> dict | None:
|
|
29
|
+
"""Obtiene nivel de riesgo por código"""
|
|
30
|
+
cls._load_data()
|
|
31
|
+
return cls._by_code.get(code)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def is_valid(cls, code: str) -> bool:
|
|
35
|
+
"""Verifica si un código de riesgo es válido"""
|
|
36
|
+
return cls.get_riesgo(code) is not None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_all(cls) -> list[dict]:
|
|
40
|
+
"""Obtiene todos los niveles de riesgo"""
|
|
41
|
+
cls._load_data()
|
|
42
|
+
return cls._data.copy()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_prima_media(cls, code: str) -> float | None:
|
|
46
|
+
"""Obtiene la prima media del nivel de riesgo"""
|
|
47
|
+
riesgo = cls.get_riesgo(code)
|
|
48
|
+
return riesgo.get("prima_media") if riesgo else None
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def validate_prima(cls, code: str, prima: float) -> bool:
|
|
52
|
+
"""Valida que la prima esté en el rango permitido"""
|
|
53
|
+
riesgo = cls.get_riesgo(code)
|
|
54
|
+
if not riesgo:
|
|
55
|
+
return False
|
|
56
|
+
return riesgo["prima_min"] <= prima <= riesgo["prima_max"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Catálogo c_TipoContrato"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TipoContratoCatalog:
|
|
8
|
+
_data: list[dict] | None = None
|
|
9
|
+
_by_code: dict[str, dict] | None = None
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def _load_data(cls) -> None:
|
|
13
|
+
if cls._data is None:
|
|
14
|
+
path = (
|
|
15
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
16
|
+
/ "shared-data"
|
|
17
|
+
/ "sat"
|
|
18
|
+
/ "nomina_1.2"
|
|
19
|
+
/ "tipo_contrato.json"
|
|
20
|
+
)
|
|
21
|
+
with open(path, encoding="utf-8") as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
# Handle both list and dict formats
|
|
24
|
+
cls._data = data if isinstance(data, list) else data.get("contratos", data)
|
|
25
|
+
cls._by_code = {item["code"]: item for item in cls._data}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_contrato(cls, code: str) -> dict | None:
|
|
29
|
+
"""Obtiene tipo de contrato por código"""
|
|
30
|
+
cls._load_data()
|
|
31
|
+
return cls._by_code.get(code)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def is_valid(cls, code: str) -> bool:
|
|
35
|
+
"""Verifica si un código de contrato es válido"""
|
|
36
|
+
return cls.get_contrato(code) is not None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_all(cls) -> list[dict]:
|
|
40
|
+
"""Obtiene todos los tipos de contrato"""
|
|
41
|
+
cls._load_data()
|
|
42
|
+
return cls._data.copy()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def is_indeterminado(cls, code: str) -> bool:
|
|
46
|
+
"""Verifica si es contrato por tiempo indeterminado"""
|
|
47
|
+
return code == "01"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Catálogo c_TipoJornada"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TipoJornadaCatalog:
|
|
8
|
+
_data: list[dict] | None = None
|
|
9
|
+
_by_code: dict[str, dict] | None = None
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def _load_data(cls) -> None:
|
|
13
|
+
if cls._data is None:
|
|
14
|
+
path = (
|
|
15
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
16
|
+
/ "shared-data"
|
|
17
|
+
/ "sat"
|
|
18
|
+
/ "nomina_1.2"
|
|
19
|
+
/ "tipo_jornada.json"
|
|
20
|
+
)
|
|
21
|
+
with open(path, encoding="utf-8") as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
# Handle both list and dict formats
|
|
24
|
+
cls._data = data if isinstance(data, list) else data.get("jornadas", data)
|
|
25
|
+
cls._by_code = {item["code"]: item for item in cls._data}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_jornada(cls, code: str) -> dict | None:
|
|
29
|
+
"""Obtiene tipo de jornada por código"""
|
|
30
|
+
cls._load_data()
|
|
31
|
+
return cls._by_code.get(code)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def is_valid(cls, code: str) -> bool:
|
|
35
|
+
"""Verifica si un código de jornada es válido"""
|
|
36
|
+
return cls.get_jornada(code) is not None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_all(cls) -> list[dict]:
|
|
40
|
+
"""Obtiene todos los tipos de jornada"""
|
|
41
|
+
cls._load_data()
|
|
42
|
+
return cls._data.copy()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Catálogo c_TipoNomina"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TipoNominaCatalog:
|
|
8
|
+
_data: list[dict] | None = None
|
|
9
|
+
_by_code: dict[str, dict] | None = None
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def _load_data(cls) -> None:
|
|
13
|
+
if cls._data is None:
|
|
14
|
+
path = (
|
|
15
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
16
|
+
/ "shared-data"
|
|
17
|
+
/ "sat"
|
|
18
|
+
/ "nomina_1.2"
|
|
19
|
+
/ "tipo_nomina.json"
|
|
20
|
+
)
|
|
21
|
+
with open(path, encoding="utf-8") as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
# Handle both list and dict formats
|
|
24
|
+
cls._data = data if isinstance(data, list) else data.get("tipos", data)
|
|
25
|
+
cls._by_code = {item["code"]: item for item in cls._data}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_tipo(cls, code: str) -> dict | None:
|
|
29
|
+
"""Obtiene tipo de nómina por código"""
|
|
30
|
+
cls._load_data()
|
|
31
|
+
return cls._by_code.get(code)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def is_valid(cls, code: str) -> bool:
|
|
35
|
+
"""Verifica si un código de tipo de nómina es válido"""
|
|
36
|
+
return cls.get_tipo(code) is not None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_all(cls) -> list[dict]:
|
|
40
|
+
"""Obtiene todos los tipos de nómina"""
|
|
41
|
+
cls._load_data()
|
|
42
|
+
return cls._data.copy()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def is_ordinaria(cls, code: str) -> bool:
|
|
46
|
+
"""Verifica si es nómina ordinaria"""
|
|
47
|
+
return code == "O"
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def is_extraordinaria(cls, code: str) -> bool:
|
|
51
|
+
"""Verifica si es nómina extraordinaria"""
|
|
52
|
+
return code == "E"
|