POS 接销售单

Odoo POS 接销售单为什么不是“把报价搬到收银台”:未付余额、下游数量回写与拣货重平衡讲透

很多人以为 POS 接销售单只是把 SO 行带到前台收款,但 Odoo 真正在处理的是“已付多少、还能在 POS 收多少、收完以后发货和下游 move 要不要重算”。本文结合 pos_sale 源码,讲清 sale.order.amount_unpaid、POS 付款与 down payment 回写、以及确认后为什么要重算 waiting picking 的需求量。

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

很多人把 pos_sale 理解成一句很简单的话:

销售单可以拿到 POS 去收尾款。

方向没错,但如果你只理解到这里,后面看到这些现象就很容易懵:

  • 为什么销售单不是全额都能在 POS 里收;
  • 为什么 POS 收完以后,SO 的未开票、未交付口径会变化;
  • 为什么收银动作会反过来影响 waiting picking 的数量;
  • 为什么某些拣货单会被取消,另一些却被重新分配库存。

结论先说:Odoo POS 接销售单不是“把订单行搬到收银台”,而是一套“剩余商业责任转移”机制。 它要同时回答三件事:

  1. 这张 SO 还有多少钱能在 POS 收;
  2. POS 收掉的部分,销售与开票口径怎么回写;
  3. 原来为 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 被取消”,建议按这个顺序查:

  1. sale.order.amount_unpaid 是如何算出来的;
  2. 该 SO 是否已有发票、付款或历史 POS 订单占额;
  3. POS 行是否正确挂了 sale_order_origin_idsale_order_line_id
  4. 是否涉及 down payment product 与回写逻辑;
  5. POS 订单状态是否已从 draft 进入 paid/done;
  6. qty_delivered 是否已 flush 到最新;
  7. waiting/confirmed/assigned 的 stock move 是否被重算 product_uom_qty
  8. picking 是该取消,还是该重新 action_assign()

最后的结论

pos_sale 的真正价值,不是“让门店也能打开销售单”,而是让销售履约链在 POS 介入后仍然保持一致:

  • 金额口径不会重复收、重复开;
  • 销售行的商业责任会被正确扣减;
  • 仓库侧等待中的需求会被重新平衡。

所以更准确的理解应该是:

POS 接销售单,不是把 SO 搬过去收钱,而是把销售链里尚未完成的那一段责任安全接手。

DISCUSSION

评论区

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