2026-11-01 20:42:53 -05:00
|
|
|
"""正式工资资料、FIFO核算和逐段明细入口。"""
|
|
|
|
|
|
|
|
|
|
from typing import Annotated
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Path, Query
|
2026-11-02 22:20:55 -05:00
|
|
|
from fastapi.responses import Response
|
|
|
|
|
from pydantic import BaseModel, Field
|
2026-11-01 20:42:53 -05:00
|
|
|
|
|
|
|
|
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=["正式工资核算"])
|
|
|
|
|
|
|
|
|
|
|
2026-11-02 22:20:55 -05:00
|
|
|
class ProbationAdjustmentIn(BaseModel):
|
|
|
|
|
personnel_id: int = Field(ge=1)
|
|
|
|
|
year: str = Field(pattern=r"^20\d{2}$")
|
|
|
|
|
fingerprint: str = Field(min_length=64, max_length=64)
|
|
|
|
|
reason: str = Field(min_length=1, max_length=255)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/probation/adjustments")
|
|
|
|
|
def probation_adjustment(payload: ProbationAdjustmentIn, session: DbSession, actor: PayrollWriter, meta: RequestMetaDep):
|
|
|
|
|
from app.services.probation_service import adjustment as apply
|
|
|
|
|
|
|
|
|
|
return ok(apply(session, payload, actor, meta))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/probation")
|
|
|
|
|
def probation_preview(session: DbSession, _: LoginUser, month: str, personnel_id: int | None = None):
|
|
|
|
|
from app.services import probation_service
|
|
|
|
|
|
|
|
|
|
return ok(probation_service.preview(session, month, personnel_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/probation/export")
|
|
|
|
|
def probation_export(session: DbSession, _: LoginUser, month: str):
|
|
|
|
|
from app.services import probation_service
|
|
|
|
|
|
|
|
|
|
return Response(probation_service.export(session, month),
|
|
|
|
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
|
|
|
headers={"Content-Disposition": 'attachment; filename="probation_balances.xlsx"'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/salary-export/{month}")
|
|
|
|
|
def salary_export(month: str, session: DbSession, _: LoginUser):
|
|
|
|
|
from app.core.month import validate_month
|
|
|
|
|
from app.services.salary_completion import export_settlement
|
|
|
|
|
|
|
|
|
|
validate_month(month)
|
|
|
|
|
return Response(export_settlement(session, month),
|
|
|
|
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
|
|
|
headers={"Content-Disposition": 'attachment; filename="monthly_salary.xlsx"'})
|
|
|
|
|
|
|
|
|
|
|
2026-11-01 20:42:53 -05:00
|
|
|
@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,
|
|
|
|
|
):
|
2026-11-01 20:42:55 -05:00
|
|
|
return ok(payroll_months.month_state(session, month))
|
2026-11-01 20:42:53 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/businesses/{business_id}/input")
|
|
|
|
|
def input_detail(business_id: int, session: DbSession, _: LoginUser):
|
2026-11-01 20:42:55 -05:00
|
|
|
return ok(payroll_inputs.detail(session, business_id))
|
2026-11-01 20:42:53 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
):
|
2026-11-01 20:42:55 -05:00
|
|
|
return ok(
|
|
|
|
|
settlement_service.create_allocation_adjustment(session, payload, operator=actor, meta=meta)
|
2026-11-01 20:42:53 -05:00
|
|
|
)
|