418 lines
21 KiB
Python
418 lines
21 KiB
Python
"""
|
||
Экспортеры отчетов в 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
|
||
|