会计源码

Odoo 银行流水为什么不是“导进来再补分录”:statement line、suspense account 与 journal entry 双向同步讲透

很多人把银行流水理解成一条辅助记录,但 Odoo 官方源码把它设计成与 account.move 强绑定、双向同步的会计对象。本文用 account.bank.statement.line 的源码说明:为什么暂记账户不是多余中间层,以及流水和分录如何彼此纠偏。

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

很多人第一次接触 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:

  1. 一条走银行/现金科目(liquidity line);
  2. 一条走 suspense account(counterpart line)。

这一步其实是在表达:

“银行事实已经成立,但业务归类还未最终确认。”

用暂记账户承接这段“不确定性”,比不生成分录、或者直接乱猜对方科目,都更符合会计控制逻辑。


三、默认分录到底怎么组出来

_prepare_move_line_default_vals 中,Odoo 先区分三层币种语义:

  • 公司币
  • journal 币
  • foreign currency

然后算三类金额:

  • journal_amount
  • transaction_amount
  • company_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_lines
  • suspense_lines
  • other_lines

然后它做了两个非常关键的约束:

  1. 与 statement line 关联的 move,必须始终只有 一条 bank/cash line;
  2. 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_ref
  • amount
  • amount_currency
  • foreign_currency_id
  • partner_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

评论区

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