先说结论
Odoo 里的 invoice_status 不是“用户点了开票后就顺手改一下”的状态。
它更像是一个从业务事实反推出来的结果字段。
这些事实至少包括:
- 订单是否已经确认到
sale - 这条产品按 ordered 还是 delivered 开票
qty_to_invoice现在是多少- 是否已经超交付
- 是否是首付款行
- 整单里能开票的,是否只剩折扣/运费这类“不能单独开”的特殊行
所以它不是一个单点逻辑,而是一条链。
第一层:先算 qty_to_invoice,再谈 invoice_status
在 sale_order_line.py 里,_compute_invoice_status() 并不是直接从天上判断状态。
它先依赖 _compute_qty_to_invoice() 的结果。
而 _compute_qty_to_invoice() 的规则很清楚:
按 ordered 开票
如果产品 invoice_policy == 'order',则:
qty_to_invoice = product_uom_qty - qty_invoiced
按 delivered 开票
否则:
qty_to_invoice = qty_delivered - qty_invoiced
这其实已经解释了很多业务现象:
“能不能开票”首先不是看你想不想开,而是看系统认定还有没有数量差额可开。
第二层:行级 invoice_status 的优先级顺序很重要
源码里行级顺序大概是:
- 订单行不在
sale→no - 是首付款行,且待开票金额为 0 →
invoiced qty_to_invoice不为 0 →to invoice- 按 ordered 开票,且实际交付大于订购 →
upselling - 已开票数量 >= 订购数量 →
invoiced - 其他情况 →
no
这个顺序本身就值得记。
因为它说明:
to invoice的判断优先级高于upsellingupselling不是“多交付就立刻出现”,而是要先满足“当前已没有正常待开票数量”
为什么会出现 upselling
很多人第一次看到 upselling 会以为:
- 系统在鼓励销售加单
- 或者是某个 CRM 机会提醒
但从源码语义看,它更像一个业务提醒:
这条线按订购数量开票,可你已经交付得比原订购量更多了。
最典型的是服务类场景:
- 原本报价 10 小时
- 实际做了 12 小时
- 因为产品按 ordered 开票,所以正常待开票数量已经用完
- 但交付事实比订单承诺更高
这时系统给出 upselling,意思不是“自动多收钱”,而是:
- 这里存在额外价值交付
- 业务上要不要改单、补单、增售,需要你自己决定
所以 upselling 本质上是商业提醒,不是会计动作。
首付款行为什么是特例
源码对 is_downpayment 做了单独判断:
- 只要
untaxed_amount_to_invoice == 0 - 就直接当作
invoiced
这说明首付款行的逻辑核心不是普通商品数量,而是金额余额。
也就是说,invoice_status 虽然经常看起来像“数量状态”,但在首付款场景它其实会回到金额语义。
第三层:整单状态不是简单取“最差的一条线”
sale_order.py 的 _compute_invoice_status() 很值得看。
它会先排掉:
is_downpayment = Falsedisplay_type = False
然后用 _read_group() 聚合订单行状态,再决定整单状态。
规则大意是:
- 只要任何一条有效行是
to invoice,整单通常就是to invoice - 如果所有有效行都是
invoiced,整单才是invoiced - 如果所有有效行都在
invoiced / upselling之间,整单会显示upselling
这说明整单状态是一个聚合后的业务结论,不是某条 line 字段的简单镜像。
一个很容易忽略的细节:不是所有“可开票行”都能单独把整单变成可开票
源码里还有一段特别实战:
当整单里既有 to invoice,又有 no 时,系统会额外检查:
- 当前那些可开票行,是不是其实都属于不能单独开票的特殊行
例如:
- 折扣行
- 运费/促销这类依附型行
如果整单剩下的可开票内容全是这类“单独开出来没有业务意义”的行,订单状态会被压回 no,而不是盲目显示 to invoice。
这就是为什么有些单子看着似乎还有一点点金额差异,但订单头上仍然不是“待开票”。
系统不是算错了,而是在做更贴近业务语义的过滤。
排查销售开票状态时,别只盯订单头
遇到状态不符合预期时,我建议按这个顺序排:
- 先看每条 line 的
product.invoice_policy - 再看
qty_delivered / qty_invoiced / qty_to_invoice - 判断是不是首付款行
- 判断是不是折扣 / delivery 之类特殊行
- 最后再看整单聚合为什么变成
no / to invoice / invoiced / upselling
大多数“状态怪怪的”问题,都不是订单头字段自己出毛病,而是底层 line 事实本来就长那样。
最后一句话
Odoo 的 invoice_status 本质不是手工状态,而是:
把订购、交付、已开票、首付款和特殊行语义综合起来后,对“这张销售单现在还能不能正常开票”给出的业务解释。
理解这点后,to invoice、invoiced、upselling 看起来就不再像三个神秘标签,而是一套很工整的推导结果。
DISCUSSION
评论区