This commit is contained in:
2026-06-24 14:40:43 +00:00
parent 22b05a7a1b
commit 7058970f98
80 changed files with 6383 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
from __future__ import annotations
import json
import os
from copy import deepcopy
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any
import requests
from jinja2 import Environment, FileSystemLoader, select_autoescape
from .formatting import clamp, currency_de, date_de, delta_de, number_de, percent_de
from .theme_catalog import THEMES, get_theme
BASE_DIR = Path(__file__).resolve().parent
TEMPLATE_DIR = BASE_DIR / "templates"
ALLOWED_THEMES = set(THEMES)
jinja = Environment(
loader=FileSystemLoader(str(TEMPLATE_DIR)),
autoescape=select_autoescape(["html", "xml", "j2"]),
trim_blocks=True,
lstrip_blocks=True,
)
jinja.filters["number_de"] = number_de
jinja.filters["currency_de"] = currency_de
jinja.filters["percent_de"] = percent_de
jinja.filters["delta_de"] = delta_de
jinja.filters["date_de"] = date_de
jinja.filters["clamp"] = clamp
def _prepare_trends(payload: dict[str, Any]) -> dict[str, Any]:
result = deepcopy(payload)
trends = result.get("trends", [])
max_revenue = max((float(item.get("revenue", 0)) for item in trends), default=1) or 1
for item in trends:
item["revenue_bar"] = round(clamp(float(item.get("revenue", 0)) / max_revenue * 100), 2)
item["occupancy_bar"] = round(clamp(item.get("occupancy_rate", 0)), 2)
return result
def render_documents(payload: dict[str, Any]) -> tuple[str, str]:
prepared = _prepare_trends(payload)
theme_id = prepared.get("design", {}).get("theme", "modern")
theme = get_theme(theme_id)
design = prepared.setdefault("design", {})
palette_mode = design.get("palette_mode")
if not palette_mode:
palette_mode = "custom" if theme.get("custom_palette_default") else "theme"
if palette_mode not in {"theme", "custom"}:
raise ValueError(f"Unknown palette mode: {palette_mode}")
if palette_mode == "theme":
design.update(theme["palette"])
design["palette_mode"] = palette_mode
prepared["_theme"] = theme
prepared["_theme"]["palette_mode"] = palette_mode
layout = theme["layout"]
context = {"data": prepared}
report_template = jinja.get_template(f"{layout}/report.html.j2")
local_footer = TEMPLATE_DIR / layout / "footer.html.j2"
footer_name = f"{layout}/footer.html.j2" if local_footer.exists() else "_shared/footer.html.j2"
footer_template = jinja.get_template(footer_name)
return report_template.render(**context), footer_template.render(**context)
def pdf_filename(payload: dict[str, Any]) -> str:
customer = payload["customer"]["id"].strip().lower().replace(" ", "-")
period = payload["report"]["period"]
return f"monatsreport-{customer}-{period['year']}-{period['month']:02d}.pdf"
def render_pdf_with_gotenberg(payload: dict[str, Any]) -> bytes:
index_html, footer_html = render_documents(payload)
gotenberg_url = os.getenv("GOTENBERG_URL", "http://gotenberg:3000").rstrip("/")
timeout = int(os.getenv("RENDER_TIMEOUT_SECONDS", "60"))
metadata = {
"Title": f"Monatsreport {payload['report']['period']['label']} - {payload['customer']['name']}",
"Author": payload["report"].get("publisher", "Monthly Report Kit"),
"Creator": "Monthly Report Kit / Gotenberg",
"Subject": "Automatisch erzeugter Monatsreport",
"Keywords": ["Monatsreport", payload["customer"]["id"], payload["report"]["period"]["label"]],
}
files = [
("files", ("index.html", index_html.encode("utf-8"), "text/html")),
("files", ("footer.html", footer_html.encode("utf-8"), "text/html")),
]
form = {
"printBackground": "true",
"preferCssPageSize": "true",
"generateDocumentOutline": "true",
"generateTaggedPdf": "true",
"failOnResourceLoadingFailed": "true",
"metadata": json.dumps(metadata, ensure_ascii=False),
}
response = requests.post(
f"{gotenberg_url}/forms/chromium/convert/html",
files=files,
data=form,
timeout=timeout,
)
if response.status_code != 200:
detail = response.text[:1000]
raise RuntimeError(f"Gotenberg returned HTTP {response.status_code}: {detail}")
return response.content