Đế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},
    }

Ảnh chụp mã XML và Python nền tối, phần XML một thẻ header chứa hai button, button thứ nhất name action_gia_han type object string Gia hạn thẻ class btn-primary có invisible khi trang_thai bằng dang_dung, button thứ hai name tham chiếu action_them_diem_wizard type action string Cộng điểm class btn-secondary, phần Python định nghĩa method action_gia_han gọi ensure_one rồi write đổi trang_thai sang dang_dung và cộng diem thêm 20 rồi return một action client display_notification với title Đã gia hạn type success và message thẻ cộng 20 điểm, kèm chú thích return có thể là True None act_window hoặc client action

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:

Ảnh chụp form Odoo thẻ VIP-0004 hạng Bạc trạng thái Đã khoá, trên thanh header có hai nút, nút Gia hạn thẻ màu tím đậm là nút primary và nút Cộng điểm màu xám nhạt là nút secondary, thân form hiển thị Mã thẻ VIP-0004, Khách hàng trống, Hạng thẻ Bạc, Trạng thái nhãn đỏ Đã khoá, Điểm tích luỹ thanh tiến độ 0 phần trăm, Số dư 0 đô la

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.client với tag: '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)d bá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_han nhầ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 trong oe_button_box cho 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 button type="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ề

  1. <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ận invisible/readonly theo điều kiện.
  2. 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.
  3. 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.