关班会计

Odoo POS 关班分录为什么不是“一单一凭证”:_accumulate_amounts、sale key/tax key 与 receivable 聚合逻辑讲透

很多人以为 POS 关班就是把每笔订单逐条做会计分录,但 point_of_sale 实际先在 session 级别跑 _accumulate_amounts,把销售、税、应收、库存和发票对冲按不同 key 聚合,再统一落账。

POS 会计
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 4 阅读

先说结论

Odoo POS 关班时并不是“一张订单生成一套独立凭证,然后全部贴到账上”。

pos_session.py 的主链路更像这样:

  1. 先创建 session 级别的 account.move
  2. 再由 _accumulate_amounts() 把本次 session 的已关闭订单按不同业务键聚合;
  3. 然后分别生成: - 不可对账分录; - 银行付款分录; - 挂账应收; - 现金结算与现金移动; - 发票应收; - 库存估值分录;
  4. 最后在需要的地方做对冲与平衡。

所以它的本质不是“订单级逐条过账”,而是:

先在 session 维度形成一张会计摘要,再把摘要拆成不同会计语义的落账动作。

一、为什么 _create_account_move() 一开始就站在 session 视角

_create_account_move() 一上来就先创建一张 account.moveref 也是 session 名称。

这非常说明问题:

  • POS 关班会计的最小单元,不是某张订单;
  • 而是某个 closing session。

因为从业务上看,POS 更像一个持续发生的小额交易池:

  • 同一班次里有多单;
  • 多种支付方式;
  • 现金与非现金混合;
  • 还有可能夹着已开发票与未开发票订单。

如果每单都孤立落账,数据当然也能做,但:

  • 噪声会很多;
  • 汇总口径会更乱;
  • 对账与关班体验会更差。

Odoo 选择 session 聚合,本质上是在让会计结果更接近门店运营视角。

二、_accumulate_amounts() 为什么是整条链的心脏

这个方法最重要的不是“算金额”,而是:

  • 按不同 key 把金额归组。

源码里能看到很多 defaultdict:

  • split_receivables_bank
  • combine_receivables_bank
  • split_receivables_cash
  • combine_receivables_cash
  • sales
  • taxes
  • stock_expense
  • stock_return
  • stock_valuation
  • combine_invoice_receivables
  • split_invoice_receivables

这说明 Odoo 从一开始就不是把 session 看成一团总额,而是把它拆成多条会计语义不同的金额流。

三、为什么要同时存在 split 和 combine

很多人第一次看这段代码都会疑惑:

  • 为什么既有 split,又有 combine?

答案很简单:

  • 有些支付方式或业务语义需要逐条保留;
  • 有些则适合按 key 合并。

比如:

  • 某些支付方式启用了 split_transactions,就不能随便合并;
  • 某些收款则可以按 payment method、account、partner 等维度归并。

这代表 Odoo 在平衡两件事:

  1. 账务上要足够精确,不能把不该混的混掉;
  2. 凭证上要足够简洁,不能每单都膨胀成海量分录。

四、sale key / tax key 真正解决了什么问题

你会在后续链路里看到 _get_sale_key()_get_sale_vals()_get_tax_vals() 这样的设计。

这类 key 的意义,不是“代码写得更优雅”,而是:

  • 找到哪些销售额可以合并到同一条收入分录;
  • 找到哪些税额可以合并到同一条税分录;
  • 在公司、账户、税、符号、伙伴等维度上避免错误串账。

也就是说,聚合不是简单 sum(amount),而是:

在满足同一会计语义的前提下再合并。

如果你忽略 key 设计,最容易出现的问题就是:

  • 不同税被混到一起;
  • 退货与销售被混掉;
  • 不同科目被错误合并;
  • 后面无法对账或分析。

五、为什么已开票订单和未开票订单要分流

源码里对 order_is_invoiced 很敏感。

原因在于:

  • 已开票订单的应收与税务语义,不能和未开票订单完全混在一起;
  • 发票相关的 payment moves 还可能需要和 invoice receivable lines 再做 reconciliation。

这也是为什么 _accumulate_amounts() 不只是收一遍款、算一遍销售额就结束。

它还在为后续:

  • _create_invoice_receivable_lines()
  • _reconcile_invoice_payments()

提前准备好可对冲的数据结构。

六、为什么 POS 会计链路总让人觉得“比预想复杂”

因为它本质上不是把零售订单塞进会计,而是在同时处理几条流:

  • 销售收入流;
  • 税流;
  • 银行收款流;
  • 现金流;
  • 挂账应收流;
  • 发票应收流;
  • 库存估值流。

这些流彼此有关,但不能互相替代。

Odoo 把它们先聚合再分批落账,就是为了让最终凭证既能闭合,又不至于失真。

七、最容易误解的几个点

误解 1:POS 关班就是逐单过账

不对。核心是 session 级聚合。

误解 2:聚合就是简单求和

不对。必须先按会计语义 key 归组。

误解 3:split / combine 只是性能优化

不对。它其实在表达是否允许合并的会计边界。

误解 4:已开票和未开票都可以混在一条应收里

不对。后续 reconciliation 语义不同。

八、做会计定制时最该保留什么

如果你要改 POS 落账,最该保留的是:

  1. session 先聚合、后生成 move lines 的总思路;
  2. sale/tax/receivable 各自的 grouping key;
  3. split 与 combine 的边界不要乱抹平;
  4. 已开票链路与未开票链路继续分流。

很多“看起来更简单”的改法,最后都会把分录做得更乱,而不是更清楚。

最后一句

理解 Odoo POS 的关班会计,重点不是某一单记到哪条分录,而是看懂这条主链:

session 建立 account.move → _accumulate_amounts() 按语义分桶 → 各类 move lines 分批生成 → 必要位置再做 reconciliation 与平衡。

看懂以后你就会发现,Odoo 追求的不是“每单都留下痕迹”,而是“整个班次的账能干净地说得通”。

DISCUSSION

评论区

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