# Business 单据红冲服务设计备查 日期:2026-06-12 ## 目标 在 `business` 模块为已审核正式单据增加整单红冲能力。第一阶段只实现 service 和测试,不新增 API。 ## 已确认口径 - 红冲后原单据 `status` 保持 `APPROVED`,不回退、不作废。 - 原单据增加“已红冲”标记,用于列表查询提速。 - 每次红冲由 service 自动生成一个 UUID 类型的 `red_flush_id`。 - `red_flush_id` 用于跨表、跨记录追踪同一次红冲涉及的所有数据,是审计关联批次 ID。 - 不新增 `red_flush_no`。该编号只适合人工展示,目前无需求。 - 第一版只支持整单红冲,不支持部分红冲。 - API 在 service 与测试完成、覆盖率达标后再增加;API 层红冲原因必填。 - API 权限复用现有审批/作废权限,不新增独立红冲权限。 - 多商户隔离必须下沉到 service。红冲 service 调用方必须传入当前 `merchant`,service 在解析单据后校验单据所属商户。 ## 字段方案 6 类正式业务单据增加: - `is_red_flushed`: 是否已红冲,布尔值,建索引。 - `red_flush_id`: 红冲批次 UUID,可空,建索引。 - `red_flushed_at`: 红冲时间,可空。 覆盖单据: - `PurchaseOrder` - `SalesOrder` - `PurchaseReturnOrder` - `SalesReturnOrder` - `PaymentOrder` - `ReceiptOrder` `BalanceChangeRecord` 增加: - `red_flush_id`: 红冲批次 UUID,可空,建索引。 `StockChangeRecord` 增加: - `red_flush_id`: 红冲批次 UUID,可空,建索引。 暂不在 `StockSnapshot` 增加 `red_flush_id`,因为快照已有 `offset_to/offset_id/cancelled` 精确匹配关系,且可以通过 `StockChangeRecord.red_flush_id` 查到相关快照。 ## 关系标记规则 一次红冲生成一个 `red_flush_id`,并在同一个事务中写入相关数据。 原业务单据: - `is_red_flushed=True` - `red_flush_id=<本次 UUID>` - `red_flushed_at=<当前时间>` 原余额变动记录: - `cancelled=True` - `cancelled_at=<当前时间>` - `offset_id=<反向余额记录 ID>` - `red_flush_id=<本次 UUID>` 反向余额变动记录: - `offset_to=<原余额记录 ID>` - `offset_at=<当前时间>` - `red_flush_id=<本次 UUID>` 原库存记录: - `red_flush_id=<本次 UUID>` 反向库存记录: - `source_type=OFFSET` - `source_id=<原库存记录 ID>` - `red_flush_id=<本次 UUID>` 库存快照继续复用现有关系: - 原快照:`cancelled=True`、`cancelled_at`、`offset_id=<反向快照 ID>` - 反向快照:`offset_to=<原快照 ID>`、`offset_at` ## Service 方案 新增业务红冲入口: - `red_flush_purchase_order(...)` - `red_flush_sales_order(...)` - `red_flush_purchase_return_order(...)` - `red_flush_sales_return_order(...)` - `red_flush_payment_order(...)` - `red_flush_receipt_order(...)` 内部复用统一实现,避免 6 类单据逻辑分叉。 校验规则: - 必须提供当前 `merchant`。 - 单据必须属于当前 `merchant`。 - 只允许 `APPROVED` 单据红冲。 - `is_red_flushed=True` 的单据不能重复红冲。 - 原始余额变动记录必须存在且未被冲抵。 - 有库存影响的单据必须找到对应库存记录,库存记录必须已完成且未被红冲。 - 资金和库存任一红冲失败,整体事务回滚。 资金侧实现: - 基于原 `BalanceChangeRecord.delta` 创建反向余额变动,`delta=-original.delta`。 - 使用现有 `BalanceService.adjust_supplier_balance()` / `adjust_customer_balance()` 写余额与记录。 - 调整 `BalanceService` 让它返回新创建的 `BalanceChangeRecord`,方便写 `offset_to/offset_id/red_flush_id`。 库存侧实现: - 复用 `StockFlowService.offset_stock_change()`。 - 扩展其参数 `red_flush_id=None`。 - 该方法负责把 `red_flush_id` 写入原库存记录和反向库存记录。 ## 测试要求 先跑现有 `business.tests` baseline。 新增 service 测试覆盖: - 采购单整单红冲:余额反向、库存反向、单据标记、批次 ID 写入。 - 销售单整单红冲:余额反向、库存反向、单据标记、批次 ID 写入。 - 采购退货单整单红冲。 - 销售退货单整单红冲。 - 付款单整单红冲。 - 收款单整单红冲。 - 非已审核单据不能红冲。 - 已红冲单据不能重复红冲。 - 缺少原始余额记录时报错。 - 库存记录未完成或已红冲时报错。 - 事务回滚场景。 目标:`business` 模块覆盖率 90%+。 ## 测试命令 开发环境在容器内运行测试,并绕过 PgBouncer: ```bash docker compose exec -T -e DB_HOST=postgres -e DB_PORT=5432 web \ uv run python manage.py test business.tests --keepdb --noinput ``` 覆盖率: ```bash docker compose exec -T -e DB_HOST=postgres -e DB_PORT=5432 web \ uv run coverage run --source=business manage.py test business.tests --keepdb --noinput ``` ```bash docker compose exec -T web uv run coverage report -m ```