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.
Files changed (81) hide show
  1. catalogmx/__init__.py +56 -0
  2. catalogmx/catalogs/__init__.py +5 -0
  3. catalogmx/catalogs/banxico/__init__.py +24 -0
  4. catalogmx/catalogs/banxico/banks.py +136 -0
  5. catalogmx/catalogs/banxico/codigos_plaza.py +287 -0
  6. catalogmx/catalogs/banxico/instituciones_financieras.py +338 -0
  7. catalogmx/catalogs/banxico/monedas_divisas.py +386 -0
  8. catalogmx/catalogs/banxico/udis.py +279 -0
  9. catalogmx/catalogs/ift/__init__.py +15 -0
  10. catalogmx/catalogs/ift/codigos_lada.py +426 -0
  11. catalogmx/catalogs/ift/operadores_moviles.py +315 -0
  12. catalogmx/catalogs/inegi/__init__.py +21 -0
  13. catalogmx/catalogs/inegi/localidades.py +207 -0
  14. catalogmx/catalogs/inegi/municipios.py +73 -0
  15. catalogmx/catalogs/inegi/municipios_completo.py +236 -0
  16. catalogmx/catalogs/inegi/states.py +148 -0
  17. catalogmx/catalogs/mexico/__init__.py +17 -0
  18. catalogmx/catalogs/mexico/hoy_no_circula.py +215 -0
  19. catalogmx/catalogs/mexico/placas_formatos.py +184 -0
  20. catalogmx/catalogs/mexico/salarios_minimos.py +156 -0
  21. catalogmx/catalogs/mexico/uma.py +207 -0
  22. catalogmx/catalogs/sat/__init__.py +13 -0
  23. catalogmx/catalogs/sat/carta_porte/__init__.py +19 -0
  24. catalogmx/catalogs/sat/carta_porte/aeropuertos.py +76 -0
  25. catalogmx/catalogs/sat/carta_porte/carreteras.py +59 -0
  26. catalogmx/catalogs/sat/carta_porte/config_autotransporte.py +54 -0
  27. catalogmx/catalogs/sat/carta_porte/material_peligroso.py +66 -0
  28. catalogmx/catalogs/sat/carta_porte/puertos_maritimos.py +63 -0
  29. catalogmx/catalogs/sat/carta_porte/tipo_embalaje.py +48 -0
  30. catalogmx/catalogs/sat/carta_porte/tipo_permiso.py +54 -0
  31. catalogmx/catalogs/sat/cfdi_4/__init__.py +42 -0
  32. catalogmx/catalogs/sat/cfdi_4/clave_prod_serv.py +383 -0
  33. catalogmx/catalogs/sat/cfdi_4/clave_unidad.py +298 -0
  34. catalogmx/catalogs/sat/cfdi_4/exportacion.py +45 -0
  35. catalogmx/catalogs/sat/cfdi_4/forma_pago.py +45 -0
  36. catalogmx/catalogs/sat/cfdi_4/impuesto.py +57 -0
  37. catalogmx/catalogs/sat/cfdi_4/meses.py +34 -0
  38. catalogmx/catalogs/sat/cfdi_4/metodo_pago.py +45 -0
  39. catalogmx/catalogs/sat/cfdi_4/objeto_imp.py +45 -0
  40. catalogmx/catalogs/sat/cfdi_4/periodicidad.py +34 -0
  41. catalogmx/catalogs/sat/cfdi_4/regimen_fiscal.py +57 -0
  42. catalogmx/catalogs/sat/cfdi_4/tasa_o_cuota.py +42 -0
  43. catalogmx/catalogs/sat/cfdi_4/tipo_comprobante.py +45 -0
  44. catalogmx/catalogs/sat/cfdi_4/tipo_factor.py +34 -0
  45. catalogmx/catalogs/sat/cfdi_4/tipo_relacion.py +45 -0
  46. catalogmx/catalogs/sat/cfdi_4/uso_cfdi.py +45 -0
  47. catalogmx/catalogs/sat/comercio_exterior/__init__.py +39 -0
  48. catalogmx/catalogs/sat/comercio_exterior/claves_pedimento.py +77 -0
  49. catalogmx/catalogs/sat/comercio_exterior/estados.py +122 -0
  50. catalogmx/catalogs/sat/comercio_exterior/incoterms.py +226 -0
  51. catalogmx/catalogs/sat/comercio_exterior/monedas.py +107 -0
  52. catalogmx/catalogs/sat/comercio_exterior/motivos_traslado.py +54 -0
  53. catalogmx/catalogs/sat/comercio_exterior/paises.py +88 -0
  54. catalogmx/catalogs/sat/comercio_exterior/registro_ident_trib.py +76 -0
  55. catalogmx/catalogs/sat/comercio_exterior/unidades_aduana.py +54 -0
  56. catalogmx/catalogs/sat/comercio_exterior/validator.py +212 -0
  57. catalogmx/catalogs/sat/nomina/__init__.py +19 -0
  58. catalogmx/catalogs/sat/nomina/banco.py +50 -0
  59. catalogmx/catalogs/sat/nomina/periodicidad_pago.py +48 -0
  60. catalogmx/catalogs/sat/nomina/riesgo_puesto.py +56 -0
  61. catalogmx/catalogs/sat/nomina/tipo_contrato.py +47 -0
  62. catalogmx/catalogs/sat/nomina/tipo_jornada.py +42 -0
  63. catalogmx/catalogs/sat/nomina/tipo_nomina.py +52 -0
  64. catalogmx/catalogs/sat/nomina/tipo_regimen.py +47 -0
  65. catalogmx/catalogs/sepomex/__init__.py +5 -0
  66. catalogmx/catalogs/sepomex/codigos_postales.py +184 -0
  67. catalogmx/cli.py +185 -0
  68. catalogmx/helpers.py +324 -0
  69. catalogmx/utils/text.py +55 -0
  70. catalogmx/validators/__init__.py +0 -0
  71. catalogmx/validators/clabe.py +233 -0
  72. catalogmx/validators/curp.py +623 -0
  73. catalogmx/validators/nss.py +255 -0
  74. catalogmx/validators/rfc.py +1004 -0
  75. catalogmx-0.3.0.dist-info/METADATA +644 -0
  76. catalogmx-0.3.0.dist-info/RECORD +81 -0
  77. catalogmx-0.3.0.dist-info/WHEEL +5 -0
  78. catalogmx-0.3.0.dist-info/entry_points.txt +2 -0
  79. catalogmx-0.3.0.dist-info/licenses/AUTHORS.rst +5 -0
  80. catalogmx-0.3.0.dist-info/licenses/LICENSE +19 -0
  81. 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"