会计源码

Odoo 发票 Quick Edit 为什么像“手填总额”,背后却在反推整张账单:suggestions、日期建议与 rounding 修正链讲透

很多人以为 Odoo 发票 Quick Edit 只是少填几列字段,但 `/home/ubuntu/odoo-temp/addons/account/models/account_move.py` 里的实现其实是在根据总额、常用科目、税和付款条款反推一条可过账的 invoice line,再用 rounding 修正把总额校到你输入的数字。

会计
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多人第一次用 Odoo 发票的 Quick Edit,会以为这只是一个“少填几列字段”的便捷模式:

  • 填一个总额;
  • 选个客户或供应商;
  • 系统帮你生出一行。

看起来像 UI 小技巧。

但如果去看 /home/ubuntu/odoo-temp/addons/account/models/account_move.py 里这几个方法:

  • account.move._get_quick_edit_suggestions()
  • account.move._quick_edit_mode_suggest_invoice_date()
  • account.move._onchange_quick_edit_total_amount()
  • account.move._check_total_amount()

就会发现 Odoo 做的根本不是“自动填几项默认值”,而是在试图把一个总额输入,反推出一条尽量合理、还能平顺进入正式会计链的 invoice line

Quick Edit 的本质不是简化录单,而是把“总额优先”的录单方式,翻译回 Odoo 标准分录生成链能够理解的数据结构。


一、Quick Edit 真正在解决什么问题

真实业务里,很多录单场景拿到的第一信息不是明细,而是总额:

  • 供应商先给了一个含税总额;
  • 业务同事只记得“这张票一共 1000”;
  • 财务想先把 bill 起草出来,明细后补;
  • 某些低复杂度费用票,本来就不值得逐行展开。

如果强制用户先填:

  • 科目
  • 单价
  • 发票日期
  • 付款条款

录入成本会很高。

所以 Quick Edit 的设计目标不是取代标准发票,而是给一个入口:

先从“我知道总额是多少”出发,尽量推到一条合理的会计行。

这也解释了为什么它一定要和常用科目、税、付款条款、发票日期建议连在一起。


二、第一步不是算金额,而是先猜“最像哪类费用/收入”

_get_quick_edit_suggestions() 的第一件事,不是直接把总额除以税率,而是先调用 _get_frequent_account_and_taxes()

这一步很关键。

Odoo 会优先根据:

  • 公司
  • 伙伴
  • 单据类型

去看历史上这个伙伴最常用的科目和税组合

这说明 Quick Edit 的底层假设是:

同一个伙伴的票据,往往有稳定的入账模式。

例如:

  • 某物流商大概率总落运费科目;
  • 某 SaaS 供应商大概率总落软件服务费;
  • 某固定客户的简易销售发票大概率总套同一类销售税。

也就是说,Quick Edit 不是“随机帮你补默认值”,而是在利用历史记账习惯做会计语义猜测。


三、历史猜不到时,才回退到 journal 和公司默认值

如果历史上没有足够频繁的科目/税信息,Odoo 才会退回:

  1. journal_id.default_account_id
  2. 该默认科目上的 sale/purchase taxes
  3. 公司级 account_sale_tax_id / account_purchase_tax_id
  4. 再经过 fiscal_position_id.map_tax()

这条 fallback 链很有代表性。

它表达的不是“随便给你找个税先顶上”,而是一个明确优先级:

  • 先尊重伙伴历史习惯
  • 没有历史,再尊重日记账默认科目;
  • 科目带不出合适税,再用公司级默认税;
  • 最后还要经过 fiscal position 重映射。

所以 Quick Edit 绝不是脱离会计配置单独运行的一套轻量模式。

它仍然深深依赖 Odoo 原有:

  • 日记账配置
  • 公司税配置
  • fiscal position
  • 历史分录统计

四、真正难的地方是:给了“含税总额”,怎么反推未税价

当科目和税大致确定后,_get_quick_edit_suggestions() 才开始反推 price_unit

这一步也不是简单的:

  • 总额 ÷ 1.13
  • 或者总额 ÷ 1.06

因为 Odoo 还要考虑付款条款里的 early payment discount

源码里专门处理了一个容易忽略的边界:

  • term.early_discount
  • term.early_pay_discount_computation == 'mixed'
  • 且只有一条百分比税

这里的意思是:

如果提前付款折扣会影响税基,系统反推未税金额时,就不能只按普通含税价倒推。

也就是说,Quick Edit 不是把发票当成一个“静态税率换算器”,而是会把付款条款也视为总额形成机制的一部分。

这就是很多人误会的地方:

误区:Quick Edit 只和税有关,和付款条款没关系

不对。

只要付款条款会影响税基,Quick Edit 反推金额时就必须把它算进去。


五、日期建议并不是今天,而是“尽量合法地贴近当前会计链”

_quick_edit_mode_suggest_invoice_date() 也很值得看。

它在 quick edit 且还没填 invoice_date 时,不是无脑给今天,而是:

  1. 先取今天;
  2. 再找同 journal、同 company 下最近一张已过账且有 invoice_date 的单据;
  3. 然后调用 _get_accounting_date(prev_move.invoice_date, False)

这说明 Odoo 的想法不是“给你一个方便的默认日期”,而是:

尽量让新建单据的发票日期,落在一条和最近会计链一致、同时不碰锁定边界的路径上。

也就是说,Quick Edit 其实很会计。

它不只是少输几个字段,而是在录入入口就尝试减少:

  • 锁定日期冲突;
  • 发票日期跳跃;
  • journal 时间链断裂。

六、真正把总额变成 invoice line 的,是 onchange

_onchange_quick_edit_total_amount() 才是把建议真正落到单据上的地方。

它会在以下前提下触发:

  • quick_edit_total_amount 有值;
  • quick_edit_mode 打开;
  • 当前还没有 invoice lines。

然后系统会:

  1. quick_encoding_vals
  2. 清空现有行;
  3. 新建一条 account.move.line
  4. 写入 partner_idaccount_idcurrency_idprice_unittax_ids
  5. 再调用 _check_total_amount()

这里最值得注意的是:

Quick Edit 不是保存一个“总额模式”的特殊状态,而是尽快把数据翻译回普通 invoice line。

这意味着后面:

  • 税计算
  • 动态行重算
  • 付款条款分行
  • 凭证过账

都还能沿用 Odoo 标准发票链路。

这也是它能长期稳定工作的核心原因。


七、为什么还要 _check_total_amount():因为四舍五入误差一定会冒出来

源码注释举了一个特别典型的例子:

  • 含税总额 100
  • 税率 21%
  • 理论未税金额 82.64
  • 理论税额 17.35
  • 合起来只有 99.99

这时 _check_total_amount() 会做什么?

它会检查 tax_totals['total_amount_currency'] 和你输入的 quick_edit_total_amount 是否还有 rounding 差额。

只要差额没有小到可忽略,它就会把这个差额补到 tax group 上,让总额精确回到用户输入值。

这一步非常关键,因为它体现出一个会计系统的现实主义:

用户输入的总额,往往比系统反推出来的未税/税分拆更“硬”。

也就是说,在 Quick Edit 场景里,Odoo 优先保证:

  • 最终发票总额和用户输入一致;
  • 再在允许范围内把 rounding 差额吸收到税额中。

这比让用户看到 100 输进去、最后出来 99.99 更符合票据录入直觉。


八、为什么 Quick Edit 适合简单票据,不适合复杂分摊票据

看到这里就能明白,Quick Edit 的整个设计都有一个前提:

  • 你输入的是一个总额;
  • 系统尽量用“一条最像的行”去承载它。

所以它特别适合:

  • 单一费用票;
  • 单一销售类票据;
  • 录入时先求速度、后补细节;
  • 历史模式稳定的伙伴。

但它不适合:

  • 多行税率混杂;
  • 多成本中心分摊;
  • 多产品混合且税口径不同;
  • 必须逐行保留明细含义的票据。

因为 Quick Edit 的核心能力是“单总额反推单主行”,不是“替你完成复杂会计建模”。


九、排查 Quick Edit 结果不对,最该先看什么

如果你遇到:

  • 默认科目不对;
  • 税不对;
  • 日期怪;
  • 总额差 0.01;

最实用的排查顺序通常是:

1)看历史最常用科目/税有没有把结果带偏

Quick Edit 优先信历史。

2)看 journal 默认科目和公司默认税

没历史时它们就是 fallback。

3)看 fiscal position 是否重映射了税

很多“默认税不对”其实是税位重映射。

4)看付款条款是否带 early discount mixed 逻辑

这会改变未税反推公式。

5)看 rounding 差额是不是被 _check_total_amount() 吸收到税额

总额一致但税额看着“差 0.01”,常常正是这里。


结论

Quick Edit 看起来像“只填一个总额”的偷懒入口,但源码说明它其实很克制。

它没有绕开 Odoo 会计主链路,而是按这个顺序做事:

  1. 从伙伴历史推测最可能的科目与税;
  2. 没有历史时退回 journal/company 默认值;
  3. 结合 fiscal position 和付款条款反推未税金额;
  4. 自动生成一条标准 invoice line;
  5. 再用 rounding 修正把总额校回用户输入值。

所以 Quick Edit 从来不是“少填字段”而已。它本质上是在把“总额优先”的录单心智,翻译成 Odoo 标准发票与会计分录链可接受的数据。

这也是为什么它用起来简单,但背后一点都不简单。

DISCUSSION

评论区

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