"""绩效核算接口(M3 ④):台账查询与单笔核算触发。 核算动作 = 把回款固化为台账行(每笔回款仅一次;重算走独立入口并授权)。 准入:读=登录即可;写(核算/重算/批量)=管理员与营销后勤 (权限清单待澄清 #6,答复后细化)。 """ from typing import Annotated from fastapi import APIRouter, Depends, Path, Query from pydantic import BaseModel, Field from app.api.deps import DbSession, RequestMetaDep, require_roles from app.core.constants import ROLE_ADMIN, ROLE_FINANCE, ROLE_MARKETING_OPS from app.core.response import ApiResponse, PageResult, ok from app.models import User from app.schemas.calc import CalcReceiptOut, LedgerOut, LedgerQuery from app.services import calc_service router = APIRouter(prefix="/calc", tags=["绩效工资 · 绩效核算"]) _CalcWriter = Annotated[User, Depends(require_roles(ROLE_ADMIN, ROLE_MARKETING_OPS))] # 重算是授权动作:管理员/财务(营销后勤不可重算) _RecalcUser = Annotated[User, Depends(require_roles(ROLE_ADMIN, ROLE_FINANCE))] @router.get("/ledger", response_model=ApiResponse[PageResult[LedgerOut]], summary="绩效台账") def list_ledger( session: DbSession, query: Annotated[LedgerQuery, Query()], ) -> ApiResponse[PageResult[LedgerOut]]: """分页查询绩效核算台账(默认仅生效行,可筛选历史版本)。""" return ok(calc_service.list_ledger_page(session, query)) @router.get("/ledger/{ledger_id}", response_model=ApiResponse[LedgerOut], summary="台账详情") def get_ledger( session: DbSession, ledger_id: Annotated[int, Path(ge=1)], ) -> ApiResponse[LedgerOut]: """台账行详情(含计算快照)。""" return ok(calc_service.get_ledger(session, ledger_id)) @router.post( "/receipts/{receipt_id}/calc", response_model=ApiResponse[CalcReceiptOut], summary="核算回款", ) def calc_receipt( receipt_id: Annotated[int, Path(ge=1)], session: DbSession, current_user: _CalcWriter, meta: RequestMetaDep, ) -> ApiResponse[CalcReceiptOut]: """把回款固化为台账行(每笔回款仅一次;业务级一次性计提,重复回款仅登记进度)。""" return ok( calc_service.calc_receipt(session, receipt_id, operator=current_user, meta=meta), message="核算完成", ) class RecalcIn(BaseModel): """重算入参:原因必填(重算授权留痕)。""" reason: str = Field(min_length=1, max_length=255, description="重算原因") class BatchIn(BaseModel): """批量核算入参。""" receipt_ids: list[int] = Field(min_length=1, description="回款 ID 列表") @router.post( "/receipts/{receipt_id}/recalc", response_model=ApiResponse[CalcReceiptOut], summary="重算回款", ) def recalc_receipt( receipt_id: Annotated[int, Path(ge=1)], payload: RecalcIn, session: DbSession, current_user: _RecalcUser, meta: RequestMetaDep, ) -> ApiResponse[CalcReceiptOut]: """重算(管理员/财务):作废旧版本、按当前参数落新版本并留痕。""" return ok( calc_service.recalc_receipt( session, receipt_id, payload.reason, operator=current_user, meta=meta ), message="重算完成", ) @router.post("/batch", response_model=ApiResponse[list[dict]], summary="批量核算") def batch_calc( payload: BatchIn, session: DbSession, current_user: _CalcWriter, meta: RequestMetaDep, ) -> ApiResponse[list[dict]]: """批量核算:逐笔独立事务,返回逐笔成败清单(失败不中断其它笔)。""" return ok( calc_service.batch_calc(session, payload.receipt_ids, operator=current_user, meta=meta), message="批量核算完成", )