库存会计

Odoo 库存会计为什么一半在发货、一半在开票:stock_account、sale_stock、purchase_stock 的 COGS 与价差桥接讲透

很多人觉得 Odoo 库存会计“记账点很分裂”:收货/发货有库存分录,客户发票又冒出 COGS,供应商账单还会补价差。本文基于 `/home/ubuntu/odoo-temp/addons/stock_account/models/account_move.py`、`stock_move.py`、`sale_stock/models/account_move.py`、`purchase_stock/models/account_invoice.py` 讲清这条桥接链。

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

很多人第一次看 Odoo 的库存会计,都会产生一种很强的“割裂感”:

  • 仓库收货 / 发货时已经记了一套库存分录;
  • 客户发票过账时又多出一组 COGS 行;
  • 供应商账单过账时还可能再来一笔 price difference。

于是直觉上会问:

  • 到底哪一步才是真正成本?
  • 为什么不是发货时一次记完?
  • 为什么账单价格和库存价值不一致时,要在 bill 阶段补?

如果你把 /home/ubuntu/odoo-temp 里的几处源码串起来看,就会发现 Odoo 并不是“重复记账”,而是在桥接三种不同语义:

  1. 库存位置移动带来的资产转移
  2. 销售确认时需要体现在 P&L 上的 COGS
  3. 采购账单价格与标准/已估值成本不一致时的价差修正

一、库存 move 先解决的是“资产在哪”,不是“利润何时确认”

/home/ubuntu/odoo-temp/addons/stock_account/models/stock_move.py 里,_action_done() 会对 valued move 做两件事:

  • _set_value() 先算 move 自己的价值;
  • _create_account_move() 再为该库存移动创建会计分录。

_get_account_move_line_vals() 的核心结构很清晰:

  • 一边是 stock_valuation
  • 另一边是来源或目的位置的 valuation account。

这说明库存移动层真正先解决的是:

货值从哪个库存语义位置转到哪个库存语义位置。

它首先关心的是 balance sheet 上的资产去向,而不是收入成本配比。


二、为什么客户发票过账时又会补一组 COGS

/home/ubuntu/odoo-temp/addons/stock_account/models/account_move.py 里,account.move._post() 会在销售单据过账前创建额外的 realtime out lines,也就是大家常看到的 COGS 行。

源码里 _stock_account_prepare_realtime_out_lines_vals() 的逻辑是:

  • 只对 sale document 生效;
  • 只对 real_time valuation 产品生效;
  • 取产品账户里的 stock_valuationexpense
  • _get_cogs_value() 算出本次销售应确认的成本;
  • 再生成两行 display_type = 'cogs'
  • 借:费用 / COGS
  • 贷:库存估值

这一步表达的不是“货又移动了一次”,而是:

前面已经发生的库存价值,现在要在销售发票语义下转成损益表上的销售成本。

所以 stock move 层和 customer invoice 层不是重复,而是各管一半:

  • stock move 负责库存资产流转;
  • invoice 负责把应确认的成本搬进利润表。

三、为什么 sale_stock 还要继续补充“最后一步 stock moves”

如果只看 stock_account,你会以为 invoice 直接知道对应哪些 stock move。

但在 /home/ubuntu/odoo-temp/addons/sale_stock/models/account_move.py 里,_stock_account_get_last_step_stock_moves() 又扩展了一层:

  • 普通销售发票取 sale line 对应、且真正到 customer 的 done moves;
  • refund 场景还要区分 reversed entry 和从 SO 直接生成的退回链。

这说明 Odoo 不是“按发票行静态算成本”,而是努力把 COGS 绑定到最后一步真正履约的库存移动

也正因为这样,销售成本确认不是简单乘一个标准成本,而是尽量沿真实 stock move 回溯。


四、为什么供应商账单过账时还要补采购价差

采购侧又是另一种桥接。

/home/ubuntu/odoo-temp/addons/purchase_stock/models/account_invoice.py 里,_stock_account_prepare_anglo_saxon_in_lines_vals() 会为 vendor bill 生成额外的 price difference 行。

适用前提包括:

  • 采购类单据;
  • Anglo-Saxon accounting 开启;
  • 标准成本产品;
  • 并且相关数量已经先交付/入库。

当账单单价与此前库存层使用的估值不一致时,源码会:

  • 借或贷 price difference account;
  • 同时反向修正当前发票行相关金额。

它解决的问题不是“库存资产移动”,而是:

采购账单这个法律/供应商价格事实,与此前库存估值之间出现了偏差,该怎么把差额单独落账。

所以它不是重复入库成本,而是“账单事实”对“库存估值假设”的结算修正。


五、为什么这三层不能粗暴合并

很多实施时想当然地希望:

  • 收货 / 发货记一套;
  • 后面发票别再动库存会计。

但 Odoo 的设计并不是这么想的,因为三层回答的是不同问题:

1)库存移动层

回答:资产位置和库存价值怎么变。

2)销售发票层

回答:本次销售该确认多少 COGS 进入损益。

3)采购账单层

回答:供应商账单价格与前期估值有差时,差额怎么单独结算。

如果你把它们合并成一步,很多边界会丢失:

  • 先发货后开票怎么办;
  • 先收货后收到账单怎么办;
  • 标准成本与账单价格差额怎么解释;
  • refund / return 怎么反推真实 stock move。

六、为什么用户会误会成“记了三遍”

因为 UI 上看到的是三次不同时间点的动作:

  • 库存 done;
  • 客户发票 post;
  • 供应商账单 post。

但会计语义上它们分别在落:

  • 库存资产移动;
  • 成本确认;
  • 采购价差。

看起来像三次,实际上是在把同一条 supply chain 的不同会计含义拆层表达。


七、排错时别只对一张凭证看借贷

如果你要解释某个产品为什么“成本不对”,建议顺着这条链排:

  1. stock move 的 value 是怎么算出来的;
  2. 对应库存 move 是否创建了 account move;
  3. 销售发票是否生成了 display_type = 'cogs' 的行;
  4. sale_stock / purchase_stock 是否把正确的 last-step move 关联上;
  5. 是否还有 vendor bill 价差补账。

只盯客户发票,通常会越看越糊。


一句话记忆

Odoo 库存会计不是“一个动作记到底”,而是把库存资产移动、销售成本确认、采购价差修正拆成三层桥接,所以看起来分散,其实语义更清楚。

DISCUSSION

评论区

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