先说结论
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_signature、require_payment、prepayment_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_signaturerequire_paymentprepayment_percentsignaturesigned_bysigned_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
评论区