收退票状态

Odoo 采购单为什么部分收货、退货后开票状态还会变:qty_received、退供应商与 invoice_status 链路讲透

采购现场最常见的疑问之一就是:明明已经部分收货、甚至做了退供应商,为什么 PO 的已收数量、待开票数量和 Billing Status 还在变化?答案不在界面,而在 qty_received、qty_invoiced、in_refund 与 purchase_method 的组合规则里。

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

先说结论

Odoo 采购单上的几个数量不是各算各的,它们是一条连锁反应:

  • qty_received 看收货/退货移动怎么记
  • qty_invoiced 看供应商账单和退款怎么记
  • qty_to_invoice 再看产品的开票策略是按订购还是按收货
  • invoice_status 最后只根据 qty_to_invoice 是否归零来判断

所以只要发生:

  • 部分收货
  • 退供应商
  • 供应商退款
  • 改了采购方法

PO 的 Billing Status 就可能继续变化。

一句话概括:

Odoo 的采购开票状态不是“收了就结束”,而是“收货、退货、账单、退款”几条链路一起回算出来的。


这篇为什么不是已有“采购状态排错”重复

站里已有文章已经讲过:

  • 采购状态排错的一般思路
  • 退货与退款的概念边界
  • 按收货/按订购开票的基本区别

这篇换一个更聚焦的场景:

  • 部分收货后又退一部分,再补账单或退款时,状态为什么来回变化

也就是说,这篇不是“概览排错”,而是专门拆开:

  • qty_received
  • qty_invoiced
  • qty_to_invoice
  • invoice_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 不是“收货完成状态”,而是可开票余额状态


第五层:部分收货 + 退货 + 退款,为什么最容易把人绕晕

举个典型链路:

  1. PO 下 10 个
  2. 先收 6 个
  3. 按收货开票,先开 6 个账单
  4. 退供应商 2 个,并要求退款
  5. 供应商开一张退款单

源码层会发生什么?

  • qty_received:从 6 变成 4
  • qty_invoiced:先到 6,再因 in_refund 变成 4
  • qty_to_invoice:如果按收货开票,会回到 4 - 4 = 0
  • invoice_status:可能回到 invoiced

但如果你做了退货,还没做退款

  • qty_received = 4
  • qty_invoiced = 6
  • qty_to_invoice = -2

只要不是零,invoice_status 就可能继续显示 to invoice

这里的“to invoice”其实不一定意味着“该再开正向账单”,而是说明当前采购和账单数量还没对齐。

这也是为什么 purchase_stock 在某些场景下会给供应商账单安排 warning activity,提示你应该去要退款。


第六层:为什么“退货后状态不对”常常其实是退款没跟上

很多实施排错时只盯着库存动作:

  • 退货单 done 了没有
  • to_refund 勾了没有

但如果你忘了看会计侧,就会卡在半路。

因为采购单的票据状态不是只看库存,还看:

  • 有没有 in_invoice
  • 有没有 in_refund
  • 当前 qty_to_invoice 是正数、零,还是负数

所以“退货后状态不对”最常见的真实原因其实是:

退货数量已经减了,但退款数量还没减回来。


实战排错顺序

遇到采购单部分收货、退货、开票状态看不懂时,建议按这个顺序查:

  1. 该采购行 qty_received_methodmanual 还是 stock_moves
  2. 相关 stock move 是否 done
  3. 退货 move 是否属于 purchase return,to_refund 是否正确
  4. qty_invoiced 里有没有对应的 in_refund
  5. 产品 purchase_method 是按订购还是按收货
  6. 最终 qty_to_invoice 是多少,正负都要看

很多所谓“状态乱了”,其实只是你只看了一半链路。


一句话记忆

Odoo 采购的 Billing Status 本质上看的是 qty_to_invoice,而 qty_to_invoice 又由收货、退货、账单、退款和采购方法一起决定。

DISCUSSION

评论区

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