Bạn đổi số lượng một dòng trong đơn hàng, và tổng tiền tự nhảy theo — không ai bấm "Tính lại". Đó không phải magic, mà là trường computed kết hợp @api.depends. Đây là một trong những tính năng làm nên sức mạnh của Odoo: dữ liệu tự giữ nhất quán. Mở màn chương "trường thông minh", ta đi vào cơ chế này.
Trường computed là gì?
Trường computed không lưu giá trị do người dùng nhập, mà tự tính từ các trường khác qua một hàm. Ba thành phần:

Hình 1: Ba thành phần. (1) Khai trường với compute='_ten_ham'. (2) Viết hàm _compute_* gán giá trị. (3) Decorator @api.depends(...) liệt kê các trường nguồn — khi chúng đổi, Odoo tự chạy lại hàm. Ba quy tắc bắt buộc: luôn for rec in self, gán ở mọi nhánh, và @api.depends liệt kê đủ trường nguồn.
Xem tận mắt: tổng đơn tự cập nhật
Trường amount_total của sale.order là computed thật. Đây là demo thật trên db blog19 — tôi chỉ đổi số lượng một dòng, tổng đơn tự tính lại:

Hình 2: Demo thật. amount_total của đơn S00013 ban đầu 269.100. Tôi đổi số lượng dòng "Cà phê phin truyền thống 1kg" từ 1 → 6, và không gọi hàm tính nào cả — vậy mà amount_total tự nhảy lên 1.764.100. Trả về 1 thì nó tự về lại 269.100. Lý do: @api.depends('order_line.price_subtotal', ...) báo Odoo rằng khi dòng con đổi, phải chạy lại _compute_amounts.
Ba quy tắc bắt buộc
1. Luôn for rec in self
Hàm compute chạy trên recordset (có thể nhiều bản ghi), nên phải duyệt và gán cho từng bản ghi:
@api.depends('so_luong', 'don_gia')
def _compute_thanh_tien(self):
for rec in self: # BẮT BUỘC duyệt
rec.thanh_tien = rec.so_luong * rec.don_gia
Quên for là gặp lỗi singleton (như bài ensure_one) khi self có nhiều bản ghi.
2. Gán ở MỌI đường đi
Mọi nhánh if đều phải gán giá trị cho trường — kể cả nhánh bằng 0/rỗng. Bỏ sót một nhánh, trường sẽ không được khởi tạo và Odoo báo lỗi "trường chưa được gán":
for rec in self:
if rec.don_gia:
rec.thanh_tien = rec.so_luong * rec.don_gia
else:
rec.thanh_tien = 0.0 # ĐỪNG quên nhánh này
3. @api.depends phải liệt kê ĐỦ trường nguồn
Đây là quy tắc quan trọng nhất và hay sai nhất. @api.depends cho Odoo biết khi nào tính lại. Thiếu một trường nguồn → trường computed không cập nhật khi trường đó đổi (bug âm thầm, rất khó tìm). Có thể xuyên quan hệ bằng dấu chấm:
@api.depends('order_line.price_subtotal') # đổi bất kỳ dòng con nào → tính lại
Chính dòng @api.depends này là thứ khiến tổng đơn ở Hình 2 tự cập nhật khi dòng con đổi.
Vì sao computed lại hay?
- Nhất quán tự động: không bao giờ có tình huống "tổng tiền không khớp các dòng" — Odoo lo việc đó.
- Không cần code nhắc tính: bạn không phải gọi "tính lại" ở khắp nơi mỗi khi dữ liệu đổi.
- Một nguồn sự thật: công thức viết một chỗ (
_compute_*), áp dụng mọi lúc.
Lưu ý
- Trường computed mặc định KHÔNG lưu (không store) — tính mỗi lần đọc. Muốn lưu và tìm kiếm/sắp xếp được thì thêm
store=True(là chủ đề bài sau). - Cho phép sửa tay: thêm
inverse='_inverse_*'để trường computed cũng ghi ngược được (sẽ có bài riêng). - Đừng gọi
writetrong hàm compute — gán thẳngrec.field = ...(Odoo xử lý đúng trong ngữ cảnh compute).
Nhớ ba ý
- Trường computed tự tính từ trường khác qua hàm
_compute_*;@api.dependsbáo Odoo khi nào tính lại — demo thật: đổi dòng con, tổng đơn tự nhảy 269.100 → 1.764.100. - Ba quy tắc bắt buộc:
for rec in self, gán ở mọi nhánh, và@api.dependsliệt kê đủ trường nguồn (xuyên quan hệ bằng dấu chấm). - Thiếu trường trong
@api.depends= bug âm thầm (không cập nhật); computed mặc định không lưu, thêmstore=Trueđể lưu.
Computed mặc định tính lại mỗi lần đọc — có lúc tốt, có lúc tốn. Phần sau so sánh store=True vs không store: khi nào nên lưu trường computed xuống CSDL, đánh đổi hiệu năng và khả năng tìm kiếm.