门店做 POS 现金管理时,最容易把问题想得太轻:
现金嘛,不就是开钱箱、塞零钱、拿备用金、关班数数吗?
但只要门店规模稍微大一点,这套“凭感觉”很快就会出事故。因为现金问题最难的不是算钱,而是事后说清楚这笔钱为什么进来、为什么出去、是谁操作的、能不能删。
先说结论:Odoo POS 的现金控制不是“钱箱 UI”,而是“面额模板 + cash in/out 原因 + bank statement line 记账 + 权限审计”的组合。前端的钱箱体验只是入口,真正被系统保护的是现金轨迹。
pos.bill 在解决什么问题
pos_config 里有 default_bill_ids,对应模型是 pos.bill,界面上通常表现为 Coins/Bills 配置。很多人第一次看到这个配置,会误以为:
- 配了哪些面额,会计就会按这些面额自动入账;
- 面额数量本身会成为正式账务明细;
- 不配也没关系,只是前端好看一点。
这三种理解都不准确。
pos.bill 真正解决的是:让收银员在 opening/closing control 或点钞场景里,按门店真实使用的币种面额快速输入现金构成。 它首先是交互层能力,其次才是现金控制体验的一部分。
所以它的价值在于:
- 降低点钞录入成本;
- 让门店按自己常用面额做结构化输入;
- 提高开班、关班和复核时的人机一致性。
但它本身并不等于“会计凭证按 20 元、50 元、100 元逐张记账”。
为什么 cash in/out 必须写 reason
前端 cash_move_popup.js 很直接:金额必须是有效浮点数,而且 reason.trim() !== "",否则不给提交。
也就是说,Odoo 明确不接受“只填金额,不写原因”的现金存取。
这不是 UI 找茬,而是因为后端 try_cash_in_out() 最终会调用 _prepare_account_bank_statement_line_vals(),把以下信息落进 account.bank.statement.line:
pos_session_idjournal_idamountdatepayment_refpartner_id
其中 payment_ref 还是由 session.name、操作类型和 reason 拼出来的。
这意味着 reason 不是备注那么简单,它是现金轨迹的一部分主标识。如果允许空 reason,事后看流水就会剩下一串“进了 300、出了 200”,没人知道发生了什么。
为什么现金存取会直接写 account.bank.statement.line
这个设计非常值得注意。Odoo 没有单独发明一个“POS 临时现金日志”模型来糊弄,而是把 cash in/out 直接落到 account.bank.statement.line。
这等于承认了一件很重要的事:
门店现金存取不是前台小动作,而是账务世界需要认账的现金事件。
因此 cash in/out 的后果不是“POS 页面多一条记录”,而是会进入会计语义里,后续会出现在 session 报表、现金核对、差异解释链路中。
删除 cash move 为什么要卡权限
源码里 delete_cash_in_out() 有两层保护:
- 当前用户必须属于
account.group_account_basic; - 这条
account.bank.statement.line必须确实属于当前 session。
这两个条件看似普通,实际非常关键。
因为现金存取最怕的是两种事后篡改:
- 收银员把自己做错的现金移除记录偷偷删掉;
- 删错了别的 session 的现金流水,导致跨班次对不上。
所以 Odoo 的态度很明确:删除现金轨迹不是普通前台操作,而是带会计权限的受限动作。 删除后系统还会 log_partner_message 记一笔消息,用于追踪谁删了什么。
钱箱打开不等于现金事件成立
前端 openCashbox() 和 hardwareProxy.openCashbox() 很容易让人误会:是不是开了钱箱就代表有一笔 cash in/out?
不是。
开钱箱只是硬件动作。真正形成现金轨迹,要看有没有:
- 通过 cash move popup 提交金额;
- 填了 reason;
- 后端成功创建
account.bank.statement.line。
这条边界非常重要。否则门店就会把“开了一次抽屉”和“做了一次现金操作”混成一件事。
为什么报表里会出现 Cash Opening、Cash in 1、Cash out 1
无论 pos_session.get_cash_in_out_list() 还是 report_sale_details.py,都会把现金移动整理成更适合门店阅读的名字:
Cash OpeningCash in 1Cash out 1- 或直接用
payment_ref
这说明 Odoo 不只是把现金流水存下来,还在尝试把它重组为门店能复盘的结构:
- 开班底钱是多少;
- 中途补进了几次钱;
- 中途拿走了几次钱;
- 每次是因为什么原因。
换句话说,Odoo 不是在管“钱箱开合次数”,而是在管班次现金叙事是否完整。
最容易误解的四件事
误区一:面额配置就是正式账务明细
不对。面额配置主要服务点钞和交互,不直接等于分录拆分。
误区二:cash in/out 只是 POS 内部备注
不对。它会落成 account.bank.statement.line,进入会计语义。
误区三:删除现金存取记录只是修正前台错误
不完全对。它其实是在改现金审计轨迹,所以必须控权限。
误区四:打开钱箱就算现金操作
不对。硬件动作和账务事件不是一回事。
实战排错顺序
如果门店遇到“现金流水看不懂、cash move 不见了、删不掉、关班解释不清”,建议按这个顺序查:
- 看配置里是否维护了合适的
default_bill_ids; - 看前端现金操作是否强制填写了 reason;
- 看
try_cash_in_out()是否成功创建了account.bank.statement.line; - 看
payment_ref是否包含 session、类型和原因; - 看该记录是不是当前 session 的 statement line;
- 若要删除,确认操作者是否有会计基础权限;
- 最后再去看 closing report 中的 cash moves 是否与现场操作一致。
最后的结论
Odoo POS 现金管理真正高级的地方,不在于它能打开钱箱,而在于它坚持把每一次现金移动都变成可追溯、可解释、有限制删除权限的轨迹。
现金控制不是“钱在不在抽屉里”,而是“这笔钱为什么会在这里、谁让它来的、谁又把它拿走了”。
DISCUSSION
评论区