POS 开票入账

Odoo POS 开票为什么不是“打张发票附件”:invoice 生成、应收对冲与 rounding 补线讲透

很多人以为 POS 开票只是把小票翻译成 account.move,但 Odoo 真正处理的是发票日期、退款回链、closed session 下的 payment move、应收科目对冲以及 rounding line 补线。本文结合 point_of_sale 源码讲清:为什么同一张 POS 单在不同会话状态下开票路径不同,以及遇到“发票已出但应收未清”时该按什么顺序排查。

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

很多公司在讨论 POS 开票时,脑子里的模型还是:

订单成交后,补一张发票就行。

但 Odoo 的实现远比这个复杂。POS 开票不是把收银单复制成 account.move,而是一次“订单语义 → 发票语义 → 支付语义 → 应收语义”的重组。 所以现场最容易出的问题,不是“发票有没有生成”,而是“为什么发票有了,应收却没清;为什么 closed session 下又多了几笔 move;为什么 rounding 看起来像系统自己补出来的一行”。

先说结论:Odoo POS 开票的主线,是先按订单和退款关系生成 invoice,再为支付生成 payment moves,然后把支付应收行与发票应收行做 reconcile;若会话已关闭,还会额外反转闭店后生成的支付分录。 这不是多此一举,而是在兼顾 POS 现场效率与会计边界。

开票第一步:先决定发票是谁、哪天、什么类型

pos_order.py_prepare_invoice_vals() 里,Odoo 会准备:

  • move_type:普通销售是 out_invoice,退款单则可能是 out_refund
  • invoice_date:若是单张订单且 session 未关闭,会尽量沿用 date_order
  • partner_id / partner_shipping_id:按客户的开票/收货地址取值;
  • fiscal_position_id:延续订单税制度;
  • invoice_line_ids:不是收银小票原样搬运,而是重新组发票行。

这就解释了为什么 POS 开票结果和小票“长得像但不完全一样”。因为到了发票层,系统已经开始按会计对象重组,而不再只忠于前台展示。

退款开票不是简单负数,而是可能带着原发票回链

源码里如果订单行关联了被退款的原订单行,且原单已经有 account_move,系统会把 pos_refunded_invoice_ids 带进新发票值里;单笔退款场景还可能设置 reversed_entry_id,并把 ref 写成 Reversal of: xxx

这意味着:POS 退款发票不是“再开一张负数发票就完了”,而是尽量与原发票建立可追溯关系。

所以如果你在财务侧看到 POS 退款票据结构比预期更“像正式红字/冲销逻辑”,那不是 Odoo 过度设计,而是它本来就在用发票模型维护追溯链。

支付 move 为什么还要单独生成

_generate_pos_order_invoice() 里,在发票过账后,系统会遍历订单支付,调用 _create_payment_moves()。这一步很多人容易疑惑:

不是已经有发票了吗,为什么还要 payment move?

因为发票代表应收主张,支付代表应收被谁、以什么方式冲掉。 这两件事在会计上不能混成一坨。

POS 收银看起来是一个动作,账务上却至少分成两层:

  1. 销售/税/应收的发票层;
  2. 现金、银行卡、延迟支付等支付层。

reconcile 才是“这张票真的结掉了”的关键动作

源码里 _reconcile_invoice_payments() 会先找客户真正的应收科目,再取 payment moves 里可用于对冲的 receivable lines,与发票未核销的 receivable lines 一起 reconcile()

这一步非常关键。很多人看到发票和支付分录都生成了,就以为流程闭环了;其实如果没成功 reconcile,财务仍会看到挂着的应收。

也就是说:

  • 发票生成成功 ≠ 应收已清;
  • 支付 move 生成成功 ≠ 已对冲;
  • 真正闭环的是应收行被核销。

为什么 closed session 场景还会出现 reversal

源码对 closed session 有一段额外处理:如果是已关闭会话里的订单在之后才触发开票,系统会记录这些 closed session 生成的 payment moves,并在 reconcile 完成后为其创建反转分录。

这背后的业务含义是:会话关闭后再补开票,不能粗暴改写原闭店会计边界。 所以 Odoo 宁可多走一步 payment move + reversal,也要把“补票”动作纳入可审计的账务路径。

这和很多人脑海里的“补开票只是多打一张纸”完全不是一回事。

rounding line 为什么会突然冒出来

_create_invoice() 里,如果支付金额与发票总额存在因为 rounding 产生的差额,源码会检查 rounding strategy,并在需要时补一条 rounding line,挂到 profit/loss account。

所以你看到发票里多出一行 rounding,不一定是脏数据,很多时候恰恰说明系统在认真处理:

  • 现金取整;
  • 支付金额与发票总额的微小差异;
  • 由 rounding strategy 决定的损益归属。

最容易误判的几个点

误区一:POS 开票就是把小票打印成 PDF

不对。PDF 只是结果之一,真正核心是 account.move、payment move 和 reconcile。

误区二:发票已过账,就说明款也结了

不对。未 reconcile 的应收依然会挂着。

误区三:closed session 后补票不该多出额外分录

也不对。额外分录往往正是为了不破坏原 session 的会计边界。

建议的排错顺序

遇到“POS 开票不对劲”,建议按下面顺序查:

  1. 先看 order 状态与 session 状态:是 opened 期间开票,还是 closed 之后补票;
  2. 再看 invoice journal 与币种:门店配置是否合法;
  3. 再看发票值:类型、日期、客户、税制、退款回链是否正确;
  4. 再看 payment moves:支付方式是否都成功生成应收相关 move;
  5. 最后看 reconcile 与 rounding:是否成功核销,应收为什么残留,取整差额落到哪里。

真正难的不是“能不能开”,而是“补开后账还能不能自洽”

POS 是前台系统,开票是后台会计动作。Odoo 这套设计真正要解决的矛盾是:

  • 前台要快;
  • 财务要准;
  • 已闭店的数据边界不能被事后随便改写;
  • 退款、取整、延迟付款都要能追溯。

所以当你看到 POS 开票路径里同时出现 invoice、payment move、reconcile、reversal、rounding line,不要把它理解成“流程复杂化”。恰恰相反,这是 Odoo 在努力把“前台一秒收银”翻译成一套经得起审计的会计语义。

这才是 POS 开票最值钱、也最容易被低估的部分。

DISCUSSION

评论区

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