按 2026-11-02 需求补齐计划交付后端能力: - 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝; 管理员判断改为按数据库角色重新判定,防止权限码持有者绕过 - 系统监控:单进程采集资源、请求与异常指标,分钟级聚合, 阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义 - 试用期计提:按回款日判定并写入快照,年度结清与调整单联动; 人员模型补试用期、离职日期与工资版本历史字段 - 计提模板:公式骨架受控,页面维护名称与参数绑定 - T+ 采集:自动化配置表与进度查询 内置角色禁停用保护对齐模块注释;核算规则版本提为常量并 补充批量核算失败堆栈日志。 影响范围:后端全部模块、数据库迁移 V22-V28 验证:uv run ruff check 通过;922 个单元测试全部通过; 集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
85 lines
2.7 KiB
Python
85 lines
2.7 KiB
Python
"""人员工资与任职资料按生效日期留存,生成历史工资不读后来变更的标准。"""
|
|
|
|
from datetime import date
|
|
from types import SimpleNamespace
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.core.exceptions import BadRequestError
|
|
from app.models import MonthlySettlement
|
|
|
|
FIELDS = (
|
|
"salary_standard",
|
|
"business_role_id",
|
|
"hire_date",
|
|
"resignation_date",
|
|
"probation_start_date",
|
|
"regularization_date",
|
|
"is_probation",
|
|
)
|
|
|
|
|
|
def capture(person, effective):
|
|
return {
|
|
"status": str(person.status),
|
|
"effective_date": str(effective),
|
|
**{
|
|
key: str(getattr(person, key)) if getattr(person, key) is not None else None
|
|
for key in FIELDS
|
|
},
|
|
}
|
|
|
|
|
|
def begin_update(session, person, effective):
|
|
if session.scalar(
|
|
select(MonthlySettlement.id).where(
|
|
MonthlySettlement.settlement_month >= effective.strftime("%Y-%m")
|
|
)
|
|
):
|
|
raise BadRequestError(
|
|
"生效月份或后续月份已生成工资,请使用未来生效日期;历史差额走当月调整"
|
|
)
|
|
history = list(person.salary_history_json or [])
|
|
if not history:
|
|
baseline = person.hire_date or person.create_time.date()
|
|
history.append(capture(person, baseline))
|
|
return history
|
|
|
|
|
|
def save_version(person, effective, history=None):
|
|
entries = list(history or person.salary_history_json or [])
|
|
entries.append(capture(person, effective))
|
|
person.salary_history_json = entries
|
|
|
|
|
|
def resolve(person, day):
|
|
entries = [
|
|
entry for entry in (person.salary_history_json or []) if entry["effective_date"] <= str(day)
|
|
]
|
|
values = {key: getattr(person, key) for key in FIELDS}
|
|
values["status"] = person.status
|
|
if person.salary_history_json and not entries:
|
|
values["salary_standard"] = None
|
|
elif entries:
|
|
selected = max(enumerate(entries), key=lambda pair: (pair[1]["effective_date"], pair[0]))[1]
|
|
values["status"] = int(selected.get("status", "1"))
|
|
for key in FIELDS:
|
|
raw = selected.get(key)
|
|
if key.endswith("date"):
|
|
values[key] = date.fromisoformat(raw) if raw else None
|
|
elif key in ("business_role_id", "is_probation"):
|
|
values[key] = int(raw) if raw else 0
|
|
elif key == "salary_standard":
|
|
from decimal import Decimal
|
|
|
|
values[key] = Decimal(raw) if raw is not None else None
|
|
return SimpleNamespace(**values)
|
|
|
|
|
|
def employed(person, profile, day):
|
|
if profile.hire_date and profile.hire_date > day:
|
|
return False
|
|
if profile.resignation_date:
|
|
return day < profile.resignation_date
|
|
return profile.status == 1
|