Ở bài trước ta chốt được một điều: sudo() không đổi user, nó chỉ bật cờ env.su=True để bỏ qua kiểm quyền. Nhưng đôi khi thứ bạn cần lại khác hẳn: bạn muốn chạy một đoạn code đúng như thể một nhân viên cụ thể đang chạy nó — áp đầy đủ quyền của họ, không bỏ qua gì cả. Hoặc bạn muốn đổi công ty đang hoạt động trong một hệ thống đa công ty. Hai việc đó là của with_user và with_company.
Ba hàm này (sudo, with_user, with_company) trông na ná nhau vì đều "đổi ngữ cảnh phiên", nhưng chúng động tới ba thứ hoàn toàn khác nhau. Nhầm là ra bug bảo mật hoặc bug đa công ty. Bài này phân biệt chúng bằng dữ liệu thật.
with_user: đổi hẳn danh tính, giữ nguyên hàng rào quyền
record.with_user(u) trả về một recordset chạy dưới danh tính user u: env.uid đổi thành u.id, và — điểm mấu chốt — quyền của u được áp đầy đủ. Đây là chỗ nó khác sudo() một trời một vực.
# đang là admin (OdooBot) trong shell
env['ir.mail_server'].search_count([]) # 1 → admin đọc được
# đổi sang nhân viên hạn chế
env['ir.mail_server'].with_user(demo).search_count([]) # AccessError → dùng ĐÚNG quyền demo
Nhìn số liệu thật chạy trên hệ thống:

Hình 1: Tương phản trực tiếp. Với with_user, uid đổi từ 1 sang 8, và su là False — không hề có chuyện bỏ qua quyền. Bằng chứng: admin đọc được ir.mail_server (1 bản ghi), nhưng with_user(demo) thì AccessError vì nhân viên đó thật sự không có quyền. So với sudo() (giữ uid, bật su=True), with_user gần như ngược lại: đổi uid, tắt su.
Vì with_user áp đúng quyền, nó là công cụ chuẩn để trả lời câu hỏi "user này có được làm việc kia không":
def nhan_vien_co_the_xoa(self, don, nhan_vien):
try:
don.with_user(nhan_vien).check_access('unlink')
return True
except AccessError:
return False
Ở đây ta không muốn bỏ qua quyền — ta muốn kiểm tra chính xác hàng rào quyền có chặn nhân viên đó không. Dùng sudo() ở chỗ này sẽ luôn trả True và câu trả lời trở nên vô nghĩa.
Một lưu ý Odoo 19: phương thức kiểm quyền giờ là check_access(operation) (ném AccessError nếu không được phép), gộp cả kiểm access rights lẫn record rules.
with_company: đổi công ty đang hoạt động
with_company(c) không đụng gì tới user hay quyền — nó đổi công ty đang hoạt động (env.company) và cập nhật allowed_company_ids trong context. Việc này quan trọng trong hệ thống đa công ty vì rất nhiều thứ phụ thuộc công ty:
- field company-dependent (như
list_price, tài khoản kế toán mặc định) trả giá trị của công ty đang hoạt động; company_idmặc định khi tạo bản ghi mới bám theoenv.company;- record rule đa công ty lọc dữ liệu theo công ty đang chọn.
c2 = env['res.company'].search([('id','!=', env.company.id)], limit=1)
sp.with_company(c2).list_price # giá theo bảng giá của công ty c2
Trong dữ liệu thật ở Hình 1: mặc định env.company là "Công ty TNHH Cà Phê Việt" (id 1); sau with_company nó thành "Cà Phê Việt - Chi nhánh Hà Nội" (id 2), và allowed_company_ids chuyển từ None sang [2]. Từ đó trở đi, mọi field phụ thuộc công ty đọc theo chi nhánh Hà Nội.
Bảng phân biệt ba anh em
đổi uid? env.su đổi company? dùng khi
sudo() không → True không cần bỏ qua quyền cho 1 thao tác hệ thống
with_user(u) → u.id → False không chạy đúng như user u, ÁP quyền của họ
with_company(c) không giữ nguyên → c đọc/ghi theo công ty c (đa công ty)

Hình 2: Ba cách và ví dụ thật. Chúng độc lập nhau và ghép được: rec.with_company(c).with_user(u) chạy như user u trong công ty c. Thứ tự không quan trọng vì mỗi cái động tới một phần khác của env.
Vài cạm bẫy thực chiến
- Đừng dùng
with_userchỉ để "nâng quyền". Nó áp đúng quyền của user đó — nếu user đó cũng không có quyền, bạn vẫnAccessError. Muốn vượt quyền thì đó là việc củasudo()(và phải cẩn thận như bài trước). with_companykhông tự lọc dữ liệu của công ty khác biến mất trong mọi truy vấn. Nó đổi công ty đang hoạt động và ảnh hưởng field/mặc định phụ thuộc công ty; việc lọc bản ghi theo công ty vẫn do record rule đa công ty đảm nhận dựa trênallowed_company_ids.- Cả hai đều trả về recordset mới, không sửa tại chỗ.
rec.with_user(u)không đổirec; bạn phải dùng giá trị trả về. Quên gán là như chưa gọi. - Ghi bản ghi qua
with_companyđểcompany_idmặc định ăn đúng công ty, thay vì set tay — để Odoo tự điền theoenv.companyít sai hơn.
Ba ý mang về
with_user(u)đổi hẳn sang useruvà áp ĐÚNG quyền của họ (su=False) — ngược hẳnsudo()(giữ uid, bậtsu=Trueđể bỏ qua quyền). Dùngwith_userkhi cần kiểm tra hay chạy code đúng như một người cụ thể.with_company(c)đổi công ty đang hoạt động (env.company,allowed_company_ids), ảnh hưởng field phụ thuộc công ty,company_idmặc định và record rule đa công ty — không đụng tới user hay quyền.- Ba cái độc lập và ghép được (
with_company(c).with_user(u)); tất cả trả về recordset mới nên phải dùng giá trị trả về, không sửa tại chỗ.
Tới đây ta đã đi hết bộ đổi-ngữ-cảnh của env. Lần sau chuyển sang một công cụ tra cứu bản ghi cực hay dùng khi viết module: Phần sau nói về env.ref() — lấy bản ghi theo external id (XML id) một cách chắc chắn.