POS 销售明细报表

Odoo POS 销售明细报表为什么和你想的“收银流水”不一样:现金流动、品类汇总与税额摘要讲透

门店常把 POS 销售明细报表当作“把每笔订单列出来”的清单,但 Odoo 真正生成的是一份带会计语义的会话摘要:按品类汇总商品、拆出退款区、拼上 cash opening 与 cash in/out,并计算 money counted、final count、money difference。本文结合 report_sale_details.py 源码,讲清为什么这张报表和订单列表对不上时不一定是 bug。

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

很多门店第一次看 Odoo POS 的 Sales Details,会下意识期待它长这样:

  • 一笔订单一行;
  • 一行里显示商品、支付方式、税;
  • 最后简单加总。

但真正打开后,很多人会困惑:

  • 为什么商品是按品类 grouped 的;
  • 为什么退款被单独拆区;
  • 为什么还夹着 Cash Opening、Cash in/out;
  • 为什么某些数字和订单列表、会计分录看起来不完全一一对应。

先说结论:Odoo POS 销售明细报表不是“订单流水导出”,而是一份面向门店复盘与会话结算的摘要报表。它把商品、退款、现金动作和计税结果压成可读结构,所以和订单原子记录不完全同形是正常设计。

report_sale_details.py 先做的不是列订单,而是重组语义

从源码看,这份报表的核心工作不是把 pos.order 直接 dump 出来,而是构建几块不同的数据:

  • products_sold
  • refund_done
  • payments
  • taxes
  • 以及现金相关的 opening / cash moves / counted difference

这说明 Odoo 的目标不是“还原数据库行”,而是回答门店真正关心的问题:

  • 今天卖了什么;
  • 哪些是退款;
  • 现金有没有中途增减;
  • 理论现金和实际现金差多少;
  • 税额汇总长什么样。

为什么商品按品类而不是按订单展示

报表里会先按 category 组装 product list,再通过 _get_total_and_qty_per_category() 计算每个品类的:

  • total
  • qty
  • 以及聚合后的 unique products 信息

这背后的取舍很明确:门店日结时更关心“饮料卖了多少、主食卖了多少、哪个品类退得多”,而不是逐单重演。

所以 Sales Details 更像一张经营摘要表,不是票据明细表。

如果你想核单据,应该去订单列表;如果你想复盘班次经营结构,Sales Details 才是正确入口。

为什么退款要单独拆成 refund_products

源码里 refund_done 和正常销售是分开的,最后生成 refund_products 区域。

这样做有两个好处:

  1. 不会把负数商品直接混到正常销售列表里,导致阅读混乱;
  2. 门店可以单独看“今天退了什么”,而不是只看到净额被冲掉。

这也解释了一个常见误会:

为什么订单列表看着卖了很多,但 Sales Details 商品总量没那么高?

因为退款在这张报表里被有意拆开了,系统希望你看到“卖了多少”和“退了多少”是两件事。

现金部分为什么要带 final_countmoney_countedmoney_difference

在现金支付方法存在时,报表会计算:

  • final_count:理论应有现金;
  • money_counted:实际数出来的现金;
  • money_difference:两者差异;
  • cash_moves:开班底钱和中途 cash in/out。

这说明 Sales Details 不是单纯的销售表,它同时承担了班次现金解释表的角色。

尤其 cash_moves 里还会插入:

  • Cash Opening
  • 每笔 cash in
  • 每笔 cash out
  • 必要时差异相关移动

所以门店看到报表里夹着现金移动,不该觉得“杂”,而该理解为:Odoo 在把销售与现金控制放进同一次 session 叙事里。

为什么有时看起来像“多出一笔现金差异”

report_sale_details.py 里,如果 session 存在现金差异,系统会把某些用于 closing counting 的差异项以特殊 move name 表达出来,例如 Profit 或 Loss。

这不是凭空多出来一笔交易,而是系统在告诉你:

  • 理论现金与实盘现金没对上;
  • 这个差异需要在 session 关闭时被解释;
  • 报表必须把它显示出来,否则门店无法复盘为什么 counted 和 expected 对不上。

为什么这张报表和会计分录不一定逐行同构

很多人想拿 Sales Details 去逐行对会计分录,结果越对越乱。原因是两者职责不同:

  • Sales Details 偏门店复盘;
  • 会计分录偏入账表达。

报表里商品会按品类重组,现金会按门店能读懂的方式命名,而会计分录则按 journal、account、tax line、reconciliation 规则表达。

所以正确做法不是期待“每一行完全一致”,而是看它们是否在总量与业务意义上相互印证。

最容易误解的三件事

误区一:Sales Details 就是订单列表的另一种皮肤

不对。它是摘要报表,不是逐单清单。

误区二:退款没和销售混算就是报表不完整

不对。拆开退款是为了让经营复盘更清楚。

误区三:看到现金差异行就说明系统多记账了

不对。很多时候那是系统在显式呈现 counted 与 expected 的差额。

实战排错顺序

如果门店说“Sales Details 跟订单列表对不上”,建议按这个顺序查:

  1. 先确认你在比的是订单明细还是 session 摘要;
  2. 看退款是否被拆到 refund_products
  3. 看商品是否被按 category 聚合;
  4. 看现金支付部分是否加入了 Cash Opening 和 cash moves;
  5. money_countedfinal_countmoney_difference 是否解释了差异;
  6. 再去和税汇总、会计结果比总额,不要先强求逐行同构。

最后的结论

Odoo POS 的 Sales Details 报表真正聪明的地方,是它不执着于“数据库原样输出”,而是试图把一个班次里最重要的经营事实压缩成一张人能看懂的总结表。

它不是为了逐单回放,而是为了让门店一眼看懂:卖了什么、退了什么、现金怎么动了、差异从哪来。

DISCUSSION

评论区

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