diff --git a/apps/reports/pdf.py b/apps/reports/pdf.py index af45695..1408ba3 100644 --- a/apps/reports/pdf.py +++ b/apps/reports/pdf.py @@ -87,3 +87,59 @@ def render_company_pdf(report, company): story.append(Paragraph(f"Итого: {_duration(company_group['minutes'])}, {company_group['amount']} ₽", total)) document.build(story) return output.getvalue() + + +def render_user_pdf(report): + output = BytesIO() + font, bold_font = _font_names() + styles = getSampleStyleSheet() + normal = ParagraphStyle("UserReportNormal", parent=styles["Normal"], fontName=font, fontSize=9, leading=12) + heading = ParagraphStyle("UserReportHeading", parent=styles["Heading1"], fontName=bold_font, fontSize=18, leading=22) + company_heading = ParagraphStyle("UserReportCompany", parent=styles["Heading2"], fontName=bold_font, fontSize=15, leading=18, textColor=colors.HexColor("#172033")) + project_heading = ParagraphStyle("UserReportProject", parent=styles["Heading3"], fontName=bold_font, fontSize=12, leading=15) + task_title = ParagraphStyle("UserReportTaskTitle", parent=normal, fontName=bold_font, fontSize=9, leading=11) + task_description = ParagraphStyle("UserReportTaskDescription", parent=normal, fontSize=8, leading=11, textColor=colors.HexColor("#5a6477")) + total = ParagraphStyle("UserReportTotal", parent=normal, fontName=bold_font, fontSize=11, alignment=TA_RIGHT) + grand_total = ParagraphStyle("UserReportGrandTotal", parent=total, fontSize=14, leading=18) + document = SimpleDocTemplate(output, pagesize=A4, rightMargin=15 * mm, leftMargin=15 * mm, topMargin=15 * mm, bottomMargin=15 * mm) + user_name = report["user"].get_full_name() or report["user"].username + story = [ + Paragraph("Общий отчёт о выполненных работах", heading), + Spacer(1, 4 * mm), + Paragraph(f"Исполнитель: {escape(user_name)}", normal), + Paragraph(f"Период: {report['month']:02d}.{report['year']}", normal), + Spacer(1, 5 * mm), + ] + for company_group in report["companies"]: + story.append(Paragraph(escape(company_group["company"].name), company_heading)) + for project_group in company_group["projects"].values(): + story.append(Paragraph(escape(project_group["project"].name), project_heading)) + rows = [["Дата", "Задача и описание", "Время", "Ставка", "Сумма"]] + for task in project_group["tasks"]: + rows.append([ + task.work_date.strftime("%d.%m.%Y"), + [ + Paragraph(escape(task.title), task_title), + Spacer(1, 1.5 * mm), + Paragraph(escape(task.description), task_description), + ], + _duration(task.duration_minutes), f"{task.hourly_rate} ₽", f"{task.amount} ₽", + ]) + rows.append(["", "Итого по проекту", _duration(project_group["minutes"]), "", f"{project_group['amount']} ₽"]) + table = Table(rows, colWidths=(22 * mm, 78 * mm, 25 * mm, 25 * mm, 25 * mm), repeatRows=1) + table.setStyle(TableStyle([ + ("FONTNAME", (0, 0), (-1, -1), font), ("FONTSIZE", (0, 0), (-1, -1), 8), + ("FONTNAME", (0, 0), (-1, 0), bold_font), + ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#e9edf4")), + ("GRID", (0, 0), (-1, -1), .35, colors.HexColor("#aeb7c5")), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5), + ])) + story.extend((table, Spacer(1, 3 * mm))) + story.extend(( + Paragraph(f"Итого по компании: {_duration(company_group['minutes'])}, {company_group['amount']} ₽", total), + Spacer(1, 6 * mm), + )) + story.append(Paragraph(f"Общий итог: {_duration(report['total_minutes'])}, {report['total_amount']} ₽", grand_total)) + document.build(story) + return output.getvalue() diff --git a/apps/reports/test_reports.py b/apps/reports/test_reports.py index 756e53d..c38b73f 100644 --- a/apps/reports/test_reports.py +++ b/apps/reports/test_reports.py @@ -61,3 +61,17 @@ class MonthlyReportTests(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/pdf") self.assertTrue(response.content.startswith(b"%PDF")) + + def test_user_pdf_is_generated_for_all_companies(self): + self.client.force_login(self.user) + response = self.client.get(reverse("user-report-pdf"), {"year": 2026, "month": 7}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "application/pdf") + self.assertEqual(response["Content-Disposition"], 'attachment; filename="report-worker-2026-07.pdf"') + self.assertTrue(response.content.startswith(b"%PDF")) + + def test_worker_cannot_export_another_users_general_report(self): + other = User.objects.create_user(username="other", password="password123", must_change_password=False) + self.client.force_login(self.user) + response = self.client.get(reverse("user-report-pdf"), {"user": other.pk, "year": 2026, "month": 7}) + self.assertEqual(response["Content-Disposition"], 'attachment; filename="report-worker-2026-07.pdf"') diff --git a/apps/reports/urls.py b/apps/reports/urls.py index baf2014..8b47667 100644 --- a/apps/reports/urls.py +++ b/apps/reports/urls.py @@ -4,5 +4,6 @@ from . import views urlpatterns = [ path("", views.monthly_report, name="monthly-report"), + path("pdf/", views.user_pdf, name="user-report-pdf"), path("company//pdf/", views.company_pdf, name="company-report-pdf"), ] diff --git a/apps/reports/views.py b/apps/reports/views.py index 7baa802..a57adf1 100644 --- a/apps/reports/views.py +++ b/apps/reports/views.py @@ -9,7 +9,7 @@ from apps.accounts.models import User from apps.companies.models import Company from .services import build_monthly_report -from .pdf import render_company_pdf +from .pdf import render_company_pdf, render_user_pdf def selected_user(request): @@ -50,3 +50,15 @@ def company_pdf(request, company_id): response = HttpResponse(pdf, content_type="application/pdf") response["Content-Disposition"] = f'attachment; filename="{filename}"' return response + + +@login_required +def user_pdf(request): + user = selected_user(request) + year, month = selected_period(request) + report = build_monthly_report(user, year, month) + pdf = render_user_pdf(report) + filename = f"report-{user.username}-{year}-{month:02d}.pdf" + response = HttpResponse(pdf, content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{filename}"' + return response diff --git a/templates/reports/monthly.html b/templates/reports/monthly.html index c04319c..f81fbde 100644 --- a/templates/reports/monthly.html +++ b/templates/reports/monthly.html @@ -1,6 +1,6 @@ {% extends 'base.html' %}{% load report_tags %}{% block content %}

Месячный отчёт

{% if user.is_app_admin %}{% endif %}
-

{{ report.user.get_full_name|default:report.user.username }}

+

{{ report.user.get_full_name|default:report.user.username }}

PDF общего отчёта
{% for group in report.companies %}

{{ group.company.name }}

{% if user.is_app_admin %}PDF для клиента{% endif %}
{% for project in group.projects.values %}

{{ project.project.name }}

{% for task in project.tasks %}{% endfor %}
ДатаЗадачаОписаниеВремяСтавкаСумма
{{ task.work_date|date:'d.m.Y' }}{{ task.title }}{{ task.description }}{{ task.duration_minutes|duration }}{{ task.hourly_rate }} ₽{{ task.amount }} ₽
Итого по проекту{{ project.minutes|duration }}{{ project.amount }} ₽
{% endfor %}

Итого по компании: {{ group.minutes|duration }}, {{ group.amount }} ₽

{% empty %}

За выбранный месяц выполненных задач нет.

{% endfor %}