2026-07-09 09:48:30 -05:00
|
|
|
"""
|
|
|
|
|
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
|
|
|
|
|
"""
|
|
|
|
|
|
2026-10-14 22:42:37 -05:00
|
|
|
import logging
|
2026-07-09 09:48:30 -05:00
|
|
|
import os
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
|
|
import email_utilities
|
|
|
|
|
import outlook_utilities
|
|
|
|
|
import database_utilities
|
2026-10-14 22:28:48 -05:00
|
|
|
import adjuntos
|
|
|
|
|
import documentos as docs
|
|
|
|
|
import persistencia
|
|
|
|
|
import draft_sap
|
|
|
|
|
import notificaciones
|
2026-10-22 08:55:54 -05:00
|
|
|
import observaciones
|
|
|
|
|
import respuesta_proveedor
|
2026-07-09 09:48:30 -05:00
|
|
|
import sapb1_manager
|
2026-10-19 23:15:23 -05:00
|
|
|
import seguimiento
|
2026-10-14 22:28:48 -05:00
|
|
|
from pipeline_utils import log
|
2026-09-10 13:21:04 -05:00
|
|
|
|
2026-10-14 22:42:37 -05:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2026-09-10 13:21:04 -05:00
|
|
|
|
2026-10-19 23:15:23 -05:00
|
|
|
# ============================================================================
|
|
|
|
|
# SEGUIMIENTO (códigos y resultado por comprobante)
|
|
|
|
|
# ============================================================================
|
|
|
|
|
def _registrar_comprobante(email: dict, factura: dict) -> dict | None:
|
|
|
|
|
"""
|
|
|
|
|
Registra la factura en ``seguimiento`` y devuelve su registro (con el código
|
|
|
|
|
de seguimiento), o None si falla: el seguimiento nunca debe impedir
|
|
|
|
|
procesar la factura.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
registro = seguimiento.registrar_comprobante(
|
|
|
|
|
email["id"], factura.get("ruc_emisor") or "", factura.get("tipo", "01"),
|
|
|
|
|
factura.get("serie", ""), factura.get("numero", ""),
|
|
|
|
|
)
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
log(f"⚠️ No se pudo registrar el comprobante en seguimiento: {ex}", severity=logging.WARNING)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
log(f"🔖 Código de seguimiento: {registro['codigo_seguimiento']}")
|
|
|
|
|
if registro["reenvio"]:
|
|
|
|
|
log(f"⚠️ Comprobante ya recibido antes ({registro['veces_recibido']} veces, estado: {registro['estado']})", severity=logging.WARNING)
|
|
|
|
|
return registro
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _registrar_resultado(registro: dict | None, estado: str, **kwargs) -> None:
|
|
|
|
|
"""Guarda el resultado del comprobante; si falla, solo lo registra en el log."""
|
|
|
|
|
if not registro:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
seguimiento.actualizar_resultado(registro["codigo_seguimiento"], estado, **kwargs)
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
log(f"⚠️ No se pudo guardar el resultado en seguimiento: {ex}", severity=logging.WARNING)
|
|
|
|
|
|
|
|
|
|
|
2026-10-22 08:55:54 -05:00
|
|
|
def _registrar_correo(email: dict) -> dict | None:
|
|
|
|
|
"""Como ``_registrar_comprobante``, para un correo sin comprobante identificable."""
|
|
|
|
|
try:
|
|
|
|
|
registro = seguimiento.registrar_correo(email["id"])
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
log(f"⚠️ No se pudo registrar el correo en seguimiento: {ex}", severity=logging.WARNING)
|
|
|
|
|
return None
|
|
|
|
|
log(f"🔖 Código de seguimiento: {registro['codigo_seguimiento']}")
|
|
|
|
|
return registro
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rechazar_correo(email: dict, token: str, email_addr: str, lista_obs: list) -> None:
|
|
|
|
|
"""
|
|
|
|
|
Rechaza un correo que no trae ningún comprobante válido: registra el motivo y
|
|
|
|
|
responde al remitente con la plantilla de rechazo (si las protecciones lo permiten).
|
|
|
|
|
"""
|
|
|
|
|
log(f"❌ Correo rechazado: {lista_obs[0].titulo}", severity=logging.WARNING)
|
|
|
|
|
registro = _registrar_correo(email)
|
|
|
|
|
_registrar_resultado(registro, "rechazado", observaciones=[o.a_dict() for o in lista_obs])
|
|
|
|
|
if not registro or not token:
|
|
|
|
|
return
|
|
|
|
|
respuesta_proveedor.responder(
|
|
|
|
|
token, email_addr, email, "rechazado",
|
|
|
|
|
respuesta_proveedor.contexto_rechazo_correo(email, registro, lista_obs), registro, sin_comprobante=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 09:48:30 -05:00
|
|
|
# ============================================================================
|
|
|
|
|
# PASO 1: Autenticación Microsoft Graph
|
|
|
|
|
# ============================================================================
|
|
|
|
|
def step_authenticate() -> str | None:
|
|
|
|
|
try:
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.info("Intentando obtener token de acceso...")
|
2026-07-09 09:48:30 -05:00
|
|
|
token = outlook_utilities.get_access_token()
|
|
|
|
|
if token:
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.info("✅ Token obtenido exitosamente.")
|
|
|
|
|
logger.info(f"Token prefix: {token[:25]}...")
|
2026-07-09 09:48:30 -05:00
|
|
|
return token
|
|
|
|
|
return None
|
|
|
|
|
except Exception as e:
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.error(f"❌ Error: {e}")
|
2026-07-09 09:48:30 -05:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# PASO 2: Descarga de correos
|
|
|
|
|
# ============================================================================
|
|
|
|
|
def step_fetch_emails(token: str, email: str, fecha_inicio: str, fecha_fin: str) -> list[dict]:
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.info("=" * 60)
|
|
|
|
|
logger.info(f"[MAIL] {email}")
|
2026-07-09 09:48:30 -05:00
|
|
|
try:
|
|
|
|
|
emails = outlook_utilities.get_emails(email, token, fecha_inicio, fecha_fin)
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.info(f"Se encontraron {len(emails)} correos.")
|
2026-07-09 09:48:30 -05:00
|
|
|
return emails
|
|
|
|
|
except Exception as e:
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.error(f"❌ Error: {e}")
|
2026-07-09 09:48:30 -05:00
|
|
|
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
|
|
|
|
|
# ========================================================================
|
|
|
|
|
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.info(f"📬 [{email['fecha']}] {email['subject'][:80]}...")
|
2026-10-14 22:28:48 -05:00
|
|
|
log("📂 Adjuntos:")
|
2026-07-09 09:48:30 -05:00
|
|
|
|
|
|
|
|
# 1-2. BD: verificar existencia y guardar
|
|
|
|
|
exists = database_utilities.get_existing_ids([email["id"]])
|
|
|
|
|
if email["id"] in exists:
|
2026-10-14 22:28:48 -05:00
|
|
|
log("⏭️ Correo ya procesado, saltando")
|
2026-07-09 09:48:30 -05:00
|
|
|
return
|
|
|
|
|
database_utilities.save_emails([email])
|
|
|
|
|
|
|
|
|
|
# 3. Descargar adjuntos
|
2026-10-14 22:28:48 -05:00
|
|
|
lista_adjuntos = adjuntos.descargar_adjuntos(email, email_addr, token, download_folder)
|
|
|
|
|
if not lista_adjuntos:
|
2026-10-22 08:55:54 -05:00
|
|
|
log("⏭️ Correo sin adjuntos") # no se responde: no hubo intento de enviar un comprobante
|
2026-07-09 09:48:30 -05:00
|
|
|
return
|
2026-10-22 08:55:54 -05:00
|
|
|
n_recibidos = len(lista_adjuntos)
|
2026-07-09 09:48:30 -05:00
|
|
|
|
|
|
|
|
# 4. Descomprimir ZIPs
|
2026-10-14 22:28:48 -05:00
|
|
|
email_utilities.descomprimir_adjuntos([email], download_folder)
|
2026-07-09 09:48:30 -05:00
|
|
|
|
|
|
|
|
# 5. Filtrar PDF/XML y limpiar si no hay válidos
|
2026-10-14 22:28:48 -05:00
|
|
|
if not adjuntos.filtrar_y_limpiar(email, download_folder):
|
2026-10-22 08:55:54 -05:00
|
|
|
_rechazar_correo(email, token, email_addr, [observaciones.sin_pdf_xml(n_recibidos)])
|
2026-07-09 09:48:30 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 6. Procesar PDFs
|
2026-10-14 22:28:48 -05:00
|
|
|
pdf_cache = docs.procesar_pdfs(email, download_folder)
|
2026-07-09 09:48:30 -05:00
|
|
|
|
|
|
|
|
# 7. Procesar XMLs
|
2026-10-14 22:28:48 -05:00
|
|
|
documentos = docs.procesar_xmls(email, download_folder, pdf_cache)
|
2026-07-09 09:48:30 -05:00
|
|
|
if not documentos:
|
2026-10-14 22:28:48 -05:00
|
|
|
log("⏭️ No se extrajeron documentos XML")
|
2026-10-22 08:55:54 -05:00
|
|
|
if any(a.lower().endswith(".xml") for a in email["adjuntos"]):
|
|
|
|
|
motivo = observaciones.sin_factura([])
|
|
|
|
|
else:
|
|
|
|
|
motivo = observaciones.falta_xml(sum(a.lower().endswith(".pdf") for a in email["adjuntos"]))
|
|
|
|
|
_rechazar_correo(email, token, email_addr, [motivo])
|
2026-07-09 09:48:30 -05:00
|
|
|
return
|
|
|
|
|
|
2026-09-02 07:40:26 -05:00
|
|
|
# 8. Completar guías faltantes desde PDF
|
2026-10-14 22:28:48 -05:00
|
|
|
n_guias = docs.completar_guias(documentos, pdf_cache)
|
2026-09-02 07:40:26 -05:00
|
|
|
if n_guias:
|
2026-10-14 22:28:48 -05:00
|
|
|
log(f"🔄 {n_guias} guías completadas desde PDF")
|
2026-07-09 09:48:30 -05:00
|
|
|
|
2026-09-02 07:40:26 -05:00
|
|
|
# 9. Validar que toda guía tenga tipo, serie y número
|
2026-10-14 22:28:48 -05:00
|
|
|
docs.validar_guias(documentos)
|
2026-09-02 07:40:26 -05:00
|
|
|
|
|
|
|
|
# 10. Guardar resultado con todos los documentos
|
2026-10-14 22:28:48 -05:00
|
|
|
paths_resultado = persistencia.guardar_resultado(documentos, email.get("datos_pdf", []), output_folder)
|
2026-09-02 07:40:26 -05:00
|
|
|
email["resultado_paths"] = paths_resultado
|
|
|
|
|
|
|
|
|
|
# 11. Filtrar solo facturas para SAP
|
2026-07-09 09:48:30 -05:00
|
|
|
facturas = [d for d in documentos if d.get("tipo") == "01"]
|
|
|
|
|
if not facturas:
|
2026-10-14 22:28:48 -05:00
|
|
|
log("⏭️ No hay facturas (tipo 01) para SAP")
|
2026-10-22 08:55:54 -05:00
|
|
|
_rechazar_correo(email, token, email_addr,
|
|
|
|
|
[observaciones.sin_factura(sorted({d.get("tipo_descripcion", "?") for d in documentos}))])
|
2026-07-09 09:48:30 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
for factura in facturas:
|
|
|
|
|
serie = factura.get("serie", "?")
|
|
|
|
|
numero = factura.get("numero", "?")
|
|
|
|
|
|
2026-10-14 22:28:48 -05:00
|
|
|
log(f"📄 Factura: {serie}-{numero}")
|
2026-10-19 23:15:23 -05:00
|
|
|
registro = _registrar_comprobante(email, factura)
|
2026-09-02 07:40:26 -05:00
|
|
|
|
2026-07-09 09:48:30 -05:00
|
|
|
adjuntos_email = [p for p in [factura.get("ruta_xml"), factura.get("ruta_pdf")] if p]
|
2026-09-02 07:40:26 -05:00
|
|
|
for p in email.get("resultado_paths", []):
|
|
|
|
|
if p and os.path.exists(p) and p not in adjuntos_email:
|
|
|
|
|
adjuntos_email.append(p)
|
2026-07-09 09:48:30 -05:00
|
|
|
|
2026-10-22 08:55:54 -05:00
|
|
|
estado = None # aceptado | rechazado | error
|
|
|
|
|
detalle_error = None
|
|
|
|
|
lista_obs: list = []
|
|
|
|
|
resultados_val: list = []
|
|
|
|
|
draft: dict = {}
|
|
|
|
|
validation_table = valor_referencial_table = ""
|
2026-07-09 09:48:30 -05:00
|
|
|
|
2026-10-22 08:55:54 -05:00
|
|
|
try:
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
# 14. Guardar draft JSON (siempre, para depuración)
|
|
|
|
|
persistencia.guardar_draft_json(draft, serie, numero, output_folder)
|
|
|
|
|
|
|
|
|
|
detalle_error = draft_sap.hay_error_de_consulta(resultados_val)
|
|
|
|
|
if not todas_coinciden:
|
|
|
|
|
lista_obs = observaciones.desde_validacion(factura, resultados_val)
|
|
|
|
|
# "guía no encontrada" también es lo que se ve si HANA está caído: no culpar al proveedor
|
|
|
|
|
if (not detalle_error and any(o.codigo == "guia_no_encontrada" for o in lista_obs)
|
|
|
|
|
and not draft_sap.hana_disponible()):
|
|
|
|
|
detalle_error = "SAP HANA no responde: no se puede confirmar que las guías no existan"
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
detalle_error = f"{type(ex).__name__}: {ex}"
|
|
|
|
|
|
|
|
|
|
# 15. Crear draft en SAP o generar el resultado
|
|
|
|
|
if detalle_error:
|
|
|
|
|
estado = "error"
|
|
|
|
|
log(f"❌ Error validando {serie}-{numero}: {detalle_error}", severity=logging.ERROR)
|
|
|
|
|
elif todas_coinciden:
|
2026-07-09 09:48:30 -05:00
|
|
|
try:
|
|
|
|
|
sap_result = sapb1_manager.create_draft(draft)
|
|
|
|
|
doc_entry = sap_result.get("DocEntry")
|
2026-10-22 08:55:54 -05:00
|
|
|
estado = "aceptado"
|
2026-10-14 22:28:48 -05:00
|
|
|
log(f"✅ Draft SAP B1 creado: {serie}-{numero} → DocEntry {doc_entry}")
|
2026-10-19 23:15:23 -05:00
|
|
|
_registrar_resultado(registro, "aceptado", doc_entry=doc_entry)
|
2026-10-14 22:28:48 -05:00
|
|
|
html_body = notificaciones.build_notification_body("created", factura, validation_table, valor_referencial_table, doc_entry=doc_entry)
|
2026-07-09 09:48:30 -05:00
|
|
|
except Exception as ex:
|
2026-10-22 08:55:54 -05:00
|
|
|
estado, detalle_error = "error", str(ex)
|
2026-10-14 22:42:37 -05:00
|
|
|
log(f"❌ Error creando draft {serie}-{numero}: {ex}", severity=logging.ERROR)
|
2026-07-09 09:48:30 -05:00
|
|
|
else:
|
2026-10-22 08:55:54 -05:00
|
|
|
estado = "rechazado"
|
2026-10-14 22:28:48 -05:00
|
|
|
log(f"⏭️ Draft NO creado: {serie}-{numero} — validación falló")
|
2026-10-22 08:55:54 -05:00
|
|
|
_registrar_resultado(registro, "rechazado", observaciones=[o.a_dict() for o in lista_obs])
|
2026-10-14 22:28:48 -05:00
|
|
|
html_body = notificaciones.build_notification_body("skipped", factura, validation_table, valor_referencial_table)
|
2026-07-09 09:48:30 -05:00
|
|
|
|
2026-10-22 08:55:54 -05:00
|
|
|
if estado == "error":
|
|
|
|
|
_registrar_resultado(registro, "error", observaciones=[{"codigo": "error_interno", "detalle": detalle_error}])
|
|
|
|
|
html_body = notificaciones.build_notification_body("error", factura, validation_table, valor_referencial_table, error=detalle_error)
|
|
|
|
|
|
|
|
|
|
# 16. Notificar: correo técnico solo al equipo; respuesta con plantilla al remitente
|
|
|
|
|
if not token:
|
|
|
|
|
continue
|
|
|
|
|
respuesta_proveedor.enviar_a_equipo(
|
|
|
|
|
token, email_addr, f"Draft SAP B1 - {serie}-{numero}",
|
|
|
|
|
respuesta_proveedor.encabezado_tecnico(email, registro) + html_body, adjuntos_email,
|
|
|
|
|
)
|
|
|
|
|
if registro is None:
|
|
|
|
|
log("⚠️ Sin código de seguimiento: no se responde al proveedor", severity=logging.WARNING)
|
|
|
|
|
elif estado == "aceptado":
|
|
|
|
|
respuesta_proveedor.responder(
|
|
|
|
|
token, email_addr, email, "aceptado",
|
|
|
|
|
respuesta_proveedor.contexto_aceptado(email, registro, factura, resultados_val, draft), registro,
|
|
|
|
|
)
|
|
|
|
|
elif estado == "rechazado":
|
|
|
|
|
respuesta_proveedor.responder(
|
|
|
|
|
token, email_addr, email, "rechazado",
|
|
|
|
|
respuesta_proveedor.contexto_rechazo_factura(email, registro, factura, resultados_val, lista_obs), registro,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
respuesta_proveedor.omitir_respuesta(registro, "error interno: se avisó solo al equipo")
|
2026-07-09 09:48:30 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# 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")
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.info(f"Rango de búsqueda: {fecha_inicio} al {fecha_fin}")
|
2026-07-09 09:48:30 -05:00
|
|
|
|
|
|
|
|
# 1. Autenticar
|
|
|
|
|
token = step_authenticate()
|
|
|
|
|
if not token:
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.error("❌ No se pudo autenticar. Abortando.")
|
2026-07-09 09:48:30 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 2. Descargar correos
|
|
|
|
|
emails = step_fetch_emails(token, EMAIL, fecha_inicio, fecha_fin)
|
|
|
|
|
if not emails:
|
2026-10-14 22:42:37 -05:00
|
|
|
logger.info("No se encontraron correos.")
|
2026-07-09 09:48:30 -05:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 3. Bucle por cada correo
|
|
|
|
|
for email in emails:
|
|
|
|
|
_procesar_un_correo(email, token, EMAIL, DOWNLOAD_FOLDER, OUTPUT_FOLDER)
|