先说结论
很多人把采购取价问题理解成:
- 系统从若干报价里挑了一个价格
但源码里的真实顺序更接近:
- 先把 不合格的 supplierinfo 排除掉
- 再按排序规则形成候选集
- 最后才从候选集中取一个“当前最合适的供应商记录”
也就是说,先是资格判断,再是供应商选择,最后才是价格落地。
这也是为什么很多现场问题表面像“价格不对”,根因却是:
- 生效日期没命中
- 最小起订量没达到
- 采购单位和供应商单位口径不一致
- 你指定的是子联系人,但系统允许父级伙伴一起匹配
- 当前公司上下文和供应商记录的公司边界不一致
这篇为什么不是“supplierinfo 价格机制”的重复题
站里已经有一篇讲 supplierinfo、数量阶梯和采购取价。
那篇重点是:
- 采购价为什么和你想的不一样
- 多条供应商价格怎么参与取值
这篇换了一个更容易踩坑、但常被忽略的视角:
系统到底先把哪些供应商记录视为“有资格参与这次采购”?
如果资格判断错了,后面的价格讨论根本立不住。
源码主链路:不是直接 _select_seller(),而是“准备 → 过滤 → 排序 → 取一条”
在 /home/ubuntu/odoo-temp/addons/product/models/product_product.py 里,这条链路很清楚:
_prepare_sellers()_get_filtered_sellers()_select_seller()
第一步:_prepare_sellers() 先准备可用卖方记录
它会先拿产品上的 seller_ids,并调用供应商过滤逻辑,再按下面的维度排序:
sequence-min_qtypriceid
这里已经埋了一个很关键的信号:
Odoo 不是简单按最低价排序。
它会先尊重供应商顺序,再考虑数量阶梯和价格。
所以现场里“为什么没有选最便宜那家”并不一定是 bug,而可能是你前面的优先级更高。
第二步:_get_filtered_sellers() 做资格过滤
这一步最值钱,因为它把很多“看似存在的供应商”排除掉了。
源码会依次检查:
date_start/date_endforce_uom与供应商 UoM 是否兼容partner_id是否等于目标供应商,或目标供应商的父级伙伴- 数量换算后是否达到
min_qty - 供应商记录是否绑定到了别的变体
product_id
这就解释了很多现实困惑。
第三步:_select_seller() 才在候选里选一条
默认它还是会围绕 price_discounted 做排序,但如果调用方指定别的优先字段,也可能改变主排序逻辑。
而且源码里还有一个很容易被忽略的处理:
候选集里如果已经选中了某个 partner,后面只继续收同一个 partner 的记录。
这意味着系统并不是“所有供应商混排后取全局第一”,而更像是:
- 先找到满足条件的一组供应商记录
- 再在同一供应商上下文里选最优那条
这对理解多联系人、多报价层级尤其重要。
生效日期为什么经常看起来像没生效
在 _get_filtered_sellers() 里,Odoo 会直接做:
seller.date_start > date→ 跳过seller.date_end < date→ 跳过
所以日期逻辑不是“谁更近就选谁”,而是很硬的资格门槛:
- 未到开始日,不能选
- 超过结束日,不能选
最容易误解的地方在于:
1. 你以为自己在看“今天的采购”
但采购规则可能传进来的是计划日期 date_planned 推出来的采购日期。
也就是说,业务用户看的是“今天在下单”,源码判断的可能是“为了赶上计划收货,这笔采购在业务上应该对应哪一天的供应商条件”。
2. 你以为日期只是价格条件
其实在 Odoo 里,日期首先是“供应商记录有没有资格参加本次选择”。
一旦日期不在范围内,这条记录连价格比较都进不去。
最小起订量为什么不能只按采购数量字面比较
源码不会直接拿采购行数量去和 min_qty 硬比。
它先会做一个动作:
- 如果采购单位
uom_id和供应商单位seller.product_uom_id不一样 - 先把数量换算成供应商单位
- 再比较是否达到
min_qty
这意味着很多“明明买了 10,为什么没命中 5 起订量”的问题,本质上是:
- 你脑子里按“件”理解
- 供应商记录按“箱”理解
- 系统是按供应商单位去判断门槛
这是非常合理的业务设计。
因为 min_qty 表达的不是你的内部计量口径,而是供应商那边的采购条件。
为什么指定了联系人,系统还可能命中父级供应商
源码里有一句很关键:
seller.partner_id not in [partner_id, partner_id.parent_id]才跳过
这说明 Odoo 允许你传入某个联系人或子伙伴时,仍然命中父级供应商记录。
它想解决的是现实世界里很常见的一种情况:
- 采购沟通人是某个联系人
- 但真正挂在产品供应商价目上的,是供应商公司主体
如果不允许父级一起匹配,很多正常采购都会“找不到供应商”。
但这也带来一个排错误区:
你以为系统“选错了联系人”,其实它是在用父级伙伴完成合法匹配。
为什么变体级 supplierinfo 会让人觉得系统在“忽略模板级记录”
过滤逻辑里还有一句:
- 如果
seller.product_id有值,且不等于当前变体,就跳过
这说明:
- 变体级供应商记录优先表达“只给某个变体用”
- 模板级供应商记录表达“整个模板都能用”
因此你在同一个产品模板下看到很多 supplierinfo,不代表每条都对当前变体有效。
如果某条记录绑死在别的变体上,它根本不会进入候选集。
采购自动化里还有一个更隐蔽的回退机制
在 /home/ubuntu/odoo-temp/addons/purchase_stock/models/stock_rule.py 的 _get_matching_supplier() 里,Odoo 先尝试:
- 用显式
supplierinfo_id - 用补货规则上的
supplier_id - 否则走产品
_select_seller()
如果都没拿到,还会有一个 fallback:
- 从
_prepare_sellers(False)里拿当前公司可见的第一条供应商记录
源码注释写得很直白:
这不理想,但比直接阻塞用户更好。
这条回退很重要,因为它解释了另一类现场现象:
- 系统不是完全找不到供应商
- 但用的并不是你以为那条“有效报价”
根因常常就是前面的日期、起订量或单位条件没过,于是系统退回到了一个“能用但不完美”的供应商。
业务上最容易误解的 5 个点
1. 把“价格没命中”误判成“系统算价有问题”
很多时候还没到算价这一步,供应商资格就已经被筛掉了。
2. 只看产品卡上的供应商列表,不看当前计划日期
供应商存在,不代表在这次采购日期上有效。
3. 忽略供应商单位
min_qty 是按供应商单位比较的,不是按你眼前采购行显示单位比较的。
4. 以为指定联系人就只会看该联系人
父级伙伴也可能合法命中。
5. 以为没命中就一定报错
自动补货链路里,系统有时会走回退供应商,而不是立刻失败。
开发和实施时最该注意什么
1. 不要把 supplierinfo 当“纯价格表”
它同时承载:
- 时间范围
- 起订量
- 单位
- 供应商层级
- 公司边界
- 变体边界
只改价格,不校验这些字段,结果常常更乱。
2. 定制 _select_seller() 前,先确认是不是该改 _get_filtered_sellers()
很多客户真正想改变的是资格门槛,而不是排序规则。
如果资格都没进来,你改排序毫无意义。
3. 排错时一定打印“被过滤掉的原因”
这是最省时间的诊断方式。
否则团队会一直围绕“为什么价格不对”打转,却看不到日期、UoM 或 min_qty 才是根因。
4. 多公司环境别只测单公司
供应商记录对公司是否可见,会影响最终候选集。开发时如果只在单公司数据库验证,很容易低估这个问题。
一句话记忆法
Odoo 采购不是先挑价格,而是先做供应商资格审查;生效日期、起订量、单位和伙伴层级没过,根本轮不到比价。
DISCUSSION
评论区