PayrollManagementSystem/backend/app/models/approval.py

164 lines
6.7 KiB
Python
Raw Normal View History

"""审批模型:审批单与节点记录(2026-10-15 复核审批补充需求)。
三级链路:营销后勤提交 → 财务复核 → 领导审核。行为由参数体系开关驱动
(`approval.*.enabled` BOOL):关闭=提交即生成「自动通过」节点留痕,
留痕不断链(开发计划 3.2(三));开启=按节点人工审核,一驳全驳。
依赖说明:节点准入按登录角色编码判定(ROLE_FINANCE/ROLE_LEADER),
不动 `permissions`/`role_permissions` 闲置表 —— 细粒度权限清单待业务方
答复(待澄清 #6)后再激活。
ref_id 为**固化无外键**(同 business_status_logs 先例):order_type=2
(月结预留)将指向 ④ 尚未落地的表,外键无法统一指向;单据删除保护由
服务层承担(详见 approval_service)。
"""
from datetime import datetime
from sqlalchemy import (
JSON,
DateTime,
ForeignKey,
Index,
Integer,
String,
UniqueConstraint,
text,
)
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, mysql_table_args
# 审批单状态(approval_orders.status;流转机见 approval_service)
APPROVAL_STATUS_PENDING_FINANCE = 1 # 待财务复核
APPROVAL_STATUS_PENDING_LEADER = 2 # 待领导审核
APPROVAL_STATUS_APPROVED = 3 # 已通过(终态)
APPROVAL_STATUS_REJECTED = 4 # 已驳回(终态)
APPROVAL_STATUS_WITHDRAWN = 5 # 已撤回(被引用方重新提交时旧在途单注销)
# 节点(approval_steps.node;0 仅用于撤回留痕)
APPROVAL_NODE_FINANCE = 1
APPROVAL_NODE_LEADER = 2
APPROVAL_NODE_WITHDRAW = 0
APPROVAL_NODES = {
APPROVAL_STATUS_PENDING_FINANCE: APPROVAL_NODE_FINANCE,
APPROVAL_STATUS_PENDING_LEADER: APPROVAL_NODE_LEADER,
}
# 动作(approval_steps.action)
APPROVAL_ACTION_APPROVE = 1 # 人工通过
APPROVAL_ACTION_REJECT = 2 # 人工驳回
APPROVAL_ACTION_AUTO_APPROVE = 3 # 系统自动通过(开关关闭时逐节点留痕)
APPROVAL_ACTION_WITHDRAW = 4 # 撤回(业务重新提交时旧在途单注销)
# 审批对象类型(approval_orders.order_type)
APPROVAL_TYPE_SECONDARY_ALLOCATION = 1
APPROVAL_TYPE_MONTHLY_SETTLEMENT = 2
APPROVAL_TYPE_ADJUSTMENT = 3
class ApprovalOrder(Base, TimestampMixin):
"""审批单:一张待审对象一条审批流(状态可流转,含 update_time)。"""
__tablename__ = "approval_orders"
__table_args__ = mysql_table_args(
"审批单表(营销后勤提交→财务复核→领导审核;开关关闭时自动通过留痕)",
# 同一引用对象的第 N 次送审在库里有唯一一行:并发提单算重版本号时
# 由 DB 兜住(应用层取号走锁读,见 approval_service._next_version)
UniqueConstraint(
"order_type",
"ref_id",
"version_no",
name="uk_approval_orders_order_type_ref_version",
),
Index("idx_approval_orders_type_status", "order_type", "status"),
feat(approval): 审批单内容快照与二次分配方案版本化 补齐 code-review 遗留 2 项:审批单此前只固化 ref_id/refer_no 指针,复核者看到 的是业务当前内容;二次分配方案在编辑替换与驳回清空时一律物理删除,方案事实 无处回溯。 - 审批单加 version_no 与 snapshot_json(信封 schema_version/order_type/ captured_at/data);二次分配、月结、调整单三类提交均固化送审内容,开关关闭 的自动通过路径同样写快照;详情按 snapshot/live/missing 三态回显,历史单 只读回退当前内容并显式告警,不回填历史快照 - 二次分配方案行改 append-only 版本化(version_no/is_active,唯一键扩为 business+version+personnel):编辑替换与驳回清空只置历史,核算与详情默认 只取生效行,业务详情新增 allocation_history - 送审内容未变不重提、方案未变不新增版本:判据统一为送审内容,两个 version_no 语义解耦(方案版本=内容第几版,审批单版本=第几次送审) - 审批单列表、工作台待办、审批超时预警三处 defer 快照列,避免整列拉取 - 月结提交取行加 for_update,防并发提交算出重复的审批单版本号 - 修复审批类型筛选上界 le=2(调整单无法筛选)、快照金额把空值写成 0.00 - V11 迁移三件套同步(Alembic / db/migration / init-dev.sql) 测试:单元 +16(方案内容比较 10、审批入参 6)、集成 +12;全量 891 passed。 Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 06:30:51 -04:00
# 版本号按 (order_type, ref_id) 递增,须有对应索引避免全表 max 扫描
Index("idx_approval_orders_type_ref", "order_type", "ref_id"),
)
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
order_no: Mapped[str] = mapped_column(
String(32),
nullable=False,
unique=True,
comment="审批单号,自动生成 AP{YYYYMMDD}-{4位当日序号}",
)
order_type: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, comment="审批对象类型:1二次分配方案 2月度结算 3调整单"
)
ref_id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED,
comment="引用对象ID(order_type=1 时为 businesses.id;固化无外键,删除保护由服务层承担)",
)
refer_no: Mapped[str] = mapped_column(
String(32), nullable=False, comment="引用对象单号(冗余固化,列表页免联查)"
)
feat(approval): 审批单内容快照与二次分配方案版本化 补齐 code-review 遗留 2 项:审批单此前只固化 ref_id/refer_no 指针,复核者看到 的是业务当前内容;二次分配方案在编辑替换与驳回清空时一律物理删除,方案事实 无处回溯。 - 审批单加 version_no 与 snapshot_json(信封 schema_version/order_type/ captured_at/data);二次分配、月结、调整单三类提交均固化送审内容,开关关闭 的自动通过路径同样写快照;详情按 snapshot/live/missing 三态回显,历史单 只读回退当前内容并显式告警,不回填历史快照 - 二次分配方案行改 append-only 版本化(version_no/is_active,唯一键扩为 business+version+personnel):编辑替换与驳回清空只置历史,核算与详情默认 只取生效行,业务详情新增 allocation_history - 送审内容未变不重提、方案未变不新增版本:判据统一为送审内容,两个 version_no 语义解耦(方案版本=内容第几版,审批单版本=第几次送审) - 审批单列表、工作台待办、审批超时预警三处 defer 快照列,避免整列拉取 - 月结提交取行加 for_update,防并发提交算出重复的审批单版本号 - 修复审批类型筛选上界 le=2(调整单无法筛选)、快照金额把空值写成 0.00 - V11 迁移三件套同步(Alembic / db/migration / init-dev.sql) 测试:单元 +16(方案内容比较 10、审批入参 6)、集成 +12;全量 891 passed。 Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 06:30:51 -04:00
version_no: Mapped[int] = mapped_column(
Integer,
nullable=False,
server_default="1",
comment="提交版本号(同一对象类型+引用对象从1递增;重提产生新版本,旧单快照不覆盖;"
"同一对象内唯一,由uk_approval_orders_order_type_ref_version强制)",
feat(approval): 审批单内容快照与二次分配方案版本化 补齐 code-review 遗留 2 项:审批单此前只固化 ref_id/refer_no 指针,复核者看到 的是业务当前内容;二次分配方案在编辑替换与驳回清空时一律物理删除,方案事实 无处回溯。 - 审批单加 version_no 与 snapshot_json(信封 schema_version/order_type/ captured_at/data);二次分配、月结、调整单三类提交均固化送审内容,开关关闭 的自动通过路径同样写快照;详情按 snapshot/live/missing 三态回显,历史单 只读回退当前内容并显式告警,不回填历史快照 - 二次分配方案行改 append-only 版本化(version_no/is_active,唯一键扩为 business+version+personnel):编辑替换与驳回清空只置历史,核算与详情默认 只取生效行,业务详情新增 allocation_history - 送审内容未变不重提、方案未变不新增版本:判据统一为送审内容,两个 version_no 语义解耦(方案版本=内容第几版,审批单版本=第几次送审) - 审批单列表、工作台待办、审批超时预警三处 defer 快照列,避免整列拉取 - 月结提交取行加 for_update,防并发提交算出重复的审批单版本号 - 修复审批类型筛选上界 le=2(调整单无法筛选)、快照金额把空值写成 0.00 - V11 迁移三件套同步(Alembic / db/migration / init-dev.sql) 测试:单元 +16(方案内容比较 10、审批入参 6)、集成 +12;全量 891 passed。 Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 06:30:51 -04:00
)
snapshot_json: Mapped[dict | None] = mapped_column(
JSON,
comment="提交时内容快照(信封:schema_version/order_type/captured_at/data;历史单可空)",
)
status: Mapped[int] = mapped_column(
TINYINT_TYPE,
nullable=False,
server_default="1",
comment="审批状态:1待财务复核 2待领导审核 3已通过 4已驳回 5已撤回",
)
submitter_id: Mapped[int | None] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("users.id", ondelete="SET NULL"),
comment="提交人ID(账号删除后置空)",
)
submitter_name: Mapped[str] = mapped_column(
String(64), nullable=False, comment="提交人用户名(冗余固化)"
)
class ApprovalStep(Base):
"""审批节点记录表(append-only):每次动作只增不改,构成时间线。
不继承 TimestampMixin(留痕无更新语义,同 business_status_logs 先例)。
"""
__tablename__ = "approval_steps"
__table_args__ = mysql_table_args(
"审批节点记录表(append-only;自动通过/撤回均有留痕)",
Index("idx_approval_steps_order_id", "order_id"),
)
id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
)
order_id: Mapped[int] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("approval_orders.id", ondelete="CASCADE"),
nullable=False,
comment="审批单ID(approval_orders.id;单删记录随删)",
)
node: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, comment="审批节点:0撤回留痕 1财务复核 2领导审核"
)
action: Mapped[int] = mapped_column(
TINYINT_TYPE, nullable=False, comment="动作:1通过 2驳回 3自动通过 4撤回"
)
opinion: Mapped[str | None] = mapped_column(String(255), comment="审批意见(驳回时填写)")
operator_id: Mapped[int | None] = mapped_column(
BIGINT_UNSIGNED,
ForeignKey("users.id", ondelete="SET NULL"),
comment="操作人ID(自动通过/系统动作置空)",
)
operator_name: Mapped[str] = mapped_column(
String(64), nullable=False, comment="操作人用户名(冗余固化;系统动作记「系统」)"
)
create_time: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"), comment="创建时间"
)