开票状态

Odoo 销售单为什么一会儿 To Invoice、一会儿又变 No:invoice_status、qty_to_invoice 与交付边界讲透

从 sale 与 sale_stock 源码讲清 Odoo 销售订单和销售订单行的 invoice_status 到底怎么算,为什么按订购、按交付、down payment、特殊行与已完成 move 会让状态看起来忽左忽右。

库存 销售
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 9 阅读

先说结论

Odoo 里销售单显示 To Invoice / Invoiced / No / Upselling,并不是只看“有没有开发票”,而是分两层算:

  1. 先算每一条 sale.order.lineqty_to_invoiceinvoice_status
  2. 再把整张 sale.order 的状态聚合出来

而影响这件事的关键,不只是发票,还包括:

  • 产品的 invoice policy(按订购 / 按交付)
  • qty_delivered
  • qty_invoiced
  • down payment 行
  • 是否是折扣/运费/奖励等“不能单独开票”的特殊行
  • sale_stock 对已完成 stock move 的特殊兜底逻辑

所以很多人看到的“状态忽左忽右”,其实不是随机,而是多层规则叠在一起。


一、先看订单级状态:整张销售单不是自己算的,是从订单行聚合出来的

addons/sale/models/sale_order.py 里,sale.order._compute_invoice_status() 的逻辑很清楚:

  • sale 状态的订单,直接是 no
  • 只要有任一正常订单行是 to invoice,订单通常就是 to invoice
  • 如果所有相关行都 invoiced,订单就是 invoiced
  • 如果所有相关行都在 invoiced / upselling,订单就是 upselling
  • 否则就是 no

而且它明确过滤了:

lines_domain = [('is_downpayment', '=', False), ('display_type', '=', False)]

也就是说,订单级状态在聚合时,会先排除:

  • down payment 行
  • display_type 行(章节标题、备注等)

这已经解释了第一个常见现象:

明明有些行在动,为什么整张订单状态看起来没跟着变?

因为订单级状态看的不是“所有行”,而是“参与正式聚合的业务行”。


二、行级状态才是关键:真正决定方向的是 qty_to_invoice

sale.order.lineinvoice_status 本质上不是“我有没有发票”,而是“我现在还剩多少可以开票”。

源码里核心判断是:

elif not float_is_zero(line.qty_to_invoice, ...):
    line.invoice_status = 'to invoice'

这句话很重要。

它说明:

行只要还有 qty_to_invoice,优先就是 to invoice

所以真正要先盯的是 qty_to_invoice 怎么算,而不是先盯 invoice_status


三、qty_to_invoice 又取决于 invoice policy:按订购和按交付完全是两套口径

在 sale 模块里,核心分支是:

  • 按订购(invoice_policy == 'order'
  • qty_to_invoice = product_uom_qty - qty_invoiced
  • 按交付
  • qty_to_invoice = qty_delivered - qty_invoiced

这就是为什么同样一张销售单,不同产品会表现完全不同。

按订购产品

确认销售单后,即使还没发货,只要还没开票,就可能立刻显示 To Invoice

按交付产品

即使订单已经确认,如果 qty_delivered 还是 0,那么 qty_to_invoice 也可能是 0,于是状态不是 To Invoice。

所以很多业务同学说:

“单都确认了,为什么还不能开票?”

答案往往不是权限,而是:

这条产品按交付开票,系统口径还没认定你有可开票数量。


四、qty_delivered 不是手写的,它常常来自 sale_stock 的 stock move 统计

到了 sale_stock,消耗型/可库存产品会把交付口径切到 stock_move

addons/sale_stock/models/sale_order_line.py 里:

  • _compute_qty_delivered_method() 会把合适的产品设为 stock_move
  • _prepare_qty_delivered() 会统计 outgoing moves 和 incoming moves
  • done 的 outgoing 加数量
  • done 的 incoming 减数量

也就是说:

  • 发出去多少,qty_delivered 往上加
  • 退回来多少,qty_delivered 往下减

所以它看的不是“你主观觉得发了多少”,而是已完成库存移动的净效果。

这也解释了几个高频现象:

1)部分交付后,按交付产品只出现部分可开票

因为 qty_to_invoice = qty_delivered - qty_invoiced

2)退货后,原先可开票数量会回落

因为 incoming move 会把 delivered 数往回扣。

3)库存 move 没 done,状态就还不到位

因为 sale_stock 统计的是已完成 move,而不是草稿或已分配 move。


五、为什么有时明明部分交付,状态却直接显示 Invoiced

sale_stock 里还补了一段很实战的特殊逻辑:

当满足这些条件时:

  • 行在 sale
  • 当前基础计算结果是 invoice_status == 'no'
  • 产品是实体商品
  • 产品按交付开票
  • 有 move
  • 并且这些 move 全部已经 donecancel,且至少有一个 done

系统会把它强制认定成:

line.invoice_status = 'invoiced'

这段逻辑是为了解决一个现实问题:

某些按重量、按实际拣货、按包装差异出货的商品,实际交付量不一定和订购量精确相等,但业务上已经结束,不想让它永远挂在一个暧昧状态里。

这也是为什么你偶尔会看到:

  • 数量没严格对齐
  • 但状态却已经是 Invoiced

它不是算错,而是 sale_stock 明确做了业务兜底。


六、down payment 行是另一套语义,别和普通销售行混着理解

行级状态里还有一个特殊判断:

elif line.is_downpayment and line.untaxed_amount_to_invoice == 0:
    line.invoice_status = 'invoiced'

这说明 down payment 行不是按普通 qty_to_invoice 的思路走,它更偏向“金额语义”,而不是“实物数量语义”。

所以:

  • 押金/预付款行
  • 普通商品行

虽然都挂在销售订单行上,但 invoice_status 的判断口径并不完全一样。

这也是为什么如果你把预付款、折扣、运费和普通商品混在一起肉眼判断,很容易误会系统状态。


七、为什么订单级状态有时会从 To Invoice 又掉回 No

这是用户最容易困惑的一类现象。

常见原因有三个:

原因 1:按交付产品的 qty_delivered 回落了

例如发生退货或修正,导致 qty_to_invoice 回到 0。

原因 2:能开票的只剩特殊行

源码里有一段很细的判断:如果当前只剩一些不能单独开票的特殊行(比如折扣/配送/奖励类),订单级状态会避免把整单标成 To Invoice。

换句话说:

不是只要某行 technically 可开票,整单就一定显示 To Invoice。

系统还会判断这些剩余行是不是“有意义单独开”。

原因 3:订单已经不在 sale 聚合口径里

例如状态变化、取消、非 sale 场景等。


八、最常见的排错顺序

如果业务问你“为什么这张单不是 To Invoice / 为什么又变回 No”,最有效的排查顺序不是先看发票,而是按下面来:

第一步:看订单行,不要先看订单头

订单头只是聚合结果,真正的源头在 sale.order.line

第二步:看这几个字段

  • product_id.invoice_policy
  • qty_delivered
  • qty_invoiced
  • qty_to_invoice
  • invoice_status
  • is_downpayment
  • display_type

第三步:如果是库存产品,再看 move

重点看:

  • move_ids.state
  • 是 outgoing 还是 incoming
  • 有没有 done
  • 有没有退货把 delivered 数扣回去

第四步:判断是不是特殊行在误导你

例如:

  • delivery fee
  • discount line
  • reward line
  • down payment

这些行的存在,常常会让“肉眼看应该能开票”的直觉失准。


九、一个最实用的心智模型

可以把销售开票状态理解成三层:

第一层:业务口径

产品到底按订购开票,还是按交付开票?

第二层:行级数量

  • 订购了多少
  • 交付了多少
  • 已经开了多少
  • 现在还剩多少可开

第三层:订单级聚合

  • 普通业务行里有没有 to invoice
  • 剩下的是不是只能单独开一些“意义不大”的特殊行
  • 是否该显示 invoicedupselling

只要按这三层看,invoice_status 的大多数异常都能拆开。


结语

Odoo 的 invoice_status 不是一个“发票有/无”的简单灯泡,而是销售、库存、预付款和特殊业务行共同作用后的结果。

真正的主轴不是“有没有发票”,而是:

  • 这条行按什么口径可开票
  • 现在还有没有 qty_to_invoice
  • sale_stock 有没有根据真实库存完成情况调整交付口径
  • 订单头在聚合时又有没有把特殊行排除或降权

理解这条链之后,再遇到 “To Invoice / No / Invoiced” 来回切换,就不会觉得它神神叨叨了——大多数时候,它只是比我们肉眼判断更严格,也更业务化。

DISCUSSION

评论区

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