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:
532
sales-bot/reports/pdf_generator.py
Normal file
532
sales-bot/reports/pdf_generator.py
Normal file
@@ -0,0 +1,532 @@
|
||||
"""
|
||||
PDF Report Generator for Sales Bot
|
||||
Uses ReportLab for PDF generation and Matplotlib for charts
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Try to import ReportLab
|
||||
try:
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import letter, A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import inch, cm
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
|
||||
Image, PageBreak, HRFlowable
|
||||
)
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||||
REPORTLAB_AVAILABLE = True
|
||||
except ImportError:
|
||||
REPORTLAB_AVAILABLE = False
|
||||
logger.warning("ReportLab not available, PDF generation disabled")
|
||||
|
||||
# Try to import Matplotlib for charts
|
||||
try:
|
||||
import matplotlib
|
||||
matplotlib.use('Agg') # Use non-interactive backend
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
MATPLOTLIB_AVAILABLE = True
|
||||
except ImportError:
|
||||
MATPLOTLIB_AVAILABLE = False
|
||||
logger.warning("Matplotlib not available, charts in PDF disabled")
|
||||
|
||||
# Mexico timezone
|
||||
TZ_MEXICO = timezone(timedelta(hours=-6))
|
||||
|
||||
|
||||
class SalesReportPDF:
|
||||
"""
|
||||
Generates PDF reports for sales data.
|
||||
"""
|
||||
|
||||
# Color scheme matching the dashboard
|
||||
COLORS = {
|
||||
'primary': colors.HexColor('#00d4ff'),
|
||||
'secondary': colors.HexColor('#00ff88'),
|
||||
'warning': colors.HexColor('#ffaa00'),
|
||||
'dark': colors.HexColor('#1a1a2e'),
|
||||
'text': colors.HexColor('#333333'),
|
||||
'muted': colors.HexColor('#666666'),
|
||||
}
|
||||
|
||||
def __init__(self, ventas: List[Dict], stats: Dict, vendedor: str = None):
|
||||
"""
|
||||
Initialize the PDF generator.
|
||||
|
||||
Args:
|
||||
ventas: List of sales data
|
||||
stats: Statistics dictionary
|
||||
vendedor: Optional vendor username (None for all)
|
||||
"""
|
||||
if not REPORTLAB_AVAILABLE:
|
||||
raise ImportError("ReportLab is required for PDF generation")
|
||||
|
||||
self.ventas = ventas or []
|
||||
self.stats = stats or {}
|
||||
self.vendedor = vendedor
|
||||
self.styles = getSampleStyleSheet()
|
||||
self._setup_custom_styles()
|
||||
|
||||
def _setup_custom_styles(self):
|
||||
"""Setup custom paragraph styles."""
|
||||
# Title style
|
||||
self.styles.add(ParagraphStyle(
|
||||
'CustomTitle',
|
||||
parent=self.styles['Heading1'],
|
||||
fontSize=24,
|
||||
textColor=self.COLORS['dark'],
|
||||
spaceAfter=20,
|
||||
alignment=TA_CENTER
|
||||
))
|
||||
|
||||
# Subtitle style
|
||||
self.styles.add(ParagraphStyle(
|
||||
'CustomSubtitle',
|
||||
parent=self.styles['Normal'],
|
||||
fontSize=12,
|
||||
textColor=self.COLORS['muted'],
|
||||
spaceAfter=10,
|
||||
alignment=TA_CENTER
|
||||
))
|
||||
|
||||
# Section header
|
||||
self.styles.add(ParagraphStyle(
|
||||
'SectionHeader',
|
||||
parent=self.styles['Heading2'],
|
||||
fontSize=14,
|
||||
textColor=self.COLORS['primary'],
|
||||
spaceBefore=15,
|
||||
spaceAfter=10
|
||||
))
|
||||
|
||||
# KPI value style
|
||||
self.styles.add(ParagraphStyle(
|
||||
'KPIValue',
|
||||
parent=self.styles['Normal'],
|
||||
fontSize=18,
|
||||
textColor=self.COLORS['dark'],
|
||||
alignment=TA_CENTER
|
||||
))
|
||||
|
||||
# KPI label style
|
||||
self.styles.add(ParagraphStyle(
|
||||
'KPILabel',
|
||||
parent=self.styles['Normal'],
|
||||
fontSize=10,
|
||||
textColor=self.COLORS['muted'],
|
||||
alignment=TA_CENTER
|
||||
))
|
||||
|
||||
def generar_reporte_diario(self) -> bytes:
|
||||
"""
|
||||
Generates a daily sales report.
|
||||
|
||||
Returns:
|
||||
PDF content as bytes
|
||||
"""
|
||||
buffer = io.BytesIO()
|
||||
doc = SimpleDocTemplate(
|
||||
buffer,
|
||||
pagesize=letter,
|
||||
rightMargin=0.75*inch,
|
||||
leftMargin=0.75*inch,
|
||||
topMargin=0.75*inch,
|
||||
bottomMargin=0.75*inch
|
||||
)
|
||||
|
||||
elements = []
|
||||
fecha_hoy = datetime.now(TZ_MEXICO).strftime('%d de %B de %Y')
|
||||
|
||||
# Title
|
||||
elements.append(Paragraph(
|
||||
"Reporte Diario de Ventas",
|
||||
self.styles['CustomTitle']
|
||||
))
|
||||
elements.append(Paragraph(
|
||||
f"{fecha_hoy}",
|
||||
self.styles['CustomSubtitle']
|
||||
))
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# KPIs Section
|
||||
elements.append(self._create_kpi_section())
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Trend Chart (if matplotlib available)
|
||||
if MATPLOTLIB_AVAILABLE and self.ventas:
|
||||
chart_image = self._create_trend_chart()
|
||||
if chart_image:
|
||||
elements.append(Paragraph("Tendencia de Ventas (Últimos 7 días)", self.styles['SectionHeader']))
|
||||
elements.append(Image(chart_image, width=6*inch, height=3*inch))
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Top Sellers Section
|
||||
elements.append(Paragraph("Top Vendedores del Día", self.styles['SectionHeader']))
|
||||
elements.append(self._create_top_sellers_table())
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Sales Detail
|
||||
if len(self.ventas) <= 20: # Only include if not too many
|
||||
elements.append(Paragraph("Detalle de Ventas", self.styles['SectionHeader']))
|
||||
elements.append(self._create_sales_table())
|
||||
|
||||
# Footer
|
||||
elements.append(Spacer(1, 30))
|
||||
elements.append(HRFlowable(width="100%", thickness=1, color=self.COLORS['muted']))
|
||||
elements.append(Spacer(1, 10))
|
||||
elements.append(Paragraph(
|
||||
f"Generado por Sales Bot - {datetime.now(TZ_MEXICO).strftime('%Y-%m-%d %H:%M')}",
|
||||
self.styles['CustomSubtitle']
|
||||
))
|
||||
|
||||
doc.build(elements)
|
||||
return buffer.getvalue()
|
||||
|
||||
def generar_reporte_ejecutivo(self) -> bytes:
|
||||
"""
|
||||
Generates an executive summary report.
|
||||
|
||||
Returns:
|
||||
PDF content as bytes
|
||||
"""
|
||||
buffer = io.BytesIO()
|
||||
doc = SimpleDocTemplate(
|
||||
buffer,
|
||||
pagesize=letter,
|
||||
rightMargin=0.75*inch,
|
||||
leftMargin=0.75*inch,
|
||||
topMargin=0.75*inch,
|
||||
bottomMargin=0.75*inch
|
||||
)
|
||||
|
||||
elements = []
|
||||
fecha_hoy = datetime.now(TZ_MEXICO).strftime('%d de %B de %Y')
|
||||
mes_actual = datetime.now(TZ_MEXICO).strftime('%B %Y')
|
||||
|
||||
# Title
|
||||
elements.append(Paragraph(
|
||||
"Reporte Ejecutivo",
|
||||
self.styles['CustomTitle']
|
||||
))
|
||||
elements.append(Paragraph(
|
||||
f"{mes_actual}",
|
||||
self.styles['CustomSubtitle']
|
||||
))
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Executive KPIs
|
||||
elements.append(self._create_executive_kpi_section())
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Monthly Trend Chart
|
||||
if MATPLOTLIB_AVAILABLE and self.ventas:
|
||||
chart_image = self._create_monthly_chart()
|
||||
if chart_image:
|
||||
elements.append(Paragraph("Tendencia Mensual", self.styles['SectionHeader']))
|
||||
elements.append(Image(chart_image, width=6*inch, height=3*inch))
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Top Performers
|
||||
elements.append(Paragraph("Top Performers del Mes", self.styles['SectionHeader']))
|
||||
elements.append(self._create_top_performers_table())
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Comparison Section
|
||||
elements.append(Paragraph("Comparativa", self.styles['SectionHeader']))
|
||||
elements.append(self._create_comparison_section())
|
||||
|
||||
# Footer
|
||||
elements.append(Spacer(1, 30))
|
||||
elements.append(HRFlowable(width="100%", thickness=1, color=self.COLORS['muted']))
|
||||
elements.append(Spacer(1, 10))
|
||||
elements.append(Paragraph(
|
||||
f"Generado por Sales Bot - {datetime.now(TZ_MEXICO).strftime('%Y-%m-%d %H:%M')}",
|
||||
self.styles['CustomSubtitle']
|
||||
))
|
||||
|
||||
doc.build(elements)
|
||||
return buffer.getvalue()
|
||||
|
||||
def _create_kpi_section(self) -> Table:
|
||||
"""Creates KPI cards section."""
|
||||
total_ventas = len(self.ventas)
|
||||
monto_total = sum(float(v.get('monto', 0) or 0) for v in self.ventas)
|
||||
tubos_total = self.stats.get('tubos_totales', 0)
|
||||
comision_total = self.stats.get('comision_total', 0)
|
||||
|
||||
data = [
|
||||
[
|
||||
Paragraph(f"{total_ventas}", self.styles['KPIValue']),
|
||||
Paragraph(f"${monto_total:,.2f}", self.styles['KPIValue']),
|
||||
Paragraph(f"{tubos_total}", self.styles['KPIValue']),
|
||||
Paragraph(f"${comision_total:,.2f}", self.styles['KPIValue']),
|
||||
],
|
||||
[
|
||||
Paragraph("Ventas", self.styles['KPILabel']),
|
||||
Paragraph("Monto Total", self.styles['KPILabel']),
|
||||
Paragraph("Tubos", self.styles['KPILabel']),
|
||||
Paragraph("Comisiones", self.styles['KPILabel']),
|
||||
]
|
||||
]
|
||||
|
||||
table = Table(data, colWidths=[1.5*inch]*4)
|
||||
table.setStyle(TableStyle([
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#f8f9fa')),
|
||||
('BOX', (0, 0), (-1, -1), 1, self.COLORS['primary']),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e9ecef')),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 12),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
|
||||
]))
|
||||
|
||||
return table
|
||||
|
||||
def _create_executive_kpi_section(self) -> Table:
|
||||
"""Creates executive KPI section with more metrics."""
|
||||
total_ventas = len(self.ventas)
|
||||
monto_total = sum(float(v.get('monto', 0) or 0) for v in self.ventas)
|
||||
promedio_ticket = monto_total / total_ventas if total_ventas > 0 else 0
|
||||
vendedores_activos = len(set(v.get('vendedor_username') for v in self.ventas))
|
||||
|
||||
data = [
|
||||
[
|
||||
Paragraph(f"${monto_total:,.2f}", self.styles['KPIValue']),
|
||||
Paragraph(f"{total_ventas}", self.styles['KPIValue']),
|
||||
Paragraph(f"${promedio_ticket:,.2f}", self.styles['KPIValue']),
|
||||
Paragraph(f"{vendedores_activos}", self.styles['KPIValue']),
|
||||
],
|
||||
[
|
||||
Paragraph("Monto Total", self.styles['KPILabel']),
|
||||
Paragraph("Total Ventas", self.styles['KPILabel']),
|
||||
Paragraph("Ticket Promedio", self.styles['KPILabel']),
|
||||
Paragraph("Vendedores", self.styles['KPILabel']),
|
||||
]
|
||||
]
|
||||
|
||||
table = Table(data, colWidths=[1.5*inch]*4)
|
||||
table.setStyle(TableStyle([
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e3f2fd')),
|
||||
('BACKGROUND', (0, 1), (-1, 1), colors.HexColor('#f8f9fa')),
|
||||
('BOX', (0, 0), (-1, -1), 1, self.COLORS['primary']),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 15),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 15),
|
||||
]))
|
||||
|
||||
return table
|
||||
|
||||
def _create_top_sellers_table(self) -> Table:
|
||||
"""Creates top sellers table."""
|
||||
# Group sales by vendor
|
||||
vendors = {}
|
||||
for venta in self.ventas:
|
||||
username = venta.get('vendedor_username', 'Desconocido')
|
||||
if username not in vendors:
|
||||
vendors[username] = {'ventas': 0, 'monto': 0}
|
||||
vendors[username]['ventas'] += 1
|
||||
vendors[username]['monto'] += float(venta.get('monto', 0) or 0)
|
||||
|
||||
# Sort by sales count
|
||||
sorted_vendors = sorted(vendors.items(), key=lambda x: x[1]['monto'], reverse=True)[:5]
|
||||
|
||||
data = [['#', 'Vendedor', 'Ventas', 'Monto']]
|
||||
for i, (username, stats) in enumerate(sorted_vendors, 1):
|
||||
medal = ['🥇', '🥈', '🥉', '4.', '5.'][i-1]
|
||||
data.append([
|
||||
medal,
|
||||
username,
|
||||
str(stats['ventas']),
|
||||
f"${stats['monto']:,.2f}"
|
||||
])
|
||||
|
||||
if len(data) == 1:
|
||||
data.append(['', 'Sin datos', '', ''])
|
||||
|
||||
table = Table(data, colWidths=[0.5*inch, 2.5*inch, 1*inch, 1.5*inch])
|
||||
table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), self.COLORS['dark']),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 10),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#f8f9fa')),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#dee2e6')),
|
||||
('TOPPADDING', (0, 1), (-1, -1), 8),
|
||||
('BOTTOMPADDING', (0, 1), (-1, -1), 8),
|
||||
]))
|
||||
|
||||
return table
|
||||
|
||||
def _create_top_performers_table(self) -> Table:
|
||||
"""Creates top performers table for executive report."""
|
||||
# Similar to top sellers but with more metrics
|
||||
return self._create_top_sellers_table()
|
||||
|
||||
def _create_sales_table(self) -> Table:
|
||||
"""Creates detailed sales table."""
|
||||
data = [['ID', 'Fecha', 'Vendedor', 'Cliente', 'Monto']]
|
||||
|
||||
for venta in self.ventas[:15]: # Limit to 15 rows
|
||||
fecha_str = venta.get('fecha_venta', '')
|
||||
try:
|
||||
fecha = datetime.fromisoformat(fecha_str.replace('Z', '+00:00'))
|
||||
fecha_formatted = fecha.strftime('%d/%m %H:%M')
|
||||
except:
|
||||
fecha_formatted = fecha_str[:16] if fecha_str else ''
|
||||
|
||||
data.append([
|
||||
str(venta.get('Id', '')),
|
||||
fecha_formatted,
|
||||
venta.get('vendedor_username', '')[:15],
|
||||
(venta.get('cliente', '') or 'N/A')[:20],
|
||||
f"${float(venta.get('monto', 0) or 0):,.2f}"
|
||||
])
|
||||
|
||||
table = Table(data, colWidths=[0.5*inch, 1*inch, 1.5*inch, 1.5*inch, 1*inch])
|
||||
table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), self.COLORS['dark']),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 9),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8f9fa')]),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#dee2e6')),
|
||||
('TOPPADDING', (0, 1), (-1, -1), 6),
|
||||
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
|
||||
]))
|
||||
|
||||
return table
|
||||
|
||||
def _create_comparison_section(self) -> Table:
|
||||
"""Creates comparison section."""
|
||||
# Placeholder data - would be filled with real comparison data
|
||||
data = [
|
||||
['Métrica', 'Período Actual', 'Período Anterior', 'Cambio'],
|
||||
['Ventas', str(len(self.ventas)), '-', '-'],
|
||||
['Monto', f"${sum(float(v.get('monto', 0) or 0) for v in self.ventas):,.2f}", '-', '-'],
|
||||
['Vendedores', str(len(set(v.get('vendedor_username') for v in self.ventas))), '-', '-'],
|
||||
]
|
||||
|
||||
table = Table(data, colWidths=[1.5*inch, 1.25*inch, 1.25*inch, 1*inch])
|
||||
table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), self.COLORS['primary']),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 10),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#dee2e6')),
|
||||
('TOPPADDING', (0, 1), (-1, -1), 8),
|
||||
('BOTTOMPADDING', (0, 1), (-1, -1), 8),
|
||||
]))
|
||||
|
||||
return table
|
||||
|
||||
def _create_trend_chart(self) -> Optional[io.BytesIO]:
|
||||
"""Creates a trend chart image."""
|
||||
if not MATPLOTLIB_AVAILABLE:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Group sales by date
|
||||
sales_by_date = {}
|
||||
for venta in self.ventas:
|
||||
fecha_str = venta.get('fecha_venta', '')
|
||||
try:
|
||||
fecha = datetime.fromisoformat(fecha_str.replace('Z', '+00:00'))
|
||||
date_key = fecha.strftime('%Y-%m-%d')
|
||||
if date_key not in sales_by_date:
|
||||
sales_by_date[date_key] = 0
|
||||
sales_by_date[date_key] += float(venta.get('monto', 0) or 0)
|
||||
except:
|
||||
continue
|
||||
|
||||
if not sales_by_date:
|
||||
return None
|
||||
|
||||
# Sort by date
|
||||
sorted_dates = sorted(sales_by_date.keys())[-7:] # Last 7 days
|
||||
values = [sales_by_date.get(d, 0) for d in sorted_dates]
|
||||
labels = [datetime.strptime(d, '%Y-%m-%d').strftime('%d/%m') for d in sorted_dates]
|
||||
|
||||
# Create chart
|
||||
fig, ax = plt.subplots(figsize=(8, 4))
|
||||
ax.plot(labels, values, color='#00d4ff', linewidth=2, marker='o')
|
||||
ax.fill_between(labels, values, alpha=0.3, color='#00d4ff')
|
||||
ax.set_xlabel('Fecha')
|
||||
ax.set_ylabel('Monto ($)')
|
||||
ax.set_title('Tendencia de Ventas')
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# Format y-axis as currency
|
||||
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
# Save to buffer
|
||||
buf = io.BytesIO()
|
||||
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
||||
buf.seek(0)
|
||||
plt.close(fig)
|
||||
|
||||
return buf
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating trend chart: {str(e)}")
|
||||
return None
|
||||
|
||||
def _create_monthly_chart(self) -> Optional[io.BytesIO]:
|
||||
"""Creates a monthly trend chart."""
|
||||
return self._create_trend_chart() # Use same logic for now
|
||||
|
||||
|
||||
# Convenience functions
|
||||
def generar_reporte_diario(ventas: List[Dict], stats: Dict, vendedor: str = None) -> bytes:
|
||||
"""
|
||||
Genera un reporte diario en PDF.
|
||||
|
||||
Args:
|
||||
ventas: Lista de ventas
|
||||
stats: Estadísticas
|
||||
vendedor: Username del vendedor (opcional)
|
||||
|
||||
Returns:
|
||||
Contenido del PDF en bytes
|
||||
"""
|
||||
if not REPORTLAB_AVAILABLE:
|
||||
raise ImportError("ReportLab is required for PDF generation. Install with: pip install reportlab")
|
||||
|
||||
report = SalesReportPDF(ventas, stats, vendedor)
|
||||
return report.generar_reporte_diario()
|
||||
|
||||
|
||||
def generar_reporte_ejecutivo(ventas: List[Dict], stats: Dict) -> bytes:
|
||||
"""
|
||||
Genera un reporte ejecutivo en PDF.
|
||||
|
||||
Args:
|
||||
ventas: Lista de ventas
|
||||
stats: Estadísticas
|
||||
|
||||
Returns:
|
||||
Contenido del PDF en bytes
|
||||
"""
|
||||
if not REPORTLAB_AVAILABLE:
|
||||
raise ImportError("ReportLab is required for PDF generation. Install with: pip install reportlab")
|
||||
|
||||
report = SalesReportPDF(ventas, stats)
|
||||
return report.generar_reporte_ejecutivo()
|
||||
Reference in New Issue
Block a user