Bấm một nút trong Odoo và một hộp thoại hiện lên: "Cộng điểm cho các thẻ đã chọn", bạn nhập số, bấm OK, hộp thoại đóng lại và dữ liệu được áp. Cái hộp thoại đó — trong Odoo gọi là wizard — không phải một widget giao diện đặc biệt. Nó là một model đầy đủ, có field, có method, có form view. Điểm khác biệt duy nhất: nó kế thừa models.TransientModel thay vì models.Model, và vì thế dữ liệu của nó tự dọn sau một thời gian ngắn.
Để hiểu tường tận, ở bài này mình tự dựng một wizard thật vào module quan_ca_phe rồi chạy thử — không mô tả suông.
TransientModel: giống Model, nhưng bản ghi có hạn sử dụng
Khai báo một wizard giống hệt khai báo một model thường, chỉ đổi lớp cha:
class ThemDiemWizard(models.TransientModel): # KHÔNG phải models.Model
_name = 'quan.them.diem.wizard'
_description = 'Wizard cộng điểm hàng loạt'
diem_cong = fields.Integer(string='Số điểm cộng thêm', default=10)
the_ids = fields.Many2many('quan.the.thanh.vien', string='Thẻ áp dụng')

Hình 1: Wizard cộng điểm. default_get nạp sẵn the_ids từ active_ids — chính là các bản ghi user đang chọn ở màn hình danh sách khi mở wizard. action_cong_diem là hàm nút bấm gọi tới: nó ghi lên model thật (quan.the.thanh.vien) rồi trả về ir.actions.act_window_close để đóng hộp thoại.
Điểm mấu chốt về ý nghĩa: wizard là công cụ tạm để thu thập input rồi tác động lên dữ liệu thật. Bản thân bản ghi wizard không phải dữ liệu nghiệp vụ cần giữ — nó chỉ sống đủ lâu để user điền form và bấm nút.
Chạy thật: wizard vừa cài, cộng điểm thật
Sau khi cài wizard vào module và nâng cấp, mình chạy thử qua odoo shell để thấy nó hoạt động end-to-end:

Hình 2: Wizard chạy thật. _transient=True. Thẻ VIP-0001 có 120 điểm; mở wizard (nó tự nạp the_ids từ active_ids), nhập +15, gọi action_cong_diem → thẻ thật thành 135. Bản ghi wizard chỉ là phương tiện; kết quả nằm ở model thật.
Vòng đời: bảng thật, nhưng tự dọn
Đây là chỗ khác biệt cốt lõi với model thường, và cũng hay bị hiểu lầm:
- TransientModel VẪN có bảng thật trong database (
quan_them_diem_wizard— kiểm trainformation_schema.tablesthấyTrue). Nó không phải "chỉ trong bộ nhớ". - Nhưng bản ghi tự bị dọn định kỳ bởi cơ chế vacuum của Odoo, dựa trên hai thuộc tính:
_transient_max_hours(mặc định 1.0): bản ghi cũ hơn ngần này giờ bị xoá._transient_max_count(mặc định 0 = không giới hạn theo số lượng): nếu đặt >0, chỉ giữ tối đa ngần đó bản ghi mới nhất.
Nhờ vacuum, bảng wizard không phình to vô hạn dù người dùng mở hàng nghìn hộp thoại mỗi ngày. Bạn không cần tự dọn — Odoo lo.
Hệ quả thực chiến
- Đừng lưu dữ liệu cần giữ lâu vào wizard. Bản ghi wizard biến mất sau ~1 giờ. Kết quả phải được ghi sang model thật (như
action_cong_diemlàm vớithe_ids). default_get+active_idslà cách chuẩn nạp ngữ cảnh. Khi user chọn nhiều dòng ở danh sách rồi mở wizard qua một action, Odoo truyềnactive_ids/active_modeltrong context. Đọc chúng trongdefault_getđể wizard biết đang thao tác trên bản ghi nào.- Field Many2one/Many2many từ wizard tới model thật là bình thường, nhưng model thật không nên tham chiếu ngược tới wizard — quan hệ chỉ một chiều, vì bản ghi wizard sẽ biến mất.
- Quyền truy cập vẫn cần khai trong
ir.model.access.csvcho model transient, y như model thường (mình đã thêm dòngaccess_them_diem_wizardchobase.group_user).
Ba ý mang về
- Mọi wizard là một
TransientModel— khai báo giốngmodels.Model(field, method, view) nhưng kế thừaTransientModel. Nó dùng để thu thập input rồi tác động lên dữ liệu thật, chứ bản thân không lưu dữ liệu nghiệp vụ. - TransientModel có bảng thật nhưng bản ghi tự dọn: vacuum xoá theo
_transient_max_hours(mặc định 1 giờ) và_transient_max_count(mặc định 0 = không giới hạn số lượng) — đã kiểm chứng cả hai giá trị trên wizard vừa dựng. - Nạp ngữ cảnh qua
default_getđọcactive_ids/active_model; hành động (action_...) ghi kết quả lên model thật rồi trảir.actions.act_window_closeđể đóng hộp thoại (đã chạy thật: VIP-0001 từ 120 lên 135).
Còn một họ model đặc biệt nữa, ngược hoàn toàn với transient — model không có bảng riêng mà dùng để chia sẻ code: Phần sau nói về AbstractModel và mixin — cách gói logic dùng lại cho nhiều model khác nhau.