Trang website ta làm ở hai phần trước là công khai — ai vào cũng thấy như nhau. Nhưng một khách hàng đăng nhập cần thấy dữ liệu của riêng họ: đơn hàng của họ, hoá đơn của họ, thẻ thành viên của họ — và tuyệt đối không thấy của người khác. Odoo có sẵn cả một hệ thống cho việc này: Portal, khu "Tài khoản của tôi" ở /my. Bài này mổ xẻ cách mở rộng nó: thêm một ô đếm vào trang /my, hiểu CustomerPortal và portal.mixin — bằng cách gắn thật một ô "Thẻ thành viên" vào portal của Odoo 19.
/my: trang chủ của khách
Khi một khách đăng nhập (tài khoản portal), vào /my họ thấy một lưới các ô: đơn hàng, hoá đơn, dự án... Mỗi ô là một "cửa" vào một loại dữ liệu, kèm con số đếm bao nhiêu bản ghi của họ. Toàn bộ do controller CustomerPortal (trong module portal) dựng. Route thật của nó:
@route(['/my', '/my/home'], type='http', auth="user", website=True)
def home(self, **kw):
values = self._prepare_portal_layout_values()
values.update(self._prepare_home_portal_values([]))
return request.render("portal.portal_my_home", values)
auth="user" — bắt buộc đăng nhập. Mỗi module (Bán hàng, Kế toán...) mở rộng controller này để thêm ô của mình. Ta làm y hệt cho thẻ thành viên.
Thêm một ô đếm: override _prepare_home_portal_values
Để thêm ô, ta kế thừa CustomerPortal và override _prepare_home_portal_values — hàm trả về số đếm cho các "huy hiệu" trên trang. Rồi một template kế thừa portal.portal_my_home vẽ ô mới:

Hình 1: Python (trên) đếm số thẻ của partner đang đăng nhập — mấu chốt là miền lọc [('partner_id','=',partner.id)], không phải đếm tất cả. XML (dưới) chèn một ô vào lưới /my bằng cách kế thừa template portal.portal_my_home và gọi portal.portal_docs_entry (ô chuẩn của Odoo).
Chạy thật, ô "Thẻ thành viên" hiện ngay trong lưới /my, nằm cạnh các ô có sẵn của Odoo:

Hình 2: Ô "Thẻ thành viên" (dưới trái) đã hoà vào trang /my thật, cạnh các ô Đơn hàng, Hoá đơn, Địa chỉ có sẵn. Con số đếm là số thẻ của chính khách này — trong lần chạy thật nó ra 1 (một thẻ được gắn với partner đang đăng nhập). Khách khác đăng nhập sẽ thấy con số của họ, không phải của bạn.
Điểm cốt lõi về bảo mật nằm ở miền lọc partner_id = user.partner_id: portal không dựa vào việc bạn nhớ lọc — nó là quy ước xuyên suốt, và bản thân dữ liệu còn được Record Rules của portal chặn thêm một lớp. Một khách không bao giờ đếm hay thấy thẻ của người khác.
portal.mixin: mỗi bản ghi một trang riêng, chia sẻ được
Khi muốn mỗi bản ghi (một đơn, một hoá đơn) có trang chi tiết riêng trong portal, model kế thừa portal.mixin. Nó thêm hai trường quan trọng:

Hình 3: portal.mixin (lõi Odoo 19). access_url là đường dẫn portal của bản ghi; access_token là token bí mật để chia sẻ. Nhờ token, Odoo tạo được link kiểu "xem hoá đơn này" gửi cho khách chưa đăng nhập mà vẫn an toàn — chỉ ai có token mới mở được. CustomerPortal._document_check_access kiểm tra: hoặc bạn là chủ (partner khớp), hoặc bạn cầm đúng token.
Đây là cách Odoo gửi cho bạn một email "Hoá đơn của bạn đã sẵn sàng" với một link bấm vào xem được ngay, không cần tài khoản: link đó mang access_token.
Portal khác website ở đâu
| Website (phần trước) | Portal (/my) |
|
|---|---|---|
| Ai xem | Công khai, ai cũng vào | Khách đã đăng nhập (auth='user') |
| Thấy gì | Nội dung chung như nhau | Dữ liệu riêng của chính khách |
| Lọc dữ liệu | Bạn tự quyết | Theo partner_id + Record Rules portal |
| Chia sẻ không đăng nhập | Mặc định công khai | Qua access_token của portal.mixin |
Nói gọn: website để giới thiệu, portal để khách tự phục vụ trên dữ liệu của họ.
Vài lưu ý
- Đếm phải lọc theo partner. Quên miền
partner_idlà lộ số liệu của người khác — lỗi bảo mật, không chỉ hiển thị. - Module phải
dependsportalđể kế thừaCustomerPortalvà templateportal.portal_my_home. _document_check_accesslà nơi chốt quyền cho trang chi tiết; đừng tự viết lại kiểm tra token — dùng hàm sẵn có để không bỏ sót ca biên.
Ba ý mang về
- Portal (
/my) là khu "Tài khoản của tôi" cho khách đăng nhập, dựng bởi controllerCustomerPortal(auth='user'); mở rộng bằng cách kế thừa nó và override_prepare_home_portal_valuesđể thêm ô đếm. - Mọi thứ lọc theo
partner_id = user.partner_id— mỗi khách chỉ thấy dữ liệu của chính mình; đó là quy ước bảo mật cốt lõi của portal, cộng thêm Record Rules. portal.mixincho mỗi bản ghi một trang riêng vớiaccess_url+access_token, để chia sẻ link an toàn cho cả khách chưa đăng nhập;_document_check_accesschốt quyền.
Ta vừa thêm một ô dẫn tới /my/the. Phần sau xây đúng trang đó: Phần sau dựng trang liệt kê bản ghi của khách trong portal — phân trang, sắp xếp, và lọc đúng dữ liệu của từng người bằng route /my/the hoàn chỉnh.