Một danh sách vài chục bản ghi thì trả hết một lần cũng được; vài nghìn thì không — trang nặng, tải chậm, người dùng lạc. Phân trang là bắt buộc. Ở phần portal ta đã dùng portal_pager có sẵn; bài này rút gọn cơ chế chung cho một route tự viết bất kỳ — bốn bước tính toán, và (quan trọng hơn) những cạm bẫy biến một trang danh sách thành trang trắng hay lỗi offset. Kiểm chứng bằng curl thật.
Bốn bước phân trang
Mọi phân trang server-side đều là bốn phép tính đơn giản: đếm tổng, suy ra số trang, lấy page từ URL và kẹp nó về khoảng hợp lệ, rồi search với limit/offset.

Hình 1: Bốn bước. (1) search_count đếm tổng. (2) page_count = ceil(total/IPP). (3) page = max(1, min(page, page_count)) — dòng clamp cứu bạn khỏi mọi giá trị page bậy. (4) offset = (page-1)*IPP, rồi search(..., limit=IPP, offset=offset). Khai hai route (/qcp_ds và /qcp_ds/page/<int:page>) để URL trang 2 đẹp là /qcp_ds/page/2.
Kết quả thật: hai trang, và clamp
Chạy trên 4 thẻ với items_per_page=2, curl từng trang:

Hình 2: Trang 1 (offset 0): VIP-0001, VIP-0003 — trang_truoc: null, trang_sau: /qcp_ds/page/2. Trang 2 (offset 2): VIP-0002, VIP-0004 — trang_sau: null. Và clamp thật: /page/99 → page=2 (về trang cuối), /page/0 → page=1. Không có trang trắng, không lỗi offset âm.
Ba cạm bẫy hay gặp
Phân trang trông đơn giản nhưng ba lỗi này rất phổ biến:
-
Không clamp
page. Người dùng (hay bot) gõ?page=99,?page=-1,?page=abc. Không kẹp thìoffsetthành số khổng lồ hoặc âm → trang trắng hoặc lỗi. Dòngpage = max(1, min(page, page_count))giải quyết hết. Route dùng<int:page>cũng đã lọc bỏ chữ; nhưng đọc từ?page=thì phải tự ép kiểu và bắt lỗi. -
Đánh mất tham số khác khi sang trang. Nếu khách đang sắp theo "tên" hoặc lọc "hạng vàng", bấm trang 2 mà URL không mang theo
sortby/filterlà mất lựa chọn — trang 2 hiện danh sách khác kiểu. Luôn nhét các tham số hiện tại vào link trang trước/sau (ở đây?sortby=%s). -
offsetkhông khớplimit.offset = (page-1)*IPP,limit = IPP— cùng mộtIPP. Tự chế công thức lệch (ví dụoffset = page*IPP) là bỏ sót/trùng bản ghi, và trang cuối thiếu số dòng.
Tự tính hay dùng portal_pager?
Hai lựa chọn, cùng bốn bước bên trong:
- Tự tính (như bài này) khi cần toàn quyền kiểm soát cấu trúc dữ liệu trả về (JSON cho API, hay logic phân trang đặc thù). Gọn, không phụ thuộc module
portal. portal_pager(from odoo.addons.portal.controllers.portal import pager) khi làm trang portal/website — nó trả sẵnoffset,page_count, danh sách số trang để templateportal.portal_tablevẽ pager đẹp, kèm dấu…khi nhiều trang. Xem phần 172.
Cả hai đều tính đúng offset và clamp — điểm khác là bạn tự dựng giao diện hay dùng khung có sẵn của Odoo.
Ba ý mang về
- Phân trang = 4 bước:
search_count(tổng) →page_count = ceil(total/IPP)→ clamppage = max(1, min(page, page_count))→search(limit=IPP, offset=(page-1)*IPP); khai/routevà/route/page/<int:page>. - Luôn clamp
pageđể?page=99/0/âm không tạo trang trắng hay offset lỗi; và giữ tham số (sortby, filter) trong link trang trước/sau, kẻo mất lựa chọn khi chuyển trang. - Tự tính cho API/logic riêng (gọn, không cần
portal);portal_pagercho trang portal/website (có sẵn pager đẹp) — cùng công thức, khác cách dựng giao diện.
Ta đã lo mọi thứ bên trong Odoo. Phần sau bước ra ngoài: Phần sau mổ xẻ gọi Odoo từ chương trình ngoài bằng XML-RPC — cách một script Python/PHP/bất kỳ ngôn ngữ nào đăng nhập và đọc/ghi dữ liệu Odoo từ xa qua API chuẩn.