POS 小票补票

Odoo POS 小票二维码为什么能补开发票却又不随便放开:ticket code、access token 与 portal 边界讲透

很多人以为 Odoo POS 小票上的二维码只是一个“补开电子发票入口”,但源码里其实设计了两层访问边界:一层是基于 order access_token 的直达入口,另一层是通过 pos_reference、date_order、ticket_code 三元组找回订单。本文结合 controllers/main.py 与 receipt 源码讲清:为什么这套机制既方便顾客补票,又不会把 POS 订单公开成随便可猜的网页。

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

很多人看到 Odoo POS 小票上那段二维码或链接,会很自然地把它理解成:

顾客扫码,填信息,系统补开发票。

这句话方向没错,但太粗了。真正重要的问题不是“能不能补票”,而是:

  • 顾客凭什么能进入这张订单的补票页;
  • 系统如何避免把所有 POS 订单暴露成可枚举页面;
  • 如果顾客没扫二维码,后续又如何靠小票信息找回订单;
  • 已经开票的订单为什么会直接跳到 portal invoice 页面。

先说结论:Odoo POS 的补票链路并不是“公开一个订单详情页”,而是“access token 直达 + ticket code/票号/日期找回 + 已开票则跳 portal invoice”的双层边界设计。它追求的是方便顾客补票,但不把订单访问权限做成裸奔。

小票上的二维码,核心不是订单 id,而是 access_token

在 receipt 代码里,链接会组装成:

/pos/ticket/validate?access_token=...

这里非常关键的一点是:路由不是靠订单 id 暴露给顾客,而是靠 access_token

pos.order 本身也会确保生成 access token,这代表系统的安全意图非常明确:

  • 订单对象内部有一个难猜的访问凭证;
  • 对外发给顾客的是凭证,不是简单序号;
  • 即便顾客不知道后台订单 id,也能通过 token 直达属于自己的补票流程。

这比公开 /pos/order/123 显然安全得多。

为什么还要有 ticket_code + pos_reference + date_order 的找回链路

现实世界里,顾客不一定当场扫码,也可能过两天才想开发票。这时最常见的问题是:

  • 二维码丢了;
  • 只剩纸质小票;
  • 顾客只记得票号和日期。

所以 controllers/main.py 还提供了一条“凭票面信息找回”的入口,要求输入:

  • pos_reference
  • date_order
  • ticket_code

并且源码里还会校验:

  • pos_reference 至少 12 字符;
  • date_order 只做一个宽松窗口搜索,而不是精确到秒;
  • ticket_code 必须匹配;
  • 找到后再 redirect 到带 access_token 的 validate 页面。

这套设计很妙,因为它把“顾客记得的现实票面信息”转译成了“系统真正信任的访问凭证”。

为什么 validate 页面必须带 token,直接裸进会 404

show_ticket_validation_screen() 里,一开始就检查:

  • 如果没有 access_token,直接 not_found()
  • 用 token 找不到 pos.order,也直接 not_found()

这说明 Odoo 很清楚:补票页面虽然是 public route,但不是公开内容页。它是带凭证访问的受控入口

这点非常重要。否则只要知道路由地址,外人就可以反复试探订单信息,门店数据安全性会非常差。

为什么已经开票的订单会直接跳到 portal invoice

源码里如果发现 pos_order.account_move 已存在,且是 sale document,就会直接跳转:

/my/invoices/<id>?access_token=<portal_token>

这背后的思路很合理:

  • POS 补票页的任务,是帮尚未开票的订单完成补票;
  • 一旦发票已经存在,顾客需要的不是再走一次流程,而是直接进入 portal invoice 查看结果。

因此系统不会重复造轮子,而是把访问权平滑切到 portal 发票对象。

为什么还要补客户地址、税号、本地化字段

validate 页面里不仅会校验普通地址信息,还会根据公司 fiscal country 去拉:

  • partner localisation required fields
  • invoice localisation required fields

也就是说,Odoo 从来没把“补票”理解成只填一个邮箱。它知道不同国家/地区开票要求不同,所以会在进入 action_pos_order_invoice() 之前,把本地化必填字段也纳入校验。

这就是为什么某些国家环境下,顾客会被要求补更多发票信息;这不是页面复杂,而是合法开票前提更复杂。

为什么这套机制既方便又不算太冒险

如果只做二维码直达,会有丢码问题; 如果只做票号查询,又容易变成弱校验入口。

Odoo 把两者结合成:

  1. 有二维码时,凭 access_token 直达;
  2. 没二维码时,用票号+日期+ticket code 找回;
  3. 找回成功后,仍然转成 token 访问;
  4. 一旦发票存在,再切到 portal invoice token。

所以你可以把它理解成:顾客可以方便找到票,但系统始终尽量避免直接裸露订单。

最容易误解的四件事

误区一:二维码链接里最好直接放订单 id,简单省事

不对。那会明显削弱访问边界。

误区二:ticket code 就是访问凭证本身

不完全对。ticket code 更像找回订单的辅助因子,真正访问 validate 页面仍靠 access token。

误区三:订单已开票还回补票页更统一

不对。已开票时直接跳 portal invoice 更符合对象边界。

误区四:补票表单只是收个邮箱

不对。不同本地化还会要求地址、税号等发票必要字段。

实战排错顺序

如果门店遇到“顾客扫了码打不开、补票找不到订单、已开票却还卡补票页”的问题,建议按这个顺序查:

  1. 看小票上的 URL 是否真的带了 access_token
  2. 看该 pos.order 是否存在且 token 有值;
  3. 没二维码时,核对 pos_referencedate_orderticket_code 是否同时匹配;
  4. 看订单是否其实已经有 account_move
  5. 看 portal invoice token 是否已生成;
  6. 看本地化必填字段是否导致补票表单校验失败;
  7. 最后再排查用户权限或网站路由问题。

最后的结论

Odoo POS 小票补票做得好的地方,不只是“顾客能自助补开”,而是它在便利性和访问边界之间做了相当克制的平衡。

顾客看到的是一张小票和一个二维码;系统真正维护的,却是一条从票面信息到 access token,再到 portal invoice 的受控访问链。

DISCUSSION

评论区

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