先说结论
Odoo 的活动售票,核心不是“把活动票放进销售订单里卖出去”,而是把三套原本容易断开的业务对象真正绑在一起:
sale.order/sale.order.line:负责交易event.registration:负责参会人event.event.ticket/event.slot:负责活动资源
从 /home/ubuntu/odoo-temp/addons/event_sale/models/ 这组源码来看,官方真正想解决的是:
一张订单成交以后,系统怎么知道哪些人可以算正式参会、他们对应哪种票、哪个场次、又该把流量来源算到哪里。
所以 event_sale 不是简单“售票插件”,而是一层把销售、运营、归因口径统一起来的联动层。
第一层:为什么报名状态不能脱离销售订单单独判断
event.registration 在接入 event_sale 后,最关键的变化之一就是 _compute_registration_status()。
这段逻辑不是只看报名记录自己,而是会连带看:
sale_order_id.state- 订单总额是不是 0
- 当前报名是否已被取消
然后把报名状态和销售状态拆成两层:
state:比如draft、open、cancelsale_status:比如free、to_pay、sold
这非常重要。
因为活动场景里,“有人提交了报名意向”和“这个人是正式参会者”根本不是一回事。
举个很典型的例子:
- 免费票:下单后可以直接算
open - 付费票但订单还没确认:报名仍可能只是
draft - 订单取消:报名也要同步变
cancel
也就是说,Odoo 不把参会资格只看成表单填写结果,而是看成交易状态的业务投影。
第二层:为什么 sale order line 要强制带 event、ticket,必要时还要带 slot
在 sale_order_line.py 里,官方对 event 类服务有非常明确的约束:
- 有
service_tracking == 'event' - 就必须有
event_id - 必须有
event_ticket_id - 如果活动支持多时段,还必须有
event_slot_id
否则直接报错。
这说明在 Odoo 的设计里,活动票不是“普通服务商品换个名字”,而是一个带上下文的业务商品。
系统真正需要的不只是卖出去一条 line,而是确认:
- 卖的是哪场活动
- 卖的是哪种票
- 卖的是哪个时间段
如果这几件事不在销售订单层就钉死,后面就会出现一串口径混乱:
- 参会人数算不到具体场次
- 同一张票卖给了错误活动
- 报名人信息无法回到正确票种
所以这个约束看似严格,其实是在前面多做一次校验,换后面整条链路稳定。
第三层:为什么确认订单时不是立刻把所有报名人都当成最终有效
SaleOrder.action_confirm() 里会做两件很关键的事:
- 检查 event 类订单行是不是都已配置完整
- 调
_init_registrations()去补齐对应的报名记录
但 _init_registrations() 还有个很细的判断:
- 如果是后台确认单笔订单,且这条 line 有金额
- 新建出来的 registration 默认会先是
draft - 让后续仍有空间补完 attendee details
这个细节很像真实业务。
因为对付费活动来说,订单确认并不总是意味着报名资料已经完整。
很多公司流程是:
- 商务先下单
- 人数先锁住
- 具体参会人姓名、邮箱、手机号后补
如果系统一确认订单就把所有参会人直接当作完全就绪,反而会让运营误判。Odoo 在这里明显是把“销售成交”和“报名资料齐全”拆开处理。
第四层:为什么 registration 会主动同步销售订单上的 UTM
event.registration 里新增了:
utm_campaign_idutm_source_idutm_medium_id
而且都是根据 sale_order_id 来计算同步。
这说明官方并不满足于“订单上有归因就行”,而是希望流量来源一路沉到参会人层。
这对活动运营很关键。
因为你真正经常会问的问题不是:
- 哪个广告带来了多少订单?
而是:
- 哪个渠道带来了多少真实参会人?
- 哪个 campaign 带来的付费参会人转化更好?
- 哪个 source 虽然有单,但最后取消率很高?
如果 UTM 只停留在销售订单,活动团队看参会名单时就断层了。把 UTM 带到 registration,报表口径才有机会统一。
第五层:为什么改票种或场次时,Odoo 会专门创建 warning activity
event.registration.write() 里还有个特别值得注意的动作:
如果你改了:
event_slot_idevent_ticket_id
系统会给对应销售订单安排一条 warning activity。
这说明官方默认认为:
报名数据一旦和销售订单挂钩,后续改票、改场次都不是“静默小改动”,而是需要被业务方看到的异常变更。
这非常合理。
因为改票种可能影响:
- 价格
- 座位/名额统计
- 票面权益
- 财务对账
改场次则可能影响:
- 签到名单
- 时间资源分配
- 会务通知内容
与其假装这是普通字段修改,不如明确给销售或活动负责人一个提示:这里有业务变更,需要确认。
第六层:为什么 partner 同步发生在订单层,而不是只在报名页里自己维护
SaleOrder.write() 里有个常被忽略但很实用的逻辑:
只要订单里有 event 类 line,且订单 partner_id 变了,系统就会把相关 registration 的 partner_id 一起同步。
这背后的思路很清楚:
- 销售订单是商业主体
- registration 是活动主体
- 两者不能长期脱节
尤其在网站下单或商务代客下单场景里,客户信息会在后续地址页、确认页继续被修正。
如果 registration 不跟着更新,就会出现:
- 订单客户和参会联系人不是一套数据
- CRM / 营销归因跟活动报名无法对上
- 后续发票、联系人分析、客户旅程被拆开
第七层:为什么事件总销售额不是简单求和,而是按币种和已确认状态汇总
event_event.py 里 sale_price_total 的计算也很说明问题。
它只统计:
- 已确认的
sale.order.line - 非 0 金额
- 且会做币种换算
换句话说,活动销售额并不是“所有 line 总价粗暴加一起”,而是一个带财务边界的运营指标。
这说明 Odoo 很清楚,活动团队需要看的销售额必须满足两个条件:
- 足够接近真实成交
- 可跨订单币种汇总
否则这个数字在国际公司、多币种公司里根本没法用。
最容易误解的三个点
误区一:有订单就等于参会已成立
不一定。 对免费票、付费票、取消单,registration 的业务状态完全不同。
误区二:UTM 只要挂在订单上就够了
不够。 活动团队最终分析的是参会人、签到人、取消人,不只是订单。
误区三:改票种和场次只是报名侧的小修小补
不是。 它会影响销售、资源、通知和统计口径,必须当成业务变更处理。
实施时怎么用最稳
如果你在做 Odoo 活动售票实施,我更建议这样理解和配置:
- 把 event 类商品看成“带业务约束的服务商品”,不要随便绕开 event/ticket/slot 绑定
- 把 registration state 和 sale_status 分开看,别拿一个字段解决所有判断
- 营销分析时优先下钻到 registration 级别,而不是只看 SO 报表
- 对票种、场次改动建立审批或提醒习惯,避免现场运营被静默改乱
- 网站售票、商务售票、免费报名要分别测试状态流转,不要只测 happy path
最后总结
Odoo Event 接上 Sales 之后,真正建立起来的不是“卖票能力”,而是:
- 订单负责交易成立
- registration 负责参会身份成立
- ticket / slot 负责资源归属成立
- UTM 负责营销归因成立
这四层一起打通,活动团队、销售团队和市场团队看到的才会是同一场活动,而不是三套互相解释不清的数据。
DISCUSSION
评论区