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.
Files changed (42) hide show
  1. py_vucem/__init__.py +5 -0
  2. py_vucem/client.py +316 -0
  3. py_vucem/exceptions.py +7 -0
  4. py_vucem/models/__init__.py +4 -0
  5. py_vucem/models/mv_builder.py +295 -0
  6. py_vucem/models/mv_catalogos.json +84 -0
  7. py_vucem/models/mv_mapper.py +250 -0
  8. py_vucem/models/mv_modelo.json +110 -0
  9. py_vucem/models/mv_modelo.xml +136 -0
  10. py_vucem/models/mv_modelo_cuerpo.xml +120 -0
  11. py_vucem/models/mv_xml.py +456 -0
  12. py_vucem/services/__init__.py +6 -0
  13. py_vucem/services/cove_consulta.py +84 -0
  14. py_vucem/services/digitalizar_documento.py +213 -0
  15. py_vucem/services/manifestacion_service.py +763 -0
  16. py_vucem/services/pedimentos_service.py +213 -0
  17. py_vucem/tipos_documento.py +124 -0
  18. py_vucem/utils/__init__.py +120 -0
  19. py_vucem/utils/banxico.py +209 -0
  20. py_vucem/utils.py +93 -0
  21. py_vucem/wsdl/DigitalizarDocumento.xsd +110 -0
  22. py_vucem/wsdl/DigitalizarDocumentoService.wsdl +87 -0
  23. py_vucem/wsdl/mv/ConsultaManifestacionService.wsdl +87 -0
  24. py_vucem/wsdl/mv/ConsultaManifestacionService.xsd +165 -0
  25. py_vucem/wsdl/mv/IngresoManifestacionService.wsdl +96 -0
  26. py_vucem/wsdl/mv/IngresoManifestacionService.xsd +306 -0
  27. py_vucem/wsdl/pedimentos/ConsultarEstadoPedimentosService.wsdl +56 -0
  28. py_vucem/wsdl/pedimentos/ConsultarEstadoPedimentosService.xsd +138 -0
  29. py_vucem/wsdl/pedimentos/ConsultarPartida.wsdl +56 -0
  30. py_vucem/wsdl/pedimentos/ConsultarPartida.xsd +46 -0
  31. py_vucem/wsdl/pedimentos/ConsultarPedimentoCompleto.wsdl +56 -0
  32. py_vucem/wsdl/pedimentos/ConsultarPedimentoCompleto.xsd +915 -0
  33. py_vucem/wsdl/pedimentos/ConsultarRemesasService.wsdl +57 -0
  34. py_vucem/wsdl/pedimentos/ConsultarRemesasService.xsd +65 -0
  35. py_vucem/wsdl/pedimentos/ListarPedimentosService.wsdl +57 -0
  36. py_vucem/wsdl/pedimentos/ListarPedimentosService.xsd +125 -0
  37. py_vucem/wsdl/pedimentos/comunes.xsd +439 -0
  38. py_vucem/wsdl/pedimentos/unidaddemedida.xsd +32 -0
  39. py_vucem/wsdl/respuesta.xsd +116 -0
  40. py_vucem-0.1.0.dist-info/METADATA +410 -0
  41. py_vucem-0.1.0.dist-info/RECORD +42 -0
  42. py_vucem-0.1.0.dist-info/WHEEL +4 -0
py_vucem/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from py_vucem.client import VucemClient
4
+ from py_vucem.tipos_documento import TipoDocumento, TIPOS_DOCUMENTO
5
+ from py_vucem.utils.banxico import BanxicoClient, BanxicoError
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,7 @@
1
+ class VucemError(Exception):
2
+ """Clase base para todas las excepciones de py_vucem."""
3
+ pass
4
+
5
+ class VucemDocumentoInvalidoError(VucemError):
6
+ """Excepción lanzada cuando un documento viola las reglas físicas de VUCEM."""
7
+ pass
@@ -0,0 +1,4 @@
1
+ from py_vucem.models.mv_mapper import desde_modelo
2
+ from py_vucem.models.mv_xml import generar_xml, generar_xml_cuerpo, validar_xml
3
+
4
+ __all__ = ["desde_modelo", "generar_xml", "generar_xml_cuerpo", "validar_xml"]
@@ -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})")