多币种取价

Odoo 供应商价目为什么在多币种下更容易看起来“选错”:日期区间、最小起订量与币种排序链讲透

站里已经讲过 Odoo 会先按日期和最小起订量过滤 supplierinfo,但很多现场真正困惑的是:同一家供应商挂了多币种价目后,系统到底按什么口径比较?答案是先过滤资格,再把折后价统一换算到公司币种排序,而采购单币种又往往来自供应商采购币种属性。

采购
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

很多团队看到多币种 supplierinfo 时,会本能地以为 Odoo 的逻辑是:

  • 先看采购单币种
  • 只在同币种记录里挑最便宜的一条

这并不准确。

从源码看,真实顺序更接近:

  1. 先按日期区间、最小起订量、伙伴、变体、UoM 等条件做资格过滤
  2. 再把候选记录的 折后价格 统一换算到公司币种排序
  3. 最终只在首个命中的供应商伙伴上下文里取最优 seller
  4. 真正落到采购行价格时,再把 seller 的币种转换成 PO 币种

所以更准确地说:

Odoo 不是先按 PO 币种选价,而是先筛资格,再按公司币种做跨币种可比排序,最后再把中选价格换回采购单币种。

这就是为什么多币种场景特别容易让人觉得“系统选错了”。


这篇为什么不是已有“日期/起订量选中链路”文章的重复

站里已有一篇重点讲:

  • 生效日期怎么过滤
  • 最小起订量怎么过滤
  • 父子伙伴与变体如何影响 seller 候选资格

那篇核心问题是:

  • 谁有资格参加本次采购

这篇往后走一步,专讲两个更容易踩坑、但经常被忽略的点:

  • 有资格之后,不同币种记录怎么比较
  • 中选 seller 的币种与 PO 币种、供应商采购币种属性之间是什么关系

所以这篇的主问题不是“有没有资格”,而是:

  • 跨币种候选一旦同时合格,系统到底按什么口径判定谁更优

第一关键点:资格过滤仍然先于币种比较

/home/ubuntu/odoo-temp/addons/product/models/product_product.py 里,_get_filtered_sellers() 还是第一道门。

它先过滤掉这些不合格记录:

  • date_start / date_end 不在范围内
  • 数量换算到供应商单位后仍低于 min_qty
  • 伙伴不匹配当前供应商或其父级伙伴
  • 强制 UoM 场景下单位不兼容
  • 记录绑定到了别的产品变体

这意味着:

  • 多币种并不会跳过前面的资格门槛
  • 你看到多条 USD、EUR、CNY 的价目,不代表它们都能进最终排序

所以排查“为什么没选那条 EUR 价格”时,第一步永远不是汇率,而是:

  • 那条记录有没有先通过日期、起订量和对象范围过滤

第二关键点:真正排序时,Odoo 用的是公司币种

_select_seller() 最关键的一段,在于 sort_function()

  • 它会把 record.price_discounted
  • record.currency_id._convert(...)
  • 转到 record.env.company.currency_id

也就是说,跨币种比较不是直接拿原币种数值比大小,而是:

先把候选 seller 的折后价格统一换算成公司币种,再排序。

这个设计非常合理,因为:

  • 10 USD 和 10 EUR 本来就不能直接比
  • 如果不统一折算,就没有真正可比较的价格基准

但它也解释了一个常见错觉:

  • 用户看 PO 是 EUR
  • 觉得系统应该优先按 EUR 口径选
  • 结果源码其实先按公司币种做候选排序

所以“PO 币种”和“候选排序币种”并不是同一个概念。


第三关键点:系统比的是 price_discounted,不是裸价 price

源码里默认排序字段不是简单的 price,而是:

  • price_discounted

而排序元组默认是:

  • price_discounted
  • sequence
  • id

这意味着多币种比较时,Odoo关心的是:

  • 折扣后的有效采购价

不是你在界面上肉眼盯着的挂牌单价。

如果同一家供应商:

  • 一条 USD 记录价格高但折扣大
  • 另一条 EUR 记录价格低但折扣小

最后谁更优,取决于 折后价换算到公司币种后的结果

这也是很多团队明明“看到了更低标价”,却还是觉得系统选错的原因。


第四关键点:中选后只会继续保留同一 partner 的记录

_select_seller() 里还有一个很容易被忽略的行为:

  • 如果 res 为空,先收第一条 seller
  • 后续只有 res.partner_id == seller.partner_id 的记录才继续加入

这意味着系统不是把所有供应商、所有币种记录混成一个全局大池后取最便宜。

它更像:

  1. 先按预排序走一遍候选 seller
  2. 一旦先命中某个 partner
  3. 后续只在这个 partner 的记录里继续收敛
  4. 最终返回该 partner 下最优的一条

所以多币种问题经常不是“为什么没在所有记录里取全局最低价”,而是:

  • 为什么排序时先把某个 partner 送进了候选优先位

这会受:

  • sequence
  • 日期和起订量过滤后剩余记录的结构
  • 折后价换算结果

共同影响。


第五关键点:采购单币种往往先由供应商采购币种决定

/home/ubuntu/odoo-temp/addons/purchase/models/purchase_order.py 里,_compute_currency_id() 会优先取:

  • partner_id.property_purchase_currency_id

没有的话才退回:

  • company_id.currency_id

这又解释了另一个常见困惑:

  • 明明产品上有多币种 supplierinfo
  • 为什么新建 PO 时订单默认就已经是某个币种

因为 PO 币种的默认来源,往往先是供应商主数据上的采购币种属性,而不是你本次中选 seller 的币种。

也就是说,系统默认先决定了:

  • 这张 PO 主要用什么采购币种表达

之后采购行价格再根据中选 seller 的币种做转换。


第六关键点:真正写回采购行时,才把 seller 币种换到 PO 币种

/home/ubuntu/odoo-temp/addons/purchase/models/purchase_order_line.py_compute_price_unit_and_date_planned_and_name() 里,如果有 selected_seller_id

  1. 先取 seller 的价格
  2. 处理税含税口径
  3. 再把 selected_seller_id.currency_id 转到 line.currency_id
  4. 然后再换算到采购行单位

所以流程不是:

  • 先把 PO 币种限制死候选

而是:

  • 先在 seller 世界里选出最优记录
  • 再把它翻译到这张 PO 的币种与单位上

这点特别关键。

因为它说明多币种场景里要区分三套口径:

  • seller 记录原币种
  • 公司币种排序口径
  • PO 最终展示币种

你把这三者混成一个,就一定会觉得系统逻辑很怪。


实战里最容易误解的 5 个点

1. 以为系统先只看和 PO 同币种的 seller

并不是。排序前的关键口径是公司币种,而不是 PO 币种。

2. 以为 supplierinfo 原价最低就一定中选

默认比较的是折后价,还会统一换算币种。

3. 以为多币种问题只和汇率有关

前面的日期、生效区间、起订量、伙伴和变体过滤往往更先决定生死。

4. 以为 PO 币种来自中选 seller

默认更多来自供应商主数据上的采购币种属性。

5. 以为系统会在所有供应商所有价目里取全局最低价

源码在命中首个 partner 后,会继续局限在同一 partner 上下文里选。


排错顺序

如果你觉得 Odoo 在多币种 supplierinfo 下“选错价”,建议按这个顺序查:

  1. 候选记录有没有通过日期区间过滤
  2. 采购数量换算后是否满足该记录的 min_qty
  3. 伙伴是否命中联系人/父公司允许范围
  4. 公司币种下的折后价换算结果分别是多少
  5. PO 币种是不是来自供应商采购币种属性
  6. 最终采购行是否又经过 PO 币种和 UoM 转换

这样查,基本就能把“资格问题”和“币种比较问题”分开。


一句话记忆法

Odoo 多币种 supplierinfo 的核心不是“按 PO 币种挑价”,而是“先过滤资格,再把折后价统一换算到公司币种排序,最后再转成采购单币种落账”。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。