Ở 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:

Ảnh chụp phiên odoo shell blog19 in ra bốn nhóm kết quả, nhóm một cho thấy admin uid 1 user OdooBot su True và with_user cho uid 8 user NV Han Che su False, nhóm hai admin đọc ir.mail_server ra 1 bản ghi còn with_user của demo bị chặn, nhóm ba company mặc định là Công ty TNHH Cà Phê Việt id 1 còn with_company đổi sang Cà Phê Việt Chi nhánh Hà Nội id 2, nhóm bốn allowed_company_ids mặc định None sau with_company thành danh sách chứa 2

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_id mặc định khi tạo bản ghi mới bám theo env.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)

Ảnh chụp đoạn mã Python nền tối liệt kê ba cách đổi ngữ cảnh, dòng một rec chấm sudo giữ user chỉ bật cờ su bỏ qua quyền, dòng hai rec chấm with_user đổi sang nhân viên và áp quyền của họ, dòng ba rec chấm with_company đổi công ty đang hoạt động, kèm hai ví dụ một hàm nhan_vien_co_the_xoa dùng with_user và check_access, và một dòng lấy list_price qua with_company của chi nhánh Hà Nội

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_user chỉ để "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ẫn AccessError. Muốn vượt quyền thì đó là việc của sudo() (và phải cẩn thận như bài trước).
  • with_company khô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ên allowed_company_ids.
  • Cả hai đều trả về recordset mới, không sửa tại chỗ. rec.with_user(u) không đổi rec; 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_id mặc định ăn đúng công ty, thay vì set tay — để Odoo tự điền theo env.company ít sai hơn.

Ba ý mang về

  1. with_user(u) đổi hẳn sang user u và áp ĐÚNG quyền của họ (su=False) — ngược hẳn sudo() (giữ uid, bật su=True để bỏ qua quyền). Dùng with_user khi cần kiểm tra hay chạy code đúng như một người cụ thể.
  2. 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_id mặc định và record rule đa công ty — không đụng tới user hay quyền.
  3. 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.