""" =============================================================================== FILE: services/scud_etl/anomaly_detector.py PROJECT: SCUD Orion AI (Unified Architecture) MODULE: services / scud_etl ROLE: Автоматическое выявление аномалий и конфликтов между 1С:ЗУП и СКУД. AI-CONTEXT-ANCHORS: - ANCHOR[ANOMALY_DETECTOR_CORE]: Проверка физического присутствия в отпуске и перемещений без входа. =============================================================================== """ import pandas as pd from typing import List, Dict, Any, Optional ALLOWED_WORK_TRIP_KEYWORDS = ['командировк', 'разъездн', 'поездк'] # ANCHOR[ANOMALY_DETECTOR_CORE] def detect_all_anomalies( merged_df: pd.DataFrame, static_reasons_dict: dict, kb_rules: List[str], scud_fios_set: Optional[set] = None ) -> List[Dict[str, Any]]: """Выявляет конфликты и аномалии между источниками СКУД и 1С.""" anomalies = [] kb_rules_text = " ".join(kb_rules).lower() if kb_rules else "" for idx, row in merged_df.iterrows(): fio = row.get('Сотрудник', row.get('fio_clean', '')) fio_clean = row.get('fio_clean', '') is_present = row.get('Пришел', False) is_exc = row.get('is_excluded', False) reason_1c = str(row.get('Вид_отсутствия', '')).strip() has_1c_reason = pd.notna(row.get('Вид_отсутствия')) and reason_1c != '' and not reason_1c.startswith('Исключение') anom_flag = row.get('anomaly_flag', 'NONE') is_fio_whitelisted = fio_clean.lower() in kb_rules_text # 1. Присутствие при официальном отсутствии if is_present and has_1c_reason: is_allowed_trip = any(kw in reason_1c.lower() for kw in ALLOWED_WORK_TRIP_KEYWORDS) if not is_allowed_trip and not is_fio_whitelisted: anomalies.append({ "type": "ФИЗИЧЕСКОЕ ПРИСУТСТВИЕ ПРИ ОФИЦИАЛЬНОМ ОТСУТСТВИИ", "fio": fio, "details": f"Сотрудник пришел по СКУД, но в 1С оформлен документ: '{reason_1c}'" }) if is_exc and not has_1c_reason: continue # 2. Перемещение внутри здания без отметки входа на КПП if anom_flag == 'ANOMALY_NO_IN_HAS_ACTIVITY': first_act = row.get('Первая_активность', '—') anomalies.append({ "type": "АНОМАЛИЯ СКУД: ПЕРЕМЕЩЕНИЕ БЕЗ ВХОДА", "fio": fio, "details": f"Отсутствует регистрация входа на КПП при зафиксированной первой активности в {first_act}" }) # 3. Сотрудник в штате 1С, но карты/профиля в СКУД нет if scud_fios_set is not None: if fio_clean not in scud_fios_set and not has_1c_reason: anomalies.append({ "type": "АНОМАЛИЯ УЧЕТА: СОТРУДНИК ОТСУТСТВУЕТ В СКУД ОРИОН PRO", "fio": fio, "details": f"Сотрудник числится в Штатном расписании 1С ({row.get('Подразделение', '—')}), но отсутствует в СКУД" }) return anomalies