7.1 KiB
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/action,API 层红冲原因必填。 - API 权限复用现有审批/作废权限,不新增独立红冲权限。
- 多商户隔离必须下沉到 service。红冲 service 调用方必须传入当前
merchant,service 在解析单据后校验单据所属商户。
字段方案
6 类正式业务单据增加:
is_red_flushed: 是否已红冲,布尔值,建索引。red_flush_id: 红冲批次 UUID,可空,建索引。red_flushed_at: 红冲时间,可空。
覆盖单据:
PurchaseOrderSalesOrderPurchaseReturnOrderSalesReturnOrderPaymentOrderReceiptOrder
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=Truered_flush_id=<本次 UUID>red_flushed_at=<当前时间>
原余额变动记录:
cancelled=Truecancelled_at=<当前时间>offset_id=<反向余额记录 ID>red_flush_id=<本次 UUID>
反向余额变动记录:
offset_to=<原余额记录 ID>offset_at=<当前时间>red_flush_id=<本次 UUID>
原库存记录:
red_flush_id=<本次 UUID>
反向库存记录:
source_type=OFFSETsource_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:
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
覆盖率:
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
docker compose exec -T web uv run coverage report -m