# 销售单审批流程与红冲规划

## 1. 背景

- 销售单在创建时同样进入 `PENDING` 状态，审批通过后才会触发出库流程。
- `business.services.review_sales_order` 统一处理审批通过 (`APPROVED`) 与作废 (`CANCELLED`) 的业务规则。
- `api_v1/views/business/sales/views.py` 暴露 `POST /api/v1/sales-orders/<id>/review/` 接口，供前端、自动化和第三方系统一致调用。

## 2. 审批流程总览

| 步骤 | 说明 |
|------|------|
| 1 | 前端调用 `POST /api/v1/sales-orders/<id>/review/`， body: `{"action": "approve"}` 或 `{"action": "cancel"}` |
| 2 | API 层校验员工身份、订单归属商户、`action` 可选项 |
| 3 | API 调用 `review_sales_order`，根据 `action` 映射到 `SalesOrderStatusEnum.APPROVED` / `CANCELLED` |
| 4 | Service 端根据状态调用 `_approve_sales_order` 或 `_cancel_sales_order`，并在必要时抛出业务异常（如重复审批、已有出入库记录等） |
| 5 | API 返回最新的 `SalesOrderSerializer` 数据，供前端刷新详情 |

## 3. 审批通过触发的事务

1. `_approve_sales_order` 构建 `stock_flow_items`（结构与采购单一致）。
2. 在事务内将 `sales_order.status` 更新为 `APPROVED`。
3. 读取商户设置 `auto_create_stock_change_tasks`：
   - 未配置或为 `False` 时，仅更新状态。
   - 为 `True` 时投递 `business.tasks.create_sales_order_stock_entries` Celery 任务。
4. Celery 任务执行 `StockFlowService.stock_out`：
   - 依据仓库模式创建出库 `StockChangeRecord` / `StockChangeDetail`。
   - `stock.services.make_stock_change_completed` 会在自动完成开启时写入 `Inventory` 并生成 `StockSnapshot`。
5. 任务返回 `stock_change_record_id` 与明细信息，并写日志用于审计。

## 4. 作废流程约束

1. 仍通过 `POST /api/v1/sales-orders/<id>/review/`， body: `{"action": "cancel"}`。
2. `_cancel_sales_order` 会检查 `source_type = SALES` 且 `source_id = 销售单ID` 的 `StockChangeRecord` 是否存在。
3. 若已经生成出库记录，则抛出 `ValueError('销售单已生成出入库记录，无法作废')`，API 返回 `400` 提示前端。
4. 若未生成记录，则在事务内将状态更新为 `CANCELLED`，且不会触发 Celery 任务。

## 5. 常见异常与返回

- 缺少 `customer` 字段：`400` + `{"error": "缺少客户 ID"}`。
- 审批重复：保持幂等，直接返回当前状态。
- 已作废单据审批：`ValueError('作废状态的销售单无法再次审批')` -> `400`。
- 作废时已有出库记录：`ValueError('销售单已生成出入库记录，无法作废')` -> `400`。
- ID 不存在或不属于当前商户：`404`。

## 6. 销售退货（销退）流程补充

- API：`/api/v1/sales-return-orders/`（创建、列表）与 `/review/` 审批接口，与销售单保持完全一致的认证/权限。
- 创建时字段改为 `return_date`，其余 `items` 结构与销售单一致（根据仓库 mode 填写 `numbers` 或 `quantity` / `num_of_rolls`）。严进严出仓在退货场景下视为“入库”，故无需 `consume_detail_ids`。
- 审批通过：
  1. 事务锁定单据与明细。
  2. 状态置为 `APPROVED`。
  3. 调用 `BalanceService.adjust_customer_balance(delta=-total_amount, source=SALES_RETURN_ORDER)`，写 `BalanceChangeRecord`，减少客户欠款。
  4. 根据商户设置触发 `create_sales_return_order_stock_entries`，底层走 `StockFlowService.stock_in`（source=`SALES_RETURN`）。
- 作废：同销售单，只允许 `PENDING` 且未生成 `StockChangeRecord` 的单据作废；审批后不可逆。

---

## 7. 创建接口参数约定

销售单创建接口沿用采购单的参数格式，根据仓库模式填写 `items`：

| 仓库模式 | 出库模式 | `items` 必填字段 |
|----------|----------|------------------|
| `UNRESTRICTED`（宽出） | 无需指定入库明细 | `product_id`, `quantity`, `num_of_rolls`, `price`, `unit` |
| `RESTRICT_IN`（严进宽出） | 无需指定入库明细 | `product_id`, `numbers` (list[int]), `price`, `unit` |
| `RESTRICT_IN_OUT`（严出） | 必须指定要消耗的入库明细 | `product_id`, `consume_detail_ids` (list[int]), `quantity`, `price`, `unit` |

> 说明：在严出模式下，`consume_detail_ids` 列表中的 ID 必须为同仓库、同商户且未被消耗的入库明细 ID。系统会在审批通过时逐条扣减对应库存。

## 8. 红冲（对冲）落地状态

销售单已支持整单红冲。已审批销售单可通过 `POST /api/v1/sales-orders/<id>/red-flush/` 触发，生成反向余额记录；若原销售单存在 `source_type=SALES` 的出库记录，则同步生成反向库存记录。关键原则：

- 采用新增反向记录的方式，保留所有历史变动。
- 存在库存记录时，红冲记录需要指向原 `StockChangeRecord` / `StockSnapshot`，保证审计可追溯。
- 红冲需与业务对象绑定；没有库存记录的销售单只处理余额侧，不执行库存红冲。

红冲能力已补充独立 API 文档与定向测试，确保审批、出库、红冲形成闭环。详细 API 见 `docs/2026-06-12_business_red_flush_api.md`。 

