很多团队一看到 Odoo POS 里的 cash rounding,就会下意识把所有“几分钱差异”归到一个桶里:
- 小票金额和原价差了几分;
- 付款行被系统拦住;
- 发票多了一条 rounding line;
- 关店盘点出现差异;
- 报表里又冒出 profit / loss。
于是大家很容易得出一个错误结论:
这些都是同一件事,只不过表现位置不同。
其实完全不是。
源码里最值得注意的恰恰是:Odoo 把这些“看起来都像金额差”的东西,拆成了不同层级的问题来处理。
先说结论
结合 order_payment_validation.js、pos_order.py、pos_session.py 和 closing_popup.js,可以先抓住五个结论:
- rounded total 是订单层的金额呈现和支付精度问题。
- payment line 是否合规,会在前台验证阶段先被拦住。
- rounding line 是订单/发票/会计层为了让金额闭合而补出的结构。
- session closing 的 cash difference 是盘点层的现实现金差异。
- 这两种“差额”可能同时存在,但业务含义根本不同。
rounded total 先解决的是“这笔单怎么付”
在 order_payment_validation.js 里,Odoo 会检查现金支付行的金额是否已经按 rounding precision 录入。
如果没有,它会直接弹出:
- 你的 payment line 必须按某个精度取整;
- 你应该录入的是多少,而不是当前金额。
这一步说明 Odoo 的第一个保护点发生在:
订单还没正式确认之前,先确保前台支付行本身符合抹零规则。
也就是说,rounded total 首先是个交易录入规范问题。
不是等到后面记账了才补救,而是尽量在收银员录付款时就拦住错误。
为什么支付行校验不是多此一举
很多人会想:
- 后台反正还能 round;
- 为什么前台还要提醒一次?
原因非常现实。
如果付款行本身没按精度录入,后面会出现多层混乱:
- 收银员以为顾客付了 9.97;
- 系统其实只接受 10.00 这种粒度;
- 找零、已付判断、票据金额都会开始出现边界误会。
所以前台这层校验保护的不是美观,而是:
- 收银员输入的金额要和门店的 rounding 规则一致;
- 本单是不是已经“付清”,要在一个明确的精度规则里判断。
rounding line 解决的是“账务怎么闭合”
到了 pos_order.py,问题就从前台交互转成了会计结构。
无论是在订单会计分录准备阶段,还是在创建发票时,Odoo 都可能根据 cash rounding 逻辑生成 display_type = 'rounding' 的行。
这类 rounding line 的作用不是告诉顾客“少收了几分”,而是让系统在账务上表达:
- 这笔差额来自抹零;
- 它应该落到 rounding 的 profit account 或 loss account;
- 应收或 payment term 需要相应调节,确保整张凭证平衡。
也就是说:
rounding line 是会计解释层,不是盘点差异层。
这两层非常容易被混淆。
为什么同样叫 difference,closing difference 却是另一回事
pos_session.py 在关店时处理的 cash_register_difference,指的是:
- 系统理论现金余额,
- 和实际数出来的钱,
- 之间的差。
这个差额最后会通过 _post_statement_difference() 走向:
- Profit Account,
- 或 Loss Account,
- 并在日志或报表里记成 closing difference。
请注意,这里的问题不是“订单要不要四舍五入”。
而是:
- 钱箱里现实世界的钱,
- 和系统按订单、付款、现金存取推算出来的钱,
- 是否一致。
这显然已经是另一个层级的问题了。
它关心的是门店现金控制,而不是单笔交易金额显示。
rounded total 和 cash difference 为什么常被混为一谈
因为从门店视角看,它们都可能表现为“差了几分钱/几毛钱”。
但业务语义完全不同:
rounded total / rounding line
这是系统按规则主动处理出来的金额调整。
它是在保护:
- 订单结账的合法精度;
- 发票或分录的平衡;
- 抹零该进哪条会计线。
closing difference
这是盘点时发现的现实偏差。
它是在表达:
- 少钱了;
- 多钱了;
- 是否允许带着这个偏差关店;
- 这笔偏差由谁接受并如何记账。
所以最值得记住的一点是:
抹零是规则内差额,closing difference 是现实偏差。
为什么 closing popup 会要求经理介入
closing_popup.js 很明确地展示了这一层边界。
如果钱数不匹配:
- 有 authority 的人,可以选择是否把差异记到账上并继续;
- 没有 authority 的人,会被要求联系经理接受差异。
这再次说明:
- rounding 规则是配置与交易层面的系统行为;
- closing difference 则是需要人来承担责任的门店异常。
系统能自动算抹零, 但不会替门店自动接受“钱箱确实不平”。
rounding difference 和 closing difference 甚至可能同时存在
这是很多实施现场最容易误判的地方。
完全可能出现:
- 单笔订单因为现金抹零,生成了 rounding line;
- 但关店时,收银员实点现金仍然和理论余额不一致。
这两者并不互相抵消,也不互相证明。
你不能因为系统允许 rounding,就默认关店差异合理; 也不能因为关店有差异,就断定是 rounding 配错了。
二者需要分开诊断。
为什么报表里 profit / loss 也不能一概而论
源码里,rounding 相关利润/损失账户,和 closing difference 相关利润/损失账户,都可能进入账务。
这会导致门店或财务人员看到:
- 都像是 small difference;
- 都进了某种 profit / loss;
- 于是误以为是同一来源。
但其实:
- 一个来自订单抹零规则;
- 一个来自盘点现实偏差。
如果不先区分来源,后续分析会越来越乱。
常见误解
误解 1:rounded total 就是关店差异
不对。
一个是交易规则内的金额调整,一个是现金盘点的现实差异。
误解 2:只要后台能补 rounding line,前台付款就随便输
不对。
前台 payment line 本身就必须符合 rounding precision。
误解 3:所有几分钱差额都应该查 cash rounding 配置
不对。
有些是 rounding 配置问题,有些是实际现金控制问题。
误解 4:有 closing difference 就说明 rounding 算错了
也不对。
closing difference 可能来自点钞、现金存取、漏录等现实操作差异。
实战排查顺序
当你遇到“金额差异”时,建议先把问题分层:
- 是 payment line 在前台就被 rounding 校验拦住了吗;
- 是订单/发票里出现了 rounding line 吗;
- 这笔抹零是否按正确 account 落账;
- 还是 session closing 时 counted cash 与 expected cash 不一致;
- 当前 closing difference 是否超出门店允许阈值,需要经理接受;
- 报表里的差额到底来自 rounding 规则还是 closing difference。
最后的理解方式
如果只记一句话,我建议记:
Odoo POS 里的 rounded total 是“按规则收多少钱”,closing difference 是“最后钱箱里到底剩多少钱”。
前者是交易与会计结构问题, 后者是门店现金控制与责任问题。
它们都可能表现成“几分钱差额”, 但绝对不是同一件事。
DISCUSSION
评论区