- 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>
377 lines
16 KiB
Python
377 lines
16 KiB
Python
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"
|