很多门店第一次接 Odoo POS 支付终端时,脑子里的模型都很简单:
收银员点“银行卡”,顾客刷卡,成功就结束,失败就重来。
这套理解一旦落到真实现场,很快就会出问题。因为现实里经常出现的不是单纯“成功/失败”,而是:
- 终端已经发起请求,但顾客还没操作;
- 收银员想取消,但终端未必立刻确认;
- 银行卡已经扣了,POS 这边还没自动结单;
- 门店发现刷错金额,需要走 reversal,而不是再删一行付款;
- 某条支付线已经和终端交互过,后面很多字段不能再随便改。
先说结论:Odoo POS 对支付终端的建模,本质不是“一笔付款”,而是一条带状态迁移的支付交互流程。前端支付行、终端接口、自动验单逻辑三者必须一致,系统才敢把订单推进到已支付。
为什么支付终端在 Odoo 里不是普通付款方式
在 pos_store.js 里,POS 会根据 use_payment_terminal 动态注册支付接口,再给对应支付方式挂上 payment_terminal 对象。也就是说,Odoo 不是把终端当成一个“多了点配置的银行卡”,而是当成一套可发请求、可轮询、可取消、可冲正的接口能力。
这就解释了为什么终端付款比现金行严格得多:
- 它要和外部设备或网关交互;
- 它的结果可能是异步返回;
- 它不是你在前端改个金额就算数;
- 它必须保护门店不被“重复发起”和“前端手滑改状态”坑到。
sendPaymentRequest() 真正做了什么
支付屏源码里,sendPaymentRequest(line) 一上来就把 this.pos.paymentTerminalInProgress = true,同时把其他支付线的 can_be_reversed 关掉。
这两个动作非常关键:
- 告诉整个收银界面:现在有一条电子支付正在进行中;
- 防止收银员在同一时刻又去反向操作其他终端支付线。
随后,Odoo 才会区分:
- 如果是
qr_code型支付,先弹二维码流程; - 否则直接
await line.pay(),把请求发给终端接口。
请求完成后,Odoo 不会无脑结束,而是继续判断:
- 本次支付是否成功;
- 当前订单是否已经 fully paid;
- 配置里是否开启
auto_validate_terminal_payment; - 当前订单是否处在退款处理中。
只有这些条件都满足,系统才会自动 validateOrder(false)。
所以你看到“终端付款成功后订单自动结单”,本质并不是 UI 写死了一个跳转,而是支付成功 + 订单已付清 + 配置允许自动验单 + 当前不是退款流程四个条件同时成立。
为什么取消和冲正是两回事
很多门店把 cancel 和 reversal 混着说,这是典型误区。
在 Odoo 的支付屏里:
sendPaymentCancel(line)会先把支付线标成waitingCancel;- 调终端接口
sendPaymentCancel(currentOrder, line.uuid); - 成功后状态回到
retry,允许重新发起; - 失败则又回到
waitingCard,说明这条线还卡在等待终端处理。
而 sendPaymentReverse(line) 则完全不同:
- 它先把状态设成
reversing; - 调
sendPaymentReversal(line.uuid); - 成功后直接把金额设为 0,并标记
reversed; - 失败则不能装作没发生,只能恢复到
done,并关掉可冲正能力。
这背后的业务区别是:
- Cancel:请求还在流程里,想把它撤回;
- Reversal:支付已经成立,现在要冲销一笔已完成终端付款。
如果把这两者混成“删除支付行”,最后最容易出现的就是:POS 看起来删掉了,终端或通道那边却没有删掉。
为什么“已刷卡,但 POS 还没结单”不一定是 bug
源码里,自动验单是在支付请求结束后再判断 currentOrder.isPaid()。这意味着下面几种情况都可能让门店看到“刷卡成功,但单子还挂着”:
- 订单还有尾差,尚未 fully paid;
- 混合支付场景里,另一笔现金/券还没补齐;
- 配置没开
auto_validate_terminal_payment; - 当前订单处在退款流程,系统故意不自动验;
- 终端成功了,但支付线状态没有正确进入 done。
所以这类问题别第一反应就怪“异步延迟”。更常见的是:订单层条件还没满足,Odoo 故意不往下走。
为什么终端支付线不该随便改金额
payment_screen.js 里有明确注释:对正在运行或已经 done 的终端支付线,要禁掉改金额这类操作。
原因很现实:
- 金额是发送给终端时的交易事实;
- 如果 UI 允许事后改,POS 就会和终端世界不一致;
- 一旦出现 dispute,门店将拿不出一致的链路解释。
所以 Odoo 的保守不是“前端做得死板”,而是它承认:电子支付不是本地草稿对象,而是外部世界已经参与进来的事实。
pos_adyen 为什么还要额外记 terminalServiceId
在 pos_adyen 里,支付线还会保存 terminalServiceId,并在 pending line 上恢复最近一次 service id。这个细节很重要,因为终端世界常见的问题不是“有没有请求”,而是:
- 这次取消/查询,到底针对哪一次请求;
- 门店重开页面后,还能不能接回那笔 pending transaction;
- 避免第二次点按钮时,系统把新请求误当成旧请求的后续。
换句话说,Odoo 并不是只管“收银台这一屏”,它还在尽量给终端交互补一层请求识别与恢复上下文。
最容易踩的四个坑
误区一:终端付款就等于普通银行卡付款
不对。终端付款有设备状态、取消、冲正、自动验单、重复请求保护。
误区二:取消失败就直接删支付行
很危险。UI 删掉不等于终端世界取消成功。
误区三:刷卡成功后没自动结单就是系统慢
不一定。更多时候是订单还没满足 fully paid 或自动验单条件。
误区四:冲正就是把金额改成 0
不对。金额变 0 是冲正成功后的结果,不是前端自己想怎么改就怎么改。
实战排错顺序
如果门店遇到“重复扣款、取消没反应、冲正失败、成功刷卡但单没结”的问题,建议按这个顺序查:
- 先看支付方式是否真的挂了
use_payment_terminal; - 看发起请求时
paymentTerminalInProgress是否正确置位; - 看支付线当前状态是
waitingCard、waitingCancel、done还是reversing; - 若是 Adyen,检查
terminalServiceId是否存在并对应当前请求; - 看终端侧到底是未应答、已取消还是已完成;
- 看订单是否真的
isPaid(); - 看是否开启
auto_validate_terminal_payment; - 若是退款单,确认系统是否故意跳过自动验单;
- 不要先删支付行,先确认 cancel/reversal 在终端世界是否成立。
最后的结论
Odoo POS 支付终端最重要的设计,不是“能连刷卡机”,而是它拒绝把电子支付简化成一个本地金额输入框。
真正该记住的是:
终端支付是一条状态机,不是一行数字。请求、取消、冲正、验单必须沿同一条链路说得通,门店数据才安全。
DISCUSSION
评论区