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:
2026-01-19 02:57:27 +00:00
parent 5d9cbd4812
commit ed1658eb2b
8 changed files with 1808 additions and 148 deletions

226
sales-bot/export_utils.py Normal file
View File

@@ -0,0 +1,226 @@
"""
Utilidades de exportación para Sales Bot
Genera archivos Excel y CSV con datos de ventas
"""
import csv
import io
from datetime import datetime
try:
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
OPENPYXL_DISPONIBLE = True
except ImportError:
OPENPYXL_DISPONIBLE = False
def generar_excel_ventas(ventas, vendedor, stats):
"""
Genera archivo Excel con múltiples hojas:
- Resumen: Estadísticas generales
- Ventas: Detalle de todas las ventas
- Comisiones: Desglose de comisiones
"""
if not OPENPYXL_DISPONIBLE:
raise ImportError("openpyxl no está instalado")
wb = Workbook()
# Estilos
header_font = Font(bold=True, color='FFFFFF')
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_alignment = Alignment(horizontal='center', vertical='center')
currency_format = '$#,##0.00'
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# ==================== HOJA 1: RESUMEN ====================
ws_resumen = wb.active
ws_resumen.title = "Resumen"
# Título
ws_resumen['A1'] = f"Reporte de Ventas - {vendedor}"
ws_resumen['A1'].font = Font(bold=True, size=16)
ws_resumen.merge_cells('A1:D1')
ws_resumen['A2'] = f"Generado: {datetime.now().strftime('%Y-%m-%d %H:%M')}"
ws_resumen['A2'].font = Font(italic=True, color='666666')
# Estadísticas
resumen_data = [
['Métrica', 'Valor'],
['Total de Ventas', len(ventas)],
['Monto Total', stats.get('monto_total', 0) if stats else 0],
['Tubos Vendidos', stats.get('tubos_totales', 0) if stats else 0],
['Comisión Total', stats.get('comision_total', 0) if stats else 0],
['Días Activos', stats.get('dias_activos', 0) if stats else 0],
['Días con Meta Cumplida', stats.get('dias_meta_cumplida', 0) if stats else 0],
['Promedio Tubos/Día', round(stats.get('promedio_tubos_dia', 0), 1) if stats else 0],
]
for row_idx, row_data in enumerate(resumen_data, start=4):
for col_idx, value in enumerate(row_data, start=1):
cell = ws_resumen.cell(row=row_idx, column=col_idx, value=value)
cell.border = thin_border
if row_idx == 4: # Header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
elif col_idx == 2 and row_idx in [6, 8]: # Montos
cell.number_format = currency_format
# Ajustar anchos
ws_resumen.column_dimensions['A'].width = 25
ws_resumen.column_dimensions['B'].width = 15
# ==================== HOJA 2: VENTAS ====================
ws_ventas = wb.create_sheet("Ventas")
headers_ventas = ['ID', 'Fecha', 'Cliente', 'Monto', 'Estado', 'Canal']
for col_idx, header in enumerate(headers_ventas, start=1):
cell = ws_ventas.cell(row=1, column=col_idx, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
for row_idx, venta in enumerate(ventas, start=2):
fecha = venta.get('fecha_venta', '')
if fecha:
try:
fecha = datetime.fromisoformat(fecha.replace('+00:00', '')).strftime('%Y-%m-%d %H:%M')
except:
pass
row_data = [
venta.get('Id', ''),
fecha,
venta.get('cliente', ''),
venta.get('monto', 0),
venta.get('estado', ''),
venta.get('canal', '')
]
for col_idx, value in enumerate(row_data, start=1):
cell = ws_ventas.cell(row=row_idx, column=col_idx, value=value)
cell.border = thin_border
if col_idx == 4: # Monto
cell.number_format = currency_format
# Ajustar anchos
column_widths = [8, 18, 25, 12, 12, 15]
for col_idx, width in enumerate(column_widths, start=1):
ws_ventas.column_dimensions[get_column_letter(col_idx)].width = width
# Agregar filtros
ws_ventas.auto_filter.ref = f"A1:F{len(ventas) + 1}"
# ==================== HOJA 3: COMISIONES ====================
ws_comisiones = wb.create_sheet("Comisiones")
headers_comisiones = ['Descripción', 'Cantidad', 'Valor']
for col_idx, header in enumerate(headers_comisiones, start=1):
cell = ws_comisiones.cell(row=1, column=col_idx, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
meta_diaria = 3
comision_por_tubo = 10
tubos_totales = stats.get('tubos_totales', 0) if stats else 0
tubos_comisionables = max(0, tubos_totales - (meta_diaria * stats.get('dias_activos', 0))) if stats else 0
comisiones_data = [
['Tubos vendidos', tubos_totales, ''],
['Meta diaria', meta_diaria, 'tubos'],
['Días activos', stats.get('dias_activos', 0) if stats else 0, 'días'],
['Tubos comisionables', tubos_comisionables, ''],
['Comisión por tubo', comision_por_tubo, ''],
['', '', ''],
['COMISIÓN TOTAL', stats.get('comision_total', 0) if stats else 0, ''],
]
for row_idx, row_data in enumerate(comisiones_data, start=2):
for col_idx, value in enumerate(row_data, start=1):
cell = ws_comisiones.cell(row=row_idx, column=col_idx, value=value)
cell.border = thin_border
if row_idx == 8: # Total
cell.font = Font(bold=True)
if col_idx == 2:
cell.number_format = currency_format
elif col_idx == 2 and row_idx == 6:
cell.number_format = currency_format
# Ajustar anchos
ws_comisiones.column_dimensions['A'].width = 20
ws_comisiones.column_dimensions['B'].width = 15
ws_comisiones.column_dimensions['C'].width = 10
# Guardar a bytes
output = io.BytesIO()
wb.save(output)
output.seek(0)
return output.getvalue()
def generar_csv_ventas(ventas):
"""
Genera archivo CSV con el detalle de ventas.
Formato simple compatible con cualquier aplicación.
"""
output = io.StringIO()
fieldnames = ['id', 'fecha', 'cliente', 'monto', 'estado', 'canal', 'vendedor']
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
for venta in ventas:
fecha = venta.get('fecha_venta', '')
if fecha:
try:
fecha = datetime.fromisoformat(fecha.replace('+00:00', '')).strftime('%Y-%m-%d %H:%M')
except:
pass
writer.writerow({
'id': venta.get('Id', ''),
'fecha': fecha,
'cliente': venta.get('cliente', ''),
'monto': venta.get('monto', 0),
'estado': venta.get('estado', ''),
'canal': venta.get('canal', ''),
'vendedor': venta.get('vendedor_username', '')
})
return output.getvalue().encode('utf-8')
def generar_reporte_comisiones_csv(historial):
"""
Genera CSV con historial de comisiones.
"""
output = io.StringIO()
fieldnames = ['mes', 'tubos', 'comision', 'ventas', 'dias_activos', 'monto_total']
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
for h in historial:
writer.writerow({
'mes': h.get('mes', ''),
'tubos': h.get('tubos_totales', 0),
'comision': h.get('comision_total', 0),
'ventas': h.get('cantidad_ventas', 0),
'dias_activos': h.get('dias_activos', 0),
'monto_total': h.get('monto_total', 0)
})
return output.getvalue().encode('utf-8')