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

210 lines
11 KiB
Markdown

# 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<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](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.