先说结论
Odoo 采购单上的几个数量不是各算各的,它们是一条连锁反应:
qty_received看收货/退货移动怎么记qty_invoiced看供应商账单和退款怎么记qty_to_invoice再看产品的开票策略是按订购还是按收货invoice_status最后只根据qty_to_invoice是否归零来判断
所以只要发生:
- 部分收货
- 退供应商
- 供应商退款
- 改了采购方法
PO 的 Billing Status 就可能继续变化。
一句话概括:
Odoo 的采购开票状态不是“收了就结束”,而是“收货、退货、账单、退款”几条链路一起回算出来的。
这篇为什么不是已有“采购状态排错”重复
站里已有文章已经讲过:
- 采购状态排错的一般思路
- 退货与退款的概念边界
- 按收货/按订购开票的基本区别
这篇换一个更聚焦的场景:
- 部分收货后又退一部分,再补账单或退款时,状态为什么来回变化
也就是说,这篇不是“概览排错”,而是专门拆开:
qty_receivedqty_invoicedqty_to_invoiceinvoice_status
在异常链路里的相互影响。
第一层:qty_received 对可储存采购行来自 stock moves,而不是界面印象
在 addons/purchase_stock/models/purchase_order_line.py 里,消耗品采购行会把 qty_received_method 设成 stock_moves。
真正的计算在 _prepare_qty_received():
收货完成时
如果 move 是正常完成的入库移动,数量会加到 qty_received。
退供应商时
如果 move._is_purchase_return(),并且:
- 没有原始 returned move,或
to_refund = True
系统会把数量减回去。
这就说明退货不是“独立的库存动作”,它会直接影响采购行的已收数量。
某些回转链路会被刻意跳过
源码里还专门排除了几种 edge case,例如:
- 某些 dropship return 到库存而不是供应商
- 某些从采购退货再回转的特殊情形
这说明 qty_received 不是简单的“done move 求和”,而是带业务语义筛选的结果。
第二层:qty_invoiced 会把供应商账单加上去,把供应商退款减回来
在 addons/purchase/models/purchase_order_line.py 里,_prepare_qty_invoiced() 的逻辑很清楚:
in_invoice增加已开票数量in_refund减少已开票数量
所以当你收到供应商退款时,系统不是只在会计里留一笔红字,而是会回头影响采购行的 qty_invoiced。
这一步非常关键,因为后面的 qty_to_invoice 就建立在它之上。
换句话说:
- 供应商退款并不是“另一个世界”的会计动作
- 它会回流到采购状态判断里
第三层:qty_to_invoice 到底按谁算,要看产品 purchase_method
源码在 _compute_qty_invoiced() 里明确分了两种:
按订购开票(purchase)
qty_to_invoice = product_qty - qty_invoiced
这时你有没有收货,不是核心判断因素。
按收货开票(通常是按 received)
qty_to_invoice = qty_received - qty_invoiced
这时部分收货、退货、补收货都会直接改变待开票数量。
这就是为什么同样都是“退了 2 个”,有的 PO 状态会立刻变化,有的却看起来不明显。
根本原因不是界面 bug,而是采购方法不同。
第四层:invoice_status 并不直接看已收数量,它只看 qty_to_invoice
在 addons/purchase/models/purchase_order.py 的 _get_invoiced() 里:
- 只要采购单不在
purchase,状态就是no - 如果任一非展示行
qty_to_invoice != 0,状态就是to invoice - 如果所有行
qty_to_invoice == 0且有账单,状态才是invoiced - 否则还是
no
这就解释了一个很反直觉的现象:
- 你明明看到已经收货
- 但如果
qty_to_invoice还没归零 - Billing Status 仍然会回到
Waiting Bills
所以 invoice_status 不是“收货完成状态”,而是可开票余额状态。
第五层:部分收货 + 退货 + 退款,为什么最容易把人绕晕
举个典型链路:
- PO 下 10 个
- 先收 6 个
- 按收货开票,先开 6 个账单
- 退供应商 2 个,并要求退款
- 供应商开一张退款单
源码层会发生什么?
qty_received:从 6 变成 4qty_invoiced:先到 6,再因in_refund变成 4qty_to_invoice:如果按收货开票,会回到4 - 4 = 0invoice_status:可能回到invoiced
但如果你做了退货,还没做退款:
qty_received = 4qty_invoiced = 6qty_to_invoice = -2
只要不是零,invoice_status 就可能继续显示 to invoice。
这里的“to invoice”其实不一定意味着“该再开正向账单”,而是说明当前采购和账单数量还没对齐。
这也是为什么 purchase_stock 在某些场景下会给供应商账单安排 warning activity,提示你应该去要退款。
第六层:为什么“退货后状态不对”常常其实是退款没跟上
很多实施排错时只盯着库存动作:
- 退货单 done 了没有
to_refund勾了没有
但如果你忘了看会计侧,就会卡在半路。
因为采购单的票据状态不是只看库存,还看:
- 有没有
in_invoice - 有没有
in_refund - 当前
qty_to_invoice是正数、零,还是负数
所以“退货后状态不对”最常见的真实原因其实是:
退货数量已经减了,但退款数量还没减回来。
实战排错顺序
遇到采购单部分收货、退货、开票状态看不懂时,建议按这个顺序查:
- 该采购行
qty_received_method是manual还是stock_moves - 相关 stock move 是否
done - 退货 move 是否属于 purchase return,
to_refund是否正确 qty_invoiced里有没有对应的in_refund- 产品
purchase_method是按订购还是按收货 - 最终
qty_to_invoice是多少,正负都要看
很多所谓“状态乱了”,其实只是你只看了一半链路。
一句话记忆
Odoo 采购的 Billing Status 本质上看的是
qty_to_invoice,而qty_to_invoice又由收货、退货、账单、退款和采购方法一起决定。
DISCUSSION
评论区