Files
descarga_factura_transporte/pipeline.py
T
GeorgeandClaude Sonnet 5.5 6cc5e03209 se divide pipeline.py en módulos (adjuntos, documentos, persistencia, draft_sap, notificaciones)
Las funciones anidadas de _procesar_un_correo pasan a funciones de módulo
con parámetros explícitos; pipeline.py queda solo como orquestador.
copiar_a_red lee las variables de entorno al ejecutarse. Se actualizan docs.

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

231 lines
9.0 KiB
Python

"""
Pipeline de procesamiento de facturas de transporte.
Cada correo se procesa individualmente en un bucle:
BD check → descargar adjuntos → descomprimir ZIPs → filtrar PDF/XML
→ procesar PDFs (lookup) → procesar XMLs (cruzar con PDF)
→ guardar resultado → construir draft SAP → validar precios
→ crear draft en SAP (si válido) → notificar → guardar draft JSON
"""
import os
from datetime import datetime, timedelta
import email_utilities
import outlook_utilities
import database_utilities
import adjuntos
import documentos as docs
import persistencia
import draft_sap
import notificaciones
import sapb1_manager
from pipeline_utils import log
# ============================================================================
# PASO 1: Autenticación Microsoft Graph
# ============================================================================
def step_authenticate() -> str | None:
try:
print("Intentando obtener token de acceso...")
token = outlook_utilities.get_access_token()
if token:
print("✅ Token obtenido exitosamente.")
print(f"Token prefix: {token[:25]}...")
return token
return None
except Exception as e:
print(f"❌ Error: {e}")
return None
# ============================================================================
# PASO 2: Descarga de correos
# ============================================================================
def step_fetch_emails(token: str, email: str, fecha_inicio: str, fecha_fin: str) -> list[dict]:
print("\n" + "=" * 60)
print(f"[MAIL] {email}")
try:
emails = outlook_utilities.get_emails(email, token, fecha_inicio, fecha_fin)
print(f"Se encontraron {len(emails)} correos.")
return emails
except Exception as e:
print(f"❌ Error: {e}")
return []
# ============================================================================
# PROCESAR UN CORREO
# ============================================================================
def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folder: str, output_folder: str) -> None:
"""
Procesa un correo completo de principio a fin.
Flujo (pasos 1-14):
1-2. Verifica en BD si el correo ya fue procesado; si no, lo guarda.
3. Descarga adjuntos desde Microsoft Graph.
4. Descomprime archivos ZIP.
5. Filtra solo archivos PDF/XML.
6. Procesa PDFs (extrae datos, renombra, construye cache).
7. Procesa XMLs (cruza con PDF, asigna guías de remisión).
8. Guarda resultado JSON con datos extraídos.
9. Filtra solo facturas tipo 01 para SAP.
10. Valida precios contra DeliveryNotes en SAP.
11. Construye payload del draft SAP B1.
12. Guarda draft JSON para depuración.
13. Crea el draft en SAP (si los precios coinciden) o notifica error.
14. Envía email de notificación con el resultado.
Args:
email: Diccionario del correo (debe tener ``id``, ``fecha``,
``subject``).
token: Token de acceso Microsoft Graph.
email_addr: Dirección de correo de la bandeja de entrada.
download_folder: Carpeta relativa para descargar adjuntos.
output_folder: Carpeta relativa para guardar resultados.
Returns:
None. Los resultados se persisten a disco (JSON) y se envían
notificaciones por email.
"""
# ========================================================================
# FLUJO DEL CORREO
# ========================================================================
print(f"\n📬 [{email['fecha']}] {email['subject'][:80]}...")
log("📂 Adjuntos:")
# 1-2. BD: verificar existencia y guardar
exists = database_utilities.get_existing_ids([email["id"]])
if email["id"] in exists:
log("⏭️ Correo ya procesado, saltando")
return
database_utilities.save_emails([email])
# 3. Descargar adjuntos
lista_adjuntos = adjuntos.descargar_adjuntos(email, email_addr, token, download_folder)
if not lista_adjuntos:
log("⏭️ Correo sin adjuntos")
return
# 4. Descomprimir ZIPs
email_utilities.descomprimir_adjuntos([email], download_folder)
# 5. Filtrar PDF/XML y limpiar si no hay válidos
if not adjuntos.filtrar_y_limpiar(email, download_folder):
return
# 6. Procesar PDFs
pdf_cache = docs.procesar_pdfs(email, download_folder)
# 7. Procesar XMLs
documentos = docs.procesar_xmls(email, download_folder, pdf_cache)
if not documentos:
log("⏭️ No se extrajeron documentos XML")
return
# 8. Completar guías faltantes desde PDF
n_guias = docs.completar_guias(documentos, pdf_cache)
if n_guias:
log(f"🔄 {n_guias} guías completadas desde PDF")
# 9. Validar que toda guía tenga tipo, serie y número
docs.validar_guias(documentos)
# 10. Guardar resultado con todos los documentos
paths_resultado = persistencia.guardar_resultado(documentos, email.get("datos_pdf", []), output_folder)
email["resultado_paths"] = paths_resultado
# 11. Filtrar solo facturas para SAP
facturas = [d for d in documentos if d.get("tipo") == "01"]
if not facturas:
log("⏭️ No hay facturas (tipo 01) para SAP")
return
for factura in facturas:
serie = factura.get("serie", "?")
numero = factura.get("numero", "?")
log(f"📄 Factura: {serie}-{numero}")
# 12. Validar precios (consulta SAP DNs, NO requiere draft)
todas_coinciden, resultados_val = draft_sap.validate_prices(factura)
validation_table = notificaciones.build_validation_html(resultados_val)
# 12.5 Comparar valor referencial del XML vs SAP (Sp_ObtenerValorReferencial)
guias_valor_ref = draft_sap.obtener_valor_referencial_sap(factura)
valor_referencial_table = notificaciones.build_valor_referencial_html(factura, guias_valor_ref)
# 13. Construir draft (usa LineTotal de los DNs, no del XML)
draft = draft_sap.build_sap_draft(factura, resultados_val)
adjuntos_email = [p for p in [factura.get("ruta_xml"), factura.get("ruta_pdf")] if p]
for p in email.get("resultado_paths", []):
if p and os.path.exists(p) and p not in adjuntos_email:
adjuntos_email.append(p)
# 14. Guardar draft JSON (siempre, para depuración)
persistencia.guardar_draft_json(draft, serie, numero, output_folder)
# 15. Crear draft en SAP o generar HTML
if todas_coinciden:
try:
sap_result = sapb1_manager.create_draft(draft)
doc_entry = sap_result.get("DocEntry")
log(f"✅ Draft SAP B1 creado: {serie}-{numero} → DocEntry {doc_entry}")
html_body = notificaciones.build_notification_body("created", factura, validation_table, valor_referencial_table, doc_entry=doc_entry)
except Exception as ex:
log(f"❌ Error creando draft {serie}-{numero}: {ex}")
html_body = notificaciones.build_notification_body("error", factura, validation_table, valor_referencial_table, error=ex)
else:
log(f"⏭️ Draft NO creado: {serie}-{numero} — validación falló")
html_body = notificaciones.build_notification_body("skipped", factura, validation_table, valor_referencial_table)
# 16. Enviar notificación al remitente original, con copia al equipo
if token:
destinatario = email.get("sender")
if destinatario and destinatario != "Desconocido":
outlook_utilities.send_email(
token, email_addr,
[destinatario],
f"Draft SAP B1 - {serie}-{numero}",
html_body, adjuntos_email,
cc_recipients=["jlopez@aberio.com.pe", "gcalle@pucp.pe"],
)
else:
log("⚠️ No se pudo determinar el remitente, notificación no enviada")
# ============================================================================
# PIPELINE COMPLETO
# ============================================================================
def run_pipeline(dias: int, EMAIL: str, DOWNLOAD_FOLDER: str, OUTPUT_FOLDER: str) -> None:
"""Ejecuta el pipeline completo: autentica, descarga correos y procesa cada uno."""
database_utilities.init_db()
hoy = datetime.now()
ayer = hoy - timedelta(days=dias)
fecha_inicio = ayer.strftime("%Y-%m-%d")
fecha_fin = hoy.strftime("%Y-%m-%d")
print(f"Rango de búsqueda: {fecha_inicio} al {fecha_fin}")
# 1. Autenticar
token = step_authenticate()
if not token:
print("❌ No se pudo autenticar. Abortando.")
return
# 2. Descargar correos
emails = step_fetch_emails(token, EMAIL, fecha_inicio, fecha_fin)
if not emails:
print("No se encontraron correos.")
return
# 3. Bucle por cada correo
for email in emails:
_procesar_un_correo(email, token, EMAIL, DOWNLOAD_FOLDER, OUTPUT_FOLDER)