预付款抵扣边界

Odoo 预付款为什么不是“开一张押金商品发票”就结束:Down Payment Wizard、Deposit Product 与最终抵扣边界讲透

很多实施知道销售可以开预付款,但经常把它理解成“建个押金商品、先开票、最后人工冲减”。官方源码真正做的是预付款基线计算、销售订单锚点回写、正式发票负向抵扣和草稿发票保护。本文专门讲清 deposit product 与 final invoice deduction 的边界。

销售
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说结论

Odoo 的预付款链路里,最容易误解的一点是:

系统并不是让你“找一个押金商品开一张普通发票”,而是先把预付款建模成销售订单上的特殊行,再在最终发票阶段把这些行作为待抵扣金额反向带回。

所以真正的边界不是“有没有一个 deposit product”,而是:

  • 预付款在销售订单上如何留下锚点
  • 这些锚点如何生成预付款发票
  • 正式开票时如何识别并负向抵扣
  • 为什么 Odoo 还要额外关心草稿发票和重复开票风险

如果把这一套看成“提前开一张票”,后面几乎所有异常你都会解释错。


核心入口:sale.advance.payment.inv 其实在分三条路

源码入口在:

  • sale/wizard/sale_make_invoice_advance.py

向导字段 advance_payment_method 明确支持三种模式:

  • delivered:普通发票
  • percentage:按百分比预付款
  • fixed:按固定金额预付款

这件事本身就已经说明:

  • 预付款不是普通发票的别名
  • 它是一条单独的业务分支

_create_invoices() 里,如果是 delivered,系统会直接走:

  • sale_orders._create_invoices(final=self.deduct_down_payments, grouped=...)

而如果是 percentage / fixed,就进入专门的预付款逻辑。

所以 Odoo 的设计是:

预付款不是普通开票流程里顺手加个金额,而是独立的销售开票语义。


“Deposit Product” 只是表现载体,不是业务语义本体

很多顾问会说:

  • 先配置一个押金商品
  • 开预付款发票
  • 最后再从正式发票里扣掉

这句话不完全错,但很容易误导。

在源码里,向导真正做的事情不是先找产品,而是先做税基和金额拆分:

  • 取销售单的非展示行 order.order_line.filtered(lambda x: not x.display_type)
  • 每行变成 base_line
  • 通过 account.tax 计算与 round 税明细
  • 再调用 _prepare_down_payment_lines(...) 生成预付款基线

换句话说,系统先确定的是:

  • 预付款究竟按哪些销售基线分配
  • 税要怎么拆
  • 固定额或百分比最终落到哪些 base lines

然后才谈如何转成订单行 / 发票行。

所以所谓 deposit product,更准确的理解应该是:

它是预付款在会计与单据上的承载形式,不是预付款语义的起点。


真正关键的一步:先把预付款回写成销售订单里的 is_downpayment

预付款分摊完之后,向导会做两件很关键的事:

  • order._create_down_payment_section_line_if_needed()
  • order._create_down_payment_lines_from_base_lines(...)

sale_order.py 里可以看到:

  • Down payment section line 的 display_type='line_section'is_downpayment=True
  • 真实的预付款订单行也有 is_downpayment=True
  • 这些行 product_uom_qty 会被设成 0.0
  • 金额主要靠 price_unitextra_tax_data 表达

这一步极其重要,因为它意味着:

预付款不会悬空存在于发票层

系统不是“偷偷开一张和销售单弱关联的票”,而是先把预付款写回销售单。

最终抵扣依赖这些订单锚点

如果销售单上没有这些 is_downpayment 行,后续 final invoice 根本无法稳定识别哪些金额应该被冲减。

预付款行故意不是普通交付语义

数量是 0.0,说明它不是“已经卖出并交付了一件商品”,而是预付款的金额占位与税务表达。


为什么最终发票抵扣不是“少开一点金额”,而是明确生成负向行

sale.order._create_invoices(final=True) 的关键逻辑在于,它会把可开票行遍历出来;一旦遇到 is_downpayment 行,就做两件事:

  1. 先插入一个 dedicated section,专门放 Down Payments
  2. 对 down payment line 设置: - quantity = -1.0 - extra_tax_data = _reverse_quantity_base_line_extra_tax_data(...)

这非常关键。

因为这说明在最终发票里,Odoo 不是采用这种模糊处理:

  • “总额直接减掉已收预付款”

而是采用更清晰、也更可审计的处理:

  • 正常表达整张订单该开的正式内容
  • 再明确追加一段“已收预付款抵扣”负向行

这就是为什么最终发票看起来不是“金额直接少了”,而是能看到完整的 order lines 和独立的 down payment deduction 段落。

从审计、税务和客户沟通上,这都比“偷偷减净额”更稳。


deduct_down_payments 的边界:它不是创建预付款时生效,而是正式开票时生效

很多人看到向导上的 Deduct down payments 会误会成:

  • 勾选后创建预付款时就自动完成所有扣减

其实不是。

看源码就知道:

  • 这个布尔值是在 advance_payment_method == 'delivered' 时传入 _create_invoices(final=self.deduct_down_payments, ...)

也就是说,它控制的是:

  • 当你以后走普通/最终开票路径时,要不要把已有 down payment lines 作为抵扣项带进去

它不改变预付款发票本身的生成方式。

所以正确理解是:

  • 先开预付款:建立预付款锚点 + 生成预付款发票
  • 后做正式发票:是否扣预付款,由 final 这条开票语义决定

这是两个阶段,不是一次按钮完成全部。


为什么向导还要提醒草稿发票

向导上有一个 display_draft_invoice_warning,它会检查当前销售单关联发票里是否存在 draft 状态。

这背后的业务含义非常实际:

  • 如果已经有草稿发票未处理
  • 你再开新的预付款或正式发票
  • 很容易出现重复金额、重复税额或对账混乱

所以 Odoo 在 UI 层先给你一个信号:

你的销售单已经存在尚未定稿的开票动作,不要把预付款和正式发票当成可以无限叠加点击的按钮。

这也是为什么成熟实施里,预付款流程必须配套明确的财务操作纪律,而不是只培训“怎么点按钮”。


会计账户边界:系统优先找 downpayment account,没有才退回 income

_get_down_payment_account(product) 的逻辑也很值得注意:

  • 先从产品账户中找 downpayment
  • 如果没有,再退回 income

这说明 Odoo 明确预留了“预付款单独会计口径”的位置。

换句话说,系统并不强迫你:

  • 预付款和正式收入一定走同一个收入科目

如果公司科目策略更细,完全可以把预付款放到专门口径;如果没配,系统才保底回到收入科目。

这就是 deposit product / deposit invoice 真正的会计边界:

  • 它能专门建模
  • 但也允许在简化配置下退回普通收入科目

实战里最容易犯的 5 个错

1. 把预付款商品当成普通销售商品维护

这样会让团队以为它对应真实交付,而忽略 is_downpayment 的特殊语义。

2. 忽略销售订单上的预付款锚点

如果你只看发票,不看 SO 上新增的 down payment section / lines,就很难调试最终抵扣异常。

3. 以为最终扣减是净额覆盖

其实正式发票常常是“完整订单 + 明确负向预付款行”。

4. 在已有 draft invoice 时继续重复开票

这会把问题从“业务理解偏差”升级成“财务对账事故”。

5. 把 deduct_down_payments 理解成预付款创建开关

它真正控制的是 final invoice 阶段是否把预付款抵掉。


一句话总结

Odoo 的 Down Payment Wizard 真正设计的不是“先卖一件押金商品”,而是:

  • 先基于订单和税额生成预付款 base lines
  • 再把它们回写成 sale.order 上的 is_downpayment
  • 再生成预付款发票
  • 最后在正式开票时用负向 down payment line 显式抵扣

所以 deposit product 只是载体,真正关键的,是:

预付款在销售订单里的锚点,以及 final invoice 对这些锚点的识别与反向表达。

DISCUSSION

评论区

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