Files
2026-06-24 14:40:43 +00:00

113 lines
4.1 KiB
Python

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