Phần trước ta thêm một ô "Thẻ thành viên" vào trang /my, dẫn tới /my/the. Giờ dựng chính trang đó: một danh sách bản ghi của khách trong portal, có phân trang, có sắp xếp, và — quan trọng nhất — chỉ hiện dữ liệu của người đang đăng nhập. Odoo cho sẵn các mảnh ghép: hàm portal_pager để phân trang, template portal.portal_table và portal.portal_searchbar để khỏi tự vẽ bảng và thanh sắp xếp. Bài này ráp chúng lại thành route /my/the hoàn chỉnh và chụp kết quả thật.

Route liệt kê: bốn việc cần làm

Một route danh sách portal luôn theo cùng một khuôn: (1) dựng miền lọc theo khách, (2) đếm tổng để phân trang, (3) gọi portal_pager, (4) search với limit/offset từ pager rồi render.

Ảnh chụp mã nền tối hai phần. Phần Python: nhập CustomerPortal và pager as portal_pager từ odoo addons portal controllers portal. Class QuanCaPhePortalList kế thừa CustomerPortal với _items_per_page bằng 3. Route http đường dẫn my the và my the page int page, type http auth user website True. Hàm portal_my_the nhận page mặc định 1 và sortby None. Lấy model quan the thanh vien, domain lọc partner_id bằng request env user partner_id id. Từ điển sortings có diem nhãn Điểm cao nhất order diem desc và name nhãn Tên thẻ order name. sortby mặc định diem, order lấy từ sortings. total bằng search_count domain. pager bằng portal_pager với url my the total page step _items_per_page url_args sortby. the_list bằng search domain order limit _items_per_page offset pager offset. Return request render quan_ca_phe portal_my_the với the_list pager page_name searchbar_sortings sortby default_url. Phần XML: template portal_my_the t-call portal portal_layout, t-call portal portal_searchbar ô sắp xếp, t-call portal portal_table bảng cộng pager, tr t-foreach the_list t-as the với td the name và td the diem

Hình 1: Python (trên) — mấu chốt lại là domain = [('partner_id','=', user.partner_id.id)]: chỉ thẻ của khách này. portal_pager tính sẵn offset, page_count, danh sách số trang; ta chỉ việc search với limit/offset. XML (dưới) — t-call portal.portal_layout cho khung portal, portal.portal_searchbar cho thanh sắp xếp, portal.portal_table cho bảng kèm pager. Không tự viết pager hay bảng.

portal_pager: phân trang trong một dòng

portal_pager (nhập from odoo.addons.portal.controllers.portal import pager as portal_pager) nhận url, total, page, step và trả về một dict có sẵn mọi thứ template cần:

pager = portal_pager(url='/my/the', total=total, page=page,
                     step=self._items_per_page, url_args={'sortby': sortby})
the_list = The.search(domain, limit=self._items_per_page, offset=pager['offset'])
  • pager['offset'] — bỏ qua bao nhiêu bản ghi cho trang hiện tại (dùng thẳng cho search).
  • pager['page_count'], pager['pages'] — số trang và danh sách nút để template vẽ.
  • url_args — giữ lại tham số khác (như sortby) khi bấm sang trang khác, để không mất lựa chọn sắp xếp.

Route khai hai đường dẫn — /my/the và /my/the/page/<int:page> — để URL trang 2 đẹp là /my/the/page/2.

Kết quả thật: bảng + pager

Chạy thật với _items_per_page = 3, một khách có 4 thẻ sẽ thấy 3 dòng ở trang 1 và một pager "1 2":

Ảnh chụp trang portal my the thật trên Odoo 19. Một bảng có tiêu đề cột Tên thẻ, Hạng, Điểm. Ba dòng dữ liệu sắp theo điểm giảm dần: VIP-0003 hạng vang 150 điểm, VIP-0001 hạng bac 150 điểm, VIP-0002 hạng bac 85 điểm. Bên dưới bảng là thanh phân trang với nút mũi tên lùi, số 1 đang được tô đậm active, số 2, và nút mũi tên tiến. Dưới cùng là footer website với Liên kết hữu ích, Về chúng tôi, Kết nối với chúng tôi

Hình 2: Trang /my/the thật. Bảng do portal.portal_table vẽ (kèm sọc, kẻ dòng), dữ liệu sắp theo diem desc — VIP-0003 (150), VIP-0001 (150), VIP-0002 (85). Pager "1 2" ở dưới do portal_pager sinh; bấm 2 đi tới /my/the/page/2 xem thẻ còn lại. Tất cả là thẻ của chính khách đang đăng nhập.

searchbar_sortings: cho khách tự đổi thứ tự

Dict searchbar_sortings khai các kiểu sắp xếp; portal.portal_searchbar biến nó thành một menu thả xuống trên giao diện:

searchbar_sortings = {
    'diem': {'label': 'Điểm cao nhất', 'order': 'diem desc'},
    'name': {'label': 'Tên thẻ', 'order': 'name'},
}
sortby = sortby or 'diem'
order = searchbar_sortings[sortby]['order']

Khách chọn "Tên thẻ", URL thành /my/the?sortby=name, route đọc sortby, lấy order tương ứng, search sắp lại. Nhờ url_args={'sortby': sortby} trong portal_pager, lựa chọn này được giữ khi chuyển trang. Cùng cơ chế này còn có searchbar_filters (lọc) và searchbar_groupby (nhóm) nếu cần.

Vài lưu ý

  • Miền lọc là hàng rào bảo mật, không phải chuyện hiển thị. Quên partner_id là khách này thấy thẻ khách khác. Với dữ liệu nhạy cảm, cân nhắc thêm sudo() có kiểm soát hoặc dựa vào Record Rules portal.
  • limit/offset phải khớp pager. Dùng pager['offset'] và step = _items_per_page — tự tính tay dễ lệch, trang cuối thiếu/thừa dòng.
  • Giữ tham số qua url_args. Thiếu nó, bấm sang trang 2 là mất luôn kiểu sắp xếp/bộ lọc.
  • Lưu lịch sử để điều hướng. Các module Odoo lưu request.session['..._history'] = ids[:100] để trang chi tiết có nút "trước/sau"; làm theo nếu bạn có trang chi tiết cho từng thẻ.

Ba ý mang về

  1. Route danh sách portal theo khuôn 4 bước: miền lọc theo partner_id → search_count → portal_pager → search với limit/offset từ pager; khai cả /my/the và /my/the/page/<int:page>.
  2. Dùng lại portal.portal_table + portal.portal_searchbar thay vì tự vẽ — có sẵn bảng, pager, menu sắp xếp; searchbar_sortings cho khách đổi thứ tự và giữ được qua url_args.
  3. Miền partner_id = user.partner_id là bảo mật cốt lõi của portal — mỗi khách chỉ thấy bản ghi của mình; đừng để hở.

Ta đã cho khách xem dữ liệu của họ. Chiều ngược lại cũng quan trọng: cho khách gửi dữ liệu vào. Phần sau làm website form nhận dữ liệu từ khách — dựng biểu mẫu công khai, nhận POST, tạo bản ghi, và chống spam/bảo mật khi mở một form ra Internet.