feat(approval): 审批单/节点状态机与二次分配接入(开关自动通过)

- approval_orders/approval_steps 服务层:submit_order(不 commit 不嵌套
  audit,业务写路径统一收口)+ act 节点动作 + 分页/详情
- 开关关闭=提单即两级「自动通过」留痕(action=3,operator 记系统);
  开启=财务→领导流转,一驳全驳,驳回必填意见
- 节点准入严格按角色(status=1 仅财务、status=2 仅领导),admin 不代办;
  模块准入 require_roles 依赖工厂(admin/finance/leader,营销后勤待
  澄清 #6 后放开)
- 业务接入:创建/编辑带方案即提单(空方案不提单);再编辑自动撤回旧
  在途单(node=0/action=4 留痕);AP 编号冲突 create 内可重取(外层
  重试兜底)、update 直接抛出防静默丢失
- conftest 兜底清理增审批单(AP 前缀)与开关参数种子基线恢复
- 集成 11 例:自动通过/两节点流转/驳回留痕/缺意见 400/节点准入 403/
  admin 不代办/重复操作 400/撤旧建新/模块准入

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-15 18:50:38 +08:00
1 parent 680661a80b
commit 8ef26e22fb
10 files changed
+842 -1

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
"""审批管理的请求/响应模型(2026-10-15 复核审批补充需求)。"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field, model_validator
from app.core.response import PageQuery
class ApprovalStepOut(BaseModel):
"""节点记录(时间线;action=3 自动通过 operator 记「系统」)。"""
model_config = ConfigDict(from_attributes=True)
id: int
node: int
action: int
opinion: str | None = None
operator_id: int | None = None
operator_name: str
create_time: datetime
class ApprovalOrderOut(BaseModel):
"""审批单列表行。"""
model_config = ConfigDict(from_attributes=True)
id: int
order_no: str
order_type: int
ref_id: int
refer_no: str
status: int
submitter_id: int | None = None
submitter_name: str
create_time: datetime
class ApprovalOrderDetailOut(ApprovalOrderOut):
"""审批单详情:附节点时间线。"""
steps: list[ApprovalStepOut] = Field(default_factory=list)
class ApprovalQuery(PageQuery):
"""审批单列表查询条件。"""
keyword: str | None = Field(default=None, max_length=32, description="审批单号/引用单号,模糊")
status: int | None = Field(default=None, ge=1, le=5, description="状态筛选")
order_type: int | None = Field(default=None, ge=1, le=2, description="对象类型筛选")
class ApprovalActionIn(BaseModel):
"""节点动作:1 通过 / 2 驳回(驳回须填意见——空驳无从追溯)。"""
model_config = ConfigDict(str_strip_whitespace=True)
action: int = Field(ge=1, le=2, description="1通过 2驳回")
opinion: str | None = Field(default=None, max_length=255, description="审批意见")
@model_validator(mode="after")
def _check_opinion(self) -> "ApprovalActionIn":
if self.action == 2 and not self.opinion:
raise ValueError("驳回必须填写审批意见")
return self