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:

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:

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 đặtbinding_model_id, action hiện ngay:

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ề
- Server action (
ir.actions.server) chạy logic, không mở view; fieldstatechọn loại:code(Python),object_create,object_write,multi,mail_post,followers,next_activity,webhook. - Sandbox
codecấm gán thuộc tính (STORE_ATTR) — dùngwrite()/create()để đổi dữ liệu, khôngrecord.field = ...; biến có sẵn:records,record,env,model,log. - Gắn action qua nút (
type="action"), quabinding_model_id(hiện ở menu "Tác vụ"), hoặcbase.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ị.