se migran los print de los módulos a logging con nivel explícito
pipeline_utils.log pasa a usar logging (parámetro severity) y los print de pipeline, sapb1_manager, hana_manager, outlook_utilities, email_utilities y main pasan a logger.info/warning/error, de modo que LOG_LEVEL filtre por severidad. El texto del log no cambia. Se mantienen los print de CLI en sunat_schema (__main__), tools/ y tests/. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5ef9c90fc0
commit
3fa26e3ef6
11 files changed
+109
-79
No files matched your search
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
"""Descarga y filtrado de adjuntos de un correo."""
|
"""Descarga y filtrado de adjuntos de un correo."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
import outlook_utilities
|
import outlook_utilities
|
||||||
from pipeline_utils import log, ruta_carpeta
|
from pipeline_utils import log, ruta_carpeta
|
||||||
@@ -33,7 +34,7 @@ def descargar_adjuntos(email: dict, email_addr: str, token: str, download_folder
|
|||||||
return adjuntos
|
return adjuntos
|
||||||
return None
|
return None
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log(f"❌ Error descargando adjuntos: {ex}")
|
log(f"❌ Error descargando adjuntos: {ex}", severity=logging.ERROR)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
|---------|--------|-----|
|
|---------|--------|-----|
|
||||||
| `main.py` | 134 | Demonio orquestador (loop de horarios) |
|
| `main.py` | 134 | Demonio orquestador (loop de horarios) |
|
||||||
| `pipeline.py` | 230 | Orquestador del pipeline (autenticar, descargar correos, procesar un correo) |
|
| `pipeline.py` | 230 | Orquestador del pipeline (autenticar, descargar correos, procesar un correo) |
|
||||||
| `pipeline_utils.py` | 15 | `log()` con indentación de árbol y `ruta_carpeta()` |
|
| `pipeline_utils.py` | 27 | `log()` (logging con indentación de árbol y `severity`) y `ruta_carpeta()` |
|
||||||
| `adjuntos.py` | 81 | Descarga de adjuntos (Graph) y filtrado PDF/XML |
|
| `adjuntos.py` | 81 | Descarga de adjuntos (Graph) y filtrado PDF/XML |
|
||||||
| `documentos.py` | 371 | Extracción PDF/XML, asignación de guías, copia a red |
|
| `documentos.py` | 371 | Extracción PDF/XML, asignación de guías, copia a red |
|
||||||
| `persistencia.py` | 163 | Guardado de JSON de resultados y de drafts |
|
| `persistencia.py` | 163 | Guardado de JSON de resultados y de drafts |
|
||||||
|
|||||||
+6
-5
@@ -1,6 +1,7 @@
|
|||||||
"""Extracción de datos de PDF/XML y asignación de guías de remisión."""
|
"""Extracción de datos de PDF/XML y asignación de guías de remisión."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import ntpath
|
import ntpath
|
||||||
@@ -47,7 +48,7 @@ def copiar_a_red(data: dict) -> None:
|
|||||||
try:
|
try:
|
||||||
fecha = datetime.strptime(data.get("fecha_emision", ""), "%Y-%m-%d")
|
fecha = datetime.strptime(data.get("fecha_emision", ""), "%Y-%m-%d")
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
log(f"⚠️ No se pudo copiar a red: fecha_emision inválida ({data.get('fecha_emision')})", 3)
|
log(f"⚠️ No se pudo copiar a red: fecha_emision inválida ({data.get('fecha_emision')})", 3, severity=logging.WARNING)
|
||||||
return
|
return
|
||||||
|
|
||||||
carpeta_destino = os.path.join(
|
carpeta_destino = os.path.join(
|
||||||
@@ -81,7 +82,7 @@ def copiar_a_red(data: dict) -> None:
|
|||||||
|
|
||||||
log(f"📁 Copiado a red: {carpeta_destino}", 3)
|
log(f"📁 Copiado a red: {carpeta_destino}", 3)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
log(f"⚠️ Error copiando a ruta de red: {e}", 3)
|
log(f"⚠️ Error copiando a ruta de red: {e}", 3, severity=logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -133,7 +134,7 @@ def procesar_pdfs(email: dict, download_folder: str) -> dict:
|
|||||||
pdf_cache[canonical] = data
|
pdf_cache[canonical] = data
|
||||||
log(f"📄 PDF procesado: {canonical} ({data.get('tipo_documento', '?')})", 3)
|
log(f"📄 PDF procesado: {canonical} ({data.get('tipo_documento', '?')})", 3)
|
||||||
else:
|
else:
|
||||||
log(f"⚠️ PDF no procesado: {adjunto} - {data.get('error', 'error desconocido')}", 2)
|
log(f"⚠️ PDF no procesado: {adjunto} - {data.get('error', 'error desconocido')}", 2, severity=logging.WARNING)
|
||||||
|
|
||||||
if datos_pdf:
|
if datos_pdf:
|
||||||
email["datos_pdf"] = datos_pdf
|
email["datos_pdf"] = datos_pdf
|
||||||
@@ -365,7 +366,7 @@ def validar_guias(documentos: list[dict]) -> None:
|
|||||||
for i, linea in enumerate(doc.get("lineas", [])):
|
for i, linea in enumerate(doc.get("lineas", [])):
|
||||||
for j, g in enumerate(linea.get("guias_remision") or []):
|
for j, g in enumerate(linea.get("guias_remision") or []):
|
||||||
if not isinstance(g, dict) or not all(k in g and g[k] for k in ("tipo", "serie", "numero")):
|
if not isinstance(g, dict) or not all(k in g and g[k] for k in ("tipo", "serie", "numero")):
|
||||||
log(f"⚠️ guias_remision[{j}] inválida en {doc.get('serie','?')}-{doc.get('numero','?')} línea {i}: {g}", 2)
|
log(f"⚠️ guias_remision[{j}] inválida en {doc.get('serie','?')}-{doc.get('numero','?')} línea {i}: {g}", 2, severity=logging.WARNING)
|
||||||
gt = linea.get("guia_transportista")
|
gt = linea.get("guia_transportista")
|
||||||
if gt and (not isinstance(gt, dict) or not all(k in gt and gt[k] for k in ("tipo", "serie", "numero"))):
|
if gt and (not isinstance(gt, dict) or not all(k in gt and gt[k] for k in ("tipo", "serie", "numero"))):
|
||||||
log(f"⚠️ guia_transportista inválida en {doc.get('serie','?')}-{doc.get('numero','?')} línea {i}: {gt}", 2)
|
log(f"⚠️ guia_transportista inválida en {doc.get('serie','?')}-{doc.get('numero','?')} línea {i}: {gt}", 2, severity=logging.WARNING)
|
||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
"""Validación de precios contra SAP y construcción del draft SAP B1."""
|
"""Validación de precios contra SAP y construcción del draft SAP B1."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import logging
|
||||||
import math
|
import math
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
@@ -340,7 +341,7 @@ def validate_prices(factura: dict) -> tuple[bool, list[dict]]:
|
|||||||
guias_lt.append({"guia": guia, "line_total": suma_line_total, "warehouse_code": ", ".join(sorted(wh_set)) if wh_set else "A-CENTRA", "quantity": max_qty, "price_after_vat_sum": sum_pav, "zona":zona, "weight1": weight1, "draft_quantity": 0, "draft_price": 0.0})
|
guias_lt.append({"guia": guia, "line_total": suma_line_total, "warehouse_code": ", ".join(sorted(wh_set)) if wh_set else "A-CENTRA", "quantity": max_qty, "price_after_vat_sum": sum_pav, "zona":zona, "weight1": weight1, "draft_quantity": 0, "draft_price": 0.0})
|
||||||
all_wh.update(wh_set)
|
all_wh.update(wh_set)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log(f"⚠️ Guía {codigo_guia}: error al consultar SAP - {ex}", 2)
|
log(f"⚠️ Guía {codigo_guia}: error al consultar SAP - {ex}", 2, severity=logging.WARNING)
|
||||||
guias_lt.append({"guia": guia, "line_total": 0.0, "warehouse_code": "A-CENTRA", "weight1": 0.0, "quantity": 0.0, "price_after_vat_sum": 0.0, "zona": "", "draft_quantity": 0, "draft_price": 0.0})
|
guias_lt.append({"guia": guia, "line_total": 0.0, "warehouse_code": "A-CENTRA", "weight1": 0.0, "quantity": 0.0, "price_after_vat_sum": 0.0, "zona": "", "draft_quantity": 0, "draft_price": 0.0})
|
||||||
all_ok = False
|
all_ok = False
|
||||||
|
|
||||||
@@ -376,7 +377,7 @@ def validate_prices(factura: dict) -> tuple[bool, list[dict]]:
|
|||||||
if ok:
|
if ok:
|
||||||
log(f"✅ Coincide: {total_linea:.6f} ≈ {sap_total:.6f}", 3)
|
log(f"✅ Coincide: {total_linea:.6f} ≈ {sap_total:.6f}", 3)
|
||||||
else:
|
else:
|
||||||
log(f"❌ NO coincide — XML: {total_linea:.6f} vs SAP: {sap_total:.6f}", 3)
|
log(f"❌ NO coincide — XML: {total_linea:.6f} vs SAP: {sap_total:.6f}", 3, severity=logging.WARNING)
|
||||||
|
|
||||||
todas_coinciden = all(r["ok"] for r in resultados)
|
todas_coinciden = all(r["ok"] for r in resultados)
|
||||||
return todas_coinciden, resultados
|
return todas_coinciden, resultados
|
||||||
|
|||||||
+7
-4
@@ -1,6 +1,9 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
def descomprimir_adjuntos(emails: list[dict], ruta: str) -> list[dict]:
|
def descomprimir_adjuntos(emails: list[dict], ruta: str) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
@@ -16,7 +19,7 @@ def descomprimir_adjuntos(emails: list[dict], ruta: str) -> list[dict]:
|
|||||||
continue
|
continue
|
||||||
ruta_zip = os.path.join(ruta, adjunto)
|
ruta_zip = os.path.join(ruta, adjunto)
|
||||||
if not os.path.exists(ruta_zip):
|
if not os.path.exists(ruta_zip):
|
||||||
print(f"⚠️ No existe: {ruta_zip}")
|
logger.warning(f"⚠️ No existe: {ruta_zip}")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(ruta_zip, "r") as zip_ref:
|
with zipfile.ZipFile(ruta_zip, "r") as zip_ref:
|
||||||
@@ -27,16 +30,16 @@ def descomprimir_adjuntos(emails: list[dict], ruta: str) -> list[dict]:
|
|||||||
]
|
]
|
||||||
zip_ref.extractall(ruta)
|
zip_ref.extractall(ruta)
|
||||||
nuevos_adjuntos.extend(nombres_extraidos)
|
nuevos_adjuntos.extend(nombres_extraidos)
|
||||||
print(
|
logger.info(
|
||||||
f"📦 {adjunto}: {len(nombres_extraidos)} archivos extraídos"
|
f"📦 {adjunto}: {len(nombres_extraidos)} archivos extraídos"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Error descomprimiendo {adjunto}: {e}")
|
logger.warning(f"⚠️ Error descomprimiendo {adjunto}: {e}")
|
||||||
email["adjuntos"].extend(nuevos_adjuntos)
|
email["adjuntos"].extend(nuevos_adjuntos)
|
||||||
return emails
|
return emails
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(f"❌ Error general: {ex}")
|
logger.error(f"❌ Error general: {ex}")
|
||||||
return emails
|
return emails
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
+6
-3
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from hdbcli import dbapi
|
from hdbcli import dbapi
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CONFIGURACIÓN
|
# CONFIGURACIÓN
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -31,7 +34,7 @@ def get_connection():
|
|||||||
)
|
)
|
||||||
return conn
|
return conn
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ Error conectando a HANA: {e}")
|
logger.error(f" ❌ Error conectando a HANA: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -89,7 +92,7 @@ def _query_delivery_table(connection, tabla: str, tipo: str, serie: str, numero:
|
|||||||
results.append(registro)
|
results.append(registro)
|
||||||
return results
|
return results
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ Error en consulta HANA get_delivery ({tabla}): {e}")
|
logger.error(f" ❌ Error en consulta HANA get_delivery ({tabla}): {e}")
|
||||||
return []
|
return []
|
||||||
finally:
|
finally:
|
||||||
if cursor:
|
if cursor:
|
||||||
@@ -127,7 +130,7 @@ def obtener_valor_referencial(serie: str, numero: str) -> float:
|
|||||||
valor = dict(zip(columns, row)).get("ValorReferencial")
|
valor = dict(zip(columns, row)).get("ValorReferencial")
|
||||||
return float(valor) if valor is not None else 0.0
|
return float(valor) if valor is not None else 0.0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ Error en consulta HANA obtener_valor_referencial: {e}")
|
logger.error(f" ❌ Error en consulta HANA obtener_valor_referencial: {e}")
|
||||||
return 0.0
|
return 0.0
|
||||||
finally:
|
finally:
|
||||||
if cursor:
|
if cursor:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import argparse
|
import argparse
|
||||||
@@ -7,6 +8,8 @@ from dotenv import load_dotenv
|
|||||||
import logger_manager
|
import logger_manager
|
||||||
from pipeline import run_pipeline
|
from pipeline import run_pipeline
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
HORARIOS = [
|
HORARIOS = [
|
||||||
@@ -72,8 +75,8 @@ def main() -> None:
|
|||||||
DOWNLOAD_FOLDER = os.getenv("DOWNLOAD_FOLDER", "")
|
DOWNLOAD_FOLDER = os.getenv("DOWNLOAD_FOLDER", "")
|
||||||
OUTPUT_FOLDER = os.getenv("OUTPUT_FOLDER", "")
|
OUTPUT_FOLDER = os.getenv("OUTPUT_FOLDER", "")
|
||||||
|
|
||||||
print("🚀 Demonio iniciado")
|
logger.info("🚀 Demonio iniciado")
|
||||||
print("=" * 60)
|
logger.info("=" * 60)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
@@ -86,8 +89,8 @@ def main() -> None:
|
|||||||
|
|
||||||
intervalo, modo = resultado
|
intervalo, modo = resultado
|
||||||
|
|
||||||
print(
|
logger.info(
|
||||||
f"\n[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] "
|
f"[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] "
|
||||||
f"Iniciando ciclo [{modo}] (cada {intervalo}s)"
|
f"Iniciando ciclo [{modo}] (cada {intervalo}s)"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,14 +99,14 @@ def main() -> None:
|
|||||||
duracion = (datetime.now() - inicio_ciclo).total_seconds()
|
duracion = (datetime.now() - inicio_ciclo).total_seconds()
|
||||||
|
|
||||||
if duracion > intervalo:
|
if duracion > intervalo:
|
||||||
print(
|
logger.warning(
|
||||||
f"⚠️ El ciclo demoró {duracion:.1f}s "
|
f"⚠️ El ciclo demoró {duracion:.1f}s "
|
||||||
f"(intervalo configurado {intervalo}s)"
|
f"(intervalo configurado {intervalo}s)"
|
||||||
)
|
)
|
||||||
|
|
||||||
dormir = max(0, intervalo - duracion)
|
dormir = max(0, intervalo - duracion)
|
||||||
|
|
||||||
print(f"💤 Próximo ciclo [{modo}] en {dormir:.0f}s...")
|
logger.info(f"💤 Próximo ciclo [{modo}] en {dormir:.0f}s...")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
@@ -111,25 +114,22 @@ def main() -> None:
|
|||||||
|
|
||||||
despertar = inicio_ciclo + timedelta(seconds=dormir)
|
despertar = inicio_ciclo + timedelta(seconds=dormir)
|
||||||
|
|
||||||
print(
|
logger.info(f"[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] Fuera del horario.")
|
||||||
f"\n[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] "
|
logger.info(f"Próxima ejecución: {despertar:%Y-%m-%d %H:%M:%S}")
|
||||||
f"Fuera del horario.\n"
|
|
||||||
f"Próxima ejecución: {despertar:%Y-%m-%d %H:%M:%S}"
|
|
||||||
)
|
|
||||||
|
|
||||||
time.sleep(dormir)
|
time.sleep(dormir)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\n🛑 Demonio detenido por el usuario.")
|
logger.info("🛑 Demonio detenido por el usuario.")
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logger_manager.setup_logging()
|
logger_manager.setup_logging()
|
||||||
|
|
||||||
print("=" * 60)
|
logger.info("=" * 60)
|
||||||
print(f"🚀 Demonio iniciado: {datetime.now():%Y-%m-%d %H:%M:%S}")
|
logger.info(f"🚀 Demonio iniciado: {datetime.now():%Y-%m-%d %H:%M:%S}")
|
||||||
print(f"📄 Log guardado en: {logger_manager.LOG_FILE}")
|
logger.info(f"📄 Log guardado en: {logger_manager.LOG_FILE}")
|
||||||
print("=" * 60)
|
logger.info("=" * 60)
|
||||||
|
|
||||||
main()
|
main()
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import msal
|
import msal
|
||||||
import requests
|
import requests
|
||||||
import warnings
|
import warnings
|
||||||
@@ -5,6 +6,8 @@ import base64
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -214,7 +217,7 @@ def send_email(access_token: str, from_email: str, to_recipients: list[str], sub
|
|||||||
"contentBytes": encoded_content
|
"contentBytes": encoded_content
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
print(f"Advertencia: El archivo no existe y no se adjuntará: {path}")
|
logger.warning(f"Advertencia: El archivo no existe y no se adjuntará: {path}")
|
||||||
|
|
||||||
# Estructura del mensaje para la API
|
# Estructura del mensaje para la API
|
||||||
email_data = {
|
email_data = {
|
||||||
@@ -234,13 +237,13 @@ def send_email(access_token: str, from_email: str, to_recipients: list[str], sub
|
|||||||
try:
|
try:
|
||||||
response = requests.post(url, headers=headers, json=email_data, verify=verify_ssl)
|
response = requests.post(url, headers=headers, json=email_data, verify=verify_ssl)
|
||||||
if response.status_code == 202:
|
if response.status_code == 202:
|
||||||
print(f" ├── SUCCESS: Correo enviado exitosamente a: {', '.join(to_recipients)}")
|
logger.info(f" ├── SUCCESS: Correo enviado exitosamente a: {', '.join(to_recipients)}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(f" ├── ERROR: Al enviar correo: {response.status_code} - {response.text}")
|
logger.error(f" ├── ERROR: Al enviar correo: {response.status_code} - {response.text}")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ├── ERROR: Excepcion al enviar correo: {e}")
|
logger.error(f" ├── ERROR: Excepcion al enviar correo: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
+17
-14
@@ -8,6 +8,7 @@ Cada correo se procesa individualmente en un bucle:
|
|||||||
→ crear draft en SAP (si válido) → notificar → guardar draft JSON
|
→ crear draft en SAP (si válido) → notificar → guardar draft JSON
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
@@ -22,21 +23,23 @@ import notificaciones
|
|||||||
import sapb1_manager
|
import sapb1_manager
|
||||||
from pipeline_utils import log
|
from pipeline_utils import log
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PASO 1: Autenticación Microsoft Graph
|
# PASO 1: Autenticación Microsoft Graph
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
def step_authenticate() -> str | None:
|
def step_authenticate() -> str | None:
|
||||||
try:
|
try:
|
||||||
print("Intentando obtener token de acceso...")
|
logger.info("Intentando obtener token de acceso...")
|
||||||
token = outlook_utilities.get_access_token()
|
token = outlook_utilities.get_access_token()
|
||||||
if token:
|
if token:
|
||||||
print("✅ Token obtenido exitosamente.")
|
logger.info("✅ Token obtenido exitosamente.")
|
||||||
print(f"Token prefix: {token[:25]}...")
|
logger.info(f"Token prefix: {token[:25]}...")
|
||||||
return token
|
return token
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error: {e}")
|
logger.error(f"❌ Error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -44,14 +47,14 @@ def step_authenticate() -> str | None:
|
|||||||
# PASO 2: Descarga de correos
|
# PASO 2: Descarga de correos
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
def step_fetch_emails(token: str, email: str, fecha_inicio: str, fecha_fin: str) -> list[dict]:
|
def step_fetch_emails(token: str, email: str, fecha_inicio: str, fecha_fin: str) -> list[dict]:
|
||||||
print("\n" + "=" * 60)
|
logger.info("=" * 60)
|
||||||
print(f"[MAIL] {email}")
|
logger.info(f"[MAIL] {email}")
|
||||||
try:
|
try:
|
||||||
emails = outlook_utilities.get_emails(email, token, fecha_inicio, fecha_fin)
|
emails = outlook_utilities.get_emails(email, token, fecha_inicio, fecha_fin)
|
||||||
print(f"Se encontraron {len(emails)} correos.")
|
logger.info(f"Se encontraron {len(emails)} correos.")
|
||||||
return emails
|
return emails
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error: {e}")
|
logger.error(f"❌ Error: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
@@ -95,7 +98,7 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
|||||||
# FLUJO DEL CORREO
|
# FLUJO DEL CORREO
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
|
|
||||||
print(f"\n📬 [{email['fecha']}] {email['subject'][:80]}...")
|
logger.info(f"📬 [{email['fecha']}] {email['subject'][:80]}...")
|
||||||
log("📂 Adjuntos:")
|
log("📂 Adjuntos:")
|
||||||
|
|
||||||
# 1-2. BD: verificar existencia y guardar
|
# 1-2. BD: verificar existencia y guardar
|
||||||
@@ -178,7 +181,7 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
|||||||
log(f"✅ Draft SAP B1 creado: {serie}-{numero} → DocEntry {doc_entry}")
|
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)
|
html_body = notificaciones.build_notification_body("created", factura, validation_table, valor_referencial_table, doc_entry=doc_entry)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
log(f"❌ Error creando draft {serie}-{numero}: {ex}")
|
log(f"❌ Error creando draft {serie}-{numero}: {ex}", severity=logging.ERROR)
|
||||||
html_body = notificaciones.build_notification_body("error", factura, validation_table, valor_referencial_table, error=ex)
|
html_body = notificaciones.build_notification_body("error", factura, validation_table, valor_referencial_table, error=ex)
|
||||||
else:
|
else:
|
||||||
log(f"⏭️ Draft NO creado: {serie}-{numero} — validación falló")
|
log(f"⏭️ Draft NO creado: {serie}-{numero} — validación falló")
|
||||||
@@ -196,7 +199,7 @@ def _procesar_un_correo(email: dict, token: str, email_addr: str, download_folde
|
|||||||
cc_recipients=["jlopez@aberio.com.pe", "gcalle@pucp.pe"],
|
cc_recipients=["jlopez@aberio.com.pe", "gcalle@pucp.pe"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log("⚠️ No se pudo determinar el remitente, notificación no enviada")
|
log("⚠️ No se pudo determinar el remitente, notificación no enviada", severity=logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -211,18 +214,18 @@ def run_pipeline(dias: int, EMAIL: str, DOWNLOAD_FOLDER: str, OUTPUT_FOLDER: str
|
|||||||
ayer = hoy - timedelta(days=dias)
|
ayer = hoy - timedelta(days=dias)
|
||||||
fecha_inicio = ayer.strftime("%Y-%m-%d")
|
fecha_inicio = ayer.strftime("%Y-%m-%d")
|
||||||
fecha_fin = hoy.strftime("%Y-%m-%d")
|
fecha_fin = hoy.strftime("%Y-%m-%d")
|
||||||
print(f"Rango de búsqueda: {fecha_inicio} al {fecha_fin}")
|
logger.info(f"Rango de búsqueda: {fecha_inicio} al {fecha_fin}")
|
||||||
|
|
||||||
# 1. Autenticar
|
# 1. Autenticar
|
||||||
token = step_authenticate()
|
token = step_authenticate()
|
||||||
if not token:
|
if not token:
|
||||||
print("❌ No se pudo autenticar. Abortando.")
|
logger.error("❌ No se pudo autenticar. Abortando.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. Descargar correos
|
# 2. Descargar correos
|
||||||
emails = step_fetch_emails(token, EMAIL, fecha_inicio, fecha_fin)
|
emails = step_fetch_emails(token, EMAIL, fecha_inicio, fecha_fin)
|
||||||
if not emails:
|
if not emails:
|
||||||
print("No se encontraron correos.")
|
logger.info("No se encontraron correos.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 3. Bucle por cada correo
|
# 3. Bucle por cada correo
|
||||||
|
|||||||
+15
-3
@@ -1,6 +1,9 @@
|
|||||||
"""Utilidades compartidas por los módulos del pipeline."""
|
"""Utilidades compartidas por los módulos del pipeline."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("pipeline")
|
||||||
|
|
||||||
|
|
||||||
def ruta_carpeta(subcarpeta: str) -> str:
|
def ruta_carpeta(subcarpeta: str) -> str:
|
||||||
@@ -8,8 +11,17 @@ def ruta_carpeta(subcarpeta: str) -> str:
|
|||||||
return os.path.join(os.path.dirname(__file__), subcarpeta)
|
return os.path.join(os.path.dirname(__file__), subcarpeta)
|
||||||
|
|
||||||
|
|
||||||
def log(msg: str, level: int = 1, last: bool = False) -> None:
|
def log(msg: str, level: int = 1, last: bool = False, severity: int = logging.INFO) -> None:
|
||||||
"""Imprime ``msg`` con la indentación de árbol usada en el log del pipeline."""
|
"""
|
||||||
|
Registra ``msg`` con la indentación de árbol usada en el log del pipeline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg: Mensaje a registrar.
|
||||||
|
level: Nivel de indentación del árbol (1-4).
|
||||||
|
last: True si es el último hijo de su rama (``└──`` en vez de ``├──``).
|
||||||
|
severity: Nivel de ``logging`` (``logging.WARNING``, ``logging.ERROR``…).
|
||||||
|
Por defecto ``logging.INFO``.
|
||||||
|
"""
|
||||||
branch = " └── " if last else " ├── "
|
branch = " └── " if last else " ├── "
|
||||||
pfx = {1: branch, 2: f" │ {branch}", 3: f" │ │ {branch}", 4: f" │ │ │ {branch}"}.get(level, branch)
|
pfx = {1: branch, 2: f" │ {branch}", 3: f" │ │ {branch}", 4: f" │ │ │ {branch}"}.get(level, branch)
|
||||||
print(f"{pfx}{msg}")
|
logger.log(severity, f"{pfx}{msg}")
|
||||||
+29
-26
@@ -1,4 +1,5 @@
|
|||||||
|
|
||||||
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
import os
|
import os
|
||||||
@@ -7,6 +8,8 @@ from typing import Optional, Dict, Any
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import hana_manager
|
import hana_manager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CONFIGURACIÓN
|
# CONFIGURACIÓN
|
||||||
@@ -37,7 +40,7 @@ def login() -> dict:
|
|||||||
"UserName": SAPB1_USER,
|
"UserName": SAPB1_USER,
|
||||||
"Password": SAPB1_PASSWORD,
|
"Password": SAPB1_PASSWORD,
|
||||||
}
|
}
|
||||||
print("🔐 Iniciando sesión en SAP B1 Service Layer...")
|
logger.info("🔐 Iniciando sesión en SAP B1 Service Layer...")
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
f"{SAPB1_URL}/Login",
|
f"{SAPB1_URL}/Login",
|
||||||
json=payload,
|
json=payload,
|
||||||
@@ -55,7 +58,7 @@ def login() -> dict:
|
|||||||
|
|
||||||
headers = {"Cookie": f"B1SESSION={b1session}; ROUTEID={routeid}"}
|
headers = {"Cookie": f"B1SESSION={b1session}; ROUTEID={routeid}"}
|
||||||
_sap_session.update({"headers": headers, "b1session": b1session, "routeid": routeid})
|
_sap_session.update({"headers": headers, "b1session": b1session, "routeid": routeid})
|
||||||
print(f"✅ Sesión SAP B1 establecida (B1SESSION: {b1session[:10]}...)")
|
logger.info(f"✅ Sesión SAP B1 establecida (B1SESSION: {b1session[:10]}...)")
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
@@ -64,9 +67,9 @@ def login() -> dict:
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
def logout() -> None:
|
def logout() -> None:
|
||||||
if not _sap_session["b1session"]:
|
if not _sap_session["b1session"]:
|
||||||
print("⚠️ No hay sesión activa para cerrar.")
|
logger.warning("⚠️ No hay sesión activa para cerrar.")
|
||||||
return
|
return
|
||||||
print("🔓 Cerrando sesión SAP B1...")
|
logger.info("🔓 Cerrando sesión SAP B1...")
|
||||||
try:
|
try:
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
f"{SAPB1_URL}/Logout",
|
f"{SAPB1_URL}/Logout",
|
||||||
@@ -75,11 +78,11 @@ def logout() -> None:
|
|||||||
verify=SAPB1_VERIFY_SSL,
|
verify=SAPB1_VERIFY_SSL,
|
||||||
)
|
)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
print("✅ Sesión SAP B1 cerrada correctamente.")
|
logger.info("✅ Sesión SAP B1 cerrada correctamente.")
|
||||||
else:
|
else:
|
||||||
print(f"⚠️ Logout respondió HTTP {resp.status_code}")
|
logger.warning(f"⚠️ Logout respondió HTTP {resp.status_code}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"❌ Error al cerrar sesión: {exc}")
|
logger.error(f"❌ Error al cerrar sesión: {exc}")
|
||||||
finally:
|
finally:
|
||||||
_sap_session.update({"headers": None, "b1session": None, "routeid": None})
|
_sap_session.update({"headers": None, "b1session": None, "routeid": None})
|
||||||
|
|
||||||
@@ -102,9 +105,9 @@ def _log_response(method: str, endpoint: str, response: requests.Response, paylo
|
|||||||
f" ({len(response.content)} bytes)"
|
f" ({len(response.content)} bytes)"
|
||||||
)
|
)
|
||||||
if response.status_code >= 400:
|
if response.status_code >= 400:
|
||||||
print(f"⚠️ {log_line} - {response.text[:300]}")
|
logger.warning(f"⚠️ {log_line} - {response.text[:300]}")
|
||||||
else:
|
else:
|
||||||
print(log_line)
|
logger.info(log_line)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -133,7 +136,7 @@ def forward_to_sapb1(
|
|||||||
if extra_headers:
|
if extra_headers:
|
||||||
req_headers.update(extra_headers)
|
req_headers.update(extra_headers)
|
||||||
|
|
||||||
print(f" ├── 🔄 [{method}] {endpoint_path}")
|
logger.info(f" ├── 🔄 [{method}] {endpoint_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.request(
|
resp = requests.request(
|
||||||
@@ -153,7 +156,7 @@ def forward_to_sapb1(
|
|||||||
raise ConnectionError(f"Error en solicitud HTTP: {exc}")
|
raise ConnectionError(f"Error en solicitud HTTP: {exc}")
|
||||||
|
|
||||||
if resp.status_code == 401:
|
if resp.status_code == 401:
|
||||||
print("⚠️ Sesión expirada (401). Reautenticando e intentando una vez...")
|
logger.warning("⚠️ Sesión expirada (401). Reautenticando e intentando una vez...")
|
||||||
req_headers["Cookie"] = login()["Cookie"]
|
req_headers["Cookie"] = login()["Cookie"]
|
||||||
resp = requests.request(
|
resp = requests.request(
|
||||||
method=method,
|
method=method,
|
||||||
@@ -191,7 +194,7 @@ def forward_to_sapb1(
|
|||||||
def create_draft(payload: dict) -> dict:
|
def create_draft(payload: dict) -> dict:
|
||||||
resp = forward_to_sapb1("POST", "Drafts", json_data=payload)
|
resp = forward_to_sapb1("POST", "Drafts", json_data=payload)
|
||||||
result = resp.json()
|
result = resp.json()
|
||||||
print(f" ├── ✅ Draft creado DocEntry={result.get('DocEntry')}")
|
logger.info(f" ├── ✅ Draft creado DocEntry={result.get('DocEntry')}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -201,7 +204,7 @@ def create_draft(payload: dict) -> dict:
|
|||||||
def get_purchase_invoice(doc_entry: int) -> dict:
|
def get_purchase_invoice(doc_entry: int) -> dict:
|
||||||
resp = forward_to_sapb1("GET", f"PurchaseInvoices({doc_entry})")
|
resp = forward_to_sapb1("GET", f"PurchaseInvoices({doc_entry})")
|
||||||
result = resp.json()
|
result = resp.json()
|
||||||
print(f" ├── ✅ PurchaseInvoices({doc_entry}) obtenida")
|
logger.info(f" ├── ✅ PurchaseInvoices({doc_entry}) obtenida")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -249,7 +252,7 @@ def get_delivery_notes(tipo: str, serie: str, numero: str) -> list:
|
|||||||
entries = _get_delivery_entries(tipo, serie, numero)
|
entries = _get_delivery_entries(tipo, serie, numero)
|
||||||
|
|
||||||
if not entries:
|
if not entries:
|
||||||
print(f" │ ⚠️ Guía {codigo}: sin registros en SAP (SL + HANA)")
|
logger.warning(f" │ ⚠️ Guía {codigo}: sin registros en SAP (SL + HANA)")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# ── Fase 2: Obtener documento completo por cada DocEntry ─────────────
|
# ── Fase 2: Obtener documento completo por cada DocEntry ─────────────
|
||||||
@@ -260,20 +263,20 @@ def get_delivery_notes(tipo: str, serie: str, numero: str) -> list:
|
|||||||
try:
|
try:
|
||||||
resp = forward_to_sapb1("GET", f"{entidad}({doc_entry})")
|
resp = forward_to_sapb1("GET", f"{entidad}({doc_entry})")
|
||||||
doc = resp.json()
|
doc = resp.json()
|
||||||
print(f" │ └─ {entidad}({doc_entry}) obtenido")
|
logger.info(f" │ └─ {entidad}({doc_entry}) obtenido")
|
||||||
|
|
||||||
if entidad == "StockTransfers":
|
if entidad == "StockTransfers":
|
||||||
num_ov = doc.get("U_IDC_NUMOV")
|
num_ov = doc.get("U_IDC_NUMOV")
|
||||||
if not num_ov:
|
if not num_ov:
|
||||||
print(f" │ ⚠️ StockTransfers({doc_entry}) sin U_IDC_NUMOV: no se puede validar precios")
|
logger.warning(f" │ ⚠️ StockTransfers({doc_entry}) sin U_IDC_NUMOV: no se puede validar precios")
|
||||||
continue
|
continue
|
||||||
resp_ov = forward_to_sapb1("GET", f"Orders({num_ov})")
|
resp_ov = forward_to_sapb1("GET", f"Orders({num_ov})")
|
||||||
doc = resp_ov.json()
|
doc = resp_ov.json()
|
||||||
print(f" │ └─ Orders({num_ov}) obtenido (Orden de Venta de StockTransfers({doc_entry}))")
|
logger.info(f" │ └─ Orders({num_ov}) obtenido (Orden de Venta de StockTransfers({doc_entry}))")
|
||||||
|
|
||||||
docs.append(doc)
|
docs.append(doc)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" │ ⚠️ Error obteniendo {entidad}({doc_entry}): {e}")
|
logger.warning(f" │ ⚠️ Error obteniendo {entidad}({doc_entry}): {e}")
|
||||||
|
|
||||||
return docs
|
return docs
|
||||||
|
|
||||||
@@ -302,13 +305,13 @@ def _get_delivery_entries(tipo: str, serie: str, numero: str) -> list:
|
|||||||
if sl_entries:
|
if sl_entries:
|
||||||
for sl_entry in sl_entries:
|
for sl_entry in sl_entries:
|
||||||
sl_entry["Entidad"] = "DeliveryNotes"
|
sl_entry["Entidad"] = "DeliveryNotes"
|
||||||
print(f" │ 🔍 Guía {codigo}: SL → {len(sl_entries)} DocEntry encontrados")
|
logger.info(f" │ 🔍 Guía {codigo}: SL → {len(sl_entries)} DocEntry encontrados")
|
||||||
return sl_entries
|
return sl_entries
|
||||||
|
|
||||||
print(f" │ 🔍 Guía {codigo}: SL → 0 resultados. Consultando HANA...")
|
logger.info(f" │ 🔍 Guía {codigo}: SL → 0 resultados. Consultando HANA...")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" │ ⚠️ Guía {codigo}: SL falló ({e}). Consultando HANA...")
|
logger.warning(f" │ ⚠️ Guía {codigo}: SL falló ({e}). Consultando HANA...")
|
||||||
|
|
||||||
# ── Fallback a HANA ──────────────────────────────────────────────────
|
# ── Fallback a HANA ──────────────────────────────────────────────────
|
||||||
try:
|
try:
|
||||||
@@ -318,11 +321,11 @@ def _get_delivery_entries(tipo: str, serie: str, numero: str) -> list:
|
|||||||
hana_entry["Entidad"] = _TABLA_A_ENTIDAD_SL.get(
|
hana_entry["Entidad"] = _TABLA_A_ENTIDAD_SL.get(
|
||||||
hana_entry.get("Tabla"), "DeliveryNotes"
|
hana_entry.get("Tabla"), "DeliveryNotes"
|
||||||
)
|
)
|
||||||
print(f" │ 🔍 Guía {codigo}: HANA → {len(hana_entries)} DocEntry encontrados")
|
logger.info(f" │ 🔍 Guía {codigo}: HANA → {len(hana_entries)} DocEntry encontrados")
|
||||||
return hana_entries
|
return hana_entries
|
||||||
print(f" │ 🔍 Guía {codigo}: HANA → 0 registros")
|
logger.info(f" │ 🔍 Guía {codigo}: HANA → 0 registros")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" │ ⚠️ Guía {codigo}: HANA falló ({e})")
|
logger.warning(f" │ ⚠️ Guía {codigo}: HANA falló ({e})")
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -361,7 +364,7 @@ def obtener_condicion_pago(card_code: str) -> dict | None:
|
|||||||
"NumberOfAdditionalDays": terms.get("NumberOfAdditionalDays") or 0,
|
"NumberOfAdditionalDays": terms.get("NumberOfAdditionalDays") or 0,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" │ ⚠️ Error obteniendo condición de pago de {card_code}: {e}")
|
logger.warning(f" │ ⚠️ Error obteniendo condición de pago de {card_code}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -394,5 +397,5 @@ def get_zonas() -> list:
|
|||||||
)
|
)
|
||||||
result = resp.json()
|
result = resp.json()
|
||||||
value = result.get("value", [])
|
value = result.get("value", [])
|
||||||
print(f" ├── ✅ U_IDC_ZONAS obtenida ({len(value)} registros)")
|
logger.info(f" ├── ✅ U_IDC_ZONAS obtenida ({len(value)} registros)")
|
||||||
return value
|
return value
|
||||||
Reference in new issue
Block a user