Phần trước ta dựng trang web bằng controller — viết một route Python, gọi request.render. Nhưng với một trang tĩnh (thể lệ, giới thiệu, chính sách) thì viết controller là thừa: không có logic, chỉ có nội dung. Odoo có đường ngắn hơn — khai trang hoàn toàn bằng XML. Bạn viết một template QWeb và một bản ghi website.page trỏ tới nó; Odoo tự tạo route, tự đưa trang vào hệ thống website. Bài này dựng thật một trang thể lệ theo cách đó, không một dòng Python, rồi chụp trang thật và cả nơi nó xuất hiện trong khu quản trị.

Hai mẩu XML là đủ

Cần đúng hai thứ: một <template> chứa nội dung, và một <record model="website.page"> gắn URL cho template đó.

Ảnh chụp mã XML nền tối gồm hai phần. Phần một là template QWeb id the_le_thanh_vien name Thể lệ thành viên, bên trong t t-call website layout bọc div container có h1 Thể lệ chương trình thành viên, h3 Cách tích điểm, và ul li Mỗi 10.000đ được cộng 1 điểm. Phần hai là record id the_le_page model website page với field name Thể lệ thành viên, field url gạch chéo the-le-thanh-vien, field view_id ref the_le_thanh_vien, field is_published True. Chú thích cuối khai file XML này vào data của manifest không một dòng Python, website page kế thừa ir ui view qua url cộng view_id thành một trang có địa chỉ

Hình 1: Hai mẩu XML. Template (trên) là nội dung, t-call="website.layout" để có header/footer website. Bản ghi website.page (dưới) nối một URL (/the-le-thanh-vien) với template đó qua view_id, và is_published=True để công khai. Khai file này vào data của manifest là xong.

website.page: URL + view gộp làm một

Nhìn vào lõi Odoo 19, website.page là một model đặc biệt:

class WebsitePage(models.Model):
    _name = 'website.page'
    _inherits = {'ir.ui.view': 'view_id'}
    url = fields.Char('Page URL', required=True)
    view_id = fields.Many2one('ir.ui.view', required=True, ondelete="cascade")

_inherits = {'ir.ui.view': 'view_id'} nghĩa là mỗi website.page là một ir.ui.view (chính template QWeb của bạn) cộng thêm trường url. Nói cách khác: template lo hiển thị cái gì, website.page lo ở địa chỉ nào và đã công khai chưa. Khi có request tới /the-le-thanh-vien, ir.http của website tra bảng website.page, thấy URL khớp, lấy view_id ra render. Không cần route Python vì chính bản ghi website.page là cái route.

Chạy thật, trang hiện ra đầy đủ như một trang website:

Ảnh chụp trang web thật tại đường dẫn the-le-thanh-vien trên Odoo 19. Header website layout gồm Your Logo, menu Home Shop, giỏ hàng, kính lúp, số điện thoại, ngôn ngữ English US, Sign in, Contact Us. Thân trang có tiêu đề lớn Thể lệ chương trình thành viên, đoạn giới thiệu Quán Cà Phê Việt tri ân khách quen bằng chương trình tích điểm, mục Cách tích điểm với gạch đầu dòng Mỗi 10.000đ chi tiêu được cộng 1 điểm và Điểm cập nhật ngay khi thanh toán, mục Hạng thẻ với Bạc từ 0 điểm ưu đãi cơ bản và Vàng từ 100 điểm giảm 10 phần trăm mỗi ly, và dòng ghi chú Trang này được khai hoàn toàn bằng XML không có dòng Python nào. Dưới cùng là footer website

Hình 2: Trang thật tại /the-le-thanh-vien, dựng hoàn toàn bằng XML. Header/footer từ website.layout, nội dung từ template. Không có controller nào phía sau — bản ghi website.page tự đóng vai route.

Khác biệt lớn: trang quản lý được trong Website

Đây là điểm mà cách controller (phần trước) không có: trang khai bằng website.page xuất hiện ngay trong khu quản trị Website > Trang, quản lý được như mọi trang khác:

Ảnh chụp danh sách Trang trong khu quản trị Website của Odoo 19. Bảng có các cột Tiêu đề trang, URL trang, Được lập chỉ mục, Có trong menu, Tối ưu SEO, Được đăng. Một dòng hiện trang Thể lệ thành viên với URL gạch chéo the-le-thanh-vien, ô Được lập chỉ mục được tick, ô Được đăng được tick

Hình 3: Trang website.page khai bằng XML xuất hiện trong Website → Trang, kèm cột "Được đăng", "Được lập chỉ mục" (SEO), "Có trong menu". Người quản trị bật/tắt công khai, thêm vào menu, sửa SEO ngay trên giao diện — và với template phù hợp còn mở được trình soạn kéo–thả để sửa nội dung. Trang từ controller không có mặt ở đây.

Khi nào XML, khi nào controller

Hai cách bổ sung cho nhau:

Trang khai bằng XML (website.page) Trang từ controller (@route)
Viết gì Chỉ XML, không Python Python + template
Hợp với Nội dung tĩnh (thể lệ, giới thiệu, FAQ) Nội dung động cần logic/tham số
Quản trị Website > Trang ✅ Có, publish/SEO/menu ❌ Không
Sửa bằng trình soạn web ✅ Được Hạn chế
Dữ liệu theo tham số/quyền ❌ Không linh hoạt ✅ Toàn quyền

Quy tắc đơn giản: trang tĩnh thì khai bằng XML để rảnh tay và cho người quản trị tự sửa; trang cần tính toán, lọc theo tham số, kiểm quyền thì viết controller như phần trước.

Vài lưu ý

  • URL nên bắt đầu bằng / và dùng gạch nối; Odoo tự chuẩn hoá (slugify) và bảo đảm không trùng.
  • is_published quyết định công khai. Đặt True trong XML để đăng ngay; để trống thì trang tồn tại nhưng khách chưa thấy.
  • Bản ghi website.page sống trong CSDL. Gỡ module không phải lúc nào cũng tự xoá sạch; khi dọn nên xoá cả bản ghi (đây là dữ liệu, không chỉ là file XML).
  • Module phải depends website để dùng website.page và t-call="website.layout".

Ba ý mang về

  1. Trang tĩnh khai được hoàn toàn bằng XML: một <template> (nội dung) + một <record model="website.page"> trỏ url và view_id — Odoo tự tạo route, không cần Python.
  2. website.page _inherits ir.ui.view: nó là template cộng thêm url và cờ is_published, nên chính bản ghi đóng vai route khi request tới URL.
  3. Trang XML quản lý được trong Website > Trang (publish, SEO, menu, trình soạn) — khác trang từ controller; chọn XML cho nội dung tĩnh, controller cho nội dung động.

Ta đã làm trang công khai cho mọi người. Nhưng nhiều trang chỉ dành cho khách hàng đã đăng nhập — xem đơn của họ, hoá đơn của họ. Phần sau bước vào Portal: trang cho khách với portal.mixin — cách Odoo dựng khu "tài khoản của tôi" an toàn, mỗi khách chỉ thấy dữ liệu của chính mình.