餐饮门店最容易把 POS 厨房链路想简单。
常见想法是:
- 前台点单,
- 厨房收到,
- 打印机打一张单,
- 事情结束。
但 Odoo 源码告诉我们的恰恰相反。
它真正要解决的问题是:
- 哪些商品该送去哪个厨房设备;
- 一张单后续被修改时,到底该发新增、撤销还是备注更新;
- 多终端同时操作同一桌单时,怎样避免重复打印;
- 如果门店用的是 preparation display 而不只是纸质厨打,同步策略又该怎么变。
所以这条链真正重要的不是“能不能打印”,而是:
厨房收到的必须是正确的增量变更,而不是一堆重复或过期的整单快照。
先说结论
结合 pos.printer、order_change.js、pos_store.js 相关代码,可以先抓住六个结论:
- 厨打不是对全部商品一股脑打印,而是按
pos.category路由到不同 printer。 - Odoo 发送给厨房的核心数据不是“订单全文”,而是“相对上次发送结果的变化量”。
- 数量增加、数量减少、整行删除、备注变化,会被当成不同类型的厨房事件。
- combo 商品不会简单按父套餐路由,子行和父行都会参与 preparation category 判断。
- 如果别的终端先改过单,系统会先警告 order outdated,再决定如何同步。
- 是否配置 preparation display,会影响发送后要不要立刻再 sync 一次。
厨打路由的核心不是打印机,而是商品分类
pos_printer.py 里定义 pos.printer 时,很关键的字段是:
product_categories_idsprinter_typeproxy_ip/epson_printer_ip
这说明 Odoo 对“厨房打印机”的理解不是“某台硬件能不能连上”,而是:
- 这台设备负责哪类商品;
- 它的输出类型是什么;
- POS 配置把哪些品类路由给它。
这就是为什么厨房分发的第一层问题永远是分类,不是网络。
如果一个 printer 只负责:
- 饮品,
- 或炸物,
- 或甜点,
那么前台修改订单时,Odoo 不会把整张单都发过去,而是先按分类过滤出这个设备该知道的那部分。
厨房收到的不是“整张单”,而是变化量
order_change.js 的注释写得非常直白:
这套逻辑是根据
last_order_preparation_change,重新计算要发送给 preparation tools 的内容。
也就是说,厨房链路并不是每次都打印“当前订单完整内容”。
它真正做的是:
- 记住上次已经发给厨房的状态;
- 比较当前订单和上次状态;
- 只把差异部分整理成要新增或要撤销的变化。
这点极其关键。
因为餐饮门店的桌单经常会发生:
- 加菜,
- 退菜,
- 备注变化,
- 同一桌多次补点。
如果每次都整单重打,厨房很快就会乱套。
Odoo 的目标不是“多打一份总没错”,而是“尽量只把新的事实发出去”。
数量增加、减少、删除,为什么会被视为不同厨房事件
在 changesToOrder() 中,Odoo 会把变化拆成:
newcancellednoteUpdate
这很像厨房世界里的三种完全不同语义:
1)new
这是“新增要做的东西”。
可能是:
- 新增一份面;
- 同一份菜数量加 1;
- 补点一杯饮料。
2)cancelled
这不是普通修改,而是“之前发过的东西,现在要撤回”。
这对厨房尤其重要,因为撤销单和新单的处理方式完全不同。
3)noteUpdate
如果数量不变,但备注变了,例如:
- 不放葱;
- 少冰;
- 加辣;
那它也不该被当成整行新单重打一遍。
Odoo 会专门把这种变化作为备注更新处理。
这说明系统在厨房链路里真正保护的是:
厨房要收到“业务语义准确”的变更,而不是模糊的重复文本。
combo 为什么会让厨打更复杂
getOrderChanges() 里有个很值得注意的判断:
- 不只看当前行商品是否属于 preparation category;
- 还会看
combo_parent_id的商品; - 还会看
combo_line_ids里的子项。
这说明 combo 场景下,厨房路由不是只看“父套餐”就完了。
因为厨房真正需要知道的是:
- 这份套餐里到底有哪些制作项;
- 哪些子项属于本打印机负责的品类;
- 父套餐是否需要和子项一起被识别为一组变化。
这也是为什么餐饮套餐在 POS 前台看起来是一笔单,厨房侧却必须更细地理解。
多终端改单时,为什么系统会先说订单过期了
checkPreparationStateAndSentOrderInPreparation() 里有个很重要的保护:
如果本地保存的 last_order_preparation_change.metadata.serverDate 和服务器最新版本不一致,系统会弹出:
- Order Outdated
- 别的设备修改过订单
- 你的修改可能覆盖别人
这不是多余提示,而是厨打链路里的关键防重机制。
因为如果 A 设备已经把某些变化发去厨房,B 设备还拿着旧状态继续算差异,很容易造成:
- 重复厨打;
- 撤销量算错;
- 备注覆盖错对象。
所以 Odoo 会先让订单状态与服务器对齐,再继续发送后续变化。
厨房场景里,“最新状态是谁说了算”远比普通零售订单更敏感。
有无 preparation display,为什么会影响发送后的 sync
sendOrderInPreparation() 的最后有个很关键的分支:
- 如果打印成功,
- 且没有
pos.prep.display, - 才会
syncAllOrders({ orders: [order] })。
注释也写得很清楚:
- 这样别的设备才能知道这些变化已经发过;
- 否则多台设备可能重复打印同一变化;
- 如果已经有 preparation display,没必要做多余同步。
这里能看出 Odoo 对两类厨房工具的不同理解:
纸质厨打场景
打印一旦完成,别的终端必须尽快知道“这些变化已经送出”,否则很容易重复打印。
preparation display 场景
显示屏本身就承担更持续的同步角色,不一定需要用同样方式做额外补 sync。
这就是为什么“厨房有屏和没屏”,在 Odoo 里不只是设备差异,而是分发策略差异。
为什么厨打问题常常不是打印机问题
门店现场一遇到厨房异常,很容易先怪:
- 打印机坏了;
- 网络断了;
- IoT 有问题。
这些当然可能,但源码告诉我们,更常见的问题其实是:
- 品类没配到正确 printer;
- 订单变化不是新增而是取消;
- 备注更新被误解成没发单;
- 终端之间版本不同步;
- combo 子项路由和预期不一致。
也就是说,很多“厨打没出来”不是硬件故障,而是系统在按自己的变更分发规则工作,只是门店没有意识到。
实战排查顺序
当厨房打印或 preparation display 行为异常时,建议按这个顺序查:
- 该 printer 绑定了哪些
product_categories_ids; - 问题商品是否真的落在这些 category 里;
- 当前变化是新增、取消,还是备注更新;
last_order_preparation_change是否已经记录过旧状态;- 是否存在多终端改单导致的 outdated 提示;
- combo 商品是否让父子项都参与了分类判断;
- 门店是否配置了 preparation display,从而影响发送后的 sync 逻辑。
最后的理解方式
如果只记一句话,我建议记:
Odoo POS 的厨房链路不是“打一张厨房单”,而是“把订单变化按品类、按语义、按设备正确分发出去”。
真正被保护的不是打印动作本身, 而是:
- 厨房不要漏做,
- 不要重复做,
- 不要做错备注版本,
- 不要把别的终端的旧状态当成最新事实。
这也是为什么 Odoo 的厨打与 preparation display,看起来只是设备功能,实际上却是一整套变更分发机制。
DISCUSSION
评论区