预付款发票

Odoo 预付款发票不是“先开一张普通发票”:sale.advance.payment.inv 真正做的是押金语义建模

很多人知道 Odoo 可以开 Down Payment Invoice,但容易把它理解成“先随便开一张发票”。其实源码里这条链路专门做了预付款建模、税额拆分、销售行锚定和后续抵扣语义。本文讲透这个向导到底在干什么。

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

先说结论

很多人把 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

评论区

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