""" Scheduler de tareas programadas para Sales Bot Maneja notificaciones automáticas y reportes periódicos """ import os import logging from datetime import datetime, timedelta, timezone from threading import Thread try: from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger APSCHEDULER_DISPONIBLE = True except ImportError: APSCHEDULER_DISPONIBLE = False logger = logging.getLogger(__name__) # Zona horaria de México TZ_MEXICO = timezone(timedelta(hours=-6)) class SalesBotScheduler: """ Programador de tareas automáticas para Sales Bot. Tareas programadas: - Recordatorio de mediodía: Notifica a vendedores que no han cumplido meta - Resumen diario: Resumen de ventas al final del día - Resumen semanal: Estadísticas de la semana cada lunes """ def __init__(self, mattermost_client, nocodb_client): if not APSCHEDULER_DISPONIBLE: logger.warning("APScheduler no está instalado. Scheduler deshabilitado.") self.scheduler = None return self.mattermost = mattermost_client self.nocodb = nocodb_client self.scheduler = BackgroundScheduler(timezone='America/Mexico_City') # Configuración desde variables de entorno self.enabled = os.getenv('SCHEDULER_ENABLED', 'True').lower() == 'true' self.hora_recordatorio = int(os.getenv('RECORDATORIO_MEDIODIA_HORA', 12)) self.hora_resumen = int(os.getenv('RESUMEN_DIARIO_HORA', 18)) self.dia_semanal = os.getenv('RESUMEN_SEMANAL_DIA', 'mon') self.hora_semanal = int(os.getenv('RESUMEN_SEMANAL_HORA', 9)) def iniciar(self): """Inicia el scheduler con todas las tareas programadas""" if not self.scheduler or not self.enabled: logger.info("Scheduler deshabilitado") return try: # Recordatorio de mediodía self.scheduler.add_job( self.recordatorio_mediodia, CronTrigger(hour=self.hora_recordatorio, minute=0), id='recordatorio_mediodia', replace_existing=True ) logger.info(f"Recordatorio de mediodía programado para las {self.hora_recordatorio}:00") # Resumen diario self.scheduler.add_job( self.resumen_diario, CronTrigger(hour=self.hora_resumen, minute=0), id='resumen_diario', replace_existing=True ) logger.info(f"Resumen diario programado para las {self.hora_resumen}:00") # Resumen semanal (lunes por defecto) self.scheduler.add_job( self.resumen_semanal, CronTrigger(day_of_week=self.dia_semanal, hour=self.hora_semanal, minute=0), id='resumen_semanal', replace_existing=True ) logger.info(f"Resumen semanal programado para {self.dia_semanal} a las {self.hora_semanal}:00") self.scheduler.start() logger.info("Scheduler iniciado correctamente") except Exception as e: logger.error(f"Error iniciando scheduler: {str(e)}") def detener(self): """Detiene el scheduler""" if self.scheduler and self.scheduler.running: self.scheduler.shutdown() logger.info("Scheduler detenido") def recordatorio_mediodia(self): """ Envía recordatorios a vendedores que no han cumplido su meta. Se ejecuta a mediodía para dar tiempo de reaccionar. """ try: logger.info("Ejecutando recordatorio de mediodía") vendedores = self.nocodb.get_vendedores_activos() for vendedor in vendedores: username = vendedor.get('username') meta = vendedor.get('meta_diaria_tubos', 3) # Obtener estadísticas del día stats = self.nocodb.get_estadisticas_vendedor_dia(username) tubos = stats.get('tubos_vendidos', 0) if stats else 0 if tubos < meta: faltan = meta - tubos # Determinar mensaje según progreso if tubos == 0: emoji = '⏰' mensaje_extra = '¡Aún no has registrado ventas hoy!' elif tubos == meta - 1: emoji = '💪' mensaje_extra = '¡Solo falta 1 tubo!' else: emoji = '📊' mensaje_extra = f'Llevas {tubos} de {meta} tubos.' mensaje = ( f"{emoji} **Recordatorio para @{username}**\n\n" f"{mensaje_extra}\n" f"**Faltan {faltan} tubos** para tu meta diaria.\n\n" f"_¡Tú puedes lograrlo!_ 💪" ) self.mattermost.post_message_webhook( mensaje, username='Sales Bot', icon_emoji=':alarm_clock:' ) logger.info("Recordatorio de mediodía completado") except Exception as e: logger.error(f"Error en recordatorio de mediodía: {str(e)}") def resumen_diario(self): """ Genera y envía el resumen diario de ventas. Se ejecuta al final del día laboral. """ try: logger.info("Ejecutando resumen diario") hoy = datetime.now(TZ_MEXICO).strftime('%Y-%m-%d') # Obtener todas las ventas del día ventas_hoy = self.nocodb.get_ventas_dia() if not ventas_hoy: logger.info("No hay ventas hoy, no se envía resumen") return # Calcular estadísticas total_monto = sum(float(v.get('monto', 0)) for v in ventas_hoy) total_ventas = len(ventas_hoy) # Agrupar por vendedor vendedores_stats = {} for venta in ventas_hoy: username = venta.get('vendedor_username') if username not in vendedores_stats: vendedores_stats[username] = { 'ventas': 0, 'monto': 0 } vendedores_stats[username]['ventas'] += 1 vendedores_stats[username]['monto'] += float(venta.get('monto', 0)) # Obtener tubos y comisiones por vendedor for username in vendedores_stats: stats = self.nocodb.get_estadisticas_vendedor_dia(username, hoy) if stats: vendedores_stats[username]['tubos'] = stats.get('tubos_vendidos', 0) vendedores_stats[username]['comision'] = stats.get('comision', 0) # Calcular totales de tubos y comisiones total_tubos = sum(v.get('tubos', 0) for v in vendedores_stats.values()) total_comisiones = sum(v.get('comision', 0) for v in vendedores_stats.values()) # Ordenar vendedores por tubos vendedores_ordenados = sorted( vendedores_stats.items(), key=lambda x: x[1].get('tubos', 0), reverse=True ) # Construir mensaje mensaje = ( f"📊 **Resumen del Día - {datetime.now(TZ_MEXICO).strftime('%d/%m/%Y')}**\n\n" f"**Totales:**\n" f"• Ventas: {total_ventas}\n" f"• Monto: ${total_monto:,.2f}\n" f"• Tubos: {total_tubos} 🧪\n" f"• Comisiones: ${total_comisiones:,.2f} 💰\n\n" ) if vendedores_ordenados: mensaje += "**Top del día:**\n" for i, (username, stats) in enumerate(vendedores_ordenados[:5], 1): emoji = '🥇' if i == 1 else '🥈' if i == 2 else '🥉' if i == 3 else '🏅' tubos = stats.get('tubos', 0) comision = stats.get('comision', 0) mensaje += f"{emoji} @{username} - {tubos} tubos" if comision > 0: mensaje += f" (+${comision:,.2f})" mensaje += "\n" mensaje += "\n_¡Excelente trabajo equipo!_ 🎉" self.mattermost.post_message_webhook( mensaje, username='Sales Bot', icon_emoji=':chart_with_upwards_trend:' ) logger.info("Resumen diario enviado") except Exception as e: logger.error(f"Error en resumen diario: {str(e)}") def resumen_semanal(self): """ Genera y envía el resumen semanal de ventas. Se ejecuta los lunes por la mañana. """ try: logger.info("Ejecutando resumen semanal") # Calcular fechas de la semana anterior ahora = datetime.now(TZ_MEXICO) fin_semana = ahora - timedelta(days=ahora.weekday()) # Lunes actual fin_semana = fin_semana.replace(hour=0, minute=0, second=0, microsecond=0) inicio_semana = fin_semana - timedelta(days=7) # Lunes anterior # Obtener ventas de la semana anterior ventas_semana = self.nocodb.get_ventas_semana() # Filtrar por semana anterior ventas_filtradas = [] for venta in ventas_semana: fecha_str = venta.get('fecha_venta', '') try: fecha = datetime.fromisoformat(fecha_str.replace('+00:00', '+00:00')) if fecha.tzinfo is None: fecha = fecha.replace(tzinfo=timezone.utc) fecha_mexico = fecha.astimezone(TZ_MEXICO) if inicio_semana <= fecha_mexico < fin_semana: ventas_filtradas.append(venta) except: continue if not ventas_filtradas: logger.info("No hay ventas de la semana anterior") return # Calcular estadísticas total_monto = sum(float(v.get('monto', 0)) for v in ventas_filtradas) total_ventas = len(ventas_filtradas) # Agrupar por vendedor vendedores_stats = {} for venta in ventas_filtradas: username = venta.get('vendedor_username') if username not in vendedores_stats: vendedores_stats[username] = { 'ventas': 0, 'monto': 0 } vendedores_stats[username]['ventas'] += 1 vendedores_stats[username]['monto'] += float(venta.get('monto', 0)) # Obtener ranking del mes para comparar ranking_mes = self.nocodb.get_ranking_vendedores() # Ordenar por monto semanal vendedores_ordenados = sorted( vendedores_stats.items(), key=lambda x: x[1]['monto'], reverse=True ) # Construir mensaje mensaje = ( f"📅 **Resumen Semanal**\n" f"_{inicio_semana.strftime('%d/%m')} - {(fin_semana - timedelta(days=1)).strftime('%d/%m/%Y')}_\n\n" f"**Totales de la semana:**\n" f"• Ventas realizadas: {total_ventas}\n" f"• Monto total: ${total_monto:,.2f}\n" f"• Vendedores activos: {len(vendedores_stats)}\n\n" ) if vendedores_ordenados: mensaje += "🏆 **Top 3 de la semana:**\n" medallas = ['🥇', '🥈', '🥉'] for i, (username, stats) in enumerate(vendedores_ordenados[:3]): monto = stats['monto'] ventas = stats['ventas'] mensaje += f"{medallas[i]} @{username} - ${monto:,.2f} ({ventas} ventas)\n" mensaje += "\n_¡Excelente semana equipo! A seguir con todo._ 💪" self.mattermost.post_message_webhook( mensaje, username='Sales Bot', icon_emoji=':calendar:' ) logger.info("Resumen semanal enviado") except Exception as e: logger.error(f"Error en resumen semanal: {str(e)}") def ejecutar_ahora(self, tarea): """ Ejecuta una tarea programada inmediatamente (útil para testing). Args: tarea: 'recordatorio', 'diario', o 'semanal' """ if tarea == 'recordatorio': self.recordatorio_mediodia() elif tarea == 'diario': self.resumen_diario() elif tarea == 'semanal': self.resumen_semanal() else: logger.warning(f"Tarea desconocida: {tarea}")