1
0
forked from erp-dev/erp
Files
erpnew/docs/2026-06-12_business_reverse_audit.md
2026-06-12 13:57:21 +08:00

159 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
- 按产品、颜色、规格、批次等维度限制退货数量。
- 校验同一原单累计已审核退货数量。
- 在退货单或余额变动记录中明确记录冲销关系。