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:
2026-01-18 02:41:53 +00:00
commit 5d9cbd4812
21 changed files with 4625 additions and 0 deletions

376
sales-bot/handlers.py Normal file
View 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"