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