Files
timerr/apps/reports/pdf.py
T
MS.Kondratev 0feeaad4fa
Build and publish Docker image / publish (push) Successful in 12s
feat: export consolidated user report PDF
2026-07-15 17:25:40 +05:00

146 lines
8.3 KiB
Python

from io import BytesIO
from pathlib import Path
from xml.sax.saxutils import escape
from reportlab.lib import colors
from reportlab.lib.enums import TA_RIGHT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
def _font_names():
candidates = (
(Path("C:/Windows/Fonts/arial.ttf"), Path("C:/Windows/Fonts/arialbd.ttf")),
(
Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"),
Path("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"),
),
)
for regular_path, bold_path in candidates:
if regular_path.exists() and bold_path.exists():
pdfmetrics.registerFont(TTFont("TimerrFont", regular_path))
pdfmetrics.registerFont(TTFont("TimerrFont-Bold", bold_path))
pdfmetrics.registerFontFamily(
"TimerrFont",
normal="TimerrFont",
bold="TimerrFont-Bold",
italic="TimerrFont",
boldItalic="TimerrFont-Bold",
)
return "TimerrFont", "TimerrFont-Bold"
return "Helvetica", "Helvetica-Bold"
def _duration(minutes):
hours, remainder = divmod(int(minutes), 60)
return f"{hours} ч {remainder:02d} мин"
def render_company_pdf(report, company):
output = BytesIO()
font, bold_font = _font_names()
styles = getSampleStyleSheet()
normal = ParagraphStyle("TimerrNormal", parent=styles["Normal"], fontName=font, fontSize=9, leading=12)
heading = ParagraphStyle("TimerrHeading", parent=styles["Heading1"], fontName=bold_font, fontSize=18, leading=22)
subheading = ParagraphStyle("TimerrSubheading", parent=styles["Heading2"], fontName=bold_font, fontSize=13, leading=16)
task_title = ParagraphStyle("TimerrTaskTitle", parent=normal, fontName=bold_font, fontSize=9, leading=11, textColor=colors.HexColor("#172033"))
task_description = ParagraphStyle("TimerrTaskDescription", parent=normal, fontSize=8, leading=11, textColor=colors.HexColor("#5a6477"))
total = ParagraphStyle("TimerrTotal", parent=normal, fontSize=12, alignment=TA_RIGHT)
document = SimpleDocTemplate(output, pagesize=A4, rightMargin=15 * mm, leftMargin=15 * mm, topMargin=15 * mm, bottomMargin=15 * mm)
story = [
Paragraph("Отчёт о выполненных работах", heading),
Spacer(1, 4 * mm),
Paragraph(f"<b>Компания:</b> {escape(company.name)}", normal),
Paragraph(f"<b>Исполнитель:</b> {escape(report['user'].get_full_name() or report['user'].username)}", normal),
Paragraph(f"<b>Период:</b> {report['month']:02d}.{report['year']}", normal),
Spacer(1, 4 * mm),
]
for company_group in report["companies"]:
for project_group in company_group["projects"].values():
story.append(Paragraph(escape(project_group["project"].name), subheading))
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"), ("FONTNAME", (0, -1), (-1, -1), font),
("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
]))
story.extend((table, Spacer(1, 5 * mm)))
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"<b>Исполнитель:</b> {escape(user_name)}", normal),
Paragraph(f"<b>Период:</b> {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()