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":

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ẻ:

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
xpathtrỏ vào//notebookrồiposition="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ề
<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.- Đ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,autofocusmở sẵn,groupsgiới hạn người thấy. - 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,
-umodule, restart — và khi mở rộng module khác thì chèn<page>bằngxpathvà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.