Ở phần trước, wizard "In thẻ theo hạng" chọn ra 3 thẻ hạng Bạc và in một phát ra hết. Không phải 3 file, cũng không phải 3 lần bấm — một file PDF duy nhất chứa cả 3 thẻ. Cơ chế gộp này là mặc định của QWeb report, chỉ cần hiểu đúng hai thứ: vòng lặp docs và cái class page. Và một bất ngờ nhỏ khi đo: số trang thường không bằng số bản ghi.
docs là cả recordset, không phải một bản ghi
Khi bạn gọi report._render_qweb_pdf(ref, [1, 47, 46, 48]), Odoo đưa cả bốn bản ghi vào template dưới biến docs. Template chỉ việc lặp qua chúng:

Hình 1: Khung của mọi QWeb report nhiều bản ghi. web.html_container bọc ngoài; t-foreach="docs" t-as="o" lặp qua từng bản ghi; mỗi vòng gọi web.external_layout (để có header/footer) rồi một <div class="page">. Chính class="page" báo cho bộ dựng PDF: đây là một trang in mới. Bốn bản ghi → bốn khối page → nối lại thành một file.
Điểm mấu chốt: bạn không viết vòng lặp trong Python để in từng cái rồi ghép file. Bạn truyền cả recordset, QWeb lặp trong template, và wkhtmltopdf nối mọi khối page thành một tài liệu liền mạch với header/footer nhất quán.
Số trang ≠ số bản ghi
Đây là chỗ dễ tưởng nhầm "4 thẻ thì 4 trang". Đo thật bằng PyPDF2:

Hình 2: Đo bằng PyPDF2. Bốn thẻ nhưng file có 5 trang. Đếm riêng từng thẻ mới thấy: VIP-0001 có nhiều dòng lịch sử nên nội dung tràn sang trang thứ hai, ba thẻ còn lại vừa một trang. Tổng: 2 + 1 + 1 + 1 = 5. Mỗi bản ghi tự nở theo lượng nội dung, wkhtmltopdf tự ngắt trang khi tràn.
Nhìn tận mắt cả 5 trang trong cùng một file:

Hình 3: Toàn bộ file. Trang 1–2 là VIP-0001 (bị tràn), trang 3–5 là ba thẻ còn lại, mỗi thẻ một trang. Để ý header/footer công ty lặp lại nhất quán ở mọi trang — đó là công của web.external_layout bọc trong mỗi vòng lặp. Cả file nặng 119.996 byte, đo bằng _render_qweb_pdf.
Kiểm soát ngắt trang
Mặc định đã tốt, nhưng đôi khi bạn cần chủ động:
- Ép ngắt trang giữa chừng — chèn
<div style="page-break-after: always;"/>ở chỗ muốn sang trang mới. Hữu ích khi một bản ghi có nhiều phần và bạn muốn mỗi phần một trang. - Tránh ngắt ngang một khối — đặt
style="page-break-inside: avoid;"lên một<table>hay<div>để nó không bị cắt đôi qua hai trang. Rất đáng dùng cho bảng nhỏ hoặc chữ ký cuối tài liệu. - Gộp không ngắt giữa các bản ghi — nếu muốn nhiều bản ghi chảy liền thay vì mỗi cái một trang, bỏ
class="page"riêng lẻ đi; nhưng khi đó bạn mất header/footer lặp cho từng cái. Hầu như luôn nên giữpagecho mỗi bản ghi.
Vài lưu ý khi in số lượng lớn
docsgiữ đúng thứ tự bạn truyền. Muốn in theo hạng thẻ hay theo tên thìsearchvớiorderphù hợp trước khi đưa ids vào report — QWeb không tự sắp xếp lại.- In hàng trăm bản ghi tốn thời gian và RAM vì
wkhtmltopdfdựng cả HTML khổng lồ. Nếu người dùng hay in cả nghìn thẻ, cân nhắc chia lô hoặc chạy nền, đừng để nghẽn request. - Mã vạch/QR nhúng dạng ảnh (như bài trước) nhân lên theo số bản ghi — mỗi thẻ một QR base64, file phình nhanh. 4 thẻ đã ~120 KB; con số này tuyến tính theo số trang.
Ba ý mang về
- Truyền cả recordset, QWeb lặp bằng
t-foreach="docs"— không tự ghép file trong Python. Mỗi vòng gói bản ghi trongweb.external_layout+<div class="page">để có một trang in với header/footer nhất quán. - Số trang không bằng số bản ghi: bản ghi dài tự tràn sang trang kế (VIP-0001 chiếm 2 trang), tổng trang = tổng số trang từng bản ghi. Đo bằng
PyPDF2để biết chắc. - Kiểm soát ngắt trang bằng CSS:
page-break-after: alwaysđể ép sang trang,page-break-inside: avoidđể không cắt đôi một khối.
Mỗi thẻ mới chỉ liệt kê lịch sử dạng bảng đơn giản. Phần sau đào sâu vào chính cái bảng đó: Phần sau dựng bảng và dòng tổng trong report — cộng dồn, gộp nhóm, và canh số cho thẳng cột trong QWeb.