113 lines
4.1 KiB
Python
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
|