list, form, kanban, calendar, pivot... — ta dùng chúng suốt mà quen nghĩ đó là "tính năng có sẵn". Nhưng ở bài registry ta đã thấy: mỗi loại view chỉ là một object đăng ký vào category "views". Không có gì thần thánh ngăn bạn viết object thứ N+1. Bài này làm đúng vậy — nhưng khôn ngoan: thay vì viết từ số 0, ta kế thừa kanban và chỉ đổi mảnh cần đổi.

Một view gồm những gì

Mở định nghĩa của kanban trong lõi, nó là một object với các "vai" rõ ràng:

export const kanbanView = {
    type: "kanban",
    ArchParser: KanbanArchParser,   // đọc <kanban>...</kanban> thành cấu trúc
    Controller: KanbanController,   // khung điều phối: control panel, nút, phân trang
    Model: RelationalModel,         // nạp và giữ dữ liệu từ server
    Renderer: KanbanRenderer,       // vẽ dữ liệu ra HTML
    Compiler: KanbanCompiler,
    buttonTemplate: "web.KanbanView.Buttons",
    props: (genericProps, view) => ({ ... }),
};
registry.category("views").add("kanban", kanbanView);

Bốn vai chính: ArchParser (phân tích arch XML), Model (dữ liệu), Renderer (hiển thị), Controller (khung bao quanh). Muốn một view mới, bạn cung cấp bốn thứ đó — nhưng hiếm khi phải viết cả bốn. Thường chỉ cần thay một.

Kế thừa và chỉ thay Renderer

Mình muốn một "bảng điểm": vẫn là kanban thẻ thành viên, nhưng có thêm một dải tổng kết hiện số thẻ và tổng điểm. Chỉ phần hiển thị đổi, nên chỉ cần thay Renderer:

Ảnh chụp hai đoạn mã nền tối. Đoạn trên là file the_bang_view js import kanbanView từ web views kanban kanban_view và KanbanRenderer từ kanban_renderer, định nghĩa lớp TheBangRenderer kế thừa KanbanRenderer với template riêng quan_ca_phe TheBangRenderer, một getter tongDiem cộng dồn diem của mọi record trong props list records, một getter soThe đếm số record, rồi export theBangView bằng cách spread kanbanView và chỉ thay Renderer thành TheBangRenderer, cuối cùng registry category views add tên the_bang. Đoạn dưới là view arch kanban với thuộc tính js_class bằng the_bang chứa field diem và templates t-name card

Hình 1: { ...kanbanView, Renderer: TheBangRenderer } — spread toàn bộ kanban rồi ghi đè đúng một khoá. Controller, Model, ArchParser, Compiler giữ nguyên; ta chỉ đổi Renderer. TheBangRenderer kế thừa KanbanRenderer nên vẫn vẽ cột và thẻ như thường, thêm hai getter tongDiem/soThe đọc từ this.props.list.records. Đăng ký vào category "views" với key the_bang.

Template của Renderer mới kế thừa template kanban gốc bằng t-inherit, chỉ chèn thêm một dải alert phía trên danh sách:

<t t-name="quan_ca_phe.TheBangRenderer"
   t-inherit="web.KanbanRenderer" t-inherit-mode="primary">
    <xpath expr="//t[@t-foreach]" position="before">
        <div class="alert alert-info w-100 ...">
            <span><b>Bảng điểm thẻ thành viên</b></span>
            <span><b t-esc="soThe"/> thẻ · tổng <b t-esc="tongDiem"/> điểm</span>
        </div>
    </xpath>
</t>

Gắn view vào bằng js_class

Làm sao Odoo biết dùng the_bang thay vì kanban thường? Qua thuộc tính js_class trên arch: một view khai <kanban js_class="the_bang"> sẽ dùng object đăng ký dưới key đó. Mình tạo một view kanban mới với js_class="the_bang" và một action mở nó. Kết quả thật:

Ảnh chụp thật màn hình view tuỳ biến trong Odoo 19, tiêu đề action Bảng điểm view tuỳ biến, một dải thông báo nền xanh nhạt trải hết chiều ngang phía trên ghi Bảng điểm thẻ thành viên gạch view JS tuỳ biến ở bên trái và 4 thẻ chấm tổng 385 điểm in đậm ở bên phải, bên dưới là bốn thẻ kanban VIP-0001 150 điểm Bạc, VIP-0003 150 điểm Vàng, VIP-0002 85 điểm Bạc, VIP-0004 0 điểm Bạc, cột trái là bộ lọc Hạng thẻ và Trạng thái

Hình 2: View the_bang chạy thật. Dải xanh phía trên là phần Renderer tuỳ biến thêm vào; "4 thẻ · tổng 385 điểm" do getter tongDiem/soThe tính trực tiếp từ dữ liệu đang hiển thị (150+150+85+0 = 385). Bên dưới vẫn là kanban đầy đủ — cột lọc, thẻ, phân trang — vì ta thừa kế chứ không viết lại. Đây là một loại view mới, đứng ngang hàng list/form/kanban.

Vì sao nên kế thừa, đừng viết lại

Viết một view từ số 0 (cả Controller, Model, Renderer, ArchParser) là việc lớn và dễ vỡ qua mỗi bản Odoo. 90% nhu cầu thực tế chỉ là "gần giống một view có sẵn nhưng khác một chỗ" — và khi đó, spread view gốc rồi ghi đè đúng mảnh cho bạn toàn bộ hành vi chuẩn (chọn, phân trang, quick create, kéo thả) miễn phí, chỉ tốn công cho phần thật sự mới.

Vài lưu ý:

  • Chọn đúng mảnh để thay. Đổi cách hiển thị → Renderer. Đổi nút/hành vi control panel → Controller. Đổi cách đọc arch → ArchParser. Đổi cách nạp dữ liệu → Model.
  • js_class là cầu nối giữa arch XML và object JS. Không có nó, arch <kanban> dùng kanban mặc định.
  • Kế thừa template bằng t-inherit để giữ nguyên phần gốc, chỉ chèn/sửa chỗ cần — đừng chép cả template kanban ra rồi tự bảo trì.

Ba ý mang về

  1. Mọi loại view (list/form/kanban...) chỉ là một object trong registry.category("views") với các vai ArchParser, Model, Renderer, Controller. Viết view mới nghĩa là cung cấp object như vậy.
  2. Cách khôn ngoan là kế thừa: { ...kanbanView, Renderer: RendererCuaBan } giữ toàn bộ hành vi chuẩn, chỉ thay đúng mảnh cần đổi. Renderer con kế thừa Renderer gốc và dùng t-inherit cho template.
  3. Gắn view vào bằng js_class="tên" trên arch. Đó là cách Odoo chọn object view của bạn thay cho loại mặc định.

Ở bài registry ta đã thêm một mục vào thanh trên cùng để minh hoạ. Phần sau quay lại systray một cách bài bản: thêm một icon có tương tác lên thanh trên cùng — mục thông báo, nút tắt nhanh — và những điều cần biết để nó hoạt động đúng.