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