Suốt mấy bài OWL vừa rồi, dòng cuối của mọi file JS đều là một biến thể của registry.category("...").add(...). Client action đăng ký vào "actions", service đăng ký vào "services". Đã đến lúc mổ chính cái cơ chế đó: registry — trái tim của khả năng mở rộng web client Odoo, thứ cho phép module của bạn thêm một mảnh giao diện mà không đụng một dòng nào của lõi.

registry là gì

Web client Odoo không hard-code danh sách các thành phần nó biết. Thay vào đó nó tra một registry — hình dung như một cái tủ nhiều ngăn, mỗi ngăn (category) chứa các mục cùng loại. Muốn thêm một client action? Bỏ nó vào ngăn "actions". Thêm một mục cho thanh trên cùng? Bỏ vào ngăn "systray". Khi web client dựng giao diện, nó duyệt các ngăn này và lấy ra mọi thứ đã đăng ký — kể cả thứ module của bạn vừa bỏ vào.

Sơ đồ nền tối liệt kê các category hay dùng của registry Odoo, mỗi dòng một khối màu, gồm actions cho client action theo tag đăng ký bằng registry category actions add tag Comp, systray cho mục nhỏ ở thanh trên cùng với value là một object chứa Component, fields cho field widget tuỳ biến với object gồm component và supportedTypes, views cho loại view mới như list form kanban và loại tự viết, services cho service dùng chung thứ mà useService lấy ra, và formatters cho cách định dạng giá trị field khi hiển thị

Hình 1: Vài category hay gặp. Mỗi category là một sổ đăng ký riêng; registry.category("tên") lấy ra sổ đó, .add(key, value) ghi một mục vào. Web client duyệt từng sổ để biết có những client action nào, systray nào, field widget nào... — đó là lý do module bên thứ ba mở rộng được giao diện.

Cấu trúc lời gọi luôn là:

registry.category(TEN_CATEGORY).add(KEY, VALUE, { sequence, force });
  • KEY là tên duy nhất trong category đó. Trùng key với một mục đã có sẽ báo lỗi, trừ khi bạn truyền { force: true } để cố ý ghi đè.
  • VALUE có hình dạng tuỳ category: "actions" nhận thẳng class Component; "systray" nhận { Component }; "fields" nhận { component, supportedTypes }.
  • { sequence } quyết định thứ tự khi category được duyệt.

Đăng ký thật một mục systray

Để thấy registry hoạt động, mình viết một component OWL nhỏ hiện tổng điểm thẻ, rồi đăng ký nó vào category "systray" — ngăn dành cho các mục ở thanh trên cùng bên phải:

Ảnh chụp hai đoạn mã nền tối. Đoạn trên là file systray js import registry từ web core registry, định nghĩa lớp DiemSystray kế thừa Component có template quan_ca_phe DiemSystray và setup dùng orm nạp tổng điểm, rồi gọi registry category systray add với key duy nhất quan_ca_phe diem_systray, value là object chứa Component DiemSystray, và options sequence bằng 1. Đoạn dưới là file systray xml khai template t-name quan_ca_phe DiemSystray hiển thị biểu tượng cốc cà phê cùng state chấm tong và chữ điểm

Hình 2: Component (trên) và template (dưới). Điểm mấu chốt là ba dòng registry.category("systray").add(...): key duy nhất quan_ca_phe.diem_systray, value là { Component: DiemSystray } (đúng hình dạng category systray đòi), và sequence: 1. Không có dòng này, component tồn tại nhưng web client chẳng bao giờ biết tới nó.

Khai file JS/XML vào bundle web.assets_backend trong manifest, nâng cấp module — và mục xuất hiện ngay trên thanh điều hướng, không sửa gì của Odoo:

Ảnh chụp thật thanh điều hướng trên cùng của Odoo 19 nền tím, bên trái là logo lưới ứng dụng và tên Quán Cà Phê cùng các menu Bảng điều khiển Thẻ thành viên Thẻ đang hiệu lực In thẻ theo hạng, bên phải là biểu tượng tin nhắn có số 3, biểu tượng hoạt động có số 2, rồi mục tự thêm gồm biểu tượng cốc cà phê với dòng chữ 385 điểm màu trắng, cuối cùng là tên công ty TNHH Cà Phê Việt và avatar chữ A

Hình 3: Mục systray "☕ 385 điểm" (giữa biểu tượng hoạt động và tên công ty) là component vừa đăng ký. Con số 385 do chính nó gọi orm.searchRead nạp từ model thẻ — tổng điểm thật của 4 thẻ. Nó ngồi cạnh các mục systray sẵn có của Odoo (chat, hoạt động) vì tất cả cùng nằm trong một category.

Vì sao thiết kế kiểu registry

Cách này giải quyết một bài toán nền tảng của phần mềm mở rộng được: làm sao để bên thứ ba thêm hành vi mà không phải sửa (rồi bảo trì bản vá của) mã gốc. Nếu web client giữ một danh sách cứng các systray, thêm một mục mới đồng nghĩa với sửa file lõi Odoo. Với registry, lõi chỉ nói "tôi sẽ hiển thị mọi thứ trong category systray", còn module tự bỏ mục của mình vào. Lõi và module rời nhau hoàn toàn.

Vài điểm thực dụng:

  • Key phải có prefix module (quan_ca_phe.diem_systray) để không đụng key của module khác trong cùng category.
  • { force: true } là con dao hai lưỡi: nó cho phép thay một mục lõi (ví dụ đổi một field widget mặc định), nhưng hai module cùng force một key thì cái nạp sau thắng — khó lần.
  • Đăng ký chạy khi file JS được nạp, tức khi bundle web.assets_backend tải. Quên khai file vào bundle trong manifest là component không bao giờ được đăng ký, mà chẳng có lỗi nào.

Ba ý mang về

  1. registry là hệ thống sổ đăng ký của web client: nhiều category (actions, systray, fields, views, services, formatters...), mỗi cái chứa các mục cùng loại. Web client duyệt chúng để dựng giao diện — nhờ vậy module mở rộng được mà không sửa lõi.
  2. Mẫu lời gọi luôn là registry.category(TÊN).add(KEY, VALUE, {sequence, force}) — KEY duy nhất (nên có prefix module), VALUE có hình dạng tuỳ category, sequence quyết thứ tự, force để cố ý ghi đè.
  3. Đăng ký chỉ chạy khi file JS được nạp qua bundle web.assets_backend. Quên khai file vào manifest thì không có đăng ký nào và cũng không có lỗi nào.

Ta đã lướt qua category "fields" mà chưa làm gì với nó. Phần sau bắt tay viết một field widget tuỳ biến bằng OWL — thay cách một field hiển thị và cho nhập trong form, đăng ký đúng vào category đó.