POS 支付终端

Odoo POS 支付终端为什么总在“请求中、可取消、可冲正”之间切换:请求、取消与冲正状态机讲透

很多人把 POS 刷卡机理解成“点一下、成功或失败”,但 Odoo 真正在维护的是一条前端支付行状态机:发送请求、等待取消、重试、完成、冲正,以及在必要时自动验单。本文结合 point_of_sale 与 pos_adyen 源码,讲清支付终端为什么不能简单等同于一笔普通付款,以及门店最常见的重复扣款、取消失败、已扣款未结单该怎么排。

POS
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多门店第一次接 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 关掉。

这两个动作非常关键:

  1. 告诉整个收银界面:现在有一条电子支付正在进行中;
  2. 防止收银员在同一时刻又去反向操作其他终端支付线。

随后,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 是冲正成功后的结果,不是前端自己想怎么改就怎么改。

实战排错顺序

如果门店遇到“重复扣款、取消没反应、冲正失败、成功刷卡但单没结”的问题,建议按这个顺序查:

  1. 先看支付方式是否真的挂了 use_payment_terminal
  2. 看发起请求时 paymentTerminalInProgress 是否正确置位;
  3. 看支付线当前状态是 waitingCardwaitingCanceldone 还是 reversing
  4. 若是 Adyen,检查 terminalServiceId 是否存在并对应当前请求;
  5. 看终端侧到底是未应答、已取消还是已完成;
  6. 看订单是否真的 isPaid()
  7. 看是否开启 auto_validate_terminal_payment
  8. 若是退款单,确认系统是否故意跳过自动验单;
  9. 不要先删支付行,先确认 cancel/reversal 在终端世界是否成立。

最后的结论

Odoo POS 支付终端最重要的设计,不是“能连刷卡机”,而是它拒绝把电子支付简化成一个本地金额输入框。

真正该记住的是:

终端支付是一条状态机,不是一行数字。请求、取消、冲正、验单必须沿同一条链路说得通,门店数据才安全。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。