- 月结锁定拦截补全:原仅新增回款一处,现覆盖回款编辑(原月+新月,防跨月 搬数)/删除、发货新增(按发货日)/编辑(原月+新月)/删除、核算与重算 (已审定月份金额只走调整单);拦截先于「被引用/已核算」判定 - 台账 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>
113 lines
3.5 KiB
Python
113 lines
3.5 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) -> 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()
|