"""月度结算服务(M3 ④):生成快照 → 提交审批 → 通过即锁定。 确认回款口径(澄清 #8 策略点,2026-10-16 暂定):settlement_status=1 的 **有效台账行**(is_active=1、settlement_id IS NULL=未被任何月结固化), 按 settlement_month(回款日所在月)归集。答复后只改 ``_confirm_scope``。 通过即锁定(status=3 终态):锁定后源数据(回款/发货)写路径由 ``ensure_month_not_locked`` 拦截;调整走调整单(T6)。 """ import logging from datetime import date from decimal import ROUND_HALF_UP, Decimal from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.core.constants import ( MODULE_SETTLEMENT, ) from app.core.db_errors import is_duplicate_entry from app.core.exceptions import BadRequestError, NotFoundError from app.models import ( MonthlySettlement, Personnel, SettlementItem, User, ) from app.models.approval import APPROVAL_TYPE_MONTHLY_SETTLEMENT from app.models.performance_ledger import ( CALC_TYPE_MANAGER_AGGREGATE, CALC_TYPE_RECEIPT, ) from app.models.settlement import ( SETTLEMENT_STATUS_APPROVED, SETTLEMENT_STATUS_DRAFT, SETTLEMENT_STATUS_PENDING, SETTLEMENT_STATUS_REJECTED, ) from app.repositories import performance_ledger_repository, settlement_repository from app.schemas.settlement import ( SettlementDetailOut, SettlementOut, SettlementQuery, ) from app.services import approval_service, audit_service, manager_share from app.services.approval_service import AuditRecord, RequestMeta from app.services.formula import manager_team_share, monthly_salary logger = logging.getLogger(__name__) # 审批对象类型引用(order_type=2 月结、3 调整单) _SETTLEMENT_ORDER_TYPE = APPROVAL_TYPE_MONTHLY_SETTLEMENT _SETTLEMENT_NOTE = "确认回款口径(#8 策略点):settlement_status=1 的有效台账行按回款日归月" def generate( session: Session, month: str, *, operator: User, meta: RequestMeta | None = None, ) -> SettlementOut: """生成月度结算快照(草稿):按确认回款口径取有效台账行,按人汇总。 - 同月已有月结行(任意状态)时 400——一行一月,重生成走「驳回/草稿态 重新生成」(_release_previous_round 解绑回填后清明细重算) - 经理聚合(calc_type=2)在生成时计算并落库(#12 参数化见 ``_build_manager_aggregates``) 本函数只做编排,各段独立私有函数(口径修改只动对应段)。 """ with audit_service.audit( session, operator=operator, record=AuditRecord( module=MODULE_SETTLEMENT, action="生成月结", summary=f"生成 {month} 月度结算快照", request_data={"month": month}, ), meta=meta, ): from app.services import payroll_months control = payroll_months.lock_month(session, month, operator) existing = settlement_repository.get_by_month(session, month) _guard_regeneration(existing, month) _release_previous_round(session, existing, month) ledger_rows = _confirm_scope(session, month) from app.services.receipt_confirmation import ensure_confirmed ensure_confirmed(session, [row.receipt_id for row in ledger_rows if row.receipt_id is not None]) from app.core.config import get_settings if not ledger_rows and get_settings().payroll_allow_legacy_calculation: raise BadRequestError(f"{month} 无可结算的核算记录(先在台账完成核算)") aggregate_rows = _build_manager_aggregates(session, ledger_rows, month) settlement = _upsert_draft(session, existing, operator, month) items, ledger_ids = _build_items(session, ledger_rows, aggregate_rows, month) settlement_repository.replace_items(session, settlement.id, items) if not items: raise BadRequestError(f"{month} 无可发薪人员或核算记录") from app.services.probation_service import persist_payments persist_payments(session, settlement, items) _apply_totals(settlement, items) settlement.snapshot_json = {"note": _SETTLEMENT_NOTE} # 入结回填:台账行 settlement_id 是「未入结」防重判据(_confirm_scope), # 不回填会让重生成把已固化行反复纳入;聚合行此刻已落库、id 可用 settlement_repository.update_ledger_settlement_id(session, ledger_ids, settlement.id) # 出参在 commit 前固化(audit 约定:commit 后查询失败会把已生成的月结 # 记成失败审计,与库中事实相反) if control.generated_at is None: from datetime import datetime control.generated_at = datetime.now() from app.core.config import get_settings approval_enabled = get_settings().payroll_allow_legacy_calculation or ( get_settings().payroll_approval_enabled and approval_service.is_enabled(session, 2) ) settlement.snapshot_json = { **settlement.snapshot_json, "approval_required": approval_enabled, } if not approval_enabled: # 2026-10-30 用户澄清:生成工资即完成,不建审批单或自动通过痕迹。 settlement.status = SETTLEMENT_STATUS_APPROVED settlement.locked_at = control.generated_at settlement.locked_by = control.maker_name settlement.approval_order_no = None settlement.snapshot_json = { "note": "工资生成即完成并锁定,仅供查看,无需系统审核", "approval_required": False, } session.flush() detail = _detail_of(session, settlement) session.commit() logger.info("月结生成 | month=%s | items=%s", month, len(items)) return detail def _guard_regeneration(existing: MonthlySettlement | None, month: str) -> None: """重生成准入:已审定 / 待审批不可重生成(草稿、已驳回放行)。""" if existing is None: return if existing.status == SETTLEMENT_STATUS_APPROVED: raise BadRequestError(f"{month} 已审定锁定,不可重新生成(调整走调整单)") if existing.status == SETTLEMENT_STATUS_PENDING: raise BadRequestError(f"{month} 有待审批的月结,不可重新生成") def _release_previous_round( session: Session, existing: MonthlySettlement | None, month: str ) -> None: """释放上一轮固化标记(必须早于 _confirm_scope)。 台账行 settlement_id 回填后,「未入结」过滤会把它们永久排除在候选集外; 旧聚合行也要作废,否则本轮重建的聚合行与旧行重复计入。 """ if existing is None: return from sqlalchemy import delete from app.models import ProbationPayment session.execute(delete(ProbationPayment).where(ProbationPayment.settlement_id == existing.id)) settlement_repository.clear_ledger_settlement_id(session, existing.id) performance_ledger_repository.deactivate_month_aggregates(session, month) def _upsert_draft( session: Session, existing: MonthlySettlement | None, operator: User, month: str ) -> MonthlySettlement: """新建或重置草稿头表(重生成时刷新提交人与状态)。""" if existing is None: settlement = MonthlySettlement( settlement_month=month, submitter_id=operator.id, submitter_name=operator.username, status=SETTLEMENT_STATUS_DRAFT, ) return settlement_repository.add(session, settlement) existing.submitter_id = operator.id existing.submitter_name = operator.username existing.status = SETTLEMENT_STATUS_DRAFT return existing def _build_items( session: Session, ledger_rows: list, aggregate_rows: list, month: str ) -> tuple[list[SettlementItem], list[int]]: """按人汇总生成明细行,返回 (明细, 本次入结的台账行 id 列表)。""" from app.core.config import get_settings if not get_settings().payroll_allow_legacy_calculation: from app.services.salary_completion import build_items return build_items(session, ledger_rows + aggregate_rows, month) by_person = _bucket_rows(ledger_rows + aggregate_rows) items = [_item_of(session, bucket, month) for _, bucket in sorted(by_person.items())] ledger_ids = [lid for bucket in by_person.values() for lid in bucket["ledger_ids"]] return items, ledger_ids def _bucket_rows(rows: list) -> dict[tuple[int, int], dict]: """台账行按 (人员, 核算类型) 维度分桶(冗余人读字段取首行)。""" by_person: dict[tuple[int, int], dict] = {} for row in rows: key = (row.personnel_id, row.calc_type) bucket = by_person.setdefault( key, { "personnel_id": row.personnel_id, "personnel_name": row.personnel_name, "role_code": row.role_code, "calc_type": row.calc_type, "performance_sum": Decimal("0"), "payable_sum": Decimal("0"), "ledger_ids": [], "is_probation": row.is_probation, }, ) bucket["performance_sum"] += row.performance_amount or Decimal("0") bucket["payable_sum"] += row.payable_amount or Decimal("0") bucket["ledger_ids"].append(row.id) return by_person def _item_of(session: Session, bucket: dict, month: str) -> SettlementItem: """单条结算明细:月工资三口径净额、试用期说明与台账行引用列表。""" person = session.get(Personnel, bucket["personnel_id"]) basic = person.salary_standard if person else None mode = _salary_mode_of(session, bucket["role_code"], month) net = ( monthly_salary(bucket["payable_sum"], basic, mode) if person and person.is_probation != 1 else None ) note = None if person and person.is_probation == 1: note = ( f"试用期绩效照算不发放;年累计超 {_probation_threshold(session, month)} 元部分" "年底一次性结算" ) return SettlementItem( personnel_id=bucket["personnel_id"], personnel_name=bucket["personnel_name"], role_code=bucket["role_code"], calc_type=bucket["calc_type"], performance_sum=bucket["performance_sum"].quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), payable_sum=bucket["payable_sum"].quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), basic_amount=basic, salary_mode=mode, net_amount=( net.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) if net is not None else None ), is_probation=bucket["is_probation"], probation_note=note, snapshot_json={"ledger_ids": bucket["ledger_ids"]}, ) def _apply_totals(settlement: MonthlySettlement, items: list[SettlementItem]) -> None: """头表三合计固化(明细已两位小数,合计再 HALF_UP 收口)。""" settlement.total_performance = sum((i.performance_sum for i in items), Decimal("0")).quantize( Decimal("0.01"), rounding=ROUND_HALF_UP ) settlement.total_payable = sum((i.payable_sum for i in items), Decimal("0")).quantize( Decimal("0.01"), rounding=ROUND_HALF_UP ) settlement.person_count = len({i.personnel_id for i in items}) def submit( session: Session, settlement_id: int, *, operator: User, meta: RequestMeta | None = None, ) -> SettlementDetailOut: """提交审批:开关分流(关=自动通过并锁定;开=待财务复核)。""" from app.core.config import get_settings if ( not get_settings().payroll_allow_legacy_calculation and not get_settings().payroll_approval_enabled ): raise BadRequestError("工资生成后直接完成,仅供查看,无需系统审批") with audit_service.audit( session, operator=operator, record=AuditRecord( module=MODULE_SETTLEMENT, action="提交月结", summary=f"提交月结 #{settlement_id}", request_data={"settlement_id": settlement_id}, ), meta=meta, ): settlement = settlement_repository.get_by_id(session, settlement_id, for_update=True) if settlement is None: raise NotFoundError("月结不存在") if settlement.status not in (SETTLEMENT_STATUS_DRAFT, SETTLEMENT_STATUS_REJECTED): raise BadRequestError("仅草稿或已驳回状态可提交") try: order = approval_service.submit_order( session, order_type=_SETTLEMENT_ORDER_TYPE, ref_id=settlement.id, refer_no=f"JS{settlement.settlement_month.replace('-', '')}", submitter=operator, # 月结明细在生成时已固化;提交时快照让复核者看到的是送审版本, # 而不是审批期间若发生重生成后的内容 snapshot=approval_service.settlement_content(session, settlement.id), ) except IntegrityError as exc: # version_no 唯一键的并发冲突(提交前已锁月结行,此项理论不可达, # 留作 DB 兜底):审计上下文会统一回滚,这里只把 1062 翻译成 # 可重试的业务提示,其余完整性错误原样上抛 if is_duplicate_entry(exc): raise BadRequestError("提交冲突(版本号生成重复),请重试") from exc raise settlement.approval_order_no = order.order_no if order.status == SETTLEMENT_STATUS_APPROVED: mark_locked(session, settlement, operator=operator) logger.info("月结自动通过并锁定 | %s", settlement.settlement_month) else: settlement.status = SETTLEMENT_STATUS_PENDING # 出参在 commit 前固化(audit 约定:commit 后查询失败会把已提交的 # 月结记成失败审计,与库中事实相反) session.flush() detail = _detail_of(session, settlement) session.commit() return detail def mark_locked(session: Session, settlement: MonthlySettlement, *, operator: User) -> None: """审批通过钩子(由 approval_service.act 回调):置已审定+锁定信息。""" from datetime import datetime settlement.status = SETTLEMENT_STATUS_APPROVED settlement.locked_at = datetime.now() settlement.locked_by = operator.username def mark_rejected(session: Session, settlement: MonthlySettlement, *, operator: User) -> None: """审批驳回钩子:回草稿(可修改后重提)。 锁定信息一并清空:草稿态残留 locked_at/locked_by 会让详情页展示 「已锁定」的假象(历史缺陷路径下驳回前可能已被提前锁定)。 """ settlement.status = SETTLEMENT_STATUS_DRAFT settlement.locked_at = None settlement.locked_by = None def ensure_month_not_locked(session: Session, month: str) -> None: """锁定拦截(回款/发货写路径调用):单据日期所在月已审定 → 400。""" settlement = settlement_repository.get_by_month(session, month) if settlement is not None and settlement.status == SETTLEMENT_STATUS_APPROVED: raise BadRequestError(f"{month} 已月结锁定,源数据不可修改(调整走调整单)") def get_detail(session: Session, settlement_id: int) -> SettlementDetailOut: settlement = settlement_repository.get_by_id(session, settlement_id) if settlement is None: raise NotFoundError("月结不存在") return _detail_of(session, settlement) def list_page(session: Session, query: SettlementQuery): from app.core.response import PageResult from app.schemas.settlement import SettlementOut rows = settlement_repository.list_settlements(session, query) total = settlement_repository.count_settlements(session, query) return PageResult[SettlementOut]( records=[SettlementOut.model_validate(r) for r in rows], total=total, page_num=query.page_num, page_size=query.page_size, ) # ---------- 内部 ---------- def _confirm_scope(session: Session, month: str) -> list: """确认回款口径(#8 策略点):本月**单笔回款**的有效台账行(未入结)。 calc_type 必须限定单笔(1):聚合行(2)也落同月台账、也会被回填 settlement_id,不排除会在重生成时与新聚合行重复计入。 """ from app.models import PerformanceLedger return list( session.execute( select(PerformanceLedger) .where( PerformanceLedger.settlement_month == month, PerformanceLedger.calc_type == CALC_TYPE_RECEIPT, PerformanceLedger.is_active == 1, PerformanceLedger.settlement_id.is_(None), ) .order_by(PerformanceLedger.id) ).scalars() ) def _manager_share_params(session: Session, month: str) -> manager_share.ManagerShareParams: """薄委托 manager_share 共享模块:月结聚合与报表部门汇总同源取参。 迁移说明(2026-10-19):实现原样迁至 manager_share,行为零变化; 参数按百分数存储(12.5 表示 12.5%),调用方不得再乘 100。 """ return manager_share.manager_share_params(session, month) def _collect_team_buckets( session: Session, ledger_rows: list, include_manager_personal: bool ) -> dict[int, dict]: """薄委托 manager_share 共享模块:按经理采集团队业绩(行为零变化)。""" return manager_share.collect_team_buckets(session, ledger_rows, include_manager_personal) def _aggregate_row( session: Session, manager, bucket: dict, company_base: Decimal, share: manager_share.ManagerShareParams, ): """单经理聚合台账行(必须落库:月结 ledger_ids 与报表下钻的锚点)。""" from app.models import PerformanceLedger from app.services.probation_service import on_date amount = manager_team_share( bucket["performance"], company_base, team_pct=share.team_pct, company_pct=share.company_pct, ) member_names = "、".join(sorted(bucket["names"])) or "(无成员)" return performance_ledger_repository.add( session, PerformanceLedger( receipt_id=None, calc_version=1, # 聚合行无回款维度,版本恒 1(uk 含 NULL 不去重) calc_type=CALC_TYPE_MANAGER_AGGREGATE, business_id=bucket["business_id"], business_no=bucket["business_no"], personnel_id=manager.id, personnel_name=manager.personnel_name, role_code="sales_manager", customer_type="personal", product_category="mixed", receipt_date=share.param_date, # 比例原文取实际参数(勿硬编码默认值——参数调整后台账说明会失真) rate_text=f"聚合:{share.team_text}%+{share.company_text}%", converted_profit=None, performance_amount=amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), barter_applied=0, payable_amount=amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), settlement_month=share.month, is_probation=int(on_date(manager, share.param_date)), snapshot_json={ "team_base": str(bucket["performance"]), "company_base": str(company_base), "members": member_names, "params_as_of": str(share.param_date), "manager_personal_included": 1 if share.include_manager_personal else 0, # 说明写给业务人员看:说清这一笔的基数是怎么凑出来的 "note": ( f"团队分成:基数{'含' if share.include_manager_personal else '不含'}" "经理本人名下业绩,按本团队与全公司两级比例合计计提" ), }, ), ) def _build_manager_aggregates(session: Session, ledger_rows: list, month: str) -> list: """按经理生成团队聚合行(calc_type=2)并落库。 #12 答复(2026-10-19)参数化:经理个人户业绩是否计入聚合基数由参数 ``manager.personal.included`` 决定(0=不计入默认,1=计入); 经理 = 台账行归属人员经 manager_id 关联的 sales_manager 岗人员。 """ from app.repositories import business_role_repository share = _manager_share_params(session, month) managers = _collect_team_buckets(session, ledger_rows, share.include_manager_personal) # 全公司基数:默认排除经理个人户行(#12 开关计入时不过滤);共享口径单点 company_base = manager_share.company_base_of(ledger_rows, share.include_manager_personal) aggregates: list = [] for manager_id, bucket in managers.items(): manager = session.get(Personnel, manager_id) if manager is None: continue role = ( business_role_repository.get_by_id(session, manager.business_role_id) if manager.business_role_id else None ) frozen_manager = any( (r.snapshot_json or {}).get("engine") == "FIFO" and (r.snapshot_json or {}).get("manager_id") == manager_id for r in ledger_rows ) if not frozen_manager and (role is None or role.role_code != "sales_manager"): continue # 非营销经理的上级不参与聚合 aggregates.append(_aggregate_row(session, manager, bucket, company_base, share)) return aggregates def _month_end(month: str) -> date: """薄委托 manager_share 共享模块:YYYY-MM → 所属月月末(取参锚点)。 保留包装:tests/unit/test_settlement_helpers.py 直接 import 本名字; 「补生成历史月份结果不随操作日漂移」的原因说明见 manager_share.month_end。 """ return manager_share.month_end(month) def _salary_mode_of(session: Session, role_code: str, month: str) -> str | None: from app.services.param_service import resolve_snapshot snapshot = resolve_snapshot(session, _month_end(month)) return snapshot.get("salary.basic.mode", role_code=role_code) or None def _probation_threshold(session: Session, month: str) -> Decimal: from app.services.param_service import parse_fraction, resolve_snapshot snapshot = resolve_snapshot(session, _month_end(month)) from app.services.formula import to_decimal return to_decimal(parse_fraction(snapshot.get("probation.annual.threshold"))) def _detail_of(session: Session, settlement: MonthlySettlement) -> SettlementDetailOut: items = settlement_repository.list_items(session, settlement.id) detail = SettlementDetailOut.model_validate(settlement) from app.schemas.settlement import SettlementItemOut detail.items = [SettlementItemOut.model_validate(i) for i in items] return detail def create_allocation_adjustment(session, payload, *, operator, meta=None): """校验已生成工资的原台账,并将分配差额转换为当月调整单。""" from app.models import PerformanceLedger from app.schemas.settlement import AdjustmentCreateIn from app.services import payroll_months old = session.get(PerformanceLedger, payload.ledger_id) if old is None or old.settlement_id is None: raise BadRequestError("请选择已生成工资的原台账") data = AdjustmentCreateIn( settlement_month=payroll_months.current_month(), personnel_id=payload.personnel_id, direction=payload.direction, amount=payload.amount, reason=payload.reason, source_ledger_id=old.id, ) return create_adjustment(session, data, operator=operator, meta=meta) def create_adjustment( session: Session, payload, *, operator: User, meta: RequestMeta | None = None, ): """新建调整单:开关分流(关=提交即生效留痕;开=待审批)。""" from app.core.constants import NO_PREFIX_ADJUSTMENT from app.models import AdjustmentOrder from app.services.no_generator import next_no amount = _adjustment_amount(payload) from app.core.config import get_settings from app.models import PerformanceLedger from app.services import payroll_months source_id = getattr(payload, "source_ledger_id", None) source = session.get(PerformanceLedger, source_id) if source_id else None if source_id and (source is None or source.settlement_id is None): raise BadRequestError("分配调整必须引用已生成工资的原台账") if source_id: from app.models import PayrollInput snapshot = source.snapshot_json or {} if source.calc_type != 1 or source.is_active != 1 or snapshot.get("engine") != "FIFO": raise BadRequestError("分配调整必须引用有效的正式FIFO单笔台账") original = session.get(PayrollInput, snapshot.get("input_id")) if original is None: raise BadRequestError("原台账的归属依据缺失") allowed = {source.personnel_id} latest = session.scalar( select(PayrollInput) .where(PayrollInput.business_id == source.business_id) .order_by(PayrollInput.version.desc()) .limit(1) ) values = (latest or original).data_json business_key = snapshot.get("event", {}).get("business_key") for handover in values.get("facts", {}).get("handovers", []): if handover.get("business_key") == business_key: for field in ("from_personnel_id", "to_personnel_id"): target = ( values.get("maps", {}).get("personnel", {}).get(str(handover.get(field))) ) if target is not None: allowed.add(target) if payload.personnel_id not in allowed: raise BadRequestError("调整人员不属于该业务已登记的归属或交接范围") adjustment_month = ( payroll_months.current_month() if source_id or not get_settings().payroll_allow_legacy_calculation else payload.settlement_month ) person = session.get(Personnel, payload.personnel_id) if person is None: raise BadRequestError("调整人员不存在") with audit_service.audit( session, operator=operator, record=AuditRecord( module=MODULE_SETTLEMENT, action="新建调整单", summary=f"调整单 {payload.direction} {payload.amount} 元({person.personnel_name})", request_data=payload.model_dump(), ), meta=meta, ): order = AdjustmentOrder( order_no=next_no( session, model=AdjustmentOrder, column_name="order_no", prefix=NO_PREFIX_ADJUSTMENT, ), settlement_month=adjustment_month, adjustment_type="ALLOCATION" if source_id else "GENERAL", source_ledger_id=source_id, attribution_json={ "business_id": source.business_id, "manager_id": (source.snapshot_json or {}).get("manager_id"), "original_personnel_id": source.personnel_id, } if source else None, personnel_id=payload.personnel_id, personnel_name=person.personnel_name, direction=payload.direction, amount=amount, reason=payload.reason.strip(), submitter_id=operator.id, submitter_name=operator.username, ) session.add(order) session.flush() if get_settings().payroll_allow_legacy_calculation or ( get_settings().payroll_approval_enabled and approval_service.is_enabled(session, 3) ): _wire_adjustment_approval(session, order, operator) else: from app.models.settlement import ADJUSTMENT_STATUS_EFFECTIVE order.status = ADJUSTMENT_STATUS_EFFECTIVE out = _adjustment_out(order) # 出参在 commit 前固化(audit 约定) session.commit() logger.info("调整单创建 | order=%s | status=%s", out["order_no"], out["status"]) return out def _adjustment_amount(payload) -> Decimal: """金额恒正校验(方向表达补发/扣减)。 列注释即「恒正」约定,负数会让方向语义反转、审批/报表按 direction 汇总出错;格式已由 schema 正则拦截,此处兜底转换 + 语义判定。 """ from decimal import InvalidOperation try: amount = Decimal(payload.amount) except InvalidOperation as exc: raise BadRequestError("调整金额格式非法(非负两位小数)") from exc if amount <= 0: raise BadRequestError("调整金额须大于 0(方向表达补发/扣减)") return amount def _wire_adjustment_approval(session: Session, order, operator: User) -> None: """开关分流与留痕统一交给 submit_order(关=自动通过留痕;开=待财务复核)。 **不得手工插 ApprovalStep**——``approval_steps.order_id`` 外键指向 approval_orders(不是 adjustment_orders),用调整单 id 落步必然外键 失败(历史缺陷:关开关时新建调整单不可用)。 """ from app.models.approval import APPROVAL_STATUS_APPROVED, APPROVAL_TYPE_ADJUSTMENT from app.models.settlement import ( ADJUSTMENT_STATUS_EFFECTIVE, ADJUSTMENT_STATUS_PENDING, ) approval_order = approval_service.submit_order( session, order_type=APPROVAL_TYPE_ADJUSTMENT, ref_id=order.id, refer_no=order.order_no, submitter=operator, snapshot=approval_service.adjustment_content(session, order.id), ) order.status = ( ADJUSTMENT_STATUS_EFFECTIVE if approval_order.status == APPROVAL_STATUS_APPROVED else ADJUSTMENT_STATUS_PENDING ) session.flush() def _adjustment_out(order) -> dict: """调整单出参(commit 前固化;expire_on_commit 关闭下也不依赖后置读取)。""" return { "id": order.id, "order_no": order.order_no, "status": order.status, "personnel_name": order.personnel_name, "amount": str(order.amount), "direction": order.direction, } def list_adjustments(session: Session) -> list[dict]: """调整单列表(月份倒序)。""" from sqlalchemy import select from app.models import AdjustmentOrder rows = ( session.execute(select(AdjustmentOrder).order_by(AdjustmentOrder.id.desc()).limit(200)) .scalars() .all() ) from app.models.settlement import adjustment_label # noqa: F401 return [ { "id": r.id, "order_no": r.order_no, "settlement_month": r.settlement_month, "personnel_name": r.personnel_name, "direction": r.direction, "amount": str(r.amount), "reason": r.reason, "status": r.status, } for r in rows ]