POS 最常见的抱怨之一是:
白天都能卖,为什么一到关店就各种报错?
因为 Odoo 的 POS session 不是“营业状态开关”,而是一个要对收银、库存、会计同时交账的边界容器。 白天很多问题可以暂时悬着;到了关店,系统必须把账说圆。
所以结论先说:POS 关店失败,往往不是 closing 功能本身坏了,而是 closing 阶段第一次逼所有未闭合的问题一起结算。
closing_control 真正做的不是“锁屏”,而是进入清算阶段
在 pos.session 里,session 状态不是只有 opened 和 closed 两个点,中间还有 closing_control。
action_pos_session_closing_control() 做的第一件事,不是生成会计分录,而是先检查:
- 今天是否还有
draft订单; - 当前 session 是否已经 closed;
- 是否启用了 cash control;
- 是否处于 rescue session。
如果还有 draft 订单,源码直接拒绝关店。这很关键,因为 draft 单代表业务动作还没完成,贸然关店会让“待确认交易”混进“已清算日结”。
所以很多人觉得 Odoo 太严格,其实它在守的是最基础的一条线:
草稿单不属于已结账日。
cash control 不是装饰项,它决定了关店路径完全不同
如果 cash_control 没开,closing_control 可能直接进入 action_pos_session_close()。
但只要开了现金控制,系统就会要求你面对现实:
- 期初现金是多少;
- 订单里按现金方式收了多少;
- 实际数出来的现金是多少;
- 差额到底进 profit account 还是 loss account。
在 _post_statement_difference() 里,Odoo 会把差额生成 statement line,并要求现金 journal 预先配置好利润/损失科目。
这也是为什么一些现场明明“就差几块钱”,却卡在关店上:
- 不是不能差;
- 是你必须明确差到哪一个科目里。
为什么 force close 会出现
很多人把 force close 当成“系统突然认输”。其实不是。
_validate_session() 在创建会计分录后,会检查 move 是否平衡。若不平衡,它会 rollback 当前事务,并弹出 _close_session_action() 对应的 wizard,让用户选择 balancing account 来强制平账。
也就是说 force close 的真实语义是:
系统发现你这次关店能算出结果,但算不平,所以要求你显式承认差额去向。
它不是推荐流程,而是一个人工兜底。
关店时其实还会顺带处理库存与毛利
如果公司设置 update_stock_at_closing,关店时 _validate_session() 还会调用 _create_picking_at_end_of_session(),并在需要时为 FIFO/AVCO 行补算成本。
这点非常容易被忽略。 很多人以为 POS closing 只影响收银和会计,其实它还可能是库存正式落账的时点。
所以某些案例里“为什么白天前台都能卖,晚上关店才报库存或成本相关错误”,答案就在这里:
- 白天交易可以先积压在 session 里;
- 到关店才统一生成库存动作和成本归集。
rescue session 的 closing 又为什么特殊
源码里对 rescue session 还有特殊处理:
- 如果 rescue session 且启用 cash control,系统只计算现金箱里的支付;
- 并明确说明 rescue session 不能完全按普通前端流程关闭。
这说明 rescue session 的设计目标是“补救晚到订单”,不是“完整替代一次正常营业班次”。
所以看到 rescue session 无法像普通会话那样顺滑关店,不要奇怪,这就是产品边界。
最容易误解的四件事
误区一:关店时报错,说明关店功能有问题
很多时候不是。 更常见的是前面某个问题被延迟到关店才集中暴露。
误区二:现金差异只影响报表,不影响关店
不对。 差异要入账,没配 profit/loss account 就可能直接卡住。
误区三:force close 等于“忽略错误”
不对。 它不是忽略,而是指定差额怎么入账。
误区四:POS closing 和库存无关
不对。 在 session closing 更新库存的公司设置下,关店就是库存与成本真正落地的时点。
实战排错顺序
遇到 POS 关不掉,建议别一上来盯着最后一个报错,先按顺序查:
- session 下是否还有 draft orders;
- cash control 是否开启;
- 收银员是否已经录入 closing cash;
- cash journal 是否配置了 profit/loss account;
- 是否存在银行支付差异或 split payment 差异;
- move 创建后是否平衡;
- 若启用 closing 时更新库存,picking / valuation / cost 是否能正常生成;
- 当前 session 是否 rescue session,是否触发了特殊路径。
最后的结论
Odoo POS 的关店,本质上不是“营业结束按钮”,而是一次结算闸门。 它同时在检查:
- 业务单据是否都走完;
- 现金是否盘清;
- 差额是否有会计去向;
- 会计分录是否平衡;
- 库存与成本是否该在此刻落账。
所以 POS closing 难的地方,从来不是按钮本身,而是它逼你回答一句白天可以暂时回避、晚上必须说清的话:
今天这班收银,到底有没有真正对上。
DISCUSSION
评论区