218 lines
8.6 KiB
Python
218 lines
8.6 KiB
Python
import os
|
|
import sys
|
|
import clr
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
import logger_utilities
|
|
|
|
# Importar configuración y utilidades personalizadas
|
|
from outlook import get_access_token, send_email
|
|
from config_loader import CONFIG
|
|
|
|
# --- CRYSTAL REPORTS RUNTIME INITIALIZATION ---
|
|
########################################################
|
|
def init_crystal_runtime():
|
|
"""
|
|
Inicializa el runtime de Crystal Reports cargando las DLLs necesarias desde el GAC.
|
|
Se basa en la ruta configurada en config.json para determinar la versión.
|
|
"""
|
|
# gac_path = r"C:\Windows\Microsoft.NET\assembly\GAC_MSIL"
|
|
gac_path = CONFIG.crystal.path
|
|
|
|
# La versión del runtime de Crystal Reports instalada en este sistema
|
|
version_folder = CONFIG.crystal.version
|
|
|
|
dlls = [
|
|
os.path.join(gac_path, "CrystalDecisions.CrystalReports.Engine", version_folder, "CrystalDecisions.CrystalReports.Engine.dll"),
|
|
os.path.join(gac_path, "CrystalDecisions.Shared", version_folder, "CrystalDecisions.Shared.dll"),
|
|
os.path.join(gac_path, "CrystalDecisions.ReportSource", version_folder, "CrystalDecisions.ReportSource.dll")
|
|
]
|
|
|
|
for dll in dlls:
|
|
if not os.path.exists(dll):
|
|
print(f"CRITICAL ERROR: No se encontró la DLL necesaria: {dll}")
|
|
print("Verifique que el SAP Crystal Reports Runtime esté instalado correctamente.")
|
|
sys.exit(1)
|
|
clr.AddReference(dll)
|
|
|
|
########################################################
|
|
# Inicializar el runtime antes de importar los tipos de .NET
|
|
init_crystal_runtime()
|
|
|
|
# Importar tipos de .NET después de cargar las referencias
|
|
# pyrefly: ignore [missing-import]
|
|
from System import DateTime
|
|
# pyrefly: ignore [missing-import]
|
|
from CrystalDecisions.CrystalReports.Engine import ReportDocument
|
|
# pyrefly: ignore [missing-import]
|
|
from CrystalDecisions.Shared import ExportFormatType
|
|
|
|
# --- HELPERS ---
|
|
########################################################
|
|
def configure_database(report):
|
|
"""
|
|
Configura las credenciales de la base de datos HANA para el reporte y todas sus sub-tablas.
|
|
"""
|
|
hc = CONFIG.hana
|
|
# Login general para el reporte
|
|
report.SetDatabaseLogon(hc.user, hc.password, hc.server, hc.database)
|
|
|
|
# Asegurar conexión en cada tabla individual
|
|
for table in report.Database.Tables:
|
|
conn_info = table.LogOnInfo.ConnectionInfo
|
|
conn_info.ServerName = hc.server
|
|
conn_info.DatabaseName = hc.database
|
|
conn_info.UserID = hc.user
|
|
conn_info.Password = hc.password
|
|
table.ApplyLogOnInfo(table.LogOnInfo)
|
|
|
|
########################################################
|
|
def ejecutar_reporte(rpt_filename: str, pdf_path: Path, params: dict) -> bool:
|
|
"""
|
|
Carga un reporte .rpt, aplica parámetros y lo exporta a PDF.
|
|
|
|
:param rpt_filename: Nombre o ruta del archivo .rpt
|
|
:param pdf_path: Ruta de salida del archivo PDF
|
|
:param params: Diccionario de parámetros {Nombre: Valor}
|
|
:return: True si tuvo éxito, False en caso contrario
|
|
"""
|
|
print(f"--- Generando: {pdf_path.name} ---")
|
|
|
|
report = ReportDocument()
|
|
try:
|
|
report.Load(rpt_filename)
|
|
configure_database(report)
|
|
|
|
# Asignar parámetros dinámicamente
|
|
for key, value in params.items():
|
|
report.SetParameterValue(key, value)
|
|
|
|
# Exportar a disco
|
|
report.ExportToDisk(ExportFormatType.PortableDocFormat, str(pdf_path))
|
|
print(f"SUCCESS: Reporte guardado en {pdf_path}")
|
|
return True
|
|
except Exception as e:
|
|
print(f"ERROR: Falló la ejecución del reporte '{rpt_filename}': {e}")
|
|
return False
|
|
finally:
|
|
# Siempre liberar recursos de Crystal Reports
|
|
report.Close()
|
|
report.Dispose()
|
|
|
|
########################################################
|
|
def get_report_dates():
|
|
"""
|
|
Calcula las fechas de reporte: Ayer y el primer día del mes actual.
|
|
Retorna objetos System.DateTime compatibles con Crystal Reports.
|
|
"""
|
|
now = datetime.now(timezone.utc)
|
|
# Si hoy es lunes (weekday() == 0), retroceder al viernes anterior (3 días atrás)
|
|
days_back = 3 if now.weekday() == 0 else 1
|
|
|
|
previous_business_day = now - timedelta(days=days_back)
|
|
|
|
# Objetos .NET DateTime
|
|
dt_yesterday = DateTime(previous_business_day.year, previous_business_day.month, previous_business_day.day)
|
|
dt_month_start = DateTime(previous_business_day.year, previous_business_day.month, 1)
|
|
|
|
return dt_month_start, dt_yesterday
|
|
|
|
# --- PROCESO PRINCIPAL ---
|
|
########################################################
|
|
def main():
|
|
print("====================================================")
|
|
print("INICIANDO AUTOMATIZACIÓN DE CRYSTAL REPORTS")
|
|
print(f"Fecha/Hora: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
print("====================================================")
|
|
|
|
# 1. Autenticación con Microsoft Graph
|
|
access_token = get_access_token()
|
|
if not access_token:
|
|
print("ABORT: Error crítico de autenticación con Outlook.")
|
|
return
|
|
|
|
# 2. Preparación del Entorno
|
|
base_path = Path(__file__).parent
|
|
output_dir = base_path / CONFIG.outlook.attachments_folder
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 3. Cálculo de Fechas
|
|
dt_month_start, dt_yesterday = get_report_dates()
|
|
str_ayer = dt_yesterday.ToString("dd/MM/yyyy")
|
|
str_inicio = dt_month_start.ToString("dd/MM/yyyy")
|
|
|
|
# 4. Generación de Reportes
|
|
rpt_file = "venta_presupuesto_linea.rpt"
|
|
adjuntos = []
|
|
|
|
# REPORTE 1: Venta del día de ayer
|
|
pdf_yesterday = output_dir / f"venta_presupuesto_linea_{dt_yesterday.ToString('dd_MM_yyyy')}.pdf"
|
|
if ejecutar_reporte(rpt_file, pdf_yesterday, {"FecIni": dt_yesterday, "FecFin": dt_yesterday}):
|
|
adjuntos.append(str(pdf_yesterday))
|
|
|
|
# REPORTE 2: Acumulado del mes actual (MTD)
|
|
pdf_mtd = output_dir / f"venta_presupuesto_linea_{dt_month_start.ToString('dd_MM_yyyy')}_al_{dt_yesterday.ToString('dd_MM_yyyy')}.pdf"
|
|
if ejecutar_reporte(rpt_file, pdf_mtd, {"FecIni": dt_month_start, "FecFin": dt_yesterday}):
|
|
adjuntos.append(str(pdf_mtd))
|
|
|
|
# 5. Envío de Correo Electrónico
|
|
if adjuntos:
|
|
subject = f"Informe de Venta de Presupuestos por Línea - {str_ayer}"
|
|
body = f"""
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif;">
|
|
<h2 style="color: #2e6c80;">Informe Automático de Ventas por Línea</h2>
|
|
<p>Buenos días</p>
|
|
<p>Me complace compartir con ustedes el reporte de avance de ventas por línea correspondiente al {str_ayer}.</p>
|
|
<p>La información se encuentra disponible en el siguiente dashboard:</p>
|
|
<p>
|
|
<a href="{CONFIG.powerbi.url}" target="_blank">
|
|
Ver reporte Power BI
|
|
</a>
|
|
</p>
|
|
<div style="height: 20px;"></div>
|
|
<p>Quedamos atentos a cualquier consulta o comentario.</p>
|
|
<br>
|
|
<p style="font-weight: bold;">Saludos cordiales,</p>
|
|
<p style="font-weight: bold;">Equipo de Inteligencia de Negocios</p>
|
|
<p style="font-size: 0.9em; color: #666;">Este es un mensaje generado automáticamente por el sistema de reportes.</p>
|
|
</body>
|
|
</html>
|
|
"""
|
|
success = send_email(
|
|
access_token=access_token,
|
|
from_email=CONFIG.outlook.from_email,
|
|
to_recipients=CONFIG.outlook.to_email,
|
|
subject=subject,
|
|
html_body=body,
|
|
attachment_paths=adjuntos
|
|
)
|
|
if success:
|
|
print("PROCESO FINALIZADO: Correo enviado correctamente.")
|
|
else:
|
|
print("ERROR: No se generaron archivos PDF. No hay nada que enviar.")
|
|
|
|
########################################################
|
|
if __name__ == "__main__":
|
|
try:
|
|
# Configurar Logger (Redirige stdout a archivo y consola)
|
|
log_path = logger_utilities.setup_logger()
|
|
logger_utilities.limpiar_logs_antiguos(dias=30)
|
|
|
|
inicio_ejecucion = datetime.now()
|
|
print("="*60)
|
|
print(f"🚀 INICIO DE EJECUCIÓN: {inicio_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
print(f"📄 Log guardado en: {log_path}")
|
|
print("="*60)
|
|
|
|
main()
|
|
|
|
fin_ejecucion = datetime.now()
|
|
duracion = fin_ejecucion - inicio_ejecucion
|
|
print("\n" + "="*60)
|
|
print(f"🏁 FIN DE EJECUCIÓN: {fin_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
print(f"⏱️ Duración total: {str(duracion).split('.')[0]}") # Sin microsegundos
|
|
print("="*60)
|
|
except Exception as e:
|
|
print(f"FATAL MAIN ERROR: {e}")
|