Skip to content

Odoo PDF Generation and Customization: Beyond the Basics

DeployMonkey Team · March 23, 2026 11 min read

How Odoo Generates PDFs

Odoo uses wkhtmltopdf to convert QWeb-rendered HTML into PDF files. The pipeline is: QWeb template renders HTML with CSS, wkhtmltopdf converts HTML to PDF, and the result is served as a download or attachment. Understanding this pipeline is key to fixing PDF layout issues.

Custom Paper Formats

Define custom paper sizes for specific reports:

<record id="paper_format_label" model="report.paperformat">
    <field name="name">Product Label (4x6)</field>
    <field name="format">custom</field>
    <field name="page_width">101.6</field>
    <field name="page_height">152.4</field>
    <field name="margin_top">5</field>
    <field name="margin_bottom">5</field>
    <field name="margin_left">5</field>
    <field name="margin_right">5</field>
    <field name="header_spacing">0</field>
    <field name="orientation">Portrait</field>
    <field name="dpi">90</field>
</record>

<record id="action_report_product_label" model="ir.actions.report">
    <field name="name">Product Label</field>
    <field name="model">product.template</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">my_module.report_product_label</field>
    <field name="paperformat_id" ref="paper_format_label"/>
</record>

Report Template Structure

<template id="report_product_label">
    <t t-call="web.html_container">
        <t t-foreach="docs" t-as="doc">
            <t t-call="web.external_layout">
                <div class="page">
                    <!-- Report content goes here -->
                    <h2 t-field="doc.name"/>
                </div>
            </t>
        </t>
    </t>
</template>

Key layout wrappers:

  • web.html_container: Sets up the HTML document structure
  • web.external_layout: Adds company header and footer
  • web.internal_layout: Simpler layout without company header

Controlling Page Breaks

<!-- Force page break -->
<div style="page-break-before: always;"/>

<!-- Avoid breaking inside an element -->
<div style="page-break-inside: avoid;">
    <h3>Section Title</h3>
    <table>...</table>
</div>

<!-- Break between records (multi-record reports) -->
<t t-foreach="docs" t-as="doc">
    <div class="page">
        <!-- Each doc gets its own page -->
    </div>
    <!-- Odoo auto-inserts page break between .page divs -->
</t>

Custom Header and Footer

Override the company layout for specific reports:

<template id="report_custom_header" inherit_id="web.external_layout">
    <xpath expr="//div[hasclass('header')]" position="replace">
        <div class="header">
            <div class="row">
                <div class="col-6">
                    <img t-if="company.logo" t-att-src="image_data_uri(company.logo)"
                         style="max-height: 45px;"/>
                </div>
                <div class="col-6 text-end">
                    <span t-esc="company.name"/><br/>
                    <small t-esc="company.phone"/>
                </div>
            </div>
        </div>
    </xpath>
</template>

CSS for Print

<template id="report_assets" inherit_id="web.report_assets_common">
    <xpath expr="." position="inside">
        <style>
            .page {
                font-family: 'Helvetica Neue', Arial, sans-serif;
                font-size: 10pt;
                line-height: 1.4;
            }
            table.report-table {
                width: 100%;
                border-collapse: collapse;
            }
            table.report-table th {
                background-color: #f5f5f5;
                border-bottom: 2px solid #333;
                padding: 6px 8px;
                text-align: left;
            }
            table.report-table td {
                border-bottom: 1px solid #ddd;
                padding: 4px 8px;
            }
            .watermark {
                position: fixed;
                top: 40%;
                left: 20%;
                font-size: 80pt;
                color: rgba(200, 200, 200, 0.3);
                transform: rotate(-45deg);
                z-index: -1;
            }
        </style>
    </xpath>
</template>

Watermarks

<div class="page">
    <t t-if="doc.state == 'draft'">
        <div class="watermark">DRAFT</div>
    </t>
    <!-- Report content -->
</div>

Dynamic Content with Report Models

Use an AbstractModel to prepare report data:

class ReportSaleDetailed(models.AbstractModel):
    _name = 'report.my_module.report_sale_detailed'
    _description = 'Detailed Sale Report'

    def _get_report_values(self, docids, data=None):
        orders = self.env['sale.order'].browse(docids)
        totals = {
            'amount': sum(orders.mapped('amount_total')),
            'tax': sum(orders.mapped('amount_tax')),
            'lines': sum(len(o.order_line) for o in orders),
        }
        return {
            'doc_ids': docids,
            'doc_model': 'sale.order',
            'docs': orders,
            'totals': totals,
            'company': self.env.company,
        }

wkhtmltopdf Troubleshooting

ProblemCauseFix
PDF is blankwkhtmltopdf not installedInstall: apt-get install wkhtmltopdf
Images missingwkhtmltopdf cannot reach image URLsUse data URIs: image_data_uri(record.image)
CSS not appliedExternal stylesheets blockedUse inline styles or report_assets_common
Footer overlaps contentmargin_bottom too smallIncrease paper format margin_bottom
Table split awkwardlyMissing page-break-insideAdd page-break-inside: avoid to rows
Slow generationLarge images or complex CSSOptimize images, simplify CSS, increase timeout

Generating PDFs Programmatically

# Generate PDF from code
report = self.env.ref('my_module.action_report_sale_detailed')
pdf_content, content_type = report._render_qweb_pdf(report.id, [order.id])

# Attach the PDF to a record
attachment = self.env['ir.attachment'].create({
    'name': f'{order.name}.pdf',
    'type': 'binary',
    'datas': base64.b64encode(pdf_content),
    'res_model': 'sale.order',
    'res_id': order.id,
    'mimetype': 'application/pdf',
})

# Send PDF via email
order.message_post(
    body='Report attached.',
    attachment_ids=[attachment.id],
)

Best Practices

  • Use web.external_layout for professional reports with company branding
  • Use data URIs (image_data_uri()) for images — wkhtmltopdf may not fetch URLs
  • Add page-break-inside: avoid to keep table rows and sections together
  • Test with multiple records to verify page break behavior
  • Use _get_report_values() for complex data preparation
  • Keep CSS simple — wkhtmltopdf's rendering engine is limited compared to modern browsers