Suốt các bài qua ta chỉnh từng mảnh: một field widget, một systray, một view kế thừa. Nhưng đôi khi thứ bạn cần không vừa bất kỳ khuôn nào có sẵn — một trang tổng quan, một bảng xếp hạng, một màn hình nhập liệu nhiều bước. Lúc đó bạn cần client action: cả một màn hình do OWL toàn quyền vẽ, mở từ menu như một app thực thụ. Cái "Bảng điều khiển" ta dùng làm demo suốt loạt này chính là một client action — bài này mổ nó ra và dựng một cái mới.

Client action là gì

Odoo có ba loại "action" chính: ir.actions.act_window (mở view của một model — list/form/kanban), ir.actions.report (in báo cáo), và ir.actions.client (chạy một component JS phía client). Loại thứ ba không gắn với model hay view nào — nó chỉ nói "hãy chạy component đăng ký dưới tag này", còn component vẽ gì là tuỳ bạn.

Cơ chế gồm hai nửa khớp nhau qua một tag:

Ảnh chụp hai đoạn mã nền tối. Đoạn trên là file xep_hang js định nghĩa lớp XepHangThe kế thừa Component với template quan_ca_phe XepHangThe, setup lấy service orm và useState rows rỗng loading true, onWillStart gọi orm searchRead trên quan chấm the chấm thanh chấm vien lấy field name diem hang_the với order diem desc name asc để sắp theo điểm giảm dần rồi map thành các dòng có thứ hạng, cuối cùng registry category actions add tag quan_ca_phe xep_hang gắn với XepHangThe. Đoạn dưới là file xml khai record ir.actions.client với field tag quan_ca_phe xep_hang khớp tag đã add, và một menuitem tên Bảng xếp hạng trỏ tới action đó

Hình 1: Hai nửa. JS: một component OWL bình thường (dùng useService, useState, onWillStart như mọi component), rồi registry.category("actions").add("tag", Component). XML: một ir.actions.client có tag khớp đúng chuỗi đã đăng ký, cộng một menuitem để mở nó. Tag là sợi dây nối bản ghi action ở CSDL với class JS trên trình duyệt.

Component vẽ gì là tuỳ bạn

Điểm giải phóng của client action: không có ràng buộc view. Component nhận toàn bộ vùng nội dung và vẽ HTML tuỳ ý. Ở đây mình làm một bảng xếp hạng — đọc thẻ qua orm.searchRead (sắp theo diem desc), gắn thứ hạng và huy chương, rồi render thành bảng:

onWillStart(async () => {
    const cards = await this.orm.searchRead(
        "quan.the.thanh.vien", [], ["name", "diem", "hang_the"],
        { order: "diem desc, name asc" });
    this.state.rows = cards.map((c, i) => ({
        hang: i + 1, name: c.name, diem: c.diem,
        huy: i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : "",
    }));
});

Nâng cấp module, mở menu "Bảng xếp hạng". Đây là màn hình thật:

Ảnh chụp thật màn hình client action trong Odoo 19, tiêu đề có biểu tượng cúp và chữ Bảng xếp hạng thẻ theo điểm, một bảng ba cột Hạng Mã thẻ Điểm, dòng một huy chương vàng hạng 1 VIP-0001 150 điểm, dòng hai huy chương bạc hạng 2 VIP-0003 kèm nhãn Vàng 150 điểm, dòng ba huy chương đồng hạng 3 VIP-0002 85 điểm, dòng bốn hạng 4 VIP-0004 0 điểm

Hình 2: Client action xep_hang chạy thật. Toàn bộ trang — tiêu đề, bảng, huy chương, nhãn "Vàng" — do component vẽ, không đi qua bất kỳ view chuẩn nào. Dữ liệu thật: 4 thẻ sắp theo điểm (VIP-0001 và VIP-0003 cùng 150, rồi VIP-0002 85, VIP-0004 0). Đây là màn hình riêng, mở từ menu như mọi app khác.

act_window hay client action?

Đừng vội dùng client action cho mọi thứ. Ranh giới:

  • Dùng act_window (list/form/kanban) khi bạn hiển thị/sửa bản ghi của một model theo cách chuẩn. Bạn được miễn phí: tìm kiếm, lọc, phân trang, sắp xếp, phân quyền theo record rule. Viết lại những thứ này bằng tay là phí phạm và dễ sai.
  • Dùng client action khi màn hình không map một-một với view của một model: trang tổng quan trộn nhiều nguồn, một wizard nhiều bước phức tạp, một biểu đồ tương tác, một bảng xếp hạng như trên.

Bảng xếp hạng này có thể làm bằng list view với default_order, nhưng huy chương và cách trình bày riêng thì client action tự do hơn. Ngược lại, nếu chỉ cần "danh sách thẻ sắp theo điểm", một list view chuẩn là đủ và tốt hơn.

Vài lưu ý:

  • Tag phải khớp tuyệt đối giữa registry.add("tag", ...) và <field name="tag">tag</field>. Lệch một ký tự thì mở action ra màn hình trống, lỗi chỉ hiện ở console.
  • Component là OWL bình thường — mọi hook, service dùng như thường. Nạp dữ liệu trong onWillStart.
  • Không có record rule tự động. act_window áp quyền theo dòng của model; client action gọi orm thẳng, nên bạn tự chịu trách nhiệm về những gì truy vấn và hiển thị.

Ba ý mang về

  1. Client action là một màn hình do component OWL toàn quyền vẽ, thuộc loại ir.actions.client — không gắn model/view nào, mở từ menu như một app. Nối qua một tag: registry.category("actions").add("tag", Component) ở JS và <field name="tag"> ở record XML.
  2. Component là OWL bình thường (useService/useState/onWillStart) và vẽ HTML tuỳ ý — hợp cho màn hình không map một-một với view chuẩn (tổng quan, wizard phức tạp, bảng xếp hạng).
  3. Cân nhắc trước khi chọn. act_window cho bạn tìm/lọc/phân trang/phân quyền miễn phí; chỉ dùng client action khi thật sự cần tự do vẽ — và nhớ rằng lúc đó không có record rule tự động, bạn tự lo truy vấn.

Ta đã thêm rất nhiều thứ mới. Phần sau học cách sửa thứ đã có mà không chép đè: dùng patch để vá một component lõi của Odoo một cách an toàn, giữ nguyên phần còn lại.