先说结论
在 Odoo 里,Bill Control Policy 不是一个“细节配置”,而是采购开票语义的总开关。
它至少会决定三件事:
- PO 什么时候进入
to invoice qty_to_invoice是看订购数量还是看已收数量- 部分收货、退货、超额开票时,系统把异常暴露在哪里
如果你没先看懂这个开关,后面看到的很多现象都会像 bug:
- 明明没收货,为什么已经能开 Vendor Bill?
- 明明退货了,为什么开票状态没变化?
- 明明只收了 5,为什么系统还允许开 10?
这些很多都不是系统乱来,而是 控制策略不同。
真正的分水岭就在 qty_to_invoice
核心逻辑在 purchase/models/purchase_order_line.py:
purchase_method == 'purchase'时qty_to_invoice = product_qty - qty_invoiced- 否则
qty_to_invoice = qty_received - qty_invoiced
别看就两行,这两行基本决定了采购开票世界的两种宇宙。
宇宙一:按订购数量开票
如果产品配置成 ordered quantities,那系统的立场是:
“采购承诺已经成立,就可以按订购数量推进账单。”
这时你很容易看到:
- PO 已确认
invoice_status = to invoiceqty_received = 0
这不是穿帮,而是设计如此。
这种模式通常适合:
- 服务采购
- 先账后货的商业关系
- 对实际仓库收货依赖不强的场景
它的好处是应付流程快,坏处是如果后续收货偏差很大,你就得靠退款、补单、人工对账来收尾。
宇宙二:按收货数量开票
如果产品配置成 received quantities,系统立场就变成:
“先有收货事实,再确认账单数量。”
这时 Vendor Bill 能走多远,取决于采购行上的 qty_received。
而 qty_received 在装了 purchase_stock 后,往往又是从 done 的 stock move 反推出来的,不是人手写一个静态数字。
所以这个模式天生更接近:
- 收货驱动开票
- 数量约束更强
- 与三单匹配语义更一致
为什么很多人会把问题误判成“三单匹配坏了”
因为他们看到的是表象:
- 账单能不能建
- PO 状态怎么跳
- 收货没完成时为什么还能付款
但真正先决定这些表现的,常常不是三单匹配按钮,而是:
- 产品的采购开票控制方式
- 采购行当前的
qty_received - 采购行当前的
qty_invoiced
也就是说:
Bill Control Policy 决定“什么算可开票数量”,三单匹配更多是在这个基础上继续做控制。
退货时,两种策略为什么结果完全不同
Odoo 官方测试把这个差异写得很明白。
按收货数量开票
在 test_vendor_bill_delivered_return 里:
- 先收 10、开票 10
- 再把净收货改成 5
qty_to_invoice直接变成-5
也就是系统明确告诉你:账已经多确认了。
按订购数量开票
在 test_vendor_bill_ordered_return 里:
- 先收 10、开票 10
- 再把收货改成 5
qty_to_invoice仍然是 0
为什么?因为这一宇宙根本不看 qty_received 作为主计量口径。
这就是为什么同样一件退货事,不同公司、不同产品配置下,后续账务表现会完全不同。
实战排错时先看这 4 个问题
1. 产品是按 ordered 还是 received 开票?
这是总前提。
2. qty_received 是 manual 还是 stock_moves?
不同来源会让排查入口不同。
3. qty_invoiced 里有没有 in_refund?
它会把已开票数量减回来。
4. 你遇到的是“不能开票”,还是“开了不该开的票”?
这两类问题往往分别落在不同策略上。
一句话记忆法
Bill Control Policy 决定的不是“按钮显示什么”,而是 Odoo 到底拿订购事实还是收货事实来定义“这笔采购现在还能开多少账”。
DISCUSSION
评论区