很多人第一次用 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 才会退回:
journal_id.default_account_id- 该默认科目上的 sale/purchase taxes
- 公司级
account_sale_tax_id/account_purchase_tax_id - 再经过
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_discountterm.early_pay_discount_computation == 'mixed'- 且只有一条百分比税
这里的意思是:
如果提前付款折扣会影响税基,系统反推未税金额时,就不能只按普通含税价倒推。
也就是说,Quick Edit 不是把发票当成一个“静态税率换算器”,而是会把付款条款也视为总额形成机制的一部分。
这就是很多人误会的地方:
误区:Quick Edit 只和税有关,和付款条款没关系
不对。
只要付款条款会影响税基,Quick Edit 反推金额时就必须把它算进去。
五、日期建议并不是今天,而是“尽量合法地贴近当前会计链”
_quick_edit_mode_suggest_invoice_date() 也很值得看。
它在 quick edit 且还没填 invoice_date 时,不是无脑给今天,而是:
- 先取今天;
- 再找同 journal、同 company 下最近一张已过账且有
invoice_date的单据; - 然后调用
_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。
然后系统会:
- 取
quick_encoding_vals; - 清空现有行;
- 新建一条
account.move.line; - 写入
partner_id、account_id、currency_id、price_unit、tax_ids; - 再调用
_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 会计主链路,而是按这个顺序做事:
- 从伙伴历史推测最可能的科目与税;
- 没有历史时退回 journal/company 默认值;
- 结合 fiscal position 和付款条款反推未税金额;
- 自动生成一条标准 invoice line;
- 再用 rounding 修正把总额校回用户输入值。
所以 Quick Edit 从来不是“少填字段”而已。它本质上是在把“总额优先”的录单心智,翻译成 Odoo 标准发票与会计分录链可接受的数据。
这也是为什么它用起来简单,但背后一点都不简单。
DISCUSSION
评论区