Ta đã gặp QWeb ở hai nơi rất khác nhau: template của OWL (chạy trên trình duyệt, có t-on, component) và template của report (render ra PDF). Còn một chỗ thứ ba: QWeb website — render HTML phía server cho một trang web công khai. Cùng cú pháp t-foreach, t-esc, t-if, nhưng chạy ở nơi khác và cho ra thứ khác. Bài này dựng một trang thật để phân biệt rạch ròi.

Ba loại QWeb

Chạy ở đâu Ra cái gì Đặc trưng
OWL trình duyệt DOM tương tác t-on-click, component, phản ứng
report server PDF web.external_layout, khổ giấy
website server trang HTML website.layout, controller type='http'

Cả ba dùng chung các directive nền (t-if, t-foreach, t-esc, t-att-, t-call), nên dễ tưởng là một. Khác biệt cốt lõi: website QWeb render HTML ngay trên server rồi gửi về trình duyệt — không phải app JS, không phải file PDF, mà một trang web bình thường ai cũng xem được.

Controller + template

Một trang web gồm hai mảnh: một controller khai đường dẫn và trả kết quả render, và một template QWeb dựng HTML:

Ảnh chụp hai đoạn mã nền tối. Đoạn trên là controller web_the py, lớp TrangTheCongKhai kế thừa http Controller, decorator http route đường dẫn the-cua-quan type http auth public website True, method trang_the tìm mọi bản ghi quan the thanh vien sudo sắp theo diem desc rồi trả request render với tên template quan_ca_phe trang_the và context là dict chứa cards. Đoạn dưới là template website_the xml, thẻ template id trang_the name Thẻ thành viên của quán, bên trong t-call website layout gọi bố cục website gồm header và footer, chứa div container h1 tiêu đề cốc cà phê Thẻ thành viên Quán Cà Phê, một table với tbody dùng tr t-foreach cards t-as the t-key the id lặp từng thẻ, mỗi dòng có td t-esc the name và td t-esc the diem

Hình 1: Controller (trên) và template (dưới). @http.route(..., type='http', auth='public', website=True): type='http' trả HTML (khác type='jsonrpc' trả JSON ở bài rpc), auth='public' cho khách vãng lai xem, website=True gắn nó vào hệ thống website. request.render('module.template', {ctx}) biến template thành HTML với dữ liệu truyền vào. Trong template, t-call="website.layout" mượn header/footer của site, t-foreach lặp qua các thẻ, t-esc in giá trị (có escape chống XSS).

Kết quả: một trang web thật

Nâng cấp module, mở /the-cua-quan. Đây là trang thật, có đầy đủ khung website:

Ảnh chụp thật trang web công khai the-cua-quan trên Odoo 19, phía trên là thanh điều hướng website với logo và các menu Trang chủ Cửa hàng Blog, giỏ hàng, tìm kiếm, số điện thoại, chọn ngôn ngữ Tiếng Việt; nội dung chính là tiêu đề cốc cà phê Thẻ thành viên Quán Cà Phê, dòng mô tả Bảng điểm công khai cập nhật theo thời gian thực, và một bảng sọc liệt kê bốn thẻ VIP-0003 hạng Vàng 150 điểm, VIP-0001 hạng Bạc 150 điểm, VIP-0002 hạng Bạc 85 điểm, VIP-0004 hạng Bạc 0 điểm; phía dưới là chân trang website với các liên kết hữu ích, giới thiệu công ty và biểu tượng mạng xã hội

Hình 2: Trang /the-cua-quan render thật. Header và footer (logo, menu, mạng xã hội) đến từ website.layout — mình không viết chúng, chỉ t-call là có. Phần giữa là nội dung riêng: tiêu đề và bảng bốn thẻ, sắp theo điểm (VIP-0003 Vàng 150, VIP-0001 Bạc 150, VIP-0002 85, VIP-0004 0), dựng bằng t-foreach. Nhãn hạng ("Vàng"/"Bạc") lấy từ nhãn hiển thị của field selection. Đây là một trang web bình thường — khách không đăng nhập vẫn xem được.

Vài lưu ý

  • website.layout cho bạn cả bộ khung site miễn phí — header, menu, footer, responsive. Chỉ t-call là hoà vào giao diện website hiện có, không phải tự dựng.
  • auth='public' mới cho khách vãng lai vào. Để mặc định (auth='user') là bắt đăng nhập — sai ý đồ một trang công khai.
  • t-esc escape, t-out cũng escape chuỗi thường (chỉ render HTML khi giá trị là markup) — như đã thấy ở bài OWL, đây là hàng rào XSS. Dữ liệu người dùng nhập thì luôn để QWeb escape.
  • Đây là server-side, không phản ứng. Trang chụp lại trạng thái lúc render; muốn cập nhật phải tải lại trang (hoặc thêm JS riêng). "Thời gian thực" ở đây nghĩa là mỗi lần mở trang đọc dữ liệu mới, không phải tự đổi khi đang xem.

Ba ý mang về

  1. QWeb có ba nơi dùng, đừng lẫn: OWL (trình duyệt, tương tác), report (server → PDF), website (server → trang HTML công khai). Cùng directive nền (t-foreach, t-esc, t-if, t-call) nhưng chạy khác chỗ, ra khác thứ.
  2. Trang web = controller + template. Controller @http.route(path, type='http', auth='public', website=True) gọi request.render('module.template', {ctx}); template <template> dùng t-call="website.layout" để có khung site rồi dựng nội dung riêng.
  3. website.layout cho header/footer miễn phí; auth='public' cho khách xem; QWeb escape (t-esc) chống XSS. Trang là server-side, chụp trạng thái lúc render — không tự phản ứng như OWL.

Trong template trên ta gọi t-call="website.layout" để mượn cả một khung. Phần sau đào sâu t-call: tái sử dụng và lồng template QWeb, truyền biến vào template được gọi, và cách nó giúp tách phần dùng chung ra khỏi từng trang.