Bài trước ta gọi ORM từ JS bằng orm service — đủ cho đọc/ghi bản ghi. Nhưng khi bạn cần một logic riêng trên server (tính toán phức tạp, trả về một cấu trúc JSON tự định nghĩa, gọi API ngoài rồi trộn kết quả), orm không có cửa. Lúc đó bạn tự viết một controller với route riêng, và gọi nó từ JS bằng rpc. Bài này làm đúng chuyện đó — và vấp đúng hai thay đổi của Odoo 19 mà nếu viết theo trí nhớ cũ sẽ hỏng.
Controller: một route trả JSON
Trong Python, một route là một method của lớp con http.Controller, gắn decorator @http.route. Mình thêm vào module demo quan_ca_phe một package controllers/ với một route trả về thống kê thẻ:

Hình 1: Controller trả JSON (trên) và JS gọi nó (dưới). Route chạy dưới request.env của user đang đăng nhập (nhờ auth='user'), nên request.env.user.name cho biết ai gọi. search([]) và mapped('diem') là ORM Python bình thường — đây chính là chỗ bạn nhét logic tuỳ ý mà orm service phía JS không làm được.
Để Odoo nạp package mới, phải khai from . import controllers trong __init__.py của module, rồi nâng cấp với -u.
Cái bẫy thứ nhất: type='json' đã thành 'jsonrpc'
Ở các phiên bản Odoo cũ, route JSON khai type='json'. Trên Odoo 19, tra trong lõi web thì không còn dòng nào dùng type='json' — tất cả đã đổi sang type='jsonrpc'. Khai sai tên kiểu route thì route hoặc không nhận request đúng cách, hoặc lỗi khó lần. Cứ theo đúng lõi 19: type='jsonrpc'.
Cái bẫy thứ hai: rpc không còn là service
Bản đầu tiên mình viết theo phản xạ cũ: this.rpc = useService("rpc"). Mở dashboard lên, cả màn hình đỏ lòm với "Rất tiếc, đã xảy ra lỗi". Bắt console trình duyệt ra thì thấy nguyên nhân:
OwlError: An error occured in the owl lifecycle
Caused by: Error: Service rpc is not available
at useService (...)
at QuanDashboard.setup (...)
Ở Odoo 19, rpc không còn được đăng ký như một service — useService("rpc") ném "Service rpc is not available" ngay trong setup(), làm cả component chết ở khâu khởi tạo. Nhìn vào lõi thì rpc giờ là một hàm thường xuất khẩu trực tiếp:
// web/static/src/core/network/rpc.js
export function rpc(url, params = {}, settings = {}) { ... }
Nên cách đúng là import thẳng và gọi như một hàm, không qua useService:
import { rpc } from "@web/core/network/rpc";
async goiController() {
const kq = await rpc("/quan_ca_phe/thong_ke");
// ...
}
Đây lại là một minh hoạ cho bài học xuyên suốt loạt OWL: lỗi JS chỉ hiện ở trình duyệt. Log server sạch bong, -u chạy không một dòng cảnh báo — chỉ khi mở màn hình thật (hoặc bắt pageerror bằng Playwright) bạn mới thấy nó.
Chạy thật: JS gọi controller, nhận JSON
Sửa xong hai chỗ, gắn một nút "Gọi controller qua RPC". Bấm nút, đây là JSON thật mà route trả về (bắt từ console trình duyệt):

Hình 2: JSON thật từ controller: {"so_the":4,"tong_diem":385,"so_vang":1,"nguoi_goi":"Administrator"}. Ba con số do Python tính trên server; nguoi_goi = "Administrator" chứng minh route chạy dưới đúng phiên đăng nhập — thứ mà auth='user' bảo đảm.
Và cái toast dựng từ chính JSON đó:

Hình 3: Toast dựng từ JSON của controller — 4 thẻ, 385 điểm, người gọi Administrator. Toàn bộ đường đi: nút OWL → rpc(...) → route Python @http.route(type='jsonrpc') → request.env truy vấn → JSON về lại JS.
orm hay rpc?
Ranh giới đơn giản:
- Dùng
ormkhi việc là CRUD hay gọi một method có sẵn trên model (searchRead,write,call...). Nó là cửa chuẩn, gọn, an toàn. - Dùng
rpckhi bạn cần một route riêng trả về cấu trúc tự định nghĩa, hoặc gộp nhiều thao tác/logic mà không muốn phơi ra thành method model. Đổi lại, bạn tự loauth, tự định hình dữ liệu trả về, và tự chịu trách nhiệm bảo mật route đó.
Đa số màn hình chỉ cần orm. rpc là công cụ cho những điểm chạm đặc thù — nhưng khi cần thì không gì thay được.
Ba ý mang về
rpcgọi thẳng một route HTTP tự viết và nhận JSON, dùng khiormkhông phủ được (logic riêng, cấu trúc trả về tự định nghĩa). Route là method củahttp.Controllergắn@http.route, chạy dướirequest.envcủa user.- Odoo 19 đổi tên kiểu route:
type='json'→type='jsonrpc'. Lõiwebkhông còn dùng'json'ở đâu; khai theo đúng bản 19. rpckhông còn là service ở Odoo 19 —useService("rpc")ném "Service rpc is not available" và giết component. Phảiimport { rpc } from "@web/core/network/rpc"rồi gọi như một hàm. Cả hai lỗi này chỉ lộ ở console trình duyệt.
Ta đã dùng registry.category("actions").add(...) để đăng ký client action mà chưa mổ kỹ. Phần sau đào vào registry — cơ chế Odoo dùng để đăng ký component, field widget, view, và mọi mảnh ghép mở rộng của web client.