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 structureweb.external_layout: Adds company header and footerweb.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
| Problem | Cause | Fix |
|---|---|---|
| PDF is blank | wkhtmltopdf not installed | Install: apt-get install wkhtmltopdf |
| Images missing | wkhtmltopdf cannot reach image URLs | Use data URIs: image_data_uri(record.image) |
| CSS not applied | External stylesheets blocked | Use inline styles or report_assets_common |
| Footer overlaps content | margin_bottom too small | Increase paper format margin_bottom |
| Table split awkwardly | Missing page-break-inside | Add page-break-inside: avoid to rows |
| Slow generation | Large images or complex CSS | Optimize 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_layoutfor professional reports with company branding - Use data URIs (
image_data_uri()) for images — wkhtmltopdf may not fetch URLs - Add
page-break-inside: avoidto 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