forked from erp-dev/erp
160 lines
4.9 KiB
Markdown
160 lines
4.9 KiB
Markdown
# 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
|
||
```
|