""" Экспортеры отчетов в 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'Ментор: {mentor.get_full_name() or mentor.email}', info_style)) story.append(Paragraph(f'Период: {start_date.strftime("%d.%m.%Y")} - {end_date.strftime("%d.%m.%Y")}', info_style)) story.append(Paragraph(f'Дата формирования: {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('Общая статистика', 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('Доходы', 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('Детальная статистика', 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('Топ клиентов', 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