Ở bài trước ta biết mọi thứ trong Odoo là recordset — tập bản ghi. Hệ quả trực tiếp: truy cập một trường như .name chỉ hợp lệ khi tập có đúng một bản ghi. Làm sai là gặp ValueError: Expected singleton — lỗi ám ảnh nhất với người mới. Bài này dạy cách lấy đúng một bản ghi và dùng ensure_one để lỗi đó không bao giờ làm bạn bất ngờ.

browse: từ id thành recordset

Khi đã có id (từ URL, từ tham số, từ một bản ghi khác), dùng browse để lấy recordset:

Cách lấy đúng bản ghi trong Odoo 19 với browse duyệt và ensure_one: browse từ id thành recordset, gọi P.browse với một id là 1 trả về tập một phần tử res.partner 1, gọi P.browse với list 1 6 10 trả về tập nhiều res.partner 1 6 10; duyệt bằng vòng for partner in nhieu thì mỗi vòng partner là tập một phần tử nên chấm name hợp lệ; ensure_one dùng để chốt chắc chắn chỉ có một bản ghi, trong hàm action_xac_nhan gọi self.ensure_one nếu self không phải tập một sẽ ném ValueError ngay, sau đó dùng self.state bằng done yên tâm; bẫy kinh điển là truy cập trường trên tập nhiều, gán x bằng nhieu.name sẽ lỗi ValueError Expected singleton, còn nhieu.mapped name thì trả về list tên của cả sáu

Hình 1: browse(1) (một id) cho tập một phần tử; browse([1,6,10]) (một list) cho tập nhiều. Khi duyệt bằng for, mỗi vòng lặp cho một recordset một phần tử — nên .name trong vòng lặp luôn hợp lệ. ensure_one() là lời chốt "tôi chắc chắn chỉ có 1"; sai thì nổ lỗi ngay.

Nhìn lỗi thật tận mắt

Đây là output thật từ odoo shell — cả trường hợp qua lẫn trường hợp lỗi:

Output thật chạy trong odoo shell trên database blog19: gán nhieu bằng search is_company True là tập 6, vòng for r in nhieu duyệt và đếm bằng 6; browse 1 trả về res.partner 1 là tập một, browse list 1 6 10 trả về res.partner 1 6 10 là tập nhiều; ensure_one trên tập một qua OK và name bằng Công ty TNHH Cà Phê Việt; ensure_one trên tập nhiều nổ lỗi ngay ValueError Expected singleton res.partner 15 24 6 1 10 23; truy cập chấm name trên tập nhiều cùng lỗi ValueError Expected singleton; ghi chú Expected singleton nghĩa là tôi cần tập một mà bạn đưa tập nhiều là lỗi hay gặp nhất của người mới

Hình 2: Output thật. ensure_one() trên tập một phần tử qua êm. Trên tập 6 phần tử thì nổ ValueError: Expected singleton: res.partner(15, 24, 6, 1, 10, 23) ngay lập tức. Và nhieu.name — truy cập trường trên tập nhiều — cho đúng cùng lỗi đó. "Expected singleton" dịch nôm na: "tôi cần tập một, mà bạn đưa tập nhiều".

ensure_one() là bạn, không phải thù

Người mới hay khó chịu với lỗi này. Nhưng ensure_one() thực ra bảo vệ bạn: nó biến một lỗi ngầm, khó lần thành một lỗi rõ ràng, đúng chỗ.

def action_gui_email(self):
    self.ensure_one()          # nếu ai đó gọi trên nhiều bản ghi → báo NGAY tại đây
    template = self.env.ref('mymod.email_template')
    template.send_mail(self.id)

Không có ensure_one(), nếu self lỡ là tập nhiều, lỗi sẽ nổ ở đâu đó sâu bên trong template.send_mail(self.id) — khó truy. Có nó, lỗi báo ngay dòng đầu, kèm đúng recordset gây ra. Quy tắc: hàm nào giả định "chỉ một bản ghi" thì đặt ensure_one() ở dòng đầu.

Khi nào KHÔNG cần ensure_one

Nút bấm trên form (action) thường chạy trên một bản ghi, nhưng nút trên danh sách (list) có thể chạy trên nhiều bản ghi được chọn. Nếu hàm của bạn cố ý xử lý cả tập, đừng dùng ensure_one — hãy duyệt:

def action_xac_nhan(self):
    for order in self:        # xử lý từng bản ghi, chạy được cho cả 1 lẫn N
        order.state = 'done'
        order.date_done = fields.Datetime.now()

Mẫu for record in self: là cách viết an toàn nhất — chạy đúng dù self là tập 1 hay tập 500.

Lấy đúng một bản ghi từ tập nhiều

  • rs[0]: phần tử đầu (theo thứ tự _order). Cẩn thận: rs[0] trên tập rỗng sẽ lỗi index.
  • search([...], limit=1): cách chuẩn để lấy "một cái", luôn trả recordset (rỗng nếu không thấy).
  • rs.filtered(...)[:1]: lọc rồi lấy tối đa một — an toàn cả khi rỗng ([:1] không lỗi).
partner = env['res.partner'].search([('email','=',email)], limit=1)
if partner:                    # tập rỗng là falsy
    partner.ensure_one()       # thừa nhưng nói rõ ý định; giờ dùng field an toàn
    print(partner.name)

Nhớ ba ý

  1. browse(id) cho tập một; browse([id,...]) cho tập nhiều — truy cập trường như .name chỉ hợp lệ trên tập một.
  2. ensure_one() đặt ở đầu hàm giả định "một bản ghi" để lỗi báo rõ ràng, đúng chỗ; tập nhiều sẽ cho ValueError: Expected singleton.
  3. Hàm xử lý cả tập thì duyệt for record in self: (an toàn cho 1 lẫn N); lấy một cái thì search(..., limit=1).

Đọc và lấy bản ghi thành thạo rồi, giờ tới lúc tạo dữ liệu. Phần sau đi vào create() — tạo một bản ghi và tạo hàng loạt bản ghi trong một lệnh, kèm khác biệt hiệu năng.