补齐源快照、资料预演、正式导入及工资核算的依赖链路。 新增版本化输入、逐段 FIFO、生成锁和结构化当月调整, 并提供默认关闭的周期采集、有限重试及 Windows 托管。 原始客户资料、私有配置及首页改动不纳入本次提交。 BREAKING CHANGE: 升级前需应用新增迁移,并通过 PAYROLL_MAKER_USER_ID 绑定实际工资制作人,否则业务只读。 旧单批次核算默认关闭;历史兼容需显式启用 PAYROLL_ALLOW_LEGACY_CALCULATION。生产启用和真实金额待验收。
109 lines
3.4 KiB
Python
109 lines
3.4 KiB
Python
"""正式工资资料、FIFO核算和逐段明细入口。"""
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Path, Query
|
|
|
|
from app.api.deps import DbSession, LoginUser, PayrollWriter, RequestMetaDep
|
|
from app.core.response import PageQuery, ok
|
|
from app.schemas.payroll import AllocationAdjustmentIn, PayrollMonthIn, PayrollSetupIn
|
|
from app.services import payroll_calculation, payroll_inputs, payroll_months, settlement_service
|
|
|
|
router = APIRouter(prefix="/payroll", tags=["正式工资核算"])
|
|
|
|
|
|
@router.get("/months/{month}")
|
|
def month_state(
|
|
month: Annotated[str, Path(pattern=r"^20[0-9]{2}-(0[1-9]|1[0-2])$")],
|
|
session: DbSession,
|
|
_: LoginUser,
|
|
):
|
|
from sqlalchemy import select
|
|
|
|
from app.core.month import validate_month
|
|
from app.models import MonthlySettlement, PayrollMonth
|
|
|
|
validate_month(month)
|
|
control = session.scalar(select(PayrollMonth).where(PayrollMonth.month == month))
|
|
existing = session.scalar(
|
|
select(MonthlySettlement.id).where(MonthlySettlement.settlement_month == month)
|
|
)
|
|
return ok(
|
|
{
|
|
"month": month,
|
|
"allocation_locked": bool(existing or (control and control.generated_at)),
|
|
"maker_name": control.maker_name if control else None,
|
|
}
|
|
)
|
|
|
|
|
|
@router.get("/businesses/{business_id}/input")
|
|
def input_detail(business_id: int, session: DbSession, _: LoginUser):
|
|
record = payroll_calculation.latest_input(session, business_id)
|
|
return ok(
|
|
None
|
|
if record is None
|
|
else {
|
|
"id": record.id,
|
|
"version": record.version,
|
|
"source": record.source_database,
|
|
"fingerprint": record.fingerprint,
|
|
"data": record.data_json,
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/businesses/{business_id}/input")
|
|
def save_input(
|
|
business_id: int,
|
|
payload: PayrollSetupIn,
|
|
session: DbSession,
|
|
actor: PayrollWriter,
|
|
meta: RequestMetaDep,
|
|
):
|
|
return ok(payroll_inputs.setup(session, business_id, payload, actor, meta))
|
|
|
|
|
|
@router.post("/businesses/{business_id}/calculate")
|
|
def calculate(
|
|
business_id: int,
|
|
payload: PayrollMonthIn,
|
|
session: DbSession,
|
|
actor: PayrollWriter,
|
|
meta: RequestMetaDep,
|
|
):
|
|
return ok(
|
|
payroll_calculation.calculate(
|
|
session, business_id, payload.month, operator=actor, meta=meta, reason=payload.reason
|
|
)
|
|
)
|
|
|
|
|
|
@router.get("/ledger/{ledger_id}/allocations")
|
|
def allocations(
|
|
ledger_id: int, query: Annotated[PageQuery, Query()], session: DbSession, _: LoginUser
|
|
):
|
|
return ok(payroll_calculation.allocation_page(session, ledger_id, query))
|
|
|
|
|
|
@router.post("/allocation-adjustments")
|
|
def adjustment(
|
|
payload: AllocationAdjustmentIn, session: DbSession, actor: PayrollWriter, meta: RequestMetaDep
|
|
):
|
|
from app.core.exceptions import BadRequestError
|
|
from app.models import PerformanceLedger
|
|
from app.schemas.settlement import AdjustmentCreateIn
|
|
|
|
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 ok(settlement_service.create_adjustment(session, data, operator=actor, meta=meta))
|