销售门户

Odoo 报价门户为什么不只是“给客户看个页面”:在线签署、付款与 access token 的真实分工

很多人以为 Odoo 报价门户只是把销售单搬到前台展示,但官方源码里它同时处理访问校验、浏览记录、在线签字、预付款门槛和支付入口。本文把 sale portal 的这条链路讲透。

销售
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo 的报价门户页,不只是“把报价单渲染成网页”。

它真正负责的是:

在客户可访问的前台里,把访问权限、报价浏览、在线签字、预付款门槛和支付动作,串成一条可成交链路。

所以 /my/orders/<id> 的本质,不是展示页,而是受控成交入口


这篇文章主要参考了哪些源码

主要看的是这些官方实现:

  • /home/ubuntu/odoo-temp/addons/sale/models/sale_order.py
  • /home/ubuntu/odoo-temp/addons/sale/controllers/portal.py
  • /home/ubuntu/odoo-temp/addons/sale/wizard/payment_link_wizard.py
  • /home/ubuntu/odoo-temp/addons/sale/models/res_company.py
  • /home/ubuntu/odoo-temp/addons/website_sale/models/sale_order.py

从这些文件里可以看到几个核心点:

  • sale.order 继承了 portal.mixin
  • 订单自身有 require_signaturerequire_paymentprepayment_percent
  • 门户控制器会用 access_token 做访问校验
  • 支付金额如果低于要求的预付款,会直接报错
  • 客户第一次打开报价,还会给销售侧留浏览痕迹

这已经不是简单前台渲染了。


为什么 Odoo 要把“访问”做成 token,而不是直接公开链接

sale/models/sale_order.py 里,销售单通过 portal.mixin 拿到门户访问能力;在 sale/controllers/portal.py 里,控制器通过 _document_check_access('sale.order', order_id, access_token=...) 校验访问。

这说明 Odoo 的思路是:

  • 订单不是公开资源
  • 但它又需要被客户低摩擦访问
  • 所以用 portal 权限 + access token 组合来解决

也就是说,access_token 的作用不是“方便拼链接”,而是:

在不要求客户先进后台的情况下,给这张报价一个可控、可验证、可失效的访问凭证。

这是典型的业务前台设计:

  • 既不能裸奔公开
  • 也不能逼所有客户先注册再走复杂权限流程

门户页为什么会记录“客户看过报价”

这点特别有意思。

portal_order_page() 里,如果是 public / portal 用户带着 access_token 访问,且不是 link preview,系统会:

  • 检查 session 里今天是否已经记录过
  • 如果没有,就 message_post() 一条“Quotation viewed by customer ...”

这个行为非常像真实销售团队的需求。

因为销售最关心的问题之一就是:

  • 客户到底有没有看
  • 是第一次看还是已经看过多次
  • 什么时候看过

所以门户页不仅是客户入口,还是销售感知客户动作的信号源。

这也解释了为什么源码里还特意排除了 link preview:

系统不想把“聊天软件自动抓预览图”误判成“客户认真看过报价”。

这是很细的产品判断。


在线签字和在线付款,为什么都挂在 sale.order 上

sale/models/sale_order.py 里,订单有:

  • require_signature
  • require_payment
  • prepayment_percent
  • signature
  • signed_by
  • signed_on

这说明 Odoo 没把“成交动作”放在外部系统里漂着,而是把它们直接建模进销售订单。

它表达的业务语义很清楚:

  • 是否需要签字,是报价确认条件的一部分
  • 是否需要付款,是报价确认条件的一部分
  • 签的是谁、什么时候签,也属于订单状态的一部分

所以签字和付款不是附件能力,而是订单确认机制的组成部分


预付款门槛为什么不是“客户想付多少都行”

portal_order_page() 里,有一段关键逻辑:

  • 先算 prepayment_amount
  • 如果客户输入的 payment_amount 小于这个门槛,且订单还没确认
  • 直接抛错:The amount is lower than the prepayment amount.

再结合 payment_link_wizard.py 里的 _compute_warning_message(),你会发现这套逻辑在生成支付链接时就已经做预警了。

这说明 Odoo 的意图不是“客户先随便付一点试试”,而是:

如果这张报价要求预付款,那支付页和支付链接都必须尊重这条成交门槛。

业务上这很重要。

否则销售明明要求 50% 预付,客户却先付 1 块钱,系统也算“进入付款流程”,这会把成交语义彻底搞乱。


门户支付页到底在准备什么

很多人以为前台支付页只是把几个支付方式列出来。

_get_payment_values() 实际上做了不少事:

  • 判断当前是全额支付还是 down payment
  • 计算本次应付金额
  • 找兼容的 payment.provider
  • 找兼容的 payment.method
  • 找可用 token
  • 计算 transaction_route
  • 准备 landing_route
  • 确保 access_token 存在

这说明支付页不是一个被动页面,而是一个按订单上下文动态装配的支付场景

同一家公司不同订单,支付侧看到的东西可能不同,因为:

  • 币种不同
  • 金额不同
  • 公司不同
  • 客户不同
  • 订单是否要求预付款不同

所以“支付方式显示异常”时,不能只看支付配置,也要看销售单上下文。


支付链接为什么要跳回门户页,而不是直接一个支付 URL

payment_link_wizard.py 里,针对 sale.order_prepare_url() 会直接返回 related_document.get_portal_url(),并通过 _prepare_query_params()payment_amount 带进去。

这非常说明问题。

Odoo 不希望支付链接脱离报价门户独立存在,而是希望:

  • 客户先回到这张报价
  • 在这张报价的上下文里完成支付
  • 支付金额、签字要求、订单状态都围绕同一张单据判断

也就是说,支付不是脱离销售单发生的,而是门户报价流程中的一个动作。

这是很“ERP”的设计:

支付服务可以外接,但业务语义必须回到订单本体。


新手最容易误解的 5 件事

1. 以为 access token 只是个“免登录参数”

实际上它是门户访问控制的一部分。

2. 以为客户打开报价页只是前台行为

其实这会反哺销售侧的跟进信号。

3. 以为支付页金额随便输都行

如果设置了预付款门槛,金额是有业务底线的。

4. 以为签字和付款只是外部集成

源码里它们本来就是 sale.order 的确认条件。

5. 以为门户页和支付页是两套系统

实际是同一条受控成交链路的前后两个阶段。


实战排错时应该先看什么

如果你在项目里遇到“报价门户不对劲”,我建议按这个顺序排:

第一步:确认访问问题还是业务问题

  • token 是否存在
  • token 是否匹配这张订单
  • 当前访问者是 public、portal 还是内部用户

第二步:确认报价确认条件

  • 是否要求签字
  • 是否要求付款
  • 预付款比例是多少
  • 报价是否已过期

第三步:再看支付能力

  • 当前公司有哪些 provider 可用
  • 当前客户 / 金额 / 币种是否兼容
  • 当前显示的是全额还是预付款语义

这个顺序很重要。

很多团队一上来就查支付通道,结果问题其实出在订单状态或预付款门槛。


一句话记忆法

Odoo 报价门户不是给客户看个网页,而是在 token 控制下,把浏览、签字、付款和成交确认串成一条链。

DISCUSSION

评论区

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