# Business 模块采购单 API

## 创建采购单（默认待审批）

- **URL**: `POST /api/v1/purchase-orders/`
- **权限**: 需要登录且具备员工身份
- **描述**: 创建业务模块的 `PurchaseOrder`，默认状态为 `PENDING`（审批中）。此时不会立即生成入库记录，需在审批通过后才会触发库存入库任务。

### 请求体

```json
{
  "supplier": 1,
  "warehouse_id": 2,
  "order_date": "2025-11-26",
  "remarks": "测试采购单",
  "items": [
    {
      "product_id": 10,
      "quantity": 120,
      "num_of_rolls": 3,
      "price": "12.50",
      "unit": "米",
      "empty_diff_percent": "0"
    }
  ]
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| supplier | integer | ✅ | 供应商 ID，必须隶属于当前商户 |
| warehouse / warehouse_id | integer | ✅ | 入库仓库 ID（两字段二选一，推荐 `warehouse_id`） |
| order_date | string (date) | ✅ | 订单日期（`YYYY-MM-DD`） |
| remarks | string | 否 | 备注 |
| items | array | ✅ | 入库明细，根据仓库模式提供不同字段 |

#### items 结构说明

- **宽进仓（UNRESTRICTED）**  
  - 必填：`product_id`、`quantity`、`num_of_rolls`  
  - `quantity_of_rolls` 会自动置空，并以 `{value, num_of_rolls}` 的形式传递给 `StockFlowService.stock_in` 的宽松模式。

- **严进仓（RESTRICT_IN / RESTRICT_IN_OUT）**  
  - 必填：`product_id`、`numbers`（数组）  
  - 后端会以数组长度设置 `num_of_rolls`，把所有数值拼为 `quantity_of_rolls="10,5,8"`，同时求和得到 `quantity`，并生成 `{'quantities': ['10','5','8']}` 传递给严谨模式。

公共可选字段：`price`、`unit`、`color`、`empty_diff_percent`、`batch_number`、`remarks`。缺省时默认使用 0 或产品单位。  
⚠️ 当仓库模式与 items 字段不匹配（例如严进仓缺少 `numbers`、宽进仓提供 `numbers`）时将返回 `400`，提示“仓库为 ×× 模式，items[n] 需要提供 …”。

### 响应

```json
{
  "id": 35,
  "status": 1,
  "message": "采购单创建成功，等待审批"
}
```

创建成功即刻返回，状态为 `审批中`。只有在审批通过后才会调用库存入库任务。

### 错误示例

| 状态码 | 示例 | 说明 |
|--------|------|------|
| 400 | `{"error": "缺少仓库 ID"}` | 请求缺失关键字段 |
| 400 | `{"error": "仓库为严进模式，items[0] 需要提供 numbers 数组"}` | 参数与仓库模式不匹配 |
| 400 | `{"error": "供应商 99 不存在"}` | 供应商不属于当前商户 |
| 403 | `{"error": "无权限访问"}` | 当前用户无员工信息 |

## 审批 / 作废采购单（API & Service）

- **URL**: `POST /api/v1/purchase-orders/<id>/review/`
- **权限**: 登录 + 员工身份
- **请求体**:

```json
{
  "action": "approve"  // 可选: approve / cancel
}
```

当 `action=approve` 时，会调用 `business.services.review_purchase_order` 将采购单状态更新为 `APPROVED`，并在商户设置 `auto_create_stock_change_tasks=True` 时自动触发 `create_purchase_order_stock_entries` Celery 任务（根据采购单明细重建 `StockFlowService.stock_in` 所需 payload）。

当 `action=cancel` 时，会尝试将采购单状态改为 `CANCELLED`。如果该采购单已经生成过出入库记录（`source_type=PURCHASE`、`source_id=采购单ID`），接口会返回 `400`，提示“采购单已生成出入库记录，无法作废”。

### 响应示例

```json
{
  "id": 12,
  "status": 2,
  "supplier_name": "供应商A",
  "warehouse_name": "严进仓",
  "total_amount": "1500.00",
  "diff_quantity": "0.00",
  "total_quantity": "120.00",
  "items": [
    {
      "id": 33,
      "product": 10,
      "quantity": "120.00",
      "unit": "米",
      "spec": "50D",
      "num_of_rolls": 3
    }
  ]
}
```

### 错误示例

| 状态码 | 示例 | 说明 |
|--------|------|------|
| 400 | `{"action": ["This field is required."]}` | 缺少 `action` |
| 400 | `{"error": "采购单已生成出入库记录，无法作废"}` | 尝试作废但已生成出入库单 |
| 404 | `{"error": "采购单不存在"}` | ID 不在当前商户下 |

### 关联任务（business/tasks.py）

`create_purchase_order_stock_entries` 任务会接收 `purchase_order_id`、`warehouse_id`、已转换好的 `items` 信息，并通过 `StockFlowService.stock_in` 创建入库记录。只有当采购单审批通过且商户开启自动入库设置时才会派发该任务。

### 测试

- `business/tests.PurchaseOrderServiceTestCase`：覆盖创建采购单、审批触发任务、作废校验以及宽/严模式的 payload 生成逻辑。  
- `business/tests.PurchaseOrderStockServiceTestCase` 与 `PurchaseOrderStockTaskTestCase`：继续验证 service 层生成入库单及 Celery 任务封装逻辑。

