Bạn đã dựng được trang tùy biến (controller hoặc XML) và đưa nó vào sitemap. Nhưng khách vào website vẫn không tìm thấy nó — vì nó chưa có trên thanh điều hướng. Trang tồn tại không có nghĩa nó xuất hiện trong menu; hai thứ đó tách biệt trong Odoo: website.page là trang, website.menu là mục trên thanh menu. Bài này chỉ cách gắn một trang vào menu — khai bằng XML một mục menu, hiểu cây parent_id/sequence, và một cơ chế thú vị: Odoo tự sao mục menu sang từng website.

website.page và website.menu là hai thứ khác nhau

Điểm dễ nhầm nhất: tạo một website.page không tự tạo mục menu. Trang có URL, truy cập được, vào sitemap được — nhưng thanh menu trên cùng chỉ hiện những gì có trong website.menu. Muốn khách bấm từ menu tới trang, bạn phải tạo cả hai và nối chúng.

Ảnh chụp mã XML nền tối hai phần. Phần một khai trang: template id uu_dai_tv_page name Ưu đãi thành viên chứa t-call website layout, và record model website page với url gạch chéo uu-dai-thanh-vien, view_id ref uu_dai_tv_page, is_published True. Phần hai khai mục menu trỏ tới trang: record id menu_uu_dai_tv model website menu với field name Ưu đãi chú thích chữ hiện trên thanh, url gạch chéo uu-dai-thanh-vien, parent_id ref website main_menu chú thích menu gốc, sequence eval 65 chú thích thứ tự. Ghi chú Odoo tự sao mục menu từ Default Main Menu sang top menu của từng website, cây menu dùng parent_id cộng sequence, page_id để gắn trang

Hình 1: Hai bản ghi. website.page (trên) là trang; website.menu (dưới) là mục trên thanh menu, trỏ tới trang qua url. name là chữ hiện trên thanh, parent_id đặt nó dưới menu gốc, sequence quyết định thứ tự. (Có thể dùng page_id thay url để gắn thẳng một website.page.)

Khai xong và -u module, mục "Ưu đãi" hiện ngay trên header website thật:

Ảnh chụp thanh header website thật trên Odoo 19. Từ trái sang phải: Your Logo, các mục menu Home, Shop, Blog, Jobs, Contact us, và mục mới Ưu đãi. Bên phải là biểu tượng giỏ hàng, kính lúp, số điện thoại +1 555-555-5556, chọn ngôn ngữ English US, nút Sign in và nút Contact Us

Hình 2: Thanh menu website thật, giờ có Ưu đãi nằm cạnh Home, Shop, Blog, Jobs, Contact us — bấm vào đi tới /uu-dai-thanh-vien. Không cần chỉnh tay gì thêm; chỉ một bản ghi website.menu.

Cây menu: parent_id, sequence, page_id

website.menu là một cây phân cấp (_parent_store = True), sắp xếp theo sequence. Các trường quan trọng:

  • name — chữ hiển thị (dịch được).
  • url — đường dẫn khi bấm (nội bộ /... hoặc ngoài https://...).
  • parent_id — menu cha; đặt dưới menu gốc để thành mục top, hay dưới một mục khác để thành menu con (dropdown).
  • sequence — thứ tự trái→phải (số nhỏ đứng trước).
  • page_id — gắn thẳng tới một website.page (thay vì gõ url); xoá trang thì mục menu tự mất (ondelete='cascade').
  • new_window, group_ids — mở tab mới, hoặc chỉ hiện cho nhóm quyền nhất định.

Nhìn cây menu thật của website qua dữ liệu:

Ảnh chụp terminal nền tối website menu thật cây điều hướng của website 1 trên Odoo 19. Gốc là Top Menu for Website 1 là website menu_id của website 1. Các nhánh con: Home đường dẫn gạch chéo seq 10, Shop gạch chéo shop seq 20, Blog gạch chéo blog seq 40, Jobs gạch chéo jobs seq 59, Contact us gạch chéo contactus seq 60, và dòng được tô sáng Ưu đãi gạch chéo uu-dai-thanh-vien seq 65 là mục ta thêm. Ghi chú sắp theo sequence, parent_id trỏ menu con, page_id gắn website page, quản trị kéo thả trong Website Site Menu Editor hoặc khai XML

Hình 3: Cây website.menu thật của website. Các mục là con của "Top Menu for Website 1", sắp theo sequence — "Ưu đãi" (seq 65) đứng sau Contact us (seq 60). Đặt sequence nhỏ hơn để đẩy lên trước.

Mỗi website một cây menu — và Odoo tự sao

Đây là cơ chế hay bị bất ngờ. Một cài đặt Odoo có thể chạy nhiều website, mỗi website có cây menu riêng (website.menu_id trỏ tới một "Top Menu" khác nhau). Khi bạn khai một mục menu trong XML dưới "Default Main Menu" (website.main_menu, không thuộc website nào), Odoo tự sao chép nó vào top menu của từng website hiện có.

Đó là lý do sau khi -u, hệ thống có hai bản ghi "Ưu đãi": một bản gốc dưới Default Main Menu, và một bản đã sao dưới "Top Menu for Website 1" — chính bản sao này mới hiện trên header của website 1. Hiểu điều này tránh bối rối khi bạn thấy menu "nhân đôi" trong dữ liệu, hay khi sửa mục ở một website mà website khác không đổi theo.

Quản trị bằng chuột: Menu Editor

Không phải lúc nào cũng cần XML. Người quản trị chỉnh menu ngay trên giao diện: vào Website → Site → Menu Editor (hoặc bấm "Sửa" rồi thao tác trên thanh menu). Ở đó kéo–thả để sắp thứ tự và lồng cấp, thêm/sửa/xoá mục, tạo mega menu. Còn trang thì quản lý ở Website → Site → Pages (bật/tắt công khai, đổi URL, SEO — như phần 170).

Quy tắc chọn: XML khi menu là một phần của module (cài module là có ngay, dựng lại được); Menu Editor khi người vận hành muốn tự chỉnh mà không đụng code.

Vài lưu ý

  • Trang và menu tách biệt — tạo trang xong nhớ tạo cả mục menu, nếu muốn khách thấy nó trên thanh.
  • page_id gắn cứng hơn url. Dùng page_id khi mục menu ứng với một website.page cụ thể — xoá trang, mục menu tự dọn theo. Dùng url cho link ngoài hay route controller.
  • Đừng khai trùng cho từng website bằng tay. Khai một lần dưới website.main_menu và để Odoo sao đi; can thiệp thủ công vào từng top menu dễ lệch.
  • group_ids cho menu theo quyền — ví dụ mục "Khu nội bộ" chỉ hiện với nhân viên đăng nhập.

Ba ý mang về

  1. website.page (trang) và website.menu (mục menu) là hai thứ khác nhau — tạo trang không tự tạo menu; phải khai website.menu (name + url/page_id + parent_id + sequence) để nó hiện trên thanh điều hướng.
  2. Menu là cây sắp theo sequence, lồng cấp bằng parent_id (menu con/dropdown); page_id gắn cứng một trang (cascade khi xoá trang).
  3. Mỗi website một cây menu, và Odoo tự sao mục khai dưới website.main_menu sang top menu từng website — nên bạn thấy bản gốc + bản sao; quản trị còn kéo–thả được trong Menu Editor.

Ta đã đi hết một vòng website và controller. Phần sau quay lại một chủ đề bảo mật đã chạm tới nhưng chưa mổ kỹ: Phần sau đào sâu CSRF trong controller và form — token hoạt động ra sao, khi nào bật/tắt, và những cạm bẫy khi để sai làm form gãy hoặc mở lỗ hổng.