PayrollManagementSystem/backend/app/services/payroll_months.py

69 lines
2.8 KiB
Python
Raw Normal View History

"""月份行锁同时保护工资生成与分配事实修改。"""
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy.dialects.mysql import insert
from app.core.exceptions import BadRequestError, ForbiddenError
from app.core.month import validate_month
from app.models import MonthlySettlement, PayrollMonth
from app.services.payroll_access import ensure_payroll_writer
def current_month() -> str:
return datetime.now(timezone(timedelta(hours=8))).strftime("%Y-%m")
def month_state(session, month):
"""查询生成锁及制作人,兼容升级前已存在的月结记录。"""
validate_month(month)
control = session.scalar(select(PayrollMonth).where(PayrollMonth.month == month))
existing = session.scalar(
select(MonthlySettlement.id).where(MonthlySettlement.settlement_month == month)
)
return {
"month": month,
"allocation_locked": bool(existing or (control and control.generated_at)),
"maker_name": control.maker_name if control else None,
}
def lock_month_row(session, month, operator):
"""以月份唯一键串行化首次建行及后续操作,不改变已有制作人。"""
validate_month(month)
values = {"month": month, "maker_id": operator.id, "maker_name": operator.username}
if session.bind.dialect.name == "mysql":
stmt = insert(PayrollMonth).values(**values)
session.execute(stmt.on_duplicate_key_update(month=stmt.inserted.month))
elif session.scalar(select(PayrollMonth.id).where(PayrollMonth.month == month)) is None:
session.add(PayrollMonth(**values))
session.flush()
control = session.scalar(
select(PayrollMonth).where(PayrollMonth.month == month).with_for_update()
.execution_options(populate_existing=True)
)
return control
def lock_month(session, month, operator, *, require_maker=True):
if require_maker:
ensure_payroll_writer(operator)
control = lock_month_row(session, month, operator)
if control.generated_at is not None and control.maker_id != operator.id:
raise ForbiddenError("该月工资由其他制作人生成,不可代为修改或重新生成")
if control.generated_at is None:
control.maker_id, control.maker_name = operator.id, operator.username
return control
def ensure_allocation_editable(session, month, operator, *, require_maker=True):
control = lock_month(session, month, operator, require_maker=require_maker)
# 兼容升级前已有月结;没有控制行不代表历史工资可以修改。
old = session.scalar(
select(MonthlySettlement.id).where(MonthlySettlement.settlement_month == month)
)
if control.generated_at is not None or old is not None:
raise BadRequestError("该月工资已生成,二次分配已锁定;差额请走当月调整单")
return control