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>
227 lines
7.8 KiB
Python
227 lines
7.8 KiB
Python
"""
|
|
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')
|