很多人看到 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_referencedate_orderticket_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 把两者结合成:
- 有二维码时,凭
access_token直达; - 没二维码时,用票号+日期+ticket code 找回;
- 找回成功后,仍然转成 token 访问;
- 一旦发票存在,再切到 portal invoice token。
所以你可以把它理解成:顾客可以方便找到票,但系统始终尽量避免直接裸露订单。
最容易误解的四件事
误区一:二维码链接里最好直接放订单 id,简单省事
不对。那会明显削弱访问边界。
误区二:ticket code 就是访问凭证本身
不完全对。ticket code 更像找回订单的辅助因子,真正访问 validate 页面仍靠 access token。
误区三:订单已开票还回补票页更统一
不对。已开票时直接跳 portal invoice 更符合对象边界。
误区四:补票表单只是收个邮箱
不对。不同本地化还会要求地址、税号等发票必要字段。
实战排错顺序
如果门店遇到“顾客扫了码打不开、补票找不到订单、已开票却还卡补票页”的问题,建议按这个顺序查:
- 看小票上的 URL 是否真的带了
access_token; - 看该
pos.order是否存在且 token 有值; - 没二维码时,核对
pos_reference、date_order、ticket_code是否同时匹配; - 看订单是否其实已经有
account_move; - 看 portal invoice token 是否已生成;
- 看本地化必填字段是否导致补票表单校验失败;
- 最后再排查用户权限或网站路由问题。
最后的结论
Odoo POS 小票补票做得好的地方,不只是“顾客能自助补开”,而是它在便利性和访问边界之间做了相当克制的平衡。
顾客看到的是一张小票和一个二维码;系统真正维护的,却是一条从票面信息到 access token,再到 portal invoice 的受控访问链。
DISCUSSION
评论区