Câu hỏi kinh doanh nào cũng dạng "tổng/trung bình/số lượng theo nhóm": doanh thu theo tháng, số đơn theo trạng thái, khách theo thành phố. Nếu bạn search hết bản ghi rồi tự cộng trong Python, vừa chậm vừa tốn RAM. Odoo có phương thức làm việc đó ngay tại cơ sở dữ liệu: read_group — chính là GROUP BY của SQL, gói trong ORM. Đây là nền tảng của mọi báo cáo, biểu đồ, pivot trong Odoo.

Cú pháp: domain + fields + groupby

Cú pháp read_group trong Odoo 19 gom nhóm và tổng hợp ngay tại cơ sở dữ liệu giống GROUP BY của SQL tính tổng đếm trung bình mà không nạp từng bản ghi: Model.read_group nhận domain lọc trước khi gom, fields là danh sách như amount_total hai chấm sum để tính SUM có thể avg min max count, groupby là trường để gom ví dụ state, lazy bằng False để gom hết không lười; trả về list dict mỗi dict là một nhóm ví dụ state sale __count 9 amount_total 99289600, state draft __count 5 amount_total 1387100, trong đó __count có sẵn còn các hàm tổng hợp viết field hai chấm sum hoặc avg max min; gom nhiều tầng groupby state user_id gom hai tầng, groupby date_order hai chấm month gom theo tháng có thể day week month year

Hình 1: read_group(domain, fields, groupby, lazy). domain lọc trước khi gom (như đã học). fields khai hàm tổng hợp dạng 'trường:sum' (hoặc :avg, :max, :min). groupby là trường để gom. Trả về list dict, mỗi dict là một nhóm, luôn có __count (số bản ghi trong nhóm).

Chạy thật — thấy sức mạnh

Đây là output thật trên db blog19:

Output read_group chạy thật trên database blog19: gom res.partner theo thành phố đếm mỗi nhóm, P.read_group với city groupby city cho city False __count 12 là nhóm chưa có thành phố, city Hà Nội __count 4, city New York __count 4, city Hồ Chí Minh __count 2; gom theo is_company cho is_company False __count 24 và is_company True __count 6; SUM thật sale.order 15 đơn gom theo trạng thái và tổng tiền, SO.read_group với amount_total sum groupby state cho state draft count 5 sum 1.387.100, state sale count 9 sum 99.289.600, state sent count 1 sum 2; ghi chú một truy vấn SQL GROUP BY không nạp 15 recordset để tự cộng

Hình 2: Output thật. Gom res.partner theo city cho số lượng từng thành phố (chú ý nhóm city=False = 12 gộp các bản ghi chưa có thành phố). Gom theo is_company: 24 cá nhân, 6 công ty. Và phần mạnh nhất: gom 15 đơn sale.order theo state kèm SUM amount_total — sale: 9 đơn, tổng 99.289.600; draft: 5 đơn, 1.387.100. Tất cả bằng một truy vấn GROUP BY, không nạp 15 recordset về cộng tay.

Các hàm tổng hợp

Khai trong fields với hậu tố sau dấu hai chấm:

  • :sum — tổng (doanh thu, số lượng).
  • :avg — trung bình (giá trị đơn trung bình).
  • :max / :min — lớn nhất / nhỏ nhất.
  • __count — số bản ghi mỗi nhóm (tự có, không cần khai).
env['sale.order'].read_group(
    [('state', '=', 'sale')],           # chỉ đơn đã xác nhận
    fields=['amount_total:sum', 'amount_total:avg'],
    groupby=['user_id'],                # theo nhân viên bán
)

Gom nhiều tầng và theo thời gian

groupby nhận list nhiều trường để gom lồng, và hỗ trợ mốc thời gian trên trường ngày:

# Doanh thu theo tháng
groupby=['date_order:month']            # cũng có :day, :week, :quarter, :year

# Gom hai tầng: theo trạng thái, rồi theo nhân viên
groupby=['state', 'user_id']

Gom theo :month chính là cách Odoo dựng biểu đồ doanh thu theo thời gian mà bạn thấy trên dashboard.

lazy=True vs lazy=False

Mặc định lazy=True: Odoo chỉ gom theo trường đầu tiên trong groupby, các tầng sau để "lười" tính khi cần (phục vụ giao diện gấp/mở nhóm). Khi bạn cần kết quả gom đầy đủ ngay (báo cáo, xử lý dữ liệu), truyền lazy=False để gom hết mọi tầng một lần — như các ví dụ ở Hình 2.

Vì sao đừng tự gom trong Python

# ✗ Cách chậm: nạp hết rồi tự cộng
tong = sum(o.amount_total for o in env['sale.order'].search([]))

# ✓ Cách đúng: CSDL cộng, chỉ trả về con số
res = env['sale.order'].read_group([], ['amount_total:sum'], [])
tong = res[0]['amount_total']

Cách đầu nạp mọi đơn thành recordset (đắt, tốn RAM); cách sau để PostgreSQL cộng và chỉ trả một con số. Với bảng lớn, khác biệt là một trời một vực — chính là bài học hiệu năng xuyên suốt create, write, search: để CSDL làm việc của CSDL.

Nhớ ba ý

  1. read_group là GROUP BY của Odoo: gom nhóm + tính sum/avg/max/min/count ngay tại CSDL, trả về list dict mỗi nhóm (luôn có __count).
  2. Khai hàm tổng hợp dạng 'trường:sum'; groupby nhận nhiều tầng và mốc thời gian (date_order:month) — nền của báo cáo, biểu đồ, pivot.
  3. Đừng search hết rồi tự cộng trong Python — dùng read_group (với lazy=False khi cần kết quả đầy đủ) để CSDL làm việc nặng.

Xong bộ CRUD đọc–tạo–sửa–tổng hợp, còn thao tác cuối: xóa. Phần sau đi vào unlink() và ràng buộc khóa ngoại — vì sao xóa một bản ghi đôi khi bị chặn, và cách xử lý đúng.