uchill/backend/apps/analytics/exporters.py

418 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Экспортеры отчетов в PDF и Excel.
"""
import io
from datetime import datetime
from decimal import Decimal
import logging
logger = logging.getLogger(__name__)
class PDFExporter:
"""Экспорт отчетов в PDF."""
@staticmethod
def generate_report(mentor, start_date, end_date, report_type='overview'):
"""
Генерация PDF отчета.
Args:
mentor: Пользователь-ментор
start_date: Начало периода
end_date: Конец периода
report_type: Тип отчета ('overview', 'detailed', 'revenue')
Returns:
io.BytesIO: Буфер с PDF файлом
"""
try:
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4, letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from .services import AnalyticsService
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=18)
story = []
styles = getSampleStyleSheet()
# Заголовок
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Heading1'],
fontSize=24,
textColor=colors.HexColor('#1f2937'),
spaceAfter=30,
alignment=TA_CENTER
)
story.append(Paragraph('Отчет по аналитике', title_style))
story.append(Spacer(1, 0.2 * inch))
# Информация о менторе и периоде
info_style = ParagraphStyle(
'Info',
parent=styles['Normal'],
fontSize=10,
textColor=colors.HexColor('#6b7280'),
)
story.append(Paragraph(f'<b>Ментор:</b> {mentor.get_full_name() or mentor.email}', info_style))
story.append(Paragraph(f'<b>Период:</b> {start_date.strftime("%d.%m.%Y")} - {end_date.strftime("%d.%m.%Y")}', info_style))
story.append(Paragraph(f'<b>Дата формирования:</b> {datetime.now().strftime("%d.%m.%Y %H:%M")}', info_style))
story.append(Spacer(1, 0.3 * inch))
# Получаем данные
if report_type == 'overview':
from django.utils import timezone
from datetime import timedelta
# Вычисляем период
now = timezone.now()
if (end_date - start_date).days <= 1:
period = 'day'
elif (end_date - start_date).days <= 7:
period = 'week'
elif (end_date - start_date).days <= 31:
period = 'month'
else:
period = 'year'
# Используем сервис напрямую для получения данных
from .services import AnalyticsService
service = AnalyticsService()
overview_data = service.get_overview_data(mentor, start_date, end_date)
revenue_data = service.get_revenue_data(mentor, start_date, end_date)
# Общая статистика
story.append(Paragraph('<b>Общая статистика</b>', styles['Heading2']))
story.append(Spacer(1, 0.1 * inch))
overview_table_data = [
['Метрика', 'Значение'],
['Всего занятий', str(overview_data['lessons']['total'])],
['Завершено', str(overview_data['lessons']['completed'])],
['Отменено', str(overview_data['lessons']['cancelled'])],
['Активных учеников', str(overview_data['students']['active'])],
['Общий доход', f"{overview_data['revenue']['total']:.2f}"],
['Средний доход за занятие', f"{overview_data['revenue']['average_per_lesson']:.2f}"],
['Средний балл', f"{overview_data['grades']['average']}"],
]
overview_table = Table(overview_table_data, colWidths=[4 * inch, 2 * inch])
overview_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3b82f6')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.black),
]))
story.append(overview_table)
story.append(Spacer(1, 0.3 * inch))
# Доходы
story.append(Paragraph('<b>Доходы</b>', styles['Heading2']))
story.append(Spacer(1, 0.1 * inch))
revenue_table_data = [['Дата', 'Доход (₽)', 'Занятий']]
for item in revenue_data.get('by_day', [])[:30]: # Ограничиваем 30 записями
revenue_table_data.append([
item['date'],
f"{item['revenue']:.2f}",
str(item['lessons_count'])
])
if len(revenue_table_data) > 1:
revenue_table = Table(revenue_table_data, colWidths=[2 * inch, 2 * inch, 2 * inch])
revenue_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#10b981')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 11),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.lightgrey),
('GRID', (0, 0), (-1, -1), 1, colors.black),
]))
story.append(revenue_table)
elif report_type == 'detailed':
stats = AnalyticsService.get_detailed_lesson_stats(mentor, start_date, end_date)
story.append(Paragraph('<b>Детальная статистика</b>', styles['Heading2']))
story.append(Spacer(1, 0.1 * inch))
detailed_table_data = [
['Метрика', 'Значение'],
['Всего занятий', str(stats['summary']['total_lessons'])],
['Завершено', str(stats['summary']['completed'])],
['Процент завершения', f"{stats['summary']['completion_rate']}%"],
['Общее время (часы)', f"{stats['time']['total_duration_hours']}"],
['Средняя длительность (мин)', f"{stats['time']['average_duration_minutes']}"],
['Общий доход', f"{stats['revenue']['total']:.2f}"],
['Средний доход за занятие', f"{stats['revenue']['average_per_lesson']:.2f}"],
['Средний балл', f"{stats['grades']['average']}"],
]
detailed_table = Table(detailed_table_data, colWidths=[4 * inch, 2 * inch])
detailed_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#8b5cf6')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.black),
]))
story.append(detailed_table)
story.append(Spacer(1, 0.3 * inch))
# Топ клиентов
if stats.get('top_clients'):
story.append(Paragraph('<b>Топ клиентов</b>', styles['Heading2']))
story.append(Spacer(1, 0.1 * inch))
clients_table_data = [['Клиент', 'Занятий', 'Доход (₽)', 'Средний балл']]
for client in stats['top_clients'][:10]:
clients_table_data.append([
client['name'],
str(client['lessons_count']),
f"{client['total_revenue']:.2f}",
f"{client['average_grade']}"
])
clients_table = Table(clients_table_data, colWidths=[2.5 * inch, 1 * inch, 1.5 * inch, 1 * inch])
clients_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f59e0b')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('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.lightgrey),
('GRID', (0, 0), (-1, -1), 1, colors.black),
]))
story.append(clients_table)
# Генерируем PDF
doc.build(story)
buffer.seek(0)
return buffer
except ImportError:
logger.error("reportlab not installed. Install it with: pip install reportlab")
raise Exception("PDF export requires reportlab library. Install it with: pip install reportlab")
except Exception as e:
logger.error(f"Error generating PDF: {e}", exc_info=True)
raise
class ExcelExporter:
"""Экспорт отчетов в Excel."""
@staticmethod
def generate_report(mentor, start_date, end_date, report_type='overview'):
"""
Генерация Excel отчета.
Args:
mentor: Пользователь-ментор
start_date: Начало периода
end_date: Конец периода
report_type: Тип отчета ('overview', 'detailed', 'revenue', 'lessons')
Returns:
io.BytesIO: Буфер с Excel файлом
"""
try:
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from .services import AnalyticsService
wb = Workbook()
ws = wb.active
ws.title = "Отчет"
# Стили
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF", size=12)
title_font = Font(bold=True, size=16)
border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# Заголовок
ws['A1'] = 'Отчет по аналитике'
ws['A1'].font = title_font
ws.merge_cells('A1:D1')
ws['A2'] = f'Ментор: {mentor.get_full_name() or mentor.email}'
ws['A3'] = f'Период: {start_date.strftime("%d.%m.%Y")} - {end_date.strftime("%d.%m.%Y")}'
ws['A4'] = f'Дата формирования: {datetime.now().strftime("%d.%m.%Y %H:%M")}'
row = 6
if report_type == 'overview':
# Используем сервис напрямую для получения данных
from .services import AnalyticsService
service = AnalyticsService()
overview_data = service.get_overview_data(mentor, start_date, end_date)
revenue_data = service.get_revenue_data(mentor, start_date, end_date)
# Общая статистика
ws[f'A{row}'] = 'Общая статистика'
ws[f'A{row}'].font = Font(bold=True, size=14)
row += 1
stats_data = [
['Метрика', 'Значение'],
['Всего занятий', overview_data['lessons']['total']],
['Завершено', overview_data['lessons']['completed']],
['Отменено', overview_data['lessons']['cancelled']],
['Активных учеников', overview_data['students']['active']],
['Общий доход', f"{overview_data['revenue']['total']:.2f}"],
['Средний доход за занятие', f"{overview_data['revenue']['average_per_lesson']:.2f}"],
['Средний балл', overview_data['grades']['average']],
]
for i, (metric, value) in enumerate(stats_data):
ws[f'A{row}'] = metric
ws[f'B{row}'] = value
if i == 0: # Заголовок
ws[f'A{row}'].fill = header_fill
ws[f'A{row}'].font = header_font
ws[f'B{row}'].fill = header_fill
ws[f'B{row}'].font = header_font
ws[f'A{row}'].border = border
ws[f'B{row}'].border = border
row += 1
row += 2
# Доходы по дням
ws[f'A{row}'] = 'Доходы по дням'
ws[f'A{row}'].font = Font(bold=True, size=14)
row += 1
revenue_headers = ['Дата', 'Доход (₽)', 'Занятий']
for col, header in enumerate(revenue_headers, 1):
cell = ws.cell(row=row, column=col, value=header)
cell.fill = header_fill
cell.font = header_font
cell.border = border
cell.alignment = Alignment(horizontal='center', vertical='center')
row += 1
for item in revenue_data.get('by_day', []):
ws.cell(row=row, column=1, value=item['date']).border = border
ws.cell(row=row, column=2, value=round(item['revenue'], 2)).border = border
ws.cell(row=row, column=3, value=item['lessons_count']).border = border
row += 1
# Доходы по предметам
row += 2
ws[f'A{row}'] = 'Доходы по предметам'
ws[f'A{row}'].font = Font(bold=True, size=14)
row += 1
subject_headers = ['Предмет', 'Доход (₽)', 'Занятий']
for col, header in enumerate(subject_headers, 1):
cell = ws.cell(row=row, column=col, value=header)
cell.fill = header_fill
cell.font = header_font
cell.border = border
cell.alignment = Alignment(horizontal='center', vertical='center')
row += 1
for item in revenue_data.get('by_subject', []):
ws.cell(row=row, column=1, value=item['subject']).border = border
ws.cell(row=row, column=2, value=round(item['revenue'], 2)).border = border
ws.cell(row=row, column=3, value=item['lessons_count']).border = border
row += 1
elif report_type == 'detailed':
stats = AnalyticsService.get_detailed_lesson_stats(mentor, start_date, end_date)
ws[f'A{row}'] = 'Детальная статистика'
ws[f'A{row}'].font = Font(bold=True, size=14)
row += 1
detailed_data = [
['Метрика', 'Значение'],
['Всего занятий', stats['summary']['total_lessons']],
['Завершено', stats['summary']['completed']],
['Процент завершения', f"{stats['summary']['completion_rate']}%"],
['Общее время (часы)', stats['time']['total_duration_hours']],
['Средняя длительность (мин)', stats['time']['average_duration_minutes']],
['Общий доход', f"{stats['revenue']['total']:.2f}"],
['Средний доход за занятие', f"{stats['revenue']['average_per_lesson']:.2f}"],
['Средний балл', stats['grades']['average']],
]
for i, (metric, value) in enumerate(detailed_data):
ws.cell(row=row, column=1, value=metric).border = border
ws.cell(row=row, column=2, value=value).border = border
if i == 0:
ws.cell(row=row, column=1).fill = header_fill
ws.cell(row=row, column=1).font = header_font
ws.cell(row=row, column=2).fill = header_fill
ws.cell(row=row, column=2).font = header_font
row += 1
# Топ клиентов
if stats.get('top_clients'):
row += 2
ws[f'A{row}'] = 'Топ клиентов'
ws[f'A{row}'].font = Font(bold=True, size=14)
row += 1
clients_headers = ['Клиент', 'Занятий', 'Доход (₽)', 'Средний балл']
for col, header in enumerate(clients_headers, 1):
cell = ws.cell(row=row, column=col, value=header)
cell.fill = header_fill
cell.font = header_font
cell.border = border
cell.alignment = Alignment(horizontal='center', vertical='center')
row += 1
for client in stats['top_clients']:
ws.cell(row=row, column=1, value=client['name']).border = border
ws.cell(row=row, column=2, value=client['lessons_count']).border = border
ws.cell(row=row, column=3, value=round(client['total_revenue'], 2)).border = border
ws.cell(row=row, column=4, value=client['average_grade']).border = border
row += 1
# Настройка ширины столбцов
ws.column_dimensions['A'].width = 30
ws.column_dimensions['B'].width = 20
ws.column_dimensions['C'].width = 20
ws.column_dimensions['D'].width = 20
# Сохраняем в буфер
buffer = io.BytesIO()
wb.save(buffer)
buffer.seek(0)
return buffer
except ImportError:
logger.error("openpyxl not installed. Install it with: pip install openpyxl")
raise Exception("Excel export requires openpyxl library. Install it with: pip install openpyxl")
except Exception as e:
logger.error(f"Error generating Excel: {e}", exc_info=True)
raise