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:
226
sales-bot/export_utils.py
Normal file
226
sales-bot/export_utils.py
Normal 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')
|
||||
Reference in New Issue
Block a user