本文目前仅提供英文版本。翻译即将推出。
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"/>
<!-- Or the modern equivalent -->
<div style="break-before: page"/>
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.
作者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
相关文章
如何将自定义按钮添加到 Odoo 表单视图 (2026)
将自定义操作按钮添加到 Odoo 19 表单视图:Python 操作方法、视图继承、条件可见性、确认对话框。经过生产测试。
如何在没有 Studio 的情况下在 Odoo 中添加自定义字段 (2026)
通过 Odoo 19 中的自定义模块添加自定义字段:模型继承、视图扩展、计算字段、存储/非存储决策。代码优先,版本控制。
如何使用外部布局在 Odoo 中添加自定义报告
使用 web.external_layout 在 Odoo 19 中构建品牌 PDF 报告:QWeb 模板、paperformat、操作绑定。带有印刷徽标+页脚覆盖。