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

7.1 KiB
Raw Blame History

Business 单据红冲服务设计备查

日期2026-06-12

目标

business 模块为已审核正式单据增加整单红冲能力,并通过独立 API 对外开放。

已确认口径

  • 红冲后原单据 status 保持 APPROVED,不回退、不作废。
  • 原单据增加“已红冲”标记,用于列表查询提速。
  • 因为 status 保持 APPROVED对账单 builder 必须额外按 is_red_flushed=False 过滤,否则红冲单会被原值计入 positive_amount/negative_amountsummary 多算一倍。
  • 每次红冲由 service 自动生成一个 UUID 类型的 red_flush_id
  • red_flush_id 用于跨表、跨记录追踪同一次红冲涉及的所有数据,是审计关联批次 ID。
  • 不新增 red_flush_no。该编号只适合人工展示,目前无需求。
  • 第一版只支持整单红冲,不支持部分红冲。
  • API 已开放独立 /red-flush/ actionAPI 层红冲原因必填。
  • API 权限复用现有审批/作废权限,不新增独立红冲权限。
  • 多商户隔离必须下沉到 service。红冲 service 调用方必须传入当前 merchantservice 在解析单据后校验单据所属商户。

字段方案

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=Truecancelled_atoffset_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_flushedred_flush_idred_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_statementrecords 列表中,被 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 字段一直是正确的;只有 recordssummary 受影响。
  • 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