先看现象
很多开发者都会疑惑:
- 我只是改了
invoice_date - 为什么
date也跟着变了? - 为什么分录行的日期还要一起重算?
这不是“前端表单自动帮你改了一下”,而是 Odoo 在模型层专门设计的一条重算链。
入口其实很清楚:_compute_date()
在 addons/account/models/account_move.py 里,和这个问题直接相关的是:
@api.depends('invoice_date', 'company_id', 'move_type', 'taxable_supply_date')
def _compute_date(self):
for move in self:
accounting_date = move._get_accounting_date_source()
if not accounting_date or not move.is_invoice(include_receipts=True):
if not move.date:
move.date = fields.Date.context_today(self)
continue
if not move.is_sale_document(include_receipts=True):
accounting_date = move._get_accounting_date(accounting_date, move._affect_tax_report())
if accounting_date and accounting_date != move.date:
move.date = accounting_date
self.env.add_to_compute(move.line_ids._fields['date'], move.line_ids)
self.env.add_to_compute(self._fields['name'], move)
这段代码告诉我们三件事:
invoice_date是依赖源之一date不是完全独立的字段line_ids.date也在同一条重算链上
_get_accounting_date_source() 先决定“看谁做源头”
它的实现很直接:
def _get_accounting_date_source(self):
self.ensure_one()
return self.invoice_date or self.date
也就是说,系统会优先看 invoice_date。
如果它没有值,才回退到 date。
这就是为什么很多场景里,开发者只改 date 看起来没效果:
因为模型逻辑已经把 invoice_date 当成更上游的日期来源了。
为什么销售单和采购单还不完全一样
在 _compute_date() 里,有一个分支很关键:
- 如果不是销售类单据,Odoo 还会调用
_get_accounting_date()做进一步换算 - 同时它会考虑税务报表影响
_affect_tax_report()
这说明会计日期不只是“用户选了哪一天”,还要受税务和公司规则影响。
对开发者来说,这里最重要的理解是:
日期字段在 Odoo 里常常不是一个独立值,而是业务、税务和公司逻辑共同算出来的结果。
为什么要 add_to_compute()
这一句最容易被忽略:
self.env.add_to_compute(move.line_ids._fields['date'], move.line_ids)
self.env.add_to_compute(self._fields['name'], move)
它的意思不是“立刻把这些字段改掉”,而是把它们加入后续重算队列。
这样做的好处是:
- 避免半成品状态被别的逻辑提前读到
- 让依赖字段按统一节奏重算
- 减少手工改一个字段却漏掉相关字段的风险
你可以把它理解成:
Odoo 不只是在改字段,而是在安排一整批依赖字段重新计算。
为什么新手总觉得“它偷偷改了我的值”
因为模型层有自己的优先级:
- 用户手工输入是一个来源
- 计算字段和依赖链也是来源
- 业务规则可能还会再修正一次
当这些来源冲突时,Odoo 会遵循模型定义里的依赖关系,而不是纯粹听表单里的最后一次输入。
所以,定制会计单据时不能只看界面,必须看字段依赖和重算逻辑。
实战开发建议
-
不要把
invoice_date和date当成完全等价字段 它们在某些单据上是相关的,但逻辑上不是一回事。 -
修改日期相关逻辑时,要一起检查分录行 很多会计 bug 都是主表和行表日期不同步导致的。
-
如果你扩展了日期计算,记得补依赖 否则
@api.depends不会触发你期望的重算。 -
先看重算链,再看表单表现 页面上看到的值,往往已经经过多轮模型层处理。
一句话总结
invoice_date 之所以会带着 date 和分录行一起变,不是因为表单层做了联动,而是因为 account.move._compute_date() 把它们放进了同一条重算链。
这正是 Odoo ORM 的典型风格:一个关键字段变化,相关依赖字段就要一起重新计算。
DISCUSSION
评论区