POS 套餐弹窗

Odoo POS 套餐为什么不只是“选几样配菜”:combo 弹窗、选择上限与小票拆分边界讲透

很多人把 POS combo 理解成“套餐主品下面挂几个子品项”,但 Odoo 真正处理的是“哪些 combo 需要弹窗、单选组如何自动清空、免费份额与加价份额怎么拆、为什么小票和报表既看见套餐又看见所选内容”。本文结合 point_of_sale 前后端源码,讲清套餐在前台展示与落单拆分上的真实边界。

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

很多人第一次接触 Odoo POS 的 combo,会把它理解成一个很简单的东西:

  • 主商品是“套餐”;
  • 下面挂几个可选品;
  • 收银员随便点几个;
  • 系统再把结果记下来。

这个理解只够解释“能选什么”,解释不了“为什么有时会弹窗、有时不会”“为什么同样是套餐,有的只能单选、有的能多选”“为什么小票上既像一行套餐,又能看见明细子项”。

也就是说,combo 真正难的地方不是有没有子项,而是:

前台怎么让人正确选,后台怎么把它拆成可追踪的订单行。

先说结论

结合 ComboConfiguratorPopuppos_store.jspos.order.linereport_sale_details.py,可以先抓住五个结论:

  1. 不是每个 combo 组都会弹复杂选择界面。
  2. qty_maxqty_free 分别控制“最多选多少”和“多少份算套餐内含”。
  3. 单选型 combo 不是靠人工记忆避免多选,而是前端会主动清空旧选择。
  4. 套餐真正落单时,会被拆成父行与子行,而不是只保留一个“套餐壳”。
  5. 小票和销售明细会把所选子项重新汇总展示出来,所以前台展示和后台结构不是一回事。

为什么有的 combo 要弹窗,有的几乎像自动带出

ComboConfiguratorPopup 里有个非常关键的判断:shouldShowCombo(combo)

它的逻辑大意是:如果这个 combo 组满足以下任一条件,就值得给用户展示更明显的选择界面:

  • 组内有多个可选项;
  • qty_max > 1
  • 即使只有一个商品,但这个商品本身还是 configurable。

反过来,如果:

  • 只有一个候选项,
  • qty_max == 1
  • 而且商品本身也不需要再配置,

那么 Odoo 甚至会自动把它视为默认选择。

这体现了一个很实用的前台设计:

POS 不应该为了“技术上是 combo”就让收银员每次都多点一轮无意义弹窗。

也就是说,Odoo 在 combo 展示上追求的不是“结构完整”,而是“收银速度和配置复杂度相匹配”。

qty_max 控制的是选择上限,不是套餐总数量

很多门店在配置套餐时,最容易把 qty_max 误解成“这个套餐总共能卖多少份”。

不是。

在弹窗选择逻辑里,它控制的是:某个 combo 组里最多能选多少个项目。

所以如果一个组设置:

  • qty_max = 1

前端就会把它当成单选组。

而在 resetSingleQtyMaxCombo(combo) 中,Odoo 甚至会在切换选择时主动把该组已有选择清空,确保只有一个结果留下来。

这说明 Odoo 并不依赖收银员“自己记得不要多选”。 它在前端层就把单选边界做死了。

这类设计非常重要,因为 POS 不是后台表单。 前台必须更偏“防误触”,不能把复杂配置理解留给高频操作人员自己消化。

qty_free 决定哪些子项算套餐内含,哪些算额外加价

getSelectedComboItems() 里,Odoo 会把选中的内容拆成两组:

  • itemsIncluded
  • itemsExtra

拆分依据就是 qty_free

含义非常直白:

  • 在免费份额内的,属于套餐内含;
  • 超出免费份额的,就进入额外项目,需要另外计价。

这跟很多线下餐饮门店的直觉完全一致:

  • 套餐送一杯饮料;
  • 第二杯不是不能点,而是要补差价;
  • 套餐内允许选两份小食,但第三份开始算加购。

也正因为这样,combo 不是“选完再平均摊价”这么简单。 它要先把业务含义拆出来:

  • 哪些是 included;
  • 哪些是 extra;
  • 哪些 extra 还可能带属性配置。

套餐真正落单时,不会只剩一行主商品

pos_store.jshandleComboProduct() 中,Odoo 最终会把 combo 结果写进:

values.combo_line_ids = comboPrices.map(...)

也就是说,当套餐加入订单时,系统不是只保留一个模糊的“套餐商品”。 它还会为所选内容创建真正的子行,带着:

  • product_id
  • combo_item_id
  • price_unit
  • qty
  • attribute_value_ids
  • custom_attribute_value_ids

对应到后端模型,就是 pos.order.line 里的:

  • combo_parent_id
  • combo_line_ids
  • combo_item_id

所以 Odoo 的 combo 结构更像:

  • 父行负责表达“这是一个套餐”;
  • 子行负责表达“你在这个套餐里具体选了什么”。

这正是套餐可追踪、可打印、可统计的前提。

为什么小票和报表既看到套餐,又看到内容

很多人会困惑:

  • 订单行里已经拆成父子结构了;
  • 那为什么销售明细里又像把所选内容拼回主套餐后面?

因为 report_sale_details.py 会在检测到 combo_line_ids 后,把子品项名称拼成:

' (' + ", ".join(line.combo_line_ids.product_id.mapped('name')) + ')'

也就是类似:

  • 双人套餐(薯条、可乐、鸡翅)

这一步很有意思。

它说明 Odoo 在展示层采用的不是“完全暴露底层结构”,而是:

  • 对数据库和逻辑层,保留父子行;
  • 对人类阅读层,把选择结果重新拼成好读的标签。

这也是为什么套餐在报表里往往看起来比底层数据更自然。

展示层与结构层为什么必须分开

如果前台展示和后台结构完全一样,门店会遇到两个问题:

1)展示太碎

收银员和店长只想知道:

  • 这卖的是哪个套餐;
  • 顾客选了哪些内容。

他们不想看一串技术味很重的父子行关系。

2)结构太粗

如果只保留“一行套餐”,后面很多事都难做:

  • 小票没法体现所选明细;
  • 某些准备环节没法识别子项;
  • 统计和售后也更难解释。

所以 Odoo 的做法其实很成熟:

面向机器时拆细,面向人时再合适度重组。

为什么套餐“展示”不是纯前端问题

很多人会把 combo 的展示视为 UI 问题,但其实它直接影响后续业务链。

因为一旦你在弹窗里做出的选择被拆成真实子行,这些子行后面会继续参与:

  • 小票展示;
  • 销售明细;
  • 某些准备单或厨房分类判断;
  • 售后解释“顾客当时到底选了什么”。

换句话说,combo 的展示不是漂亮不漂亮的问题,而是能不能把顾客的选择,稳定地传进后续链路。

常见误解

误解 1:所有 combo 都应该弹窗

不对。

如果组合足够简单,Odoo 会尽量减少无效交互。

误解 2:qty_max 就是套餐卖几份

不对。

它首先控制的是某个 combo 组内可选项目上限。

误解 3:套餐只要前台显示清楚就够了

不对。

如果落单结构不清楚,后面的小票、报表和准备流程都会混乱。

误解 4:小票看到的是底层真实结构

也不完全对。

展示层通常会把子项重新拼成更适合阅读的文本。

实战排查顺序

当套餐展示或拆分不符合预期时,建议按这个顺序查:

  1. 该 combo 组是否真的需要弹窗;
  2. qty_maxqty_free 的业务含义有没有配反;
  3. 单选组是否因 reset 逻辑自动清掉了旧项;
  4. 额外项目是否进入了 extra 逻辑;
  5. 订单里是否真的生成了 combo_line_ids
  6. 报表和小票是结构层问题,还是只是展示拼接问题。

最后的理解方式

如果只记一句话,我建议记:

Odoo POS 的 combo 不是“让顾客选配菜”,而是“把一次选择过程拆成既方便收银、又方便后续追踪的父子订单结构”。

前台弹窗负责降低误操作, 父子订单行负责保留真实选择, 小票与报表再把这些结构重新翻译成人能看懂的结果。

这三层一起,才是 Odoo POS 套餐真正成熟的地方。

DISCUSSION

评论区

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