# Business 模块已审核单据反向能力审计

审计日期：2026-06-12

## 结论

`business` 模块的正式单据在 `status = APPROVED` 后，没有发现可将原单据改回审批中、撤销审核、反审或直接红冲原单据的 service/API。

已审核单据不能作废这一点由统一作废逻辑实现：`_cancel_order_impl()` 在目标单据已审批时直接抛错。

但存在“通过新单据抵消业务影响”的能力：

- 采购单、销售单：可通过新建并审核采购退货单/销售退货单形成库存和应收/应付反向影响。
- 付款单、收款单：创建时允许负金额，用于退款场景，审核后形成与正向付款/收款相反的余额影响。
- 余额变动记录模型有冲抵字段，但当前 service 未使用，未形成可调用的冲抵能力。

## 审计范围

核心文件：

- `business/services.py`
- `business/models.py`
- `api_v1/views/business/*/views.py`

正式单据类型：

- 采购单 `PurchaseOrder`
- 销售单 `SalesOrder`
- 采购退货单 `PurchaseReturnOrder`
- 销售退货单 `SalesReturnOrder`
- 付款单 `PaymentOrder`
- 收款单 `ReceiptOrder`

## 已审核后禁止作废

所有正式单据的作废最终进入 `business/services.py::_cancel_order_impl()`：

```python
if locked.status == approved_status:
    raise ValueError(f'已审批的{error_label}无法作废')
```

覆盖的入口：

- `review_purchase_order(..., target_status=CANCELLED)`
- `review_sales_order(..., target_status=CANCELLED)`
- `review_purchase_return_order(..., target_status=CANCELLED)`
- `review_sales_return_order(..., target_status=CANCELLED)`
- `review_payment_order(..., target_status=CANCELLED)`
- `review_receipt_order(..., target_status=CANCELLED)`

API 层只暴露 `approve` 和 `cancel` 两类 action，没有发现 `unapprove`、`reverse`、`red_flush` 等动作。

## 已审核后的反向能力

| 原单据 | 是否可直接撤销/反审 | 是否有业务反向单据 | 说明 |
| --- | --- | --- | --- |
| 采购单 | 否 | 有，采购退货单 | 采购退货单审核后出库，并减少供应商应付。 |
| 销售单 | 否 | 有，销售退货单 | 销售退货单审核后入库，并减少客户应收。 |
| 采购退货单 | 否 | 未发现专用反向单据 | 只能再做新的采购单产生正向影响。 |
| 销售退货单 | 否 | 未发现专用反向单据 | 只能再做新的销售单产生正向影响。 |
| 付款单 | 否 | 无专用反向单据，但允许负金额 | `create_payment_order()` 允许 `amount < 0`，注释说明为供应商退款。 |
| 收款单 | 否 | 无专用反向单据，但允许负金额 | `create_receipt_order()` 允许 `amount < 0`，注释说明为退款给客户。 |

## 采购/销售退货单细节

`create_purchase_return_order()` 可传 `purchase_order` 或 `purchase_order_id` 关联原采购单。

当前校验：

- 原采购单存在且属于当前商户。
- 原采购单供应商必须与退货单供应商一致。

当前未校验：

- 原采购单必须是 `APPROVED`。
- 退货数量不能超过原采购单数量。
- 同一原采购单累计退货数量不能超过原单数量。

`create_sales_return_order()` 可传 `sales_order` 或 `sales_order_id` 关联原销售单。

当前校验：

- 原销售单存在且属于当前商户。
- 原销售单客户必须与退货单客户一致。

当前未校验：

- 原销售单必须是 `APPROVED`。
- 退货数量不能超过原销售单数量。
- 同一原销售单累计退货数量不能超过原单数量。

因此，退货单属于“新单据反向业务影响”，不是“撤销原单据”；它不会改变原采购单/销售单的 `status`，也不会标记原单已被红冲。

## 付款/收款负金额细节

`_ensure_non_zero_amount()` 只禁止金额为 0，不禁止负数，并有明确注释：

```python
# 金额类字段要求非零（允许负数用于处理退款场景）
# 付款单负金额 = 供应商退款，收款单负金额 = 退款给客户
```

影响：

- 负数付款单审核后，`review_payment_order()` 使用 `delta=-locked.settlement_amount`，当 `settlement_amount < 0` 时会增加供应商应付余额。
- 负数收款单审核后，`review_receipt_order()` 使用 `delta=-locked.settlement_amount`，当 `settlement_amount < 0` 时会增加客户应收余额。

这是一种资金侧反向能力，但没有和原付款/收款单建立关联，也没有把原单据标记为红冲或冲销。

## 余额冲抵字段

`BalanceChangeRecord` 存在以下字段：

- `offset_to`
- `offset_at`
- `offset_id`
- `cancelled`
- `cancelled_at`

但代码搜索只发现模型定义和创建余额变动记录，未发现 service/API 更新这些字段。因此当前没有可用的余额变动冲抵流程。

## API 暴露情况

`api_v1/views/business/*/views.py` 的审核接口统一只接受：

```python
action = approve | cancel
```

对应模块：

- `purchase/views.py`
- `sales/views.py`
- `purchase_return/views.py`
- `sales_return/views.py`
- `payment/views.py`
- `receipt/views.py`

未发现 API 层绕过 service 暴露已审核撤销、反审或红冲原单的能力。

## 风险点

1. 退货单可作为采购单/销售单的反向能力，但当前不要求原单已审核。
2. 退货单未限制数量，可能产生超原单退货或重复累计退货。
3. 负金额付款/收款具备资金反向效果，但没有原单关联，后续对账只能从业务含义判断，不能从数据结构追溯到被冲销单据。
4. `BalanceChangeRecord` 的冲抵字段未被 service 使用，容易让维护者误以为已有冲抵流程。

## 建议

如果业务需求只是确认“已审核后是否有撤销/红冲能力”，当前答案是：没有直接撤销/反审/红冲原单的能力；有通过新单据产生反向影响的能力。

如果要把退货单作为正式红冲/撤销路径，建议补充：

- 原单必须 `APPROVED`。
- 按产品、颜色、规格、批次等维度限制退货数量。
- 校验同一原单累计已审核退货数量。
- 在退货单或余额变动记录中明确记录冲销关系。
