81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
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
|
|
from .types import RenderResult
|
|
from .utils import money_fmt
|
|
|
|
def render_html(inv: Invoice, template_path: Path) -> str:
|
|
env = Environment(
|
|
loader=FileSystemLoader(str(template_path.parent)),
|
|
autoescape=select_autoescape(["html", "xml"]),
|
|
)
|
|
env.globals["money"] = lambda x: money_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"] = float(it.quantity) * float(it.unit_price_net)
|
|
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))
|
|
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).")
|