PayrollManagementSystem/backend/app/repositories/performance_ledger_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

137 lines
4.9 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 PerformanceLedger
from app.models.performance_ledger import CALC_TYPE_MANAGER_AGGREGATE
from app.schemas.calc import LedgerQuery
def get_by_id(session: Session, ledger_id: int) -> PerformanceLedger | None:
"""按主键取台账行。"""
return session.get(PerformanceLedger, ledger_id)
def get_active_by_receipt(
session: Session, receipt_id: int, *, calc_type: int = 1
) -> PerformanceLedger | None:
"""取某回款的当前生效核算行(is_active=1;无则 None)。"""
return session.execute(
select(PerformanceLedger).where(
PerformanceLedger.receipt_id == receipt_id,
PerformanceLedger.calc_type == calc_type,
PerformanceLedger.is_active == 1,
)
).scalar_one_or_none()
def has_active_for_business(session: Session, business_id: int, *, calc_type: int = 1) -> bool:
"""该业务是否已有生效核算行(核算粒度 #25:业务级一次性计提的判定)。"""
return (
session.execute(
select(func.count())
.select_from(PerformanceLedger)
.where(
PerformanceLedger.business_id == business_id,
PerformanceLedger.calc_type == calc_type,
PerformanceLedger.is_active == 1,
)
).scalar_one()
> 0
)
def get_active_by_receipt_of_business(
session: Session, business_id: int, *, calc_type: int = 1
) -> PerformanceLedger:
"""取该业务当前生效的核算行(重复回款登记进度时回显既有台账)。"""
return (
session.execute(
select(PerformanceLedger)
.where(
PerformanceLedger.business_id == business_id,
PerformanceLedger.calc_type == calc_type,
PerformanceLedger.is_active == 1,
)
.order_by(PerformanceLedger.id.desc())
)
.scalars()
.first()
)
def add(session: Session, row: PerformanceLedger) -> PerformanceLedger:
"""落台账行(flush 取 id;commit 在服务层)。"""
session.add(row)
session.flush()
return row
def mark_inactive(session: Session, row: PerformanceLedger) -> None:
"""重算时把旧版本行翻为已替代(全表唯一的受控 UPDATE 例外;不 commit)。"""
row.is_active = 0
session.flush()
def deactivate_month_aggregates(session: Session, month: str) -> None:
"""月结重生成时作废该月全部经理聚合行(calc_type=2)。
聚合行每次生成重建,旧行不翻 0 会与新行重复计入(按人聚合按 is_active
过滤)。与 mark_inactive 同属 is_active 翻 0 的受控 UPDATE 例外;set 批量
版本而非逐行——聚合行一个月至多几行,且无逐行回读必要。
"""
from sqlalchemy import update
session.execute(
update(PerformanceLedger)
.where(
PerformanceLedger.settlement_month == month,
PerformanceLedger.calc_type == CALC_TYPE_MANAGER_AGGREGATE,
PerformanceLedger.is_active == 1,
)
.values(is_active=0)
)
session.flush()
def next_version(session: Session, receipt_id: int) -> int:
"""某回款的下一个核算版本号(max+1;无行时 1)。"""
current = session.execute(
select(func.max(PerformanceLedger.calc_version)).where(
PerformanceLedger.receipt_id == receipt_id
)
).scalar_one()
return int(current or 0) + 1
def list_ledger(session: Session, query: LedgerQuery) -> list[PerformanceLedger]:
"""分页查询台账(筛选:月/人员/业务/生效/类型)。"""
stmt = select(PerformanceLedger).where(*_filters(query))
stmt = stmt.order_by(PerformanceLedger.id.desc()).offset(query.offset).limit(query.page_size)
return list(session.execute(stmt).scalars())
def count_ledger(session: Session, query: LedgerQuery) -> int:
"""台账计数(与 list_ledger 同筛选)。"""
return int(
session.execute(
select(func.count()).select_from(PerformanceLedger).where(*_filters(query))
).scalar_one()
)
def _filters(query: LedgerQuery) -> tuple:
filters = []
if query.settlement_month:
filters.append(PerformanceLedger.settlement_month == query.settlement_month)
if query.personnel_id is not None:
filters.append(PerformanceLedger.personnel_id == query.personnel_id)
if query.business_id is not None:
filters.append(PerformanceLedger.business_id == query.business_id)
if query.is_active is not None:
filters.append(PerformanceLedger.is_active == query.is_active)
if query.calc_type is not None:
filters.append(PerformanceLedger.calc_type == query.calc_type)
return tuple(filters)