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:

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êntong_cuoi), cònt-fieldđể in giá trị của một field (nó tự định dạng theo kiểu field, ví dụ ngày radd/mm/yyyy). Đừng lẫn:t-field="tong_cuoi"sẽ lỗi vìtong_cuoikhô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.colspancho 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:

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:

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ề
- 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 đượct-field. - Cộng dồn trong QWeb bằng
t-settích luỹ: khai biến= 0ngoài vòng lặp, mỗi vòng ghi lạit-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. t-outin giá trị đã tính,t-fieldin giá trị field (tự định dạng); canh phải cột số bằngclass="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.