Ở 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:

Ảnh chụp đoạn mã QWeb nền tối template report_the_document, mở bằng t t-call web.html_container, bên trong t t-foreach docs t-as o chú thích docs là tất cả bản ghi được truyền vào, rồi t t-call web.external_layout, bên trong là div class page chú thích class page là một trang in cho mỗi bản ghi chứa h2 Thẻ thành viên với span t-field o name và nội dung của bản ghi o, cuối ảnh có chú thích gọi render_qweb_pdf với danh sách bốn id cho ra một file năm trang và cách ép ngắt trang thủ công bằng div style page-break-after always

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:

Ảnh chụp phiên odoo shell nền tối, lấy ids bằng The search rỗng ra danh sách 1 47 46 48 là bốn thẻ, render_qweb_pdf với ids rồi đếm len PdfReader pages ra 5 nghĩa là bốn thẻ nhưng năm trang, sau đó lặp từng thẻ đếm trang riêng cho thấy CARD 1 VIP-0001 pages 2 vì nhiều lịch sử nên tràn hai trang, CARD 47 VIP-0003 pages 1, CARD 46 VIP-0002 pages 1, CARD 48 VIP-0004 pages 1, tổng lại hai cộng một cộng một cộng một bằng năm

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:

Ảnh dàn năm trang PDF thật cạnh nhau trên nền kem, tiêu đề ghi một lệnh in một file PDF bốn thẻ thành năm trang VIP-0001 tràn sang hai trang; Trang 1 là VIP-0001 phần đầu với thông tin mã QR mã vạch và bảng lịch sử; Trang 2 là phần đuôi của VIP-0001 gồm khối chính sách và lời cảm ơn; Trang 3 là thẻ VIP-0003 hạng Vàng; Trang 4 là thẻ VIP-0002; Trang 5 là thẻ VIP-0004; mỗi trang đều có chân trang thông tin công ty giống nhau

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ữ page cho mỗi bản ghi.

Vài lưu ý khi in số lượng lớn

  • docs giữ đúng thứ tự bạn truyền. Muốn in theo hạng thẻ hay theo tên thì search với order phù 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ì wkhtmltopdf dự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ề

  1. 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 trong web.external_layout + <div class="page"> để có một trang in với header/footer nhất quán.
  2. 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.
  3. 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.