GuideMarch 13, 2026

Level Up Your Billing:
Customizing Odoo Invoices

INTRODUCTION

Your Invoice Is the Last Thing Your Customer Reads Before Paying You

Most Odoo implementations treat the invoice template as an afterthought. The default PDF ships with generic layout, missing payment details, and no brand identity. Customers receive a document that looks like it came from a software demo—not from a company they trust with five-figure purchase orders.

The business impact is real. Unclear invoices generate payment delays. Missing bank details force customers to email your AP team for wire instructions. Invoices without PO numbers get rejected by procurement departments that require three-way matching. Every one of these friction points adds days to your DSO (Days Sales Outstanding).

Odoo's invoice system is fully customizable—from QWeb report templates to computed fields, conditional sections, and multi-company layouts. This guide walks you through every layer of invoice customization in Odoo 19: how the QWeb template is structured, how to add custom fields, how to redesign the layout for your brand, and the mistakes that break PDF generation in production.

01

Understanding the Odoo Invoice QWeb Template Architecture

Before customizing anything, you need to understand what you're modifying. Odoo's invoice PDF is rendered by the QWeb templating engine through a chain of templates that inherit from each other. Editing the wrong template—or inheriting at the wrong point—is the #1 cause of broken invoice layouts after upgrades.

The Template Inheritance Chain

The invoice PDF rendering follows this hierarchy:

TemplateModuleResponsibilityCustomize Here?
web.external_layoutwebPage structure: header, footer, CSS, page marginsOnly for global header/footer changes
web.internal_layoutwebCompany logo, address block, page numberingOnly for company-level branding
account.report_invoice_documentaccountInvoice body: lines, totals, taxes, payment termsYes — this is your main target
account.report_invoiceaccountWrapper that loops over invoices for batch printingRarely
Golden Rule

Never edit the base templates directly. Always create an inheriting template in your custom module using t-call or xpath. Direct edits are overwritten on every Odoo update. Inheriting templates survive upgrades because the ORM applies them on top of the base.

Locating the Template in Your Codebase

Bash — Find the invoice template
# Find the base invoice template in Odoo source
grep -r "report_invoice_document" /opt/odoo/odoo/addons/account/report/

# The main file is:
# addons/account/report/account_report_invoice.xml

# To see all templates that inherit from it:
grep -r "inherit_id.*report_invoice_document" /opt/odoo/
02

Adding Custom Fields to Odoo Invoices: PO Numbers, Project Codes, and Payment Instructions

The most common customization request: adding fields to the invoice that aren't there by default. Customer PO numbers, internal project codes, bank wire instructions, early payment discounts, or compliance notices. Here's the complete pattern.

Step 1: Add the Field to the Model

Python — models/account_move.py
from odoo import fields, models


class AccountMove(models.Model):
    _inherit = "account.move"

    x_customer_po = fields.Char(
        string="Customer PO Number",
        copy=True,
        tracking=True,
        help="Purchase order reference from the customer. "
             "Printed on the invoice PDF.",
    )
    x_project_code = fields.Char(
        string="Project Code",
        copy=True,
        help="Internal project code for cost allocation.",
    )
    x_wire_instructions = fields.Text(
        string="Wire Transfer Instructions",
        help="Bank details shown on the invoice PDF. "
             "Falls back to company-level instructions.",
    )

Step 2: Add the Fields to the Invoice Form View

XML — views/account_move_views.xml
<odoo>
  <record id="view_move_form_inherit_custom"
          model="ir.ui.view">
    <field name="name">account.move.form.custom</field>
    <field name="model">account.move</field>
    <field name="inherit_id"
           ref="account.view_move_form"/>
    <field name="arch" type="xml">
      <xpath expr="//field[@name='invoice_date']"
             position="after">
        <field name="x_customer_po"
               placeholder="e.g. PO-2026-00451"
               attrs="{'invisible': [
                 ('move_type', 'not in',
                  ['out_invoice', 'out_refund'])]}" />
        <field name="x_project_code" />
      </xpath>
    </field>
  </record>
</odoo>

Step 3: Display the Fields on the PDF Template

This is where most tutorials stop at the form view. The field exists in the backend but never appears on the PDF. You need a QWeb template inheritance:

XML — report/invoice_report_templates.xml
<odoo>
  <template id="report_invoice_custom_fields"
            inherit_id="account.report_invoice_document">

    <!-- Add PO number and project code in the info box -->
    <xpath expr="//div[@name='reference']" position="after">
      <div t-if="o.x_customer_po" class="col-auto col-3 mw-100 mb-2">
        <strong>Customer PO:</strong>
        <p class="m-0" t-field="o.x_customer_po"/>
      </div>
      <div t-if="o.x_project_code" class="col-auto col-3 mw-100 mb-2">
        <strong>Project Code:</strong>
        <p class="m-0" t-field="o.x_project_code"/>
      </div>
    </xpath>

    <!-- Add wire instructions above the payment terms -->
    <xpath expr="//div[@name='payment_term']" position="before">
      <div t-if="o.x_wire_instructions" class="mt-3">
        <strong>Wire Transfer Instructions:</strong>
        <p t-field="o.x_wire_instructions"
           style="white-space: pre-line;"/>
      </div>
    </xpath>

  </template>
</odoo>

Key patterns in this template:

  • t-if guards — fields only render when they have a value. Empty fields don't leave blank gaps in the PDF.
  • t-field directive — renders the field with proper formatting (dates, currencies, etc.) based on the field type. Never use t-out for model fields on reports—t-field handles i18n and access rights.
  • Bootstrap grid classes — Odoo's report CSS includes Bootstrap 5. Use col-* classes for consistent column layouts.
  • xpath with position="after" — inserts your content after the matched element without replacing anything. Safer than position="replace" which breaks if another module targets the same node.
Don't Forget the Manifest

Your template XML file must be listed under 'data' in __manifest__.py, not 'demo'. Report templates listed under 'demo' only load when demo data is enabled—your production invoices will render without your customizations, and debugging this takes longer than it should.

03

Redesigning the Odoo Invoice Layout: Branding, Colors, and Multi-Company Templates

Adding fields is one thing. Making the invoice look like it belongs to your brand is another. Odoo's default invoice is functional but generic—white background, minimal styling, and a tiny company logo that looks like an afterthought.

Custom CSS for Invoice PDFs

Invoice PDFs are rendered by wkhtmltopdf, which supports a subset of CSS 2.1 plus some CSS3 properties. The key constraint: no flexbox, no grid, no CSS variables. You're limited to floats, tables, and absolute positioning. Here's how to add branded styling:

XML — report/invoice_report_style.xml
<odoo>
  <template id="report_invoice_style"
            inherit_id="web.report_assets_common">
    <xpath expr="." position="inside">
      <link rel="stylesheet"
            href="/my_module/static/src/css/invoice.css"/>
    </xpath>
  </template>
</odoo>
CSS — static/src/css/invoice.css
/* ── Brand Colors ── */
.o_report_layout .header-left {
    border-left: 4px solid #1a5c2e;  /* Your brand green */
}

/* ── Invoice Title Bar ── */
.o_report_layout h2.mt-4 {
    background-color: #1a5c2e;
    color: #ffffff;
    padding: 8px 16px;
    font-size: 14pt;
    margin-bottom: 16px;
}

/* ── Table Header Row ── */
.o_report_layout thead th {
    background-color: #f0f7f2;
    border-bottom: 2px solid #1a5c2e;
    font-size: 9pt;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* ── Totals Section ── */
.o_report_layout #total table {
    border-top: 2px solid #1a5c2e;
}
.o_report_layout #total .fw-bold {
    font-size: 11pt;
    color: #1a5c2e;
}

/* ── Footer ── */
.o_report_layout .footer {
    font-size: 7pt;
    color: #666666;
    border-top: 1px solid #cccccc;
    padding-top: 8px;
}

Multi-Company Invoice Templates

If you operate multiple companies (subsidiaries, brands, or regional entities), each needs its own invoice layout. Odoo supports this through the external layout selector and company-specific report paperformats:

XML — Multi-company layout selection
<odoo>
  <!-- Custom external layout for Brand B -->
  <template id="external_layout_brand_b">
    <div class="header brand-b-header">
      <img t-att-src="'/my_module/static/img/brand_b_logo.png'"
           style="max-height: 60px;"/>
      <div class="company-address"
           t-field="o.company_id.partner_id"
           t-options='{"widget": "contact",
             "fields": ["address", "phone", "email"]}'/>
    </div>
    <t t-out="0"/>
    <div class="footer brand-b-footer">
      <span t-field="o.company_id.name"/> |
      VAT: <span t-field="o.company_id.vat"/> |
      <span t-field="o.company_id.company_registry"/>
    </div>
  </template>

  <!-- Register it as an available layout option -->
  <record id="action_report_invoice_brand_b"
          model="ir.actions.report">
    <field name="name">Invoice (Brand B)</field>
    <field name="model">account.move</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">
      my_module.report_invoice_brand_b</field>
    <field name="paperformat_id"
           ref="my_module.paperformat_brand_b"/>
  </record>
</odoo>

Conditional Content Based on Invoice Type

Different invoice types often need different content. A credit note should display different legal text than a standard invoice. A proforma invoice might need a "NOT A TAX INVOICE" watermark:

XML — Conditional sections
<!-- Legal notice for credit notes -->
<div t-if="o.move_type == 'out_refund'"
     class="alert alert-info mt-3">
  <strong>Credit Note:</strong> This document
  reduces the amount of invoice
  <span t-if="o.reversed_entry_id"
        t-field="o.reversed_entry_id.name"/>.
</div>

<!-- Proforma watermark -->
<div t-if="o.state == 'draft'"
     style="position: fixed; top: 40%; left: 20%;
            transform: rotate(-35deg);
            font-size: 72pt; color: rgba(0,0,0,0.06);
            z-index: -1;">
  PROFORMA
</div>

<!-- Early payment discount -->
<div t-if="o.invoice_payment_term_id.discount_percentage"
     class="mt-2 p-2"
     style="background: #f0f7f2; border-left: 3px solid #1a5c2e;">
  <strong>Early Payment Discount:</strong>
  <span t-out="o.invoice_payment_term_id.discount_percentage"/>%
  discount if paid within
  <span t-out="o.invoice_payment_term_id.discount_days"/> days.
</div>
Paper Format Matters

If your invoices will be printed on pre-printed letterhead (common in Europe), you need a custom ir.report.paperformat with adjusted margins. The default margins assume a blank page. Pre-printed letterhead typically needs 25mm top margin (for the logo area) and 20mm bottom margin (for the footer strip). Getting this wrong means your content overlaps the pre-printed elements.

04

3 Invoice Customization Mistakes That Break PDF Generation in Production

1

Using Flexbox or CSS Grid in Invoice Templates

The invoice PDF looks perfect in your browser's print preview. Then the actual PDF arrives with all elements stacked vertically, overlapping, or missing entirely. The reason: Odoo uses wkhtmltopdf (a WebKit-based renderer from 2020) for PDF generation. It supports CSS 2.1 and partial CSS3—but not flexbox, not CSS grid, and not CSS variables. Every display: flex in your stylesheet is silently ignored in the PDF.

Our Fix

Use Bootstrap's row/col-* grid (which uses floats in the report context), <table> layouts for complex alignments, and display: inline-block for side-by-side elements. Always test with the actual wkhtmltopdf binary, not browser print preview. Run wkhtmltopdf --version to confirm you're on 0.12.6 or 0.12.7.

2

xpath Targeting That Breaks When Other Modules Inherit the Same Template

You write an xpath expression like expr="//table/tbody/tr[last()]" to add a row at the bottom of the invoice lines. It works in your dev environment. Then the client installs sale_stock or a third-party module that also inherits report_invoice_document and adds rows. Your tr[last()] now targets their row, not yours. The result: content appears in the wrong position, or worse, both modules break each other.

Our Fix

Use named anchors in your xpath expressions: target elements with name attributes (expr="//div[@name='payment_term']") or unique IDs instead of positional selectors. Odoo's base templates provide many named anchors for exactly this reason. If no anchor exists, use position="inside" on a parent element rather than fragile positional tr[last()] patterns.

3

Forgetting to Handle Multi-Currency and Localization

Your custom invoice template hardcodes $ symbols or uses t-out with manual formatting for monetary values. It looks fine for your US-based test company. Then a Canadian customer receives an invoice where the currency symbol is wrong, the decimal separator is a period instead of a comma, and the tax label says "Tax" instead of "TPS/TVQ." Hardcoded formatting breaks the moment your invoices cross a border.

Our Fix

Always use t-field with t-options='{"widget": "monetary"}' for currency values. This respects the invoice's currency, the customer's locale, and the company's decimal precision settings. For tax labels, use the tax_id.description or tax_id.name fields instead of hardcoded strings. For dates, use t-options='{"widget": "date"}' to render in the customer's locale.

BUSINESS ROI

What Professional Invoice Customization Saves Your Business

Invoice customization isn't a cosmetic project. It's a cash flow optimization:

5-8 daysFaster Payment

Clear invoices with bank details, PO references, and payment terms visible at a glance reduce back-and-forth and accelerate payment cycles.

60%Fewer Invoice Queries

When customers can find PO numbers, project codes, and wire instructions on the invoice itself, they stop emailing your finance team.

100%Brand Consistency

Every customer touchpoint reinforces your brand. A professional invoice signals operational maturity—the kind that wins enterprise contracts.

For a company sending 500 invoices per month, reducing average DSO by just 5 days on a $10,000 average invoice value frees up $833,000 in working capital. That's not a nice-to-have—it's a CFO-level win from a one-time template customization.

SEO NOTES

Optimization Metadata

Meta Desc

Step-by-step guide to customizing Odoo invoices. Add custom fields, brand the PDF layout, handle multi-currency, and avoid wkhtmltopdf pitfalls.

H2 Keywords

1. "Understanding the Odoo Invoice QWeb Template Architecture"
2. "Adding Custom Fields to Odoo Invoices: PO Numbers, Project Codes, and Payment Instructions"
3. "3 Invoice Customization Mistakes That Break PDF Generation in Production"

Your Invoice Should Work as Hard as Your Sales Team

A well-designed invoice does more than request payment. It reinforces your brand, reduces friction in your customer's AP workflow, and eliminates the back-and-forth emails that delay cash collection. Every missing PO number, every unclear payment instruction, and every generic layout is costing you days of DSO and hours of finance team productivity.

If your Odoo invoices still look like the default template, we should fix that. We design and implement custom invoice templates that match your brand, include the fields your customers need, and survive Odoo upgrades without breaking. The project typically takes 2-3 days and the ROI shows up in your next cash flow report.

Book a Free Invoice Audit