# 采购单审批流程与红冲规划 ## 1. 背景 - 采购单在创建时默认进入 `PENDING`(审批中)状态,不再立即创建出入库记录。 - `business.services.review_purchase_order` 统一处理审批通过 (`APPROVED`) 与作废 (`CANCELLED`) 的业务规则。 - `api_v1/views/business/purchase/views.py` 暴露 `POST /api/v1/purchase-orders//review/` 接口作为唯一入口,便于前端、自动化流程和第三方系统统一调用。 ## 2. 审批流程总览 | 步骤 | 说明 | |------|------| | 1 | 前端在采购单详情页或审批面板触发 `POST /api/v1/purchase-orders//review/`,请求体只包含 `{"action": "approve"}` 或 `{"action": "cancel"}` | | 2 | API 层验证:当前用户必须具备员工身份、采购单必须属于当前商户;`action` 字段必须是 `approve/cancel` 之一 | | 3 | API 调用 `business.services.review_purchase_order`,根据 `action` 映射到 `PurchaseOrderStatusEnum.APPROVED` 或 `CANCELLED` | | 4 | Service 内部基于状态决定调用 `_approve_purchase_order` 或 `_cancel_purchase_order`,并在必要时抛出业务错误(例如重复审批、作废时已有出入库记录等) | | 5 | API 返回最新的 `PurchaseOrderSerializer` 数据(包含总金额、总数量、明细与状态等)供前端刷新页面 | ## 3. 审批通过触发的事务 1. `_approve_purchase_order` 先构建 `stock_flow_items`: - 宽进仓:`items=[{'product_id', 'value', 'num_of_rolls'}]` - 严进/严进严出:`items=[{'product_id', 'quantities': [...] }]` 2. 在数据库事务中,将 `purchase_order.status` 置为 `APPROVED` 并保存,保证状态更新原子性。 3. 读取商户设置 `auto_create_stock_change_tasks`: - 若未配置或为 `False`,审批仅改变状态,不会触发后续任务。 - 若为 `True`,投递 `business.tasks.create_purchase_order_stock_entries` Celery 任务。 4. Celery 任务执行 `StockFlowService.stock_in`: - 依据仓库模式创建入库 `StockChangeRecord` 及 `StockChangeDetail`。 - 后续在 `stock.services.make_stock_change_completed` 中写入 `Inventory` 并生成 `StockSnapshot`。 5. 任务返回 `stock_change_record_id`、明细数量等信息,并写日志用于追踪审计。 ## 4. 作废流程约束 1. 同样通过 `POST /api/v1/purchase-orders//review/`,但 `{"action": "cancel"}`。 2. Service 端调用 `_cancel_purchase_order`,首先查询是否存在 `source_type = PURCHASE` 且 `source_id = 采购单ID` 的 `StockChangeRecord`。 3. 若已存在入库记录,直接抛出错误“采购单已生成出入库记录,无法作废”;API 返回 `400` 并提示原因。 4. 若未生成记录,则在事务内将状态更新为 `CANCELLED`。不会触发 Celery 任务。 ## 5. 常见异常与返回 - 缺少 `action` 字段:`400` + DRF 序列化错误。 - 审批已是目标状态:直接返回现状(幂等)。 - 已作废的单据请求审批:`ValueError('作废状态的采购单无法再次审批')` -> API 返回 `400`。 - 作废时已有出入库记录:`ValueError('采购单已生成出入库记录,无法作废')` -> API 返回 `400`。 - ID 不存在或不属于当前商户:API 返回 `404`。 ## 6. 采购退货(采退)流程补充 - API:`POST /api/v1/purchase-return-orders/` 创建,`POST /api/v1/purchase-return-orders//review/` 审批。 - 创建阶段与采购单一致,只是字段改为 `return_date`,并允许传入 `purchase_order` 以关联原单。 - 审批通过: 1. 在事务内锁单、生成 `stock_flow_items`。 2. 状态置为 `APPROVED`。 3. 调用 `BalanceService.adjust_supplier_balance(delta=-总金额,source=PURCHASE_RETURN_ORDER)`,写入 `BalanceChangeRecord`。 4. 触发 `create_purchase_return_order_stock_entries`,底层调用 `StockFlowService.stock_out`(source=`PURCHASE_RETURN`),受仓库 mode 约束: - 宽进仓:传递 `value + num_of_rolls` - 严进仓:传递 `numbers` - 严进严出:必须提供 `consume_detail_ids` - 作废逻辑沿用采购单:只有 `PENDING` 且未产生 `StockChangeRecord` 时才允许取消,一旦审批成功便禁止。 --- ## 7. 下一阶段:库存红冲(对冲)方案规划 ### 6.1 目标与原则 - **目标**:允许在采购单及其衍生的出入库记录完成后,通过“红冲”方式撤销或抵消错误的库存变动,同时保持库存快照与主库存的可追溯性。 - **原则**: - 采用“新增反向记录”方式对冲,而非直接修改原记录;保留每一次实际发生的库存变更。 - 红冲记录需要指向原 `StockChangeRecord` / `StockSnapshot`(如 `offset_to`、`offset_id` 字段),便于审计。 - 红冲动作必须与业务流程挂钩(例如采购单作废或红冲),避免孤立的库存操作。 ### 6.2 行动步骤 1. **业务入口确定** - 定义哪些业务对象可以触发红冲(采购单、销售单等)。 - 明确触发条件:如采购单审批通过后,允许“创建红冲请求”并指向原采购单。 2. **库存服务层改造** - 在 `stock.services.StockFlowService` 或相关函数中增加创建红冲记录的能力: - 根据原 `StockChangeRecord` 生成反向 `StockChangeRecord` 和明细。 - 更新 `StockSnapshot` 的 `offset_to` / `offset_id` / `cancelled` 字段,确保链路闭环。 - 确保 `Inventory` 调整遵循相同的原子逻辑(事务 + 快照)。 3. **业务服务与模型扩展** - 在 `business.services` 中引入“红冲采购单”或“撤销审批”的新方法: - 校验:原单是否允许红冲、是否存在未处理的红冲记录。 - 调用库存红冲服务并记录关联。 - 必要时在 `PurchaseOrder` 或新模型中记录红冲状态/引用。 4. **API 与文档更新** - 设计新的红冲 API(例如 `/api/v1/purchase-orders//offset/`),请求体注明原因、操作者。 - 文档中要说明红冲流程与限制(只能对已审批单据、需管理员权限等)。 5. **测试与审计** - 单元测试:业务 Service、库存 Service、API 都要覆盖正常流程与错误场景。 - 集成测试:验证红冲后库存数量恢复、快照关系正确、消息/通知链路是否需要补充。 - 如有需要,增加日志或审计表,记录红冲操作人、时间、关联记录。 通过以上步骤,审批流程与红冲机制可以衔接:审批负责“正向入库”与任务分发,红冲负责在业务需要时生成配对的反向库存记录,保持库存账实一致,同时具备可回溯、可审计的业务闭环。 --- ## 8. 库存记录红冲标识字段 库存变动记录的 API 响应新增以下字段,用于前端识别红冲关系: - `cancelled`:是否已被红冲(等价于 `is_reversed`) - `is_reversed`:当前记录是否已被红冲 - `is_offset`:当前记录是否为红冲记录(用于冲抵其他记录) 说明:红冲关系来源于库存快照的 `offset_to/offset_id/cancelled` 字段,记录本身不落库状态字段。