先抓主线
销售开票在 Odoo 里不是“点一下按钮,直接生成发票”这么简单。
真正的链路可以拆成三步:
- 先算每条销售行还有多少可开票 →
qty_to_invoice - 再汇总出订单层的开票状态 →
invoice_status - 最后把可开票行转成发票行并创建
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?
这通常和两件事直接相关:
- 产品的 invoicing policy
- 发货 / 交付数量有没有真正推进
为什么很多人误会“销售不能开票”
常见误会是:
- 销售单确认了
- 我就应该能开票
但如果产品配置的是 Delivered Quantities,那 Odoo 会认为:
你只是答应卖了,还没有证明你已经交付,所以现在不能开。
所以“不能开票”往往不是 bug,而是配置 + 履约状态共同导致的自然结果。
这也是 Odoo 很讲业务一致性的地方。
第 2 层:销售行 invoice_status 怎么来的
sale.order.line._compute_invoice_status() 会基于 qty_to_invoice、qty_invoiced、qty_delivered 等字段,把每一行归到几个状态之一:
noto invoiceinvoicedupselling
最常见的判断逻辑
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() 真正做了什么
核心流程大致是:
- 遍历销售订单
- 取出 invoiceable lines
- 调每条行的
_prepare_invoice_lines_vals_list() - 组装成
invoice_vals_list - 调
_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(),本质上是在说:
- 订单行里没有任何真正可开票的内容
- 可能是还没交付
- 也可能是产品发票策略不匹配
- 也可能是下游数量已经全部开完
所以出现这个错误时,第一反应不要是“按钮坏了”,而是按顺序检查:
qty_to_invoice- 产品 invoicing policy
qty_deliveredqty_invoiced_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
评论区