很多实施顾问第一次被门店追问 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为真,才汇总margin和margin_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、关班后变了”,建议这样查:
- 先看商品是不是 storable;
- 看成本法是不是 FIFO 或 AVCO;
- 看 session 是否启用了
update_stock_at_closing; - 看订单/订单行的
is_total_cost_computed; - 看实时阶段
_compute_total_cost_in_real_time()是否跳过了部分行; - 看 closing 阶段是否执行了
_compute_total_cost_at_session_closing(); - 若有退款,确认是否通过
refunded_orderline_id正确回链原成本。
最后的结论
Odoo POS 在毛利这件事上最成熟的一点,不是“无论何时都给你一个数字”,而是它愿意承认:有些成本要等库存和 session 边界稳定后,才能算得可信。
比起即时给一个可能错的毛利,Odoo 更愿意暂时给你 0,然后在该补算的时候把它算对。
DISCUSSION
评论区