很多人把 pos_sale 理解成一句很简单的话:
销售单可以拿到 POS 去收尾款。
方向没错,但如果你只理解到这里,后面看到这些现象就很容易懵:
- 为什么销售单不是全额都能在 POS 里收;
- 为什么 POS 收完以后,SO 的未开票、未交付口径会变化;
- 为什么收银动作会反过来影响 waiting picking 的数量;
- 为什么某些拣货单会被取消,另一些却被重新分配库存。
结论先说:Odoo POS 接销售单不是“把订单行搬到收银台”,而是一套“剩余商业责任转移”机制。 它要同时回答三件事:
- 这张 SO 还有多少钱能在 POS 收;
- POS 收掉的部分,销售与开票口径怎么回写;
- 原来为 SO 准备的发货链路,要不要因为 POS 直接成交而重平衡。
为什么要单独算 amount_unpaid
在 pos_sale/models/sale_order.py 里,sale.order 增加了 amount_unpaid,计算时会扣掉:
- 已有发票金额;
- 已有 POS 订单金额;
- SO 自己已记录的付款金额。
这说明 Odoo 并不把“SO 总金额”直接当成“POS 可收金额”。 它真正关心的是:
为了避免重复收款和重复开票,这张销售单剩余多少商业责任还没有被别的链路吃掉。
所以如果顾问现场说“为什么明明 SO 是 1000,POS 里只能收 300”,先别怀疑系统算错,先看另外 700 是否已经通过发票、付款或别的 POS 订单占掉了。
POS 收款为什么会回写销售口径,而不是只影响 POS 自己
SaleOrder._compute_amount_to_invoice() 和 _compute_amount_invoiced() 都扩展了 POS 订单行影响。
这意味着 Odoo 明确认定:
- 通过 POS 收掉的部分,不能假装没发生;
- 它会影响销售单剩余可开票、已开票、已付款等商业口径。
尤其是 down payment 场景,_prepare_down_payment_line_values_from_base_line() 还会把 POS 订单行挂回 SO 的 down payment line。
这背后的真实理念是:
POS 不是销售链路外的一条野路子,而是销售履约的一种收尾入口。
既然是入口,后面报表和业务口径就必须跟着改。
为什么 POS 确认后还要反过来改 stock move 的需求量
这是 pos_sale 最容易被忽略、也最有深度的一段。
在 pos_order.sync_from_ui() 之后,模块会:
- 找到 POS 行关联的
sale_order_line_id; - 刷新
qty_delivered; - 遍历 SO 行相关的 stock move;
- 根据已交付量和“预计稍后由 POS 交付的量”重算
product_uom_qty; - 对 waiting / confirmed / assigned 的 picking 重新处理;
- 如果某张 picking 全部需求量变成 0,则直接取消;
- 否则重新
action_assign()。
这说明 Odoo 在努力修正一个现实问题:
原来销售单下游已经准备好的拣货需求,不一定还和“顾客现在在 POS 端买走的量”一致。
如果不重平衡,你会出现非常典型的业务错乱:
- 前台已经卖掉了,仓库还按旧 SO 继续拣;
- 部分需求已经被 POS 吃掉,但 waiting picking 仍保留原需求;
- 后面既可能重复出货,也可能产生奇怪的保留库存。
为什么有些 picking 会被取消,有些只是重分配库存
源码的判断逻辑其实很业务化:
- 如果 waiting picking 里所有 move 的
product_uom_qty都变成 0,就取消; - 否则说明需求还在,只是数量变了,于是重新分配库存。
也就是说,系统不是一刀切“POS 一介入就把 SO 下游全删掉”,而是按剩余需求精细调整。
这恰好解释了很多现场困惑:
- 为什么收银后某张交货单消失了;
- 为什么另一张没消失,但数量自己变了;
- 为什么仓库说“刚才明明留货了,现在系统又重算了”。
这不是随机波动,而是 pos_sale 在做需求重平衡。
为什么 POS 并不总是立即等于 fully delivered
PosOrderLine._compute_qty_delivered() 里还有一个关键边界:
- 如果存在完成的 outgoing picking,且订单有
shipping_date,按真实出库 move 计交付; - 如果存在完成的 outgoing picking 但不是延后发货模式,则可视为已全交付;
- 否则交付量可能还是 0。
所以 POS 收掉销售单,并不必然等于库存层已经完全履约。它仍要看发货模式和实际 picking 完成情况。
最容易误解的四件事
误区一:SO 接到 POS,就是全额复制到前台
不对。
前台看到的是经过 amount_unpaid 过滤后的剩余责任,不是 SO 总额原样照搬。
误区二:POS 收款只影响门店,不影响销售单
不对。 它会回写销售单的 amount to invoice、amount invoiced、down payment 等口径。
误区三:POS 与仓库链路互不干扰
不对。 POS 收掉的数量会反过来重算 waiting picking 和关联 stock move。
误区四:拣货单被取消说明流程出错
不一定。 如果剩余需求确实变成 0,取消反而是正确行为。
实战排错顺序
如果你遇到“SO 在 POS 可收金额不对 / 收完后销售口径怪 / 拣货单数量变化 / picking 被取消”,建议按这个顺序查:
sale.order.amount_unpaid是如何算出来的;- 该 SO 是否已有发票、付款或历史 POS 订单占额;
- POS 行是否正确挂了
sale_order_origin_id与sale_order_line_id; - 是否涉及 down payment product 与回写逻辑;
- POS 订单状态是否已从 draft 进入 paid/done;
qty_delivered是否已 flush 到最新;- waiting/confirmed/assigned 的 stock move 是否被重算
product_uom_qty; - picking 是该取消,还是该重新
action_assign()。
最后的结论
pos_sale 的真正价值,不是“让门店也能打开销售单”,而是让销售履约链在 POS 介入后仍然保持一致:
- 金额口径不会重复收、重复开;
- 销售行的商业责任会被正确扣减;
- 仓库侧等待中的需求会被重新平衡。
所以更准确的理解应该是:
POS 接销售单,不是把 SO 搬过去收钱,而是把销售链里尚未完成的那一段责任安全接手。
DISCUSSION
评论区