先说结论
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_unit和extra_tax_data表达
这一步极其重要,因为它意味着:
预付款不会悬空存在于发票层
系统不是“偷偷开一张和销售单弱关联的票”,而是先把预付款写回销售单。
最终抵扣依赖这些订单锚点
如果销售单上没有这些 is_downpayment 行,后续 final invoice 根本无法稳定识别哪些金额应该被冲减。
预付款行故意不是普通交付语义
数量是 0.0,说明它不是“已经卖出并交付了一件商品”,而是预付款的金额占位与税务表达。
为什么最终发票抵扣不是“少开一点金额”,而是明确生成负向行
sale.order._create_invoices(final=True) 的关键逻辑在于,它会把可开票行遍历出来;一旦遇到 is_downpayment 行,就做两件事:
- 先插入一个 dedicated section,专门放 Down Payments
- 对 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
评论区