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