Nút "In" mặc định của Odoo cứ thế xuất PDF, không hỏi gì. Nhưng nhiều lúc bạn cần hỏi trước: in thẻ hạng nào, có kèm lịch sử không, khoảng ngày nào. Chỗ để hỏi là một wizard — hộp thoại thu tham số, rồi từ đó gọi report với đúng những gì người dùng chọn. Bài này dựng một wizard "In thẻ theo hạng" hoàn chỉnh, và vạch ra một điểm dễ sai: tham số bạn truyền vào không xuất hiện trong QWeb dưới cái tên bạn tưởng.
Wizard gọi report thế nào
Wizard là một TransientModel (dữ liệu tạm, tự dọn). Nó có các field để người dùng nhập, và một nút gọi report_action(records, data=...) — hàm này trả về đúng cái action mà nút "In" vẫn dùng, nhưng kèm theo records bạn tự chọn và một data tuỳ ý:

Hình 1: Wizard quan.in.the.wizard thu hai tham số — hạng thẻ và có kèm lịch sử không. action_in lọc đúng các thẻ theo hạng rồi gọi report_action(the_ids, data={'kem_lich_su': ...}). Chú ý phần dưới: report đọc kem_lich_su như một biến rời, không phải data['kem_lich_su'] — lát nữa sẽ rõ vì sao.
Trên giao diện, action của wizard đặt target="new" nên nó bật ra thành hộp thoại:

Hình 2: Hộp thoại wizard thật trong Odoo 19. Người dùng chọn Hạng thẻ (Bạc), bật/tắt In kèm lịch sử tích điểm, và một field tính sẵn Số thẻ khớp cho biết sẽ in bao nhiêu thẻ (ở đây 3). Bấm In PDF là action_in chạy, xuất PDF của cả 3 thẻ hạng Bạc trong một lần.
Field "Số thẻ khớp" là một compute nhỏ chạy search_count theo hạng đang chọn — kiểu phản hồi tức thời này rất đáng thêm vào wizard để người dùng biết trước phạm vi trước khi bấm.
Cái bẫy: data không vào QWeb dưới tên data
Đây là chỗ tôi vấp. Theo thói quen, tôi viết trong template:
<t t-set="kem_ls" t-value="data.get('kem_lich_su', True) if data else True"/>
Và nó luôn hiện lịch sử, dù wizard gửi kem_lich_su=False. Đào vào nguồn ir_actions_report._get_rendering_context mới hiểu: Odoo lấy dict data bạn truyền, cập nhật thêm docs, doc_ids, doc_model vào chính nó, rồi đưa cả dict đó làm bộ biến top-level cho QWeb. Tức là các khoá của data bị trải phẳng thành biến rời — trong template có biến kem_lich_su, chứ không có biến tên data. Biểu thức if data else True của tôi tham chiếu một cái tên không tồn tại, QWeb coi như rỗng → luôn rơi vào nhánh True.
Sửa lại cho đúng là đọc thẳng biến rời, kèm mặc định cho trường hợp in không qua wizard (khi đó biến vắng mặt):
<t t-set="kem_ls" t-value="kem_lich_su if kem_lich_su is not None else True"/>
<div t-if="kem_ls">
<h3>Lịch sử tích điểm</h3> ...
</div>
Đo lại bằng ba cách render, con số khớp mong đợi:
data={'kem_lich_su': True} -> "Lịch sử tích điểm" xuất hiện 3 lần (3 thẻ)
data={'kem_lich_su': False} -> 0 lần (ẩn hẳn)
không truyền data (in thẳng) -> 3 lần (mặc định vẫn in kèm)
Kết quả: cùng thẻ, data quyết định nội dung
Cùng thẻ VIP-0001, chỉ khác giá trị kem_lich_su mà wizard gửi:

Hình 3: Cùng một thẻ, cùng một template — chỉ khác data wizard gửi vào. Bên trái (True) có đầy đủ bảng "Lịch sử tích điểm"; bên phải (False) bảng đó biến mất, tài liệu ngắn lại. Mọi thứ khác (QR, mã vạch, chân trang) giữ nguyên. PDF cũng nhẹ đi tương ứng: 72.024 byte so với 68.410 byte, đo bằng _render_qweb_pdf.
Vài lưu ý khi làm thật
report_action(records, data)nhận cả recordset lẫn data. Recordset quyết định in những bản ghi nào;datalà tham số phụ điều khiển in ra sao. Ở đây records = các thẻ đúng hạng, data = cờ kèm lịch sử.- Chỉ truyền dữ liệu JSON-hoá được trong
data(chuỗi, số, bool, list, dict). Đừng nhét recordset vàodata— recordset đi qua tham sốrecords, còndatađể tuần tự hoá được khi Odoo dựng URL tải PDF. - Chặn trường hợp rỗng bằng
UserError. Chọn hạng không có thẻ nào mà cứ gọi report sẽ ra PDF trắng khó hiểu; báo lỗi rõ ràng tốt hơn. - Nhớ cấp quyền cho model wizard trong
ir.model.access.csv— TransientModel vẫn cần dòng quyền chobase.group_user, thiếu là người dùng mở hộp thoại liền báo lỗi truy cập.
Ba ý mang về
- Wizard
TransientModellà nơi hỏi tham số trước khi in: nút của nó trả vềreport_action(records, data=...)— cùng action như nút In, nhưng bạn tự chọn bản ghi và kèm tham số. dataKHÔNG là một biến trong QWeb — Odoo trải các khoá của nó thành biến top-level. Đọckem_lich_sutrực tiếp, đừngdata.get('kem_lich_su'); và đặt mặc định cho trường hợp in không qua wizard.- Tách vai trò rõ ràng:
recordschọn in gì,data(JSON-hoá được) điều khiển in thế nào; chặn rỗng bằngUserErrorvà nhớ khai quyền cho model wizard.
Wizard vừa in cả 3 thẻ hạng Bạc trong một lần — mà mỗi thẻ lại ra một trang riêng. Làm sao gộp nhiều bản ghi gọn vào một tài liệu, ngắt trang cho đẹp? Phần sau bàn về in nhiều bản ghi vào một file và cách kiểm soát ngắt trang trong QWeb.