先说结论
Odoo 的采购去重提醒,不是在比“这两张单明细是不是一模一样”。
在 /home/ubuntu/odoo-temp/addons/purchase/models/purchase_order.py 里,_compute_duplicated_order_ids() 和 _fetch_duplicate_orders() 关注的核心条件是:
- 同公司
- 同供应商
- 非取消状态
- 当前单有
partner_ref - 并且满足以下任一:
- 当前单
origin = 另一张单.name - 当前单
partner_ref = 另一张单.partner_ref
换句话说,它识别的是:
业务引用关系上的疑似重复
而不是“采购行逐行完全相同”的重复。
为什么它只对 draft 单做这件事
源码里先把记录限定为:
state == 'draft'
这背后的意思很清楚:
- 去重提醒的最佳时机,是你还没正式确认前
- 一旦单据已进入正式流程,系统更倾向于保留事实,而不是继续强行怀疑它重复
这很合理。
因为草稿阶段的重复,通常意味着:
- 录单员重复录了
- 邮件导入重复生成了 RFQ
- 供应商参考号被重复使用
而确认后的重复,往往已经变成需要业务判断和审计处理的问题,不适合再用“轻提醒”一把梭。
它为什么强依赖 partner_ref
_fetch_duplicate_orders() 里首先把当前单过滤成:
- 有
id - 且有
partner_ref
没有 partner_ref 的单据,直接不参与这条重复识别。
这说明 Odoo 设计者认为:
- 采购内部编号
name是自己生成的 - 真正最容易反映“外部世界是不是同一笔业务”的,是供应商参考号
这在现实里非常成立。
因为重复 RFQ 最常见的线索,不是两个内部单号相似,而是:
- 同一家供应商
- 同一个 vendor quotation / acknowledgement / reference
- 却被系统或人工录成两张草稿单
origin = duplicate_po.name 这一句为什么很值钱
这行条件看起来不起眼,其实很有意思。
它不是在比较两个 origin 是否相等,而是检查:
- 当前单的
origin - 是否直接指向另一张采购单的
name
这能抓住一种很常见但容易漏掉的重复模式:
- 某张新 RFQ 并不是完全独立创建
- 而是从另一张采购单、补货链或整合流程衍生出来
- 结果又被当成独立采购需求继续处理
也就是说,Odoo 在尝试识别“从别的采购单长出来的重复分支”。
为什么它不用采购行明细做比对
很多人第一次看到这个逻辑,会问:
- 为什么不比产品、数量、价格?
答案其实很现实:
1. 明细级查重成本更高
不仅查询更重,误报率也高。
2. 采购重复往往先暴露在引用层
例如同一个 vendor reference 被录了两次,比“两张单正好买一样的东西”更有说服力。
3. Odoo 这里做的是提醒,不是法务级判定
它要的是高性价比地找出“值得你看一眼”的可疑草稿,而不是给你一个绝对判决。
最容易误解的 4 个点
1. 以为所有采购单都会被重复识别
不是,重点是草稿单。
2. 以为没提示就一定不重复
如果没有 partner_ref,这条链根本不会工作。
3. 以为提示重复是因为采购行完全一样
源码主判断不看明细。
4. 以为这是阻断逻辑
它更像风险提醒,不是绝对禁止你继续操作。
排错顺序
如果你想解释“为什么这张 RFQ 被判重复 / 为什么没被判重复”,建议按这个顺序看:
- 当前单是否仍为 draft
- 当前单是否有
partner_ref - 同公司同供应商下,是否有别的非取消采购单共享同样
partner_ref - 当前单
origin是否等于另一张采购单的name - 如果仍不符合预期,再考虑是否需要你自己的明细级查重扩展
一句话记忆法
Odoo 采购重复识别查的不是“明细像不像”,而是“这张草稿单在业务引用上像不像另一张已存在的采购单”。
DISCUSSION
评论区