很多人第一次接触 Odoo 银行对账,会把银行流水想成一种“辅助数据”:
- 先把银行流水导进来;
- 之后再人工补一张会计分录;
- 最后把两边对上。
但如果你看官方源码,会发现 Odoo 的设计不是“流水在一边、分录在另一边”,而是:
一条
account.bank.statement.line背后,本来就绑定着一张account.move,两者还会双向同步。
而这条链里最关键的中间角色,就是 suspense account(暂记账户)。
很多实施项目里,大家一听到“暂记账户”就觉得它像是临时凑数的中转层;其实从源码看,它恰恰是 Odoo 让“银行流水对象”与“会计分录对象”保持一致的关键缓冲层。
一、银行流水不是附件,而是带分录主键的会计对象
看 /home/ubuntu/odoo-temp/addons/account/models/account_bank_statement_line.py。
在这个模型里,官方没有把 statement line 处理成一个仅供导入展示的外部表,而是围绕它设计了:
- 默认分录生成逻辑;
- 流水与 move 的字段同步;
- 币种/金额/伙伴的回写;
- 对不一致状态的校验。
这意味着 Odoo 的理解是:
银行流水不是“会计前的数据”,而是会计处理过程中的正式对象。
所以你在对账界面上看到的每条流水,不只是待处理消息,而是已经和总账对象产生关系的记录。
二、为什么一开始就要有 suspense account
先看 _prepare_move_line_default_vals。
源码里最直接的逻辑是:
- 如果没有指定 counterpart account,就用
journal.suspense_account_id; - 如果连 suspense account 都没有,直接报错,禁止创建新 statement line。
错误提示非常直白:
没有暂记账户,就不能为这条银行流水创建对应分录。
这说明 suspense account 不是“高级功能”,而是 银行流水可会计化 的基本前提。
为什么?
因为银行流水刚导入时,系统通常已经知道:
- 银行金额;
- 银行日记账;
- 交易日期;
- 某些情况下的伙伴或备注。
但它往往还不知道最终对方科目是谁。
所以 Odoo 先生成两条核心 journal items:
- 一条走银行/现金科目(liquidity line);
- 一条走 suspense account(counterpart line)。
这一步其实是在表达:
“银行事实已经成立,但业务归类还未最终确认。”
用暂记账户承接这段“不确定性”,比不生成分录、或者直接乱猜对方科目,都更符合会计控制逻辑。
三、默认分录到底怎么组出来
在 _prepare_move_line_default_vals 中,Odoo 先区分三层币种语义:
- 公司币
- journal 币
- foreign currency
然后算三类金额:
journal_amounttransaction_amountcompany_amount
最后生成两条默认 line:
1)liquidity line
它使用:
journal.default_account_id- 以银行日记账对应的流动性科目入账
2)counterpart line
它使用:
- 指定 counterpart account,默认就是 suspense account
这个设计非常稳。
因为银行流水导入时,Odoo 至少能百分百确认一件事:
银行科目一定动了。
但另一边究竟是:
- 应收核销;
- 应付核销;
- 手续费;
- 预收预付;
- 其他营业外;
还不一定。
所以先把“已知的一边”固定下来,把“未知的一边”暂挂起来,后续再通过对账或人工调整消化掉。
四、为什么 Odoo 要强行要求“恰好一条银行行、一条暂记行”
看 _seek_for_lines 和 _synchronize_from_moves。
Odoo 会把当前 move 里的 journal items 分成三组:
liquidity_linessuspense_linesother_lines
然后它做了两个非常关键的约束:
- 与 statement line 关联的 move,必须始终只有 一条 bank/cash line;
- suspense line 也不能失控,最多只能保持 一条。
如果不满足,源码会直接抛 UserError。
这代表 Odoo 官方在捍卫一个对象边界:
一条银行流水,必须能被唯一映射回一条“银行影响”。
否则 statement line 就失去“单笔银行事实”的语义,会变成一团混合分录。
很多自定义对账逻辑喜欢往流水关联 move 上乱加行,最后出现对账界面金额、伙伴、币种莫名其妙,就是因为破坏了这个边界。
五、为什么流水改了,分录会跟着改;分录改了,流水也会回写
这是本文最值得理解的地方。
1)从 move 回写 statement line:_synchronize_from_moves
当 account.move 发生变化,statement line 会重新读取:
- liquidity line 的名称、伙伴;
- 金额;
- 暂记行的外币信息;
- move 自己的伙伴与币种。
换句话说,如果你直接从会计分录层改了银行流水相关分录,Odoo 不会默认 statement line 还是旧数据,而是试图把流水对象一起拉回一致状态。
2)从 statement line 推回 move:_synchronize_to_moves
反过来,如果你改的是 statement line 上的:
payment_refamountamount_currencyforeign_currency_idpartner_id
Odoo 就会重新生成默认 line vals,并更新 move 里的 liquidity line / suspense line。
这说明两者的关系不是“流水生成分录,一次性结束”,而是:
在 Odoo 眼里,它们是同一个会计事件的两种视图,需要持续同步。
六、外币为什么在这里特别容易看晕
银行流水最容易让人头疼的就是币种。
源码里单独区分:
- journal currency
- company currency
- foreign currency
并根据场景决定:
- statement line 的
amount代表什么; amount_currency是否有意义;foreign_currency_id是否需要保留。
例如在 _synchronize_from_moves 里,如果 suspense line 的币种和 journal currency 或 company currency 已经一致,源码会主动把:
amount_currency归零;foreign_currency_id清空。
这背后的意图不是丢信息,而是避免“无意义地声明一个外币维度”。
也就是说,Odoo 不希望你把“银行日记账本币”误认为“这笔流水存在额外外币语义”。
这一步做得很细,所以你会感觉银行流水的币种逻辑有点严格,但其实它是在防止重复表达同一层币种信息。
七、这套设计到底解决了什么问题
1)解决“流水先到、归类后定”的真实业务顺序
现实里银行事实往往先发生,业务归属稍后才能确认。
suspense account 正好接住这段时间差。
2)解决流水对象与总账对象脱节的问题
如果 statement line 和 move 不双向同步,最终会出现:
- 对账界面显示一套;
- 总账实际是另一套;
- 用户不知道哪个才是真的。
3)解决多币种银行流水的解释一致性
通过严格同步金额与币种语义,Odoo 尽量保证流水层和分录层讲的是同一个故事。
八、新手最容易误解什么
误解 1:暂记账户只是过渡,所以无所谓
错。
它不是随便挂着玩的,它是让“银行事实已入账,但归类未完成”这件事在会计上可控的关键结构。
误解 2:statement line 只是导入记录
也错。
从源码看,它和 account.move 是强绑定、双向同步的正式会计对象。
误解 3:对账就是把暂记账户清掉
这只说对了一半。
更准确地说,对账是在把一开始“不确定的对方”逐渐替换成真正的会计归属,同时保持流水对象与 move 对象一致。
九、实施和开发建议
实施上
- 银行 journal 的 suspense account 必须严肃配置;
- 不要把它当临时凑数科目;
- 它的清理机制、余额监控、月末对账流程都要设计清楚。
开发上
如果你改:
- statement import
- bank reconciliation
- move line 自动拆分
- 外币同步逻辑
一定要重看:
_prepare_move_line_default_vals_seek_for_lines_synchronize_from_moves_synchronize_to_moves
你要保护的不是某个界面动作,而是 statement line 与 move 之间的一致性契约。
十、最后总结
Odoo 的银行流水设计,核心不是“导入后再记账”,而是:
- 流水一进系统,就尽量形成可解释的会计分录;
- 未确认归类先挂 suspense account;
- statement line 和 account.move 持续双向同步;
- 通过唯一 liquidity line / suspense line 约束,保持单笔银行事实的清晰边界。
所以如果你真正理解了这条链,就会明白:
suspense account 不是多余中间层,statement line 也不是附件表。
它们一起构成了 Odoo 银行对账链路里最关键的结构骨架。
DISCUSSION
评论区