Pattern Odoo mỗi ngày #1: Computed field với store=True và depends chuẩn
Khi nào dùng computed field store=True trong Odoo CE 19, cách khai báo @api.depends đúng để không vỡ ORM cache, và so sánh với related field.
Pattern Odoo m\u1ed7i ng\u00e0y #1: Computed field v\u1edbi store=True v\u00e0 @api.depends chu\u1ea9n
Computed field l\u00e0 m\u1ed9t trong nh\u1eefng c\u00f4ng c\u1ee5 \u0111\u01b0\u1ee3c d\u00f9ng nhi\u1ec1u nh\u1ea5t khi m\u1edf r\u1ed9ng module trong Odoo, nh\u01b0ng c\u0169ng l\u00e0 ch\u1ed7 developer Vi\u1ec7t Nam hay v\u01b0\u1edbng nh\u1ea5t khi m\u1edbi chuy\u1ec3n t\u1eeb Odoo 16/17 l\u00ean CE 19. B\u00e0i \u0111\u1ea7u ti\u00ean trong series "pattern m\u1ed7i ng\u00e0y" s\u1ebd \u0111i th\u1eb3ng v\u00e0o c\u00e2u h\u1ecfi quan tr\u1ecdng nh\u1ea5t: khi n\u00e0o n\u00ean store=True, khi n\u00e0o \u0111\u1ec3 store=False, v\u00e0 l\u00e0m sao khai b\u00e1o @api.depends \u0111\u1ec3 ORM kh\u00f4ng t\u00ednh sai gi\u00e1 tr\u1ecb sau m\u1ed7i l\u1ea7n update.
Hai ch\u1ebf \u0111\u1ed9 c\u1ee7a computed field
M\u1ed9t computed field trong Odoo c\u00f3 hai ch\u1ebf \u0111\u1ed9 v\u1eadn h\u00e0nh r\u1ea5t kh\u00e1c nhau, v\u00e0 vi\u1ec7c ch\u1ecdn sai ch\u1ebf \u0111\u1ed9 l\u00e0 ngu\u1ed3n g\u1ed1c c\u1ee7a ph\u1ea7n l\u1edbn bug "field kh\u00f4ng c\u1eadp nh\u1eadt" m\u00e0 b\u1ea1n th\u1ea5y tr\u00ean forum.
from odoo import api, fields, models
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
margin_amount = fields.Monetary(
string="Margin",
compute="_compute_margin_amount",
store=True,
currency_field="currency_id",
)
@api.depends("price_subtotal", "purchase_price", "product_uom_qty")
def _compute_margin_amount(self):
for line in self:
cost = line.purchase_price * line.product_uom_qty
line.margin_amount = line.price_subtotal - cost
\u1ede v\u00ed d\u1ee5 n\u00e0y, store=True ngh\u0129a l\u00e0 gi\u00e1 tr\u1ecb margin_amount \u0111\u01b0\u1ee3c ghi v\u00e0o c\u1ed9t Postgres t\u01b0\u01a1ng \u1ee9ng v\u00e0 ch\u1ec9 t\u00ednh l\u1ea1i khi m\u1ed9t trong c\u00e1c field trong @api.depends thay \u0111\u1ed5i. Ng\u01b0\u1ee3c l\u1ea1i, n\u1ebfu \u0111\u1ec3 m\u1eb7c \u0111\u1ecbnh store=False, Odoo s\u1ebd t\u00ednh gi\u00e1 tr\u1ecb on-the-fly m\u1ed7i l\u1ea7n record \u0111\u01b0\u1ee3c \u0111\u1ecdc, kh\u00f4ng l\u01b0u v\u00e0o DB v\u00e0 kh\u00f4ng xu\u1ea5t hi\u1ec7n trong c\u00e1c c\u00e2u l\u1ec7nh SQL tr\u1ef1c ti\u1ebfp.
Khi n\u00e0o ch\u1ecdn store=True
Quy t\u1eafc th\u1ef1c d\u1ee5ng: ch\u1ec9 store=True khi field c\u1ea7n ph\u1ee5c v\u1ee5 m\u1ed9t trong ba nhu c\u1ea7u sau.
Th\u1ee9 nh\u1ea5t, field xu\u1ea5t hi\u1ec7n trong search domain ho\u1eb7c filter tr\u00ean tree view. N\u1ebfu user click filter "Margin > 1000" tr\u00ean view, ORM ph\u1ea3i d\u1ecbch th\u00e0nh SQL WHERE margin_amount > 1000. Field kh\u00f4ng stored kh\u00f4ng c\u00f3 c\u1ed9t \u0111\u1ec3 query, Odoo s\u1ebd raise l\u1ed7i ho\u1eb7c fallback v\u1ec1 Python filter c\u1ef1c ch\u1eadm tr\u00ean dataset l\u1edbn.
Th\u1ee9 hai, field \u0111\u01b0\u1ee3c d\u00f9ng trong report SQL view (lo\u1ea1i model v\u1edbi _auto = False v\u00e0 init() build view). C\u00e1c b\u00e1o c\u00e1o BI trong Odoo th\u01b0\u1eddng join tr\u1ef1c ti\u1ebfp v\u00e0o c\u1ed9t v\u1eadt l\u00fd, kh\u00f4ng g\u1ecdi compute method.
Th\u1ee9 ba, field \u0111\u01b0\u1ee3c groupby ho\u1eb7c sort. C\u00f9ng l\u00fd do: groupby trong Odoo d\u1ecbch sang GROUP BY column_name \u1edf SQL.
N\u1ebfu kh\u00f4ng r\u01a1i v\u00e0o ba nh\u00f3m tr\u00ean, m\u1eb7c \u0111\u1ecbnh n\u00ean store=False. T\u00ednh l\u1ea1i on-the-fly t\u1ed1n v\u00e0i ms, nh\u01b0ng ti\u1ebft ki\u1ec7m \u0111\u01b0\u1ee3c to\u00e0n b\u1ed9 chi ph\u00ed migration khi business logic thay \u0111\u1ed5i: gi\u00e1 tr\u1ecb stored c\u0169 trong DB kh\u00f4ng t\u1ef1 update khi b\u1ea1n s\u1eeda code compute, ph\u1ea3i vi\u1ebft script _recompute_todo \u0111\u1ec3 mark dirty h\u00e0ng tri\u1ec7u record.
So s\u00e1nh v\u1edbi related field
Nhi\u1ec1u developer d\u00f9ng compute trong khi t\u00ecnh hu\u1ed1ng ch\u1ec9 c\u1ea7n related. Hai c\u1ea5u tr\u00fac gi\u1ed1ng nhau b\u1ec1 ngo\u00e0i nh\u01b0ng kh\u00e1c ho\u00e0n to\u00e0n v\u1ec1 performance.
class AccountMove(models.Model):
_inherit = "account.move"
partner_country_id = fields.Many2one(
"res.country",
related="partner_id.country_id",
store=True,
readonly=True,
)
related l\u00e0 c\u00fa ph\u00e1p \u0111\u1eb7c bi\u1ec7t: ORM t\u1ef1 generate compute method, t\u1ef1 khai b\u00e1o dependencies d\u1ef1a tr\u00ean dotted path, v\u00e0 t\u1ef1 t\u00ednh l\u1ea1i khi partner_id ho\u1eb7c partner_id.country_id \u0111\u1ed5i. B\u1ea1n kh\u00f4ng vi\u1ebft m\u1ed9t d\u00f2ng Python compute n\u00e0o.
Khi n\u00e0o d\u00f9ng c\u00e1i n\u00e0o? Nguy\u00ean t\u1eafc \u0111\u01a1n gi\u1ea3n: n\u1ebfu gi\u00e1 tr\u1ecb m\u1edbi ch\u1ec9 l\u00e0 ph\u00e9p truy c\u1eadp field qua Many2one chain (nh\u01b0 order_id.partner_id.name), d\u00f9ng related. N\u1ebfu c\u1ea7n t\u00ednh to\u00e1n, conditional, ho\u1eb7c g\u1ecdi method, d\u00f9ng compute. Tr\u1ed9n c\u1ea3 hai th\u01b0\u1eddng l\u00e0 d\u1ea5u hi\u1ec7u \u0111o\u1ea1n code \u0111ang l\u00e0m qu\u00e1 nhi\u1ec1u th\u1ee9 trong m\u1ed9t field.
@api.depends \u0111\u00fang v\u00e0 sai
Decorator @api.depends b\u00e1o cho ORM bi\u1ebft khi n\u00e0o c\u1ea7n invalidate cache v\u00e0 recompute. Sai nh\u1ea5t l\u00e0 qu\u00ean m\u1ed9t dependency, d\u1eabn \u0111\u1ebfn gi\u00e1 tr\u1ecb stale trong DB m\u00e0 developer kh\u00f4ng bi\u1ebft t\u1edbi khi user complaint.
Ba quy t\u1eafc khi vi\u1ebft depends.
@api.depends(
"order_line.price_subtotal",
"order_line.purchase_price",
"currency_id.rate",
)
def _compute_total_margin(self):
for order in self:
margin = sum(
line.price_subtotal - line.purchase_price * line.product_uom_qty
for line in order.order_line
)
order.total_margin = order.currency_id.compute(margin, order.currency_id)
Quy t\u1eafc 1: li\u1ec7t k\u00ea m\u1ecdi field th\u1ef1c s\u1ef1 \u0111\u01b0\u1ee3c \u0111\u1ecdc trong compute, k\u1ec3 c\u1ea3 gi\u00e1n ti\u1ebfp qua relation. Trong v\u00ed d\u1ee5 tr\u00ean, order_line.purchase_price \u0111\u01b0\u1ee3c \u0111\u1ecdc n\u00ean ph\u1ea3i khai b\u00e1o, d\u00f9 n\u00f3 kh\u00f4ng xu\u1ea5t hi\u1ec7n tr\u1ef1c ti\u1ebfp tr\u00ean order.
Quy t\u1eafc 2: v\u1edbi One2many v\u00e0 Many2many, ph\u1ea3i qualify field con b\u1eb1ng c\u00fa ph\u00e1p dotted (order_line.price_subtotal), kh\u00f4ng ch\u1ec9 ghi order_line. Khai b\u00e1o bare order_line ch\u1ec9 trigger khi danh s\u00e1ch record \u0111\u1ed5i, kh\u00f4ng trigger khi gi\u00e1 tr\u1ecb b\u00ean trong record \u0111\u1ed5i.
Quy t\u1eafc 3: n\u1ebfu compute \u0111\u1ecdc m\u1ed9t field th\u00f4ng qua nhi\u1ec1u c\u1ea5p Many2one, ph\u1ea3i khai b\u00e1o t\u1eebng c\u1ea5p. partner_id.commercial_partner_id.country_id.code c\u1ea7n \u0111\u1ee7 chu\u1ed7i.
C\u00f3 m\u1ed9t m\u1eb9o verify: ch\u1ea1y odoo-bin shell v\u00e0 import model, sau \u0111\u00f3 g\u1ecdi env["sale.order"]._fields["total_margin"].depends. N\u1ebfu list tr\u1ea3 v\u1ec1 thi\u1ebfu field b\u1ea1n \u0111ang \u0111\u1ecdc, depends sai.
Gi\u00e1 tr\u1ecb th\u1ef1c t\u1ebf v\u00e0 \u0111o l\u01b0\u1eddng
Tr\u00ean m\u1ed9t database Odoo CE 19 th\u1eed nghi\u1ec7m v\u1edbi 50,000 sale order line, m\u1ed9t computed field store=False m\u1ea5t kho\u1ea3ng 80ms \u0111\u1ec3 render trong tree view 80 row m\u1ed7i l\u1ea7n m\u1edf (do m\u1ed7i row g\u1ecdi compute). C\u00f9ng field v\u1edbi store=True m\u1ea5t 5ms v\u00ec \u0111\u1ecdc tr\u1ef1c ti\u1ebfp c\u1ed9t \u2014 nhanh h\u01a1n 16 l\u1ea7n. Nh\u01b0ng khi update business logic v\u00e0 c\u1ea7n recompute to\u00e0n b\u1ed9, script ch\u1ea1y m\u1ea5t 4 ph\u00fat tr\u00ean dataset \u0111\u00f3. Trade-off r\u00f5 r\u00e0ng: stored = read fast nh\u01b0ng migration ch\u1eadm; non-stored = read ch\u1eadm h\u01a1n nh\u01b0ng zero migration cost.
Quy\u1ebft \u0111\u1ecbnh n\u00ean d\u1ef1a tr\u00ean t\u1ea7n su\u1ea5t read so v\u1edbi t\u1ea7n su\u1ea5t s\u1eeda logic. Field xu\u1ea5t hi\u1ec7n tr\u00ean dashboard m\u1edf 1000 l\u1ea7n/ng\u00e0y \u2192 store=True. Field ch\u1ec9 d\u00f9ng trong wizard \u00edt ng\u01b0\u1eddi d\u00f9ng \u2192 store=False.
Pattern khuy\u1ebfn ngh\u1ecb
Khi t\u1ea1o model m\u1edbi ho\u1eb7c inherit, vi\u1ebft theo tr\u00ecnh t\u1ef1 n\u00e0y: vi\u1ebft compute v\u1edbi store=False tr\u01b0\u1edbc, \u0111\u1ea3m b\u1ea3o logic \u0111\u00fang v\u00e0 test pass. \u0110o th\u1eddi gian render tr\u00ean dataset g\u1ea7n production. N\u1ebfu ch\u1eadm v\u00e0 field c\u00f3 nhu c\u1ea7u search/filter, \u0111\u1ed5i sang store=True v\u00e0 vi\u1ebft migration script update c\u00e1c record c\u0169. \u0110\u1eebng default store=True ch\u1ec9 v\u00ec "cho ch\u1eafc".
Cu\u1ed1i c\u00f9ng, lu\u00f4n c\u1eb7p computed field v\u1edbi unit test trong tests/test_<model>.py. Test ph\u1ea3i verify c\u1ea3 gi\u00e1 tr\u1ecb t\u00ednh \u0111\u01b0\u1ee3c v\u00e0 behavior khi dependencies thay \u0111\u1ed5i (g\u1ecdi write() v\u00e0 check field t\u1ef1 update). Test n\u00e0y r\u1ebb v\u00e0 b\u1eaft \u0111\u01b0\u1ee3c 90% bug stale-cache tr\u01b0\u1edbc khi deploy l\u00ean production.
References: