PayrollManagementSystem/backend/app/repositories/settlement_repository.py
liuwenxin 76ded6d5ec fix(m3): code-review P1——锁定不变量、入结回填与双轨漂移
- 月结锁定拦截补全:原仅新增回款一处,现覆盖回款编辑(原月+新月,防跨月
  搬数)/删除、发货新增(按发货日)/编辑(原月+新月)/删除、核算与重算
  (已审定月份金额只走调整单);拦截先于「被引用/已核算」判定
- 台账 settlement_id 回填落地:generate 在 replace_items 后回填纳入行、
  重生成前先解绑(clear_ledger_settlement_id)——不回填/不解绑会让
  「未入结」判据失效:重生成漏算整月或把已固化行反复计入
- 经理聚合行落库:原只进内存参与 items 汇总,台账表永远无 calc_type=2
  行、报表下钻落空;重生成先作废旧聚合行(deactivate_month_aggregates),
  _confirm_scope 限定计算类型避免聚合行混入单笔池
- 月结取参锚定所属月月末(_month_end):原用 date.today(),补生成历史
  月份时分成/月工资口径随生成日漂移;聚合行 receipt_date 同步为月末
- generate/submit 出参固化前置到 commit 之前(commit 后 SELECT 失败会把
  已生成的月结记成失败审计,与库中事实相反)
- V9 迁移:business_secondary_allocations 唯一键条件式补齐(幂等裸 SQL,
  临时库验证「无键→补」「有键→跳过」两分支),修复旧 V6 升级库拿不到
  V6 就地加入的约束;V8 迁移 revision 声明补类型注解(原格式漂移使
  schema 同步测试的 head 推断静默漏检 V8);init-dev 版本号推进至 V9
- V7 留存 SQL 约束/索引名单数改复数(与 alembic op.f 与 init-dev 对齐,
  消除 SHOW CREATE TABLE 双轨漂移)
- 测试:显式控制审批开关(不依赖 V8 翻转后的种子默认值);新增回归 8 例
  (回填落库/聚合不重复/月末取参/月末纯函数 5 例/锁定拦截回款与发货与
  核算);全量 773 passed

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-19 11:59:49 +08:00

113 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""月度结算仓储:只读写数据并 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) -> MonthlySettlement | None:
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()