# Stock Management API Documentation

This document provides detailed documentation for the stock management module APIs, focusing on the three main stock change creation modes: Full, Relaxed, and Restricted.

## Table of Contents

1. [Overview](#overview)
2. [Stock Models](#stock-models)
3. [Stock Change Modes](#stock-change-modes)
4. [API Endpoints](#api-endpoints)
5. [Examples](#examples)
6. [Error Handling](#error-handling)

## Overview

The stock management system handles all inventory movement operations including stock-in (入库) and stock-out (出库) transactions. The system supports three different modes of operation:

1. **Full Mode**: Standard operation with explicit quantity lists
2. **Relaxed Mode**: Automatically splits quantities based on total and unit count
3. **Restricted Mode**: Strict "restricted-in/out" mode for warehouse operations

## Stock Models

### StockChangeRecord
Represents a stock change record (either stock-in or stock-out).

**Fields**:
- `id`: Record ID
- `type`: Change type (1=Stock-in, 2=Stock-out)
- `warehouse`: Associated warehouse
- `source_type`: Source of the change (1=Purchase, 6=Sales, etc.)
- `source_id`: ID of the source document (optional)
- `created_by`: User who created the record
- `is_finished`: Whether the stock change is completed
- `finished_at`: When the record was completed
- `remarks`: Additional notes

### StockChangeDetail
Represents detailed items within a stock change record.

**Fields**:
- `id`: Detail ID
- `product`: Associated product
- `stock_change_record`: Parent stock change record
- `quantity`: Quantity of the product
- `unit`: Unit of measurement
- `is_consumed`: Whether this detail has been consumed (for restricted mode)
- `consume_with`: Reference to the inbound detail being consumed (for restricted mode)

### Inventory
Represents current inventory levels for a product in a warehouse.

**Fields**:
- `id`: Inventory ID
- `product`: Associated product
- `warehouse`: Warehouse location
- `quantity`: Current quantity
- `num_of_rolls`: Number of rolls
- `spec`: Product specification
- `description`: Additional notes

## Stock Change Modes

### Source Types

The system supports various source types for stock changes:

**Incoming (入库) Sources**:
- `1`: Purchase (采购)
- `2`: Sales Return (销退)
- `3`: Transport In (调入)
- `4`: Recheck Addition (盘盈)
- `5`: Combine (合并)
- `11`: Offset (红冲，反向抵销入库)

**Outgoing (出库) Sources**:
- `6`: Sales (销售)
- `7`: Purchase Return (采购退货)
- `8`: Transport Out (调出)
- `9`: Recheck Removal (盘亏)
- `10`: Explode (拆卷)
- `11`: Offset (红冲，反向抵销出库)

### Warehouse Modes

Warehouses can operate in different modes:
- `1`: Restricted In (严进) - Strict control on stock-in
- `2`: Restricted In/Out (严进严出) - Strict control on both stock-in and stock-out
- `3`: Unrestricted (宽进宽出) - Relaxed controls for both directions

## API Endpoints

### 1. Full Mode Stock Change

**URL**: `POST /api/v1/stock-change/`

**Description**: Creates a stock change record with explicit quantity lists for each product.

**Request Parameters**:
```json
{
  "type": 1,  // 1=入库, 2=出库
  "warehouse": 1,  // 仓库ID
  "source_type": 1,  // 来源类型，见上文说明
  "source_id": 123,  // 可选，来源单据ID
  "products": [
    {
      "product": 1,  // 产品ID
      "quantity": [85.5, 75.2, 90.0]  // 数量列表，每个值对应一条明细
    },
    {
      "product": 2,
      "quantity": [120.0]
    }
  ]
}
```

**Response**:
```json
{
  "stock_change_record": {
    "id": 15,
    "type": 1,
    "warehouse": 1,
    "source_type": 1,
    "source_id": 123,
    "is_finished": false,
    "created_at": "2025-11-25T10:30:00Z",
    "updated_at": "2025-11-25T10:30:00Z"
  },
  "details": [
    {
      "id": 45,
      "product": 1,
      "product_name": "纯棉印花布",
      "quantity": 85.5,
      "unit": 1,
      "unit_display": "米",
      "stock_change_record": 15,
      "is_consumed": false,
      "consume_with_id": null
    },
    {
      "id": 46,
      "product": 1,
      "product_name": "纯棉印花布",
      "quantity": 75.2,
      "unit": 1,
      "unit_display": "米",
      "stock_change_record": 15,
      "is_consumed": false,
      "consume_with_id": null
    }
    // ... 更多明细
  ],
  "message": "成功创建库存变动记录及 3 条明细",
  "created_details_count": 3
}
```

### 2. Relaxed Mode Stock Change

**URL**: `POST /api/v1/stock-change/relaxed/`

**Description**: Creates a stock change record using total quantity and unit count to automatically split into details.

**Request Parameters**:
```json
{
  "type": 1,  // 1=入库, 2=出库
  "warehouse": 1,  // 仓库ID
  "source_type": 1,  // 来源类型
  "source_id": 123,  // 可选，来源单据ID
  "products": [
    {
      "product": 1,  // 产品ID
      "quantity": {
        "value": 250.8,  // 总数量
        "unit_count": 2.5  // 单条数量，默认为1
      }
    }
  ]
}
```

**Response**:
```json
{
  "stock_change_record": {
    "id": 16,
    "type": 1,
    "warehouse": 1,
    "source_type": 1,
    "source_id": 123,
    "is_finished": false,
    "created_at": "2025-11-25T11:00:00Z",
    "updated_at": "2025-11-25T11:00:00Z"
  },
  "details": [
    {
      "id": 48,
      "product": 1,
      "product_name": "纯棉印花布",
      "quantity": 100.0,  // 自动拆分
      "unit": 1,
      "unit_display": "米",
      "stock_change_record": 16,
      "is_consumed": false,
      "consume_with_id": null
    },
    {
      "id": 49,
      "product": 1,
      "product_name": "纯棉印花布",
      "quantity": 100.0,  // 自动拆分
      "unit": 1,
      "unit_display": "米",
      "stock_change_record": 16,
      "is_consumed": false,
      "consume_with_id": null
    },
    {
      "id": 50,
      "product": 1,
      "product_name": "纯棉印花布",
      "quantity": 50.8,  // 剩余数量
      "unit": 1,
      "unit_display": "米",
      "stock_change_record": 16,
      "is_consumed": false,
      "consume_with_id": null
    }
  ],
  "message": "成功创建库存变动记录及 3 条明细",
  "created_details_count": 3
}
```

### 3. Restricted Mode Stock Change

**URL**: `POST /api/v1/stock-change/restrict/`

**Description**: Creates a stock change record in "restricted-in-out" mode where outbound details must reference existing inbound details.

**Request Parameters**:
```json
{
  "type": 2,  // 1=入库, 2=出库
  "warehouse": 1,  // 仓库ID，必须为严进严出模式
  "source_type": 6,  // 来源类型
  "source_id": 123,  // 可选，来源单据ID
  "products": [
    {
      "product": 1,  // 产品ID
      "quantity": [150.5],  // 数量列表
      "consume_with": [23, 24]  // 必须指定消耗的入库明细ID列表
    }
  ]
}
```

**Response**:
```json
{
  "stock_change_record": {
    "id": 17,
    "type": 2,
    "warehouse": 1,
    "source_type": 6,
    "source_id": 123,
    "is_finished": false,
    "created_at": "2025-11-25T11:15:00Z",
    "updated_at": "2025-11-25T11:15:00Z"
  },
  "details": [
    {
      "id": 51,
      "product": 1,
      "product_name": "纯棉印花布",
      "quantity": 75.25,
      "unit": 1,
      "unit_display": "米",
      "stock_change_record": 17,
      "is_consumed": false,
      "consume_with_id": 23  // 消耗的入库明细ID
    },
    {
      "id": 52,
      "product": 1,
      "product_name": "纯棉印花布",
      "quantity": 75.25,
      "unit": 1,
      "unit_display": "米",
      "stock_change_record": 17,
      "is_consumed": false,
      "consume_with_id": 24  // 消耗的入库明细ID
    }
  ],
  "message": "成功创建库存变动记录及 2 条明细",
  "created_details_count": 2
}
```

  ### 4. Stock Transfer (调拨)

  **URL**: `POST /api/v1/stock-change/transfer/`

  **Description**: Performs an in-transaction transfer between two warehouses that share the same warehouse **mode** (可以跨类型，例如整件仓 → 散件仓)。一次调用会同步创建调出与调入的 `StockChangeRecord` 并立即完成库存扣加，确保库存一致性。

  **Business Constraints**:
  - `from_warehouse` 与 `to_warehouse` 必须属于当前商户，且不可相同。
  - 两个仓库的 `mode` 必须一致（RESTRICT_IN、UNRESTRICTED 均可）。目前不支持 `RESTRICT_IN_OUT`（严进严出）的调拨。
  - 请求在数据库事务内完成，不能依赖异步任务；支持 `request_id` 幂等重试。
  - `products` 列表沿用原出入库的 payload 结构，允许：
    - `quantities`: 严谨/严进模式的显式数量数组；
    - `value + unit_count/num_of_rolls`: 宽松模式的拆分定义；
    - `consume_detail_ids`: 若未来开放严进严出调拨，可在此补充（当前会拒绝）。

  **Request Parameters**:
  ```json
  {
    "from_warehouse": 3,
    "to_warehouse": 5,
    "transfer_date": "2025-12-02",     // 可选，默认当天
    "remarks": "整件仓调拨到散件仓",   // 可选
    "request_id": "transfer-20251202-001", // 可选幂等键
    "products": [
      { "product_id": 12, "quantities": ["10.50", "5.25"] },
      { "product_id": 27, "value": "120.0", "unit_count": "30" }
    ]
  }
  ```

  **Response**:
  ```json
  {
    "id": 88,
    "merchant": 6,
    "from_warehouse": 3,
    "from_warehouse_name": "整件一号仓",
    "to_warehouse": 5,
    "to_warehouse_name": "散件贵宾仓",
    "mode": 1,
    "mode_display": "严进宽出",
    "operator": 24,
    "transfer_date": "2025-12-02",
    "status": 2,
    "status_display": "已完成",
    "remarks": "整件仓调拨到散件仓",
    "request_id": "transfer-20251202-001",
    "outgoing_record_id": 512,
    "incoming_record_id": 513,
    "items": [
      {
        "id": 143,
        "product": 12,
        "product_name": "贡缎白坯布",
        "total_quantity": "15.75",
        "unit": 1,
        "unit_display": "米",
        "num_of_rolls": 2
      },
      {
        "id": 144,
        "product": 27,
        "product_name": "彩纱混纺",
        "total_quantity": "120.00",
        "unit": 1,
        "unit_display": "米",
        "num_of_rolls": 4
      }
    ]
  }
  ```

  **Error Cases**:
  | HTTP | Payload 示例 | 说明 |
  |------|--------------|------|
  | 400 | `{ "error": "调拨要求调出仓与调入仓的出入库模式一致" }` | 两仓 mode 不同 |
  | 400 | `{ "error": "调入仓与调出仓不能相同" }` | 相同仓库 |
  | 400 | `{ "error": "调出仓不存在或不属于当前商户" }` | 仓库不属于当前商户 |
  | 400 | `{ "error": "调拨产品明细不能为空" }` | products 为空 |
  | 409 | `{ "error": "请求重复" }` *(未来可扩展)* | 同一 `request_id` 重复提交（当前直接返回已存在记录） |

  > **提示**：响应中附带 `outgoing_record_id` 与 `incoming_record_id`，可继续使用既有的 `GET /api/v1/stock-change/<id>/` 接口查看具体的明细与快照。

### Other Related Endpoints

#### List Stock Changes
**URL**: `GET /api/v1/stock-changes/`

**Response**: List of stock change records with pagination
```json
{
  "count": 100,
  "next": "http://example.com/api/v1/stock-changes/?page=2",
  "previous": null,
  "results": [
    {
      "id": 15,
      "type": 1,
      "type_display": "入库",
      "warehouse": 1,
      "source_type": 1,
      "source_type_display": "采购",
      "is_finished": true,
      "finished_at": "2025-11-25T12:00:00Z",
      "created_at": "2025-11-25T10:30:00Z"
    }
  ]
}
```

#### Get Stock Change Details
**URL**: `GET /api/v1/stock-change/<id>/`

**Response**: Detailed view of a specific stock change record with its details
```json
{
  "stock_change_record": {
    "id": 15,
    "type": 1,
    "type_display": "入库",
    "warehouse": 1,
    "source_type": 1,
    "source_type_display": "采购",
    "is_finished": true,
    "finished_at": "2025-11-25T12:00:00Z",
    "created_at": "2025-11-25T10:30:00Z"
  },
  "details": [
    {
      "id": 45,
      "product": 1,
      "product_name": "纯棉印花布",
      "quantity": 85.5,
      "unit": 1,
      "unit_display": "米",
      "is_consumed": false,
      "consume_with_id": null
    }
  ]
}
```

### 4. 库存红冲（Stock Change Offset）

- **URL**: `POST /api/v1/stock-change/<id>/offset/`
- **权限**：登录员工，且仓库必须属于当前商户。
- **描述**：针对已完成的库存变动记录生成反向库存记录，恢复库存数量并标记原快照 `cancelled=true`。

**Request Body**
```json
{
  "reason": "采购单作废，冲销库存",
  "request_id": "rcf-20251201-0001",   // 可选，幂等键
  "extra_meta": { "source": "purchase_order", "operator": 18 }
}
```

> **注意**：当前版本仅支持“全量红冲”，`items` 字段请留空；未来版本会按需要开放部分明细红冲能力。

**Response**
```json
{
  "status": "success",
  "message": "红冲记录已创建",
  "stock_change_record": {
    "id": 602,
    "type": 2,
    "source_type": 11,
    "source_id": 498,
    "warehouse": 3,
    "is_finished": true
  },
  "details": [
    {
      "id": 1880,
      "product": 15,
      "quantity": "100.00",
      "unit": 1
    }
  ],
  "created_details_count": 1
}
```

**错误返回**

| HTTP | 状态 | 说明 |
|------|------|------|
| 400 | `{"error": "仅允许对已完成的库存变动执行红冲"}` | 原记录未完成或仍在处理中 |
| 400 | `{"error": "该库存变动记录已执行红冲"}` | 阻止重复红冲 |
| 403 | `{"error": "无权访问该库存变动记录"}` | 仓库不属于当前商户 |
| 404 | `{"error": "库存变动记录ID xxx 不存在"}` | 记录不存在 |

红冲成功后，会生成 `source_type=OFFSET` 的库存记录，`source_id` 指向原库存记录 ID；原 `StockSnapshot` 会设置 `cancelled=true`、`offset_id=<新快照ID>`，便于审计追踪。

---

### 5. Stock Snapshot List

- **URL**: `GET /api/v1/stock-snapshots/`
- **Description**: Read-only list of `StockSnapshot` entries. Supports `limit/offset` pagination,搜索与过滤。
- **Query Parameters**:
  | 参数 | 说明 |
  |------|------|
  | `product` / `product_name` / `product_code` | 指定产品或模糊搜索 |
  | `warehouse` / `warehouse_name` | 指定仓库或模糊搜索 |
  | `stock_change_record` | 关联的出入库记录 ID |
  | `unit` | 单位过滤 |
  | `cancelled` | `true/false`，是否被标记取消 |
  | `has_offset` | `true/false`，筛选是否存在冲抵记录 |
  | `date_from` / `date_to` | 创建时间范围（`YYYY-MM-DD`） |
  | `search` | 针对产品名、产品编码、仓库名的全文搜索 |
  | `ordering` | 排序字段，默认 `-created_at` |

**Response 示例**：
```json
{
  "count": 2,
  "results": [
    {
      "id": 901,
      "product": 15,
      "product_name": "纯棉印花布",
      "warehouse": 3,
      "warehouse_name": "严进仓",
      "delta": "120.00",
      "quantity_before": "80.00",
      "quantity_after": "200.00",
      "stock_change_record": 456,
      "stock_change_record_type": "入库",
      "stock_change_record_source": "采购",
      "unit": 1,
      "unit_display": "米",
      "num_of_rolls": 3,
      "offset_to": null,
      "cancelled": false,
      "created_at": "2025-11-25T12:05:00Z"
    }
  ]
}
```

### 5. Merchant Auto Complete Setting

- **URL**: `POST /api/v1/set-merchant-auto-complete-stock-change/`
- **Description**: 为当前商户开启 / 关闭“创建出入库记录后自动完成”功能。开启后，`create_*` 接口在事务内会立即调用 `stock.services.make_stock_change_completed`，自动写入库存与快照。
- **Request**:
```json
{
  "auto_complete": true
}
```
- **Response**:
```json
{
  "status": "success"
}
```

只有具备员工身份的登录用户才可调用该接口。

### 6. Stock Change Completion Behavior

- **自动完成**：当商户开启 `auto_complete_stock_change` 时，任意模式的创建接口都会立即调用 `make_stock_change_completed`，更新 `Inventory` 并生成 `StockSnapshot`。
- **手动完成**：当需要补处理或未开启自动完成时，可调用 `POST /api/v1/stock-change/<id>/finish/` 手动触发 `make_stock_change_completed`，也可直接在后台脚本里调用服务函数。
- **状态字段**：`StockChangeRecord.is_finished` / `finished_at` 会在完成时标记，用于列表和详情接口判断记录是否已经生效。

### 7. Finish Stock Change Record (Manual)

- **URL**: `POST /api/v1/stock-change/<id>/finish/`
- **Description**: 手动触发指定出入库记录的库存扣减与快照写入，适用于未开启自动完成或需要补处理的场景。仅当前商户下且仓库可见的员工可调用。
- **Request**: 无需请求体，只需保证 URL 中的 `<id>` 为目标 `StockChangeRecord` 的 ID。
- **Response**:
```json
{
  "status": "success",
  "message": "库存变动记录已标记为完成",
  "stock_change_record": {
    "id": 17,
    "type": 1,
    "warehouse": 1,
    "warehouse_name": "严谨仓",
    "source_type": 1,
    "source_type_display": "采购",
    "source_id": 123,
    "is_finished": true,
    "finished_at": "2025-11-25T12:30:00Z",
    "created_at": "2025-11-25T11:50:00Z",
    "created_by": "stock_admin",
    "remarks": null
  }
}
```

### 8. Stock Change Offset 与业务红冲

`POST /api/v1/stock-change/<id>/offset/` 已在上文“库存红冲（Stock Change Offset）”说明。业务单据红冲会通过 `business` service 调用同一库存对冲能力，并把同一个 `red_flush_id` 写入原库存记录和反向库存记录。

面向前端或业务系统的正式业务红冲入口请优先使用 `business` 单据 API，例如：

- `POST /api/v1/purchase-orders/<id>/red-flush/`
- `POST /api/v1/sales-orders/<id>/red-flush/`
- `POST /api/v1/purchase-return-orders/<id>/red-flush/`
- `POST /api/v1/sales-return-orders/<id>/red-flush/`

完整业务 API 见 `docs/2026-06-12_business_red_flush_api.md`。

## Examples

### Example 1: Creating a Stock-in Record (Full Mode)
```bash
curl -X POST http://example.com/api/v1/stock-change/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your_token" \
  -d '{
    "type": 1,
    "warehouse": 1,
    "source_type": 1,
    "products": [
      {
        "product": 1,
        "quantity": [100.5, 75.3, 120.0]
      }
    ]
  }'
```

### Example 2: Creating a Stock-out Record (Relaxed Mode)
```bash
curl -X POST http://example.com/api/v1/stock-change/relaxed/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your_token" \
  -d '{
    "type": 2,
    "warehouse": 1,
    "source_type": 6,
    "products": [
      {
        "product": 2,
        "quantity": {
          "value": 250.8,
          "unit_count": 2.5
        }
      }
    ]
  }'
```

### Example 3: Creating a Stock-out Record (Restricted Mode)
```bash
curl -X POST http://example.com/api/v1/stock-change/restrict/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your_token" \
  -d '{
    "type": 2,
    "warehouse": 1,
    "source_type": 6,
    "products": [
      {
        "product": 3,
        "quantity": [100.0, 50.0],
        "consume_with": [45, 46]
      }
    ]
  }'
```

### Example 4: Finish a Stock Change Record Manually
```bash
curl -X POST http://example.com/api/v1/stock-change/17/finish/ \
  -H "Authorization: Bearer your_token"
```

## Error Handling

### Common Error Responses

#### Authentication Error
```json
{
  "error": "无权限访问"
}
```

#### Missing Required Parameters
```json
{
  "error": "缺少必要参数",
  "message": "请提供 type, warehouse, source_type"
}
```

#### Validation Error
```json
{
  "error": "产品数据验证失败",
  "details": {
    "products": [
      "产品列表中存在重复的产品ID"
    ]
  }
}
```

#### Warehouse/Product Visibility Error
```json
{
  "error": "仓库ID 1 对当前用户不可见"
}
```

#### Business Logic Error
```json
{
  "error": "仓库出入库模式为【严进严出】，该模式暂未支持当前操作"
}
```

## Business Rules

1. **Warehouse Mode Restrictions**:
   - Full mode works with all warehouse modes
   - Relaxed mode requires "unrestricted" (宽进宽出) or "restricted-in" (严进) warehouses
   - Restricted mode requires "restricted-in-out" (严进严出) warehouses

2. **Stock Change Validation**:
   - Stock-in can only use incoming source types (1, 2, 3, 4, 5)
   - Stock-out can only use outgoing source types (6, 7, 8, 9, 10)

3. **Restricted Mode Specific Rules**:
   - Outbound details must reference existing inbound details
   - Referenced details must belong to the same warehouse
   - Referenced details must not already be consumed

4. **Permission Checks**:
   - Users can only access warehouses they have permission to see
   - Users can only access products they have permission to see
   - All stock change operations require proper authentication