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ẻ:

Ảnh chụp hai đoạn mã nền tối. Đoạn trên là file controllers main chấm py định nghĩa lớp QuanCaPheController kế thừa http chấm Controller, có decorator http chấm route với đường dẫn quan_ca_phe thong_ke type jsonrpc auth user, method thong_ke tìm mọi bản ghi quan chấm the chấm thanh chấm vien rồi trả về một dict gồm so_the là số lượng, tong_diem là tổng điểm, và nguoi_goi là request env user name tức tên người đang đăng nhập. Đoạn dưới là dashboard js ghi chú Odoo 19 rpc là hàm import thẳng không phải useService, import rpc từ web core network rpc, và method goiController await rpc với đường dẫn quan_ca_phe thong_ke rồi console log kết quả

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):

Ảnh chụp console trình duyệt nền tối hiển thị một dòng RPC tới đường dẫn quan_ca_phe thong_ke trả về đối tượng JSON gồm so_the bằng 4, tong_diem bằng 385, so_vang bằng 1, nguoi_goi bằng Administrator, kèm ghi chú rằng ba số đầu do controller tự tính từ recordset trên server còn nguoi_goi bằng Administrator chứng tỏ route chạy dưới đúng user đăng nhập nhờ auth user

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 đó:

Ả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 Gọi controller qua RPC màu tím, và ở góc trên bên phải một toast xanh lá tiêu đề RPC OK với nội dung Controller trả về 4 thẻ 385 điểm người gọi Administrator

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 orm khi 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 rpc khi 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ự lo auth, 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ề

  1. rpc gọi thẳng một route HTTP tự viết và nhận JSON, dùng khi orm không phủ được (logic riêng, cấu trúc trả về tự định nghĩa). Route là method của http.Controller gắn @http.route, chạy dưới request.env của user.
  2. Odoo 19 đổi tên kiểu route: type='json' → type='jsonrpc'. Lõi web không còn dùng 'json' ở đâu; khai theo đúng bản 19.
  3. rpc không còn là service ở Odoo 19 — useService("rpc") ném "Service rpc is not available" và giết component. Phải import { 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.