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:

Ảnh chụp mã nền tối hai phần. Phần Python: class QuanCaPhePortal kế thừa CustomerPortal nhập từ odoo addons portal controllers portal. Hàm _the_domain lấy partner bằng request env user partner_id và trả về miền lọc partner_id bằng partner id, chú thích chỉ thẻ của chính khách đang đăng nhập. Hàm _prepare_home_portal_values gọi super rồi nếu the_count nằm trong counters thì gán values the_count bằng search_count trên model quan the thanh vien với miền _the_domain. Phần XML: template id portal_my_home_the inherit_id portal portal_my_home, xpath vào div class o_portal_docs position inside, gọi t-call portal portal_docs_entry với t-set title Thẻ thành viên, url gạch chéo my gạch chéo the, count bằng the_count

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:

Ảnh chụp trang Tài khoản của tôi /my thật trên Odoo 19. Lưới các ô gồm Đơn hàng của chúng tôi kèm mô tả thực hiện theo các đơn đặt hàng, Hóa đơn của chúng tôi theo dõi tải xuống hoặc thanh toán, Địa chỉ thêm xóa hoặc chỉnh sửa địa chỉ của bạn, Kết nối và Bảo mật cấu hình các tham số kết nối, và ô mới Thẻ thành viên với mô tả Xem thẻ và điểm tích luỹ của bạ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: Ô "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:

Ảnh chụp mã Python nền tối file addons portal models portal_mixin py Odoo 19. Class PortalMixin kế thừa models AbstractModel với _name portal mixin. Trường access_url kiểu Char Portal Access URL compute _compute_access_url. Trường access_token kiểu Char Security Token copy False. Hàm _portal_ensure_token sinh token bí mật để chia sẻ link không cần đăng nhập, nếu chưa có access_token thì gán bằng secrets token_hex 16 rồi trả về access_token. Chú thích model kế thừa portal mixin có access_url cộng access_token mỗi bản ghi thành một trang my riêng chia sẻ được bằng link token, CustomerPortal _document_check_access lọc theo partner cộng token

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_id là 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 depends portal để kế thừa CustomerPortal và template portal.portal_my_home.
  • _document_check_access là 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ề

  1. Portal (/my) là khu "Tài khoản của tôi" cho khách đăng nhập, dựng bởi controller CustomerPortal (auth='user'); mở rộng bằng cách kế thừa nó và override _prepare_home_portal_values để thêm ô đếm.
  2. 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.
  3. portal.mixin cho mỗi bản ghi một trang riêng với access_url + access_token, để chia sẻ link an toàn cho cả khách chưa đăng nhập; _document_check_access chố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.