from __future__ import annotations from pathlib import Path import subprocess import shutil from jinja2 import Environment, FileSystemLoader, select_autoescape from .models import Invoice from .calc import compute_totals, line_net from .types import RenderResult from .utils import money_fmt, number_fmt def render_html(inv: Invoice, template_path: Path) -> str: env = Environment( loader=FileSystemLoader(str(template_path.parent)), autoescape=select_autoescape(["html", "xml", "j2"]), ) env.globals["money"] = lambda x: money_fmt(x, inv.meta.currency) env.globals["number"] = number_fmt env.filters["de_date"] = lambda d: d.strftime("%d.%m.%Y") if d else "" env.globals["unit_label"] = lambda unit: {"HUR": "Std.", "C62": "Stück", "DAY": "Tage"}.get(unit, unit) env.globals["price"] = lambda x: f"{number_fmt(x)} {inv.meta.currency}" tpl = env.get_template(template_path.name) totals = compute_totals(inv) items = [] for it in inv.items: d = it.model_dump() d["line_total_net"] = line_net(it) items.append(type("Obj", (), d)) ctx = { "meta": inv.meta.model_dump(), "seller": inv.seller.model_dump(), "buyer": inv.buyer.model_dump(), "payment": inv.payment.model_dump(), "items": items, "totals": totals, } return tpl.render(**ctx) def _weasyprint_available() -> bool: try: import weasyprint # noqa return True except Exception: return False def render_pdf(html: str, out_pdf: Path, logger, prefer_weasyprint: bool = True, base_url: str | None = None) -> RenderResult: out_pdf.parent.mkdir(parents=True, exist_ok=True) if prefer_weasyprint and _weasyprint_available(): logger.info("Rendering PDF using WeasyPrint") try: from weasyprint import HTML # type: ignore HTML(string=html, base_url=base_url).write_pdf(str(out_pdf), pdf_variant="pdf/a-3b") return RenderResult(ok=True, backend="weasyprint", path=out_pdf) except Exception as e: logger.exception("WeasyPrint rendering failed; trying wkhtmltopdf if available: %s", e) wk = shutil.which("wkhtmltopdf") if wk: logger.info("Rendering PDF using wkhtmltopdf (%s)", wk) tmp_html = out_pdf.with_suffix(".tmp.html") tmp_html.write_text(html, encoding="utf-8") try: proc = subprocess.run( [wk, "--quiet", "--enable-local-file-access", str(tmp_html), str(out_pdf)], capture_output=True, text=True, check=False, ) if proc.returncode != 0: logger.error("wkhtmltopdf failed (rc=%s). stderr=%s", proc.returncode, proc.stderr.strip()) return RenderResult(ok=False, backend="wkhtmltopdf", path=None, error="wkhtmltopdf failed") return RenderResult(ok=True, backend="wkhtmltopdf", path=out_pdf) finally: try: tmp_html.unlink(missing_ok=True) except Exception: pass return RenderResult(ok=False, backend="none", path=None, error="No PDF backend available (install weasyprint or wkhtmltopdf).")