Ở bài trước ta biết component OWL mượn chức năng qua useService, và orm là service hay đụng nhất. Bài này mổ vào chính nó: gọi ORM của Odoo ngay từ JavaScript trên trình duyệt, đọc dữ liệu thật, và một cái bẫy rất dễ vấp khi lên Odoo 19.
orm service là cái gì, và vì sao không tự fetch
Trên trình duyệt, muốn lấy dữ liệu từ server bạn có thể tự gọi fetch("/web/dataset/..."). Nhưng làm vậy là tự lo session, CSRF token, định dạng tham số JSON-RPC, và tự bắt lỗi. orm service gói tất cả lại: mỗi phương thức của nó tương ứng một thao tác ORM quen thuộc từ phía Python, chỉ khác là gọi từ JS và trả về Promise.

Hình 1: Ba phương thức đọc hay dùng của orm. searchRead(model, domain, fields) gộp search + read trong một lần đi server, chỉ kéo về đúng field khai. searchCount(model, domain) chỉ trả về một con số. formattedReadGroup(model, domain, groupby, aggregates) gộp nhóm và tính tổng — chú ý thứ tự tham số.
Điểm mấu chốt: mọi phương thức đều bất đồng bộ, trả Promise, nên phải await (và hàm chứa nó phải là async). Bỏ await là bạn cầm một Promise chưa hoàn thành thay vì dữ liệu.
Những phương thức đọc hay dùng
searchRead(model, domain, fields)— thứ dùng nhiều nhất. Một vòng đi server cho cả tìm lẫn đọc. Luôn liệt kêfieldscần, đừng để trống mà kéo về mọi cột.read(model, ids, fields)— khi đã có sẵn danh sách id.searchCount(model, domain)— chỉ cần con số, không cần dữ liệu. Nhẹ hơn nhiều so vớisearchReadrồi đếm.length.formattedReadGroup(model, domain, groupby, aggregates)— gộp nhóm + tính tổng/đếm ngay trên server.call(model, method, args, kwargs)— gọi bất kỳ method nào của model, kể cả method nghiệp vụ bạn tự viết bằng Python. Đây là cửa để JS chạm tới logic server tuỳ ý.
Ghi (create, write, unlink) cũng có, dùng đúng như bên Python nhưng nhớ await.
Chạy thật trên dashboard
Mình gắn một nút "Chạy thử ORM từ JS" vào bảng điều khiển của module demo quan_ca_phe, cho nó gọi ba phương thức đọc rồi console.log kết quả:
async chayThuOrm() {
const dsVang = await this.orm.searchRead(
"quan.the.thanh.vien", [["hang_the", "=", "vang"]], ["name", "diem"]);
console.log("[ORM] searchRead vang ->", JSON.stringify(dsVang));
const soThe = await this.orm.searchCount("quan.the.thanh.vien", []);
console.log("[ORM] searchCount ->", soThe);
const nhom = await this.orm.formattedReadGroup(
"quan.the.thanh.vien", [], ["hang_the"], ["diem:sum"]);
console.log("[ORM] formattedReadGroup theo hang_the ->", JSON.stringify(nhom));
this.notification.add(`Có ${soThe} thẻ, ${dsVang.length} thẻ Vàng`,
{ type: "info", title: "Kết quả ORM" });
}
Bấm nút, đây là những gì thật sự in ra console trình duyệt (mình bắt bằng page.on('console') của Playwright, không gõ tay):

Hình 2: Kết quả thật in ra console. searchRead trả về đúng một thẻ Vàng (VIP-0003, 150 điểm) và chỉ có hai field đã khai. searchCount trả về 4. formattedReadGroup gộp theo hạng thẻ: bạc 235 điểm, vàng 150 điểm — cộng lại đúng 385, khớp con số "Tổng điểm" trên dashboard.
Và cái toast notification.add(...) với con số thật (4 thẻ, 1 thẻ Vàng) — cả hai đều đến từ ORM chứ không phải viết cứng:

Hình 3: Nút chạy xong, số liệu trong toast (4 thẻ, 1 thẻ Vàng) do chính searchCount và searchRead tính ra ngay trong trình duyệt.
Cái bẫy Odoo 19: readGroup không còn nữa
Bản đầu tiên mình viết this.orm.readGroup(...) như quen tay từ các phiên bản cũ. Trình duyệt ném ngay:
TypeError: this.orm.readGroup is not a function
at QuanDashboard.chayThuOrm (...web.assets_web.min.js)
Nhìn vào source của orm_service.js trong Odoo 19 thì rõ: readGroup đã bị bỏ, thay bằng formattedReadGroup (và webReadGroup). Không chỉ đổi tên — thứ tự tham số cũng khác:
// Odoo cũ: readGroup(model, domain, fields, groupby)
// Odoo 19: formattedReadGroup(model, domain, groupby, aggregates)
groupby giờ đứng trước aggregates. Viết theo trí nhớ cũ mà không đọc chữ ký mới thì hoặc lỗi "not a function", hoặc tệ hơn: gọi đúng tên nhưng nhét sai vị trí tham số và nhận kết quả gộp sai mà không có lỗi nào bật lên. Đây đúng là kiểu lỗi mà bài học về OWL nhắc — lỗi JS chỉ hiện ở trình duyệt, log server không hề có, phải mở console (hoặc bắt pageerror) mới thấy.
Khi nào rơi xuống rpc
orm phủ gần hết nhu cầu dữ liệu. Nhưng nếu bạn cần gọi một route HTTP tuỳ ý (một controller @http.route tự viết trả JSON, không phải thao tác trên model), thì orm không có cửa cho việc đó — lúc ấy mới dùng rpc service. Đó là chủ đề của phần sau.
Ba ý mang về
ormservice là ORM của Odoo gọi từ JS:searchRead/read/searchCount/create/write/unlinkvàcall(gọi method Python bất kỳ). Mọi phương thức trả Promise — luônawaittrong hàmasync.searchRead(model, domain, fields)là con dao hay dùng nhất — một vòng đi server cho cả tìm và đọc; luôn khaifieldsđể không kéo thừa. Cần con số thìsearchCountnhẹ hơn; cần gộp nhóm thìformattedReadGroup.- Odoo 19 đã bỏ
readGroup→ dùngformattedReadGroup(model, domain, groupby, aggregates)vớigroupbyđứng trướcaggregates. Lỗi kiểu này chỉ hiện ở console trình duyệt, không có trong log server.
Còn khi orm không đủ — bạn cần gọi thẳng một controller tự viết — thì sao? Phần sau nói về rpc service: gọi một route HTTP tuỳ ý từ JavaScript và nhận về JSON.