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 đó.

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:

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:

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_publishedquyết định công khai. ĐặtTruetrong XML để đăng ngay; để trống thì trang tồn tại nhưng khách chưa thấy.- Bản ghi
website.pagesố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
dependswebsiteđể dùngwebsite.pagevàt-call="website.layout".
Ba ý mang về
- 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ỏurlvàview_id— Odoo tự tạo route, không cần Python. website.page_inherits ir.ui.view: nó là template cộng thêmurlvà cờis_published, nên chính bản ghi đóng vai route khi request tới URL.- 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.