很多实施项目一碰到 POS 收款,就会下意识地把它理解成一句话:
- 顾客付了钱;
- 订单状态改成 paid;
- 后面会计自己处理。
这在 Odoo 里远远不够。
从 /home/ubuntu/odoo-temp/addons/point_of_sale/models/pos_payment.py、pos_order.py 与 pos_payment_method.py 可以看出,Odoo 真正在意的是:
- 这条 payment 能不能出现在当前 POS session;
- 零头找回是不是一条单独的
change语义; - 发票场景里 payment move 究竟记到客户应收还是 POS 默认应收;
- 后续和发票 receivable line 对冲时,到底该捞哪些 line 去 reconcile。
所以更准确的理解是:POS 收款不是“把钱收了”,而是把订单支付语义翻译成一组可对账的会计对象。
结论先说
想把 Odoo POS 支付看明白,先抓住四个点:
- payment 方法不是前端想用就能用,后端会校验它是否属于当前 config;
- 找零不是负收入,而是
is_change的独立支付语义; - payment move 的借贷方向和账户选择,会随 split transaction / reverse 场景改变;
- 真正能跟发票对账的,不是随便一条 payment line,而是被筛出来的 receivable lines。
为什么 payment method 要被后端再验一次
pos.payment 上有 _check_payment_method_id(),会验证这条 payment 的方式是否真的属于当前 session 的 config_id.payment_method_ids。
这一步不能省。
因为 POS 前端可能存在:
- 本地缓存旧了;
- 某支付方式后台刚被移除;
- 不同门店配置不同;
- 开发定制时前端误塞了不该出现的方法。
如果不做这层校验,就会出现很典型的脏账:
- 前台显示收款成功;
- 后台 session 配置根本不承认这条 payment method;
- 关店、报表、会计分录全都开始歪。
所以这里 Odoo 的逻辑非常清楚:
支付方式合法性,最终以后端配置边界为准。
为什么 posted order 的 payment 不能改
_check_amount() 里还有一道很硬的限制:如果订单已 done 或已有关联 account_move,payment 不允许再编辑。
这个限制的本质,不是为了“防止用户手滑”,而是为了守住一个更深的边界:
- 订单已经成为正式会计/业务事实;
- payment 再改,就不只是改界面数字,而是在动已进入对账链的对象。
也就是说,Odoo 在这里保护的是:
支付一旦进入已过账或已发票化语境,就不再是门店前台的自由编辑数据。
找零为什么要单独成 is_change
在 pos_order.py 的 _process_payment_lines() 里,如果不是 draft 且 amount_return 不为零,后端会找一条现金 payment method,自动创建一个:
amount = amount_returnis_change = True
的 payment line。
这一步特别关键,因为它明确把“多收后找回”跟“真正营业收款”分开了。
否则你会很容易误读:
- 顾客掏了 100;
- 订单 92;
- 收银台找回 8;
- 如果没有 change line,看起来像门店收了 100。
而有了 is_change,系统就能在支付 move 层把现金主支付和找零动作一起解释成净额结果,而不是把找零误当收入。
_create_payment_moves() 真正在做什么
很多人以为 payment move 就是“一条付款分录”。其实源码里细得多。
_create_payment_moves() 会遍历非 change payment,并按支付方式和场景决定:
pay_later或金额为零的,直接跳过;- 找到顾客的 accounting partner;
- 建 payment move;
- 生成一条 credit receivable line;
- 再按是否 reverse、是否 split transactions,决定 debit 端挂到哪里;
- 最后 post move。
这里最值得看的是 receivable 账户的选择逻辑。
普通场景
默认会用公司级 account_default_pos_receivable_account_id 去承接 POS 侧应收。
这体现的是 POS 常见做法:
- 前台先把收款汇总进 POS 应收/中间账户;
- 后面再和发票或结算链做桥接。
reverse / refund 场景
如果是 reverse,逻辑会重新考虑 receivable account 应该落在客户应收还是 payment method 指定应收上。
这说明退款/反向支付在会计上不能简单“金额乘负号”,而是要重新判断谁在承担这笔应收回转。
split transactions 场景
如果支付方式启用了 split_transactions,某些分录会把 partner_id 留在 receivable line 上,使后续对账更精细。
这意味着 split transaction 不是“技术上拆几条线”,而是:
系统允许同一支付方式把资金流和客户应收归因表达得更细。
为什么对账时不是所有 payment move line 都能拿来冲发票
_get_receivable_lines_for_invoice_reconciliation() 这段也很关键。
它不会粗暴地把 payment move 里的所有 line 都拉去核销,而是先按:
- 这条 payment 有没有
account_move_id; - line 是否属于目标 receivable account;
- line 是否已 reconcile;
- payment 金额正负与 line balance 正负是否匹配;
再筛出真正可用于和发票 receivable 对冲的 line。
这个筛选逻辑很重要,因为在 POS + 发票场景里,经常会出现:
- POS payment receivable 与客户正式应收账户重合;
- 同一 move 里有多条方向不同的 receivable line;
- 正向付款和退款付款对账方向相反。
如果你不做这层有方向感的筛选,核销很容易:
- 冲到错误的 line;
- 把本来该留着的回冲掉;
- 让发票看似结清,实际链路不对。
最容易误解的四件事
误解一:订单 paid 就等于会计闭环了
不对。
paid 只是订单状态的一层;后面还涉及 payment move、invoice move、receivable reconciliation。
误解二:找零就是负收款,没必要单独记
不对。
找零有自己独立的 is_change 语义,否则现金净额会被看错。
误解三:所有支付都应该直接记客户应收
不一定。
POS 默认常会先走 POS 默认应收/中间账户,再与后续单据桥接。
误解四:核销时把 payment move 的 receivable line 全部拿去冲就行
风险很高。
正负方向、账户一致性、是否已 reconcile 都必须先筛。
实战排错顺序
如果你遇到“POS 已收款但发票没结清、找零金额怪、退款支付分录不对、某支付方式突然不能用”,建议按这个顺序查:
- 当前 payment method 是否仍属于 session 的 config;
- 订单是否已进入
done或已有account_move,从而禁止改 payment; amount_return是否生成了is_change=True的 payment line;_create_payment_moves()是否跳过了不该记账的零金额 / pay later;- 普通、reverse、split transaction 三种场景下 receivable account 分别落到了哪里;
- payment move 中哪几条 line 真正属于目标 receivable;
- 这些 line 是否已被 reconcile 或方向不匹配;
- 发票 receivable 与 POS payment receivable 是应桥接、应替代,还是账户配置本身就错了。
最后的结论
Odoo POS 支付机制真正解决的,不是“收没收钱”这么简单,而是:
- 这笔付款在当前门店配置下是否合法;
- 多收少找如何与真实收入分开;
- 支付 move 如何把 POS 收款翻译成可过账对象;
- 后续如何和发票应收做方向正确、账户正确的对冲。
所以最值得记住的一句话是:
在 Odoo POS 里,收款不是订单状态上的一个勾,而是一座把前台支付动作桥接到会计应收与核销链的结构化桥梁。
DISCUSSION
评论区