Commit inicial: Sales Bot - Sistema de Automatización de Ventas
- Stack completo con Mattermost, NocoDB y Sales Bot - Procesamiento OCR de tickets con Tesseract - Sistema de comisiones por tubos de tinte - Comandos slash /metas y /ranking - Documentación completa del proyecto Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
376
sales-bot/handlers.py
Normal file
376
sales-bot/handlers.py
Normal file
@@ -0,0 +1,376 @@
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from utils import extraer_monto, extraer_cliente, formatear_moneda, extraer_tubos
|
||||
from ocr_processor import OCRProcessor
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def handle_venta_message(data, mattermost, nocodb):
|
||||
"""
|
||||
Maneja mensajes de venta en Mattermost
|
||||
NUEVO: Sistema de comisiones por tubos vendidos
|
||||
"""
|
||||
try:
|
||||
user_name = data.get('user_name')
|
||||
text = data.get('text', '').strip()
|
||||
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
|
||||
ocr_info = ""
|
||||
productos_ocr = []
|
||||
|
||||
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
|
||||
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}")
|
||||
|
||||
if not monto:
|
||||
mensaje = (
|
||||
f"@{user_name} Necesito el monto de la venta.\n"
|
||||
"**Formatos válidos:**\n"
|
||||
"• `venta @monto 1500 @cliente Juan Pérez`\n"
|
||||
"• `vendí $1500 a Juan Pérez`\n"
|
||||
"• Adjunta foto del ticket"
|
||||
)
|
||||
mattermost.post_message_webhook(mensaje, username='Sales Bot', icon_emoji=':moneybag:')
|
||||
return {'text': mensaje}
|
||||
|
||||
if not cliente:
|
||||
cliente = "Cliente sin nombre"
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
if vendedor:
|
||||
mensaje_bienvenida = (
|
||||
f"👋 ¡Bienvenido @{user_name}!\n"
|
||||
f"**Sistema de comisiones:**\n"
|
||||
f"• Meta diaria: {nocodb.META_DIARIA_TUBOS} tubos de tinte\n"
|
||||
f"• Comisión: ${nocodb.COMISION_POR_TUBO:.0f} por tubo después del {nocodb.META_DIARIA_TUBOS}º\n"
|
||||
f"¡Empieza a registrar tus ventas!"
|
||||
)
|
||||
mattermost.post_message_webhook(mensaje_bienvenida, username='Sales Bot', icon_emoji=':wave:')
|
||||
|
||||
# Registrar venta
|
||||
venta = nocodb.registrar_venta(
|
||||
vendedor_username=user_name,
|
||||
monto=monto,
|
||||
cliente=cliente,
|
||||
descripcion=text,
|
||||
mensaje_id=post_id,
|
||||
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
|
||||
elif tubos_manual and tubos_manual > 0:
|
||||
productos_manuales = [{
|
||||
'producto': 'Tinte (registro manual)',
|
||||
'marca': 'Manual',
|
||||
'cantidad': tubos_manual,
|
||||
'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}")
|
||||
|
||||
# NUEVO: 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)}")
|
||||
|
||||
# Reacción de éxito
|
||||
if post_id:
|
||||
mattermost.add_reaction(post_id, 'white_check_mark')
|
||||
|
||||
# NUEVO: Obtener estadísticas del día
|
||||
stats_dia = nocodb.get_estadisticas_vendedor_dia(user_name)
|
||||
|
||||
# Construir mensaje
|
||||
mensaje_confirmacion = (
|
||||
f"✅ **Venta 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 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 = '🔥'
|
||||
mensaje_extra = '¡Increíble día!'
|
||||
elif tubos_hoy >= meta:
|
||||
emoji = '⭐'
|
||||
mensaje_extra = '¡Meta cumplida!'
|
||||
elif tubos_hoy >= meta - 1:
|
||||
emoji = '💪'
|
||||
mensaje_extra = '¡Casi llegas!'
|
||||
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"
|
||||
f"• Comisión ganada hoy: {formatear_moneda(comision_hoy)} 💰\n"
|
||||
)
|
||||
else:
|
||||
faltan = meta - tubos_hoy
|
||||
mensaje_confirmacion += f"• Faltan {faltan} tubos para comisión\n"
|
||||
|
||||
mensaje_confirmacion += f"• {mensaje_extra}"
|
||||
|
||||
# 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."
|
||||
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)
|
||||
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')
|
||||
|
||||
# Obtener todas las ventas del día
|
||||
ventas_hoy = nocodb.get_ventas_dia()
|
||||
|
||||
# Agrupar por vendedor
|
||||
vendedores_hoy = {}
|
||||
for venta in ventas_hoy:
|
||||
vendedor = venta.get('vendedor_username')
|
||||
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"**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'}
|
||||
else:
|
||||
logger.warning(f"Canal {channel_reportes} no encontrado")
|
||||
return {'status': 'error', 'message': 'Canal no encontrado'}
|
||||
|
||||
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
|
||||
Comando: /stats o /estadisticas
|
||||
"""
|
||||
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"• 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 += (
|
||||
f"**Este mes**\n"
|
||||
f"• Tubos totales: {stats_mes.get('tubos_totales', 0)} 🧪\n"
|
||||
f"• Comisión total: {formatear_moneda(stats_mes.get('comision_total', 0))} 💰\n"
|
||||
f"• Monto total: {formatear_moneda(stats_mes.get('monto_total', 0))}\n"
|
||||
f"• Ventas: {stats_mes.get('cantidad_ventas', 0)}\n"
|
||||
f"• Días activos: {stats_mes.get('dias_activos', 0)}\n"
|
||||
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"
|
||||
Reference in New Issue
Block a user