Este artigo está atualmente disponível apenas em inglês. Tradução em breve.
Odoo prints PDF reports with wkhtmltopdf, which ignores the CSS3 break-before: page. Put page-break-before: always on the element that starts the new page, keep it inside the report's page wrapper, and use page-break-inside: avoid on a table that fits on one page.
You add <div class="page-break"/> to an Odoo report. The PDF renders the content but ignores the break — content flows continuously across what should be page boundaries. Or the inverse: every section forces a new page, including ones that should fit. wkhtmltopdf and web.external_layout interact in subtle ways that make page breaks one of the most asked-about parts of Odoo report design on 17.0/18.0/19.0.
Quick Fix
Use the CSS page-break-before or page-break-after properties, not the <div class="page-break"/> shortcut:
<!-- Before each major section -->
<div style="page-break-before: always"/>
For conditional page breaks (only break if too little space remains), use page-break-inside:
<table style="page-break-inside: avoid">
<!-- table contents stay on one page if possible -->
</table>
Why This Happens
wkhtmltopdf supports CSS print media queries and the page-break-* family of properties, but with quirks:
<div class="page-break"/>is an Odoo convention — Odoo'sweb.external_layoutstyles map this topage-break-before: always. If your template wraps the page-break div incorrectly (outside the<div class="page">wrapper), the style does not apply.page-break-beforeon the first element in a wrapper does nothing — the renderer is already at a page boundary.- Tables and page breaks.
page-break-inside: avoidon a table works only if the table fits on a page; tables larger than one page break wherever fits. - Float and absolute positioning mess with page breaks. Floats can flow off the bottom of one page and reappear at the top of another in unexpected places.
- wkhtmltopdf's outdated WebKit does not support
break-before: page(the modern CSS3 syntax) — onlypage-break-before: always(the legacy syntax).
Step-by-Step Diagnosis
1. Render to HTML to inspect:
http://localhost:8069/report/html/sale.report_saleorder/14?debug=1
In the browser DevTools, look at where page-break-* styles are computed. If the style is not always, the rule did not match.
2. Check wkhtmltopdf's print CSS support.
echo '<html><body><div style="page-break-before: always">Page 2</div></body></html>' > test.html
wkhtmltopdf --print-media-type test.html out.pdf
If the PDF has only one page, wkhtmltopdf is ignoring the break. Try --enable-internal-links and other flags.
3. Inspect Odoo's rendered template. Find where <div class="page-break"/> appears in the composed HTML. It should be a sibling of <div class="page">, not nested inside.
4. Check CSS specificity. A more specific rule may be overriding your page-break:
.row > div { page-break-before: avoid; } /* defeats your "always" rule */
5. Test with a minimum reproduction. A QWeb template with three <div class="page"> wrappers, one page-break between each, and only "Hello" text in each. If this works, the issue is CSS in the rest of your template.
Permanent Fix
For unconditional breaks between sections:
<template id="report_priority_document">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<h2>Order: <span t-field="o.name"/></h2>
<!-- First-page content -->
<table>...</table>
</div>
</t>
<!-- Force a page break -->
<div style="page-break-before: always"/>
<t t-call="web.external_layout">
<div class="page">
<h2>Continuation: <span t-field="o.name"/></h2>
<!-- Second-page content -->
</div>
</t>
</t>
</t>
</template>
The page break sits between two <t t-call="web.external_layout"> blocks, each rendering its own page header/footer.
For conditional breaks (keep table together if possible):
<table class="table" style="page-break-inside: avoid">
<thead>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
</thead>
<tbody>
<tr t-foreach="o.order_line" t-as="line">
<td><span t-field="line.product_id.name"/></td>
<td><span t-field="line.product_uom_qty"/></td>
<td><span t-field="line.price_unit"/></td>
</tr>
</tbody>
</table>
page-break-inside: avoid keeps the table on one page if it fits.
For row-level breaks (break before this row):
<tr style="page-break-before: always" t-if="line.is_section_break">
<td colspan="3"><b><span t-field="line.name"/></b></td>
</tr>
For headers that should repeat on each page:
<table>
<thead style="display: table-header-group">
<tr><th>Item</th><th>Qty</th></tr>
</thead>
<tbody>
<!-- rows -->
</tbody>
</table>
display: table-header-group makes the <thead> repeat on every page wkhtmltopdf renders for the table.
For complex flows, use <div class="page"> per logical page. Odoo's external_layout wrapping each <div class="page"> gives you per-page header/footer plus implicit page boundaries. This is the most reliable structure.
How to Prevent It
- Always test with multi-page data. Reports with one item fit on one page; you only see page-break bugs when there are 50 items. QA with realistic record counts.
- Avoid
<div class="page-break"/>shorthand. Use explicitstyle="page-break-before: always". The shorthand depends on Odoo's CSS, which can change between versions. - Use legacy
page-break-*syntax. wkhtmltopdf 0.12.6 does not supportbreak-before: page(CSS3). Stick withpage-break-before: always. - One
<div class="page">per logical page. If your report has an order summary page and an order detail page, two<div class="page">wrappers is the cleanest structure. - CI test page count. A test that generates a PDF and asserts page count using
pdfinfoor PyPDF catches break regressions. - Print-stylesheet review. When inheriting
web.external_layoutstyles, ensure your CSS does not accidentally setpage-break-before: avoidon elements that should break.
Related Errors
- PDF report renders blank page — adjacent rendering bug.
- wkhtmltopdf protocol error — sibling toolchain failure.
- Report translations missing — content-level report bug.
- QWeb template not found at runtime — pre-render failure.
Frequently Asked Questions
Can I number pages in an Odoo report?
Yes, in web.external_layout footer:
<div class="footer">
Page <span class="page"/> of <span class="topage"/>
</div>
wkhtmltopdf replaces class="page" with the current page and class="topage" with the total at PDF generation time. Standard wkhtmltopdf magic.
Why does page-break-after: always get ignored on the last element?
Because there is no content after it to push to a new page. The renderer correctly drops the break. If you need an empty trailing page (rare), add a hidden div with content after the break.
Can I use modern CSS like break-before: page?
Not safely. wkhtmltopdf 0.12.6's WebKit is from 2014 and does not support the CSS3 fragmentation properties consistently. Use page-break-* syntax for predictable behaviour.
My table breaks in the middle of a row. How do I fix it?
Add style="page-break-inside: avoid" to the <tr>. If rows are too tall to ever fit on a single page, this falls back to row-internal breaks anyway. For uniformity, set the rule on tr globally in your stylesheet:
@media print { tr { page-break-inside: avoid; } }
Can I force a specific page count, like always 3 pages?
Not directly through CSS. The closest pattern is to author three explicit <div class="page"> wrappers and pad them with empty space if needed. For form-style documents (purchase orders, invoices) where you need a fixed structure, this manual approach is more reliable than computed.
How do I add a watermark across all pages?
Use CSS position: fixed with @media print:
.watermark {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%) rotate(-30deg);
font-size: 4em; opacity: 0.1; color: red;
z-index: 9999;
}
wkhtmltopdf renders fixed-position elements once per page, exactly the watermark behaviour you want. Inline this CSS in the report's QWeb to keep it self-contained.
Page numbering shows wrong total — why?
<span class="topage"/> is replaced with the total page count after wkhtmltopdf finishes laying out. If your template includes table-of-contents-style placeholders that change page count after replacement, totals drift. Run with --javascript-delay 1000 to give wkhtmltopdf time to settle the layout before computing totals.
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
Escrito por
ECOSIRE Research and Development TeamRedação técnica
A equipe de redação técnica da ECOSIRE escreve sobre Odoo ERP, e-commerce com Shopify, agentes de IA, análise de dados com Power BI, automação com GoHighLevel e boas práticas de software empresarial. Nossos guias ajudam as empresas a tomar decisões de tecnologia bem fundamentadas.
ECOSIRE
Transforme seu negócio com o Odoo ERP
Implementação, personalização e suporte especializado do Odoo para agilizar suas operações.
Artigos Relacionados
Segmentação de clientes baseada em IA: do RFM ao clustering preditivo
Saiba como a IA transforma a segmentação de clientes, desde a análise estática de RFM até o clustering preditivo dinâmico. Guia de implementação com dados Python, Odoo e ROI real.
IA para otimização da cadeia de suprimentos: visibilidade, previsão e automação
Transforme as operações da cadeia de suprimentos com IA: detecção de demanda, pontuação de risco de fornecedores, otimização de rotas, automação de armazéns e previsão de interrupções. Guia 2026.
Guia de ecommerce B2B de atacado: portais, prazos de pagamento e preços por cliente
Monte um portal de atacado B2B que os compradores realmente usam: preços por cliente, prazos e limites de crédito, checkout com pedido de compra e aprovações, e recompras pelo histórico.