FASE 1 - PWA y Frontend: - Crear templates/base.html, dashboard.html, analytics.html, executive.html - Crear static/css/main.css con diseño responsivo - Agregar static/js/app.js, pwa.js, camera.js, charts.js - Implementar manifest.json y service-worker.js para PWA - Soporte para captura de tickets desde cámara móvil FASE 2 - Analytics: - Crear módulo analytics/ con predictions.py, trends.py, comparisons.py - Implementar predicción básica con promedio móvil + tendencia lineal - Agregar endpoints /api/analytics/trends, predictions, comparisons - Integrar Chart.js para gráficas interactivas FASE 3 - Reportes PDF: - Crear módulo reports/ con pdf_generator.py - Implementar SalesReportPDF con generar_reporte_diario y ejecutivo - Agregar comando /reporte [diario|semanal|ejecutivo] - Agregar endpoints /api/reports/generate y /api/reports/download FASE 4 - Mejoras OCR: - Crear módulo ocr/ con processor.py, preprocessor.py, patterns.py - Implementar AmountDetector con patrones múltiples de montos - Agregar preprocesador adaptativo con pipelines para diferentes condiciones - Soporte para corrección de rotación (deskew) y threshold Otsu Dependencias agregadas: - reportlab, matplotlib (PDF) - scipy, pandas (analytics) - imutils, deskew, cachetools (OCR) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1178 lines
41 KiB
Python
1178 lines
41 KiB
Python
from flask import Flask, request, jsonify, render_template_string, render_template, send_from_directory
|
|
import os
|
|
import logging
|
|
from dotenv import load_dotenv
|
|
from datetime import datetime, timedelta, timezone
|
|
import json
|
|
import base64
|
|
|
|
# Cargar variables de entorno
|
|
load_dotenv()
|
|
|
|
# Timezone México
|
|
TZ_MEXICO = timezone(timedelta(hours=-6))
|
|
|
|
# Importar módulos personalizados
|
|
from mattermost_client import MattermostClient
|
|
from nocodb_client import NocoDBClient
|
|
from handlers import handle_venta_message, generar_reporte_diario
|
|
from utils import validar_token_outgoing
|
|
from websocket_listener import MattermostWebsocketListener
|
|
from scheduler import SalesBotScheduler
|
|
|
|
# Configurar logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Inicializar Flask con templates y static folders
|
|
app = Flask(__name__,
|
|
template_folder='templates',
|
|
static_folder='static')
|
|
|
|
# Inicializar clientes
|
|
mattermost = MattermostClient(
|
|
url=os.getenv('MATTERMOST_URL'),
|
|
token=os.getenv('MATTERMOST_BOT_TOKEN')
|
|
)
|
|
|
|
nocodb = NocoDBClient(
|
|
url=os.getenv('NOCODB_URL'),
|
|
token=os.getenv('NOCODB_TOKEN')
|
|
)
|
|
|
|
# Inicializar websocket listener
|
|
ws_listener = MattermostWebsocketListener(mattermost, nocodb, handle_venta_message)
|
|
ws_listener.start()
|
|
logger.info("Websocket listener iniciado para escuchar mensajes de Mattermost")
|
|
|
|
# Inicializar scheduler de tareas programadas
|
|
scheduler = SalesBotScheduler(mattermost, nocodb)
|
|
scheduler.iniciar()
|
|
logger.info("Scheduler de tareas programadas iniciado")
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
def health_check():
|
|
"""Endpoint para verificar que el bot está funcionando"""
|
|
return jsonify({
|
|
'status': 'healthy',
|
|
'timestamp': datetime.now().isoformat(),
|
|
'version': '1.0.0'
|
|
}), 200
|
|
|
|
@app.route('/webhook/mattermost', methods=['POST'])
|
|
def mattermost_webhook():
|
|
"""
|
|
Recibe webhooks salientes de Mattermost cuando hay mensajes en el canal de ventas
|
|
"""
|
|
try:
|
|
data = request.json
|
|
logger.info(f"Webhook recibido: {json.dumps(data, indent=2)}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
if not validar_token_outgoing(token):
|
|
logger.warning(f"Token inválido: {token}")
|
|
return jsonify({'error': 'Token inválido'}), 403
|
|
|
|
# Ignorar mensajes del propio bot
|
|
if data.get('user_name') == 'sales-bot':
|
|
return jsonify({'status': 'ignored', 'reason': 'bot message'}), 200
|
|
|
|
# Obtener información del mensaje
|
|
channel_name = data.get('channel_name')
|
|
user_name = data.get('user_name')
|
|
text = data.get('text', '').strip()
|
|
|
|
logger.info(f"Procesando mensaje de {user_name} en #{channel_name}: {text}")
|
|
|
|
# Verificar si es un mensaje de venta
|
|
palabras_clave = ['venta', 'vendi', 'vendí', 'cliente', 'ticket']
|
|
es_venta = any(palabra in text.lower() for palabra in palabras_clave)
|
|
|
|
if es_venta or data.get('file_ids'):
|
|
respuesta = handle_venta_message(data, mattermost, nocodb)
|
|
return jsonify(respuesta), 200
|
|
|
|
return jsonify({'status': 'ok', 'message': 'Mensaje procesado'}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando webhook: {str(e)}", exc_info=True)
|
|
return jsonify({'error': 'Error interno del servidor'}), 500
|
|
|
|
@app.route('/webhook/nocodb', methods=['POST'])
|
|
def nocodb_webhook():
|
|
"""
|
|
Recibe webhooks de NocoDB cuando se insertan/actualizan datos
|
|
"""
|
|
try:
|
|
data = request.json
|
|
logger.info(f"Webhook NocoDB recibido: {json.dumps(data, indent=2)}")
|
|
|
|
# Aquí puedes agregar lógica para notificar en Mattermost
|
|
# cuando se actualicen datos en NocoDB
|
|
|
|
return jsonify({'status': 'ok'}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando webhook NocoDB: {str(e)}", exc_info=True)
|
|
return jsonify({'error': 'Error interno del servidor'}), 500
|
|
|
|
@app.route('/comando/metas', methods=['POST'])
|
|
def comando_metas():
|
|
"""
|
|
Endpoint para el comando slash /metas en Mattermost
|
|
"""
|
|
try:
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /metas recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
expected_tokens = [
|
|
os.getenv('MATTERMOST_SLASH_TOKEN_METAS'),
|
|
os.getenv('MATTERMOST_OUTGOING_TOKEN')
|
|
]
|
|
if token not in expected_tokens:
|
|
return jsonify({'text': 'Token inválido'}), 403
|
|
|
|
user_name = data.get('user_name')
|
|
|
|
# Obtener meta del vendedor
|
|
meta = nocodb.get_meta_vendedor(user_name)
|
|
|
|
if not meta:
|
|
mensaje = (
|
|
f"@{user_name} Aún no tienes ventas registradas este mes.\n"
|
|
"¡Empieza a vender y registra tus ventas!"
|
|
)
|
|
else:
|
|
porcentaje = meta.get('porcentaje_completado', 0)
|
|
total_vendido = meta.get('total_vendido', 0)
|
|
meta_establecida = meta.get('meta_establecida', 0)
|
|
ventas_count = meta.get('ventas_realizadas', 0)
|
|
falta = meta_establecida - total_vendido
|
|
|
|
# Barra de progreso visual
|
|
barra_length = 20
|
|
completado = int((porcentaje / 100) * barra_length)
|
|
barra = '█' * completado + '░' * (barra_length - completado)
|
|
|
|
mensaje = (
|
|
f"📊 **Reporte de {user_name}**\n\n"
|
|
f"`{barra}` {porcentaje:.1f}%\n\n"
|
|
f"**Total vendido:** ${total_vendido:,.2f} MXN\n"
|
|
f"**Meta mensual:** ${meta_establecida:,.2f} MXN\n"
|
|
f"**Falta:** ${falta:,.2f} MXN\n"
|
|
f"**Ventas realizadas:** {ventas_count}\n"
|
|
)
|
|
|
|
if porcentaje >= 100:
|
|
mensaje += "\n🎉 **¡Felicidades! Meta completada**"
|
|
elif porcentaje >= 75:
|
|
mensaje += f"\n🔥 **¡Casi llegas! Solo faltan ${falta:,.2f}**"
|
|
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /metas: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
@app.route('/comando/ranking', methods=['POST'])
|
|
def comando_ranking():
|
|
"""
|
|
Endpoint para el comando slash /ranking en Mattermost
|
|
"""
|
|
try:
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /ranking recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
expected_tokens = [
|
|
os.getenv('MATTERMOST_SLASH_TOKEN_RANKING'),
|
|
os.getenv('MATTERMOST_OUTGOING_TOKEN')
|
|
]
|
|
if token not in expected_tokens:
|
|
return jsonify({'text': 'Token inválido'}), 403
|
|
|
|
# Obtener ranking
|
|
ranking = nocodb.get_ranking_vendedores()
|
|
|
|
if not ranking:
|
|
mensaje = "No hay datos de ventas este mes."
|
|
else:
|
|
mensaje = "🏆 **Ranking de Vendedores - Mes Actual**\n\n"
|
|
|
|
for i, vendedor in enumerate(ranking[:10], 1):
|
|
username = vendedor.get('vendedor_username')
|
|
total = vendedor.get('total_vendido', 0)
|
|
porcentaje = vendedor.get('porcentaje_completado', 0)
|
|
ventas = vendedor.get('ventas_realizadas', 0)
|
|
|
|
# Medallas para top 3
|
|
if i == 1:
|
|
emoji = '🥇'
|
|
elif i == 2:
|
|
emoji = '🥈'
|
|
elif i == 3:
|
|
emoji = '🥉'
|
|
else:
|
|
emoji = f'{i}.'
|
|
|
|
mensaje += (
|
|
f"{emoji} **@{username}**\n"
|
|
f" └ ${total:,.2f} MXN ({porcentaje:.1f}%) - {ventas} ventas\n"
|
|
)
|
|
|
|
return jsonify({
|
|
'response_type': 'in_channel',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /ranking: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
@app.route('/comando/ayuda', methods=['POST'])
|
|
def comando_ayuda():
|
|
"""
|
|
Endpoint para el comando slash /ayuda en Mattermost
|
|
"""
|
|
try:
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /ayuda recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
expected_tokens = [
|
|
os.getenv('MATTERMOST_SLASH_TOKEN_AYUDA'),
|
|
os.getenv('MATTERMOST_OUTGOING_TOKEN')
|
|
]
|
|
if token not in expected_tokens:
|
|
return jsonify({'text': 'Token inválido'}), 403
|
|
|
|
mensaje = (
|
|
"🤖 **Bot de Ventas - Guía de Uso**\n\n"
|
|
"**Para registrar una venta:**\n"
|
|
"• `venta @monto 1500 @cliente Juan Pérez`\n"
|
|
"• `vendí $1500 a María García`\n"
|
|
"• También puedes adjuntar foto(s) del ticket\n"
|
|
"• _Todas las ventas requieren confirmación_\n\n"
|
|
"**Comandos básicos:**\n"
|
|
"• `/metas` - Ver tu progreso del mes\n"
|
|
"• `/ranking` - Ver ranking de vendedores\n"
|
|
"• `/ayuda` - Mostrar esta ayuda\n\n"
|
|
"**Comandos de gestión:**\n"
|
|
"• `/cancelar <id> [motivo]` - Cancelar una venta\n"
|
|
"• `/editar <id> @monto X @cliente Y` - Editar una venta\n"
|
|
"• `/deshacer` - Deshacer última venta (5 min)\n\n"
|
|
"**Comandos de comisiones:**\n"
|
|
"• `/comisiones` - Ver historial de comisiones\n"
|
|
"• `/racha` - Ver tu racha actual y bonos\n"
|
|
"• `/exportar [csv] [mes]` - Exportar ventas a Excel/CSV\n\n"
|
|
"**Sistema de confirmación:**\n"
|
|
"1. Envía tu mensaje de venta o foto\n"
|
|
"2. El bot muestra un preview\n"
|
|
"3. Responde **si** para confirmar o **no** para cancelar\n\n"
|
|
"**Bonos por racha:**\n"
|
|
"• 3 días consecutivos: +$20\n"
|
|
"• 5 días consecutivos: +$50\n"
|
|
"• 10 días consecutivos: +$150\n\n"
|
|
"¡Sigue adelante! 💪"
|
|
)
|
|
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /ayuda: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
# ============== NUEVOS COMANDOS FASE 1-3 ==============
|
|
|
|
@app.route('/comando/cancelar', methods=['POST'])
|
|
def comando_cancelar():
|
|
"""
|
|
Endpoint para el comando slash /cancelar en Mattermost
|
|
Uso: /cancelar <id_venta> [motivo]
|
|
"""
|
|
try:
|
|
from utils import extraer_id_venta, extraer_motivo, validar_tokens_comando
|
|
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /cancelar recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
if not validar_tokens_comando(token, 'cancelar'):
|
|
return jsonify({'text': '❌ Token inválido'}), 403
|
|
|
|
user_name = data.get('user_name')
|
|
texto = data.get('text', '').strip()
|
|
|
|
# Extraer ID de venta
|
|
venta_id = extraer_id_venta(texto)
|
|
if not venta_id:
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': '❌ **Uso incorrecto**\n\nFormato: `/cancelar <id_venta> [motivo]`\n\nEjemplo: `/cancelar 123 "cliente canceló"`'
|
|
}), 200
|
|
|
|
# Extraer motivo opcional
|
|
motivo = extraer_motivo(texto)
|
|
|
|
# Cancelar venta
|
|
resultado = nocodb.cancelar_venta(venta_id, user_name, motivo)
|
|
|
|
if resultado.get('success'):
|
|
mensaje = (
|
|
f"✅ **Venta #{venta_id} cancelada**\n\n"
|
|
f"**Monto:** ${resultado.get('monto', 0):,.2f}\n"
|
|
f"**Cliente:** {resultado.get('cliente', 'N/A')}\n"
|
|
)
|
|
if motivo:
|
|
mensaje += f"**Motivo:** {motivo}\n"
|
|
mensaje += "\n_Las metas han sido recalculadas._"
|
|
else:
|
|
mensaje = f"❌ **Error:** {resultado.get('error')}"
|
|
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /cancelar: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/comando/deshacer', methods=['POST'])
|
|
def comando_deshacer():
|
|
"""
|
|
Endpoint para el comando slash /deshacer en Mattermost
|
|
Cancela la última venta del usuario (dentro de los últimos 5 minutos)
|
|
"""
|
|
try:
|
|
from utils import validar_tokens_comando
|
|
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /deshacer recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
if not validar_tokens_comando(token, 'deshacer'):
|
|
return jsonify({'text': '❌ Token inválido'}), 403
|
|
|
|
user_name = data.get('user_name')
|
|
|
|
# Obtener última venta
|
|
ultima_venta = nocodb.get_ultima_venta_usuario(user_name, minutos=5)
|
|
|
|
if not ultima_venta:
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': '❌ **No hay ventas recientes para deshacer**\n\nSolo puedes deshacer ventas de los últimos 5 minutos.'
|
|
}), 200
|
|
|
|
venta_id = ultima_venta.get('Id')
|
|
|
|
# Cancelar la venta
|
|
resultado = nocodb.cancelar_venta(venta_id, user_name, 'Deshecho por usuario')
|
|
|
|
if resultado.get('success'):
|
|
mensaje = (
|
|
f"↩️ **Venta #{venta_id} deshecha**\n\n"
|
|
f"**Monto:** ${resultado.get('monto', 0):,.2f}\n"
|
|
f"**Cliente:** {resultado.get('cliente', 'N/A')}\n\n"
|
|
"_Las metas han sido recalculadas._"
|
|
)
|
|
else:
|
|
mensaje = f"❌ **Error:** {resultado.get('error')}"
|
|
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /deshacer: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/comando/editar', methods=['POST'])
|
|
def comando_editar():
|
|
"""
|
|
Endpoint para el comando slash /editar en Mattermost
|
|
Uso: /editar <id_venta> @monto 1600 @cliente Nuevo Cliente
|
|
"""
|
|
try:
|
|
from utils import extraer_id_venta, extraer_monto, extraer_cliente, validar_tokens_comando
|
|
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /editar recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
if not validar_tokens_comando(token, 'editar'):
|
|
return jsonify({'text': '❌ Token inválido'}), 403
|
|
|
|
user_name = data.get('user_name')
|
|
texto = data.get('text', '').strip()
|
|
|
|
# Extraer ID de venta
|
|
venta_id = extraer_id_venta(texto)
|
|
if not venta_id:
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': '❌ **Uso incorrecto**\n\nFormato: `/editar <id_venta> @monto 1600 @cliente NuevoCliente`\n\nEjemplo: `/editar 123 @monto 2000`'
|
|
}), 200
|
|
|
|
# Extraer campos a actualizar
|
|
campos = {}
|
|
nuevo_monto = extraer_monto(texto)
|
|
nuevo_cliente = extraer_cliente(texto)
|
|
|
|
if nuevo_monto:
|
|
campos['monto'] = nuevo_monto
|
|
if nuevo_cliente:
|
|
campos['cliente'] = nuevo_cliente
|
|
|
|
if not campos:
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': '❌ **No se especificaron cambios**\n\nUsa `@monto` y/o `@cliente` para especificar los cambios.'
|
|
}), 200
|
|
|
|
# Editar venta
|
|
resultado = nocodb.editar_venta(venta_id, user_name, campos)
|
|
|
|
if resultado.get('success'):
|
|
cambios_texto = '\n'.join([
|
|
f"• **{c['campo']}:** {c['anterior']} → {c['nuevo']}"
|
|
for c in resultado.get('cambios', [])
|
|
])
|
|
mensaje = (
|
|
f"✏️ **Venta #{venta_id} editada**\n\n"
|
|
f"**Cambios realizados:**\n{cambios_texto}\n\n"
|
|
"_Las metas han sido recalculadas._"
|
|
)
|
|
else:
|
|
mensaje = f"❌ **Error:** {resultado.get('error')}"
|
|
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /editar: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/comando/comisiones', methods=['POST'])
|
|
def comando_comisiones():
|
|
"""
|
|
Endpoint para el comando slash /comisiones en Mattermost
|
|
Muestra historial de comisiones de los últimos meses
|
|
"""
|
|
try:
|
|
from utils import validar_tokens_comando
|
|
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /comisiones recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
if not validar_tokens_comando(token, 'comisiones'):
|
|
return jsonify({'text': '❌ Token inválido'}), 403
|
|
|
|
user_name = data.get('user_name')
|
|
|
|
# Obtener historial
|
|
historial = nocodb.get_historial_comisiones(user_name, meses=6)
|
|
|
|
if not historial:
|
|
mensaje = f"📊 **Historial de Comisiones - @{user_name}**\n\nNo hay datos de comisiones."
|
|
else:
|
|
mensaje = f"📊 **Historial de Comisiones - @{user_name}**\n\n"
|
|
mensaje += "| Mes | Tubos | Comisión | Ventas |\n"
|
|
mensaje += "|-----|-------|----------|--------|\n"
|
|
|
|
total_comision = 0
|
|
for h in historial:
|
|
mensaje += f"| {h['mes']} | {h['tubos_totales']} | ${h['comision_total']:,.2f} | {h['cantidad_ventas']} |\n"
|
|
total_comision += h['comision_total']
|
|
|
|
mensaje += f"\n**Total acumulado:** ${total_comision:,.2f} 💰"
|
|
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /comisiones: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/comando/racha', methods=['POST'])
|
|
def comando_racha():
|
|
"""
|
|
Endpoint para el comando slash /racha en Mattermost
|
|
Muestra la racha actual del vendedor
|
|
"""
|
|
try:
|
|
from utils import validar_tokens_comando
|
|
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /racha recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
if not validar_tokens_comando(token, 'racha'):
|
|
return jsonify({'text': '❌ Token inválido'}), 403
|
|
|
|
user_name = data.get('user_name')
|
|
|
|
# Obtener racha
|
|
racha = nocodb.verificar_racha(user_name)
|
|
|
|
dias = racha.get('dias_consecutivos', 0)
|
|
bonus = racha.get('bonus', 0)
|
|
proximo = racha.get('proximo_bonus', {})
|
|
|
|
# Determinar emoji según racha
|
|
if dias >= 10:
|
|
emoji = '🔥🔥🔥'
|
|
nivel = 'LEGENDARIO'
|
|
elif dias >= 5:
|
|
emoji = '🔥🔥'
|
|
nivel = 'EN FUEGO'
|
|
elif dias >= 3:
|
|
emoji = '🔥'
|
|
nivel = 'RACHA ACTIVA'
|
|
else:
|
|
emoji = '💪'
|
|
nivel = 'CONSTRUYENDO'
|
|
|
|
mensaje = f"{emoji} **Racha de @{user_name}** {emoji}\n\n"
|
|
mensaje += f"**Nivel:** {nivel}\n"
|
|
mensaje += f"**Días consecutivos:** {dias}\n"
|
|
mensaje += f"**Meta diaria:** {racha.get('meta_diaria', 3)} tubos\n\n"
|
|
|
|
if bonus > 0:
|
|
mensaje += f"**💰 Bonus ganado:** ${bonus:,.2f}\n\n"
|
|
|
|
if proximo.get('dias_faltan', 0) > 0:
|
|
mensaje += f"**Próximo bonus:** ${proximo['bonus']:,.2f} en {proximo['dias_faltan']} día(s)\n"
|
|
elif proximo.get('mensaje'):
|
|
mensaje += f"**{proximo['mensaje']}**\n"
|
|
|
|
mensaje += "\n_¡Sigue cumpliendo tu meta diaria para mantener la racha!_"
|
|
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /racha: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/comando/exportar', methods=['POST'])
|
|
def comando_exportar():
|
|
"""
|
|
Endpoint para el comando slash /exportar en Mattermost
|
|
Exporta ventas a Excel o CSV
|
|
"""
|
|
try:
|
|
from utils import validar_tokens_comando, parsear_formato_exportar
|
|
from export_utils import generar_excel_ventas, generar_csv_ventas
|
|
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /exportar recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
if not validar_tokens_comando(token, 'exportar'):
|
|
return jsonify({'text': '❌ Token inválido'}), 403
|
|
|
|
user_name = data.get('user_name')
|
|
texto = data.get('text', '').strip()
|
|
channel_id = data.get('channel_id')
|
|
|
|
# Parsear parámetros
|
|
formato, mes = parsear_formato_exportar(texto)
|
|
if not mes:
|
|
mes = datetime.now().strftime('%Y-%m')
|
|
|
|
# Obtener datos
|
|
ventas = nocodb.get_ventas_mes(user_name, mes)
|
|
stats = nocodb.get_estadisticas_vendedor_mes(user_name, mes)
|
|
|
|
if not ventas:
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': f'📊 No hay ventas para {user_name} en {mes}'
|
|
}), 200
|
|
|
|
# Generar archivo
|
|
if formato == 'excel':
|
|
contenido = generar_excel_ventas(ventas, user_name, stats)
|
|
filename = f'ventas_{user_name}_{mes}.xlsx'
|
|
else:
|
|
contenido = generar_csv_ventas(ventas)
|
|
filename = f'ventas_{user_name}_{mes}.csv'
|
|
|
|
# Subir a Mattermost
|
|
file_response = mattermost.upload_file(channel_id, contenido, filename)
|
|
|
|
if file_response:
|
|
mensaje = f"📊 **Exportación completada**\n\nArchivo: `{filename}`\n\n_El archivo ha sido subido al canal._"
|
|
else:
|
|
mensaje = f"❌ Error al subir el archivo. Intenta de nuevo."
|
|
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except ImportError:
|
|
return jsonify({
|
|
'response_type': 'ephemeral',
|
|
'text': '❌ Módulo de exportación no disponible. Contacta al administrador.'
|
|
}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /exportar: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/reporte/diario', methods=['POST'])
|
|
def reporte_diario_manual():
|
|
"""
|
|
Endpoint para generar reporte diario manualmente
|
|
"""
|
|
try:
|
|
resultado = generar_reporte_diario(mattermost, nocodb)
|
|
return jsonify(resultado), 200
|
|
except Exception as e:
|
|
logger.error(f"Error generando reporte: {str(e)}", exc_info=True)
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/test/mattermost', methods=['GET'])
|
|
def test_mattermost():
|
|
"""Test de conexión con Mattermost"""
|
|
try:
|
|
resultado = mattermost.test_connection()
|
|
return jsonify(resultado), 200
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/test/nocodb', methods=['GET'])
|
|
def test_nocodb():
|
|
"""Test de conexión con NocoDB"""
|
|
try:
|
|
resultado = nocodb.test_connection()
|
|
return jsonify(resultado), 200
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
# ============== DASHBOARD ==============
|
|
|
|
@app.route('/api/dashboard/resumen', methods=['GET'])
|
|
def api_dashboard_resumen():
|
|
"""API: Resumen general del día y mes"""
|
|
try:
|
|
from datetime import datetime, timedelta, timezone
|
|
TZ_MEXICO = timezone(timedelta(hours=-6))
|
|
|
|
hoy = datetime.now(TZ_MEXICO).strftime('%Y-%m-%d')
|
|
mes = datetime.now(TZ_MEXICO).strftime('%Y-%m')
|
|
|
|
# Ventas del día
|
|
ventas_hoy = nocodb.get_ventas_dia()
|
|
total_hoy = sum(float(v.get('monto', 0)) for v in ventas_hoy)
|
|
|
|
# Ventas del mes
|
|
ventas_mes = nocodb.get_ventas_mes()
|
|
total_mes = sum(float(v.get('monto', 0)) for v in ventas_mes)
|
|
|
|
# Contar vendedores activos hoy
|
|
vendedores_hoy = set(v.get('vendedor_username') for v in ventas_hoy)
|
|
|
|
return jsonify({
|
|
'fecha': hoy,
|
|
'mes': mes,
|
|
'ventas_hoy': len(ventas_hoy),
|
|
'monto_hoy': total_hoy,
|
|
'ventas_mes': len(ventas_mes),
|
|
'monto_mes': total_mes,
|
|
'vendedores_activos_hoy': len(vendedores_hoy)
|
|
}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error en API resumen: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/dashboard/ranking', methods=['GET'])
|
|
def api_dashboard_ranking():
|
|
"""API: Ranking de vendedores del mes"""
|
|
try:
|
|
ranking = nocodb.get_ranking_vendedores()
|
|
return jsonify(ranking), 200
|
|
except Exception as e:
|
|
logger.error(f"Error en API ranking: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/dashboard/ventas-recientes', methods=['GET'])
|
|
def api_dashboard_ventas_recientes():
|
|
"""API: Últimas ventas registradas con nombres de vendedores"""
|
|
try:
|
|
import requests
|
|
|
|
ventas = nocodb.get_ventas_dia()
|
|
|
|
# Obtener lista de vendedores para mapear usernames a nombres
|
|
vendedores_response = requests.get(
|
|
f"{nocodb.url}/api/v2/tables/{nocodb.table_vendedores}/records",
|
|
headers=nocodb.headers,
|
|
params={'limit': 100},
|
|
timeout=10
|
|
)
|
|
vendedores_response.raise_for_status()
|
|
vendedores = vendedores_response.json().get('list', [])
|
|
|
|
# Crear mapa de username -> nombre_completo
|
|
nombres_map = {v.get('username'): v.get('nombre_completo', v.get('username')) for v in vendedores}
|
|
|
|
# Agregar nombre_completo a cada venta
|
|
for venta in ventas:
|
|
username = venta.get('vendedor_username', '')
|
|
venta['nombre_completo'] = nombres_map.get(username, username)
|
|
|
|
# Ordenar por fecha descendente y tomar las últimas 20
|
|
ventas_ordenadas = sorted(ventas, key=lambda x: x.get('fecha_venta', ''), reverse=True)[:20]
|
|
return jsonify(ventas_ordenadas), 200
|
|
except Exception as e:
|
|
logger.error(f"Error en API ventas recientes: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/api/dashboard/metas', methods=['GET'])
|
|
def api_dashboard_metas():
|
|
"""API: Estado de metas de todos los vendedores"""
|
|
try:
|
|
import requests
|
|
response = requests.get(
|
|
f"{nocodb.url}/api/v2/tables/{nocodb.table_metas}/records",
|
|
headers=nocodb.headers,
|
|
params={'limit': 100},
|
|
timeout=10
|
|
)
|
|
response.raise_for_status()
|
|
metas = response.json().get('list', [])
|
|
return jsonify(metas), 200
|
|
except Exception as e:
|
|
logger.error(f"Error en API metas: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/dashboard')
|
|
def dashboard():
|
|
"""Dashboard principal de ventas"""
|
|
try:
|
|
return render_template('dashboard.html')
|
|
except Exception as e:
|
|
logger.error(f"Error renderizando dashboard: {str(e)}")
|
|
# Fallback al HTML embebido si no hay template
|
|
return render_template_string('''
|
|
<!DOCTYPE html>
|
|
<html><head><title>Sales Bot Dashboard</title></head>
|
|
<body style="background:#1a1a2e;color:#fff;font-family:sans-serif;padding:40px;text-align:center;">
|
|
<h1>Sales Bot Dashboard</h1>
|
|
<p>Error cargando templates. Verifica que la carpeta templates/ existe.</p>
|
|
<p>Error: ''' + str(e) + '''</p>
|
|
</body></html>
|
|
''')
|
|
|
|
|
|
@app.route('/dashboard/analytics')
|
|
def dashboard_analytics():
|
|
"""Dashboard de analytics con gráficas"""
|
|
try:
|
|
return render_template('analytics.html')
|
|
except Exception as e:
|
|
logger.error(f"Error renderizando analytics: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@app.route('/dashboard/executive')
|
|
def dashboard_executive():
|
|
"""Dashboard ejecutivo con KPIs"""
|
|
try:
|
|
return render_template('executive.html')
|
|
except Exception as e:
|
|
logger.error(f"Error renderizando executive: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ============== PWA ROUTES ==============
|
|
|
|
@app.route('/manifest.json')
|
|
def serve_manifest():
|
|
"""Servir manifest.json para PWA"""
|
|
return send_from_directory('static', 'manifest.json', mimetype='application/manifest+json')
|
|
|
|
|
|
@app.route('/service-worker.js')
|
|
def serve_service_worker():
|
|
"""Servir service worker para PWA"""
|
|
return send_from_directory('static', 'service-worker.js', mimetype='application/javascript')
|
|
|
|
|
|
# ============== ANALYTICS API ==============
|
|
|
|
@app.route('/api/analytics/trends', methods=['GET'])
|
|
def api_analytics_trends():
|
|
"""API: Tendencias de ventas"""
|
|
try:
|
|
from analytics.trends import TrendAnalyzer
|
|
|
|
dias = request.args.get('days', 30, type=int)
|
|
vendedor = request.args.get('vendedor', None)
|
|
|
|
analyzer = TrendAnalyzer(nocodb)
|
|
trends = analyzer.get_daily_trends(dias, vendedor)
|
|
|
|
return jsonify(trends), 200
|
|
except ImportError:
|
|
logger.warning("Módulo analytics.trends no disponible")
|
|
return jsonify({'error': 'Módulo de analytics no disponible', 'labels': [], 'ventas': []}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error en API trends: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@app.route('/api/analytics/predictions', methods=['GET'])
|
|
def api_analytics_predictions():
|
|
"""API: Predicciones de ventas"""
|
|
try:
|
|
from analytics.predictions import prediccion_basica
|
|
from analytics.trends import TrendAnalyzer
|
|
|
|
dias = request.args.get('days', 30, type=int)
|
|
dias_prediccion = request.args.get('predict', 7, type=int)
|
|
|
|
analyzer = TrendAnalyzer(nocodb)
|
|
trends = analyzer.get_daily_trends(dias)
|
|
|
|
ventas_diarias = trends.get('ventas', [])
|
|
prediccion = prediccion_basica(ventas_diarias, dias_prediccion)
|
|
|
|
return jsonify(prediccion), 200
|
|
except ImportError:
|
|
logger.warning("Módulo analytics.predictions no disponible")
|
|
return jsonify({
|
|
'error': 'Módulo de predicciones no disponible',
|
|
'next_day': 0,
|
|
'next_week': 0,
|
|
'tendencia': 'stable'
|
|
}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error en API predictions: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@app.route('/api/analytics/comparisons', methods=['GET'])
|
|
def api_analytics_comparisons():
|
|
"""API: Comparativas de períodos"""
|
|
try:
|
|
from analytics.comparisons import ComparisonAnalyzer
|
|
|
|
tipo = request.args.get('type', 'weekly')
|
|
|
|
analyzer = ComparisonAnalyzer(nocodb)
|
|
comparison = analyzer.get_comparison_summary(tipo)
|
|
|
|
return jsonify(comparison), 200
|
|
except ImportError:
|
|
logger.warning("Módulo analytics.comparisons no disponible")
|
|
return jsonify({'error': 'Módulo de comparaciones no disponible'}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error en API comparisons: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@app.route('/api/analytics/performance/<username>', methods=['GET'])
|
|
def api_analytics_performance(username):
|
|
"""API: Rendimiento de un vendedor específico"""
|
|
try:
|
|
# Obtener datos del vendedor
|
|
meta = nocodb.get_meta_vendedor(username)
|
|
racha = nocodb.verificar_racha(username)
|
|
ranking = nocodb.get_ranking_vendedores()
|
|
|
|
# Encontrar posición en ranking
|
|
posicion = 0
|
|
for i, v in enumerate(ranking, 1):
|
|
if v.get('vendedor_username') == username:
|
|
posicion = i
|
|
break
|
|
|
|
return jsonify({
|
|
'username': username,
|
|
'tubos_totales': meta.get('tubos_totales', 0) if meta else 0,
|
|
'total_vendido': meta.get('total_vendido', 0) if meta else 0,
|
|
'comision': meta.get('comision_total', 0) if meta else 0,
|
|
'ventas_realizadas': meta.get('ventas_realizadas', 0) if meta else 0,
|
|
'racha': racha.get('dias_consecutivos', 0),
|
|
'ranking': posicion,
|
|
'porcentaje_meta': meta.get('porcentaje_completado', 0) if meta else 0
|
|
}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error en API performance: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ============== REPORTS API ==============
|
|
|
|
@app.route('/comando/reporte', methods=['POST'])
|
|
def comando_reporte():
|
|
"""
|
|
Endpoint para el comando slash /reporte en Mattermost
|
|
Uso: /reporte [diario|semanal|ejecutivo]
|
|
"""
|
|
try:
|
|
from utils import validar_tokens_comando
|
|
|
|
data = request.form.to_dict()
|
|
logger.info(f"Comando /reporte recibido de {data.get('user_name')}")
|
|
|
|
# Validar token
|
|
token = data.get('token')
|
|
if not validar_tokens_comando(token, 'reporte'):
|
|
return jsonify({'text': 'Token inválido'}), 403
|
|
|
|
user_name = data.get('user_name')
|
|
channel_id = data.get('channel_id')
|
|
texto = data.get('text', '').strip().lower()
|
|
|
|
# Determinar tipo de reporte
|
|
if 'ejecutivo' in texto or 'executive' in texto:
|
|
tipo = 'ejecutivo'
|
|
elif 'semanal' in texto or 'weekly' in texto:
|
|
tipo = 'semanal'
|
|
else:
|
|
tipo = 'diario'
|
|
|
|
# Generar reporte
|
|
try:
|
|
from reports.pdf_generator import SalesReportPDF, generar_reporte_diario, generar_reporte_ejecutivo
|
|
|
|
ventas = nocodb.get_ventas_dia() if tipo == 'diario' else nocodb.get_ventas_mes()
|
|
ranking = nocodb.get_ranking_vendedores()
|
|
|
|
# Calcular estadísticas
|
|
stats = {
|
|
'monto_total': sum(float(v.get('monto', 0) or 0) for v in ventas),
|
|
'cantidad_ventas': len(ventas),
|
|
'tubos_totales': sum(int(v.get('tubos', 0) or 0) for v in ventas),
|
|
'comision_total': sum(float(v.get('comision', 0) or 0) for v in ventas)
|
|
}
|
|
|
|
if tipo == 'ejecutivo':
|
|
pdf_content = generar_reporte_ejecutivo(ventas, ranking, stats)
|
|
filename = f"reporte_ejecutivo_{datetime.now(TZ_MEXICO).strftime('%Y%m%d')}.pdf"
|
|
else:
|
|
pdf_content = generar_reporte_diario(ventas, ranking, stats)
|
|
filename = f"reporte_{tipo}_{datetime.now(TZ_MEXICO).strftime('%Y%m%d')}.pdf"
|
|
|
|
# Subir PDF a Mattermost
|
|
file_response = mattermost.upload_file(channel_id, pdf_content, filename)
|
|
|
|
if file_response:
|
|
mensaje = f"📊 **Reporte {tipo.capitalize()} generado**\n\nArchivo: `{filename}`"
|
|
else:
|
|
mensaje = "❌ Error al subir el reporte. Intenta de nuevo."
|
|
|
|
except ImportError as ie:
|
|
logger.warning(f"Módulo de reportes no disponible: {ie}")
|
|
mensaje = (
|
|
f"📊 **Reporte {tipo.capitalize()}** (texto)\n\n"
|
|
f"Instala `reportlab` para generar PDFs.\n\n"
|
|
f"**Resumen:**\n"
|
|
f"• Ventas: {len(ventas)}\n"
|
|
f"• Monto: ${stats['monto_total']:,.2f}\n"
|
|
)
|
|
|
|
return jsonify({
|
|
'response_type': 'in_channel',
|
|
'text': mensaje
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando comando /reporte: {str(e)}", exc_info=True)
|
|
return jsonify({
|
|
'text': f'❌ Error procesando comando: {str(e)}'
|
|
}), 500
|
|
|
|
|
|
@app.route('/api/reports/generate', methods=['POST'])
|
|
def api_reports_generate():
|
|
"""API: Generar reporte PDF"""
|
|
try:
|
|
from reports.pdf_generator import generar_reporte_diario, generar_reporte_ejecutivo
|
|
|
|
data = request.json or {}
|
|
tipo = data.get('type', 'daily')
|
|
vendedor = data.get('vendedor', None)
|
|
|
|
# Obtener datos
|
|
if tipo == 'daily':
|
|
ventas = nocodb.get_ventas_dia()
|
|
else:
|
|
ventas = nocodb.get_ventas_mes()
|
|
|
|
if vendedor:
|
|
ventas = [v for v in ventas if v.get('vendedor_username') == vendedor]
|
|
|
|
ranking = nocodb.get_ranking_vendedores()
|
|
|
|
stats = {
|
|
'monto_total': sum(float(v.get('monto', 0) or 0) for v in ventas),
|
|
'cantidad_ventas': len(ventas),
|
|
'tubos_totales': sum(int(v.get('tubos', 0) or 0) for v in ventas),
|
|
'comision_total': sum(float(v.get('comision', 0) or 0) for v in ventas)
|
|
}
|
|
|
|
# Generar PDF
|
|
if tipo == 'executive':
|
|
pdf_content = generar_reporte_ejecutivo(ventas, ranking, stats)
|
|
else:
|
|
pdf_content = generar_reporte_diario(ventas, ranking, stats)
|
|
|
|
# Guardar temporalmente y devolver ID
|
|
import hashlib
|
|
report_id = hashlib.md5(f"{tipo}_{datetime.now().isoformat()}".encode()).hexdigest()[:12]
|
|
|
|
# Guardar en directorio temporal
|
|
reports_dir = os.getenv('REPORTS_OUTPUT_DIR', '/tmp/salesbot_reports')
|
|
os.makedirs(reports_dir, exist_ok=True)
|
|
report_path = os.path.join(reports_dir, f"{report_id}.pdf")
|
|
|
|
with open(report_path, 'wb') as f:
|
|
f.write(pdf_content)
|
|
|
|
return jsonify({
|
|
'report_id': report_id,
|
|
'status': 'generated',
|
|
'type': tipo,
|
|
'download_url': f'/api/reports/download/{report_id}'
|
|
}), 200
|
|
|
|
except ImportError:
|
|
return jsonify({'error': 'Módulo de reportes no disponible. Instala reportlab.'}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error generando reporte: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@app.route('/api/reports/download/<report_id>', methods=['GET'])
|
|
def api_reports_download(report_id):
|
|
"""API: Descargar reporte PDF"""
|
|
try:
|
|
from flask import send_file
|
|
|
|
reports_dir = os.getenv('REPORTS_OUTPUT_DIR', '/tmp/salesbot_reports')
|
|
report_path = os.path.join(reports_dir, f"{report_id}.pdf")
|
|
|
|
if not os.path.exists(report_path):
|
|
return jsonify({'error': 'Reporte no encontrado'}), 404
|
|
|
|
return send_file(
|
|
report_path,
|
|
mimetype='application/pdf',
|
|
as_attachment=True,
|
|
download_name=f"reporte_{report_id}.pdf"
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error descargando reporte: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ============== CAMERA/OCR API ==============
|
|
|
|
@app.route('/api/capture/ticket', methods=['POST'])
|
|
def api_capture_ticket():
|
|
"""API: Procesar imagen de ticket desde cámara (base64)"""
|
|
try:
|
|
data = request.json or {}
|
|
image_base64 = data.get('image')
|
|
user_name = data.get('user_name', 'anonymous')
|
|
|
|
if not image_base64:
|
|
return jsonify({'error': 'No se recibió imagen'}), 400
|
|
|
|
# Decodificar imagen base64
|
|
if ',' in image_base64:
|
|
image_base64 = image_base64.split(',')[1]
|
|
|
|
image_bytes = base64.b64decode(image_base64)
|
|
|
|
# Procesar con OCR
|
|
try:
|
|
from ocr.processor import procesar_ticket_imagen
|
|
resultado = procesar_ticket_imagen(image_bytes)
|
|
except ImportError:
|
|
# Fallback al procesador existente si el módulo OCR no existe
|
|
from handlers import procesar_imagen_ticket
|
|
resultado = procesar_imagen_ticket(image_bytes)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'monto_detectado': resultado.get('monto', 0),
|
|
'cliente_detectado': resultado.get('cliente', ''),
|
|
'texto_extraido': resultado.get('texto', ''),
|
|
'confianza': resultado.get('confianza', 0)
|
|
}), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error procesando imagen de ticket: {str(e)}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
if __name__ == '__main__':
|
|
port = int(os.getenv('FLASK_PORT', 5000))
|
|
host = os.getenv('FLASK_HOST', '0.0.0.0')
|
|
debug = os.getenv('DEBUG', 'False').lower() == 'true'
|
|
|
|
logger.info(f"Iniciando Sales Bot en {host}:{port}")
|
|
app.run(host=host, port=port, debug=debug)
|