先说结论
很多人看到 Vendor Bill 价格和采购单、收货成本不一致时,第一反应是:
- 是不是采购价填错了?
- 为什么账单一过账,库存或费用就怪怪的?
但 Odoo 的真实逻辑更像:
采购价差不是一个前端显示问题,而是“收货估值口径”和“账单确认口径”之间的差额,要不要以及怎么记到账里。
特别是在 标准成本 + 实时库存估值 + Anglo-Saxon 场景下,这件事非常关键。
先分清三种价格,不然很容易脑内混线
一条采购业务里,至少可能同时存在:
- PO/收货价格:库存 move 在收货时采用的价值口径
- 产品标准成本:某些会计逻辑里拿来作为估值参考
- Vendor Bill 价格:供应商此刻正式要求你确认的单价
如果这三者完全一致,世界很安静。
但现实里经常会出现:
- 采购单先按一个价格下单
- 货先按一个估值口径收进来
- 最终 Vendor Bill 又改了价格,或者带折扣、汇率差、补差
于是系统必须回答:
- 差额打到库存?
- 打到费用?
- 打到价差科目?
- 还是部分打库存、部分打 COGS?
源码里“价差”是被明确建模的
在 stock_account/models/product.py 里,你能看到 property_price_difference_account_id。
帮助文本写得很直白:
- 在 perpetual valuation 下,这个科目用来承接 标准价与账单价之间的差额。
也就是说,Odoo 从模型层就承认:
采购价差不是异常噪音,而是一个应该被单独落账的业务对象。
Vendor Bill 过账时,系统在看什么
关键逻辑在 purchase_stock/models/account_invoice.py 的 _stock_account_prepare_anglo_saxon_in_lines_vals()。
这段代码说明了几个重点:
- 只在
in_invoice/in_refund/in_receipt且公司启用 Anglo-Saxon 时进入 - 只处理满足库存会计条件的行
- 对标准成本产品,会去找
property_price_difference_account_id - 然后计算
price_unit_val_dif与相关数量relevant_qty - 再额外生成两条分录:
- 一条打到价差科目
- 一条反向修正当前行科目金额
这就说明:
Vendor Bill 过账不是简单确认应付,而是在必要时补做“采购价差分录”。
_get_price_unit_val_dif_and_relevant_qty() 真正在比什么
在 purchase_stock/models/account_move_line.py 里,Odoo 会先算一个 valuation_price_unit:
- 基于产品标准价
- 转成账单行单位
- 再按账单日期换算到对应币种
然后再拿它和账单的 price_unit 去比较,得到:
price_unit_val_dif
换句话说,系统不是在比“PO 行显示价格”和“Bill 行显示价格”这么简单,而是在比:
当前账单的真实单价,与会计估值口径下应该参考的单价之间,差了多少。
这也是为什么汇率、单位、折扣一进来,很多人的直觉就会开始失效。
为什么部分已出库时,价差不会只落在库存里
Odoo 测试 test_price_diff_with_partial_bills_and_delivered_qties 很能说明问题。
它验证的是:
- 部分数量已经卖出 / 发出
- 部分数量还在库里
- 这时再来一张价格不同的 Vendor Bill
系统处理思路不是“一刀切”。
而是:
- 已经流出库存的那部分,不应再全留在库存价值里
- 还在库里的部分,才适合继续反映到库存价值 / SVL
- 因此会出现一部分直接走 COGS,一部分通过库存估值层调整
这很关键,因为它说明:
采购价差不是只看账单,也不是只看库存,而是要看“差额对应的货现在还在不在库里”。
实战里最容易误判的 4 件事
1. 把价差问题理解成采购单改单价失败
它往往已经进入库存估值与会计分录层了。
2. 只看 PO price_unit,不看账单币种和日期汇率
源码比较时会做币种换算。
3. 忽略标准成本 / FIFO / AVCO 的差异
不同成本法下,价差落账路径并不完全一样。
4. 以为所有价差都应继续留在库存里
部分数量已经出库时,差额语义会变。
一句话记忆法
Odoo 的采购价差,本质上是在协调“账单确认的真实采购成本”和“库存/会计当前采用的估值口径”之间的差额;货还在库里和已经流出库里,处理也会不一样。
DISCUSSION
评论区