modified: Dockerfile
new file: pdf_export.py modified: requirements.txt modified: routes/journal_routes.py modified: static/css/style.css modified: templates/base.html modified: templates/day.html new file: templates/edit_future.html new file: templates/edit_note.html modified: templates/future.html modified: templates/monat.html new file: templates/report.html modified: templates/week.html
This commit is contained in:
159
pdf_export.py
Normal file
159
pdf_export.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""PDF-Export für Wochen-/Monatsberichte (ReportLab).
|
||||
|
||||
Unterstützt Umlaute über die DejaVu-Schriftarten, die im Docker-Image
|
||||
(fonts-dejavu-core) enthalten sind. Falls keine TTF gefunden wird,
|
||||
fällt die Generierung auf Helvetica zurück (dann evtl. ohne Umlaute).
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_LEFT
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
|
||||
)
|
||||
|
||||
_DEJAVU_PATHS = [
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/DejaVuSans.ttf",
|
||||
]
|
||||
_DEJAVU_BOLD_PATHS = [
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
|
||||
]
|
||||
|
||||
|
||||
def _register_fonts():
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
regular = next((p for p in _DEJAVU_PATHS if os.path.exists(p)), None)
|
||||
bold = next((p for p in _DEJAVU_BOLD_PATHS if os.path.exists(p)), None)
|
||||
|
||||
if regular:
|
||||
pdfmetrics.registerFont(TTFont("DejaVu", regular))
|
||||
if bold:
|
||||
pdfmetrics.registerFont(TTFont("DejaVu-Bold", bold))
|
||||
return "DejaVu", "DejaVu-Bold"
|
||||
return "Helvetica", "Helvetica-Bold"
|
||||
|
||||
|
||||
def _build_styles(font_name: str, font_bold: str):
|
||||
styles = getSampleStyleSheet()
|
||||
base = ParagraphStyle(
|
||||
"ReportBase",
|
||||
fontName=font_name,
|
||||
fontSize=9,
|
||||
leading=12,
|
||||
textColor=colors.HexColor("#2b2b2b"),
|
||||
)
|
||||
return {
|
||||
"title": ParagraphStyle(
|
||||
"ReportTitle", parent=styles["Title"], fontName=font_bold,
|
||||
fontSize=18, leading=22, spaceAfter=4, textColor=colors.HexColor("#2b2b2b"),
|
||||
),
|
||||
"subtitle": ParagraphStyle(
|
||||
"ReportSub", parent=styles["Normal"], fontName=font_name,
|
||||
fontSize=10, leading=14, textColor=colors.HexColor("#8a867e"),
|
||||
),
|
||||
"h2": ParagraphStyle(
|
||||
"ReportH2", parent=styles["Heading2"], fontName=font_bold,
|
||||
fontSize=12, leading=16, spaceBefore=10, spaceAfter=4,
|
||||
textColor=colors.HexColor("#a8871d"),
|
||||
),
|
||||
"body": base,
|
||||
"cell": ParagraphStyle(
|
||||
"Cell", parent=base, fontSize=8.5, leading=11,
|
||||
alignment=TA_LEFT,
|
||||
),
|
||||
"cellheader": ParagraphStyle(
|
||||
"CellHeader", parent=base, fontName=font_bold,
|
||||
fontSize=8.5, leading=11, textColor=colors.white,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def build_pdf(title: str, subtitle: str, rows: list[dict],
|
||||
summary: dict | None = None) -> bytes:
|
||||
"""Erzeugt ein PDF mit einer Übersichtstabelle der Einträge.
|
||||
|
||||
rows: Liste von dicts mit Schluesseln
|
||||
date_str, weekday, bullet, bullet_label, note_text, priority,
|
||||
status, refs
|
||||
summary: optionales dict mit open/done/total/Zeitraum.
|
||||
"""
|
||||
font_name, font_bold = _register_fonts()
|
||||
st = _build_styles(font_name, font_bold)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
|
||||
doc = SimpleDocTemplate(
|
||||
buffer, pagesize=A4,
|
||||
leftMargin=15 * mm, rightMargin=15 * mm,
|
||||
topMargin=18 * mm, bottomMargin=18 * mm,
|
||||
title=title,
|
||||
)
|
||||
|
||||
story = []
|
||||
story.append(Paragraph(title, st["title"]))
|
||||
story.append(Paragraph(subtitle, st["subtitle"]))
|
||||
story.append(Spacer(1, 4))
|
||||
|
||||
# Zusammenfassung
|
||||
if summary:
|
||||
story.append(Paragraph("Zusammenfassung", st["h2"]))
|
||||
sum_rows = [
|
||||
["Offen", "Erledigt", "Gesamt"],
|
||||
[str(summary.get("open", 0)), str(summary.get("done", 0)),
|
||||
str(summary.get("total", 0))],
|
||||
]
|
||||
sum_table = Table(sum_rows, colWidths=[40 * mm, 40 * mm, 40 * mm])
|
||||
sum_table.setStyle(TableStyle([
|
||||
("FONTNAME", (0, 0), (-1, 0), font_bold),
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#a8871d")),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
|
||||
("FONTNAME", (0, 1), (-1, -1), font_name),
|
||||
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#e5ded2")),
|
||||
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 4),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||||
]))
|
||||
story.append(sum_table)
|
||||
story.append(Spacer(1, 6))
|
||||
|
||||
# Einträge
|
||||
story.append(Paragraph("Einträge", st["h2"]))
|
||||
header = ["Datum", "Wochentag", "Bullet", "Notiz", "Priorität", "Status", "Verweise"]
|
||||
table_data = [[Paragraph(h, st["cellheader"]) for h in header]]
|
||||
|
||||
for r in rows:
|
||||
table_data.append([
|
||||
Paragraph(r.get("date_str", ""), st["cell"]),
|
||||
Paragraph(r.get("weekday", ""), st["cell"]),
|
||||
Paragraph(f"{r.get('bullet', '')} {r.get('bullet_label', '')}", st["cell"]),
|
||||
Paragraph(r.get("note_text", ""), st["cell"]),
|
||||
Paragraph(r.get("priority", ""), st["cell"]),
|
||||
Paragraph(r.get("status", ""), st["cell"]),
|
||||
Paragraph(r.get("refs", ""), st["cell"]),
|
||||
])
|
||||
|
||||
col_widths = [18 * mm, 18 * mm, 20 * mm, 60 * mm, 16 * mm, 16 * mm, 30 * mm]
|
||||
table = Table(table_data, colWidths=col_widths, repeatRows=1)
|
||||
table.setStyle(TableStyle([
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#a8871d")),
|
||||
("GRID", (0, 0), (-1, -1), 0.3, colors.HexColor("#e5ded2")),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("ROWBACKGROUNDS", (0, 1), (-1, -1),
|
||||
[colors.white, colors.HexColor("#faf7ef")]),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 3),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
|
||||
]))
|
||||
story.append(table)
|
||||
|
||||
doc.build(story)
|
||||
buffer.seek(0)
|
||||
return buffer.getvalue()
|
||||
Reference in New Issue
Block a user