an abundance of strawberries

Odoo module upgrades, done for you

Migrating a customized invoice PDF from Odoo 15 to 19

The third pattern: no business logic at all, just paper. The customer's invoice PDF shows extra customer information (their reference, industry code, account manager), custom boxes in the layout for regulatory info, and two extra columns on the product lines. In Odoo terms: an inherit of account.report_invoice_document, an inherit of the external layout, and some report SCSS.

Why report inherits rot faster than Python

QWeb customizations are xpath surgery on templates Odoo keeps redesigning. Between 15 and 19 the invoice document and the stock external layouts were restructured more than once โ€” Odoo 17 alone reworked the standard layouts and their markup. Every positional xpath is a bet that the parent template never changes. Over four versions, most of those bets lose:

<!-- Odoo 15 inherit: worked for years, dies on upgrade -->
<xpath expr="//table[@name='invoice_line_table']/thead/tr/th[3]"
       position="after">
    <th class="text-right">Batch</th>
</xpath>

On the target version that's a ParseError: element cannot be located in parent view โ€” or worse, it matches a different column and the layout is subtly wrong. The port re-anchors every xpath on named nodes, which survive redesigns:

<xpath expr="//th[@name='th_quantity']" position="after">
    <th name="th_batch" class="text-end">Batch</th>
</xpath>

(Note even the CSS class changed โ€” Bootstrap's text-right became text-end when Odoo moved to Bootstrap 5.)

What each customization hit

  • Extra customer info: the informations block of the invoice was restructured, so the inherit re-anchors there; where possible we move partner-level extras into the layout's information-block hook instead of hard xpaths, so the next upgrade is cheaper.
  • Custom layout boxes: the external layouts were redesigned in 17 (new markup, company-scoped classes). Two footguns matter here: the header/footer are extracted by looking for the literal class tokens header/footer, and they render without the asset bundle โ€” styles that lived in SCSS silently vanish from the header unless they're inlined. Custom boxes also interact with the paperformat: get header_spacing wrong relative to margin_top and the header prints on top of the body.
  • Extra line columns: re-anchored as above, plus a check that the columns still fit โ€” core added its own conditional columns since 15, and a crowded table wraps badly in PDF while looking fine in the browser.

How we verified it

PDFs need their own kind of testing, because wkhtmltopdf is not a browser. We render real invoices on the target version โ€” a one-line invoice and a multi-page one โ€” and check the actual output: header offset correct on every page, columns aligned, custom boxes where they belong, both in PDF and in the HTML preview. "The template compiles" catches maybe half the ways a report migration goes wrong.

Try the working result before you pay a cent. No signup to get started.

Common questions

Is a report-only module the $50 tier?

For a one- or two-version jump, usually yes โ€” no JavaScript, no business logic; the work is xpath re-anchoring and layout verification. This one priced at the complex tier because of the four-version span (and fully custom external layouts with their own paperformat and fonts land there too).

Does Odoo 19 still use wkhtmltopdf?

Yes โ€” reports still render through wkhtmltopdf (the patched-Qt 0.12.6 build). Its CSS quirks are exactly why we verify the rendered PDF, not just the template.