Suốt loạt bài front-end vừa rồi ta ở trong trình duyệt. Giờ lùi về phía server, nhưng vẫn là "web": làm sao để một URL như https://quan/quan_ca_phe/thong_ke gọi tới một hàm Python của bạn và trả về đúng thứ bạn muốn? Câu trả lời là http.Controller và trình trang trí @http.route. Đây là cách Odoo lộ ra một webhook, một API, một trang tuỳ biến — bất cứ thứ gì cần một địa chỉ web mà không phải là một model/view thông thường. Bài này dựng một controller thật trong module quan_ca_phe, rồi curl lấy kết quả sống để thấy chính xác nó hoạt động ra sao trên Odoo 19.
Controller là gì
Một controller là một class kế thừa http.Controller; mỗi phương thức được @http.route(...) trang trí sẽ ứng với một (hoặc nhiều) đường dẫn URL. Odoo quét các class này lúc nạp module và tự dựng bảng định tuyến. Đặt file ở controllers/main.py, khai from . import controllers trong __init__.py của module — thế là xong.

Hình 1: Một controller, ba route. Route 1 trả JSON qua request.make_response; route 2 trả thẳng HTML; route 3 là kiểu jsonrpc. Chú ý cách khai type, auth, methods, csrf trong @http.route — mục dưới bóc từng cái.
Mã đầy đủ (rút gọn phần import json):
import json
from odoo import http
from odoo.http import request
class QuanCaPheController(http.Controller):
@http.route('/quan_ca_phe/thong_ke', type='http', auth='public',
methods=['GET'], csrf=False)
def thong_ke(self, **kwargs):
The = request.env['quan.the.thanh.vien'].sudo()
du_lieu = {'so_the': The.search_count([]),
'tong_diem': sum(The.search([]).mapped('diem')),
'nguoi_goi': request.env.user.name}
return request.make_response(
json.dumps(du_lieu, ensure_ascii=False),
headers=[('Content-Type', 'application/json; charset=utf-8')])
type: http hay jsonrpc
Tham số quan trọng nhất là type, quyết định tìm tham số ở đâu và tuần tự hoá đáp án ra sao:
type='http'— request web thường. Tham số lấy từ query string / form; hàm trả về một chuỗi, mộtResponse, hoặc một template render. Bạn tự quyết định định dạng đầu ra.type='jsonrpc'— giao thức JSON-RPC 2.0 mà front-end OWL dùng để gọi server. Tham số nằm trong khốiparamscủa thân JSON; đáp án được tự động bọc trong phong bì{"jsonrpc": "2.0", "result": ...}.
Điểm mới Odoo 19: kiểu này giờ tên là
'jsonrpc'. Cú pháp cũtype='json'vẫn chạy nhưng đã bị đánh dấu deprecated — lõihttp.pyghi rõ "Since 19.0, @route(type='json') is a deprecated alias to @route(type='jsonrpc')" và tự đổi sang'jsonrpc'kèm một cảnh báo. Code mới nên viết thẳng'jsonrpc'.
Curl thật hai route type='http' cho thấy chúng trả về đúng thứ hàm dựng, không bọc gì thêm:

Hình 2: Hai route type='http' chạy thật. /thong_ke trả JSON {"so_the": 4, "tong_diem": 385, "nguoi_goi": "Public user"} (4 thẻ, tổng 385 điểm — số thật trong CSDL). /xin_chao?ten=An cho thấy tham số URL tự bind vào đối số hàm: ?ten=An → ten='An'. Vì auth='public', chưa đăng nhập vẫn chạy, và request.env.user là "Public user".
Route type='jsonrpc' thì khác hẳn ở đáp án — luôn nằm trong phong bì JSON-RPC:

Hình 3: Route type='jsonrpc' với auth='user'. Chưa đăng nhập → server trả phong bì lỗi {"error": {"code": 100, "message": "Odoo Session Expired"}} — auth='user' chặn đúng. Đăng nhập rồi → {"result": {"so_the": 1}} (1 thẻ hạng "vang"). Cả kết quả lẫn lỗi đều được bọc trong phong bì JSON-RPC, khác với type='http' trả nội dung thô.
auth: ai được gọi
auth quyết định điều kiện thực thi, lấy đúng từ docstring lõi Odoo 19:
auth='user'— bắt buộc đã đăng nhập; chạy với quyền của chính người đó. Chưa đăng nhập →SessionExpiredException(như Hình 3).auth='public'— đăng nhập hay không đều được; nếu chưa, chạy bằng "Public user" dùng chung.auth='none'— luôn hoạt động, kể cả khi không có database. Dành cho hạ tầng/khung xác thực.auth='bearer'— (mới ở Odoo 19) xác thực bằng headerAuthorization: Bearer <api_token>; chạy với quyền của người sở hữu token. Tiện cho API máy-gọi-máy.
csrf và request
csrf — mặc định bật cho type='http', tắt cho type='jsonrpc'. Với route http nhận POST từ form web, cứ để bật để chống giả mạo request; với một endpoint API (như /thong_ke chỉ đọc, hoặc webhook bên ngoài gọi vào) thì đặt csrf=False.
Trong thân hàm, request (nhập từ odoo.http) là cửa ngõ tới mọi thứ:
request.env— environment ORM, gọi model như trong code server thường (request.env['quan.the.thanh.vien']).request.params— tham số đã gộp (query + form + body JSON).request.httprequest— đối tượng request thô của Werkzeug (headers, cookies, method).request.make_response/request.render/request.redirect— dựng đáp án: response tuỳ biến, render một QWeb template, hay chuyển hướng.
Ba ý mang về
http.Controller+@http.routebiến một hàm Python thành một URL — đặt ởcontrollers/main.py, khaifrom . import controllers; không cần model hay view.typequyết định định dạng:'http'trả nội dung thô (chuỗi/Response/template),'jsonrpc'bọc trong phong bì JSON-RPC. Odoo 19 đổi tên'json'cũ thành'jsonrpc'(cũ vẫn chạy nhưng deprecated).authchốt ai được gọi (user/public/none/bearermới);csrfmặc định bật cho http, tắt cho jsonrpc; mọi thứ trong hàm đi quarequest(.env,.params,.httprequest,.make_response).
Ta vừa lướt qua auth, nhưng nó đáng một bài riêng vì chọn sai là mở toang hoặc khoá chặt nhầm. Phần sau đào sâu auth: public, user, none — mỗi mức cho code truy cập tới đâu, "Public user" thực chất là ai, và cạm bẫy hay gặp khi để nhầm mức.