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,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Catálogo c_ClavePedimento - Claves de Pedimento Aduanero
|
|
3
|
+
|
|
4
|
+
Identificadores del tipo de operación aduanera que ampara el CFDI.
|
|
5
|
+
|
|
6
|
+
Fuente: SAT - Anexo 22 de las RGCE
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClavePedimentoCatalog:
|
|
14
|
+
"""Catálogo de claves de pedimento aduanero"""
|
|
15
|
+
|
|
16
|
+
_data: list[dict] | None = None
|
|
17
|
+
_clave_by_code: dict[str, dict] | None = None
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def _load_data(cls) -> None:
|
|
21
|
+
"""Carga los datos del catálogo desde el archivo JSON compartido"""
|
|
22
|
+
if cls._data is None:
|
|
23
|
+
current_file = Path(__file__)
|
|
24
|
+
shared_data_path = (
|
|
25
|
+
current_file.parent.parent.parent.parent.parent.parent
|
|
26
|
+
/ "shared-data"
|
|
27
|
+
/ "sat"
|
|
28
|
+
/ "comercio_exterior"
|
|
29
|
+
/ "claves_pedimento.json"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
with open(shared_data_path, encoding="utf-8") as f:
|
|
33
|
+
data = json.load(f)
|
|
34
|
+
# Handle both list and dict formats
|
|
35
|
+
cls._data = data if isinstance(data, list) else data.get("claves", data)
|
|
36
|
+
|
|
37
|
+
cls._clave_by_code = {item["clave"]: item for item in cls._data}
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def get_clave(cls, code: str) -> dict | None:
|
|
41
|
+
"""Obtiene una clave de pedimento por su código"""
|
|
42
|
+
cls._load_data()
|
|
43
|
+
return cls._clave_by_code.get(code.upper())
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def is_valid(cls, code: str) -> bool:
|
|
47
|
+
"""Verifica si una clave de pedimento es válida"""
|
|
48
|
+
return cls.get_clave(code) is not None
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def is_export(cls, code: str) -> bool:
|
|
52
|
+
"""Verifica si la clave corresponde a exportación"""
|
|
53
|
+
clave = cls.get_clave(code)
|
|
54
|
+
return clave.get("regimen") == "exportacion" if clave else False
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def is_import(cls, code: str) -> bool:
|
|
58
|
+
"""Verifica si la clave corresponde a importación"""
|
|
59
|
+
clave = cls.get_clave(code)
|
|
60
|
+
return clave.get("regimen") == "importacion" if clave else False
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def get_by_regime(cls, regime: str) -> list[dict]:
|
|
64
|
+
"""
|
|
65
|
+
Obtiene claves por régimen
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
regime: exportacion, importacion, retorno, transito, etc.
|
|
69
|
+
"""
|
|
70
|
+
cls._load_data()
|
|
71
|
+
return [item for item in cls._data if item.get("regimen") == regime]
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def get_all(cls) -> list[dict]:
|
|
75
|
+
"""Retorna todas las claves de pedimento"""
|
|
76
|
+
cls._load_data()
|
|
77
|
+
return cls._data.copy()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Catálogo c_Estado - Estados de USA y Provincias de Canadá"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EstadoCatalog:
|
|
8
|
+
"""Catálogo de estados/provincias de USA y Canadá para comercio exterior"""
|
|
9
|
+
|
|
10
|
+
_estados_usa: list[dict] | None = None
|
|
11
|
+
_provincias_canada: list[dict] | None = None
|
|
12
|
+
_estado_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._estados_usa 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
|
+
/ "estados_usa_canada.json"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
with open(shared_data_path, encoding="utf-8") as f:
|
|
28
|
+
data = json.load(f)
|
|
29
|
+
# Handle both dict with keys or direct list
|
|
30
|
+
if isinstance(data, dict):
|
|
31
|
+
cls._estados_usa = data.get("estados_usa", [])
|
|
32
|
+
cls._provincias_canada = data.get("provincias_canada", [])
|
|
33
|
+
else:
|
|
34
|
+
# Direct list - separate by country field
|
|
35
|
+
cls._estados_usa = [item for item in data if item.get("country") == "USA"]
|
|
36
|
+
cls._provincias_canada = [item for item in data if item.get("country") == "CAN"]
|
|
37
|
+
|
|
38
|
+
# Crear índice unificado por código
|
|
39
|
+
cls._estado_by_code = {}
|
|
40
|
+
for estado in cls._estados_usa:
|
|
41
|
+
cls._estado_by_code[estado["code"]] = estado
|
|
42
|
+
for provincia in cls._provincias_canada:
|
|
43
|
+
cls._estado_by_code[provincia["code"]] = provincia
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def get_estado(cls, code: str, country: str | None = None) -> dict | None:
|
|
47
|
+
"""
|
|
48
|
+
Obtiene un estado/provincia por su código
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
code: Código del estado (TX, CA, ON, etc.)
|
|
52
|
+
country: Opcional - 'USA' o 'CAN' para filtrar
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict con información del estado/provincia
|
|
56
|
+
"""
|
|
57
|
+
cls._load_data()
|
|
58
|
+
code_upper = code.upper()
|
|
59
|
+
estado = cls._estado_by_code.get(code_upper)
|
|
60
|
+
|
|
61
|
+
if estado and country:
|
|
62
|
+
if estado["country"] != country.upper():
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
return estado
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_estado_usa(cls, code: str) -> dict | None:
|
|
69
|
+
"""Obtiene un estado de USA por su código"""
|
|
70
|
+
return cls.get_estado(code, "USA")
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def get_provincia_canada(cls, code: str) -> dict | None:
|
|
74
|
+
"""Obtiene una provincia de Canadá por su código"""
|
|
75
|
+
return cls.get_estado(code, "CAN")
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def is_valid(cls, code: str, country: str | None = None) -> bool:
|
|
79
|
+
"""Verifica si un código de estado/provincia es válido"""
|
|
80
|
+
return cls.get_estado(code, country) is not None
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def get_all_usa(cls) -> list[dict]:
|
|
84
|
+
"""Retorna todos los estados de USA"""
|
|
85
|
+
cls._load_data()
|
|
86
|
+
return cls._estados_usa.copy()
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def get_all_canada(cls) -> list[dict]:
|
|
90
|
+
"""Retorna todas las provincias de Canadá"""
|
|
91
|
+
cls._load_data()
|
|
92
|
+
return cls._provincias_canada.copy()
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def get_all(cls) -> list[dict]:
|
|
96
|
+
"""Retorna todos los estados y provincias"""
|
|
97
|
+
cls._load_data()
|
|
98
|
+
return cls._estados_usa + cls._provincias_canada
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def validate_foreign_address(cls, address_data: dict) -> dict:
|
|
102
|
+
"""
|
|
103
|
+
Valida dirección extranjera para comercio exterior
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
address_data: dict con 'pais', 'estado', 'num_reg_id_trib'
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
dict con 'valid' (bool) y 'errors' (list)
|
|
110
|
+
"""
|
|
111
|
+
errors = []
|
|
112
|
+
pais = address_data.get("pais", "").upper()
|
|
113
|
+
estado = address_data.get("estado", "").upper()
|
|
114
|
+
|
|
115
|
+
# Validar que USA/CAN tengan estado
|
|
116
|
+
if pais in ["USA", "CAN"]:
|
|
117
|
+
if not estado:
|
|
118
|
+
errors.append(f"Campo Estado es obligatorio para país {pais}")
|
|
119
|
+
elif not cls.is_valid(estado, pais):
|
|
120
|
+
errors.append(f"Estado {estado} no válido para país {pais}")
|
|
121
|
+
|
|
122
|
+
return {"valid": len(errors) == 0, "errors": errors}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Catálogo c_INCOTERM - Términos Internacionales de Comercio (INCOTERMS 2020)
|
|
3
|
+
|
|
4
|
+
Los INCOTERMS definen las responsabilidades entre comprador y vendedor
|
|
5
|
+
en operaciones de comercio internacional.
|
|
6
|
+
|
|
7
|
+
Fuente: ICC - International Chamber of Commerce / SAT México
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IncotermsValidator:
|
|
15
|
+
"""Validador y catálogo de INCOTERMS 2020 para Comercio Exterior"""
|
|
16
|
+
|
|
17
|
+
_data: list[dict] | None = None
|
|
18
|
+
_incoterm_by_code: dict[str, dict] | None = None
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def _load_data(cls) -> None:
|
|
22
|
+
"""Carga los datos del catálogo desde el archivo JSON compartido"""
|
|
23
|
+
if cls._data is None:
|
|
24
|
+
current_file = Path(__file__)
|
|
25
|
+
# Navegar a shared-data desde packages/python/catalogmx/catalogs/sat/comercio_exterior
|
|
26
|
+
shared_data_path = (
|
|
27
|
+
current_file.parent.parent.parent.parent.parent.parent
|
|
28
|
+
/ "shared-data"
|
|
29
|
+
/ "sat"
|
|
30
|
+
/ "comercio_exterior"
|
|
31
|
+
/ "incoterms.json"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
with open(shared_data_path, encoding="utf-8") as f:
|
|
35
|
+
data = json.load(f)
|
|
36
|
+
# Handle both list and dict formats
|
|
37
|
+
cls._data = data if isinstance(data, list) else data.get("incoterms", data)
|
|
38
|
+
|
|
39
|
+
# Crear índice por código
|
|
40
|
+
cls._incoterm_by_code = {item["code"]: item for item in cls._data}
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def get_incoterm(cls, code: str) -> dict | None:
|
|
44
|
+
"""
|
|
45
|
+
Obtiene un INCOTERM por su código
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
code: Código INCOTERM (EXW, FCA, FOB, CIF, etc.)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict con información del INCOTERM o None si no existe
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> incoterm = IncotermsValidator.get_incoterm('CIF')
|
|
55
|
+
>>> print(incoterm['name'])
|
|
56
|
+
Cost, Insurance and Freight
|
|
57
|
+
"""
|
|
58
|
+
cls._load_data()
|
|
59
|
+
return cls._incoterm_by_code.get(code.upper())
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def is_valid(cls, code: str) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Verifica si un código INCOTERM es válido
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
code: Código INCOTERM a validar
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True si el código es válido
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> IncotermsValidator.is_valid('FOB')
|
|
74
|
+
True
|
|
75
|
+
>>> IncotermsValidator.is_valid('XXX')
|
|
76
|
+
False
|
|
77
|
+
"""
|
|
78
|
+
return cls.get_incoterm(code) is not None
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def is_valid_for_transport(cls, code: str, transport_type: str) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Verifica si un INCOTERM es válido para un tipo de transporte
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
code: Código INCOTERM
|
|
87
|
+
transport_type: Tipo de transporte ('sea', 'land', 'air', 'multimodal', 'any')
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True si el INCOTERM es válido para ese transporte
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
>>> IncotermsValidator.is_valid_for_transport('CIF', 'sea')
|
|
94
|
+
True
|
|
95
|
+
>>> IncotermsValidator.is_valid_for_transport('CIF', 'land')
|
|
96
|
+
False
|
|
97
|
+
>>> IncotermsValidator.is_valid_for_transport('FCA', 'land')
|
|
98
|
+
True
|
|
99
|
+
"""
|
|
100
|
+
incoterm = cls.get_incoterm(code)
|
|
101
|
+
if not incoterm:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
suitable_for = incoterm.get("suitable_for", [])
|
|
105
|
+
|
|
106
|
+
if transport_type == "any" or incoterm["transport_mode"] == "any":
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
return transport_type in suitable_for
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def get_multimodal_incoterms(cls) -> list[str]:
|
|
113
|
+
"""
|
|
114
|
+
Retorna lista de INCOTERMS válidos para cualquier modo de transporte
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Lista de códigos INCOTERM multimodales
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
>>> multimodal = IncotermsValidator.get_multimodal_incoterms()
|
|
121
|
+
>>> print(multimodal)
|
|
122
|
+
['EXW', 'FCA', 'CPT', 'CIP', 'DAP', 'DPU', 'DDP']
|
|
123
|
+
"""
|
|
124
|
+
cls._load_data()
|
|
125
|
+
return [item["code"] for item in cls._data if item["transport_mode"] == "any"]
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def get_maritime_incoterms(cls) -> list[str]:
|
|
129
|
+
"""
|
|
130
|
+
Retorna lista de INCOTERMS válidos solo para transporte marítimo
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Lista de códigos INCOTERM marítimos
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
>>> maritime = IncotermsValidator.get_maritime_incoterms()
|
|
137
|
+
>>> print(maritime)
|
|
138
|
+
['FAS', 'FOB', 'CFR', 'CIF']
|
|
139
|
+
"""
|
|
140
|
+
cls._load_data()
|
|
141
|
+
return [item["code"] for item in cls._data if item["transport_mode"] == "maritime"]
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def seller_pays_freight(cls, code: str) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Verifica si el vendedor paga el flete en este INCOTERM
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
code: Código INCOTERM
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True si el vendedor paga flete
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
>>> IncotermsValidator.seller_pays_freight('CIF')
|
|
156
|
+
True
|
|
157
|
+
>>> IncotermsValidator.seller_pays_freight('EXW')
|
|
158
|
+
False
|
|
159
|
+
"""
|
|
160
|
+
incoterm = cls.get_incoterm(code)
|
|
161
|
+
return incoterm.get("seller_pays_freight", False) if incoterm else False
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def seller_pays_insurance(cls, code: str) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Verifica si el vendedor paga el seguro en este INCOTERM
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
code: Código INCOTERM
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True si el vendedor paga seguro
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> IncotermsValidator.seller_pays_insurance('CIF')
|
|
176
|
+
True
|
|
177
|
+
>>> IncotermsValidator.seller_pays_insurance('CFR')
|
|
178
|
+
False
|
|
179
|
+
"""
|
|
180
|
+
incoterm = cls.get_incoterm(code)
|
|
181
|
+
return incoterm.get("seller_pays_insurance", False) if incoterm else False
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def get_all(cls) -> list[dict]:
|
|
185
|
+
"""
|
|
186
|
+
Retorna todos los INCOTERMS disponibles
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Lista completa de INCOTERMS
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
>>> all_incoterms = IncotermsValidator.get_all()
|
|
193
|
+
>>> print(len(all_incoterms))
|
|
194
|
+
11
|
|
195
|
+
"""
|
|
196
|
+
cls._load_data()
|
|
197
|
+
return cls._data.copy()
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def search(cls, query: str) -> list[dict]:
|
|
201
|
+
"""
|
|
202
|
+
Busca INCOTERMS por nombre o descripción
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
query: Texto a buscar
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Lista de INCOTERMS que coinciden
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> results = IncotermsValidator.search('insurance')
|
|
212
|
+
>>> print([r['code'] for r in results])
|
|
213
|
+
['CIP', 'CIF']
|
|
214
|
+
"""
|
|
215
|
+
cls._load_data()
|
|
216
|
+
query_lower = query.lower()
|
|
217
|
+
|
|
218
|
+
return [
|
|
219
|
+
item
|
|
220
|
+
for item in cls._data
|
|
221
|
+
if (
|
|
222
|
+
query_lower in item["name"].lower()
|
|
223
|
+
or query_lower in item["name_es"].lower()
|
|
224
|
+
or query_lower in item["description"].lower()
|
|
225
|
+
)
|
|
226
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Catálogo c_Moneda - Códigos de Monedas ISO 4217"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MonedaCatalog:
|
|
8
|
+
"""Catálogo de monedas para operaciones de comercio exterior"""
|
|
9
|
+
|
|
10
|
+
_data: list[dict] | None = None
|
|
11
|
+
_moneda_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
|
+
/ "monedas.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("monedas", data)
|
|
30
|
+
|
|
31
|
+
cls._moneda_by_code = {item["codigo"]: item for item in cls._data}
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_moneda(cls, code: str) -> dict | None:
|
|
35
|
+
"""Obtiene una moneda por su código ISO 4217"""
|
|
36
|
+
cls._load_data()
|
|
37
|
+
return cls._moneda_by_code.get(code.upper())
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def is_valid(cls, code: str) -> bool:
|
|
41
|
+
"""Verifica si un código de moneda es válido"""
|
|
42
|
+
return cls.get_moneda(code) is not None
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def validate_conversion_usd(cls, cfdi_data: dict) -> dict:
|
|
46
|
+
"""
|
|
47
|
+
Valida la conversión a USD según reglas SAT
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
cfdi_data: dict con 'moneda', 'total', 'tipo_cambio_usd', 'total_usd'
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
dict con 'valid' (bool) y 'errors' (list)
|
|
54
|
+
"""
|
|
55
|
+
errors = []
|
|
56
|
+
moneda = cfdi_data.get("moneda", "").upper()
|
|
57
|
+
tipo_cambio = cfdi_data.get("tipo_cambio_usd")
|
|
58
|
+
total = cfdi_data.get("total")
|
|
59
|
+
total_usd = cfdi_data.get("total_usd")
|
|
60
|
+
|
|
61
|
+
# Validar que la moneda existe
|
|
62
|
+
if not cls.is_valid(moneda):
|
|
63
|
+
errors.append(f"Moneda {moneda} no válida")
|
|
64
|
+
return {"valid": False, "errors": errors}
|
|
65
|
+
|
|
66
|
+
# Si es USD, tipo_cambio debe ser 1
|
|
67
|
+
if moneda == "USD":
|
|
68
|
+
if tipo_cambio and tipo_cambio != 1:
|
|
69
|
+
errors.append("Para USD, TipoCambioUSD debe ser 1")
|
|
70
|
+
|
|
71
|
+
if total != total_usd:
|
|
72
|
+
errors.append("Para USD, Total debe ser igual a TotalUSD")
|
|
73
|
+
|
|
74
|
+
# Si NO es USD, tipo_cambio es obligatorio
|
|
75
|
+
else:
|
|
76
|
+
if not tipo_cambio:
|
|
77
|
+
errors.append("TipoCambioUSD es obligatorio cuando Moneda != USD")
|
|
78
|
+
|
|
79
|
+
# Validar cálculo de TotalUSD
|
|
80
|
+
if tipo_cambio and total and total_usd:
|
|
81
|
+
expected_total_usd = round(total * tipo_cambio, 2)
|
|
82
|
+
if abs(total_usd - expected_total_usd) > 0.01:
|
|
83
|
+
errors.append(f"TotalUSD incorrecto. Esperado: {expected_total_usd}")
|
|
84
|
+
|
|
85
|
+
return {"valid": len(errors) == 0, "errors": errors}
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def get_all(cls) -> list[dict]:
|
|
89
|
+
"""Retorna todas las monedas"""
|
|
90
|
+
cls._load_data()
|
|
91
|
+
return cls._data.copy()
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def search(cls, query: str) -> list[dict]:
|
|
95
|
+
"""Busca monedas por código, nombre o país"""
|
|
96
|
+
cls._load_data()
|
|
97
|
+
query_lower = query.lower()
|
|
98
|
+
|
|
99
|
+
return [
|
|
100
|
+
item
|
|
101
|
+
for item in cls._data
|
|
102
|
+
if (
|
|
103
|
+
query_lower in item["codigo"].lower()
|
|
104
|
+
or query_lower in item["nombre"].lower()
|
|
105
|
+
or query_lower in item.get("pais", "").lower()
|
|
106
|
+
)
|
|
107
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Catálogo c_MotivoTraslado - Motivos de Traslado para CFDI tipo T"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MotivoTrasladoCatalog:
|
|
8
|
+
"""Catálogo de motivos de traslado para CFDI con comercio exterior"""
|
|
9
|
+
|
|
10
|
+
_data: list[dict] | None = None
|
|
11
|
+
_motivo_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
|
+
/ "motivos_traslado.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("motivos", data)
|
|
30
|
+
|
|
31
|
+
cls._motivo_by_code = {item["code"]: item for item in cls._data}
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_motivo(cls, code: str) -> dict | None:
|
|
35
|
+
"""Obtiene un motivo de traslado por su código"""
|
|
36
|
+
cls._load_data()
|
|
37
|
+
return cls._motivo_by_code.get(code)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def is_valid(cls, code: str) -> bool:
|
|
41
|
+
"""Verifica si un código de motivo es válido"""
|
|
42
|
+
return cls.get_motivo(code) is not None
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def requires_propietario(cls, code: str) -> bool:
|
|
46
|
+
"""Verifica si el motivo requiere nodo <Propietario>"""
|
|
47
|
+
motivo = cls.get_motivo(code)
|
|
48
|
+
return motivo.get("requires_propietario", False) if motivo else False
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def get_all(cls) -> list[dict]:
|
|
52
|
+
"""Retorna todos los motivos de traslado"""
|
|
53
|
+
cls._load_data()
|
|
54
|
+
return cls._data.copy()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Catálogo c_Pais - Códigos de Países ISO 3166-1"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from catalogmx.utils.text import normalize_text
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PaisCatalog:
|
|
10
|
+
"""Catálogo de países para identificar origen/destino en comercio exterior"""
|
|
11
|
+
|
|
12
|
+
_data: list[dict] | None = None
|
|
13
|
+
_pais_by_code: dict[str, dict] | None = None
|
|
14
|
+
_pais_by_iso2: dict[str, dict] | None = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _load_data(cls) -> None:
|
|
18
|
+
"""Carga los datos del catálogo desde el archivo JSON compartido"""
|
|
19
|
+
if cls._data is None:
|
|
20
|
+
current_file = Path(__file__)
|
|
21
|
+
shared_data_path = (
|
|
22
|
+
current_file.parent.parent.parent.parent.parent.parent
|
|
23
|
+
/ "shared-data"
|
|
24
|
+
/ "sat"
|
|
25
|
+
/ "comercio_exterior"
|
|
26
|
+
/ "paises.json"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
with open(shared_data_path, encoding="utf-8") as f:
|
|
30
|
+
cls._data = json.load(f)
|
|
31
|
+
|
|
32
|
+
cls._pais_by_code = {item["codigo"]: item for item in cls._data}
|
|
33
|
+
cls._pais_by_iso2 = {item["iso2"]: item for item in cls._data}
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_pais(cls, code: str) -> dict | None:
|
|
37
|
+
"""Obtiene un país por su código ISO 3166-1 Alpha-3"""
|
|
38
|
+
cls._load_data()
|
|
39
|
+
code_upper = code.upper()
|
|
40
|
+
|
|
41
|
+
# Intentar primero con Alpha-3
|
|
42
|
+
pais = cls._pais_by_code.get(code_upper)
|
|
43
|
+
if pais:
|
|
44
|
+
return pais
|
|
45
|
+
|
|
46
|
+
# Intentar con Alpha-2
|
|
47
|
+
return cls._pais_by_iso2.get(code_upper)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def is_valid(cls, code: str) -> bool:
|
|
51
|
+
"""Verifica si un código de país es válido"""
|
|
52
|
+
return cls.get_pais(code) is not None
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def requires_subdivision(cls, code: str) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Verifica si el país requiere subdivisión (estado/provincia)
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
code: Código del país (USA, CAN, etc.)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True si requiere estado/provincia
|
|
64
|
+
"""
|
|
65
|
+
pais = cls.get_pais(code)
|
|
66
|
+
return pais.get("requiere_subdivision", False) if pais else False
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def get_all(cls) -> list[dict]:
|
|
70
|
+
"""Retorna todos los países"""
|
|
71
|
+
cls._load_data()
|
|
72
|
+
return cls._data.copy()
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def search(cls, query: str) -> list[dict]:
|
|
76
|
+
"""Busca países por código o nombre (insensible a acentos)"""
|
|
77
|
+
cls._load_data()
|
|
78
|
+
query_normalized = normalize_text(query)
|
|
79
|
+
|
|
80
|
+
return [
|
|
81
|
+
item
|
|
82
|
+
for item in cls._data
|
|
83
|
+
if (
|
|
84
|
+
query_normalized in normalize_text(item["codigo"])
|
|
85
|
+
or query_normalized in normalize_text(item["nombre"])
|
|
86
|
+
or query_normalized in normalize_text(item.get("iso2", ""))
|
|
87
|
+
)
|
|
88
|
+
]
|