Files
descarga_factura_transporte/docs/pipeline.md
T
GeorgeandClaude Sonnet 5.5 f0ad6aa78e se valida si la factura ya existe en SAP y se avisa cuando SAP está fuera de línea
- sapb1_manager.buscar_factura_existente busca la factura (proveedor, tipo,
  serie, número) en facturas de compra no anuladas y en drafts; si existe no
  se crea otro draft y se responde "ya está registrada" sin pisar el registro
  de una aceptación anterior.
- SapNoDisponible (subclase de ConnectionError) distingue una caída (timeout,
  sin conexión, HTTP 5xx) de un rechazo normal de SAP (HTTP 4xx). Una caída
  de Service Layer con HANA sin resultados ya no se lee como "guía no existe".
- Con SAP o HANA caídos se avisa una sola vez al proveedor con la plantilla
  "En cola" y el correo se reintenta en los ciclos siguientes (hasta 24), sin
  repetir avisos; al agotarse se avisa al equipo.
- La lógica por factura pasa a _procesar_factura; lo ya resuelto dentro de un
  correo no se repite al reintentarlo.
- seguimiento: columna intentos (con migración), registrar_intento y el
  estado de notificación en_espera.
- database_utilities.DB_NAME pasa a ser absoluto (junto al código).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 12:08:38 -05:00

11 KiB

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()

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()

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<br>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<br>u otro error?}
    T -- Caída --> FL[estado fuera de línea:<br>aviso 'En cola' una vez<br>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<br>+ respuesta con plantilla al remitente]
    V --> W
    X --> W2[16. Correo técnico al equipo<br>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

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.

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.