forked from jiaoly/financial_system
229 lines
8.7 KiB
Python
229 lines
8.7 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
from dataclasses import dataclass
|
||
|
||
from ..core.config import AppConfig, AttendanceRules, PayrollRules
|
||
from ..core.logger import AppLogger
|
||
from ..database.orm import SalaryConfigORM
|
||
from ..repositories import SalaryConfigRepository
|
||
|
||
logger = AppLogger.get_logger(__name__)
|
||
|
||
|
||
DEFAULT_CONFIGS = [
|
||
{
|
||
"config_key": "attendance.on_work_time",
|
||
"config_name": "上班时间",
|
||
"config_group": "attendance",
|
||
"config_value": "08:30",
|
||
"value_type": "string",
|
||
"remark": "用于迟到识别,格式HH:MM",
|
||
},
|
||
{
|
||
"config_key": "attendance.off_work_time",
|
||
"config_name": "下班时间",
|
||
"config_group": "attendance",
|
||
"config_value": "17:00",
|
||
"value_type": "string",
|
||
"remark": "加班按下班打卡超过该时间后向下取整",
|
||
},
|
||
{
|
||
"config_key": "attendance.overtime_round_minutes",
|
||
"config_name": "加班取整分钟",
|
||
"config_group": "attendance",
|
||
"config_value": "60",
|
||
"value_type": "integer",
|
||
"remark": "例如60表示20:30按3小时计算",
|
||
},
|
||
{
|
||
"config_key": "attendance.standard_daily_hours",
|
||
"config_name": "标准日工时",
|
||
"config_group": "attendance",
|
||
"config_value": "8",
|
||
"value_type": "number",
|
||
"remark": "计时工资按出勤天数折算工时",
|
||
},
|
||
{
|
||
"config_key": "attendance.minor_late_minutes",
|
||
"config_name": "轻微迟到分钟",
|
||
"config_group": "attendance",
|
||
"config_value": "5",
|
||
"value_type": "integer",
|
||
"remark": "小于等于该分钟数按轻微迟到统计",
|
||
},
|
||
{
|
||
"config_key": "attendance.minor_late_free_times",
|
||
"config_name": "轻微迟到免扣次数",
|
||
"config_group": "attendance",
|
||
"config_value": "3",
|
||
"value_type": "integer",
|
||
"remark": "5分钟内迟到3次内合格,超过后按次扣款",
|
||
},
|
||
{
|
||
"config_key": "attendance.late_penalty",
|
||
"config_name": "迟到扣款",
|
||
"config_group": "attendance",
|
||
"config_value": "30",
|
||
"value_type": "number",
|
||
"remark": "每次计扣迟到扣款金额",
|
||
},
|
||
{
|
||
"config_key": "attendance.missing_card_penalty",
|
||
"config_name": "缺卡扣款",
|
||
"config_group": "attendance",
|
||
"config_value": "20",
|
||
"value_type": "number",
|
||
"remark": "每次缺卡扣款金额",
|
||
},
|
||
{
|
||
"config_key": "attendance.weekend_days",
|
||
"config_name": "周末星期",
|
||
"config_group": "attendance",
|
||
"config_value": "[5, 6]",
|
||
"value_type": "json",
|
||
"remark": "Python weekday:周一0,周六5,周日6",
|
||
},
|
||
{
|
||
"config_key": "attendance.legal_holidays",
|
||
"config_name": "法定节假日",
|
||
"config_group": "attendance",
|
||
"config_value": "[]",
|
||
"value_type": "json",
|
||
"remark": "日期数组,例如[\"2026-10-01\"]",
|
||
},
|
||
{
|
||
"config_key": "attendance.leave_keywords",
|
||
"config_name": "请假关键字",
|
||
"config_group": "attendance",
|
||
"config_value": json.dumps(list(AttendanceRules.leave_keywords), ensure_ascii=False),
|
||
"value_type": "json",
|
||
"remark": "从钉钉单元格识别请假/调休类型",
|
||
},
|
||
{
|
||
"config_key": "payroll.default_overtime_rate",
|
||
"config_name": "工作日加班单价",
|
||
"config_group": "payroll",
|
||
"config_value": "0",
|
||
"value_type": "number",
|
||
"remark": "未维护员工单价时使用",
|
||
},
|
||
{
|
||
"config_key": "payroll.default_weekend_overtime_rate",
|
||
"config_name": "周末加班单价",
|
||
"config_group": "payroll",
|
||
"config_value": "0",
|
||
"value_type": "number",
|
||
"remark": "未维护员工周末单价时使用",
|
||
},
|
||
{
|
||
"config_key": "payroll.default_holiday_overtime_rate",
|
||
"config_name": "节假日加班单价",
|
||
"config_group": "payroll",
|
||
"config_value": "0",
|
||
"value_type": "number",
|
||
"remark": "未维护员工节假日单价时使用",
|
||
},
|
||
]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SalaryConfigUpdate:
|
||
config_name: str
|
||
config_group: str
|
||
config_value: str
|
||
value_type: str
|
||
is_enabled: bool
|
||
remark: str
|
||
|
||
|
||
class SalaryConfigService:
|
||
"""系统配置服务:维护规则配置,并把数据库配置合成为 AppConfig。"""
|
||
|
||
def __init__(self, repository: SalaryConfigRepository):
|
||
self.repository = repository
|
||
|
||
def ensure_defaults(self) -> int:
|
||
created = self.repository.create_defaults(DEFAULT_CONFIGS)
|
||
logger.info("系统默认配置初始化完成 created=%s", created)
|
||
return created
|
||
|
||
def list_configs(self, *, config_group: str | None = None) -> list[SalaryConfigORM]:
|
||
configs = self.repository.list_configs(config_group=_clean_optional(config_group))
|
||
logger.info("查询系统配置成功 count=%s group=%s", len(configs), config_group)
|
||
return configs
|
||
|
||
def update_config(self, config_key: str, values: SalaryConfigUpdate) -> SalaryConfigORM:
|
||
self._parse_value(values.config_value, values.value_type)
|
||
config = self.repository.upsert_config(
|
||
config_key=config_key,
|
||
config_name=values.config_name.strip(),
|
||
config_group=values.config_group.strip(),
|
||
config_value=values.config_value.strip(),
|
||
value_type=values.value_type.strip(),
|
||
is_enabled=values.is_enabled,
|
||
remark=values.remark.strip(),
|
||
)
|
||
logger.info("更新系统配置成功 config_key=%s", config_key)
|
||
return config
|
||
|
||
def resolve_app_config(self, base_config: AppConfig) -> AppConfig:
|
||
values = {
|
||
config.config_key: self._parse_value(config.config_value, config.value_type)
|
||
for config in self.repository.list_configs()
|
||
if config.is_enabled
|
||
}
|
||
attendance = AttendanceRules(
|
||
on_work_time=str(values.get("attendance.on_work_time", base_config.attendance.on_work_time)),
|
||
off_work_time=str(values.get("attendance.off_work_time", base_config.attendance.off_work_time)),
|
||
overtime_round_minutes=int(
|
||
values.get("attendance.overtime_round_minutes", base_config.attendance.overtime_round_minutes)
|
||
),
|
||
standard_daily_hours=float(
|
||
values.get("attendance.standard_daily_hours", base_config.attendance.standard_daily_hours)
|
||
),
|
||
minor_late_minutes=int(values.get("attendance.minor_late_minutes", base_config.attendance.minor_late_minutes)),
|
||
minor_late_free_times=int(
|
||
values.get("attendance.minor_late_free_times", base_config.attendance.minor_late_free_times)
|
||
),
|
||
late_penalty=float(values.get("attendance.late_penalty", base_config.attendance.late_penalty)),
|
||
missing_card_penalty=float(
|
||
values.get("attendance.missing_card_penalty", base_config.attendance.missing_card_penalty)
|
||
),
|
||
weekend_days=tuple(int(item) for item in values.get("attendance.weekend_days", base_config.attendance.weekend_days)),
|
||
legal_holidays=tuple(str(item) for item in values.get("attendance.legal_holidays", base_config.attendance.legal_holidays)),
|
||
leave_keywords=tuple(str(item) for item in values.get("attendance.leave_keywords", base_config.attendance.leave_keywords)),
|
||
)
|
||
payroll = PayrollRules(
|
||
default_overtime_rate=float(
|
||
values.get("payroll.default_overtime_rate", base_config.payroll.default_overtime_rate)
|
||
),
|
||
default_weekend_overtime_rate=float(
|
||
values.get("payroll.default_weekend_overtime_rate", base_config.payroll.default_weekend_overtime_rate)
|
||
),
|
||
default_holiday_overtime_rate=float(
|
||
values.get("payroll.default_holiday_overtime_rate", base_config.payroll.default_holiday_overtime_rate)
|
||
),
|
||
employees=base_config.payroll.employees,
|
||
)
|
||
logger.info("已合并数据库规则配置到运行时计算配置")
|
||
return AppConfig(attendance=attendance, payroll=payroll)
|
||
|
||
def _parse_value(self, raw_value: str, value_type: str):
|
||
if value_type == "integer":
|
||
return int(raw_value)
|
||
if value_type == "number":
|
||
return float(raw_value)
|
||
if value_type == "boolean":
|
||
return str(raw_value).lower() in {"1", "true", "yes", "on"}
|
||
if value_type == "json":
|
||
return json.loads(raw_value or "null")
|
||
return raw_value
|
||
|
||
|
||
def _clean_optional(value: str | None) -> str | None:
|
||
if value is None:
|
||
return None
|
||
stripped = value.strip()
|
||
return stripped or None
|