Bài trước là window action — mở một model ở các view. Nhưng có một họ action khác không mở gì cả, chỉ chạy logic: server action (ir.actions.server). Nó là công cụ để "làm một việc" — cộng điểm, đổi trạng thái, gửi email, tạo bản ghi — khi người dùng bấm nút, chọn từ menu, hoặc khi một sự kiện xảy ra. Bài này dựng một server action thật, chạy nó, và kể luôn cái bẫy sandbox đã chặn tôi ngay lần đầu.

state quyết định loại việc

Mọi server action là ir.actions.server, và field state quyết định nó làm loại việc gì. Loại linh hoạt nhất là code — chạy một đoạn Python:

Ảnh chụp đoạn mã nền tối, bản ghi action_server_tri_an model ir.actions.server với model_id và binding_model_id trỏ tới model thẻ thành viên, state code, field code chứa vòng lặp for the in records gọi the.write cập nhật diem cộng 5 kèm ghi chú phải dùng write vì cấm the.diem cộng bằng; phần liệt kê các state gồm code chạy Python, object_create tạo bản ghi, object_write cập nhật field, multi gọi nhiều action con, mail_post followers next_activity webhook; kèm chú thích cách gắn action qua nút binding_model_id hoặc base automation

Hình 1: Server action loại code. Trong code có sẵn các biến records, record, env, model, log. Chú ý dòng đỏ: phải dùng the.write(...), không được the.diem += 5 — lý do ngay dưới. Bảng state liệt kê các loại: code, object_create, object_write, multi, mail_post...

Cái bẫy: sandbox cấm gán thuộc tính

Lần đầu tôi viết the.diem += 5 cho gọn. Nâng cấp module là lỗi ngay:

odoo.tools.convert.ParseError: while parsing .../server_action.xml
forbidden opcode(s) in '...the.diem += 5...': STORE_ATTR

Field code của server action chạy trong một sandbox safe_eval — vì nó cho phép người dùng (admin) nhập code qua giao diện, Odoo chặn những thao tác nguy hiểm. Trong đó có STORE_ATTR: gán thẳng vào thuộc tính bản ghi (the.diem = ... hay the.diem += ...) bị cấm. Cách đúng là gọi method write():

for the in records:
    the.write({'diem': the.diem + 5})   # ĐÚNG — write() là method, được phép

Bẫy này khó chịu vì thông báo "forbidden opcode" khá lạ; nhớ quy tắc là xong: trong server action code, đổi dữ liệu bằng write()/create(), đừng gán thuộc tính trực tiếp.

Chạy thật và thấy kết quả

Sửa xong, tôi chạy action lên thẻ VIP-0004 (đang 0 điểm) qua odoo shell:

Ảnh chụp phiên odoo shell chạy server action: lấy action bằng env.ref trả về tên Cộng 5 điểm tri ân state code, tìm thẻ VIP-0004, in điểm trước là 0, gọi sa.with_context active_model active_ids rồi run, in điểm sau là 5 nghĩa là code đã cộng 5 điểm bằng write, và binding_model_id trả về quan.the.thanh.vien nghĩa là hiện ở menu Tác vụ

Hình 2: Chạy thật. Lấy action bằng env.ref, gọi .run() với context active_ids (các bản ghi được chọn). Điểm VIP-0004: 0 → 5. binding_model_id trỏ tới model thẻ, nghĩa là action cũng xuất hiện ở menu "Tác vụ".

Gắn action vào đâu

Một server action vô dụng nếu không ai gọi. Ba cách gắn phổ biến:

  • Qua nút trên form: <button name="%(xmlid)d" type="action" .../> — bấm là chạy.
  • Qua binding_model_id: gắn action vào một model để nó tự hiện trong menu "Tác vụ" (Actions) của list/form khi người dùng chọn bản ghi. Vì tôi đặt binding_model_id, action hiện ngay:

Ảnh chụp giao diện Odoo thật danh sách thẻ với dòng chọn hết 4 bản ghi, menu Tác vụ mở ra liệt kê Xuất, Nhân bản, Xoá và ở dưới cùng là Cộng 5 điểm tri ân chính là server action vừa tạo

Hình 3: Chọn các thẻ rồi mở menu "Tác vụ" — "Cộng 5 điểm tri ân" nằm đó cùng các tác vụ chuẩn (Xuất, Nhân bản, Xoá). Đây là hiệu quả của binding_model_id: action tự gắn vào menu ngữ cảnh, không cần thêm nút.

  • Qua base.automation: kích hoạt tự động khi bản ghi được tạo/sửa/đến hạn — server action không cần người bấm.

Ba ý mang về

  1. Server action (ir.actions.server) chạy logic, không mở view; field state chọn loại: code (Python), object_create, object_write, multi, mail_post, followers, next_activity, webhook.
  2. Sandbox code cấm gán thuộc tính (STORE_ATTR) — dùng write()/create() để đổi dữ liệu, không record.field = ...; biến có sẵn: records, record, env, model, log.
  3. Gắn action qua nút (type="action"), qua binding_model_id (hiện ở menu "Tác vụ"), hoặc base.automation (tự chạy theo sự kiện).

Server action chạy ở máy chủ. Bài sau là người anh em ở phía ngược lại: Phần sau về client action — một màn hình do JavaScript (OWL) tự vẽ ở trình duyệt, khi các view có sẵn không đủ cho thứ bạn muốn hiển thị.