先说结论
很多人把 Odoo 的 Down Payment Invoice 理解成:
- 销售还没完整交付
- 先手工开一张普通发票
- 之后再想办法对账
这其实低估了 Odoo 设计。
在源码里,sale.advance.payment.inv 并不是“帮你快点开票”的便捷按钮,而是在做一件更正式的事:
把“预付款”单独建模成一种受控的销售开票语义。
它关心的不只是“生成一张发票”,还关心:
- 这是普通发票,还是预付款发票
- 是按百分比,还是固定金额
- 税额怎么拆
- 销售订单上如何留下预付款行锚点
- 后续正式开票时如何扣减已开预付款
所以如果你把它理解成“先开一张票”,很多边界都会看错。
源码入口:sale.advance.payment.inv
在 addons/sale/wizard/sale_make_invoice_advance.py 里,这个向导有三种核心模式:
delivered:普通开票percentage:按百分比开预付款fixed:按固定金额开预付款
这已经说明它不是一个单一路径按钮,而是一个“销售开票策略选择器”。
普通开票和预付款开票,在源码里一开始就分叉了
create_invoices() 会走 _create_invoices(self.sale_order_ids)。
而在 _create_invoices() 里,第一层判断就是:
如果是 delivered
直接委托给:
sale_orders._create_invoices(final=self.deduct_down_payments, grouped=not self.consolidated_billing)
这还是普通销售开票主链,只是这个向导顺手作为入口。
如果是 percentage / fixed
就不再走普通发票生成思路,而是进入专门的预付款链路。
这说明“预付款”在 Odoo 里不是普通发票套个名字,而是另一条明确分开的业务逻辑。
为什么预付款不能简单理解成“销售行提前开票”
很多人第一反应是:
- 销售订单不是已经有 order line 了吗
- 那就按那些 line 提前开一部分票不就好了
但源码没有这么做。
它先拿真实销售行去做的是:
_prepare_base_line_for_taxes_computation()account.tax._add_tax_details_in_base_lines(...)account.tax._round_base_lines_tax_details(...)
也就是说,真实销售行先被当成:
预付款比例 / 固定额应该如何在业务基础上拆税、分摊、表达的参考底稿。
而不是直接把原销售行拿去剪一刀变发票行。
这个设计非常关键,因为预付款发票的语义不是“交付了多少”,而是:
- 客户现在先付一部分
- 这部分金额要在税务和会计上被明确表达
- 以后还要和最终结算衔接
源码最值钱的一步:先在销售订单上创建 Down Payment 行
向导在计算出预付款 base lines 后,会调用:
order._create_down_payment_section_line_if_needed()order._create_down_payment_lines_from_base_lines(...)
这一步特别重要。
因为它说明 Odoo 并没有把预付款发票当成一张“和销售订单弱相关的临时票据”,而是:
先在销售订单内部造出正式的 down payment 语义锚点,再用这些锚点去生成 invoice line。
换句话说,预付款不是漂在销售单外面的一张票,而是回写进销售链路的一部分。
这就是为什么后续:
- 预付款抵扣
- 已开预付款统计
- 正式发票扣减
这些事情能继续站得住。
发票创建时,Odoo 不是只会“抄销售行”
在准备发票值时,向导会调用:
_prepare_down_payment_invoice_values(order, so_lines)
里面会把新生成的预付款销售行,转成:
invoice_line_ids = [Command.create(...)]
而每行又来自:
_prepare_down_payment_invoice_line_values(...)
如果是百分比预付款,发票行名称会带出:
Down payment of xx%
这说明发票行本身也带有明确的预付款语义,而不是伪装成正常商品行。
账户怎么来:预付款不是随便挂收入科目
源码里还会通过:
self.company_id.downpayment_account_id- 或
_get_down_payment_account(product)
来决定预付款应落哪个科目。
这很重要,因为从会计语义看:
- 预付款并不总等于已经实现的正常收入
- 它需要有更明确的会计承接方式
所以 Odoo 在这里不是“产品收入科目随便顶一下”,而是优先寻找专门的 down payment account。
这就是为什么这个向导天然横跨:
- 销售
- 税务
- 会计
三层边界。
为什么源码里会 sudo().create(invoice_values)
你会看到向导在创建发票时用的是:
self.env['account.move'].sudo().create(invoice_values)
这背后的意思不是“无脑提权”,而是业务链路的执行角色和底层对象权限并不总是完全重合。
现实里很常见:
- 销售人员能推进销售流程
- 但不一定拥有完整的会计底层建票权限
所以系统会在受控位置提权把票建出来,然后再根据当前环境把后续消息、来源链路补齐。
源码后面又专门做了:
invoice = invoice_sudo.sudo(self.env.su)message_post_with_source(...)- 在销售订单上记录“Down payment invoice has been created”
这说明 Odoo 不是只在乎“把票建成功”,而是连来源可追踪性也一起维护。
预付款为什么常让人误会成“会计功能”,其实它是销售-会计交界功能
从界面看,它像在开票; 从源码看,它却一直紧贴销售订单。
这正是它最容易被理解错的地方。
更准确的说法是:
Down payment wizard 是用会计单据表达销售承诺中的预收部分。
所以它既不是纯销售逻辑,也不是纯会计逻辑,而是销售兑现节奏进入会计表达的一道桥。
实战里最常见的 4 个误区
1. 以为预付款发票就是普通发票提早开
不对,它有独立的 down payment 行与账户语义。
2. 以为百分比预付款只是 amount_total * x%
不完全对,税额和 base line 分摊也在参与。
3. 以为开完预付款和销售订单没多大关系
不对,源码会在 SO 上创建 down payment section / line 作为锚点。
4. 以为后续抵扣只是财务手工处理
其实前面之所以做这么多锚定,正是为了后续正式开票时能有可追踪的扣减语义。
一句话总结
sale.advance.payment.inv 真正做的,不是“帮你快点开票”,而是:
把销售预付款这件事,正式地建模成一条可计算、可追踪、可抵扣的业务链。
理解了这一点,你再去看百分比预付款、固定金额预付款、SO 上的 down payment line、发票科目和后续抵扣,就不会把它误当成一张“提前开的普通票”了。
DISCUSSION
评论区