Đến giờ form của ta chỉ hiển thị dữ liệu. Nhưng người dùng cần làm việc: bấm "Gia hạn thẻ" để đổi trạng thái và cộng điểm, bấm "Cộng điểm..." để mở một hộp thoại nhập số. Những nút đó là <button>, và chúng có hai loại chính: type="object" gọi thẳng một method Python, type="action" mở một action (wizard, màn hình khác). Bài này dựng cả hai và xem nút "Gia hạn thẻ" thật sự cộng 20 điểm.
Hai loại nút
Đặt nút trong <header> ở đầu form. Mỗi loại type hướng tới một thứ khác nhau:
<header>
<!-- type="object": bấm → gọi method Python cùng tên trên model -->
<button name="action_gia_han" type="object"
string="Gia hạn thẻ" class="btn-primary"
invisible="trang_thai == 'dang_dung'"/>
<!-- type="action": bấm → mở một action (đây là wizard) -->
<button name="%(action_them_diem_wizard)d" type="action"
string="Cộng điểm..." class="btn-secondary"/>
</header>
Và method Python mà nút type="object" gọi tới:
def action_gia_han(self):
self.ensure_one()
self.write({'trang_thai': 'dang_dung', 'diem': self.diem + 20})
return { # trả về action → hiện thông báo
'type': 'ir.actions.client', 'tag': 'display_notification',
'params': {'title': "Đã gia hạn", 'type': 'success',
'message': "Thẻ %s +20 điểm." % self.name},
}

Hình 1: Hai loại nút. type="object" → name là tên method trên model; bấm là gọi method đó với self là bản ghi đang mở. type="action" → name là id của một action (dùng %(xmlid)d để lấy id từ external id); bấm là mở action đó. Method có thể return một action (như thông báo ở đây), hoặc True/None để làm xong và tải lại form.
Xem thật: nút trên form
Mở thẻ VIP-0004 (đang khoá), header hiện hai nút:

Hình 2: Header với hai nút. "Gia hạn thẻ" (tím, btn-primary) là nút type="object" — hiện ra vì thẻ đang khoá (không phải "đang dùng"). "Cộng điểm..." (xám, btn-secondary) là nút type="action" mở wizard cộng điểm. Nút cũng nhận invisible="..." như field — ẩn/hiện theo trạng thái bản ghi.
Bấm "Gia hạn thẻ" trên thẻ đang khoá gọi action_gia_han: mình đã chạy thử method này — thẻ chuyển từ khoá/0 điểm sang đang dùng/20 điểm, và một thông báo "Thẻ VIP-0004 +20 điểm" hiện lên (do method return một display_notification). Đúng như thiết kế.
Method trả về gì?
Giá trị return của method quyết định điều gì xảy ra sau khi bấm:
True/None: làm xong, form tự tải lại (thấy dữ liệu mới).- Một
ir.actions.act_window(dict hoặc từenv.ref(...).read()): mở một màn hình khác — ví dụ mở danh sách đơn hàng liên quan. - Một
ir.actions.clientvớitag: 'display_notification': hiện thông báo (như ví dụ), không rời form. env.ref('module.action').report_action(self): xuất một báo cáo PDF.
Cạm bẫy: thứ tự nạp và đúng class
Hai lỗi mình vấp khi dựng bài này, đáng ghi nhớ:
- Nút
type="action"tham chiếu action phải được khai TRƯỚC. Odoo nạp XML theo thứ tự; nếu record action nằm sau form view trong file,%(action_them_diem_wizard)dbáo "External ID not found". Đặt record action lên trước view dùng nó. - Method phải nằm trong đúng class. Nếu file có nhiều model, viết
action_gia_hannhầm vào class khác thì view báo "action_gia_han is not a valid action on quan.the.thanh.vien". Method của nút phải thuộc model của form.
Vài lưu ý Odoo 19
- Đặt nút trong
<header>cho nút quy trình (đầu form), hoặc trongoe_button_boxcho nút thống kê (góc phải, kiểu "12 Đơn hàng"). class="btn-primary"(nổi bật) /btn-secondary(phụ) /btn-link(như link) quyết định kiểu nút.confirm="Bạn chắc chứ?"trên button để hỏi xác nhận trước khi chạy.context="{...}"trên buttontype="action"truyền ngữ cảnh cho action mở ra (ví dụactive_idđể wizard biết đang thao tác thẻ nào).
Ba ý mang về
<button type="object" name="method">gọi thẳng method Python trên bản ghi;<button type="action" name="%(xmlid)d">mở một action (wizard/màn hình). Đặt trong<header>; nút cũng nhậninvisible/readonlytheo điều kiện.- Method của nút quyết định hậu quả qua
return:True/None(tải lại),act_window(mở màn khác),client display_notification(thông báo), report (PDF). Đã chứng minh: "Gia hạn thẻ" đổi khoá/0 → đang dùng/20 + hiện thông báo. - Cạm bẫy: action tham chiếu phải khai trước view dùng nó (nếu không "External ID not found"); method phải nằm trong đúng class của model form.
Nói tới quy trình đổi trạng thái, có một widget chuyên cho việc đó — dải trạng thái bấm được ở đầu form: Phần sau mổ xẻ statusbar — biến một field trạng thái thành quy trình các bước (nháp → xác nhận → hoàn tất) mà người dùng bấm để chuyển.