Form của một đối tượng thật hiếm khi chỉ có dăm ba field. Một thẻ thành viên có thông tin cơ bản, lịch sử điểm, ghi chú, ưu đãi... nhồi tất cả lên một màn hình là bắt người dùng cuộn mãi. Odoo giải quyết bằng một cặp thẻ quen thuộc: <notebook> bọc ngoài, và bên trong là các <page> — mỗi page hiện ra thành một tab. Bài này mổ xẻ cặp thẻ đó, và để chắc chắn nó là thật, tôi sẽ thêm hẳn một tab mới vào form của module demo rồi xem nó xuất hiện.

Cấu trúc: một notebook, nhiều page

Quy tắc đơn giản: đặt <notebook> ở nơi bạn muốn có tab (thường là phần dưới của <sheet>, sau các <group> thông tin chính), rồi mỗi <page string="..."> là một tab. Thuộc tính string chính là nhãn hiện trên tab.

Đây là notebook trong form thẻ thành viên của module quan_ca_phe. Ban đầu nó có 2 page; tôi vừa thêm page thứ ba "Ưu đãi":

Ảnh chụp đoạn mã XML nền tối minh hoạ cấu trúc notebook với ba page: page Lịch sử điểm chứa field lich_su_ids, page Ghi chú chứa field ghi_chu_html, page Ưu đãi vừa thêm chứa field uu_dai_ids dạng list một cột name; phía dưới là ba dòng ví dụ thuộc tính page gồm invisible theo điều kiện trang_thai bằng huy, autofocus mở sẵn tab, và groups giới hạn nhóm nhìn thấy

Hình 1: Khối trên là notebook thật trong module, giờ có 3 page. Mỗi <page> là một tab. Khối dưới là ba thuộc tính hay dùng trên <page> (minh hoạ cú pháp Odoo 19). Chú ý invisible viết biểu thức thẳng — không còn bọc attrs như bản cũ.

Cách thêm cũng chính là quy trình sửa view của một module đã cài: lấy file XML ra khỏi container, chèn <page> mới trước </notebook>, đưa lại vào, rồi nâng cấp module:

docker cp odoo19:.../views/the_thanh_vien_views.xml ./
# chèn <page string="Ưu đãi"> ... </page> trước </notebook>
docker cp ./the_thanh_vien_views.xml odoo19:.../views/
docker exec odoo19 odoo -d blog19 -u quan_ca_phe --stop-after-init
docker restart odoo19

Nhìn ba tab trên form thật

Sau khi nâng cấp, mở lại form thẻ VIP-0001. Giờ có đúng ba tab, và tab "Ưu đãi" hiển thị danh sách ưu đãi của thẻ:

Ảnh chụp giao diện Odoo thật cho thấy dải tab gồm Lịch sử điểm, Ghi chú và Ưu đãi; tab Ưu đãi đang mở, cột Tên ưu đãi liệt kê Giảm 10 phần trăm đồ uống và Freeship nội thành, dưới cùng có link Thêm một dòng

Hình 2: Ba tab hiện ra từ ba <page>. Tab "Ưu đãi" (vừa thêm) đang mở, bên trong là field uu_dai_ids render dưới dạng list. Bấm sang tab khác, phần thân đổi theo — Odoo chỉ tải/hiện nội dung của tab đang chọn, phần còn lại nằm chờ.

Điểm đáng chú ý: nội dung của mọi page đều nằm trong cùng một form, cùng một bản ghi. Tab chỉ là cách tổ chức hiển thị — nó không tạo bản ghi riêng hay request riêng cho từng tab. Bạn lưu form một lần là lưu hết mọi thay đổi ở mọi tab.

Điều khiển từng page

<page> nhận những thuộc tính giúp tab "thông minh" hơn:

  • invisible="<biểu thức>" — ẩn cả tab theo điều kiện. Ví dụ invisible="trang_thai == 'huy'": thẻ đã huỷ thì giấu tab kế toán đi. Biểu thức tính trên chính bản ghi.
  • autofocus="autofocus" — chọn tab mở sẵn khi vào form. Không đặt thì Odoo mở tab đầu tiên.
  • groups="base.group_system" — chỉ người thuộc nhóm đó mới thấy tab. Hữu ích khi một tab chứa dữ liệu nội bộ/nhạy cảm.

Cả ba đều đặt thẳng trên thẻ <page>, không cần thêm dòng JavaScript nào. Nhờ vậy, một form có thể trông rất khác nhau tuỳ trạng thái bản ghi và tuỳ người đăng nhập — mà vẫn chỉ là một định nghĩa view duy nhất.

Vài lưu ý khi chia tab

  • Đừng nhét thông tin quan trọng nhất vào tab. Field mà người dùng cần thấy ngay (tên, trạng thái, số dư) nên nằm ở phần <group> phía trên, ngoài notebook. Tab dành cho nội dung phụ, dữ liệu chi tiết, danh sách con.
  • Một tab, một chủ đề. Mỗi page nên gom những field liên quan với nhau. "Lịch sử điểm", "Ghi chú", "Ưu đãi" — mỗi cái một việc rõ ràng, thay vì trộn lẫn.
  • Kế thừa view thêm được tab. Khi viết module mở rộng người khác, bạn dùng xpath trỏ vào //notebook rồi position="inside" để chèn thêm <page> — đó là cách các app của Odoo bồi thêm tab "Kế toán", "Kho"... vào form liên hệ mà không sửa gốc.

Ba ý mang về

  1. <notebook> bọc ngoài, mỗi <page string="..."> là một tab — cách chuẩn để chia một form dài thành các mảng gọn; mọi tab vẫn thuộc cùng một bản ghi, lưu một lần là lưu hết.
  2. Điều khiển tab bằng thuộc tính viết thẳng (Odoo 19): invisible="<expr>" ẩn theo điều kiện, autofocus mở sẵn, groups giới hạn người thấy.
  3. Thêm/sửa tab của module đã cài đi qua đúng quy trình sửa view: cp file ra, vá XML, cp vào, -u module, restart — và khi mở rộng module khác thì chèn <page> bằng xpath vào //notebook.

Tab giúp form gọn theo chiều dọc. Bài sau ta thêm một thứ giúp form "nói chuyện" với dữ liệu liên quan: Phần sau về smart button — nút thống kê gắn ở đầu form, bấm vào là nhảy sang danh sách bản ghi liên quan.