Một bảng liệt kê thì dễ: cứ t-foreach là ra các dòng. Nhưng báo cáo thật luôn cần thêm phần khó hơn: cộng dồn thành một dòng tổng, gộp nhóm rồi ra subtotal mỗi nhóm. Mà QWeb thì không có hàm SUM, không có GROUP BY. Bài này dựng một bảng sao kê điểm gộp theo tháng — mỗi tháng một subtotal, dưới cùng một dòng TỔNG CỘNG — và chỉ ra hai kỹ thuật làm nên nó: gộp nhóm ở Python và tích luỹ bằng t-set trong QWeb.

Gộp nhóm: làm ở model, không làm ở template

QWeb cố tình không mạnh về xử lý dữ liệu — đó là việc của Python. Nên phần gộp lịch sử theo tháng tôi viết thành một method trên model, trả về cấu trúc mà template chỉ việc lặp:

Ảnh chụp đoạn mã nền tối hai phần, phần trên là method Python sao_ke_theo_thang trên model gọi ensure_one khởi tạo nhom là dict rỗng và thu_tu là list rỗng, lặp qua lich_su_ids đã sorted theo ngay lấy key là ngay strftime tháng trên năm, nếu key chưa có thì gán recordset rỗng và append vào thu_tu rồi hợp ls vào nhom key, cuối cùng return list dict gồm thang dong và tong bằng sum của mapped diem_cong; phần dưới là QWeb khai t-set tong_cuoi bằng 0 rồi t-foreach o sao_ke_theo_thang t-as nhom in dòng tiêu đề Tháng, lặp nhom dong in ngày và diem_cong canh phải, một dòng Cộng tháng in nhom tong, rồi t-set tong_cuoi bằng tong_cuoi cộng nhom tong, ngoài vòng lặp là dòng TỔNG CỘNG in tong_cuoi

Hình 1: sao_ke_theo_thang gom các dòng lịch sử theo khoá MM/YYYY, giữ đúng thứ tự tháng bằng list thu_tu (dict thường không đảm bảo thứ tự chèn theo cách ta muốn hiển thị), và trả về list dict {'thang', 'dong', 'tong'}. Mỗi phần tử đã có sẵn subtotal tong = sum(...). Template chỉ còn việc trình bày.

Chú ý cách gom: nhom[key] |= ls — hợp recordset bằng toán tử |, giữ nguyên kiểu recordset để template vẫn dùng được t-field cho từng dòng. Và subtotal tính bằng sum(nhom[k].mapped('diem_cong')) — một dòng Python gọn, thay cho cả vòng lặp cộng tay.

Tích luỹ trong QWeb: một biến t-set sống qua vòng lặp

Đây là mẹo cốt lõi. QWeb không có SUM, nhưng t-set chia sẻ một không gian tên — biến khai ngoài vòng lặp vẫn được các vòng lặp bên trong đọc và ghi đè. Nên dòng TỔNG CỘNG làm được thế này:

<t t-set="tong_cuoi" t-value="0"/>              <!-- khởi tạo NGOÀI vòng lặp -->
<t t-foreach="o.sao_ke_theo_thang()" t-as="nhom">
    ... in các dòng của tháng ...
    <t t-set="tong_cuoi" t-value="tong_cuoi + nhom['tong']"/>  <!-- cộng dồn -->
</t>
<tr><td class="text-end"><strong>TỔNG CỘNG</strong></td>
    <td class="text-end"><strong><t t-out="tong_cuoi"/></strong></td></tr>

Mỗi vòng, tong_cuoi được ghi lại bằng chính nó cộng thêm subtotal của tháng. Ra khỏi vòng lặp, tong_cuoi giữ tổng của mọi tháng. Đây cũng đúng là cách bảng lịch sử gốc của module cộng điểm từng dòng — chỉ khác là ở đây ta cộng theo nhóm.

Vài chi tiết trình bày đáng nhớ:

  • t-out để in giá trị đã tính (số nguyên tong_cuoi), còn t-field để in giá trị của một field (nó tự định dạng theo kiểu field, ví dụ ngày ra dd/mm/yyyy). Đừng lẫn: t-field="tong_cuoi" sẽ lỗi vì tong_cuoi không phải field.
  • class="text-end" (Bootstrap) canh phải cột số — bảng số mà canh trái thì rất khó đọc.
  • colspan cho dòng tiêu đề nhóm trải hết bề ngang.

Kết quả: số khớp từ shell tới PDF

Đo bằng odoo shell trước để chắc method trả đúng, rồi mới tin bản in:

Ảnh chụp phiên odoo shell nền tối, lấy thẻ browse 1, lặp qua sao_ke_theo_thang in ra ba dòng 07 trên 2026 có 2 dòng tổng 35 với chú thích 20 cộng 15, 08 trên 2026 có 1 dòng tổng 35, 09 trên 2026 có 3 dòng tổng 120 với chú thích 40 cộng 50 cộng 30, rồi sum các tong trả về 190 kèm chú thích khớp TỔNG CỘNG trên PDF

Hình 2: Kiểm ở shell: tháng 07 cộng được 35, tháng 08 được 35, tháng 09 được 120, tổng ba tháng là 190. Có con số kỳ vọng rồi mới nhìn PDF — đó là cách chắc chắn bảng in đúng chứ không phải trông "có vẻ đúng".

Và bản in ra khớp từng dòng:

Ảnh chụp trang PDF sao kê điểm VIP-0001 khổ A5, dòng khách hàng Nguyễn Văn An, bảng có tiêu đề cột Ngày Điểm cộng Ghi chú; nhóm Tháng 07 trên 2026 gồm hai dòng 05 và 20 tháng 7 cộng 20 và 15 rồi Cộng tháng 07 trên 2026 là 35; nhóm Tháng 08 trên 2026 một dòng 12 tháng 8 cộng 35 rồi Cộng tháng 08 là 35; nhóm Tháng 09 trên 2026 ba dòng cộng 40 50 30 rồi Cộng tháng 09 là 120; dưới cùng dòng TỔNG CỘNG in đậm 190; các cột điểm canh phải; chân trang thông tin công ty

Hình 3: Sao kê in ra. Ba nhóm tháng, mỗi nhóm có dòng "Cộng tháng" riêng, và dòng TỔNG CỘNG 190 đúng bằng con số đo ở shell. Cột "Điểm cộng" canh phải nên các số thẳng hàng, dễ dò. (Các dòng tháng 7–8 là dữ liệu demo tôi thêm để có nhiều nhóm, đã xoá sau khi đo — thẻ trở lại 3 dòng gốc.)

Ba ý mang về

  1. Gộp nhóm làm ở model, không ở template: một method Python trả về list dict {'thang', 'dong', 'tong'} với subtotal tính sẵn; QWeb chỉ trình bày. Giữ recordset bằng |= để vẫn dùng được t-field.
  2. Cộng dồn trong QWeb bằng t-set tích luỹ: khai biến = 0 ngoài vòng lặp, mỗi vòng ghi lại t-value="bien + phan_moi"; ra khỏi lặp là có tổng. QWeb chia sẻ một không gian tên nên biến sống xuyên vòng.
  3. t-out in giá trị đã tính, t-field in giá trị field (tự định dạng); canh phải cột số bằng class="text-end" để bảng dễ đọc.

Bảng đã biết cộng và nhóm. Nhưng nhiều lúc ta còn muốn giấu hẳn một phần khi không có dữ liệu, hoặc đổi cách hiển thị theo trạng thái. Phần sau đi vào ẩn/hiện phần theo điều kiện trong report — t-if, t-elif, và cách tránh in ra khoảng trống vô nghĩa.