Document layouts & PDF printing (document_layout module)¶
Odoo-style external document layout: each company configures logo, address block, accent colours, paper format, and a layout variant. Business modules register printable documents (invoices, delivery slips, receipts); the framework wraps the body HTML in the company layout and renders PDF via wkhtmltopdf.
Install¶
document_layout ships in the wheel under pyvelm/modules/. Add it to your
app's module roots (or install from Apps) and declare a dependency:
Requires base (company branding fields, attachments). Optional admin
menus are provided by the module's own views/menu.py — not required for
printing.
Server dependency: wkhtmltopdf¶
PDF routes call the wkhtmltopdf binary on PATH. HTML preview routes work
without it.
When the binary is missing, GET /report/pdf/… returns 503 with an
install hint. Use GET /report/html/… to iterate on templates without PDF
round-trips.
Company configuration¶
Settings → Organization → Layout & Print (or open
res_company_layout.form for a company).
| Area | Fields |
|---|---|
| Document Layout | Variant (light, boxed, bold, …), paper (A4 / Letter), Google Font, logo, primary/secondary colours |
| Design layout | Opens the live designer dialog (design_button widget) |
| Company Address | Street, city, ZIP, phone, email, website, VAT — rendered in the PDF header |
| Branding | Copyright line on the footer |
Layout variants include light, boxed, bold, striped, editorial, split, dark, and folder (Odoo 19-style tab header). The designer previews unsaved changes before save.
Company fields are added via _inherit on res.company in
document_layout/models/company_ext.py (address block, document_layout,
paper_format, etc.).
Register a printable document¶
During module load (typically an INSTALL_HOOK or at import time in a
hooks.py that runs before routes are registered), call
document_layout.api.register_document:
from document_layout.api import register_document
def _render_invoice_body(env, record):
return f"<h1>Invoice {record.name}</h1><p>Total: {record.amount_total}</p>"
def install(env):
register_document(
"account.invoice",
model="account.move",
render_body=_render_invoice_body,
title="Invoice",
)
| Argument | Meaning |
|---|---|
key |
URL segment and registry id (e.g. account.invoice) |
model |
Model technical name — read ACL is enforced on print |
render_body |
(env, record) -> html str — inner body only; layout wrapper is automatic |
title |
Document title in the header (defaults to key) |
Register keys must be unique across the registry.
HTTP routes¶
Registered via WEB_ROUTES = "document_layout.web:register_routes". All
routes require login and read access on the underlying model (not sudo).
| Route | Purpose |
|---|---|
GET /report/pdf/{key}/{record_id} |
Download branded PDF |
GET /report/html/{key}/{record_id} |
Full HTML preview (same markup as PDF) |
GET /report/layout/preview/{company_id} |
Sample document in company layout |
GET /report/layout/designer |
Designer fragment (PvDialog) |
GET /web/report/layout/designer |
Designer full page in app shell |
GET /report/layout/preview-live |
Live preview with query-string overrides (designer JS) |
POST /report/layout/designer/save |
Persist layout fields on res.company |
Link to a PDF from a list or form:
Python API¶
from document_layout.api import (
document_spec,
register_document,
render_html,
render_layout_preview,
render_pdf,
)
# Full branded HTML (preview=True wraps in paper sheet for browser)
html = render_html(env, "account.invoice", move_id, preview=True)
# PDF bytes + paper format string
pdf_bytes, paper = render_pdf(env, "account.invoice", move_id)
# Design-time sample (optional overrides dict for live designer)
fragment = render_layout_preview(env, company_id=1, overrides={"layout": "folder"}, inline=True)
render_pdf pipes HTML through wkhtmltopdf with margins matching the
preview CSS (16 mm top/bottom, 12 mm left/right).
Templates & packaging¶
Module Jinja templates live in document_layout/templates/ and extend the
framework shell where needed. They are shipped in the wheel via
modules/**/templates/** in pyproject.toml package-data.
Logo URLs stored as /api/attachment/{id}/download are inlined as data:
URIs server-side so wkhtmltopdf (a separate process with no session) can
embed them.
Tests¶
Unit tests:
pyvelm/tests/test_document_layout_all.py— layout rendering (bundled module tests)pyvelm/tests/test_document_layout_coverage.py— hooks, PDF, web routes, registry
Run: