upload
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user