销售开票链

Odoo 销售开票链讲透:invoice_status、qty_to_invoice 与 _create_invoices 是怎么配合的

很多人会用销售开票,但不清楚 invoice_status 为什么变、哪些行能开票、系统如何从销售单生成 account.move。本文把这条链一次讲透。

Odoo 开发 会计 销售
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 11 阅读

先抓主线

销售开票在 Odoo 里不是“点一下按钮,直接生成发票”这么简单。

真正的链路可以拆成三步:

  1. 先算每条销售行还有多少可开票qty_to_invoice
  2. 再汇总出订单层的开票状态invoice_status
  3. 最后把可开票行转成发票行并创建 account.move_create_invoices()

这三步是层层递进的。

如果你调试销售开票问题,一定不要只盯着“创建发票”按钮,很多问题其实在前两层就已经决定了结果。


第 1 层:qty_to_invoice 才是真正的基础

sale.order.line 上,qty_to_invoice 是开票链最核心的数字。

源码逻辑很清楚:

  • 如果产品发票策略是 按订购数量,就用: product_uom_qty - qty_invoiced
  • 如果产品发票策略是 按已交付数量,就用: qty_delivered - qty_invoiced

所以当你说“为什么这个销售行还不能开票”,本质上就是在问:

它的 qty_to_invoice 为什么等于 0?

这通常和两件事直接相关:

  1. 产品的 invoicing policy
  2. 发货 / 交付数量有没有真正推进

为什么很多人误会“销售不能开票”

常见误会是:

  • 销售单确认了
  • 我就应该能开票

但如果产品配置的是 Delivered Quantities,那 Odoo 会认为:

你只是答应卖了,还没有证明你已经交付,所以现在不能开。

所以“不能开票”往往不是 bug,而是配置 + 履约状态共同导致的自然结果。

这也是 Odoo 很讲业务一致性的地方。


第 2 层:销售行 invoice_status 怎么来的

sale.order.line._compute_invoice_status() 会基于 qty_to_invoiceqty_invoicedqty_delivered 等字段,把每一行归到几个状态之一:

  • no
  • to invoice
  • invoiced
  • upselling

最常见的判断逻辑

1. to invoice

只要 qty_to_invoice 不为 0,通常就是可开票。

2. invoiced

如果已开票数量达到或超过目标数量,就认为已经开完。

3. upselling

这个状态很有意思。

它通常出现在:

  • 该产品按订购数量开票
  • 但实际交付数量超过原订购数量
  • 你又不打算继续为多出的部分补开

它更像一个业务提醒:

客户实际用了更多,但你现在没有进一步开票。

很多实施顾问会看到这个状态却不知道为什么冒出来,本质上它是在提示潜在追加销售机会。


第 3 层:订单级 invoice_status 不是简单求和

到了 sale.order 层,Odoo 不会粗暴地看“有没有发票”。

它会聚合各个订单行的 invoice_status,然后再判断整单状态:

  • 有任一行 to invoice → 订单通常 to invoice
  • 全部行都 invoiced → 订单 invoiced
  • 全部行都在 invoiced / upselling 范围 → 订单 upselling
  • 不满足条件 → no

这里有个细节特别值得注意:

Odoo 会排除一些并不适合“单独开票”的特殊行,例如某些折扣 / 促销 / 运费的边界场景。

也就是说,订单层状态并不是机械复制行状态,而是带着业务语义做了一层过滤。


真正创建发票前,系统先找“哪些行能上发票”

点击创建发票时,核心不是直接 create account.move,而是先经过:

order._get_invoiceable_lines(final)

这一步会筛出真正应该进入发票的行。

它会处理:

  • section / subsection
  • note 行
  • 普通商品行
  • down payment 行
  • final invoice 时的负数量修正

这就是为什么你在页面上看到的订单行,不一定会 1:1 原样进入发票。

Odoo 会先把“哪些内容算业务上可开票”整理好。


_prepare_invoice():先建发票头,再塞发票行

_create_invoices() 里,每个订单先调用 _prepare_invoice() 生成发票头信息。

它会把这些内容装进去:

  • 发票类型 out_invoice
  • 客户 / 开票地址 / 收货地址
  • 币种
  • 销售团队
  • fiscal position
  • 付款条款
  • 来源订单号
  • 业务员
  • 交易记录等

也就是说,销售单不是只贡献“发票行”,它还给发票头提供了完整商业上下文。


_create_invoices() 真正做了什么

核心流程大致是:

  1. 遍历销售订单
  2. 取出 invoiceable lines
  3. 调每条行的 _prepare_invoice_lines_vals_list()
  4. 组装成 invoice_vals_list
  5. _create_account_invoices() 创建 account.move

其中有个常被忽略但很重要的实现:

return self.env['account.move'].sudo().with_context(default_move_type='out_invoice').create(invoice_vals_list)

也就是:

  • 创建发票时会 sudo()
  • 因为销售员未必拥有“直接手工创建会计分录”的权限
  • 但在业务流程里,他应当可以从自己的销售单推进到客户发票

这和销售触发库存 move 的设计是同一思路:

业务对象权限底层执行对象权限 分离。


为什么会报 “No items are available to invoice”

这个报错非常经典。

源码里对应 _nothing_to_invoice_error_message(),本质上是在说:

  • 订单行里没有任何真正可开票的内容
  • 可能是还没交付
  • 也可能是产品发票策略不匹配
  • 也可能是下游数量已经全部开完

所以出现这个错误时,第一反应不要是“按钮坏了”,而是按顺序检查:

  1. qty_to_invoice
  2. 产品 invoicing policy
  3. qty_delivered
  4. qty_invoiced
  5. _get_invoiceable_lines() 最终筛出来了什么

一个特别实用的调试顺序

如果某张销售单“看起来应该能开票,但就是不行”,推荐按这个顺序看:

1. 看订单行 qty_to_invoice

这是根因入口。

2. 看订单行 invoice_status

确认系统是否认定它可开票。

3. 看订单 invoice_status

确认整单是不是被某些特殊行影响。

4. 看 _get_invoiceable_lines() 的结果

很多异常死在这里。

5. 看 _prepare_invoice_lines_vals_list()

确认发票行值有没有被定制模块改坏。

6. 看 _create_account_invoices()

确认是否有权限、会计上下文或扩展逻辑问题。


用一句话理解这条链

可以把销售开票链记成:

先算差额数量,再判断状态,最后只把真正“可开票”的销售内容翻译成 account.move

你一旦抓住这句话,再看 Odoo 销售开票,基本就不会迷糊了。

DISCUSSION

评论区

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