很多人对 Odoo POS 的第一层理解是:
断网时先存在本地,来网后再补传到服务器。
这句话没错,但太粗了。 真正让人踩坑的,不是“会不会补传”,而是:
- 为什么有的订单恢复网络后马上出现,有的却拖很久;
- 为什么原来的 session 已经关了,订单还能进系统;
- 为什么有时明明是同一个收银台,后台却冒出新的 rescue session;
- 为什么多台 POS 设备里,一台补传成功,另一台却开始读到一堆“旧单”。
结论先说:Odoo POS 的离线补传不是简单批量重放,而是一套“逐单同步 + 会话纠偏 + 多设备通知”的容错机制。 你看到的 rescue session,不是 bug 名字,而是 Odoo 在“原会话已不可写、但订单又必须落库”时的兜底策略。
先把主链路看清:前端不是一次性全量提交,而是逐单 sync
在 point_of_sale/static/src/app/services/pos_store.js 里,syncAllOrders() 做了几件很关键的事:
- 先判断当前是否离线;
- 从 pending 集合里挑出需要 create / update / delete 的订单;
- 不是整批原子提交,而是一单一单调用后端
pos.order.sync_from_ui; - 某一单失败时,不一定把所有单都废掉;
- 若发现 session 被切换,还会把前端手上的 draft 单重新绑定到新 session。
这解释了一个常见误区:
“我恢复网络了,为什么不是所有离线单一起秒到?”
因为源码就是按单同步。这样做的好处是,一张坏单不会拖死整批;坏处是,你会看到“有些单先进来,有些单还在排队”的表象。
后端真正关心的第一件事,不是金额,而是 session 还合不合法
订单到后端后,pos.order._process_order() 会先看 session_id。如果这张单来自的 session 已经是 closing_control 或 closed,源码不会硬写,而是走 _get_valid_session()。
_get_valid_session() 的逻辑非常直接:
- 找原 order 关联的关闭会话;
- 按同一个
config_id再找一个仍然可用、未关闭的 session; - 找到就把订单改挂到这个新 session;
- 找不到就报错,要求先开新会话。
所以离线订单“跑进新会话”不是随机行为,而是有意设计。 Odoo 在这里 prioritise 的不是“保持历史归属绝对不变”,而是“别让真实成交单丢失”。
rescue session 到底是什么
如果原会话已经不能承接,但同一 POS 配置下又需要一个新的会话来接盘,这时系统就会生成或使用 rescue session。
在源码里,pos.config 会区分:
- 普通当前会话;
rescue=True的恢复会话;- 当前配置已经打开了多少个 rescue session。
前端同步完成后,如果发现服务器返回了新的 session 数据,syncAllOrders() 还会主动把本地持有的 draft 单改绑到最新的 session,对旧 session 做替换清理。
所以 rescue session 的本质不是“多开了一个店”,而是:
给迟到的订单找一个合法的落点。
它解决的是“订单真实发生了,但原账期容器已经关了”的矛盾。
为什么 Odoo 不信前端金额,却还信前端订单骨架
很多人以为客户端 serialize 出来的数据,服务端会原样入库。其实并不会。
在 _process_payment_lines() 里,Odoo 会重新计算 amount_paid;在处理找零时,也会按后端支付方式再补 return line。源码的态度很明确:
- 前端负责交出订单结构;
- 后端负责确认金额语义;
- 前端不是最终账务真相。
这也是离线架构里一个非常重要的边界:
POS 可以离线收银,但不能把“财务可信度”完全下放给浏览器。
多设备场景下,为什么一台补传后另一台也会被惊动
这部分很多实施顾问没注意。
订单同步成功后,后端会调用 config.notify_synchronisation(),通过 bus 把同步事件推给同配置或 trusted config 下的其他设备。前端的 DevicesSynchronisation 会收到 SYNCHRONISATION 通知,再去服务器读取 open orders、静态记录和被删除记录。
换句话说,离线补传不是“这台机子自己的私事”。 在共享会话或多终端模式下,它会触发整个 POS 前端状态重整。
这就是为什么你经常看到:
- A 机补传后,B 机单据列表突然刷新;
- 已取消的旧 draft 会被强制删除;
- 新 session 信息会被其他设备感知到。
最容易误解的三件事
误区一:rescue session 说明数据坏了
不对。 rescue session 更多是在说明:数据没坏,但时机晚了。
误区二:离线订单一定回原 session 才算“正确”
不对。 如果原 session 已经进入 closing/closed,强塞回去反而会破坏关店与会计边界。
误区三:补传失败一定是网络问题
也不对。 它还可能是:
- 当前没有可用 open session;
- 配置或用户权限变了;
- 客户、产品、支付方式等关联对象已失效;
- 某张订单本身数据不再满足后端约束。
实战排错顺序
遇到“离线单不同步 / 跑进救援会话 / 多端状态混乱”,建议按这个顺序看:
- 浏览器前端是否仍把订单标记为 dirty / pending;
- 网络恢复后
syncAllOrders()是否真的触发; - 后端
sync_from_ui有没有报错; - 原 session 是否已经
closing_control或closed; - 同一
config_id下是否存在新的 open session; - 是否生成了 rescue session;
- bus 同步后其他设备有没有读到旧 draft / deleted record;
- 订单关联的 partner、payment method、journal 是否仍有效。
最后的结论
Odoo POS 的离线同步,本质上不是“本地缓存恢复上传”这么简单,而是三层协作:
- 前端逐单补传,尽量缩小失败面;
- 后端检查 session 合法性,必要时改挂新会话;
- 多设备通过 bus 重新对齐状态。
所以当你看到 rescue session 时,别先想着“系统乱了”,而要先理解这套机制真正想保护的是什么:
不是保护某个旧 session 的纯洁性,而是保护已经发生的交易不要丢。
DISCUSSION
评论区