补齐 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>
122 lines
3.9 KiB
Python
122 lines
3.9 KiB
Python
"""月度结算仓储:只读写数据并 flush;commit/rollback 在服务层。"""
|
||
|
||
from sqlalchemy import func, select
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.models import MonthlySettlement, SettlementItem
|
||
from app.schemas.settlement import SettlementQuery
|
||
|
||
|
||
def get_by_id(
|
||
session: Session, settlement_id: int, *, for_update: bool = False
|
||
) -> MonthlySettlement | None:
|
||
"""月结行;``for_update`` 供提交审批路径加行锁用。
|
||
|
||
提交要算审批单版本号(同一月结的第几次送审),并发提交会让两次
|
||
算出同一个版本号;锁住月结行使并发提交串行化。
|
||
"""
|
||
if for_update:
|
||
return session.get(MonthlySettlement, settlement_id, with_for_update=True)
|
||
return session.get(MonthlySettlement, settlement_id)
|
||
|
||
|
||
def get_by_month(session: Session, month: str) -> MonthlySettlement | None:
|
||
"""按月份取月结行(settlement_month 全表唯一)。"""
|
||
return session.execute(
|
||
select(MonthlySettlement).where(MonthlySettlement.settlement_month == month)
|
||
).scalar_one_or_none()
|
||
|
||
|
||
def add(session: Session, settlement: MonthlySettlement) -> MonthlySettlement:
|
||
session.add(settlement)
|
||
session.flush()
|
||
return settlement
|
||
|
||
|
||
def list_settlements(session: Session, query: SettlementQuery) -> list[MonthlySettlement]:
|
||
return list(
|
||
session.execute(
|
||
select(MonthlySettlement)
|
||
.where(*_filters(query))
|
||
.order_by(MonthlySettlement.settlement_month.desc())
|
||
.offset(query.offset)
|
||
.limit(query.page_size)
|
||
).scalars()
|
||
)
|
||
|
||
|
||
def count_settlements(session: Session, query: SettlementQuery) -> int:
|
||
return int(
|
||
session.execute(
|
||
select(func.count()).select_from(MonthlySettlement).where(*_filters(query))
|
||
).scalar_one()
|
||
)
|
||
|
||
|
||
def _filters(query: SettlementQuery) -> tuple:
|
||
filters = []
|
||
if query.status is not None:
|
||
filters.append(MonthlySettlement.status == query.status)
|
||
return tuple(filters)
|
||
|
||
|
||
def list_items(session: Session, settlement_id: int) -> list[SettlementItem]:
|
||
return list(
|
||
session.execute(
|
||
select(SettlementItem)
|
||
.where(SettlementItem.settlement_id == settlement_id)
|
||
.order_by(SettlementItem.id)
|
||
).scalars()
|
||
)
|
||
|
||
|
||
def replace_items(session: Session, settlement_id: int, rows: list[SettlementItem]) -> None:
|
||
"""全量替换明细(先删后 flush 再插,防御写法;不 commit)。"""
|
||
session.execute(delete_items_stmt(settlement_id))
|
||
session.flush()
|
||
for row in rows:
|
||
row.settlement_id = settlement_id
|
||
session.add(row)
|
||
session.flush()
|
||
|
||
|
||
def delete_items_stmt(settlement_id: int):
|
||
from sqlalchemy import delete
|
||
|
||
return delete(SettlementItem).where(SettlementItem.settlement_id == settlement_id)
|
||
|
||
|
||
def update_ledger_settlement_id(
|
||
session: Session, ledger_ids: list[int], settlement_id: int
|
||
) -> None:
|
||
"""月结生成时把纳入的台账行回填 settlement_id(append-only 的受控回填)。"""
|
||
from sqlalchemy import update
|
||
|
||
from app.models import PerformanceLedger
|
||
|
||
if ledger_ids:
|
||
session.execute(
|
||
update(PerformanceLedger)
|
||
.where(PerformanceLedger.id.in_(ledger_ids))
|
||
.values(settlement_id=settlement_id)
|
||
)
|
||
session.flush()
|
||
|
||
|
||
def clear_ledger_settlement_id(session: Session, settlement_id: int) -> None:
|
||
"""重生成前解绑:释放本结算名下台账行的 settlement_id(「未入结」判据复位)。
|
||
|
||
草稿/驳回态重新生成时,上一轮回填的行必须重新进入候选集——否则
|
||
``settlement_id IS NULL`` 会把它们永久排除,重生成漏算整月工资。
|
||
"""
|
||
from sqlalchemy import update
|
||
|
||
from app.models import PerformanceLedger
|
||
|
||
session.execute(
|
||
update(PerformanceLedger)
|
||
.where(PerformanceLedger.settlement_id == settlement_id)
|
||
.values(settlement_id=None)
|
||
)
|
||
session.flush()
|