1
0
forked from erp-dev/erp
Files
erpnew/docs/2026-06-12_business_red_flush_design.md
2026-06-22 22:27:28 +08:00

185 lines
7.1 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` 模块为已审核正式单据增加整单红冲能力,并通过独立 API 对外开放。
## 已确认口径
- 红冲后原单据 `status` 保持 `APPROVED`,不回退、不作废。
- 原单据增加“已红冲”标记,用于列表查询提速。
- 因为 `status` 保持 `APPROVED`**对账单 builder 必须额外按 `is_red_flushed=False` 过滤**,否则红冲单会被原值计入 `positive_amount`/`negative_amount`summary 多算一倍。
- 每次红冲由 service 自动生成一个 UUID 类型的 `red_flush_id`
- `red_flush_id` 用于跨表、跨记录追踪同一次红冲涉及的所有数据,是审计关联批次 ID。
- 不新增 `red_flush_no`。该编号只适合人工展示,目前无需求。
- 第一版只支持整单红冲,不支持部分红冲。
- API 已开放独立 `/red-flush/` actionAPI 层红冲原因必填。
- 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 写入;若存在销售出库记录则同步库存反向,若无库存记录则跳过库存红冲。
- 采购退货单整单红冲。
- 销售退货单整单红冲。
- 付款单整单红冲。
- 收款单整单红冲。
- 非已审核单据不能红冲。
- 已红冲单据不能重复红冲。
- 缺少原始余额记录时报错。
- 库存记录未完成或已红冲时报错。
- 事务回滚场景。
API 测试覆盖:
- 6 类单据 `/red-flush/` 成功路径。
- `reason` 必填。
- 无员工身份权限拒绝。
- 跨商户查询隔离。
- 外部付款/收款单拒绝。
- 未审核、重复红冲、采购等要求库存闭环的单据缺库存记录等 service 校验错误映射为 400。
- 成功响应包含 `is_red_flushed``red_flush_id``red_flushed_at`,并抽样断言余额/库存副作用。
目标red flush 关键 service/API 路径需要有定向测试覆盖。
## 对账单 builder 联动修复2026-06-22
`business.services._CustomerStatementBuilder._build_sales_records` / `_build_sales_return_records` / `_build_receipt_records` 以及 `_SupplierStatementBuilder._build_purchase_records` / `_build_purchase_return_records` / `_build_payment_records` 之前只过滤 `status=APPROVED`,未过滤 `is_red_flushed`
红冲后单据 `status` 仍为 `APPROVED`,所以会继续以原金额出现在 `build_customer_statement` / `build_supplier_statement``records` 列表中,被 `build_statement_summary` 双倍计入 `positive_total`/`negative_total`
修复办法6 个 builder queryset 全部追加 `is_red_flushed=False`
注意:
- `BalanceService.get_customer_balance` / `get_supplier_balance` 读取 `CustomerBalance/SupplierBalance` 表,红冲服务通过反向 `BalanceChangeRecord` 已经把余额抵消,因此 `current_balance` 字段一直是正确的;只有 `records``summary` 受影响。
- `ExternalCustomerStatementOrder` 目前不在 6 类红冲入口里,相关 builder/adjustment 暂不需要过滤,将来若开放外部单红冲必须同步更新。
定向测试:见 `business.tests.test_statement_services.BusinessStatementRedFlushExclusionTestCase`,覆盖 6 类正式单据红冲后 statement 中 `records` 不再包含、`summary` 不再多算。
## 测试命令
开发环境在容器内运行测试,并绕过 PgBouncer
```bash
docker compose exec -T -e DB_HOST=postgres -e DB_PORT=5432 web \
uv run python manage.py test business.tests.test_red_flush_services api_v1.tests.BusinessRedFlushAPITestCase --keepdb --noinput
```
覆盖率:
```bash
docker compose exec -T -e DB_HOST=postgres -e DB_PORT=5432 web \
uv run coverage run --source=business,api_v1 manage.py test business.tests.test_red_flush_services api_v1.tests.BusinessRedFlushAPITestCase --keepdb --noinput
```
```bash
docker compose exec -T web uv run coverage report -m
```