112 lines
5.6 KiB
Python
112 lines
5.6 KiB
Python
"""
|
|
===============================================================================
|
|
FILE: services/scud_etl/merger.py
|
|
PROJECT: SCUD Orion AI (Unified Architecture)
|
|
MODULE: services / scud_etl
|
|
ROLE: Агрегация реестров СКУД и 1С, наложение исключений, нормализация кодов
|
|
подразделений и расчет сходящегося баланса присутствия.
|
|
===============================================================================
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Any, List
|
|
import pandas as pd
|
|
|
|
# Используем правильные имена функций из services/knowledge/service.py
|
|
from services.knowledge.service import get_department_synonyms_dict, get_rules
|
|
|
|
logger = logging.getLogger("SCUD_MERGER")
|
|
|
|
|
|
def merge_scud_and_1c(
|
|
df_scud: pd.DataFrame,
|
|
df_staff_1c: pd.DataFrame,
|
|
df_absences_1c: pd.DataFrame
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Объединяет реестры СКУД и 1С:ЗУП:
|
|
- Применяет исключения (уборщики, ОВК и др.).
|
|
- Устанавливает короткие аббревиатуры подразделений (КО, ОАН, РУК и др.).
|
|
- Привязывает кадровые документы отсутствий из 1С.
|
|
"""
|
|
# Получаем словарь синонимов
|
|
synonyms = get_department_synonyms_dict()
|
|
|
|
df_res = df_scud.copy()
|
|
if "Сотрудник" in df_res.columns:
|
|
df_res["fio_clean"] = df_res["Сотрудник"].astype(str).str.strip()
|
|
|
|
# 1. Фильтрация системных исключений
|
|
# Загружаем исключения из базы знаний или задаем системный фильтр
|
|
excluded_depts = {"ОВК", "Отдел вневедомственного контроля", "Служба уборки", "Клининг"}
|
|
excluded_positions = {"Уборщик", "Уборщица", "Дворник"}
|
|
|
|
if not df_res.empty:
|
|
if "Подразделение" in df_res.columns:
|
|
df_res = df_res[~df_res["Подразделение"].astype(str).isin(excluded_depts)]
|
|
if "Должность" in df_res.columns:
|
|
df_res = df_res[~df_res["Должность"].astype(str).isin(excluded_positions)]
|
|
|
|
# 2. Трансляция подразделений в короткие аббревиатуры СКУД
|
|
if "Подразделение" in df_res.columns:
|
|
df_res["Подразделение"] = df_res["Подразделение"].apply(
|
|
lambda d: synonyms.get(d, d)
|
|
)
|
|
|
|
# 3. Привязка документов отсутствий из 1С
|
|
absences_map = {}
|
|
if not df_absences_1c.empty and "Сотрудник" in df_absences_1c.columns and "Причина" in df_absences_1c.columns:
|
|
for _, row in df_absences_1c.iterrows():
|
|
fio = str(row["Сотрудник"]).strip()
|
|
reason = str(row["Причина"]).strip()
|
|
absences_map[fio] = reason
|
|
|
|
df_res["причина отсутствия"] = df_res["fio_clean"].map(absences_map)
|
|
|
|
return df_res
|
|
|
|
|
|
def calculate_summary_metrics(df_merged: pd.DataFrame) -> Dict[str, Any]:
|
|
"""
|
|
Расчет строго сходящегося баланса присутствия:
|
|
A (Итого на работе) + B (Удаленная работа) + C (Официально отсутствуют) + D (Неизвестно) = N (Всего)
|
|
"""
|
|
total_staff = len(df_merged)
|
|
|
|
# 1. Все, кто физически пришел в офис по СКУД (включая удаленщиков, пришедших в офис)
|
|
came_to_office_mask = df_merged["Начало_дня"].astype(str).str.strip().ne("Нет входа")
|
|
working_in_office = df_merged[came_to_office_mask]
|
|
working_in_office_count = len(working_in_office)
|
|
|
|
# 2. Все, кто сегодня НЕ пришел в офис
|
|
not_came_mask = ~came_to_office_mask
|
|
df_not_came = df_merged[not_came_mask]
|
|
|
|
# 3. Из непришедших выделяем удаленщиков (работают из дома)
|
|
reason_series = df_not_came["причина отсутствия"].astype(str).str.lower()
|
|
is_remote_mask = reason_series.str.contains("удален|дистанцион", regex=True, na=False)
|
|
|
|
remote_home = df_not_came[is_remote_mask]
|
|
remote_home_count = len(remote_home)
|
|
|
|
# 4. Из оставшихся непришедших выделяем официальные отсутствия (отпуск, больничный, командировка и т.д.)
|
|
df_remaining_absent = df_not_came[~is_remote_mask]
|
|
has_doc_mask = df_remaining_absent["причина отсутствия"].notna() & \
|
|
df_remaining_absent["причина отсутствия"].ne("") & \
|
|
df_remaining_absent["причина отсутствия"].ne("nan")
|
|
|
|
official_absent = df_remaining_absent[has_doc_mask]
|
|
official_absent_count = len(official_absent)
|
|
|
|
# 5. Оставшиеся непришедшие без документов — истинно неизвестные
|
|
unknown = df_remaining_absent[~has_doc_mask]
|
|
unknown_count = len(unknown)
|
|
|
|
return {
|
|
"total_staff": total_staff,
|
|
"working_in_office_count": working_in_office_count,
|
|
"remote_home_count": remote_home_count,
|
|
"official_absent_count": official_absent_count,
|
|
"unknown_count": unknown_count,
|
|
"unknown_list": unknown[["fio_clean", "Подразделение", "Должность"]].to_dict(orient="records") if not unknown.empty else []
|
|
} |