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>
This commit is contained in:
2026-01-19 03:26:16 +00:00
parent ed1658eb2b
commit 9936deaa90
25 changed files with 5501 additions and 282 deletions

View File

@@ -0,0 +1,15 @@
"""
Analytics module for Sales Bot
Provides predictions, trends, and performance analysis
"""
from .predictions import prediccion_basica, calcular_tendencia
from .trends import TrendAnalyzer
from .comparisons import ComparisonAnalyzer
__all__ = [
'prediccion_basica',
'calcular_tendencia',
'TrendAnalyzer',
'ComparisonAnalyzer'
]

View File

@@ -0,0 +1,293 @@
"""
Comparison analysis for Sales Bot
Compares sales data across different time periods
"""
import logging
from datetime import datetime, timedelta, timezone
from typing import Dict, Optional, List
logger = logging.getLogger(__name__)
# Mexico timezone
TZ_MEXICO = timezone(timedelta(hours=-6))
class ComparisonAnalyzer:
"""
Analyzes and compares sales data across different periods.
"""
def __init__(self, nocodb_client):
self.nocodb = nocodb_client
def comparar_semanas(self) -> Dict:
"""
Compara la semana actual con la semana anterior.
Returns:
dict con comparación semanal
"""
try:
hoy = datetime.now(TZ_MEXICO).date()
# Semana actual (lunes a hoy)
dias_desde_lunes = hoy.weekday()
inicio_semana_actual = hoy - timedelta(days=dias_desde_lunes)
# Semana anterior
inicio_semana_anterior = inicio_semana_actual - timedelta(days=7)
fin_semana_anterior = inicio_semana_actual - timedelta(days=1)
# Obtener ventas
ventas_actual = self._obtener_ventas_rango(inicio_semana_actual, hoy)
ventas_anterior = self._obtener_ventas_rango(inicio_semana_anterior, fin_semana_anterior)
monto_actual = sum(float(v.get('monto', 0) or 0) for v in ventas_actual)
monto_anterior = sum(float(v.get('monto', 0) or 0) for v in ventas_anterior)
# Calcular diferencia
if monto_anterior > 0:
diff_pct = ((monto_actual - monto_anterior) / monto_anterior) * 100
else:
diff_pct = 100 if monto_actual > 0 else 0
return {
'current_week': round(monto_actual, 2),
'previous_week': round(monto_anterior, 2),
'difference': round(monto_actual - monto_anterior, 2),
'diff_percent': round(diff_pct, 1),
'ventas_actual': len(ventas_actual),
'ventas_anterior': len(ventas_anterior),
'periodo_actual': {
'inicio': inicio_semana_actual.strftime('%Y-%m-%d'),
'fin': hoy.strftime('%Y-%m-%d')
},
'periodo_anterior': {
'inicio': inicio_semana_anterior.strftime('%Y-%m-%d'),
'fin': fin_semana_anterior.strftime('%Y-%m-%d')
}
}
except Exception as e:
logger.error(f"Error comparando semanas: {str(e)}")
return {
'current_week': 0,
'previous_week': 0,
'difference': 0,
'diff_percent': 0,
'error': str(e)
}
def comparar_meses(self, mes1: str = None, mes2: str = None) -> Dict:
"""
Compara dos meses.
Args:
mes1: Primer mes (formato YYYY-MM), default mes actual
mes2: Segundo mes (formato YYYY-MM), default mes anterior
Returns:
dict con comparación mensual
"""
try:
hoy = datetime.now(TZ_MEXICO).date()
# Determinar meses a comparar
if not mes1:
mes1 = hoy.strftime('%Y-%m')
if not mes2:
# Mes anterior
primer_dia_mes_actual = hoy.replace(day=1)
ultimo_dia_mes_anterior = primer_dia_mes_actual - timedelta(days=1)
mes2 = ultimo_dia_mes_anterior.strftime('%Y-%m')
# Obtener ventas de cada mes
ventas_mes1 = self._obtener_ventas_mes(mes1)
ventas_mes2 = self._obtener_ventas_mes(mes2)
monto_mes1 = sum(float(v.get('monto', 0) or 0) for v in ventas_mes1)
monto_mes2 = sum(float(v.get('monto', 0) or 0) for v in ventas_mes2)
# Calcular diferencia
if monto_mes2 > 0:
diff_pct = ((monto_mes1 - monto_mes2) / monto_mes2) * 100
else:
diff_pct = 100 if monto_mes1 > 0 else 0
# Calcular mes anterior al mes2 para tendencia de 3 meses
anio2, mes2_num = map(int, mes2.split('-'))
if mes2_num == 1:
mes3 = f"{anio2 - 1}-12"
else:
mes3 = f"{anio2}-{mes2_num - 1:02d}"
ventas_mes3 = self._obtener_ventas_mes(mes3)
monto_mes3 = sum(float(v.get('monto', 0) or 0) for v in ventas_mes3)
return {
'current_month': round(monto_mes1, 2),
'previous_month': round(monto_mes2, 2),
'month_2': round(monto_mes3, 2),
'difference': round(monto_mes1 - monto_mes2, 2),
'diff_percent': round(diff_pct, 1),
'ventas_actual': len(ventas_mes1),
'ventas_anterior': len(ventas_mes2),
'months': [mes3, mes2, mes1],
'values': [round(monto_mes3, 2), round(monto_mes2, 2), round(monto_mes1, 2)],
'mes1': mes1,
'mes2': mes2
}
except Exception as e:
logger.error(f"Error comparando meses: {str(e)}")
return {
'current_month': 0,
'previous_month': 0,
'difference': 0,
'diff_percent': 0,
'error': str(e)
}
def comparar_anios(self, anio1: int = None, anio2: int = None) -> Dict:
"""
Compara ventas anuales (hasta la fecha actual del año).
Args:
anio1: Primer año, default año actual
anio2: Segundo año, default año anterior
Returns:
dict con comparación anual
"""
try:
hoy = datetime.now(TZ_MEXICO).date()
if not anio1:
anio1 = hoy.year
if not anio2:
anio2 = anio1 - 1
# Obtener ventas YTD de cada año
dia_mes = hoy.strftime('%m-%d')
# Para comparación justa, comparamos hasta el mismo día del año
ventas_anio1 = self._obtener_ventas_ytd(anio1, dia_mes)
ventas_anio2 = self._obtener_ventas_ytd(anio2, dia_mes)
monto_anio1 = sum(float(v.get('monto', 0) or 0) for v in ventas_anio1)
monto_anio2 = sum(float(v.get('monto', 0) or 0) for v in ventas_anio2)
# Calcular diferencia
if monto_anio2 > 0:
diff_pct = ((monto_anio1 - monto_anio2) / monto_anio2) * 100
else:
diff_pct = 100 if monto_anio1 > 0 else 0
return {
'anio_actual': anio1,
'anio_anterior': anio2,
'monto_actual': round(monto_anio1, 2),
'monto_anterior': round(monto_anio2, 2),
'difference': round(monto_anio1 - monto_anio2, 2),
'diff_percent': round(diff_pct, 1),
'ventas_actual': len(ventas_anio1),
'ventas_anterior': len(ventas_anio2),
'comparacion_hasta': hoy.strftime('%d de %B')
}
except Exception as e:
logger.error(f"Error comparando años: {str(e)}")
return {
'monto_actual': 0,
'monto_anterior': 0,
'difference': 0,
'diff_percent': 0,
'error': str(e)
}
def _obtener_ventas_rango(self, inicio: datetime.date, fin: datetime.date) -> List[Dict]:
"""Obtiene ventas en un rango de fechas."""
try:
# Obtener todas las ventas recientes
ventas = self.nocodb.get_ventas_mes() if hasattr(self.nocodb, 'get_ventas_mes') else []
ventas_filtradas = []
for venta in ventas:
try:
fecha_str = venta.get('fecha_venta', '')
if 'T' in fecha_str:
fecha = datetime.fromisoformat(fecha_str.replace('Z', '+00:00'))
else:
fecha = datetime.strptime(fecha_str[:10], '%Y-%m-%d')
if fecha.tzinfo is None:
fecha = fecha.replace(tzinfo=timezone.utc)
fecha_local = fecha.astimezone(TZ_MEXICO).date()
if inicio <= fecha_local <= fin:
ventas_filtradas.append(venta)
except:
continue
return ventas_filtradas
except Exception as e:
logger.error(f"Error obteniendo ventas del rango: {str(e)}")
return []
def _obtener_ventas_mes(self, mes: str) -> List[Dict]:
"""Obtiene ventas de un mes específico."""
try:
# Usar método existente si está disponible
if hasattr(self.nocodb, 'get_ventas_mes'):
ventas = self.nocodb.get_ventas_mes()
else:
return []
# Filtrar por mes
ventas_filtradas = []
for venta in ventas:
fecha_str = venta.get('fecha_venta', '')
if fecha_str and fecha_str[:7] == mes:
ventas_filtradas.append(venta)
return ventas_filtradas
except Exception as e:
logger.error(f"Error obteniendo ventas del mes {mes}: {str(e)}")
return []
def _obtener_ventas_ytd(self, anio: int, hasta_dia_mes: str) -> List[Dict]:
"""Obtiene ventas del año hasta una fecha específica."""
try:
inicio = datetime(anio, 1, 1).date()
mes, dia = map(int, hasta_dia_mes.split('-'))
fin = datetime(anio, mes, dia).date()
return self._obtener_ventas_rango(inicio, fin)
except Exception as e:
logger.error(f"Error obteniendo ventas YTD {anio}: {str(e)}")
return []
def get_comparison_summary(self, tipo: str = 'weekly') -> Dict:
"""
Obtiene resumen de comparación según el tipo solicitado.
Args:
tipo: 'weekly', 'monthly', o 'yearly'
Returns:
dict con comparación
"""
if tipo == 'weekly':
return self.comparar_semanas()
elif tipo == 'monthly':
return self.comparar_meses()
elif tipo == 'yearly':
return self.comparar_anios()
else:
return {'error': f'Tipo de comparación no válido: {tipo}'}

View File

@@ -0,0 +1,241 @@
"""
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)
}
}

View File

@@ -0,0 +1,282 @@
"""
Trend analysis for Sales Bot
Aggregates and analyzes sales data over time
"""
import logging
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Optional
from collections import defaultdict
logger = logging.getLogger(__name__)
# Mexico timezone
TZ_MEXICO = timezone(timedelta(hours=-6))
class TrendAnalyzer:
"""
Analyzes sales trends over different time periods.
"""
def __init__(self, nocodb_client):
self.nocodb = nocodb_client
def get_daily_trends(self, dias: int = 30, vendedor: str = None) -> Dict:
"""
Obtiene tendencias diarias de ventas.
Args:
dias: Número de días a analizar
vendedor: Username del vendedor (opcional, None para todos)
Returns:
dict con labels, ventas, tubos, y predicciones
"""
try:
# Obtener ventas del período
ventas = self._obtener_ventas_periodo(dias, vendedor)
# Agrupar por día
ventas_por_dia = self._agrupar_por_dia(ventas, dias)
# Generar labels (fechas)
hoy = datetime.now(TZ_MEXICO).date()
labels = []
valores_ventas = []
valores_tubos = []
for i in range(dias - 1, -1, -1):
fecha = hoy - timedelta(days=i)
fecha_str = fecha.strftime('%Y-%m-%d')
labels.append(fecha.strftime('%d/%m'))
dia_data = ventas_por_dia.get(fecha_str, {'monto': 0, 'tubos': 0})
valores_ventas.append(dia_data['monto'])
valores_tubos.append(dia_data['tubos'])
# Generar predicciones si hay datos
prediccion = []
if valores_ventas:
from .predictions import prediccion_basica
pred_result = prediccion_basica(valores_ventas, 7)
if pred_result.get('predicciones'):
# Añadir None para los días históricos y luego las predicciones
prediccion = [None] * len(valores_ventas)
prediccion.extend(pred_result['predicciones'])
return {
'labels': labels,
'ventas': valores_ventas,
'tubos': valores_tubos,
'prediccion': prediccion,
'periodo': {
'inicio': (hoy - timedelta(days=dias-1)).strftime('%Y-%m-%d'),
'fin': hoy.strftime('%Y-%m-%d'),
'dias': dias
}
}
except Exception as e:
logger.error(f"Error obteniendo tendencias diarias: {str(e)}")
return {
'labels': [],
'ventas': [],
'tubos': [],
'prediccion': [],
'error': str(e)
}
def _obtener_ventas_periodo(self, dias: int, vendedor: str = None) -> List[Dict]:
"""Obtiene ventas de los últimos N días."""
try:
# Usar método existente del cliente NocoDB
if hasattr(self.nocodb, 'get_ventas_historico'):
return self.nocodb.get_ventas_historico(dias, vendedor)
# Fallback: obtener ventas del mes y filtrar
ventas = self.nocodb.get_ventas_mes(vendedor) if vendedor else self.nocodb.get_ventas_mes()
# Filtrar por días
hoy = datetime.now(TZ_MEXICO).date()
fecha_inicio = hoy - timedelta(days=dias)
ventas_filtradas = []
for venta in ventas:
fecha_str = venta.get('fecha_venta', '')
try:
if 'T' in fecha_str:
fecha = datetime.fromisoformat(fecha_str.replace('Z', '+00:00'))
else:
fecha = datetime.strptime(fecha_str[:10], '%Y-%m-%d')
if fecha.tzinfo is None:
fecha = fecha.replace(tzinfo=timezone.utc)
fecha_local = fecha.astimezone(TZ_MEXICO).date()
if fecha_local >= fecha_inicio:
ventas_filtradas.append(venta)
except:
continue
return ventas_filtradas
except Exception as e:
logger.error(f"Error obteniendo ventas del período: {str(e)}")
return []
def _agrupar_por_dia(self, ventas: List[Dict], dias: int) -> Dict[str, Dict]:
"""Agrupa ventas por día."""
ventas_por_dia = defaultdict(lambda: {'monto': 0, 'tubos': 0, 'count': 0})
for venta in ventas:
try:
fecha_str = venta.get('fecha_venta', '')
if 'T' in fecha_str:
fecha = datetime.fromisoformat(fecha_str.replace('Z', '+00:00'))
else:
fecha = datetime.strptime(fecha_str[:10], '%Y-%m-%d')
if fecha.tzinfo is None:
fecha = fecha.replace(tzinfo=timezone.utc)
fecha_local = fecha.astimezone(TZ_MEXICO).date()
fecha_key = fecha_local.strftime('%Y-%m-%d')
monto = float(venta.get('monto', 0) or 0)
ventas_por_dia[fecha_key]['monto'] += monto
ventas_por_dia[fecha_key]['count'] += 1
# Contar tubos si hay información de productos
tubos = self._contar_tubos_venta(venta)
ventas_por_dia[fecha_key]['tubos'] += tubos
except Exception as e:
continue
return dict(ventas_por_dia)
def _contar_tubos_venta(self, venta: Dict) -> int:
"""Cuenta tubos de tinte en una venta."""
# Esto depende de la estructura de datos
# Por ahora retorna un valor estimado basado en el monto
# En producción, debería consultar la tabla de detalles
tubos = venta.get('tubos', 0)
if tubos:
return int(tubos)
# Estimación: ~$50 por tubo en promedio
monto = float(venta.get('monto', 0) or 0)
return max(1, int(monto / 50)) if monto > 0 else 0
def get_weekly_summary(self, semanas: int = 4) -> Dict:
"""
Obtiene resumen semanal de ventas.
Args:
semanas: Número de semanas a analizar
Returns:
dict con datos semanales
"""
try:
dias_totales = semanas * 7
ventas = self._obtener_ventas_periodo(dias_totales)
# Agrupar por semana
hoy = datetime.now(TZ_MEXICO).date()
semanas_data = []
for i in range(semanas):
fin_semana = hoy - timedelta(days=i * 7)
inicio_semana = fin_semana - timedelta(days=6)
# Filtrar ventas de esta semana
ventas_semana = []
for venta in ventas:
try:
fecha_str = venta.get('fecha_venta', '')
if 'T' in fecha_str:
fecha = datetime.fromisoformat(fecha_str.replace('Z', '+00:00'))
else:
fecha = datetime.strptime(fecha_str[:10], '%Y-%m-%d')
if fecha.tzinfo is None:
fecha = fecha.replace(tzinfo=timezone.utc)
fecha_local = fecha.astimezone(TZ_MEXICO).date()
if inicio_semana <= fecha_local <= fin_semana:
ventas_semana.append(venta)
except:
continue
monto_total = sum(float(v.get('monto', 0) or 0) for v in ventas_semana)
semanas_data.append({
'semana': i + 1,
'inicio': inicio_semana.strftime('%Y-%m-%d'),
'fin': fin_semana.strftime('%Y-%m-%d'),
'label': f"{inicio_semana.strftime('%d/%m')} - {fin_semana.strftime('%d/%m')}",
'monto': round(monto_total, 2),
'ventas': len(ventas_semana)
})
# Invertir para orden cronológico
semanas_data.reverse()
return {
'semanas': semanas_data,
'total_semanas': semanas
}
except Exception as e:
logger.error(f"Error obteniendo resumen semanal: {str(e)}")
return {'semanas': [], 'error': str(e)}
def get_monthly_summary(self, meses: int = 6) -> Dict:
"""
Obtiene resumen mensual de ventas.
Args:
meses: Número de meses a analizar
Returns:
dict con datos mensuales
"""
try:
# Esto requeriría acceso a más datos históricos
# Por ahora retornamos estructura básica
hoy = datetime.now(TZ_MEXICO).date()
meses_data = []
for i in range(meses):
# Calcular mes
mes_actual = hoy.month - i
anio_actual = hoy.year
while mes_actual < 1:
mes_actual += 12
anio_actual -= 1
mes_str = f"{anio_actual}-{mes_actual:02d}"
mes_nombre = datetime(anio_actual, mes_actual, 1).strftime('%B %Y')
meses_data.append({
'mes': mes_str,
'nombre': mes_nombre,
'monto': 0, # Se llenaría con datos reales
'ventas': 0
})
meses_data.reverse()
return {
'meses': meses_data,
'total_meses': meses
}
except Exception as e:
logger.error(f"Error obteniendo resumen mensual: {str(e)}")
return {'meses': [], 'error': str(e)}