Một component OWL, tự thân nó, chẳng biết làm gì nhiều ngoài vẽ ra HTML từ state. Nó không biết cách gọi server, không biết cách bật một cái toast lên góc màn hình, không biết cách mở một view khác. Tất cả những việc đó là chức năng dùng chung của cả web client, và Odoo gói chúng thành service. Component muốn dùng thì mượn service về bằng một hook duy nhất: useService.

Ở bài trước ta đã bắt được cú click của người dùng bằng t-on-click. Nhưng khi cú click đó xảy ra, ta cần làm gì đó có ích — gọi server, báo cho người dùng, chuyển màn hình. Bài này nói về nơi những hành động đó đến từ đâu.

Service là gì, và vì sao không tự làm

Hãy nghĩ về những việc mà mọi màn hình trong Odoo đều cần: đọc/ghi bản ghi, hiện thông báo, mở view, gọi một route HTTP. Nếu mỗi component tự viết lại logic gọi server — tự dựng URL, tự xử lý session, tự bắt lỗi mạng — thì vừa lặp code kinh khủng, vừa mỗi nơi làm một kiểu.

Thay vào đó Odoo dựng sẵn các service: mỗi service là một đối tượng chức năng dùng chung, khởi tạo một lần cho cả web client. Component chỉ việc mượn nó về trong setup():

Ảnh chụp đoạn mã JavaScript nền tối minh hoạ file dashboard.js, import useService từ web core utils hooks, trong hàm setup gán this.orm bằng useService orm để CRUD với server, this.notification bằng useService notification để hiện toast, this.action bằng useService action để mở view hay report, một phương thức baoTin gọi notification.add với type success và title Thành công, một phương thức moDanhSach gọi action.doAction với external id action_the_thanh_vien

Hình 1: useService("tên") trả về một service, ta gán vào this trong setup() để các phương thức khác dùng. useService là một hook — bắt buộc gọi trong setup(), không gọi lung tung giữa phương thức khác. Ba service hay dùng nhất: orm (CRUD với server), notification (toast), action (mở view/report).

Vì sao gọi trong setup() chứ không phải chỗ khác? Vì useService là một hook — giống useState, onWillStart — nó móc vào vòng đời của component. Gọi ngoài setup() là gọi hook sai chỗ, OWL sẽ ném lỗi. Lấy về một lần trong setup(), gán vào this, rồi mọi phương thức dùng lại.

Bản đồ vài service hay gặp

Sơ đồ nền tối liệt kê năm service OWL hay dùng, mỗi dòng một khối màu riêng: useService orm để CRUD với server gồm searchRead read create write unlink và call gọi method của model, useService notification chấm add nhận msg và type hiện toast góc phải với type success warning hoặc danger, useService action chấm doAction nhận xmlid hoặc dict mở một view wizard hay report như menu bấm tay, useService dialog chấm add nhận Component và props mở hộp thoại chứa một component OWL khác, useService rpc nhận route và params gọi thẳng một route HTTP của server khi không có sẵn service

Hình 2: Năm service hay gặp nhất. orm gần như bài nào cũng đụng tới; notification và action là hai thứ hay dùng sau mỗi thao tác của người dùng; dialog để bật hộp thoại; rpc là cửa sau khi không có service chuyên biệt.

Điểm chung: lấy service bằng useService, rồi gọi phương thức của nó. orm.searchRead(...), notification.add(...), action.doAction(...). Bản thân service lo hết phần khó bên dưới (session, CSRF, xử lý lỗi mạng), component chỉ gọi một dòng.

Thử thật: thêm hai nút vào dashboard

Nói lý thuyết thế đủ rồi. Module demo quan_ca_phe có sẵn một client action là bảng điều khiển thẻ thành viên (viết bằng OWL, lấy số liệu qua orm). Mình thêm vào đó hai nút để thử notification và action:

setup() {
    this.orm = useService("orm");
    this.notification = useService("notification"); // mới
    this.action = useService("action");             // mới
    // ... phần load số liệu giữ nguyên
}

baoTin() {
    this.notification.add("Đã làm mới bảng điều khiển thẻ thành viên!",
        { type: "success", title: "Thành công" });
}
moDanhSach() {
    this.action.doAction("quan_ca_phe.action_the_thanh_vien");
}

Trong template thêm hai nút, mỗi nút một t-on-click trỏ tới một phương thức:

<div class="mt-3">
    <button class="btn btn-success" t-on-click="baoTin">Báo tin (notification)</button>
    <button class="btn btn-primary ms-2" t-on-click="moDanhSach">Mở danh sách thẻ (action)</button>
</div>

Sửa file JS/XML tĩnh xong phải nâng cấp module để Odoo gói lại bundle asset, rồi khởi động lại:

docker cp dashboard.js  odoo19:/.../quan_ca_phe/static/src/js/dashboard.js
docker cp dashboard.xml odoo19:/.../quan_ca_phe/static/src/xml/dashboard.xml
docker exec odoo19 odoo -u quan_ca_phe -d blog19 \
    --db_host=odoo19-db --db_user=odoo --db_password=odoo --stop-after-init
docker restart odoo19

Mở bảng điều khiển, bấm nút Báo tin. Đây là ảnh chụp thật màn hình sau cú bấm:

Ảnh chụp thật bảng điều khiển Thẻ thành viên của Odoo 19, tiêu đề có hình cốc cà phê, ba thẻ số liệu hiển thị Tổng số thẻ bằng 4, Tổng điểm tích luỹ bằng 385 màu xanh, Thẻ hạng Vàng bằng 1 màu vàng, bên dưới là khu Gọi dịch vụ Odoo với hai nút Báo tin notification màu xanh lá và Mở danh sách thẻ action màu tím, góc trên bên phải hiện một toast xanh lá viền xanh với tiêu đề Thành công và nội dung Đã làm mới bảng điều khiển thẻ thành viên

Hình 3: Sau cú bấm "Báo tin", notification.add(...) với type: "success" bật lên một toast xanh ở góc phải, tự tắt sau vài giây. Số liệu ba thẻ (4 thẻ, 385 điểm, 1 thẻ Vàng) là dữ liệu thật orm.searchRead đọc từ model quan.the.thanh.vien. Nút "Mở danh sách thẻ" gọi action.doAction mở đúng view list/kanban của thẻ.

Ba service, ba việc khác nhau, cùng một cách lấy về: orm đọc số liệu lúc onWillStart, notification hiện toast khi bấm, action chuyển màn hình. Không có service nào được import như một class rồi new ra — tất cả đều đi qua useService.

Vì sao phải là useService, không phải import thẳng

Một câu hỏi rất hợp lý: sao không import cái orm rồi gọi thẳng? Vì service có trạng thái gắn với môi trường đang chạy — session người dùng hiện tại, công ty đang chọn, ngôn ngữ. useService lấy đúng thể hiện (instance) đã được khởi tạo cho web client này, chứ không phải dựng một cái mới rời rạc. Nó cũng đăng ký component vào vòng đời để dọn dẹp đúng lúc khi component bị gỡ.

Nói cách khác, useService là cơ chế dependency injection của OWL: component khai "tôi cần orm, notification, action", còn việc cung cấp đúng thể hiện nào là do web client lo. Đó là lý do test dễ hơn (thay service thật bằng service giả) và code không dính cứng vào một cách khởi tạo.

Ba ý mang về

  1. Component OWL mượn chức năng dùng chung qua useService("tên"), gọi trong setup() vì nó là một hook. Gán vào this rồi mọi phương thức dùng lại — không import và new service ra như class thường.
  2. Ba service hay đụng nhất: orm (CRUD với server: searchRead/read/create/write/unlink/call), notification (.add(msg, {type}) hiện toast success/warning/danger), action (.doAction(xmlid | dict) mở view/wizard/report). Ngoài ra có dialog và rpc.
  3. useService là dependency injection của OWL: nó trả về đúng thể hiện service đã khởi tạo cho web client hiện tại (kèm session, công ty, ngôn ngữ), và móc component vào vòng đời để dọn dẹp đúng lúc.

Ta đã dùng orm như một hộp đen "gọi server". Nhưng nó gọi cái gì dưới đó, và khác gì với rpc gọi route thô? Phần sau đào vào orm service — cách gọi CRUD và cả method tuỳ ý của model từ JavaScript, và khi nào phải rơi xuống rpc.