# pipeline.py — run_pipeline ## Estructura general ``` pipeline.py ├── PASO 1: step_authenticate() → token | None ├── PASO 2: step_fetch_emails() → list[dict] ├── _procesar_un_correo() → None (orquesta los módulos de abajo) └── run_pipeline() → None Módulos que orquesta _procesar_un_correo(): ├── adjuntos.py descargar_adjuntos, filtrar_y_limpiar ├── documentos.py procesar_pdfs, procesar_xmls, completar_guias, validar_guias, copiar_a_red ├── persistencia.py guardar_resultado, guardar_draft_json ├── draft_sap.py validate_prices, obtener_valor_referencial_sap, build_sap_draft ├── notificaciones.py build_validation_html, build_valor_referencial_html, build_notification_body (correo técnico) ├── observaciones.py motivos de rechazo con sus textos ├── respuesta_proveedor.py respuesta con plantilla al remitente (modo prueba/real, protecciones) ├── plantillas.py renderizador de las plantillas HTML └── pipeline_utils.py log, ruta_carpeta ``` --- ## Proceso de `run_pipeline()` ```mermaid flowchart TD A[run_pipeline] --> B[init_db] B --> C[Calcular rango fechas: ayer-dias → hoy] C --> D[step_authenticate] D --> E{Token obtenido?} E -- No --> F[❌ Abortar] E -- Sí --> G[step_fetch_emails] G --> H{Hay correos?} H -- No --> I[Fin] H -- Sí --> J[Bucle: por cada email] J --> K[_procesar_un_correo]:::highlight K --> J classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px; ``` --- ## Proceso de `_procesar_un_correo()` ```mermaid flowchart TD A[_procesar_un_correo] --> B[1-2. BD: ¿ya procesado?] B -- Sí --> C[⏭️ Saltar] B -- No --> D[Guardar en BD] D --> E[3. descargar_adjuntos] E --> F{Hay adjuntos?} F -- No --> C F -- Sí --> G[4. _descomprimir ZIPs] G --> H[5. filtrar_y_limpiar] H --> I{Hay PDF/XML?} I -- No --> RC[Rechazo del correo
responder al remitente] I -- Sí --> J[6. procesar_pdfs]:::highlight J --> K[7. procesar_xmls]:::highlight K --> K2[8. completar_guias] K2 --> K3[9. validar_guias] K3 --> L{Hay documentos?} L -- No --> RC L -- Sí --> M[10. guardar_resultado] M --> N[11. Filtrar facturas tipo 01] N --> O{Hay facturas?} O -- No --> RC O -- Sí --> P[Bucle: por cada factura] P --> P2{11.5 ¿Ya existe en SAP?} P2 -- Sí --> V P2 -- No --> Q[12. validate_prices]:::highlight Q --> Q2[12.5 obtener_valor_referencial_sap] Q2 --> R[13. build_sap_draft]:::highlight R --> S[14. guardar_draft_json] S --> T{¿SAP/HANA caídos
u otro error?} T -- Caída --> FL[estado fuera de línea:
aviso 'En cola' una vez
y el correo se reintenta] T -- Otro error --> X[estado error] T -- No --> T2{Todas coinciden?} T2 -- Sí --> U[15. sapb1_manager.create_draft] T2 -- No --> V[estado rechazado + observaciones] U --> U2[estado aceptado] U2 --> W[16. Correo técnico al equipo
+ respuesta con plantilla al remitente] V --> W X --> W2[16. Correo técnico al equipo
NO se responde al proveedor] FL --> P W --> P W2 --> P classDef highlight fill:#fff3e0,stroke:#e65100,stroke-width:3px; ``` `procesar_xmls` (paso 7) incluye internamente la copia de PDF/XML a la ruta de red (`copiar_a_red`, ver más abajo) por cada comprobante procesado. --- ## Funciones por módulo > Son funciones de módulo (antes anidadas en `_procesar_un_correo`) y reciben explícitamente `email`, `download_folder`, `output_folder`, etc. `_procesar_un_correo` solo las orquesta. | Función | Módulo | Propósito | |---------|--------|-----------| | `ruta_carpeta()` | `pipeline_utils` | Resuelve una ruta relativa al directorio del proyecto | | `log(msg, level, last)` | `pipeline_utils` | Imprime con indentación de árbol | | `copiar_a_red(data)` | `documentos` | Copia PDF/XML de un comprobante a la ruta de red configurable | | `descargar_adjuntos(email, email_addr, token, download_folder)` | `adjuntos` | Descarga adjuntos vía Microsoft Graph | | `filtrar_y_limpiar(email, download_folder)` | `adjuntos` | Filtra solo PDF/XML, elimina el resto | | **`procesar_pdfs(email, download_folder)`** | `documentos` | **Extrae datos de PDFs, renombra, genera cache** | | **`procesar_xmls(email, download_folder, pdf_cache)`** | `documentos` | **Procesa XMLs, cruza con PDF, asigna guías, copia a red** | | `completar_guias(documentos, pdf_cache)` | `documentos` | Completa guías faltantes en líneas XML desde el PDF cache | | `validar_guias(documentos)` | `documentos` | Advierte (log) guías con datos incompletos (falta tipo/serie/numero) | | `guardar_resultado(documentos, datos_pdf, output_folder)` | `persistencia` | Guarda JSON con datos extraídos (XML y PDF) | | `guardar_draft_json(draft, serie, numero, output_folder)` | `persistencia` | Guarda draft JSON para depuración | | `calcular_fecha_vencimiento(fecha_base, condicion_pago)` | `draft_sap` | Calcula `DocDueDate` = fecha de contabilización + días de `PaymentTermsTypes` | | **`build_sap_draft(factura, resultados_val)`** | `draft_sap` | **Construye payload del draft SAP B1 (incluye `U_VS_ANEXO_PDF`/`U_VS_ANEXO_XML`, `DocDueDate` por condición de pago)** | | **`validate_prices(factura)`** | `draft_sap` | **Valida precios contra DeliveryNotes/Orders SAP** | | `obtener_valor_referencial_sap(factura)` | `draft_sap` | Consulta `Sp_ObtenerValorReferencial` en HANA por cada guía única | | `build_validation_html(resultados)` | `notificaciones` | Genera tabla HTML de validación de precios | | `build_valor_referencial_html(factura, guias_valor_ref)` | `notificaciones` | Genera tabla HTML comparando valor referencial XML vs SAP | | `build_notification_body(status, factura, validation_table, valor_referencial_table, **kwargs)` | `notificaciones` | Arma cuerpo del email de notificación | | `_procesar_factura(email, token, email_addr, factura, output_folder)` | `pipeline` | Valida una factura contra SAP, crea el draft y notifica; devuelve si hay que reintentar el correo | | `importes_coinciden(a, b)` | `pipeline_utils` | Compara importes a nivel de céntimos: diferencia redondeada a 2 decimales, hasta 1 céntimo | | `version_del_proyecto()` | `pipeline_utils` | Versión de `pyproject.toml`, para el log de arranque | | `hay_error_de_consulta(resultados_val)` | `draft_sap` | Detalle del primer error de consulta a SAP que dejó `validate_prices`, o `None` | | `hana_disponible()` | `draft_sap` | Abre y cierra una conexión a HANA: descarta que una guía "no encontrada" sea una caída | | `buscar_factura_existente(ruc, tipo, serie, numero)` | `sapb1_manager` | ¿La factura ya está en SAP (factura de compra no anulada o draft)? Ver [sapb1_manager.md](sapb1_manager.md) | --- ## `copiar_a_red(data)` — Copia a ruta de red Copia el PDF y el XML de cada comprobante procesado a una ruta de red UNC configurable, en paralelo al guardado local existente (no lo reemplaza). No falla el pipeline si la ruta no está configurada o no está disponible: solo registra una advertencia. - **Configuración** (`.env`): `RUTA_RED_FACTURACION` (ruta base UNC) y `RUC_EMPRESA` (RUC fijo de Aberio). - **Estructura de destino**: ``` {RUTA_RED_FACTURACION}\{año}\{mes}\{día}\{tipo}\{RUC_EMPRESA}\{ruc_emisor}\ {RUC_EMPRESA}-{tipo}-{serie}-{numero}_FormatoImpreso.pdf {RUC_EMPRESA}-{tipo}-{serie}-{numero}.xml ``` `RUC_EMPRESA` es el RUC de Aberio (fijo); `ruc_emisor` es el RUC del proveedor que emitió la factura (varía por comprobante). Año/mes/día salen de `fecha_emision`, sin ceros a la izquierda en mes/día. - Guarda las rutas resultantes en `data["ruta_red_pdf"]` / `data["ruta_red_xml"]`, que luego usa `build_sap_draft` para llenar `U_VS_ANEXO_PDF` / `U_VS_ANEXO_XML` en el draft SAP B1. - Se invoca desde `procesar_xmls`, justo después de resolver `data["ruta_pdf"]`. --- ## Flujo de llamadas ``` run_pipeline ├── database_utilities.init_db ├── step_authenticate │ └── outlook_utilities.get_access_token ├── step_fetch_emails │ └── outlook_utilities.get_emails └── _procesar_un_correo (por cada email) ├── database_utilities.get_existing_ids / save_emails ├── descargar_adjuntos │ └── outlook_utilities.download_attachments ├── _descomprimir │ └── email_utilities.descomprimir_adjuntos ├── filtrar_y_limpiar ├── procesar_pdfs │ └── pdf_utilities.extraer_datos_pdf / renombrar_pdf ├── procesar_xmls │ ├── sunat_schema.extraer_datos_xml │ ├── pdf_utilities.extraer_referencias │ └── copiar_a_red ├── completar_guias ├── validar_guias ├── guardar_resultado │ └── sunat_schema.split_serie_numero, pdf_utilities.TIPO_MAP ├── (por cada factura tipo 01) │ ├── validate_prices │ │ ├── sapb1_manager.get_zonas │ │ └── sapb1_manager.get_delivery_notes │ │ ├── hana_manager.get_delivery (fallback si SL no responde; │ │ │ busca en ODLN y, si no hay, en OWTR) │ │ └── si el DocEntry vino de OWTR (StockTransfers), resuelve │ │ además la Orden de Venta asociada (Orders vía │ │ U_IDC_NUMOV) para poder validar las líneas FL* │ ├── build_validation_html │ ├── obtener_valor_referencial_sap │ │ └── hana_manager.obtener_valor_referencial │ ├── build_valor_referencial_html │ ├── build_sap_draft │ │ └── sapb1_manager.obtener_condicion_pago (BusinessPartners + │ │ PaymentTermsTypes, para calcular DocDueDate) │ ├── guardar_draft_json │ ├── sapb1_manager.create_draft (si todas las líneas coinciden) │ ├── build_notification_body │ ├── respuesta_proveedor.enviar_a_equipo (correo técnico, solo equipo) │ └── respuesta_proveedor.responder (plantilla aceptado/rechazado, ver respuesta_proveedor.md) ``` --- ## Qué se envía y a quién (paso 16) | Resultado de la factura | Correo técnico (tablas de SAP, JSON adjuntos) | Respuesta con plantilla | |-------------------------|-----------------------------------------------|-------------------------| | `aceptado` (draft creado) | Equipo | Aceptado, al remitente | | `rechazado` (precio, guía, o la factura **ya existe en SAP**) | Equipo | Rechazado con observaciones, al remitente | | `fuera_de_linea` (SAP o HANA no responden) | Equipo (primer intento y al agotarse) | "En cola", **una sola vez**; el correo se reintenta | | `error` (fallo inesperado, rechazo normal de SAP) | Equipo | **Ninguna**: es una falla nuestra, no del proveedor | | Correo sin comprobante válido (sin PDF/XML, sin XML, sin factura) | — | Rechazado sin comprobante, al remitente | | Correo sin adjuntos | — | Ninguna | "Al remitente" depende de `NOTIFICACION_MODO` (en modo `prueba` va al equipo) y de las protecciones. Todo en [respuesta_proveedor.md](respuesta_proveedor.md). Un fallo inesperado al procesar una factura ya no interrumpe el ciclo: se registra como `error`, se avisa al equipo y se sigue con la siguiente. La lógica por factura vive en `_procesar_factura`; `_procesar_un_correo` la llama una vez por factura y, si alguna pide reintento, deja el correo en cola.