feat(web): стабилизация UI, Gemini-скроллинг, роутеры контекста/снапшотов и актуализация роадмапа
This commit is contained in:
+21
-20
@@ -2,6 +2,7 @@
|
||||
===============================================================================
|
||||
FILE: services/scud_etl/merger.py
|
||||
ROLE: Агрегация реестров, выбор совместителей 1С по отделу СКУД и авто-связки.
|
||||
Распределение исключений в общий рабочий пул при отсутствии справок.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
@@ -17,10 +18,8 @@ logger = logging.getLogger("SCUD_MERGER")
|
||||
|
||||
|
||||
def load_identity_mappings() -> Dict[str, str]:
|
||||
"""Загружает подтвержденные сопоставления ФИО (СКУД -> 1С:ЗУП) из SQLite."""
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
# Автоматическая инициализация таблицы при первом обращении
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS person_identity_mapping (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -44,7 +43,6 @@ def load_identity_mappings() -> Dict[str, str]:
|
||||
|
||||
|
||||
def aggregate_scud_by_person(df_scud: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Схлопывает дубликаты пропусков одного человека в СКУД."""
|
||||
if df_scud is None or df_scud.empty:
|
||||
return df_scud
|
||||
|
||||
@@ -53,7 +51,6 @@ def aggregate_scud_by_person(df_scud: pd.DataFrame) -> pd.DataFrame:
|
||||
fio_col = 'Сотрудник' if 'Сотрудник' in df.columns else 'ФИО'
|
||||
df['fio_clean'] = df[fio_col].apply(normalize_fio)
|
||||
|
||||
# Применяем сохраненный кэш связок ФИО
|
||||
mapping_dict = load_identity_mappings()
|
||||
if mapping_dict:
|
||||
df['fio_clean'] = df['fio_clean'].apply(lambda f: mapping_dict.get(f, f))
|
||||
@@ -95,10 +92,6 @@ def aggregate_scud_by_person(df_scud: pd.DataFrame) -> pd.DataFrame:
|
||||
|
||||
|
||||
def select_best_zup_position(df_staff_1c: pd.DataFrame, df_scud_agg: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
⭐️ Для совместителей с несколькими должностями в 1С:ЗУП
|
||||
выбирает ту ставку, которая соответствует отделу физического нахождения по СКУД.
|
||||
"""
|
||||
if df_staff_1c is None or df_staff_1c.empty:
|
||||
return pd.DataFrame()
|
||||
|
||||
@@ -210,15 +203,20 @@ def merge_scud_and_1c(
|
||||
fio = row.get("fio_clean", "")
|
||||
has_official_absence = pd.notna(row.get("Вид_отсутствия")) and str(row.get("Вид_отсутствия")).strip() not in ["", "nan", "None", "Исключение"]
|
||||
|
||||
if fio in whitelist_fios or has_official_absence:
|
||||
if fio in whitelist_fios:
|
||||
df_res.at[idx, "is_excluded"] = False
|
||||
continue
|
||||
|
||||
dep = str(row.get("Подразделение", "")).upper()
|
||||
pos = str(row.get("Должность", "")).lower()
|
||||
|
||||
if fio in exc_fios or dep in exc_depts or any(d in dep for d in exc_depts) or pos in exc_pos or any(k in pos for k in pos_kw):
|
||||
df_res.at[idx, "is_excluded"] = True
|
||||
is_match_exc = (fio in exc_fios or dep in exc_depts or any(d in dep for d in exc_depts) or pos in exc_pos or any(k in pos for k in pos_kw))
|
||||
|
||||
if is_match_exc:
|
||||
if has_official_absence:
|
||||
df_res.at[idx, "is_excluded"] = False
|
||||
else:
|
||||
df_res.at[idx, "is_excluded"] = True
|
||||
|
||||
mask_exc = (df_res["is_excluded"] == True) & (df_res["Вид_отсутствия"].isna() | (df_res["Вид_отсутствия"] == ""))
|
||||
df_res.loc[mask_exc, "Вид_отсутствия"] = "Исключение"
|
||||
@@ -228,21 +226,24 @@ def merge_scud_and_1c(
|
||||
|
||||
|
||||
def calculate_summary_metrics(df_merged: pd.DataFrame) -> Dict[str, Any]:
|
||||
active_df = df_merged[df_merged.get("is_excluded", False) == False].copy()
|
||||
total_staff = len(active_df)
|
||||
total_staff = len(df_merged)
|
||||
|
||||
came_to_office_mask = active_df["Начало_дня"].astype(str).str.strip().ne("Нет входа")
|
||||
working_in_office = active_df[came_to_office_mask]
|
||||
working_in_office_count = len(working_in_office)
|
||||
came_to_office_mask = (df_merged["Начало_дня"].astype(str).str.strip().ne("Нет входа")) & (df_merged.get("is_excluded", False) == False)
|
||||
exc_without_doc_mask = (df_merged.get("is_excluded", False) == True) & (
|
||||
df_merged["Вид_отсутствия"].isna() |
|
||||
df_merged["Вид_отсутствия"].astype(str).str.strip().isin(["", "nan", "Исключение"])
|
||||
)
|
||||
|
||||
df_not_came = active_df[~came_to_office_mask]
|
||||
working_in_office_count = len(df_merged[came_to_office_mask | exc_without_doc_mask])
|
||||
|
||||
reason_series = df_not_came["причина отсутствия"].astype(str).str.lower()
|
||||
df_not_working = df_merged[~came_to_office_mask & ~exc_without_doc_mask]
|
||||
|
||||
reason_series = df_not_working["причина отсутствия"].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 = df_not_working[is_remote_mask]
|
||||
remote_home_count = len(remote_home)
|
||||
|
||||
df_remaining_absent = df_not_came[~is_remote_mask]
|
||||
df_remaining_absent = df_not_working[~is_remote_mask]
|
||||
has_doc_mask = df_remaining_absent["причина отсутствия"].notna() & \
|
||||
df_remaining_absent["причина отсутствия"].ne("") & \
|
||||
df_remaining_absent["причина отсутствия"].ne("nan") & \
|
||||
|
||||
Reference in New Issue
Block a user