feat: Implementar mejoras de funcionalidad y UX del Sales Bot
Nuevas funcionalidades: - /cancelar: Cancelar ventas propias con motivo opcional - /deshacer: Deshacer última venta (dentro de 5 minutos) - /editar: Editar monto y cliente de ventas propias - /comisiones: Historial de comisiones de últimos 6 meses - /racha: Sistema de bonos por días consecutivos cumpliendo meta - /exportar: Exportar ventas a Excel o CSV Sistema de confirmación obligatoria: - Todas las ventas requieren confirmación explícita (si/no) - Preview de venta antes de registrar - Timeout de 2 minutos para ventas pendientes Scheduler de notificaciones: - Recordatorio de mediodía para vendedores sin meta - Resumen diario automático al final del día - Resumen semanal los lunes Otras mejoras: - Soporte para múltiples imágenes en una venta - Autocompletado de clientes frecuentes - Metas personalizadas por vendedor - Bonos por racha: $20 (3 días), $50 (5 días), $150 (10 días) Archivos nuevos: - export_utils.py: Generación de Excel y CSV - scheduler.py: Tareas programadas con APScheduler Dependencias nuevas: - APScheduler==3.10.4 - openpyxl==3.1.2 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,129 +1,284 @@
|
||||
"""
|
||||
Handlers para procesamiento de ventas en Sales Bot
|
||||
Incluye sistema de confirmación interactiva obligatoria
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from utils import extraer_monto, extraer_cliente, formatear_moneda, extraer_tubos
|
||||
from ocr_processor import OCRProcessor
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Zona horaria de México
|
||||
TZ_MEXICO = timezone(timedelta(hours=-6))
|
||||
|
||||
# Cache de ventas pendientes de confirmación
|
||||
# Formato: {username: {datos_venta, timestamp}}
|
||||
VENTAS_PENDIENTES = {}
|
||||
|
||||
# Timeout para confirmación (en minutos)
|
||||
CONFIRMACION_TIMEOUT = int(os.getenv('CONFIRMACION_TIMEOUT_MINUTOS', 2))
|
||||
|
||||
|
||||
def limpiar_ventas_expiradas():
|
||||
"""Elimina ventas pendientes que han expirado"""
|
||||
ahora = datetime.now(TZ_MEXICO)
|
||||
expiradas = []
|
||||
|
||||
for username, datos in VENTAS_PENDIENTES.items():
|
||||
timestamp = datos.get('timestamp')
|
||||
if timestamp:
|
||||
if ahora - timestamp > timedelta(minutes=CONFIRMACION_TIMEOUT):
|
||||
expiradas.append(username)
|
||||
|
||||
for username in expiradas:
|
||||
del VENTAS_PENDIENTES[username]
|
||||
logger.info(f"Venta pendiente de {username} expirada")
|
||||
|
||||
|
||||
def handle_venta_message(data, mattermost, nocodb):
|
||||
"""
|
||||
Maneja mensajes de venta en Mattermost
|
||||
NUEVO: Sistema de comisiones por tubos vendidos
|
||||
Maneja mensajes de venta en Mattermost con confirmación interactiva obligatoria.
|
||||
|
||||
Flujo:
|
||||
1. Usuario envía mensaje de venta
|
||||
2. Bot muestra preview y pide confirmación
|
||||
3. Usuario responde "si" o "no"
|
||||
4. Bot registra la venta (si confirmada)
|
||||
"""
|
||||
try:
|
||||
user_name = data.get('user_name')
|
||||
text = data.get('text', '').strip()
|
||||
text_lower = text.lower()
|
||||
|
||||
# Limpiar ventas expiradas
|
||||
limpiar_ventas_expiradas()
|
||||
|
||||
# ==================== PROCESAR CONFIRMACIÓN ====================
|
||||
|
||||
# Si es confirmación de venta pendiente
|
||||
if text_lower in ['si', 'sí', 'confirmar', 'ok', 'yes'] and user_name in VENTAS_PENDIENTES:
|
||||
venta_data = VENTAS_PENDIENTES.pop(user_name)
|
||||
return registrar_venta_confirmada(venta_data, mattermost, nocodb)
|
||||
|
||||
# Si es cancelación de venta pendiente
|
||||
if text_lower in ['no', 'cancelar', 'cancel'] and user_name in VENTAS_PENDIENTES:
|
||||
VENTAS_PENDIENTES.pop(user_name)
|
||||
mensaje = '❌ **Venta cancelada**\n\nPuedes registrar una nueva venta cuando quieras.'
|
||||
mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':x:')
|
||||
return {'text': mensaje}
|
||||
|
||||
# Si ya tiene venta pendiente, recordar
|
||||
if user_name in VENTAS_PENDIENTES:
|
||||
venta_pendiente = VENTAS_PENDIENTES[user_name]
|
||||
tiempo_restante = CONFIRMACION_TIMEOUT - (datetime.now(TZ_MEXICO) - venta_pendiente['timestamp']).seconds // 60
|
||||
|
||||
mensaje = (
|
||||
f"⏳ **Ya tienes una venta pendiente**\n\n"
|
||||
f"**Monto:** ${venta_pendiente.get('monto', 0):,.2f}\n"
|
||||
f"**Cliente:** {venta_pendiente.get('cliente', 'Sin especificar')}\n\n"
|
||||
f"Responde **si** para confirmar o **no** para cancelar.\n"
|
||||
f"_Expira en {tiempo_restante} minuto(s)_"
|
||||
)
|
||||
mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':hourglass:')
|
||||
return {'text': mensaje}
|
||||
|
||||
# ==================== PROCESAR NUEVA VENTA ====================
|
||||
|
||||
channel_name = data.get('channel_name')
|
||||
post_id = data.get('post_id')
|
||||
file_ids = data.get('file_ids', '')
|
||||
|
||||
|
||||
if file_ids and isinstance(file_ids, str):
|
||||
file_ids = [f.strip() for f in file_ids.split(',') if f.strip()]
|
||||
elif not file_ids:
|
||||
file_ids = []
|
||||
|
||||
|
||||
logger.info(f"Procesando venta de {user_name}: {text}, archivos: {file_ids}")
|
||||
|
||||
|
||||
# Extraer información del texto
|
||||
monto = extraer_monto(text)
|
||||
cliente = extraer_cliente(text)
|
||||
tubos_manual = extraer_tubos(text) # NUEVO: tubos manuales
|
||||
|
||||
# Procesar imágenes adjuntas
|
||||
imagen_url = None
|
||||
tubos_manual = extraer_tubos(text)
|
||||
|
||||
# ==================== PROCESAR IMÁGENES (MÚLTIPLES) ====================
|
||||
imagenes_url = []
|
||||
ocr_info = ""
|
||||
productos_ocr = []
|
||||
|
||||
monto_ocr_total = 0
|
||||
tubos_ocr_total = 0
|
||||
|
||||
if file_ids:
|
||||
logger.info(f"Procesando {len(file_ids)} archivos adjuntos")
|
||||
file_id = file_ids[0]
|
||||
|
||||
imagen_data = mattermost.get_file(file_id)
|
||||
file_info = mattermost.get_file_info(file_id)
|
||||
|
||||
if file_info and imagen_data:
|
||||
filename = file_info.get('name', 'ticket.jpg')
|
||||
file_size = file_info.get('size', 0)
|
||||
|
||||
bot_token = os.getenv('MATTERMOST_BOT_TOKEN')
|
||||
mattermost_url = os.getenv('MATTERMOST_URL')
|
||||
imagen_url = f"{mattermost_url}/api/v4/files/{file_id}?access_token={bot_token}"
|
||||
|
||||
logger.info(f"Archivo adjunto: {filename}, tamaño: {file_size} bytes")
|
||||
|
||||
# Procesar con OCR
|
||||
|
||||
for file_id in file_ids:
|
||||
try:
|
||||
ocr = OCRProcessor()
|
||||
resultado_ocr = ocr.procesar_ticket(imagen_data)
|
||||
|
||||
if resultado_ocr:
|
||||
monto_ocr = resultado_ocr.get('monto_detectado')
|
||||
fecha_ocr = resultado_ocr.get('fecha_detectada')
|
||||
productos_ocr = resultado_ocr.get('productos', [])
|
||||
|
||||
if not monto and monto_ocr:
|
||||
monto = monto_ocr
|
||||
logger.info(f"Usando monto detectado por OCR: ${monto}")
|
||||
ocr_info += f"\n💡 Monto detectado: ${monto:,.2f}"
|
||||
|
||||
elif monto and monto_ocr:
|
||||
es_valido, mensaje = ocr.validar_monto_con_ocr(monto, monto_ocr, tolerancia=0.05)
|
||||
ocr_info += f"\n{mensaje}"
|
||||
|
||||
if not es_valido:
|
||||
logger.warning(mensaje)
|
||||
|
||||
if fecha_ocr:
|
||||
ocr_info += f"\n📅 Fecha: {fecha_ocr}"
|
||||
|
||||
if productos_ocr:
|
||||
# NUEVO: Contar tubos de tinte
|
||||
tubos_tinte = sum(
|
||||
p['cantidad'] for p in productos_ocr
|
||||
if 'tinte' in p['marca'].lower() or 'tinte' in p['producto'].lower()
|
||||
or 'cromatique' in p['marca'].lower()
|
||||
)
|
||||
ocr_info += f"\n🧪 Tubos de tinte: {tubos_tinte}"
|
||||
ocr_info += f"\n📦 Total productos: {len(productos_ocr)}"
|
||||
logger.info(f"Tubos de tinte detectados: {tubos_tinte}")
|
||||
|
||||
except Exception as ocr_error:
|
||||
logger.error(f"Error en OCR: {str(ocr_error)}")
|
||||
ocr_info = "\n⚠️ No se pudo leer el ticket"
|
||||
productos_ocr = []
|
||||
|
||||
logger.info(f"URL de imagen: {imagen_url}")
|
||||
|
||||
imagen_data = mattermost.get_file(file_id)
|
||||
file_info = mattermost.get_file_info(file_id)
|
||||
|
||||
if file_info and imagen_data:
|
||||
filename = file_info.get('name', 'ticket.jpg')
|
||||
file_size = file_info.get('size', 0)
|
||||
|
||||
bot_token = os.getenv('MATTERMOST_BOT_TOKEN')
|
||||
mattermost_url = os.getenv('MATTERMOST_URL')
|
||||
imagen_url = f"{mattermost_url}/api/v4/files/{file_id}?access_token={bot_token}"
|
||||
imagenes_url.append(imagen_url)
|
||||
|
||||
logger.info(f"Archivo adjunto: {filename}, tamaño: {file_size} bytes")
|
||||
|
||||
# Procesar con OCR
|
||||
try:
|
||||
ocr = OCRProcessor()
|
||||
resultado_ocr = ocr.procesar_ticket(imagen_data)
|
||||
|
||||
if resultado_ocr:
|
||||
monto_ocr = resultado_ocr.get('monto_detectado', 0)
|
||||
fecha_ocr = resultado_ocr.get('fecha_detectada')
|
||||
productos = resultado_ocr.get('productos', [])
|
||||
|
||||
if monto_ocr:
|
||||
monto_ocr_total += monto_ocr
|
||||
|
||||
if productos:
|
||||
productos_ocr.extend(productos)
|
||||
# Contar tubos de tinte
|
||||
for p in productos:
|
||||
marca = p.get('marca', '').lower()
|
||||
producto = p.get('producto', '').lower()
|
||||
if 'tinte' in marca or 'tinte' in producto or 'cromatique' in marca:
|
||||
tubos_ocr_total += p.get('cantidad', 0)
|
||||
|
||||
if fecha_ocr and not ocr_info:
|
||||
ocr_info += f"\n📅 Fecha ticket: {fecha_ocr}"
|
||||
|
||||
except Exception as ocr_error:
|
||||
logger.error(f"Error en OCR para {file_id}: {str(ocr_error)}")
|
||||
|
||||
except Exception as file_error:
|
||||
logger.error(f"Error procesando archivo {file_id}: {str(file_error)}")
|
||||
|
||||
# Resumen de OCR
|
||||
if len(file_ids) > 1:
|
||||
ocr_info += f"\n📷 Imágenes procesadas: {len(imagenes_url)}"
|
||||
|
||||
if monto_ocr_total > 0:
|
||||
ocr_info += f"\n💡 Monto OCR: ${monto_ocr_total:,.2f}"
|
||||
if not monto:
|
||||
monto = monto_ocr_total
|
||||
|
||||
if tubos_ocr_total > 0:
|
||||
ocr_info += f"\n🧪 Tubos detectados: {tubos_ocr_total}"
|
||||
|
||||
if productos_ocr:
|
||||
ocr_info += f"\n📦 Productos: {len(productos_ocr)}"
|
||||
|
||||
# ==================== VALIDAR DATOS ====================
|
||||
|
||||
if not monto:
|
||||
# Sugerir clientes frecuentes si no hay cliente
|
||||
sugerencias = ""
|
||||
if not cliente:
|
||||
clientes_frecuentes = nocodb.get_clientes_frecuentes(user_name, limit=3)
|
||||
if clientes_frecuentes:
|
||||
lista = '\n'.join([f" • {c}" for c in clientes_frecuentes])
|
||||
sugerencias = f"\n\n💡 **Clientes frecuentes:**\n{lista}"
|
||||
|
||||
mensaje = (
|
||||
f"@{user_name} Necesito el monto de la venta.\n"
|
||||
f"@{user_name} Necesito el monto de la venta.\n\n"
|
||||
"**Formatos válidos:**\n"
|
||||
"• `venta @monto 1500 @cliente Juan Pérez`\n"
|
||||
"• `vendí $1500 a Juan Pérez`\n"
|
||||
"• `venta @monto 1500 @tubos 3`\n"
|
||||
"• Adjunta foto del ticket"
|
||||
f"{sugerencias}"
|
||||
)
|
||||
mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':moneybag:')
|
||||
return {'text': mensaje}
|
||||
|
||||
|
||||
if not cliente:
|
||||
cliente = "Cliente sin nombre"
|
||||
|
||||
|
||||
# Usar tubos manuales o de OCR
|
||||
tubos_detectados = tubos_manual or tubos_ocr_total or 0
|
||||
|
||||
# ==================== CREAR PREVIEW Y PEDIR CONFIRMACIÓN ====================
|
||||
|
||||
# Guardar datos de la venta pendiente
|
||||
VENTAS_PENDIENTES[user_name] = {
|
||||
'user_name': user_name,
|
||||
'monto': monto,
|
||||
'cliente': cliente,
|
||||
'tubos': tubos_detectados,
|
||||
'tubos_manual': tubos_manual,
|
||||
'productos_ocr': productos_ocr,
|
||||
'imagenes_url': imagenes_url,
|
||||
'ocr_info': ocr_info,
|
||||
'channel_name': channel_name,
|
||||
'post_id': post_id,
|
||||
'text': text,
|
||||
'timestamp': datetime.now(TZ_MEXICO)
|
||||
}
|
||||
|
||||
# Construir mensaje de preview
|
||||
mensaje_preview = f"📋 **Preview de Venta**\n\n"
|
||||
mensaje_preview += f"**Vendedor:** @{user_name}\n"
|
||||
mensaje_preview += f"**Monto:** ${monto:,.2f}\n"
|
||||
mensaje_preview += f"**Cliente:** {cliente}\n"
|
||||
|
||||
if tubos_detectados > 0:
|
||||
mensaje_preview += f"**Tubos:** {tubos_detectados} 🧪\n"
|
||||
|
||||
if imagenes_url:
|
||||
mensaje_preview += f"**Tickets:** {len(imagenes_url)} imagen(es) 📸\n"
|
||||
|
||||
if ocr_info:
|
||||
mensaje_preview += f"\n**Datos detectados:**{ocr_info}\n"
|
||||
|
||||
mensaje_preview += f"\n¿Confirmar esta venta? Responde **si** o **no**\n"
|
||||
mensaje_preview += f"_(Expira en {CONFIRMACION_TIMEOUT} minutos)_"
|
||||
|
||||
mattermost.post_message_webhook(mensaje_preview, username='Sales Bot', icon_emoji=':clipboard:')
|
||||
return {'text': mensaje_preview}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en handle_venta_message: {str(e)}", exc_info=True)
|
||||
mensaje_error = f"❌ Error: {str(e)}"
|
||||
return {'text': mensaje_error}
|
||||
|
||||
|
||||
def registrar_venta_confirmada(venta_data, mattermost, nocodb):
|
||||
"""
|
||||
Registra una venta después de ser confirmada por el usuario.
|
||||
"""
|
||||
try:
|
||||
user_name = venta_data.get('user_name')
|
||||
monto = venta_data.get('monto')
|
||||
cliente = venta_data.get('cliente')
|
||||
tubos_manual = venta_data.get('tubos_manual')
|
||||
productos_ocr = venta_data.get('productos_ocr', [])
|
||||
imagenes_url = venta_data.get('imagenes_url', [])
|
||||
ocr_info = venta_data.get('ocr_info', '')
|
||||
channel_name = venta_data.get('channel_name')
|
||||
post_id = venta_data.get('post_id')
|
||||
text = venta_data.get('text', '')
|
||||
|
||||
# Verificar/crear vendedor
|
||||
vendedor = nocodb.get_vendedor(user_name)
|
||||
if not vendedor:
|
||||
user_info = mattermost.get_user_by_username(user_name)
|
||||
email = user_info.get('email', f"{user_name}@consultoria-as.com") if user_info else f"{user_name}@consultoria-as.com"
|
||||
nombre = user_info.get('first_name', user_name) if user_info else user_name
|
||||
|
||||
|
||||
vendedor = nocodb.crear_vendedor(
|
||||
username=user_name,
|
||||
nombre_completo=nombre,
|
||||
email=email,
|
||||
meta_diaria_tubos=3 # NUEVO: Meta de 3 tubos diarios
|
||||
meta_diaria_tubos=3
|
||||
)
|
||||
|
||||
|
||||
if vendedor:
|
||||
mensaje_bienvenida = (
|
||||
f"👋 ¡Bienvenido @{user_name}!\n"
|
||||
@@ -133,7 +288,10 @@ def handle_venta_message(data, mattermost, nocodb):
|
||||
f"¡Empieza a registrar tus ventas!"
|
||||
)
|
||||
mattermost.post_message_webhook(mensaje_bienvenida, username='Sales Bot', icon_emoji=':wave:')
|
||||
|
||||
|
||||
# Usar primera imagen como principal
|
||||
imagen_url = imagenes_url[0] if imagenes_url else None
|
||||
|
||||
# Registrar venta
|
||||
venta = nocodb.registrar_venta(
|
||||
vendedor_username=user_name,
|
||||
@@ -144,17 +302,17 @@ def handle_venta_message(data, mattermost, nocodb):
|
||||
canal=channel_name,
|
||||
imagen_url=imagen_url
|
||||
)
|
||||
|
||||
|
||||
if venta:
|
||||
venta_id = venta.get('Id')
|
||||
|
||||
|
||||
# Guardar productos detectados por OCR
|
||||
if productos_ocr:
|
||||
productos_guardados = nocodb.guardar_productos_venta(venta_id, productos_ocr)
|
||||
if productos_guardados:
|
||||
logger.info(f"Guardados {len(productos_guardados)} productos para venta {venta_id}")
|
||||
|
||||
# NUEVO: Guardar tubos manuales si se especificaron
|
||||
# Guardar tubos manuales si se especificaron
|
||||
elif tubos_manual and tubos_manual > 0:
|
||||
productos_manuales = [{
|
||||
'producto': 'Tinte (registro manual)',
|
||||
@@ -163,42 +321,44 @@ def handle_venta_message(data, mattermost, nocodb):
|
||||
'precio_unitario': monto / tubos_manual if tubos_manual > 0 else 0,
|
||||
'importe': monto
|
||||
}]
|
||||
productos_guardados = nocodb.guardar_productos_venta(venta_id, productos_manuales)
|
||||
if productos_guardados:
|
||||
logger.info(f"Guardados {tubos_manual} tubos manuales para venta {venta_id}")
|
||||
nocodb.guardar_productos_venta(venta_id, productos_manuales)
|
||||
logger.info(f"Guardados {tubos_manual} tubos manuales para venta {venta_id}")
|
||||
|
||||
# NUEVO: Actualizar tabla de metas
|
||||
# Actualizar tabla de metas
|
||||
try:
|
||||
nocodb.actualizar_meta_vendedor(user_name)
|
||||
logger.info(f"Metas actualizadas para {user_name}")
|
||||
except Exception as meta_error:
|
||||
logger.error(f"Error actualizando metas: {str(meta_error)}")
|
||||
|
||||
# Verificar racha
|
||||
racha = nocodb.verificar_racha(user_name)
|
||||
|
||||
# Reacción de éxito
|
||||
if post_id:
|
||||
mattermost.add_reaction(post_id, 'white_check_mark')
|
||||
|
||||
# NUEVO: Obtener estadísticas del día
|
||||
|
||||
# Obtener estadísticas del día
|
||||
stats_dia = nocodb.get_estadisticas_vendedor_dia(user_name)
|
||||
|
||||
# Construir mensaje
|
||||
|
||||
# Construir mensaje de confirmación
|
||||
mensaje_confirmacion = (
|
||||
f"✅ **Venta registrada**\n\n"
|
||||
f"✅ **Venta #{venta_id} registrada**\n\n"
|
||||
f"**Vendedor:** @{user_name}\n"
|
||||
f"**Monto:** {formatear_moneda(monto)}\n"
|
||||
f"**Cliente:** {cliente}\n"
|
||||
)
|
||||
|
||||
if imagen_url:
|
||||
mensaje_confirmacion += f"📸 **Ticket:** Guardado{ocr_info}\n"
|
||||
|
||||
# NUEVO: Mostrar estadísticas de tubos y comisiones
|
||||
|
||||
if imagenes_url:
|
||||
mensaje_confirmacion += f"📸 **Tickets:** {len(imagenes_url)} guardado(s){ocr_info}\n"
|
||||
|
||||
# Mostrar estadísticas de tubos y comisiones
|
||||
if stats_dia:
|
||||
tubos_hoy = stats_dia.get('tubos_vendidos', 0)
|
||||
comision_hoy = stats_dia.get('comision', 0)
|
||||
meta = stats_dia.get('meta_diaria', 3)
|
||||
tubos_comisionables = stats_dia.get('tubos_comisionables', 0)
|
||||
|
||||
|
||||
# Determinar emoji según progreso
|
||||
if tubos_hoy >= meta * 2:
|
||||
emoji = '🔥'
|
||||
@@ -212,13 +372,13 @@ def handle_venta_message(data, mattermost, nocodb):
|
||||
else:
|
||||
emoji = '📊'
|
||||
mensaje_extra = '¡Sigue así!'
|
||||
|
||||
|
||||
mensaje_confirmacion += (
|
||||
f"\n**Resumen del día:** {emoji}\n"
|
||||
f"• Tubos vendidos hoy: {tubos_hoy} 🧪\n"
|
||||
f"• Meta diaria: {meta} tubos\n"
|
||||
)
|
||||
|
||||
|
||||
if tubos_hoy > meta:
|
||||
mensaje_confirmacion += (
|
||||
f"• Tubos con comisión: {tubos_comisionables}\n"
|
||||
@@ -227,41 +387,46 @@ def handle_venta_message(data, mattermost, nocodb):
|
||||
else:
|
||||
faltan = meta - tubos_hoy
|
||||
mensaje_confirmacion += f"• Faltan {faltan} tubos para comisión\n"
|
||||
|
||||
|
||||
mensaje_confirmacion += f"• {mensaje_extra}"
|
||||
|
||||
|
||||
# Mostrar info de racha si aplica
|
||||
if racha and racha.get('dias_consecutivos', 0) >= 3:
|
||||
dias = racha['dias_consecutivos']
|
||||
bonus = racha.get('bonus', 0)
|
||||
mensaje_confirmacion += f"\n\n🔥 **Racha: {dias} días** "
|
||||
if bonus > 0:
|
||||
mensaje_confirmacion += f"(+${bonus:,.2f} bonus)"
|
||||
|
||||
# Enviar confirmación
|
||||
mattermost.post_message_webhook(
|
||||
mensaje_confirmacion,
|
||||
username='Sales Bot',
|
||||
icon_emoji=':moneybag:'
|
||||
)
|
||||
|
||||
|
||||
return {'text': mensaje_confirmacion}
|
||||
else:
|
||||
mensaje_error = f"❌ Error al registrar la venta. Intenta de nuevo."
|
||||
mensaje_error = "❌ Error al registrar la venta. Intenta de nuevo."
|
||||
mattermost.post_message_webhook(mensaje_error, username='Sales Bot', icon_emoji=':x:')
|
||||
return {'text': mensaje_error}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en handle_venta_message: {str(e)}", exc_info=True)
|
||||
logger.error(f"Error en registrar_venta_confirmada: {str(e)}", exc_info=True)
|
||||
mensaje_error = f"❌ Error: {str(e)}"
|
||||
return {'text': mensaje_error}
|
||||
|
||||
|
||||
def generar_reporte_diario(mattermost, nocodb):
|
||||
"""
|
||||
Genera reporte diario de ventas y comisiones
|
||||
NUEVO: Muestra tubos vendidos y comisiones ganadas
|
||||
"""
|
||||
try:
|
||||
import os
|
||||
|
||||
hoy = datetime.now().strftime('%Y-%m-%d')
|
||||
mes_actual = datetime.now().strftime('%Y-%m')
|
||||
|
||||
hoy = datetime.now(TZ_MEXICO).strftime('%Y-%m-%d')
|
||||
|
||||
# Obtener todas las ventas del día
|
||||
ventas_hoy = nocodb.get_ventas_dia()
|
||||
|
||||
|
||||
# Agrupar por vendedor
|
||||
vendedores_hoy = {}
|
||||
for venta in ventas_hoy:
|
||||
@@ -269,64 +434,70 @@ def generar_reporte_diario(mattermost, nocodb):
|
||||
if vendedor not in vendedores_hoy:
|
||||
vendedores_hoy[vendedor] = []
|
||||
vendedores_hoy[vendedor].append(venta)
|
||||
|
||||
|
||||
# Calcular estadísticas por vendedor
|
||||
stats_vendedores = []
|
||||
for vendedor in vendedores_hoy.keys():
|
||||
stats = nocodb.get_estadisticas_vendedor_dia(vendedor, hoy)
|
||||
if stats:
|
||||
stats_vendedores.append(stats)
|
||||
|
||||
|
||||
# Ordenar por tubos vendidos
|
||||
stats_vendedores.sort(key=lambda x: x.get('tubos_vendidos', 0), reverse=True)
|
||||
|
||||
|
||||
# Calcular totales
|
||||
total_tubos_dia = sum(s.get('tubos_vendidos', 0) for s in stats_vendedores)
|
||||
total_comisiones = sum(s.get('comision', 0) for s in stats_vendedores)
|
||||
total_monto = sum(s.get('monto_total_dia', 0) for s in stats_vendedores)
|
||||
|
||||
|
||||
# Construir mensaje
|
||||
mensaje = (
|
||||
f"📊 **Reporte Diario - {datetime.now().strftime('%d/%m/%Y')}**\n\n"
|
||||
f"📊 **Reporte Diario - {datetime.now(TZ_MEXICO).strftime('%d/%m/%Y')}**\n\n"
|
||||
f"**Resumen del día:**\n"
|
||||
f"• Tubos vendidos: {total_tubos_dia} 🧪\n"
|
||||
f"• Comisiones pagadas: {formatear_moneda(total_comisiones)} 💰\n"
|
||||
f"• Monto total: {formatear_moneda(total_monto)}\n"
|
||||
f"• Ventas: {len(ventas_hoy)}\n\n"
|
||||
)
|
||||
|
||||
|
||||
if stats_vendedores:
|
||||
mensaje += "**Top Vendedores del Día:**\n"
|
||||
for i, stats in enumerate(stats_vendedores[:5], 1):
|
||||
vendedor = stats.get('vendedor')
|
||||
tubos = stats.get('tubos_vendidos', 0)
|
||||
comision = stats.get('comision', 0)
|
||||
|
||||
|
||||
emoji = '🥇' if i == 1 else '🥈' if i == 2 else '🥉' if i == 3 else '🏅'
|
||||
|
||||
|
||||
if comision > 0:
|
||||
mensaje += f"{emoji} @{vendedor} - {tubos} tubos ({formatear_moneda(comision)} comisión)\n"
|
||||
else:
|
||||
mensaje += f"{emoji} @{vendedor} - {tubos} tubos\n"
|
||||
|
||||
|
||||
# Obtener canal de reportes
|
||||
team_name = os.getenv('MATTERMOST_TEAM_NAME')
|
||||
channel_reportes = os.getenv('MATTERMOST_CHANNEL_REPORTES')
|
||||
|
||||
canal = mattermost.get_channel_by_name(team_name, channel_reportes)
|
||||
|
||||
if canal:
|
||||
mattermost.post_message(canal['id'], mensaje)
|
||||
logger.info("Reporte diario generado")
|
||||
return {'status': 'success', 'message': 'Reporte generado'}
|
||||
|
||||
if channel_reportes:
|
||||
canal = mattermost.get_channel_by_name(team_name, channel_reportes)
|
||||
|
||||
if canal:
|
||||
mattermost.post_message(canal['id'], mensaje)
|
||||
logger.info("Reporte diario generado")
|
||||
return {'status': 'success', 'message': 'Reporte generado'}
|
||||
else:
|
||||
logger.warning(f"Canal {channel_reportes} no encontrado")
|
||||
return {'status': 'error', 'message': 'Canal no encontrado'}
|
||||
else:
|
||||
logger.warning(f"Canal {channel_reportes} no encontrado")
|
||||
return {'status': 'error', 'message': 'Canal no encontrado'}
|
||||
|
||||
# Enviar por webhook si no hay canal específico
|
||||
mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':chart_with_upwards_trend:')
|
||||
return {'status': 'success', 'message': 'Reporte enviado por webhook'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generando reporte diario: {str(e)}", exc_info=True)
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
|
||||
def comando_estadisticas(user_name, mattermost, nocodb):
|
||||
"""
|
||||
Muestra estadísticas personales del vendedor
|
||||
@@ -335,26 +506,26 @@ def comando_estadisticas(user_name, mattermost, nocodb):
|
||||
try:
|
||||
# Estadísticas del día
|
||||
stats_hoy = nocodb.get_estadisticas_vendedor_dia(user_name)
|
||||
|
||||
|
||||
# Estadísticas del mes
|
||||
stats_mes = nocodb.get_estadisticas_vendedor_mes(user_name)
|
||||
|
||||
|
||||
if not stats_hoy and not stats_mes:
|
||||
mensaje = f"@{user_name} Aún no tienes ventas registradas."
|
||||
return mensaje
|
||||
|
||||
|
||||
mensaje = f"📈 **Estadísticas de @{user_name}**\n\n"
|
||||
|
||||
|
||||
# Hoy
|
||||
if stats_hoy:
|
||||
mensaje += (
|
||||
f"**Hoy ({datetime.now().strftime('%d/%m')})**\n"
|
||||
f"**Hoy ({datetime.now(TZ_MEXICO).strftime('%d/%m')})**\n"
|
||||
f"• Tubos: {stats_hoy.get('tubos_vendidos', 0)} 🧪\n"
|
||||
f"• Comisión: {formatear_moneda(stats_hoy.get('comision', 0))}\n"
|
||||
f"• Monto: {formatear_moneda(stats_hoy.get('monto_total_dia', 0))}\n"
|
||||
f"• Ventas: {stats_hoy.get('cantidad_ventas', 0)}\n\n"
|
||||
)
|
||||
|
||||
|
||||
# Mes
|
||||
if stats_mes:
|
||||
mensaje += (
|
||||
@@ -367,10 +538,10 @@ def comando_estadisticas(user_name, mattermost, nocodb):
|
||||
f"• Días con meta: {stats_mes.get('dias_meta_cumplida', 0)}\n"
|
||||
f"• Promedio/día: {stats_mes.get('promedio_tubos_dia', 0):.1f} tubos\n"
|
||||
)
|
||||
|
||||
|
||||
mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':bar_chart:')
|
||||
return mensaje
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en comando_estadisticas: {str(e)}", exc_info=True)
|
||||
return f"❌ Error obteniendo estadísticas"
|
||||
return "❌ Error obteniendo estadísticas"
|
||||
|
||||
Reference in New Issue
Block a user