Skip to content
Back to blog
Odoo integration invoices

Print Odoo Invoices and Quotations with a pdfs.build Template

pdfs.build Team

Odoo prints every PDF the same way: a QWeb HTML template is rendered, then wkhtmltopdf turns it into a PDF. Changing how an invoice looks means editing XML views, inheriting the right t-call, and fighting a headless browser over page breaks. Most Odoo shops have a folder of such customizations that nobody wants to reopen.

The pdfs.build PDF Templates module takes a different route. You design the document in the pdfs.build editor, as a Typst template with a JSON Schema, and the module renders Odoo records through it. Odoo keeps doing everything else: the Print menu, email attachments, portal downloads, attachment storage and multi-record merging all work unchanged, because the module only swaps the rendering step underneath Odoo’s own report pipeline.

It is free, LGPL-3, and available for Odoo 17.0, 18.0 and 19.0 on Odoo Apps and GitHub. This post walks through a full setup: install, connect, map a quotation template, then replace the stock customer invoice.

Before you start

1. Install the module

Download the module from Odoo Apps, or clone the branch that matches your Odoo version:

git clone --branch 19.0 https://github.com/pdfs-build/odoo.git pdfs-build-odoo

Add the clone to your addons_path, restart Odoo, update the apps list and install pdfs.build PDF Templates. The module depends only on base and web, so it installs on Community and Enterprise alike. Its one Python dependency, requests, ships with Odoo.

2. Connect it to your organization

Open Settings and find the pdfs.build section. Paste the API key and the organization ID, then click Test Connection. The module calls the API and tells you plainly if the key is wrong, if it belongs to a different organization, or if the plan does not allow API renders.

Click Sync Templates. Every published template in the organization is pulled in with its name, its fields and its sample data, and listed under the new pdfs.build app in the top menu.

The synced template list in Odoo, with each template’s data fields and the number of reports using it

Two things worth knowing about the sync:

3. Map a template to a model

Open a template and click New Report. Pick the Odoo model, for example sale.order, and give the report the name that should appear in the Print menu.

The important part is the Data tab. It holds one Python expression that is evaluated for each printed record and must return the data the template expects. The module prefills it from the template’s JSON Schema, with one entry per field and the field’s description as a comment, so your job is to replace the placeholders with Odoo fields.

The report form in Odoo with the data expression on the left and the template’s fields on the right

For a quotation it ends up looking like this:

{
    "quoteNumber": record.name,
    "issueDate": format_date(record.date_order),
    "clientName": record.partner_id.commercial_partner_id.name,
    "clientEmail": record.partner_id.email,
    "items": [
        {"name": line.product_id.name, "description": line.name, "amount": line.price_subtotal}
        for line in record.order_line
        if not line.display_type
    ],
    "total": record.amount_total,
    "notes": html2plaintext(record.note or ""),
    "logo": image(record.company_id.logo),
}

The form has three tabs that make this quick:

Empty Odoo fields are False, and the module coerces them to whatever the schema asks for: an empty string, a zero or an empty list. Dates become ISO strings. If a required field is still a placeholder, the API rejects the render with the exact field name, so nothing half-rendered reaches a customer.

Click Preview Data to see the JSON for a chosen record, and Preview PDF to render that record for real. The PDF opens in a viewer inside Odoo.

A rendered quotation PDF opened in a dialog inside Odoo

Save the report. It now sits in the Print menu of every sales order, and selecting several orders in the list and printing merges them into one PDF, one document per order.

The Print menu of an Odoo record listing the pdfs.build report

Or skip the mapping: designs built for Odoo

The template gallery has an Odoo category: two designs each for the invoice, the quotation, the purchase order and the delivery slip, whose fields are named after Odoo’s own. partner_id is the customer block, invoice_line_ids the lines, amount_total, tax_totals and payment_state what they are in Odoo. In the module, Designs for Odoo (on the Templates list and in Settings) shows them with previews; Add to Odoo copies the design into your organization, syncs it and creates its report on the right model, expression already written. Nothing is left to fill in. They also read your company’s logo and document-layout colours from Odoo’s settings, so a multi-company database prints each company in its own colours without a second template.

4. Replace the stock invoice PDF

The step most people actually want. Instead of creating a new report, open the invoice template and click Use for Existing Report. The dialog lists every PDF report in the database that is not yet backed by a pdfs.build template. Pick Invoices, the report Odoo uses for the Print button, the Send & Print wizard and the customer portal.

Picking the stock customer invoice report to be rendered by a pdfs.build template

The report keeps its name, its model and every reference to it. Only the rendering changes. Fill in the data expression as before and from then on:

Odoo’s post-processing still runs on the result. If you use account_edi or Factur-X, the XML is embedded into the pdfs.build PDF; the sales quote builder still prepends and appends its pages. The module takes care to call Odoo’s report pipeline rather than bypass it, which is what keeps those modules working.

A customer invoice rendered by pdfs.build, generated from a real Odoo invoice

To go back, clear the template on the report (Settings → Technical → Reports) and Odoo renders the original QWeb view again.

Keeping Odoo and the template in sync

Templates change. A report follows the template’s latest published version by default, so publishing a new version on pdfs.build changes what Odoo prints on the next render. If the new version adds a field, run Sync Templates: the field list in Odoo is refreshed and the Template Fields tab shows what is now missing from the data expression.

For a stricter setup, the report’s Version field lets you pin a version number, or a channel such as staging. A pinned report keeps the schema of the version it was pinned to, so a change on pdfs.build cannot alter a production invoice until you decide it should.

What leaves your server

Only the data the expression returns for the printed record, sent over HTTPS to api.pdfs.build with your API key. The module reads nothing else from your database and stores nothing on pdfs.build beyond the render log. Each printed document counts as one API render against your monthly quota.

Where to go from here

Back to blog