很多人第一次接触 Odoo 餐饮 POS,会自然地把它理解成:
一张桌台单,不断往里加菜,最后一起结账。
这句话只说对了一半。 真正让餐饮场景和普通零售 POS 拉开差距的,不是“有桌台”这么简单,而是:
- 同一桌的草稿订单什么时候应当合并;
- 同一桌点的菜,什么时候应该进同一轮 course;
- 后厨“已发单 / 未发单”的边界在哪里。
结论先说:Odoo 餐饮 POS 不是把零售订单硬套到餐桌上,而是额外引入了 table 上下文和 restaurant.order.course 节奏层。 这就是为什么它不是“有桌号的普通订单”,而更像“桌面会话 + 出菜批次”的组合。
为什么 open order 会按桌台合并,而不是只按 UUID 认单
在 pos_restaurant/models/pos_order.py 里,_get_open_order() 对餐饮配置做了特殊处理。
如果订单:
- 带有
table_id; - 状态仍是
draft;
那么检索条件不是单纯找同 UUID,而是:
- 同 UUID,或者
- 同一桌台且仍是 draft 的订单。
这件事非常关键。它意味着在餐饮模式下,系统默认承认一种业务现实:
同一桌台的持续加单,本来就可能不是“一次创建、一口气完成”的单据行为。
这也是为什么实施现场常问:
- 为什么服务员二次点单时没有新开一张单;
- 为什么换设备后还能接着同桌点;
- 为什么桌台还没买单,就继续叠在原单上。
答案通常不是前端缓存,而是后端 open order 识别策略本来就优先保留桌台上下文。
为什么 Odoo 要单独建一个 restaurant.order.course
pos_restaurant/models/restaurant_order_course.py 新建了模型 restaurant.order.course,字段很少,但意义很重:
order_idline_idsindexfiredfired_dateuuid
这说明 Odoo 对“餐饮出菜轮次”的理解,不是给订单行多加一个状态而已,而是专门抽出一层 course 对象。
为什么要这样设计?
因为餐饮里真正重要的不是“这道菜属于哪张单”,而是:
- 它属于第几轮;
- 这一轮有没有正式 fire 给后厨;
- 后厨端和前台端能否围绕这轮批次达成一致。
如果只是给 pos.order.line 加一个 sent_to_kitchen=True/False,你就很难优雅表达:
- 一轮里有哪些菜一起发;
- 这轮什么时候发出去;
- 新增菜是补进旧轮还是进入新轮。
而单独建 course 后,这个层次一下就清楚了。
fired 和 fired_date 为什么是餐饮排错的关键字段
在 create() 和 write() 里,只要 course 被标记为 fired=True,就会自动补 fired_date。
看起来只是顺手记个时间,但这其实定义了后厨协作边界:
fired=False:这还是前台草拟节奏,没正式交给后厨;fired=True且有fired_date:这轮已经成为厨房执行对象。
这和零售 POS 完全不同。零售更关心“是否支付”“是否打印”“是否出库”,而餐饮要在支付之前就先处理一层“是否送厨”。
所以如果一线人员反馈:
- 菜品已经在桌面订单里了,但后厨没收到;
- 同桌后加菜进了错误批次;
- 已送厨的批次又被当作未送厨修改;
你第一反应不应只看订单状态,而要先看 course 是否 fired、何时 fired。
为什么订单行不是直接记 course 文本,而是 Many2one 到 course
pos_order_line.py 给 pos.order.line 新增了 course_id。这说明订单行和 course 是明确的引用关系,而不是字符串标签。
这带来的好处有三点:
- 一轮 course 可以挂多行菜品;
- 订单行可以稳定跟随后厨批次,而不是跟随前端 UI 文本;
- 加单、拆单、同步时,course 仍然是可追踪对象。
换句话说,Odoo 这里在保护的是“出菜批次的可计算性”,不是“前台显示好不好看”。
一桌多设备协同时,系统真正在同步什么
read_pos_data() 会把 restaurant.order.course 一起装进 POS 数据里。这意味着餐饮多设备协同时,并不只是同步订单头和订单行,还要同步 course 层。
这件事很重要,因为很多人以为餐饮多端同步只是在抢同一张单,实际上同步对象至少有三层:
- 订单头;
- 订单行;
- 出菜轮次。
如果只同步前两层,不同步 course,前台就会出现非常典型的问题:
- A 设备看到这道菜已进第二轮;
- B 设备却把它当第一轮未发单菜;
- 后厨节奏与前台显示逐渐脱轨。
最容易误解的四件事
误区一:同桌就应该永远只保留一张单
不对。 系统更准确的说法是:同桌 draft 单倾向于被识别为同一 open order。 一旦结账、换桌、状态变化或业务流程切换,边界就不同了。
误区二:course 只是餐饮版的备注分组
不对。 course 不是 UI 分组,而是后厨节奏对象,带有 fired 状态和时间边界。
误区三:后厨没收到菜,一定是打印机或网络问题
不一定。 更常见的是这轮 course 还没 fire,或新增菜被放进了错误 course。
误区四:只看 pos.order.state 就能判断餐饮流程卡在哪
不对。 在餐饮里,订单是否 draft 只是一层。course 是否 fired 往往更早决定执行状态。
实战排错顺序
如果你遇到“一桌多单混乱 / 加菜进错轮次 / 后厨没收到 / 同桌重复单”,建议按这个顺序查:
- 当前 POS 配置是否启用了
module_pos_restaurant; - 订单是否仍是
draft; - 是否存在同一
table_id的 open order; - 二次点单时命中的到底是 UUID 还是 table 归并逻辑;
- 相关订单行的
course_id是否正确; - 对应 course 是否已
fired; fired_date是否合理记录;- 多设备读到的
restaurant.order.course数据是否一致。
最后的结论
Odoo 餐饮 POS 的深层设计,不是“给订单加张桌号图纸”,而是把餐饮场景拆成两层:
- 桌台层:解决同桌持续加单、跨设备续单;
- course 层:解决一桌订单内部的出菜批次与送厨边界。
所以如果你想真正理解餐饮 POS,不该只盯着“订单有没有合并”,而应盯住:
桌台上下文是怎么续上的,course 又是怎么把“已点”变成“已送厨”的。
DISCUSSION
评论区