Ở 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.

Ảnh chụp đoạn mã JavaScript nền tối minh hoạ cách gọi ORM từ JS, lấy orm bằng useService trong setup, gọi searchRead với model quan chấm the chấm thanh chấm vien cùng domain lọc hang_the bằng vang và danh sách field name và diem, gọi searchCount chỉ để đếm số bản ghi, ghi chú Odoo 19 đã bỏ readGroup nên phải dùng formattedReadGroup với thứ tự tham số model domain groupby aggregates để gộp nhóm và tính tổng ngay trên server, và lưu ý mọi lời gọi đều trả Promise nên phải await

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ê fields cầ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ới searchRead rồ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):

Ảnh chụp console trình duyệt DevTools nền tối hiển thị ba dòng kết quả thật, dòng đầu searchRead vang trả về một mảng gồm một bản ghi id 47 name VIP-0003 diem 150 tức đúng một thẻ Vàng và chỉ đúng hai field name với diem như đã yêu cầu, dòng hai searchCount trả về số 4 là tổng số thẻ, dòng ba formattedReadGroup theo hang_the trả về mảng hai nhóm hạng bạc tổng điểm 235 và hạng vàng tổng điểm 150 cộng lại đúng 385

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:

Ảnh chụp thật bảng điều khiển Thẻ thành viên của Odoo 19 với ba thẻ số liệu 4 thẻ 385 điểm 1 thẻ Vàng, một nút Chạy thử ORM từ JS màu tím, và ở góc trên bên phải một toast thông báo viền xanh dương với tiêu đề Kết quả ORM cùng nội dung Có 4 thẻ 1 thẻ Và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ề

  1. orm service là ORM của Odoo gọi từ JS: searchRead/read/searchCount/create/write/unlink và call (gọi method Python bất kỳ). Mọi phương thức trả Promise — luôn await trong hàm async.
  2. 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 khai fields để không kéo thừa. Cần con số thì searchCount nhẹ hơn; cần gộp nhóm thì formattedReadGroup.
  3. Odoo 19 đã bỏ readGroup → dùng formattedReadGroup(model, domain, groupby, aggregates) với groupby đứng trước aggregates. 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.