POS 活动票务

Odoo POS 卖活动门票为什么不是“卖一张票品就结束”:registration、座位回写与退款取消链路讲透

很多人以为 POS 卖活动票只是把 event ticket 当成普通商品,但 Odoo 真正在处理的是报名记录建档、可售座位同步、badge 邮件发送,以及退款时按数量取消对应 registration。本文结合 pos_event 源码讲清:为什么活动票不是简单产品销售、为什么座位数会跨会话刷新,以及退票时系统到底取消了什么。

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

很多人第一次看到 pos_event,都会下意识地把它理解成一句话:

在 POS 里卖活动门票,本质上就是卖一个 product。

这句话不算全错,但如果你只停在这里,后面几乎一定会看不懂这些现象:

  • 为什么一张票卖出后,可售座位数会在其他 POS 会话里同步变化;
  • 为什么退票不是简单冲一张负数单,而会取消报名记录;
  • 为什么 paid 订单读回前端时,还会把 event.registrationevent.slot、问卷答案一起带回来;
  • 为什么某些买票订单还会自动发 badge 邮件。

先说结论:在 Odoo 里,POS 活动票从来不是“普通商品换个名字”。它真正销售的是“门票商品 + 报名实体 + 座位占用 + 退款取消语义”的组合。

活动票为什么不是普通商品

pos_event 里,pos.order.line 被扩展了两个关键字段:

  • event_ticket_id
  • event_registration_ids

这说明一条 POS 订单行不只是“卖了某个产品”,它还能直接挂一组报名记录。

也就是说,前台卖出一张活动票后,后台真正沉淀下来的不仅是销售事实,还有:

  • 这张票对应哪个 event;
  • 用了哪种 ticket;
  • 形成了哪些 attendee / registration;
  • 是否有 slot、问卷答案等附加上下文。

所以活动票在 POS 里的真实对象,不只是 order line,而是 order line 背后的 registration 集合。

pos_event.models.pos_order.read_pos_data() 很值得注意。它在父类返回 POS 订单数据后,还会针对 paid / done / invoiced 的订单,额外把这些数据一起读回:

  • event.registration
  • event.event
  • event.event.ticket
  • event.slot
  • event.registration.answer

这很关键,因为它说明 Odoo 的目标不是“卖完票就跟 POS 无关了”,而是让 POS 前后端都能继续拿到完整票务上下文。

比如门店现场可能还需要:

  • 重看 attendee 信息;
  • 打印 full page ticket;
  • 打印 badge;
  • 核对 slot 或报名答案。

为什么卖出一张票后,其他会话座位数也会变化

event.registration.create() / write() 之后,会触发 _update_available_seat();而这个方法会找到所有未关闭的 pos.session,再调用 config._update_events_seats()

_update_events_seats() 会通过 bus 推送 UPDATE_AVAILABLE_SEATS,里面带着:

  • event 的剩余座位;
  • 每个 ticket 的剩余座位;
  • 每个 slot 的剩余座位。

这意味着:活动票的库存不是等门店手动刷新才变化,而是会主动广播给所有 open session。

这和普通商品库存认知很不一样。因为活动票卖掉后,系统更关心的是“剩余报名容量”,而不是仓库货架上的实物数量。

event.registration 在 POS 语义里扮演什么角色

event.registration 被扩展成 pos.load.mixin,并新增:

  • pos_order_id
  • pos_order_line_id

同时 _compute_registration_status() 里会根据 POS 订单状态决定 registration 的状态:

  • 订单取消 → registration 取消;
  • 金额为 0 → sale_status='free'state='open'
  • 正常售出 → sale_status='sold'state='open'

这说明 registration 不是外围附属表,而是门票销售语义的核心承载体。

POS 卖票真正想维护的是:

是谁,以什么票种,在什么事件/时段下,获得了一个有效报名名额。

退款时系统到底取消了什么

pos_event.models.pos_order._process_order() 对退款有一段非常关键的处理。

它会:

  1. 找出退款行对应的原 pos.order.line
  2. 看原行上是否存在 event_registration_ids
  3. 计算本次退款数量与已取消数量;
  4. 只把尚未取消的那部分 registration 改成 cancel

这意味着退票不是粗暴“原单整票作废”,而是尽量按退款数量精确取消报名名额。

所以活动票退款真正被撤回的,不只是销售金额,还包括 attendee 占掉的 event capacity。

为什么活动票退款比普通商品退款更需要原单回链

普通商品退款,很多时候只要金额和库存链闭环就行;但活动票还得处理:

  • 取消哪几个报名名额;
  • 剩余名额是否重新开放;
  • 已取消和未取消 registration 如何避免重复取消;
  • 后续 badge / attendee 名单如何同步变化。

因此活动票退款特别依赖原 order line 的回链,而不只是负数量本身。

自动发 badge 邮件说明了什么

read_pos_data() 里对 registration 还会做一件事:若 registration 有 email,则调用 action_send_badge_email()

这件事很能说明 Odoo 的设计方向:POS 卖票不是“前台收完钱就收口”,而是把活动后续触达也串起来。

所以如果门店问“为什么在 POS 卖票也会触发 badge 邮件”,这不是旁支功能,而是票务销售链的一部分。

最容易误解的三件事

误区一:POS 活动票就是普通 product 销售

不对。它还会产生 registration、slot 和问卷答案等实体。

误区二:座位数变化只跟后台活动模块有关

不对。POS 卖票和退票也会主动驱动 seat availability 广播。

误区三:退票只是在财务上做负数冲回

也不对。它还会取消对应 attendee registration,占用名额也随之释放。

实战排错顺序

遇到“门票卖出后座位没更新 / 退票后 attendee 没取消 / POS 前端看不到票务上下文”时,建议按这个顺序查:

  1. 先看订单行上是否正确写入 event_ticket_id
  2. 再看 event_registration_ids 是否真的生成;
  3. 看相关 registrationstatesale_status 是否符合订单状态;
  4. _update_available_seat() 是否触发、open session 是否收到 UPDATE_AVAILABLE_SEATS
  5. 若是退款问题,检查原单回链与已取消数量计算是否正确;
  6. 若前端信息不全,再查 read_pos_data() 是否把 event 相关模型一起带回。

最后的结论

POS 卖活动票之所以复杂,不是因为 Odoo 过度设计,而是因为“卖票”本来就不是单纯卖一件商品。

它至少同时涉及四层语义:

  • 销售订单;
  • 报名实体;
  • 座位容量;
  • 退款取消与后续通知。

所以别再把 pos_event 理解成“POS 里卖个 event ticket product”。

它真正卖出去的,是一个可被追踪、可被取消、会占座位、还能继续触达参会人的报名资格。

DISCUSSION

评论区

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