# 库存成本结算可行性分析

## 背景

纺织/印花行业 ERP 中，库存成本核算通常使用两种方法：

- **先进先出（FIFO）**：先入库的批次先出库，出库成本按对应入库批次的实际采购价计算。
- **加权平均**：每次入库后重新计算库存均价（总成本 ÷ 总数量），出库统一按当前均价计算。

本文档对当前系统的库存模块进行研判，评估是否具备实现这两种成本结算方法的要素。

---

## 1. 当前库存模块数据结构

### 1.1 核心模型

```
StockChangeRecord（库存变动记录）
├── type:          入库/出库
├── source_type:   来源类型（采购/销售/调拨/盘盈盘亏/红冲等）
├── warehouse:     仓库
├── is_finished:   是否已完成
└── details ────── StockChangeDetail（库存变动明细）
                    ├── product:     产品
                    ├── quantity:    数量
                    ├── consume_with: 消耗关联（出库指向入库明细，自引用 FK）
                    └── is_consumed: 是否已被消耗

Inventory（库存汇总）
├── product:       产品
├── warehouse:     仓库
├── quantity:      当前库存数量
└── num_of_rolls:  匹数

StockSnapshot（库存变动快照）
├── delta:          变动量
├── quantity_before: 变动前库存
├── quantity_after:  变动后库存
└── (offset/cancelled 红冲链路)
```

### 1.2 价格数据所在位置

价格数据**不在库存模块**，而在业务模块的明细行中：

| 模型 | 价格字段 | 说明 |
|------|---------|------|
| `business.PurchaseOrderItem.price` | 采购单价 | 入库成本价的唯一来源 |
| `business.SalesOrderItem.price` | 销售单价 | 出库售价，非成本价 |

### 1.3 关键发现：库存模块不做任何成本核算

所有 `StockChangeDetail`、`Inventory`、`StockSnapshot` 只记录**数量（quantity）**，完全没有 `cost_price`、`unit_cost`、`total_cost` 等成本维度字段。

---

## 2. 按结算方法逐一分析

### 2.1 先进先出（FIFO）

#### 原理

每次出库时，从最早未消耗的入库批次开始扣除，出库成本等于对应入库批次的实际采购单价。

#### 已有基础设施 ✅

`StockChangeDetail.consume_with` 是一个自引用外键，在严进严出模式下，出库明细通过它指向被消耗的入库明细：

```python
# stock/models.py
consume_with = models.OneToOneField(
    'self',
    on_delete=models.PROTECT,
    null=True, blank=True,
    related_name='consumed_by_detail',
    verbose_name='所消耗的入库明细',
)
```

这是**天然的 FIFO 追踪链**。如果入库明细携带了成本价，出库成本可以直接通过 `consume_with.unit_cost` 确定。

#### 缺失要素

| 缺失项 | 说明 |
|--------|------|
| `StockChangeDetail.unit_cost` | 入库明细需要记录该批次的采购成本单价 |
| 宽进宽出模式的批次追踪 | 当前 `consume_with` 仅在严进严出模式下使用，宽进宽出需要补充批次追踪或按 FIFO 规则自动匹配 |

#### 落地难度：低

改动范围极小——给 `StockChangeDetail` 加一个 `unit_cost` 字段，在入库时从采购单同步价格，出库成本沿 `consume_with` 链读取即可。

---

### 2.2 加权平均

#### 原理

每次入库后，重新计算加权平均单价：
```
加权均价 = (库存总成本 + 本次入库成本) ÷ (库存数量 + 本次入库数量)
```
出库时统一按当前加权均价计算成本。

#### 已有基础设施 ⚠️

`Inventory` 表是天然的加权平均计算锚点——它汇总了每个产品在每个仓库的当前库存数量。只需增加一个总成本字段即可完成计算。

#### 缺失要素

| 缺失项 | 说明 |
|--------|------|
| `Inventory.total_cost` | 库存表需要增加总成本字段，每次入库累加 |
| `StockChangeDetail.unit_cost` | 同上，入库明细需要知道入库单价 |
| 红冲/退货的成本回冲逻辑 | 红冲或退货时需反向调整 `total_cost` 和重新计算均价 |

#### 落地难度：低

给 `Inventory` 加一个 `total_cost` 字段，在 `make_stock_change_completed()` 中补充成本累加逻辑即可。

---

## 3. 两种方法对比

| 维度 | 先进先出 (FIFO) | 加权平均 |
|------|:---:|:---:|
| 追踪粒度 | 批次级别 | 仓库+产品级别 |
| 已有基础设施 | `consume_with` 链 ✅ | `Inventory` 汇总表 ⚠️ |
| 需新增字段 | `StockChangeDetail.unit_cost` | `StockChangeDetail.unit_cost` + `Inventory.total_cost` |
| 计算复杂度 | 需维护批次消耗顺序 | 每次入库后重新算均价 |
| 红冲处理 | 恢复原批次 | 重新计算均价 |
| 适用场景 | 价格波动大、需精确追踪每批成本 | 价格稳定、简化核算 |

---

## 4. 实施建议

### 最小改动方案

两个方法都需要以下共同改动：

1. **`StockChangeDetail` 增加 `unit_cost` 字段**
   ```python
   unit_cost = models.DecimalField(max_digits=15, decimal_places=6, null=True, verbose_name='成本单价')
   ```

2. **入库时填充 `unit_cost`**
   - 采购入库：从 `PurchaseOrderItem.price` 同步
   - 销退入库：从原销售出库的成本回冲
   - 调拨入库：从调出仓的当前成本同步
   - 盘盈：可设为 0 或要求手动录入

3. **出库时计算成本**
   - FIFO：沿 `consume_with` 链读取对应入库批次的 `unit_cost`
   - 加权平均：需额外在 `Inventory` 表增加 `total_cost` 字段，在 `make_stock_change_completed` 中维护

### 建议优先实现 FIFO

对于纺织行业（布料批次间价格差异大），**FIFO 更合适**。且当前 `consume_with` 链已就绪，实现成本最低。

若后续需要加权平均，在 FIFO 的基础上给 `Inventory` 增加 `total_cost` 即可，两者不冲突。

---

## 5. 结论

**当前系统不具备直接进行先进先出或加权平均成本结算的能力**，库存模块完全是数量管理。

但 **FIFO 所需的基础设施已经存在**（`consume_with` 追踪链），改动范围极小——仅需给 `StockChangeDetail` 增加 `unit_cost` 字段，并在入库时同步采购价格。加权平均也只需在 `Inventory` 表增加 `total_cost` 字段即可。

两个方法的落地成本都很低，不存在结构性障碍。