Style một report tưởng đơn giản: viết SCSS, khai vào bundle, xong. Odoo có hẳn một bundle riêng cho bản in — web.report_assets_common. Tôi làm đúng bài: tạo file SCSS, đăng ký, gắn class. Rồi in ra... y như cũ, chẳng có gì đổi. Không lỗi, không cảnh báo. Bài này đi từ cách chuẩn đến cái bẫy khiến nó lặng lẽ không ăn, và cách làm chắc chắn ăn ở mọi môi trường.
Cách chuẩn: SCSS vào bundle report
Report của Odoo dùng các bundle asset riêng, tách khỏi giao diện backend: web.report_assets_common (áp cho cả bản HTML lẫn PDF) và web.report_assets_pdf (chỉ PDF). Muốn thêm style cho report, bạn khai SCSS vào các bundle này trong __manifest__.py, rồi dùng class thay cho style="..." rải rác:

Hình 1: Cách chuẩn (trên) — SCSS gom vào web.report_assets_common, dùng class .o_the_document để style tập trung thay vì nhét style vào từng thẻ. Đây là cách nên dùng khi nhiều report chia sẻ chung một bộ style.
Về mặt tổ chức code, cách này đẹp: style tách khỏi cấu trúc, tái dùng được, dễ bảo trì. Nhưng khi tôi nâng cấp module và in ra, bản PDF không đổi gì.
Cái bẫy: bundle vào PDF qua một link ngoài
Đào vào HTML mà Odoo sinh ra để đưa cho wkhtmltopdf, lý do lộ ngay:

Hình 2: Bundle report được nhúng vào PDF dưới dạng <link rel="stylesheet" href="/web/assets/.../web.report_assets_common.min.css"> — một file ngoài, không phải CSS nội tuyến ('<style' in html trả về False). Để áp được, wkhtmltopdf phải gọi ngược HTTP về máy chủ Odoo lấy file đó. Với workers = 0 (mặc định khi phát triển), tiến trình duy nhất đang bận dựng PDF nên không ai phục vụ request CSS → style rơi mất. Đúng cùng gốc rễ với bẫy nhúng barcode bằng URL.
Nói cách khác: trên production nhiều worker thì bundle chạy tốt, nhưng trong môi trường dev một tiến trình, style biến mất mà không báo gì. Rất dễ ngồi sửa SCSS hàng giờ mà không hiểu vì sao không ăn.
Cách chắc ăn mọi nơi: <style> nội tuyến
wkhtmltopdf đọc thẳng thẻ <style> nằm trong HTML — không cần fetch gì. Nên với style riêng của một report mà bạn muốn chắc chắn hiển thị ở mọi môi trường, đặt luôn một khối <style> trong template:
<div class="page o_the_document">
<style>
.o_the_document h2 { border-bottom: 3px double #6f4e37; padding-bottom: 4px; }
.o_the_document table.table thead th {
background-color: #f3e9df; color: #6f4e37;
}
.o_the_document table.table tbody tr:nth-child(even) td {
background-color: #faf6f0;
}
</style>
... nội dung ...
</div>
Nâng cấp lại, in ra — lần này style ăn ngay:

Hình 3: Cùng thẻ, cùng dữ liệu. Bên trái chưa style; bên phải khối <style> nội tuyến đã ăn: tiêu đề gạch chân đôi, đầu bảng nền kem, và các hàng chẵn tô nền nhạt (sọc zebra bằng :nth-child(even)). Không cần fetch file ngoài nên render ở đâu cũng ra đúng.
Giới hạn của WebKit trong wkhtmltopdf
Một điều nữa phải nhớ: wkhtmltopdf chạy trên một bản WebKit rất cũ. Nên khi style report:
- Flexbox và CSS Grid không đáng tin. Muốn chia cột, dùng
<table>hoặcfloat/Bootstrap grid (row/col-*) — những thứ WebKit cũ hiểu chắc. Đừng dựng layout bản in bằngdisplay:flex. :nth-child,border,background,paddingđều ổn — như sọc zebra ở trên chạy tốt.- Class Bootstrap có sẵn trong report (
text-end,table,table-sm,mt-3...), tận dụng trước khi tự viết CSS. - Style nội tuyến
style="..."luôn ăn vì nó nằm ngay trong thẻ, không phụ thuộc fetch. Dùng cho những chỉnh sửa nhỏ, lẻ.
Ba ý mang về
- Bundle
web.report_assets_commonlà cách chuẩn để chia sẻ CSS/SCSS cho nhiều report — khai trong manifestassets, dùng class tập trung. - Nhưng bundle vào PDF qua
<link>ngoài màwkhtmltopdfphải fetch; vớiworkers=0(dev) nó rơi mất lặng lẽ. Cách chắc ăn mọi nơi: đặt khối<style>nội tuyến trong template. - wkhtmltopdf là WebKit cũ: tránh flexbox/grid, dùng
table/float/Bootstrap grid;border/background/:nth-child/style nội tuyến đều chạy tốt.
Xong phần report, ta rời QWeb để quay lại tầng action — nơi quyết định người dùng thấy gì khi mở một menu. Phần sau mổ xẻ context và domain mặc định của action — cách một menu lọc sẵn dữ liệu và đặt sẵn giá trị cho bản ghi mới.