先说结论
Odoo 官网活动报名,如果涉及收费门票,就不再是“提交报名表单,再跳到支付”这么线性的流程。
website_event_sale 的真正做法是:
- 先把报名里选中的票种、场次和人数整理出来;
- 再把它们落成购物车里的
sale.order.line; - 报名人与订单、订单行、event ticket 一一挂上;
- 如果门票免费且当前没有既有购物车,可以直接跳过销售与支付;
- 如果有收费票,系统又会继续检查匿名地址、checkout 与库存/席位边界。
所以活动报名在 Odoo 里并不是 event 子系统单打独斗,而是:
一旦进入收费模式,报名流程就会主动并入网站销售订单体系。
一、为什么 Odoo 先处理“门票”,再处理“报名人”
_create_attendees_from_registration_post() 很能说明问题。
如果这次报名数据里根本没有 event_ticket_id,系统会回到普通活动报名逻辑。
但只要存在 ticket,流程就变了:
- 先把所有报名项里的
event_ticket_id抽出来; - 建立 ticket 映射;
- 判断这些票是不是全免费;
- 如果不是,就先确保当前有
sale.order购物车。
这说明 Odoo 在收费活动里,优先解决的不是“报名表单长什么样”,而是:
- 这笔交易要不要进入销售单;
- 哪些报名人对应哪种票;
- 同一票种在同一场次到底买了几张。
也就是说,报名人信息只是前台输入; 真正决定链路的是门票和订单关系。
二、为什么票会按“场次 + 票种”聚合到购物车
源码里把 tickets_data 的 key 设计成:
(event_slot_id, event_ticket_id)
然后再对每组数量调用 _cart_add()。
这意味着 Odoo 不是给每一个报名人都单独加一条 cart line,而是先按:
- 哪个场次;
- 哪个票种;
- 总共几张;
进行归并。
这么做非常合理。
因为购物车本质上是交易容器,不是报名问卷。
- 报名人细节适合挂在 registration 上;
- 订单行适合承载“买了几张某类票”。
如果不分层,订单会很快变成一堆几乎不可维护的冗余行。
三、为什么席位校验不是最后支付时才统一做
sale.order._verify_updated_quantity() 在 event ticket 场景下被专门扩展了。
如果带了 event_ticket_id,它会:
- 检查 ticket 是否存在;
- 场次 slot 是否存在;
- 计算该票当前剩余席位;
- 对比这次新增数量;
- 若已售罄则不允许继续增加;
- 若超出剩余量,则自动截到最大可买数量并返回 warning。
这说明 Odoo 不想等到支付最后一步才告诉用户“票没了”。
而是尽量在购物车更新阶段就把边界收紧。
这对官网活动尤其重要,因为:
- 活动票常常是稀缺资源;
- 用户对“临门一脚才报错”最敏感;
- 报名页如果反馈太晚,转化和口碑都会受影响。
四、为什么免费票可以自动确认,而收费票一定要进入 checkout
registration_confirm() 里这段分支特别关键。
如果订单里根本没有收费 ticket,或者总金额为 0,系统会:
- 直接
action_confirm(); sale_reset()清掉当前网站购物车;- 写入
sale_last_order_id; - 跳去
/shop/confirmation。
但只要订单有金额,系统就会:
- 把
sale_last_order_id记进 session; - 引导去
/shop/checkout?try_skip_step=true。
也就是说,Odoo 没把“活动报名”做成和“销售单支付”并行的第二套流程。
它的思路是:
- 免费票本质上不需要支付确认,可以快速收口;
- 收费票则应复用成熟的 checkout / payment 链路。
这让活动和电商在支付层保持一致,后期维护成本更低。
五、为什么匿名用户买票时,系统还会顺手补客户地址
如果 order_sudo._is_anonymous_cart() 且订单有金额,源码会调用:
CustomerPortal()._create_or_update_address(...)
并把报名数据里的第一位报名人信息拿去补订单 partner_id。
这个设计很聪明。
因为活动报名里,很多时候你还没正式登录,但又必须进入支付。
如果继续保持公共用户身份:
- 税、账单、确认邮件、后续订单查询都会很别扭。
所以 Odoo 在进入 checkout 前,尽量用报名人信息把匿名单“扶正”为一个真实客户订单。
六、为什么减数量时还要取消多出来的报名记录
_cart_update_order_line() 在 event ticket 场景下还有一层补充:
- 如果订单行数量减少了,系统会找到超出的
event.registration; - 按创建时间顺序把多出来的那几条
action_cancel()。
这件事非常关键。
因为活动票不是普通商品:
- 订单行数量变化,背后对应的是座位、报名名额和 attendee 记录;
- 如果只改 sale order line,不同步 cancel registration,活动人数会立刻失真。
所以 Odoo 明确保证:
交易数量和报名人数不能各走各的。
七、实施时最容易误解的地方
1. 以为活动报名和购物车是两套平行逻辑
不对。收费票场景下,报名会主动并入 sale.order。
2. 以为一个报名人就该一条销售订单行
不对。订单行按票种/场次聚合,报名人信息另挂 registration。
3. 以为席位校验只在最终付款前做
不对。更新购物车数量时就已经开始校验。
4. 以为免费票和收费票只差一个金额字段
不对。免费票可以直接确认,收费票必须进入 checkout 生命周期。
最后总结
Odoo 的 website_event_sale 真正做的是:
- 用 event ticket 定义“卖什么”;
- 用 sale order 定义“买了多少”;
- 用 registration 定义“谁来参加”;
- 用席位校验和自动取消保证数量一致;
- 用 checkout / payment 把收费报名接入统一交易链路。
所以它不是“活动页面带支付”,而是:
把官网活动报名做成了一个以门票和席位为核心的销售型报名系统。
DISCUSSION
评论区