py-vucem 0.1.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.
- py_vucem/__init__.py +5 -0
- py_vucem/client.py +316 -0
- py_vucem/exceptions.py +7 -0
- py_vucem/models/__init__.py +4 -0
- py_vucem/models/mv_builder.py +295 -0
- py_vucem/models/mv_catalogos.json +84 -0
- py_vucem/models/mv_mapper.py +250 -0
- py_vucem/models/mv_modelo.json +110 -0
- py_vucem/models/mv_modelo.xml +136 -0
- py_vucem/models/mv_modelo_cuerpo.xml +120 -0
- py_vucem/models/mv_xml.py +456 -0
- py_vucem/services/__init__.py +6 -0
- py_vucem/services/cove_consulta.py +84 -0
- py_vucem/services/digitalizar_documento.py +213 -0
- py_vucem/services/manifestacion_service.py +763 -0
- py_vucem/services/pedimentos_service.py +213 -0
- py_vucem/tipos_documento.py +124 -0
- py_vucem/utils/__init__.py +120 -0
- py_vucem/utils/banxico.py +209 -0
- py_vucem/utils.py +93 -0
- py_vucem/wsdl/DigitalizarDocumento.xsd +110 -0
- py_vucem/wsdl/DigitalizarDocumentoService.wsdl +87 -0
- py_vucem/wsdl/mv/ConsultaManifestacionService.wsdl +87 -0
- py_vucem/wsdl/mv/ConsultaManifestacionService.xsd +165 -0
- py_vucem/wsdl/mv/IngresoManifestacionService.wsdl +96 -0
- py_vucem/wsdl/mv/IngresoManifestacionService.xsd +306 -0
- py_vucem/wsdl/pedimentos/ConsultarEstadoPedimentosService.wsdl +56 -0
- py_vucem/wsdl/pedimentos/ConsultarEstadoPedimentosService.xsd +138 -0
- py_vucem/wsdl/pedimentos/ConsultarPartida.wsdl +56 -0
- py_vucem/wsdl/pedimentos/ConsultarPartida.xsd +46 -0
- py_vucem/wsdl/pedimentos/ConsultarPedimentoCompleto.wsdl +56 -0
- py_vucem/wsdl/pedimentos/ConsultarPedimentoCompleto.xsd +915 -0
- py_vucem/wsdl/pedimentos/ConsultarRemesasService.wsdl +57 -0
- py_vucem/wsdl/pedimentos/ConsultarRemesasService.xsd +65 -0
- py_vucem/wsdl/pedimentos/ListarPedimentosService.wsdl +57 -0
- py_vucem/wsdl/pedimentos/ListarPedimentosService.xsd +125 -0
- py_vucem/wsdl/pedimentos/comunes.xsd +439 -0
- py_vucem/wsdl/pedimentos/unidaddemedida.xsd +32 -0
- py_vucem/wsdl/respuesta.xsd +116 -0
- py_vucem-0.1.0.dist-info/METADATA +410 -0
- py_vucem-0.1.0.dist-info/RECORD +42 -0
- py_vucem-0.1.0.dist-info/WHEEL +4 -0
py_vucem/__init__.py
ADDED
py_vucem/client.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
from py_vucem.utils import FielHandler
|
|
2
|
+
from py_vucem.services.cove_consulta import CoveConsultaService
|
|
3
|
+
from py_vucem.services.digitalizar_documento import DigitalizacionService
|
|
4
|
+
from py_vucem.services.pedimentos_service import PedimentosService
|
|
5
|
+
from py_vucem.services.manifestacion_service import ManifestacionService
|
|
6
|
+
|
|
7
|
+
class VucemClient:
|
|
8
|
+
"""Cliente central de VUCEM."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, cer_path: str, key_path: str, password: str, clave_ws: str, rfc: str, ambiente: str = "PROD"):
|
|
11
|
+
self.ambiente = ambiente.upper()
|
|
12
|
+
self.clave_ws = clave_ws
|
|
13
|
+
|
|
14
|
+
self.fiel = FielHandler(cer_path, key_path, password)
|
|
15
|
+
self.rfc = rfc or self.fiel.obtener_rfc()
|
|
16
|
+
|
|
17
|
+
# Caché de instancias
|
|
18
|
+
self._cove_consulta_service = None
|
|
19
|
+
self._digitalizacion_service = None
|
|
20
|
+
self._pedimentos_service = None
|
|
21
|
+
self._manifestacion_service = None
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def url_base(self) -> str:
|
|
25
|
+
if self.ambiente == "TEST":
|
|
26
|
+
return "https://qa.vucem.gob.mx"
|
|
27
|
+
return "https://www.ventanillaunica.gob.mx"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def servicio_cove_consulta(self) -> CoveConsultaService:
|
|
31
|
+
"""Propiedad perezosa para el servicio de Consulta de COVE."""
|
|
32
|
+
if self._cove_consulta_service is None:
|
|
33
|
+
wsdl_url = f"{self.url_base}/ventanilla/ConsultarEdocumentService?wsdl"
|
|
34
|
+
self._cove_consulta_service = CoveConsultaService(
|
|
35
|
+
wsdl_url=wsdl_url,
|
|
36
|
+
username=self.rfc,
|
|
37
|
+
password_ws=self.clave_ws
|
|
38
|
+
)
|
|
39
|
+
return self._cove_consulta_service
|
|
40
|
+
|
|
41
|
+
# -------------------------------------------------------------------------
|
|
42
|
+
# Métodos Públicos
|
|
43
|
+
# -------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def consultar_cove(self, edocument: str):
|
|
46
|
+
"""
|
|
47
|
+
Consulta y descarga un COVE específico utilizando su eDocument.
|
|
48
|
+
"""
|
|
49
|
+
return self.servicio_cove_consulta.consultar_cove(
|
|
50
|
+
fiel=self.fiel,
|
|
51
|
+
rfc=self.rfc,
|
|
52
|
+
edocument=edocument
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def servicio_digitalizacion(self) -> DigitalizacionService:
|
|
57
|
+
if self._digitalizacion_service is None:
|
|
58
|
+
# VUCEM no sirve el ?wsdl de digitalización; el servicio usa el WSDL
|
|
59
|
+
# local empaquetado y sólo necesita el endpoint SOAP real (puerto 443).
|
|
60
|
+
endpoint_url = f"{self.url_base}/ventanilla/DigitalizarDocumentoService"
|
|
61
|
+
|
|
62
|
+
self._digitalizacion_service = DigitalizacionService(
|
|
63
|
+
endpoint_url=endpoint_url,
|
|
64
|
+
username=self.rfc,
|
|
65
|
+
password_ws=self.clave_ws
|
|
66
|
+
)
|
|
67
|
+
return self._digitalizacion_service
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def digitalizar_documento(self, correo: str, id_tipo_documento: str, nombre_documento: str,
|
|
71
|
+
archivo_bytes: bytes, rfc_consulta: str = None):
|
|
72
|
+
"""Genera un e-Document a partir de un archivo PDF válido en VUCEM."""
|
|
73
|
+
return self.servicio_digitalizacion.digitalizar_documento(
|
|
74
|
+
fiel=self.fiel,
|
|
75
|
+
rfc_solicitante=self.rfc,
|
|
76
|
+
correo=correo,
|
|
77
|
+
id_tipo_documento=id_tipo_documento,
|
|
78
|
+
nombre_documento=nombre_documento,
|
|
79
|
+
archivo_bytes=archivo_bytes,
|
|
80
|
+
rfc_consulta=rfc_consulta
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def consultar_tipos_documento(self):
|
|
84
|
+
"""Consulta el catálogo de tipos de documento disponibles en VUCEM."""
|
|
85
|
+
return self.servicio_digitalizacion.consultar_tipos_documento(
|
|
86
|
+
fiel=self.fiel,
|
|
87
|
+
rfc=self.rfc
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def consultar_estatus_digitalizacion(self, numero_operacion: int):
|
|
91
|
+
"""Consulta el estatus/resultado de una digitalización por su numeroOperacion."""
|
|
92
|
+
return self.servicio_digitalizacion.consultar_estatus(
|
|
93
|
+
fiel=self.fiel,
|
|
94
|
+
rfc=self.rfc,
|
|
95
|
+
numero_operacion=numero_operacion
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def servicio_pedimentos(self) -> PedimentosService:
|
|
100
|
+
if self._pedimentos_service is None:
|
|
101
|
+
self._pedimentos_service = PedimentosService(
|
|
102
|
+
url_base=self.url_base,
|
|
103
|
+
username=self.rfc,
|
|
104
|
+
password_ws=self.clave_ws,
|
|
105
|
+
)
|
|
106
|
+
return self._pedimentos_service
|
|
107
|
+
|
|
108
|
+
def listar_pedimentos(self, aduana: str,
|
|
109
|
+
patente: int = None, pedimento: int = None,
|
|
110
|
+
edocument_cove: str = None, rfc: str = None,
|
|
111
|
+
contenedor: str = None, guia: str = None,
|
|
112
|
+
fecha_inicio=None, fecha_fin=None):
|
|
113
|
+
"""Lista pedimentos filtrados. aduana debe ser string de 3 dígitos (ej. '016').
|
|
114
|
+
|
|
115
|
+
Combinaciones válidas (aduana siempre requerida):
|
|
116
|
+
listar_pedimentos('016', pedimento=6006665)
|
|
117
|
+
listar_pedimentos('016', patente=3977, fecha_inicio=date(2026,1,1), fecha_fin=date(2026,6,30))
|
|
118
|
+
listar_pedimentos('016', rfc='PME241011C34', fecha_inicio=date(2026,1,1), fecha_fin=date(2026,6,30))
|
|
119
|
+
"""
|
|
120
|
+
return self.servicio_pedimentos.listar_pedimentos(
|
|
121
|
+
aduana=aduana,
|
|
122
|
+
patente=patente,
|
|
123
|
+
pedimento=pedimento,
|
|
124
|
+
edocument_cove=edocument_cove,
|
|
125
|
+
rfc=rfc,
|
|
126
|
+
contenedor=contenedor,
|
|
127
|
+
guia=guia,
|
|
128
|
+
fecha_inicio=fecha_inicio,
|
|
129
|
+
fecha_fin=fecha_fin,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def consultar_estado_pedimentos(self, aduana: int, patente: int, pedimento: int, numero_operacion: int):
|
|
133
|
+
"""Consulta el estado de un pedimento aduanal."""
|
|
134
|
+
return self.servicio_pedimentos.consultar_estado_pedimentos(
|
|
135
|
+
aduana=aduana,
|
|
136
|
+
patente=patente,
|
|
137
|
+
pedimento=pedimento,
|
|
138
|
+
numero_operacion=numero_operacion,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def consultar_remesas(self, aduana: int, patente: int, pedimento: int, numero_operacion: int):
|
|
142
|
+
"""Consulta las remesas asociadas a un pedimento."""
|
|
143
|
+
return self.servicio_pedimentos.consultar_remesas(
|
|
144
|
+
aduana=aduana,
|
|
145
|
+
patente=patente,
|
|
146
|
+
pedimento=pedimento,
|
|
147
|
+
numero_operacion=numero_operacion,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def consultar_partida(self, aduana: str, patente: int, pedimento: int,
|
|
151
|
+
numero_operacion: int, numero_partida: int):
|
|
152
|
+
"""Consulta el detalle de una partida específica de un pedimento."""
|
|
153
|
+
return self.servicio_pedimentos.consultar_partida(
|
|
154
|
+
aduana=aduana,
|
|
155
|
+
patente=patente,
|
|
156
|
+
pedimento=pedimento,
|
|
157
|
+
numero_operacion=numero_operacion,
|
|
158
|
+
numero_partida=numero_partida,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def consultar_pedimento_completo(self, aduana: str, patente: int, pedimento: int):
|
|
162
|
+
"""Consulta el detalle completo de un pedimento aduanal."""
|
|
163
|
+
return self.servicio_pedimentos.consultar_pedimento_completo(
|
|
164
|
+
aduana=aduana,
|
|
165
|
+
patente=patente,
|
|
166
|
+
pedimento=pedimento,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
170
|
+
# Manifestación de Valor Electrónica (MV)
|
|
171
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def servicio_manifestacion(self) -> ManifestacionService:
|
|
175
|
+
if self._manifestacion_service is None:
|
|
176
|
+
self._manifestacion_service = ManifestacionService(
|
|
177
|
+
username=self.rfc,
|
|
178
|
+
password_ws=self.clave_ws,
|
|
179
|
+
)
|
|
180
|
+
return self._manifestacion_service
|
|
181
|
+
|
|
182
|
+
def consultar_manifestacion(self, numero_operacion: int = None, edocument: str = None):
|
|
183
|
+
"""Consulta una MV por su numero_operacion o su eDocument (número MV tipo MNVA…).
|
|
184
|
+
|
|
185
|
+
Solo uno de los dos parámetros es necesario.
|
|
186
|
+
"""
|
|
187
|
+
return self.servicio_manifestacion.consultar_manifestacion(
|
|
188
|
+
numero_operacion=numero_operacion,
|
|
189
|
+
edocument=edocument,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def registrar_manifestacion(self, rfc_importador: str, documentos: list,
|
|
193
|
+
informacion_coves: list, valor_en_aduana: dict,
|
|
194
|
+
personas_consulta: list = None):
|
|
195
|
+
"""Registra una nueva Manifestación de Valor en VUCEM.
|
|
196
|
+
|
|
197
|
+
Consulta ManifestacionService.registrar_manifestacion para la estructura
|
|
198
|
+
esperada de informacion_coves y valor_en_aduana.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
{'numeroOperacion': int, 'mensaje': [...]} en éxito.
|
|
202
|
+
"""
|
|
203
|
+
return self.servicio_manifestacion.registrar_manifestacion(
|
|
204
|
+
fiel=self.fiel,
|
|
205
|
+
rfc_importador=rfc_importador,
|
|
206
|
+
documentos=documentos,
|
|
207
|
+
informacion_coves=informacion_coves,
|
|
208
|
+
valor_en_aduana=valor_en_aduana,
|
|
209
|
+
personas_consulta=personas_consulta,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def registrar_manifestacion_desde_xml(self, xml_str: str) -> dict:
|
|
213
|
+
"""Envia el XML SOAP firmado directamente a VUCEM (sin validacion zeep).
|
|
214
|
+
|
|
215
|
+
Usa registrar_desde_xml() para evitar el rechazo por minOccurs
|
|
216
|
+
cuando precioPorPagar / compensoPago / decrementables estan vacios.
|
|
217
|
+
"""
|
|
218
|
+
return self.servicio_manifestacion.registrar_desde_xml(xml_str)
|
|
219
|
+
|
|
220
|
+
def descargar_acuse(self, id_edocument: str, es_cove: bool = False) -> dict:
|
|
221
|
+
"""Descarga el acuse PDF de un trámite por su eDocument.
|
|
222
|
+
|
|
223
|
+
Usa el servicio oficial VUCEM (Hoja Informativa No. 23):
|
|
224
|
+
https://www.ventanillaunica.gob.mx/ventanilla-acuses-HA/ConsultaAcusesServiceWS?wsdl
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
id_edocument: eDocument del trámite (número numérico o COVE...).
|
|
228
|
+
es_cove: True para usar consultarAcuseCove (COVEs),
|
|
229
|
+
False para consultarAcuseEdocument (digitalizaciones).
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
dict con 'pdf' (bytes) si se descargó, o 'error'/'_raw' para depuración.
|
|
233
|
+
"""
|
|
234
|
+
return self.servicio_manifestacion.descargar_acuse(id_edocument, es_cove=es_cove)
|
|
235
|
+
|
|
236
|
+
def actualizar_manifestacion(self, numero_mv: str, edocuments: list,
|
|
237
|
+
personas_consulta: list = None):
|
|
238
|
+
"""Agrega e-Documents y/o personas autorizadas a una MV existente.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
numero_mv: Identificador MV, ej. "MNVA250000XXX".
|
|
242
|
+
edocuments: Lista de eDocument strings a agregar.
|
|
243
|
+
personas_consulta: Lista de {"rfc": ..., "tipoFigura": ...} (opcional).
|
|
244
|
+
"""
|
|
245
|
+
return self.servicio_manifestacion.actualizar_manifestacion(
|
|
246
|
+
fiel=self.fiel,
|
|
247
|
+
numero_mv=numero_mv,
|
|
248
|
+
edocuments=edocuments,
|
|
249
|
+
personas_consulta=personas_consulta,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def registrar_manifestacion_desde_modelo(self, modelo: dict):
|
|
253
|
+
"""Registra una MV a partir del modelo amigable (formato mv_modelo.json).
|
|
254
|
+
|
|
255
|
+
El mapper:
|
|
256
|
+
- Distribuye los pedimentos del root a todos los COVEs
|
|
257
|
+
(cada factura puede sobreescribir con su propio campo 'pedimentos')
|
|
258
|
+
- Calcula valor_en_aduana automáticamente sumando todos los montos
|
|
259
|
+
- Convierte fechas (YYYY-MM-DD o DD/MM/YYYY → datetime)
|
|
260
|
+
- Convierte vinculacion/cargoImportador bool → 0/1
|
|
261
|
+
|
|
262
|
+
Uso::
|
|
263
|
+
|
|
264
|
+
import json
|
|
265
|
+
modelo = json.load(open("mi_mv.json"))
|
|
266
|
+
resultado = client.registrar_manifestacion_desde_modelo(modelo)
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
modelo: dict cargado desde mv_modelo.json (o construido en código).
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
{'numeroOperacion': int, 'mensaje': [...]} en éxito.
|
|
273
|
+
"""
|
|
274
|
+
from py_vucem.models.mv_mapper import desde_modelo
|
|
275
|
+
datos = desde_modelo(modelo)
|
|
276
|
+
return self.registrar_manifestacion(**datos)
|
|
277
|
+
|
|
278
|
+
def generar_xml_mv(self, modelo: dict, con_firma: bool = False) -> str:
|
|
279
|
+
"""Genera el XML SOAP completo de MVE sin enviarlo (para preview o depuración).
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
modelo: dict con el formato amigable de mv_modelo.json.
|
|
283
|
+
con_firma: si True incluye la firma electrónica real de la FIEL configurada;
|
|
284
|
+
si False (default) usa placeholders para revisión rápida.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
String XML SOAP formateado, listo para imprimir, guardar o depurar.
|
|
288
|
+
"""
|
|
289
|
+
from py_vucem.models.mv_xml import generar_xml
|
|
290
|
+
return generar_xml(
|
|
291
|
+
modelo=modelo,
|
|
292
|
+
rfc=self.rfc,
|
|
293
|
+
clave_ws=self.clave_ws,
|
|
294
|
+
fiel=self.fiel if con_firma else None,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def validar_xml_mv(self, xml_string: str) -> list[str]:
|
|
298
|
+
"""Valida un XML SOAP de MVE contra el XSD de VUCEM.
|
|
299
|
+
|
|
300
|
+
Útil para verificar que el XML generado es correcto antes de enviarlo.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
xml_string: XML generado por ``generar_xml_mv`` o ``generar_xml``.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Lista de errores (vacía = XML válido).
|
|
307
|
+
|
|
308
|
+
Ejemplo::
|
|
309
|
+
|
|
310
|
+
xml = client.generar_xml_mv(modelo)
|
|
311
|
+
errores = client.validar_xml_mv(xml)
|
|
312
|
+
if not errores:
|
|
313
|
+
resultado = client.registrar_manifestacion_desde_modelo(modelo)
|
|
314
|
+
"""
|
|
315
|
+
from py_vucem.models.mv_xml import validar_xml
|
|
316
|
+
return validar_xml(xml_string)
|
py_vucem/exceptions.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Builder de Manifestacion de Valor Electronica.
|
|
2
|
+
|
|
3
|
+
Permite construir una MVE paso a paso en codigo, sin necesidad de JSON.
|
|
4
|
+
Si se configura un BanxicoClient, el TC se consulta automaticamente
|
|
5
|
+
por fecha y moneda desde el SIE de Banco de Mexico.
|
|
6
|
+
|
|
7
|
+
Uso SIN TC automatico (tc manual obligatorio):
|
|
8
|
+
mv = ManifestacionValor("PME241011C34")
|
|
9
|
+
f = mv.nueva_factura("COVE2680TJDY6", incoterm="TIPINC.FOB")
|
|
10
|
+
f.agregar_pago(total=21328, forma="FORPAG.TE", moneda="USD",
|
|
11
|
+
tc=20.1234, fecha="2026-02-03")
|
|
12
|
+
|
|
13
|
+
Uso CON TC automatico (Banxico):
|
|
14
|
+
from py_vucem.utils.banxico import BanxicoClient
|
|
15
|
+
banxico = BanxicoClient("tu-token-banxico")
|
|
16
|
+
|
|
17
|
+
mv = ManifestacionValor("PME241011C34", banxico=banxico)
|
|
18
|
+
f = mv.nueva_factura("COVE2680TJDY6", incoterm="TIPINC.FOB")
|
|
19
|
+
f.agregar_pago(total=21328, forma="FORPAG.TE", moneda="USD",
|
|
20
|
+
fecha="2026-02-03") # tc se consulta solo
|
|
21
|
+
|
|
22
|
+
Registro final:
|
|
23
|
+
resultado = client.registrar_manifestacion_desde_modelo(mv.to_dict())
|
|
24
|
+
xml = client.generar_xml_mv(mv.to_dict())
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from decimal import Decimal
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Factura:
|
|
31
|
+
"""Una factura (COVE) dentro de una ManifestacionValor."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, cove: str, incoterm: str, vinculacion: bool = False,
|
|
34
|
+
metodo: str = "VALADU.VTM", banxico=None):
|
|
35
|
+
self.cove = cove
|
|
36
|
+
self.incoterm = incoterm
|
|
37
|
+
self.vinculacion = vinculacion
|
|
38
|
+
self.metodo = metodo
|
|
39
|
+
self._banxico = banxico
|
|
40
|
+
self._pagado: list[dict] = []
|
|
41
|
+
self._por_pagar: list[dict] = []
|
|
42
|
+
self._compenso: list[dict] = []
|
|
43
|
+
self._incrementables: list[dict] = []
|
|
44
|
+
self._decrementables: list[dict] = []
|
|
45
|
+
self._pedimentos: list[dict] | None = None
|
|
46
|
+
|
|
47
|
+
# ── Resolucion de TC ──────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
def _resolver_tc(self, moneda: str, fecha: str, tc=None) -> Decimal:
|
|
50
|
+
"""Devuelve el TC: usa el provisto, o lo consulta en Banxico si es None."""
|
|
51
|
+
if tc is not None:
|
|
52
|
+
tc_dec = Decimal(str(tc))
|
|
53
|
+
if self._banxico is not None:
|
|
54
|
+
# Valida el TC declarado contra Banxico
|
|
55
|
+
try:
|
|
56
|
+
tc_oficial, diff_pct = self._banxico.validar_tc(moneda, fecha, tc_dec)
|
|
57
|
+
if diff_pct > 1.0:
|
|
58
|
+
print(
|
|
59
|
+
f" [!] TC declarado {tc_dec} para {moneda}/{fecha} "
|
|
60
|
+
f"difiere {diff_pct:.2f}% del TC Banxico oficial ({tc_oficial}). "
|
|
61
|
+
f"Considera usar el TC oficial."
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
print(f" [TC] {moneda}/{fecha}: {tc_dec} (declarado, "
|
|
65
|
+
f"Banxico={tc_oficial}, diff={diff_pct:.3f}%)")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f" [!] No se pudo validar TC contra Banxico: {e}")
|
|
68
|
+
return tc_dec
|
|
69
|
+
|
|
70
|
+
# tc no proporcionado — consultar Banxico
|
|
71
|
+
if self._banxico is None:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"tc no proporcionado para {moneda}/{fecha} y no hay cliente "
|
|
74
|
+
f"Banxico configurado.\n"
|
|
75
|
+
f"Opciones:\n"
|
|
76
|
+
f" 1. Agrega tc=XX.XXXX al llamado\n"
|
|
77
|
+
f" 2. Configura banxico=BanxicoClient(token) en ManifestacionValor"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
tc_val = self._banxico.tc(moneda, fecha)
|
|
82
|
+
print(f" [TC] {moneda}/{fecha}: {tc_val} (Banxico FIX)")
|
|
83
|
+
return tc_val
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"No se pudo obtener TC de Banxico para {moneda}/{fecha}: {e}\n"
|
|
87
|
+
f"Proporciona tc= manualmente."
|
|
88
|
+
) from e
|
|
89
|
+
|
|
90
|
+
# ── Precios pagados ───────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
def agregar_pago(self, total: float, forma: str, moneda: str, fecha: str,
|
|
93
|
+
tc: float = None, especifique: str = None) -> "Factura":
|
|
94
|
+
"""Agrega un precio pagado (pago ya realizado).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
total: Monto pagado en la moneda indicada.
|
|
98
|
+
forma: Clave de forma de pago (ej. 'FORPAG.TE').
|
|
99
|
+
moneda: Clave ISO 4217 (ej. 'USD', 'EUR', 'CNY').
|
|
100
|
+
fecha: Fecha del pago ('YYYY-MM-DD' o 'DD/MM/YYYY').
|
|
101
|
+
tc: Tipo de cambio MXN por unidad de moneda extranjera.
|
|
102
|
+
Si es None y hay BanxicoClient configurado, se consulta
|
|
103
|
+
automaticamente el TC FIX de Banxico para esa fecha.
|
|
104
|
+
especifique: Descripcion obligatoria cuando forma='FORPAG.OT'.
|
|
105
|
+
"""
|
|
106
|
+
tc_resuelto = self._resolver_tc(moneda, fecha, tc)
|
|
107
|
+
entry = {"fecha": fecha, "total": total, "forma": forma,
|
|
108
|
+
"moneda": moneda, "tc": float(tc_resuelto)}
|
|
109
|
+
if especifique:
|
|
110
|
+
entry["especifique"] = especifique
|
|
111
|
+
self._pagado.append(entry)
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def agregar_por_pagar(self, total: float, forma: str, moneda: str, fecha: str,
|
|
115
|
+
tc: float = None, situacion: str = None,
|
|
116
|
+
especifique: str = None) -> "Factura":
|
|
117
|
+
"""Agrega un precio por pagar (pago pendiente o diferido).
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
situacion: Descripcion de la situacion del pago pendiente.
|
|
121
|
+
Ej. 'Credito a 60 dias plazo desde la fecha de embarque'.
|
|
122
|
+
"""
|
|
123
|
+
tc_resuelto = self._resolver_tc(moneda, fecha, tc)
|
|
124
|
+
entry = {"fecha": fecha, "total": total, "forma": forma,
|
|
125
|
+
"moneda": moneda, "tc": float(tc_resuelto)}
|
|
126
|
+
if situacion:
|
|
127
|
+
entry["situacion"] = situacion
|
|
128
|
+
if especifique:
|
|
129
|
+
entry["especifique"] = especifique
|
|
130
|
+
self._por_pagar.append(entry)
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def agregar_compenso(self, motivo: str, mercancia: str, forma: str,
|
|
134
|
+
fecha: str, especifique: str = None) -> "Factura":
|
|
135
|
+
"""Agrega un pago por compensacion (intercambio de bienes/servicios)."""
|
|
136
|
+
entry = {"fecha": fecha, "motivo": motivo, "mercancia": mercancia,
|
|
137
|
+
"forma": forma}
|
|
138
|
+
if especifique:
|
|
139
|
+
entry["especifique"] = especifique
|
|
140
|
+
self._compenso.append(entry)
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
# ── Incrementables / Decrementables ───────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
def agregar_incrementable(self, tipo: str, total: float, moneda: str,
|
|
146
|
+
fecha: str, tc: float = None,
|
|
147
|
+
cargo_importador: bool = True) -> "Factura":
|
|
148
|
+
"""Agrega un concepto incrementable al valor en aduana.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
tipo: Clave del catalogo (ej. 'INCRE.GS' para fletes).
|
|
152
|
+
cargo_importador: True si fue a cargo del importador (FOB = True).
|
|
153
|
+
tc: TC del dia de la erogacion. Si es None, se
|
|
154
|
+
consulta Banxico automaticamente.
|
|
155
|
+
"""
|
|
156
|
+
tc_resuelto = self._resolver_tc(moneda, fecha, tc)
|
|
157
|
+
self._incrementables.append({
|
|
158
|
+
"tipo": tipo, "fecha": fecha, "total": total,
|
|
159
|
+
"moneda": moneda, "tc": float(tc_resuelto),
|
|
160
|
+
"cargoImportador": cargo_importador,
|
|
161
|
+
})
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def agregar_decrementable(self, tipo: str, total: float, moneda: str,
|
|
165
|
+
fecha: str, tc: float = None) -> "Factura":
|
|
166
|
+
"""Agrega un concepto decrementable al valor en aduana."""
|
|
167
|
+
tc_resuelto = self._resolver_tc(moneda, fecha, tc)
|
|
168
|
+
self._decrementables.append({
|
|
169
|
+
"tipo": tipo, "fecha": fecha, "total": total,
|
|
170
|
+
"moneda": moneda, "tc": float(tc_resuelto),
|
|
171
|
+
})
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
# ── Pedimentos propios (sobreescribe los del root) ────────────────────────
|
|
175
|
+
|
|
176
|
+
def agregar_pedimento(self, pedimento: str, patente: str,
|
|
177
|
+
aduana: str) -> "Factura":
|
|
178
|
+
"""Agrega un pedimento especifico a esta factura (sobreescribe los del root)."""
|
|
179
|
+
if self._pedimentos is None:
|
|
180
|
+
self._pedimentos = []
|
|
181
|
+
self._pedimentos.append({"pedimento": pedimento,
|
|
182
|
+
"patente": patente, "aduana": aduana})
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
# ── Serialización ─────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def to_dict(self) -> dict:
|
|
188
|
+
d = {
|
|
189
|
+
"cove": self.cove,
|
|
190
|
+
"incoterm": self.incoterm,
|
|
191
|
+
"vinculacion": self.vinculacion,
|
|
192
|
+
"pagado": list(self._pagado),
|
|
193
|
+
"porPagar": list(self._por_pagar),
|
|
194
|
+
"compenso": list(self._compenso),
|
|
195
|
+
"metodo": self.metodo,
|
|
196
|
+
"incrementables": list(self._incrementables),
|
|
197
|
+
"decrementables": list(self._decrementables),
|
|
198
|
+
}
|
|
199
|
+
if self._pedimentos is not None:
|
|
200
|
+
d["pedimentos"] = list(self._pedimentos)
|
|
201
|
+
return d
|
|
202
|
+
|
|
203
|
+
def __repr__(self):
|
|
204
|
+
return (f"Factura(cove={self.cove!r}, pagos={len(self._pagado)}, "
|
|
205
|
+
f"inc={len(self._incrementables)}, dec={len(self._decrementables)})")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class ManifestacionValor:
|
|
209
|
+
"""Builder de una Manifestacion de Valor Electronica (MVE).
|
|
210
|
+
|
|
211
|
+
Construye el modelo paso a paso y genera el dict compatible con
|
|
212
|
+
``registrar_manifestacion_desde_modelo()`` y ``generar_xml()``.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
rfc_importador: RFC del importador.
|
|
216
|
+
banxico: (Opcional) BanxicoClient para consultar TC automaticamente.
|
|
217
|
+
Si no se proporciona, tc= es obligatorio en cada pago.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
def __init__(self, rfc_importador: str, banxico=None):
|
|
221
|
+
self.rfc_importador = rfc_importador
|
|
222
|
+
self._banxico = banxico
|
|
223
|
+
self._consulta: list[dict] = []
|
|
224
|
+
self._documentos: list[str] = []
|
|
225
|
+
self._pedimentos: list[dict] = []
|
|
226
|
+
self._facturas: list[Factura] = []
|
|
227
|
+
|
|
228
|
+
# ── Metadatos de la operacion ─────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
def agregar_consulta(self, rfc: str, figura: str) -> "ManifestacionValor":
|
|
231
|
+
"""Agrega una persona autorizada a consultar la MV.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
rfc: RFC de la persona o agencia aduanal.
|
|
235
|
+
figura: Clave del catalogo ('TIPFIG.AGE', 'TIPFIG.REP', etc.).
|
|
236
|
+
"""
|
|
237
|
+
self._consulta.append({"rfc": rfc, "figura": figura})
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
def agregar_documento(self, edocument: str) -> "ManifestacionValor":
|
|
241
|
+
"""Agrega un eDocument (numero de COVE) a la operacion."""
|
|
242
|
+
self._documentos.append(edocument)
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
def agregar_pedimento(self, pedimento: str, patente: str,
|
|
246
|
+
aduana: str) -> "ManifestacionValor":
|
|
247
|
+
"""Agrega un pedimento compartido entre todas las facturas.
|
|
248
|
+
|
|
249
|
+
Si una factura necesita pedimentos diferentes usa
|
|
250
|
+
``Factura.agregar_pedimento()`` en su lugar.
|
|
251
|
+
"""
|
|
252
|
+
self._pedimentos.append({"pedimento": pedimento,
|
|
253
|
+
"patente": patente, "aduana": aduana})
|
|
254
|
+
return self
|
|
255
|
+
|
|
256
|
+
# ── Facturas ──────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
def nueva_factura(self, cove: str, incoterm: str, vinculacion: bool = False,
|
|
259
|
+
metodo: str = "VALADU.VTM") -> Factura:
|
|
260
|
+
"""Crea y registra una nueva factura (COVE) en la MV.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
cove: Numero de COVE (ej. 'COVE2680TJDY6').
|
|
264
|
+
incoterm: Clave del catalogo (ej. 'TIPINC.FOB').
|
|
265
|
+
vinculacion: True si hay vinculacion entre comprador y vendedor.
|
|
266
|
+
metodo: Metodo de valoracion (default VALADU.VTM).
|
|
267
|
+
"""
|
|
268
|
+
f = Factura(cove=cove, incoterm=incoterm,
|
|
269
|
+
vinculacion=vinculacion, metodo=metodo,
|
|
270
|
+
banxico=self._banxico)
|
|
271
|
+
self._facturas.append(f)
|
|
272
|
+
return f
|
|
273
|
+
|
|
274
|
+
# ── Exportar ──────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
def to_dict(self) -> dict:
|
|
277
|
+
"""Convierte la MV al formato dict compatible con todas las funciones.
|
|
278
|
+
|
|
279
|
+
Compatible con:
|
|
280
|
+
client.registrar_manifestacion_desde_modelo(mv.to_dict())
|
|
281
|
+
generar_xml(mv.to_dict(), rfc=..., clave_ws=...)
|
|
282
|
+
"""
|
|
283
|
+
return {
|
|
284
|
+
"importador": self.rfc_importador,
|
|
285
|
+
"consulta": list(self._consulta),
|
|
286
|
+
"documentos": list(self._documentos),
|
|
287
|
+
"pedimentos": list(self._pedimentos),
|
|
288
|
+
"facturas": [f.to_dict() for f in self._facturas],
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
def __repr__(self):
|
|
292
|
+
modo_tc = "Banxico auto" if self._banxico else "tc manual"
|
|
293
|
+
return (f"ManifestacionValor(importador={self.rfc_importador!r}, "
|
|
294
|
+
f"facturas={len(self._facturas)}, "
|
|
295
|
+
f"docs={len(self._documentos)}, tc={modo_tc})")
|