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

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:

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 ý
browse(id)cho tập một;browse([id,...])cho tập nhiều — truy cập trường như.namechỉ hợp lệ trên tập một.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ẽ choValueError: Expected singleton.- 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.