Files
consultoria-as 9936deaa90 feat: Implementar PWA, Analytics, Reportes PDF y mejoras OCR
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>
2026-01-19 03:26:16 +00:00

242 lines
7.2 KiB
Python

"""
Sales prediction algorithms for Sales Bot
Uses moving average and linear regression for basic predictions
"""
import logging
from typing import List, Dict, Optional
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
# Try to import scipy for linear regression
try:
from scipy import stats
import numpy as np
SCIPY_AVAILABLE = True
except ImportError:
SCIPY_AVAILABLE = False
logger.warning("scipy not available, using basic prediction methods")
def prediccion_basica(ventas_diarias: List[float], dias_prediccion: int = 7) -> Dict:
"""
Genera predicciones combinando promedio móvil con tendencia lineal.
Args:
ventas_diarias: Lista de montos de ventas por día
dias_prediccion: Número de días a predecir
Returns:
dict con predicciones, tendencia y confianza
"""
if not ventas_diarias or len(ventas_diarias) < 3:
return {
'predicciones': [],
'next_day': 0,
'next_week': 0,
'tendencia': 'stable',
'confidence': 0,
'error': 'Datos insuficientes para predicción'
}
try:
# Calcular promedio móvil (últimos 7 días o menos si no hay suficientes)
ventana = min(7, len(ventas_diarias))
promedio_movil = sum(ventas_diarias[-ventana:]) / ventana
# Calcular tendencia
if SCIPY_AVAILABLE:
# Usar regresión lineal con scipy
x = np.arange(len(ventas_diarias))
y = np.array(ventas_diarias)
slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)
confianza = abs(r_value)
else:
# Método básico sin scipy
slope, intercept, confianza = _calcular_regresion_simple(ventas_diarias)
# Generar predicciones
predicciones = []
for i in range(1, dias_prediccion + 1):
# Predicción lineal
pred_lineal = slope * (len(ventas_diarias) + i) + intercept
# Combinar promedio móvil con tendencia lineal
pred_combinada = (promedio_movil + pred_lineal) / 2
# No permitir predicciones negativas
predicciones.append(max(0, pred_combinada))
# Determinar tendencia
if slope > 0.5:
tendencia = 'increasing'
elif slope < -0.5:
tendencia = 'decreasing'
else:
tendencia = 'stable'
return {
'predicciones': predicciones,
'next_day': round(predicciones[0], 2) if predicciones else 0,
'next_week': round(sum(predicciones[:7]), 2) if len(predicciones) >= 7 else round(sum(predicciones), 2),
'tendencia': tendencia,
'trend': tendencia, # Alias for frontend
'confidence': round(confianza, 2),
'promedio_movil': round(promedio_movil, 2),
'slope': round(slope, 4)
}
except Exception as e:
logger.error(f"Error en predicción: {str(e)}")
return {
'predicciones': [],
'next_day': 0,
'next_week': 0,
'tendencia': 'stable',
'trend': 'stable',
'confidence': 0,
'error': str(e)
}
def _calcular_regresion_simple(datos: List[float]) -> tuple:
"""
Calcula regresión lineal simple sin scipy.
Returns:
(slope, intercept, r_value)
"""
n = len(datos)
if n < 2:
return 0, datos[0] if datos else 0, 0
x = list(range(n))
y = datos
# Calcular medias
x_mean = sum(x) / n
y_mean = sum(y) / n
# Calcular slope
numerador = sum((x[i] - x_mean) * (y[i] - y_mean) for i in range(n))
denominador = sum((x[i] - x_mean) ** 2 for i in range(n))
if denominador == 0:
return 0, y_mean, 0
slope = numerador / denominador
intercept = y_mean - slope * x_mean
# Calcular R-squared (coeficiente de determinación)
y_pred = [slope * xi + intercept for xi in x]
ss_res = sum((y[i] - y_pred[i]) ** 2 for i in range(n))
ss_tot = sum((y[i] - y_mean) ** 2 for i in range(n))
r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
r_value = r_squared ** 0.5 if r_squared >= 0 else 0
return slope, intercept, r_value
def calcular_tendencia(ventas_diarias: List[float]) -> Dict:
"""
Analiza la tendencia de los datos de ventas.
Returns:
dict con información de tendencia
"""
if not ventas_diarias or len(ventas_diarias) < 2:
return {
'direccion': 'stable',
'cambio_porcentual': 0,
'volatilidad': 0
}
# Dividir en dos mitades
mitad = len(ventas_diarias) // 2
primera_mitad = ventas_diarias[:mitad] if mitad > 0 else ventas_diarias[:1]
segunda_mitad = ventas_diarias[mitad:] if mitad > 0 else ventas_diarias[1:]
promedio_primera = sum(primera_mitad) / len(primera_mitad) if primera_mitad else 0
promedio_segunda = sum(segunda_mitad) / len(segunda_mitad) if segunda_mitad else 0
# Calcular cambio porcentual
if promedio_primera > 0:
cambio_pct = ((promedio_segunda - promedio_primera) / promedio_primera) * 100
else:
cambio_pct = 0
# Determinar dirección
if cambio_pct > 5:
direccion = 'increasing'
elif cambio_pct < -5:
direccion = 'decreasing'
else:
direccion = 'stable'
# Calcular volatilidad (desviación estándar relativa)
media = sum(ventas_diarias) / len(ventas_diarias)
if media > 0:
varianza = sum((x - media) ** 2 for x in ventas_diarias) / len(ventas_diarias)
desviacion = varianza ** 0.5
volatilidad = (desviacion / media) * 100
else:
volatilidad = 0
return {
'direccion': direccion,
'cambio_porcentual': round(cambio_pct, 2),
'volatilidad': round(volatilidad, 2),
'promedio_anterior': round(promedio_primera, 2),
'promedio_actual': round(promedio_segunda, 2)
}
def generar_prediccion_extendida(
ventas_diarias: List[float],
dias_historico: int = 30,
dias_futuro: int = 14
) -> Dict:
"""
Genera predicción extendida con más detalles.
Args:
ventas_diarias: Datos históricos
dias_historico: Días de historia a considerar
dias_futuro: Días a predecir
Returns:
dict con predicción completa
"""
# Limitar datos al histórico solicitado
datos = ventas_diarias[-dias_historico:] if len(ventas_diarias) > dias_historico else ventas_diarias
# Obtener predicción básica
prediccion = prediccion_basica(datos, dias_futuro)
# Agregar análisis de tendencia
tendencia_info = calcular_tendencia(datos)
# Calcular estadísticas adicionales
if datos:
maximo = max(datos)
minimo = min(datos)
promedio = sum(datos) / len(datos)
mejor_dia_idx = datos.index(maximo)
else:
maximo = minimo = promedio = 0
mejor_dia_idx = 0
return {
**prediccion,
'tendencia_detalle': tendencia_info,
'estadisticas': {
'maximo': round(maximo, 2),
'minimo': round(minimo, 2),
'promedio': round(promedio, 2),
'mejor_dia_indice': mejor_dia_idx,
'dias_analizados': len(datos)
}
}