结论先行
在 Odoo 的库存 / 补货链路里,日期不是“单据上填一次就一路原样传下去”。
至少有三套机制会一起影响它:
stock.rule.delay:在 pull / pull_push 规则创建 move 时,把计划日期往前推- push 规则的新 move 日期:按上一条 move 的日期再往后加
delay - 前序 move 延误感知:如果上游 move 日期晚于当前 move,系统会算出
delay_alert_date,并在相关单据上记录“deadline 因前序延误被更新”的日志
所以很多人看到“交期怎么自己变了”“为什么这里有延误预警”“为什么链路后段的日期会跟前段联动”,答案往往就在 stock_rule.py 和 stock_move.py 这条日期传播链里。
第一层:pull 规则创建 move 时,计划日期会先被往前扣掉 delay
在 /home/ubuntu/odoo-temp/addons/stock/models/stock_rule.py 的 _get_stock_move_values() 里,核心逻辑很直白:
date_scheduled = fields.Datetime.to_string(
fields.Datetime.from_string(values['date_planned']) - relativedelta(days=self.delay or 0)
)
如果还带 date_deadline,也会一起减:
date_deadline = values.get('date_deadline') and (
fields.Datetime.to_datetime(values['date_deadline']) - relativedelta(days=self.delay or 0)
) or False
翻译成人话就是:
下游需要在某天到货,那上游这条 move 就得更早启动,所以规则 lead time 会把计划日期往前推。
这也是补货链路里很常见的“倒排日期”思路。
第二层:push 规则不是前推,而是基于上一条 move 日期往后算
同一个文件里,push 规则走的是另一套逻辑:
return fields.Datetime.to_string(move.date + relativedelta(days=self.delay))
也就是说:
- pull 更像“为了赶上下游目标时间,先把当前动作排早一点”
- push 更像“上一段什么时候完成,下一段就在那个基础上再加几天”
这两者虽然都叫 delay / lead time,但语义方向不一样。
如果把它们混为一谈,就很容易在多步仓路线里看不懂为什么有的日期往前倒、有的日期往后顺。
第三层:propagate_cancel 不只是取消链,也会影响你怎么理解整条补货依赖
在 _get_stock_move_values() 和 _push_prepare_move_copy_values() 里,都会把:
'propagate_cancel': self.propagate_cancel,
带到新建 move 上。
这说明 rule 上的 propagate_cancel 不是 UI 装饰,而是链路依赖的一部分:
- 上游取消时,下游是不是跟着取消
- 这条 move 链是不是仍然保持紧耦合
它虽然不直接“计算日期”,但会影响你如何理解日期变化是否还值得继续传播。
因为一条已经不再有效的依赖链,后续延误和 deadline 联动的业务意义也就不同了。
第四层:系统怎么判断“你延误了”
在 /home/ubuntu/odoo-temp/addons/stock/models/stock_move.py 里,_compute_delay_alert_date() 的逻辑非常朴素:
prev_moves = move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel') and m.date)
prev_max_date = max(prev_moves.mapped('date'), default=False)
if prev_max_date and prev_max_date > move.date:
move.delay_alert_date = prev_max_date
意思就是:
- 找前序 move
- 取最晚的那个日期
- 如果它比当前 move 自己计划的日期还晚
- 那当前 move 就应该亮起延误预警
这个判断很像现实里的依赖任务管理:
前一步最晚都得那天才能完成,那你这一步还写更早的日期,本质上就是已经晚了。
第五层:延误不只是算个字段,还会在业务单据上留痕
stock_move.py 里还有一个很实战的方法:_propagate_date_log_note()。
它会在相关文档上发日志:
msg = _("The deadline has been automatically updated due to a delay on %s.", ...)
也就是说,Odoo 不是只在 move 上悄悄改时间,而是会尽量把“为什么 deadline 变了”这件事留出一条可追溯痕迹。
这对实际项目特别重要,因为业务方最怕的是:
- 交期变了
- 但没人知道为什么变
- 最后只能怀疑系统乱算
有了这条日志,至少链路解释是可追溯的。
第六层:为什么这件事容易被误解
因为开发者常把下面几种时间混在一起:
- 销售承诺时间
- 补货需求时间
date_planned - stock.rule 的
delay - 供应商交期
- move 的
date - move 的
date_deadline - 延误预警
delay_alert_date
它们不是同一个概念,只是在同一条链上互相换算、互相影响。
一旦你只盯某一个表单字段,很容易觉得“系统在偷偷改时间”;但如果回到源码链路,其实大多都有明确的推导关系。
实战排查建议
如果你在项目里遇到日期异常,建议按这个顺序查:
- 这条 move 是 pull 生成的,还是 push 生成的
- 相关 rule 的
delay配了多少 - 上游传进来的
date_planned/date_deadline是什么 - 这条 move 有没有
move_orig_ids - 上游 move 的最晚日期是否晚于当前 move 日期
- 文档 chatter 里有没有自动写入的 deadline 变更说明
这样查,一般比只盯表单界面高效得多。
总结
Odoo 的供应链日期传播,本质上是一条“依赖链上的时间换算规则”:
- pull 规则为了满足下游时间,先把当前动作往前排
- push 规则在上一动作日期基础上往后推
- 上游延误会被当前 move 感知成
delay_alert_date - 关键 deadline 变更还会留日志解释来源
理解了这条链,项目里很多“交期为什么自己变了”的问题,就不再像黑盒,而会变成一条能顺着源码讲清楚的业务逻辑。
DISCUSSION
评论区