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:

Trường computed trong Odoo 19 giá trị tự tính từ trường khác: khai trường compute bằng tên hàm _compute rồi định nghĩa hàm _compute và dùng decorator api.depends; ví dụ so_luong là Integer, don_gia là Float, thanh_tien là Float với compute bằng _compute_thanh_tien; decorator api.depends so_luong don_gia nghĩa đổi hai trường này thì tính lại; def _compute_thanh_tien self với for rec in self luôn duyệt vì self có thể là tập nhiều, gán rec.thanh_tien bằng rec.so_luong nhân rec.don_gia; ba quy tắc bắt buộc là một for rec in self gán field cho từng bản ghi, hai gán mọi đường đi mọi nhánh if đều phải gán kể cả bằng 0, ba api.depends đúng liệt kê đủ trường nguồn có thể xuyên quan hệ; api.depends xuyên quan hệ dùng dấu chấm ví dụ order_line.price_subtotal đổi dòng con thì tính lại

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:

Demo computed tự tính lại chạy thật trên sale.order database blog19: định nghĩa thật trong core sale_order.py có decorator api.depends order_line.price_subtotal currency_id và hàm _compute_amounts, amount_total là computed store True; đơn S00013 thay đổi số lượng một dòng, so.amount_total trước là 269.100, dòng là Cà phê phin truyền thống 1kg qty 1 subtotal 260.000; đổi qty 1 thành 6 mà không gọi hàm tính nào, subtotal mới thành 1.560.000 và so.amount_total tự động tính lại thành 1.764.100; trả qty về 1 thì amount_total tự về lại 269.100; kết luận ta chỉ đổi dòng con Odoo tự chạy _compute_amounts nhờ api.depends

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 write trong hàm compute — gán thẳng rec.field = ... (Odoo xử lý đúng trong ngữ cảnh compute).

Nhớ ba ý

  1. Trường computed tự tính từ trường khác qua hàm _compute_*; @api.depends bá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.
  2. Ba quy tắc bắt buộc: for rec in self, gán ở mọi nhánh, và @api.depends liệt kê đủ trường nguồn (xuyên quan hệ bằng dấu chấm).
  3. 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êm store=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.