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.

Ảnh chụp mã Python nền tối file controllers main.py với ba kiểu route. Đầu file nhập http từ odoo và request từ odoo http. Class QuanCaPheController kế thừa http Controller. Route thứ nhất trang trí http route đường dẫn quan_ca_phe thong_ke type http auth public methods GET csrf False, hàm thong_ke lấy model quan the thanh vien sudo, dựng dict gồm so_the bằng search_count, tong_diem bằng tổng mapped diem, nguoi_goi bằng request env user name, rồi trả về request make_response với json dumps và header Content-Type application json. Route thứ hai http route quan_ca_phe xin_chao type http auth public, hàm xin_chao nhận tham số ten mặc định khách trả về chuỗi HTML h1 Xin chào. Route thứ ba http route quan_ca_phe rpc_diem type jsonrpc auth user, hàm rpc_diem nhận hang trả về dict so_the bằng search_count theo miền lọc hang_the

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ột Response, 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ối params củ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õi http.py ghi 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:

Ảnh chụp terminal nền tối curl thật tới route type http trên Odoo 19 với dữ liệu blog19 có 4 thẻ tổng điểm 385. Lệnh curl đường dẫn quan_ca_phe thong_ke route JSON auth public trả về chuỗi JSON so_the 4 tong_diem 385 nguoi_goi Public user, mã 200 Content-Type application json charset utf-8. Lệnh curl thứ hai quan_ca_phe xin_chao với tham số ten bằng An trả về HTML h1 Xin chào An từ quán cà phê, mã 200. Chú thích auth public nghĩa chưa đăng nhập vẫn chạy và request env user là Public user

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:

Ảnh chụp terminal nền tối route type jsonrpc auth user cùng một URL nhưng khác nhau ở đăng nhập. Lệnh POST đầu tới quan_ca_phe rpc_diem với params hang vang khi CHƯA đăng nhập trả về JSON có error code 100 message Odoo Session Expired, chú thích auth user chặn. Lệnh POST thứ hai cùng URL params hang vang khi ĐÃ đăng nhập admin trả về JSON jsonrpc 2.0 id null result so_the 1. Chú thích jsonrpc bọc kết quả trong result lỗi trong error khác http trả thô

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 header Authorization: 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ề

  1. http.Controller + @http.route biến một hàm Python thành một URL — đặt ở controllers/main.py, khai from . import controllers; không cần model hay view.
  2. type quyế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).
  3. auth chốt ai được gọi (user / public / none / bearer mới); csrf mặc định bật cho http, tắt cho jsonrpc; mọi thứ trong hàm đi qua request (.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.