Bạn đã gặp: mở form đơn hàng, chọn khách hàng, và bảng giá/điều khoản/địa chỉ tự điền theo. Hay chọn một tùy chọn thì hiện cảnh báo ngay. Đó là @api.onchange — cơ chế phản ứng tức thì trên form đang nhập. Nó cực tiện cho trải nghiệm người dùng, nhưng cũng là thứ hay bị dùng sai thay cho ràng buộc. Nối tiếp @api.constrains và SQL constraint, bài này làm rõ vai trò thật của onchange.

onchange: tự điền và cảnh báo trên form

@api.onchange('field') chạy method mỗi khi người dùng đổi trường đó trên form (chưa lưu). Bên trong, bạn gán các trường khác để tự điền, hoặc return {'warning': {...}} để cảnh báo:

Cách viết @api.onchange trong Odoo 19 phản ứng ngay khi người dùng nhập trên form: decorator api.onchange partner_id chạy khi người dùng chọn partner trên form, def _onchange_partner_goi_y_ma self, một là tự điền gợi ý mã thẻ nếu có partner_id và chưa có name thì self.name bằng TV cộng id partner, hai là cảnh báo không chặn nếu partner_id.customer_rank bằng 0 thì return dict warning với title Lưu ý message Khách hàng này chưa từng mua hàng; onchange khác constrains đừng nhầm, api.onchange chạy trên form đang nhập để gợi ý tự điền cảnh báo không lưu không chặn không chạy khi tới từ API hoặc import, còn api.constrains chạy lúc lưu ở mọi nguồn là hàng rào thật; đặc điểm onchange gán self.field để tự điền ô khác trên form, return warning để hiện hộp cảnh báo không chặn, chạy trên bản ghi ảo chưa lưu chỉ trong phiên nhập của form

Hình 1: Một @api.onchange('partner_id') thật. Khi người dùng chọn khách hàng: (1) tự điền name = 'TV-<id>' nếu ô còn trống; (2) nếu khách chưa mua (customer_rank == 0) thì return {'warning': {...}} để hiện cảnh báo. Chú ý khác biệt cốt lõi với @api.constrains ở phần dưới.

Cho nó chạy thật

Tôi thêm onchange trên vào module quan_ca_phe, nâng cấp, rồi giả lập form đang nhập (T.new({}) tạo bản ghi ảo chưa lưu). Đây là output thật:

Output api.onchange chạy thật trên module quan_ca_phe database blog19: giả lập form đang nhập bằng T.new tạo bản ghi ảo chưa lưu, form.name trả False chưa có gì; người dùng chọn khách hàng đã mua Nguyễn Văn An id 7, form.partner_id bằng partner_da_mua thì onchange chạy, form.name trả TV-7 tự điền; chọn khách chưa mua customer_rank 0 thì onchange trả cảnh báo, form2.partner_id bằng partner_chua_mua, res bằng onchange trả về res warning dict với title Lưu ý message Khách hàng này chưa từng mua hàng, form2.name trả TV-3 vẫn tự điền nhưng kèm cảnh báo; onchange chỉ gợi ý không lưu không chặn muốn chặn thì dùng api.constrains

Hình 2: Output thật. form = T.new({}) là bản ghi ảo (name = False). Gán partner_id = khách đã mua → onchange chạy, name tự điền thành 'TV-7'. Với khách chưa mua (customer_rank = 0), onchange trả về một dict warning với "Khách hàng này chưa từng mua hàng." — form vẫn tự điền name, kèm hộp cảnh báo. onchange chỉ gợi ý, không lưu gì.

onchange KHÁC constrains — đừng nhầm

Đây là nhầm lẫn nguy hiểm nhất của người mới. onchange không phải hàng rào bảo vệ dữ liệu:

@api.onchange @api.constrains
Chạy khi Đổi trường trên form (chưa lưu) Lưu (create/write)
Nguồn Chỉ giao diện form Mọi nguồn (form, API, import, shell)
Mục đích Tự điền, gợi ý, cảnh báo Chặn dữ liệu sai
Chặn được không Không (chỉ cảnh báo) Có (raise → rollback)

Điểm chí mạng: onchange không chạy khi dữ liệu đến từ API, import, hay create trong code. Nên nếu bạn dựa vào onchange để "kiểm tra hợp lệ", dữ liệu xấu vẫn lọt qua API. Muốn bảo vệ dữ liệu chắc chắn, luôn dùng @api.constrains/SQL constraint — onchange chỉ để hỗ trợ trải nghiệm nhập liệu.

Ba việc onchange làm tốt

  1. Tự điền phụ thuộc: chọn khách → điền địa chỉ giao/bảng giá; chọn sản phẩm → điền đơn giá/đơn vị.
  2. Cảnh báo mềm: nhắc nhở nhưng vẫn cho làm (như "khách chưa mua", "tồn kho thấp").
  3. Lọc lựa chọn động: đổi domain của một trường Many2one khác theo giá trị vừa chọn.

Vài lưu ý

  • Chạy trên bản ghi ảo: trong onchange, self là bản ghi chưa lưu (NewId), nên đừng gọi những thứ cần id thật (như tạo bản ghi liên kết).
  • Đừng viết logic nặng/ghi CSDL trong onchange — nó chạy mỗi lần gõ, và không nên có tác dụng phụ lên CSDL.
  • Trùng lặp với computed: nhiều trường hợp "tự điền" nên dùng trường computed (chạy cả khi tới từ API) thay vì onchange (chỉ form). Cân nhắc: cần đúng ở mọi nguồn → computed; chỉ hỗ trợ nhập tay → onchange.

Nhớ ba ý

  1. @api.onchange('field') chạy khi người dùng đổi trường trên form: gán self.x = ... để tự điền, return {'warning': {...}} để cảnh báo — đo thật: name tự điền 'TV-7', khách chưa mua ra warning.
  2. onchange KHÁC constrains: chỉ chạy trên form, không chạy từ API/import, không chặn được — dựa vào nó để kiểm hợp lệ là lỗ hổng.
  3. Bảo vệ dữ liệu → @api.constrains/SQL constraint; "tự điền" cần đúng ở mọi nguồn → computed; onchange chỉ để hỗ trợ nhập tay.

Nói về "tự điền", có một cơ chế điền giá trị ban đầu khi tạo bản ghi mới. Phần sau đi vào default và default_get — cách đặt giá trị mặc định tĩnh và động cho trường khi tạo mới.