先说结论
Odoo POS 关班时并不是“一张订单生成一套独立凭证,然后全部贴到账上”。
pos_session.py 的主链路更像这样:
- 先创建 session 级别的
account.move; - 再由
_accumulate_amounts()把本次 session 的已关闭订单按不同业务键聚合; - 然后分别生成: - 不可对账分录; - 银行付款分录; - 挂账应收; - 现金结算与现金移动; - 发票应收; - 库存估值分录;
- 最后在需要的地方做对冲与平衡。
所以它的本质不是“订单级逐条过账”,而是:
先在 session 维度形成一张会计摘要,再把摘要拆成不同会计语义的落账动作。
一、为什么 _create_account_move() 一开始就站在 session 视角
_create_account_move() 一上来就先创建一张 account.move,ref 也是 session 名称。
这非常说明问题:
- POS 关班会计的最小单元,不是某张订单;
- 而是某个 closing session。
因为从业务上看,POS 更像一个持续发生的小额交易池:
- 同一班次里有多单;
- 多种支付方式;
- 现金与非现金混合;
- 还有可能夹着已开发票与未开发票订单。
如果每单都孤立落账,数据当然也能做,但:
- 噪声会很多;
- 汇总口径会更乱;
- 对账与关班体验会更差。
Odoo 选择 session 聚合,本质上是在让会计结果更接近门店运营视角。
二、_accumulate_amounts() 为什么是整条链的心脏
这个方法最重要的不是“算金额”,而是:
- 按不同 key 把金额归组。
源码里能看到很多 defaultdict:
split_receivables_bankcombine_receivables_banksplit_receivables_cashcombine_receivables_cashsalestaxesstock_expensestock_returnstock_valuationcombine_invoice_receivablessplit_invoice_receivables
这说明 Odoo 从一开始就不是把 session 看成一团总额,而是把它拆成多条会计语义不同的金额流。
三、为什么要同时存在 split 和 combine
很多人第一次看这段代码都会疑惑:
- 为什么既有 split,又有 combine?
答案很简单:
- 有些支付方式或业务语义需要逐条保留;
- 有些则适合按 key 合并。
比如:
- 某些支付方式启用了
split_transactions,就不能随便合并; - 某些收款则可以按 payment method、account、partner 等维度归并。
这代表 Odoo 在平衡两件事:
- 账务上要足够精确,不能把不该混的混掉;
- 凭证上要足够简洁,不能每单都膨胀成海量分录。
四、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 落账,最该保留的是:
- session 先聚合、后生成 move lines 的总思路;
- sale/tax/receivable 各自的 grouping key;
- split 与 combine 的边界不要乱抹平;
- 已开票链路与未开票链路继续分流。
很多“看起来更简单”的改法,最后都会把分录做得更乱,而不是更清楚。
最后一句
理解 Odoo POS 的关班会计,重点不是某一单记到哪条分录,而是看懂这条主链:
session 建立 account.move →
_accumulate_amounts()按语义分桶 → 各类 move lines 分批生成 → 必要位置再做 reconciliation 与平衡。
看懂以后你就会发现,Odoo 追求的不是“每单都留下痕迹”,而是“整个班次的账能干净地说得通”。
DISCUSSION
评论区