POS 毛利成本

Odoo POS 毛利为什么有时先是 0、关班后才对:成本计算时机与 session closing 边界讲透

很多人看到 POS 订单毛利一开始是 0,或者和最终报表不一致,就以为 Odoo 算错了。其实源码里明确区分了实时成本计算和 session closing 后补算:当商品是 storable 且成本法是 FIFO/AVCO,而库存更新又延后到关班时,系统会先让毛利保持未完成状态。本文讲清 `_compute_total_cost_in_real_time()`、`_compute_total_cost_at_session_closing()` 和 `is_total_cost_computed` 背后的真实边界。

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

很多实施顾问第一次被门店追问 POS 毛利时,都会被问到一个非常尖锐的问题:

为什么这张 POS 单刚出来时毛利是 0,过一阵子或者关班后又变了?是不是 Odoo 算错了?

如果你只懂“售价减成本”的口号,这题几乎一定会答错。

先说结论:Odoo POS 的毛利不是永远实时可得。对可库存商品,尤其成本法为 FIFO/AVCO、且 session 配置为关班才更新库存时,系统会明确承认“现在还不能可靠算成本”,于是先不把毛利算实,等 session closing 再补。

这不是 bug,而是比“假装实时精确”更诚实的设计。

_compute_total_cost_in_real_time() 为什么会故意跳过一部分行

源码写得很直白:

  • 订单进服务器处理时,会尝试实时计算 total cost;
  • 但如果 order._should_create_picking_real_time() 不成立;
  • 那么 storable 且 FIFO/AVCO 的行会先被剔出去;
  • 剩下能算的行先算。

这背后的业务逻辑是:

  • 对服务类或可直接确定成本的商品,实时算问题不大;
  • 对依赖库存移动结果才能确定实际成本的商品,如果库存移动本身还没落稳,强行给出毛利只会制造伪精确。

所以 Odoo 不是“算不出来”,而是拒绝在证据不足时装作算出来。

is_total_cost_computed 在保护什么

订单和订单行里都有 is_total_cost_computed 相关标记。

订单层 _compute_margin() 也写得非常明确:

  • 只有 is_total_cost_computed 为真,才汇总 marginmargin_percent
  • 否则毛利和毛利率先给 0。

很多人看到 0 就慌,以为系统把利润吞了。其实 0 在这里不是“真的没利润”,而是“系统尚未确认所有行的真实成本”。

换句话说,0 是一个保守信号,不是最终结论。

为什么 FIFO/AVCO 尤其容易触发“先 0 后正常”

_compute_total_cost() 里,storable 商品如果成本法是 FIFO/average,会优先看 stock moves 的 _get_price_unit();如果拿不到足够信息,某些场景还会回退到其他逻辑。

这说明 POS 毛利并不是只看 standard_price 就完事,尤其在 FIFO/AVCO 下,真实成本往往要等库存流转落地后才更可信。

因此当 session 设置为 update_stock_at_closing 时,门店会看到:

  • 销售已经发生;
  • 收款已经完成;
  • 但成本与毛利还没有完全确定。

这并不矛盾,因为销售确认和成本最终确定不是同一时间点。

为什么 session closing 要补算成本

pos_session.py 在 closing 过程中,会对尚未 is_total_cost_computed 的 closed orders 调 _compute_total_cost_at_session_closing(self.picking_ids.move_ids)

这一步很关键,因为它代表系统在说:

现在库存移动、班次边界、订单集合都更稳定了,可以把之前暂缓的那部分成本补算回来。

所以你看到“关班后毛利变准了”,不是因为 closing 做了魔法,而是因为 closing 给了系统一个更完整的成本计算上下文。

退款单为什么也会影响成本理解

订单层 _compute_margin() 会根据 order.is_refund 调整 sign,订单行层也会在 margin 计算中考虑正负号。

这意味着退款不是简单“负销售额”,还会牵涉:

  • 退货对应哪条原始成本;
  • 是否能沿 refunded_orderline_id 找回原线的 total cost;
  • 当前库存移动是否已经完整。

所以毛利问题在退款场景里更容易被误判。门店看到负数,不一定是系统错;可能只是它正在用正确的符号和补算时机反映一笔回退业务。

为什么产品信息里能看成本/毛利,不代表订单毛利已经最终确定

前端 pos_store.js 会在产品信息弹窗里给出成本、毛利、订单总成本、总毛利等信息,还会受 is_margins_costs_accessible_to_every_user 控制权限。

但这只说明 Odoo 愿意展示当前可用的估计或已知数据,不代表每一张订单的最终成本都已经在服务器闭环确认。

这两个层次别混:

  • 前端产品信息:给收银员或经理做即时判断;
  • 订单最终毛利:依赖后端成本计算是否完成。

最容易误解的四件事

误区一:毛利显示 0 就是系统算错

不对。很多时候那是在诚实地表示“成本还没完全确认”。

误区二:只要知道售价和标准成本,就该立刻算出毛利

不对。FIFO/AVCO 场景下真实成本可能依赖库存移动落地。

误区三:关班后毛利变化说明数据不稳定

也不对。更准确地说,是关班提供了补算剩余成本的时机。

误区四:前端能看到毛利字段,就说明订单毛利已最终确定

不对。即时展示和最终确认不是一个层次。

实战排错顺序

如果门店说“POS 毛利不对、先是 0、关班后变了”,建议这样查:

  1. 先看商品是不是 storable;
  2. 看成本法是不是 FIFO 或 AVCO;
  3. 看 session 是否启用了 update_stock_at_closing
  4. 看订单/订单行的 is_total_cost_computed
  5. 看实时阶段 _compute_total_cost_in_real_time() 是否跳过了部分行;
  6. 看 closing 阶段是否执行了 _compute_total_cost_at_session_closing()
  7. 若有退款,确认是否通过 refunded_orderline_id 正确回链原成本。

最后的结论

Odoo POS 在毛利这件事上最成熟的一点,不是“无论何时都给你一个数字”,而是它愿意承认:有些成本要等库存和 session 边界稳定后,才能算得可信。

比起即时给一个可能错的毛利,Odoo 更愿意暂时给你 0,然后在该补算的时候把它算对。

DISCUSSION

评论区

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