From 0fd145df88a7cc75f46462ff8a7ee87a187d11dd Mon Sep 17 00:00:00 2001 From: manoraga Date: Tue, 4 Aug 2026 22:54:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BC=D0=B5=D0=B6=D1=83?= =?UTF-8?q?=D1=82=D0=BE=D1=87=D0=BD=D1=8B=D0=B9=20=D1=8D=D1=82=D0=B0=D0=BF?= =?UTF-8?q?.=20=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/database.py | 50 +++++++++++++++------- data/scud_orion_ai.db | Bin 614400 -> 815104 bytes main.py | 36 +++++++++++----- migrate_structure.py | 2 - scripts/db_cli.py | 1 - services/ai_verifier.py | 45 ++++++++++++++++++++ services/data_loader.py | 82 +++++++++++++++++++++++++++++++------ services/excel_exporter.py | 54 ++++++++++++++++-------- services/scud_export.py | 29 +++++++++---- update_imports.py | 6 +-- 10 files changed, 235 insertions(+), 70 deletions(-) diff --git a/core/database.py b/core/database.py index 3a622e6..09ac731 100644 --- a/core/database.py +++ b/core/database.py @@ -21,7 +21,7 @@ def init_db(): with get_connection() as conn: cursor = conn.cursor() - # 1. Таблица СКУД + # 1. Таблица СКУД (Добавлены first_activity, anomaly_flag, snapshot_time) cursor.execute(""" CREATE TABLE IF NOT EXISTS scud_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -31,9 +31,12 @@ def init_db(): department TEXT, position TEXT, time_in TEXT, + first_activity TEXT DEFAULT '—', time_out TEXT, time_in_building TEXT, is_present INTEGER NOT NULL, + anomaly_flag TEXT DEFAULT 'NONE', + snapshot_time TEXT DEFAULT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) @@ -104,26 +107,32 @@ def sync_knowledge_base_to_db(): Автоматически мигрирует правила из knowledge_base.json и системный промпт в таблицу ai_knowledge_base SQLite. """ system_ai_prompt = ( - "СИСТЕМНЫЙ ПРОМПТ ИИ (MATCHING & VERIFICATION):\n" - "Ты — экспертный ИИ-аудитор СКУД и кадрового учета 1С. Твоя задача — проводить интеллектуальный анализ аномалий " - "и сопоставление (Fuzzy Matching) нераспознанных ФИО из СКУД с эталонным Штатным расписанием 1С.\n\n" - "1. ЗАЩИТА ОТ ПОЛНЫХ ТЁЗОК (FULL NAME COLLISION):\n" + "СИСТЕМНЫЙ ПРОМПТ ИИ (MATCHING, VERIFICATION & ANOMALY DETECTION):\n" + "Ты — экспертный ИИ-аудитор СКУД и кадрового учета 1С. Твоя задача — проводить интеллектуальный анализ аномалий, " + "выявление массовых сбоев оборудования и сопоставление (Fuzzy Matching) нераспознанных ФИО из СКУД с эталонным Штатным расписанием 1С.\n\n" + "1. ДЕТЕКТИРОВАНИЕ АНОМАЛИЙ И МАССОВЫХ СБОЕВ СКУД:\n" + " - Если у сотрудника 'Начало_дня' = 'Нет входа', но заполнено поле 'Первая_активность' (например, '14:11:06') или 'Конец_дня' != 'Нет выхода', " + "классифицируй случай как одиночную аномалию: '🚨 АНОМАЛИЯ СКУД: Присутствие/активность без утреннего входа'.\n" + " - Если процент сотрудников с подобной аномалией превышает 5% от смены (или >10 человек), СГЕНЕРИРУЙ КРИТИЧЕСКИЙ СИСТЕМНЫЙ АЛЕРТ: " + "'⚠️ ВНИМАНИЕ! Обнаружен массовый сбой регистраторов входа СКУД на главном КПП. Требуется проверка контроллеров.'\n\n" + "2. ЗАЩИТА ОТ ПОЛНЫХ ТЁЗОК (FULL NAME COLLISION):\n" " - Если в эталонном штате 1С присутствуют два или более сотрудников с ОДИНАКОВЫМИ ФИО (полные тёзки), " "ты ОБЯЗАН учитывать контекст: Подразделение и Должность.\n" " - Ни в коем случае не объединяй записи разных людей! Если контекст не позволяет однозначно определить, кто именно из тёзок совершил проход, ПОМЕТЬ СЛУЧАЙ КАК НЕОДНОЗНАЧНЫЙ и НЕ делай автоматическую подмену.\n\n" - "2. ПРАВИЛА СОПОСТАВЛЕНИЯ (MATCHING RULES):\n" + "3. ПРАВИЛА СОПОСТАВЛЕНИЯ (MATCHING RULES):\n" " - Допускаются исправления мелких опечаток, перестановки порядка (Имя Фамилия -> Фамилия Имя), замена букв Е/Ё, И/Й.\n" " - Учитывай сокращения инициалов (например, 'Иванов И.В.' -> 'Иванов Иван Васильевич').\n" " - Если уверенность в совпадении меньше 85% — НЕ сопоставляй, оставляй запись как нераспознанную.\n\n" - "3. ФОРМАТ ВЫХОДНЫХ ДАННЫХ:\n" - " Верни строго JSON-объект без вводных слов и Markdown-разметки: {\"mappings\": [...], \"unmatched\": [...]}" + "4. ФОРМАТ ВЫХОДНЫХ ДАННЫХ:\n" + " Верни строго JSON-объект без вводных слов и Markdown-разметки: {\"mappings\": [...], \"unmatched\": [...], \"alerts\": [...]}" ) rules_to_insert = [ system_ai_prompt, "ГЛОБАЛЬНОЕ ПРАВИЛО 1: Любое физическое присутствие сотрудника по СКУД при наличии в 1С документа отсутствия (отпуск, больничный, уход за ребенком и др.), КРОМЕ командировок и служебных поездок — ЯВЛЯЕТСЯ СТРОГОЙ ИСТИННОЙ АНОМАЛИЕЙ.", "ГЛОБАЛЬНОЕ ПРАВИЛО 2: Документы отсутствий из 1С:ЗУП имеют 100% высший приоритет над статическим реестром удалёнщиков.", - "ГЛОБАЛЬНОЕ ПРАВИЛО 3: Удалённые сотрудники имеют право беспрепятственно работать в офисе по СКУД в любое время (это не является аномалией)." + "ГЛОБАЛЬНОЕ ПРАВИЛО 3: Удалённые сотрудники имеют право беспрепятственно работать в офисе по СКУД в любое время (это не является аномалией).", + "ГЛОБАЛЬНОЕ ПРАВИЛО 4: Отсутствие утренней отметки входа при наличии дневных перемещений (первая активность) является аномалией оборудования и не должно автоматически превращать запись в подтверждённое присутствие." ] kb_path = os.path.join(os.path.dirname(DATA_DIR), "knowledge_base.json") @@ -148,8 +157,8 @@ def sync_knowledge_base_to_db(): conn.commit() -def save_scud_to_db(df_scud, date_str): - """Оптимизированное сохранение логов СКУД за дату.""" +def save_scud_to_db(df_scud, date_str, snapshot_time=None): + """Оптимизированное сохранение логов СКУД за дату с поддержкой первой активности, флага аномалии и метки выгрузки.""" if df_scud.empty: return @@ -162,17 +171,28 @@ def save_scud_to_db(df_scud, date_str): r.get('Подразделение', ''), r.get('Должность', ''), r.get('Начало_дня', 'Нет входа'), + r.get('Первая_активность', '—'), r.get('Конец_дня', 'Нет выхода'), r.get('Находился_в_здании', '00:00'), - 1 if r.get('Пришел', False) else 0 + 1 if r.get('Пришел', False) else 0, + r.get('anomaly_flag', 'NONE'), + snapshot_time )) with get_connection() as conn: cursor = conn.cursor() - cursor.execute("DELETE FROM scud_logs WHERE log_date = ?", (date_str,)) + if snapshot_time: + cursor.execute("DELETE FROM scud_logs WHERE log_date = ? AND snapshot_time = ?", (date_str, snapshot_time)) + else: + cursor.execute("DELETE FROM scud_logs WHERE log_date = ? AND snapshot_time IS NULL", (date_str,)) + cursor.executemany(""" - INSERT INTO scud_logs (log_date, fio, fio_clean, department, position, time_in, time_out, time_in_building, is_present) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO scud_logs ( + log_date, fio, fio_clean, department, position, + time_in, first_activity, time_out, time_in_building, + is_present, anomaly_flag, snapshot_time + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, data_to_insert) conn.commit() diff --git a/data/scud_orion_ai.db b/data/scud_orion_ai.db index 8326b6e1d3df2c7b36b9cfbd0d89e76fff90dcb2..b723b73e256f296c2f418620796dc826a8454467 100644 GIT binary patch delta 92118 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 DLIW4K delta 37010 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 RCI<>TQoRc$nFXK+&OaL5C}IEr diff --git a/main.py b/main.py index 87a6c87..2743561 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import os import sys import argparse import pandas as pd +from datetime import datetime sys.path.append(os.path.dirname(os.path.abspath(__file__))) @@ -11,7 +12,7 @@ from services.share_copier import copy_1c_files_from_share from services.data_validator import check_file_freshness from services.data_loader import load_all_data from services.excel_exporter import generate_summary_excel, generate_detailed_excel -from services.ai_verifier import ai_verify_scud_against_staff +from services.ai_verifier import ai_verify_scud_against_staff, analyze_scud_mass_failure_ai from services.text_reporter import generate_markdown_report from services.feedback_loop import review_ai_decisions from services.knowledge_base import load_knowledge_base @@ -42,12 +43,6 @@ def load_static_reason_workers(): def detect_all_anomalies(merged_df, static_reasons_dict, kb_rules): """ Автоматически находит ИСТИННЫЕ аномалии. - - ПРАВИЛА КОМПАНИИ: - 1. Удалённые сотрудники имеют право беспрепятственно работать в офисе (НЕ аномалия). - 2. Документы отсутствий из 1С имеют 100% приоритет над стат. реестром (НЕ аномалия). - 3. АНОМАЛИЯ — Физический выход по СКУД при наличии в 1С ЛЮБЫХ причин отсутствия - (отпуск, больничный, уход за ребенком и т.д.), КРОМЕ командировок и служебных поездок. """ anomalies = [] kb_rules_text = " ".join(kb_rules).lower() if kb_rules else "" @@ -60,9 +55,11 @@ def detect_all_anomalies(merged_df, static_reasons_dict, kb_rules): is_present = row.get('Пришел', False) reason_1c = str(row.get('Вид_отсутствия', '')).strip() has_1c_reason = pd.notna(row.get('Вид_отсутствия')) and reason_1c != '' + anom_flag = row.get('anomaly_flag', 'NONE') is_fio_whitelisted_in_kb = fio_clean.lower() in kb_rules_text + # АНОМАЛИЯ 1: Физическое присутствие при документе отсутствия 1С if is_present and has_1c_reason: reason_lower = reason_1c.lower() is_allowed_trip = any(kw in reason_lower for kw in ALLOWED_WORK_TRIP_KEYWORDS) @@ -74,6 +71,15 @@ def detect_all_anomalies(merged_df, static_reasons_dict, kb_rules): "details": f"Сотрудник пришел по СКУД, но в 1С оформлен документ: '{reason_1c}'" }) + # АНОМАЛИЯ 2: Перемещение внутри здания без отметки утреннего входа + if anom_flag == 'ANOMALY_NO_IN_HAS_ACTIVITY': + first_act = row.get('Первая_активность', '—') + anomalies.append({ + "type": "АНОМАЛИЯ СКУД: ПЕРЕМЕЩЕНИЕ БЕЗ ВХОДА", + "fio": fio, + "details": f"Отсутствует регистрация входа на КПП при зафиксированной первой активности в {first_act}" + }) + return anomalies @@ -106,6 +112,7 @@ def main(): args = parser.parse_args() DEBUG = args.debug + snapshot_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") print("=" * 60) print(f"ЗАПУСК СИСТЕМЫ МОДУЛЬНОГО КОНТРОЛЛИНГА СКУД ⟷ 1С {'[DEBUG MODE]' if DEBUG else ''}") @@ -116,7 +123,7 @@ def main(): kb_data = load_knowledge_base() kb_rules = kb_data.get("rules", []) - # 0.1 Выгрузка из БД СКУД напрямую в SQLite и Excel (save_xlsx=True требуется для data_loader) + # 0.1 Выгрузка из БД СКУД напрямую в SQLite и Excel if not args.skip_export: try: run_export(save_xlsx=True, debug=DEBUG) @@ -219,13 +226,12 @@ def main(): generate_detailed_excel(merged_df=merged_yesterday, date_str=DATE_YESTERDAY) # 💾 СОХРАНЕНИЕ ВЧЕРАШНИХ ДАННЫХ В SQLite: - save_scud_to_db(merged_yesterday, DATE_YESTERDAY) + save_scud_to_db(merged_yesterday, DATE_YESTERDAY, snapshot_time=snapshot_time) # ============================================================ # 🎯 ЧАСТЬ 2: ЕЖЕДНЕВНАЯ СВОДКА ЗА СЕГОДНЯ # ============================================================ - # 💾 СОХРАНЯЕМ СЕГОДНЯШНИЙ СКУД В SQLite (Даже если 1С еще не пришла): - save_scud_to_db(raw_scud_today_df, DATE_TODAY) + save_scud_to_db(raw_scud_today_df, DATE_TODAY, snapshot_time=snapshot_time) if has_today_1c and df_staff_today is not None and df_absent_today is not None: print("\n[4/5] Обработка данных за СЕГОДНЯ...") @@ -269,6 +275,14 @@ def main(): anomalies_list = detect_all_anomalies(merged_today, static_reasons_dict, kb_rules) + # ⭐️ ПРОВЕРКА МАССОВОГО СБОЯ ОБОРУДОВАНИЯ ЧЕРЕЗ ИИ + mass_failure_result = analyze_scud_mass_failure_ai(merged_today) + if mass_failure_result and mass_failure_result.get("is_mass_failure"): + print("\n" + "!" * 60) + print(f"🚨 ВНИМАНИЕ! ИИ ОБНАРУЖИЛ МАССОВЫЙ СБОЙ ТУРНИКЕТОВ ВХОДА ({mass_failure_result['anomaly_percent']}% СМЕНЫ)") + print(mass_failure_result["alert_text"]) + print("!" * 60 + "\n") + # 💾 СОХРАНЕНИЕ СЕГОДНЯШНИХ АНОМАЛИЙ В БАЗУ ДАННЫХ SQLite save_anomalies_to_db(anomalies_list, DATE_TODAY) diff --git a/migrate_structure.py b/migrate_structure.py index a3f9cb3..42500d5 100644 --- a/migrate_structure.py +++ b/migrate_structure.py @@ -28,14 +28,12 @@ def run_git_move(): open(init_file, 'w').close() for filename, dest in file_mapping.items(): - # Проверим, где файл может находиться: в корне или уже в папке назначения if os.path.exists(filename): src = filename elif os.path.exists(dest): print(f"[ℹ️ Уже на месте] {dest}") continue else: - # Попробуем поискать файл рекурсивно found = None for root, dirs, files in os.walk("."): if filename in files and "venv" not in root: diff --git a/scripts/db_cli.py b/scripts/db_cli.py index 63f2884..ea8470e 100644 --- a/scripts/db_cli.py +++ b/scripts/db_cli.py @@ -3,7 +3,6 @@ import os import sqlite3 import pandas as pd -# Добавляем корневую директорию проекта в sys.path SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.dirname(SCRIPT_DIR) sys.path.append(PROJECT_ROOT) diff --git a/services/ai_verifier.py b/services/ai_verifier.py index a1f0f79..71dc9d9 100644 --- a/services/ai_verifier.py +++ b/services/ai_verifier.py @@ -1,6 +1,7 @@ import json import re import requests +import pandas as pd from config import OLLAMA_URL, OLLAMA_MODEL from core.database import get_all_rules_from_db @@ -44,6 +45,50 @@ def get_system_rules_context(): return f"\nОБЯЗАТЕЛЬНЫЕ ГЛОБАЛЬНЫЕ ПРАВИЛА И ПРИОРИТЕТЫ КОМПАНИИ:\n{rules_text}\n" +def analyze_scud_mass_failure_ai(df_scud): + """ + Оценивает процент аномалий 'ANOMALY_NO_IN_HAS_ACTIVITY' в выгрузке. + Если процент аномалий превышает 5% от смены, вызывает ИИ для генерации критического алерта. + """ + if df_scud is None or df_scud.empty: + return None + + total_records = len(df_scud) + anomaly_rows = df_scud[df_scud.get('anomaly_flag', 'NONE') == 'ANOMALY_NO_IN_HAS_ACTIVITY'] + anomaly_count = len(anomaly_rows) + + if total_records == 0: + return None + + anomaly_percent = round((anomaly_count / total_records) * 100, 1) + + if anomaly_percent > 5.0 or anomaly_count >= 10: + rules_context = get_system_rules_context() + prompt = f""" +{rules_context} +ВНИМАНИЕ! Проведён анализ дневной смены СКУД: +- Всего записей за смену: {total_records} +- Выявлено сотрудников без утреннего входа, но с зафиксированной дневной активностью (первая активность): {anomaly_count} ({anomaly_percent}% от смены) + +Сформируй понятное предупреждение для Администратора СКУД и Руководителя. +Объясни, что это критический массовый сбой турникетов/контроллеров входа на КПП, и порекомендуй действия. +""" + alert_text = ask_ollama(prompt, system_prompt="Ты — ИИ-аналитик контроллинга СКУД. Отвечай кратко и строго по делу.") + return { + "is_mass_failure": True, + "anomaly_count": anomaly_count, + "anomaly_percent": anomaly_percent, + "alert_text": alert_text + } + + return { + "is_mass_failure": False, + "anomaly_count": anomaly_count, + "anomaly_percent": anomaly_percent, + "alert_text": "Массовых сбоев оборудования не зафиксировано." + } + + def ai_verify_scud_against_staff(unrecognized_scud_fios, staff_fios): """ ИИ-сверка опечаток и несовпадений ФИО между СКУД и 1С:ЗУП. diff --git a/services/data_loader.py b/services/data_loader.py index 56ca277..88af675 100644 --- a/services/data_loader.py +++ b/services/data_loader.py @@ -72,8 +72,74 @@ def load_absent_data(date_str): return None +def process_scud_anomalies(df_scud, exceptions=None): + """ + Вычисляет 'Первую активность' и маркирует аномалии СКУД. + Если 'Начало дня' == 'Нет входа', но есть любая физическая активность — + записывает первейшее время в 'Первая_активность', сбрасывает 'Пришел' в False и выставляет флаг аномалии. + """ + if exceptions is None: + exceptions = load_exceptions() + + start_col = 'Начало_дня' if 'Начало_дня' in df_scud.columns else ('Начало дня' if 'Начало дня' in df_scud.columns else None) + end_col = 'Конец_дня' if 'Конец_дня' in df_scud.columns else ('Конец дня' if 'Конец дня' in df_scud.columns else None) + status_col = 'Статус' if 'Статус' in df_scud.columns else None + + first_activities = [] + anomaly_flags = [] + is_present_flags = [] + + for _, row in df_scud.iterrows(): + fio = str(row.get('Сотрудник', row.get('fio_raw', ''))).strip() + fio_clean = normalize_fio(fio) + dept = str(row.get('Подразделение', '')).strip() + pos = str(row.get('Должность', '')).strip() + + val_start = str(row.get(start_col, '')).strip() if start_col else '' + val_end = str(row.get(end_col, '')).strip() if end_col else '' + val_status = str(row.get(status_col, '')).strip().lower() if status_col else '' + + # Извлечение первого зафиксированного события из сырых логов (pLogData), если передавалось + raw_first_event = row.get('raw_first_event', None) + if pd.isna(raw_first_event) or str(raw_first_event).strip() in ['', 'None', 'nan', '00:00:00']: + raw_first_event = None + + has_no_in = (val_start in ['нет входа', 'none', 'nan', '', '00:00:00', '00:00']) + has_no_out = (val_end in ['нет выхода', 'none', 'nan', '', '00:00:00', '00:00']) + + first_act = '—' + anom_flag = 'NONE' + + # Стандартная проверка присутствия + is_present = not ('отсутствовал' in val_status or has_no_in) + + # ЛОГИКА ВЫЧИСЛЕНИЯ ПЕРВОЙ АКТИВНОСТИ И ДЕТЕКТИРОВАНИЯ АНОМАЛИИ + if has_no_in: + # Если нет входа, но есть проход внутри или зафиксирован выход + if raw_first_event is not None or not has_no_out: + first_act = str(raw_first_event) if raw_first_event else val_end + anom_flag = 'ANOMALY_NO_IN_HAS_ACTIVITY' + + # ЖЕСТКАЯ ВАЛИДАЦИЯ: Если человек не исключен официально — сбрасываем фейковый Пришел = True + if not is_excluded(fio, dept, pos, exceptions): + is_present = False + else: + first_act = '—' + is_present = False + + first_activities.append(first_act) + anomaly_flags.append(anom_flag) + is_present_flags.append(is_present) + + df_scud['Первая_активность'] = first_activities + df_scud['anomaly_flag'] = anomaly_flags + df_scud['Пришел'] = is_present_flags + + return df_scud + + def load_scud_data(target_date_type="today"): - """Загружает файл СКУД за 'today' или 'yesterday'.""" + """Загружает файл СКУД за 'today' или 'yesterday' и выполняет валидацию аномалий.""" target_date = DATE_TODAY if target_date_type == "today" else DATE_YESTERDAY scud_path = find_dated_file("Сотрудники", target_date, search_dirs=[SCUD_DIR]) @@ -88,19 +154,9 @@ def load_scud_data(target_date_type="today"): df_scud['fio_raw'] = df_scud['Сотрудник'].astype(str) if 'Сотрудник' in df_scud.columns else df_scud[df_scud.columns[0]].astype(str) df_scud['fio_clean'] = df_scud['fio_raw'].apply(clean_scud_fio_light) - start_col = 'Начало_дня' if 'Начало_дня' in df_scud.columns else ('Начало дня' if 'Начало дня' in df_scud.columns else None) - status_col = 'Статус' if 'Статус' in df_scud.columns else None + # Применяем обработчик аномалий и первой активности + df_scud = process_scud_anomalies(df_scud, exceptions=exceptions) - def is_person_present(row): - val_status = str(row.get(status_col, '')).strip().lower() if status_col else '' - val_time = str(row.get(start_col, '')).strip() if start_col else '' - - if 'отсутствовал' in val_status or val_time in ['нет входа', 'none', 'nan', '', '00:00']: - return False - return True - - df_scud['Пришел'] = df_scud.apply(is_person_present, axis=1) - df_scud['is_excluded'] = df_scud.apply( lambda r: is_excluded(r.get('Сотрудник', ''), r.get('Подразделение', ''), r.get('Должность', ''), exceptions), axis=1 diff --git a/services/excel_exporter.py b/services/excel_exporter.py index 405192d..db6a6ca 100644 --- a/services/excel_exporter.py +++ b/services/excel_exporter.py @@ -16,6 +16,7 @@ MONTHS_RU = { 9: "сентября", 10: "октября", 11: "ноября", 12: "декабря" } + def format_date_ru(date_str): """Преобразует дату формата '27.07.2026' в '27 июля 2026'""" try: @@ -24,6 +25,7 @@ def format_date_ru(date_str): except Exception: return date_str + # Границы ячеек THIN_SIDE = Side(border_style="thin", color="D3D3D3") THIN_BORDER = Border(left=THIN_SIDE, right=THIN_SIDE, top=THIN_SIDE, bottom=THIN_SIDE) @@ -142,7 +144,7 @@ def generate_summary_excel(merged_df, date_str="27.07.2026", filename=None): ws.row_dimensions[current_row].hidden = False current_row += 1 - # 4. КАТЕГОРИИ ОТСУТСТВИЙ (Свернуты hidden=True, ИСКЛЮЧЕНИЯ ИСКЛЮЧЕНЫ) + # 4. КАТЕГОРИИ ОТСУТСТВИЙ (Свернуты hidden=True) absent_only = merged_df[ (merged_df['Пришел'] == False) & (merged_df['Вид_отсутствия'].notna()) & @@ -186,9 +188,12 @@ def generate_summary_excel(merged_df, date_str="27.07.2026", filename=None): current_row += 1 # 6. АНОМАЛИИ (Свернут hidden=True) - anomalies = merged_df[(merged_df['Пришел'] == True) & (merged_df['Вид_отсутствия'].notna())] + anomalies = merged_df[ + ((merged_df['Пришел'] == True) & (merged_df['Вид_отсутствия'].notna())) | + (merged_df.get('anomaly_flag', 'NONE') == 'ANOMALY_NO_IN_HAS_ACTIVITY') + ] - ws.cell(row=current_row, column=1, value="Аномалии") + ws.cell(row=current_row, column=1, value="Аномалии СКУД и 1С") ws.cell(row=current_row, column=2, value=len(anomalies)) format_row_cells(ws, current_row, FILL_ANOMALY, is_bold=True, bold_font=bold_font) current_row += 1 @@ -198,10 +203,16 @@ def generate_summary_excel(merged_df, date_str="27.07.2026", filename=None): if not anomalies.empty: for _, row in anomalies.iterrows(): fio = row.get('Сотрудник', '') - reason = row.get('Вид_отсутствия', 'Неизвестная причина') - reason_text = f"В 1С: {reason}" + reason = row.get('Вид_отсутствия', '') + anom_flag = row.get('anomaly_flag', 'NONE') + + if anom_flag == 'ANOMALY_NO_IN_HAS_ACTIVITY': + first_act = row.get('Первая_активность', '—') + reason_text = f"🚨 АНОМАЛИЯ СКУД: Нет входа (первая активность: {first_act})" + else: + reason_text = f"В 1С: {reason}" - cell_a = ws.cell(row=current_row, column=1, value=f"{fio} (На работе)") + cell_a = ws.cell(row=current_row, column=1, value=f"{fio}") cell_b = ws.cell(row=current_row, column=2, value=reason_text) cell_a.fill = FILL_ANOMALY @@ -223,7 +234,7 @@ def generate_summary_excel(merged_df, date_str="27.07.2026", filename=None): current_row += 1 ws.column_dimensions['A'].width = 45.0 - ws.column_dimensions['B'].width = 32.0 + ws.column_dimensions['B'].width = 38.0 try: wb.save(output_path) @@ -235,7 +246,7 @@ def generate_summary_excel(merged_df, date_str="27.07.2026", filename=None): print(f"[⚠️] Файл открыт в Excel! Сохранено как: {alt_path}") -# --- 2. ДЕТАЛЬНЫЙ ОТЧЕТ ЗА ВЧЕРА --- +# --- 2. ДЕТАЛЬНЫЙ ОТЧЕТ ЗА ВЧЕРА (С КОЛОНКОЙ 'первая активность' СРАЗУ ПОСЛЕ 'время входа') --- def generate_detailed_excel(merged_df, date_str="26.07.2026", filename=None): if not filename: filename = f"{format_date_ru(date_str)} отчет.xlsx" @@ -250,8 +261,9 @@ def generate_detailed_excel(merged_df, date_str="26.07.2026", filename=None): ws["B2"].font = Font(name="Arial", size=10, bold=True) ws["D2"].font = Font(name="Arial", size=10, bold=True) + # ⭐️ Порядок колонок: "первая активность" строго после "время входа" headers = [ - "№", "ФИО", "Подразделение", "время входа", "время выхода", + "№", "ФИО", "Подразделение", "время входа", "первая активность", "время выхода", "находился в здании", "причина отсутствия", "норма", "отклонение от нормы" ] ws.append([]) @@ -269,14 +281,16 @@ def generate_detailed_excel(merged_df, date_str="26.07.2026", filename=None): end_col = 'Конец дня' if 'Конец дня' in merged_df.columns else 'Конец_дня' hours_col = 'Часы' if 'Часы' in merged_df.columns else 'Находился_в_здании' - chars_per_line_g = 24 + chars_per_line_h = 24 for idx, row in merged_df.reset_index(drop=True).iterrows(): is_present = row.get('Пришел', False) absence_reason = row.get('Вид_отсутствия', '') has_reason = pd.notna(absence_reason) and str(absence_reason).strip() != '' + anom_flag = row.get('anomaly_flag', 'NONE') in_building_str = str(row.get(hours_col, '00:00')) + first_act_val = row.get('Первая_активность', '—') deviation_val = calculate_deviation(in_building_str, reason=absence_reason if has_reason else "", norm_hours=8, lunch_minutes=30) @@ -285,6 +299,7 @@ def generate_detailed_excel(merged_df, date_str="26.07.2026", filename=None): row.get('Сотрудник', ''), row.get('Подразделение', ''), row.get(start_col, 'Нет входа'), + first_act_val, # ⭐️ Колонки №5: "первая активность" row.get(end_col, 'Нет выхода'), in_building_str, absence_reason if has_reason else '', @@ -294,16 +309,19 @@ def generate_detailed_excel(merged_df, date_str="26.07.2026", filename=None): row_num = 5 + idx - if is_present and has_reason: + # Заливка строк по категориям + if anom_flag == 'ANOMALY_NO_IN_HAS_ACTIVITY': + row_fill = LIGHT_RED_FILL + elif is_present and has_reason: row_fill = GREEN_FILL elif not is_present: row_fill = YELLOW_FILL if has_reason else LIGHT_RED_FILL else: row_fill = None - val_g_str = str(absence_reason) if has_reason else "" - if len(val_g_str) > chars_per_line_g: - needed_lines = math.ceil(len(val_g_str) / chars_per_line_g) + val_h_str = str(absence_reason) if has_reason else "" + if len(val_h_str) > chars_per_line_h: + needed_lines = math.ceil(len(val_h_str) / chars_per_line_h) ws.row_dimensions[row_num].height = max(needed_lines * 18, 22) else: ws.row_dimensions[row_num].height = 20 @@ -314,17 +332,17 @@ def generate_detailed_excel(merged_df, date_str="26.07.2026", filename=None): if row_fill: cell.fill = row_fill - if col_idx == 7: + if col_idx == 8: cell.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True) - elif col_idx in [1, 4, 5, 6, 8, 9]: + elif col_idx in [1, 4, 5, 6, 7, 9, 10]: cell.alignment = Alignment(horizontal="center", vertical="center") else: cell.alignment = Alignment(horizontal="left", vertical="center") for col in ws.columns: col_letter = get_column_letter(col[0].column) - if col_letter == 'G': - ws.column_dimensions['G'].width = 25.0 + if col_letter == 'H': + ws.column_dimensions['H'].width = 25.0 else: max_len = 0 for cell in col: diff --git a/services/scud_export.py b/services/scud_export.py index 84e21cf..bdab488 100644 --- a/services/scud_export.py +++ b/services/scud_export.py @@ -1,7 +1,6 @@ """ Модуль автоматического экспорта данных СКУД (Orion) из MS SQL Server в SQLite и Excel. -Исправлена логика определения направления проходов (Mode = 2 -> OUT). -Адаптирован для Linux (Ubuntu 24.04). +Добавлена фиксация Первой Активности (без учета направления) и среза по времени. """ import argparse @@ -82,11 +81,11 @@ WITH DailyLogs AS ( WHERE log.TimeVal BETWEEN @StartDate AND @EndDate AND log.HozOrgan IS NOT NULL AND log.HozOrgan > 0 - AND (log.Event IN (28, 29, 26, 27, 32, 33) OR log.Mode IN (1, 2)) ), Passages AS ( SELECT EmployeeID, + MIN(TimeVal) AS FirstRawEvent, -- ⭐️ Первое событие ДНЯ ВООБЩЕ (любая дверь/туалет/выход) MIN(CASE WHEN Direction = 'IN' THEN TimeVal END) AS FirstIn, MAX(CASE WHEN Direction = 'OUT' THEN TimeVal END) AS LastOut, MAX(CASE WHEN RowNumDesc = 1 THEN Direction END) AS LastEventType @@ -107,6 +106,14 @@ SELECT ISNULL(CAST(p.TabNumber AS NVARCHAR(50)), N'—') AS [Таб_№], CONVERT(VARCHAR(10), @TargetDate, 104) AS [Дата], ISNULL(CAST(CONVERT(VARCHAR(8), pass.FirstIn, 108) AS NVARCHAR(20)), N'Нет входа') AS [Начало_дня], + + -- ⭐️ НОВАЯ КОЛОНКА: "Первая активность" (если нет входа, но есть события в здании) + CASE + WHEN pass.FirstIn IS NULL AND pass.FirstRawEvent IS NOT NULL + THEN CAST(CONVERT(VARCHAR(8), pass.FirstRawEvent, 108) AS NVARCHAR(20)) + ELSE N'—' + END AS [Первая_активность], + CASE WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN N'Нет выхода' @@ -204,6 +211,7 @@ def run_export(input_date: str | None = None, save_xlsx: bool = True, debug: boo os.makedirs(SCUD_DIR, exist_ok=True) targets = get_targets(input_date) + snapshot_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") conn_str = ( f"DRIVER={{{ODBC_DRIVER}}};" @@ -235,11 +243,18 @@ def run_export(input_date: str | None = None, save_xlsx: bool = True, debug: boo if len(df) > 0: # 1. Готовим колонки для локальной БД df['fio_clean'] = df['Сотрудник'].apply(clean_scud_fio_light) - df['Пришел'] = df['Статус'].str.contains('Присутствовал', case=False, na=False) + + # Валидация прихода с учетом аномалии неявки на вход + df['anomaly_flag'] = 'NONE' + mask_anomaly = (df['Начало_дня'] == 'Нет входа') & (df['Первая_активность'] != '—') + df.loc[mask_anomaly, 'anomaly_flag'] = 'ANOMALY_NO_IN_HAS_ACTIVITY' - # 2. ПРЯМОЕ СОХРАНЕНИЕ В SQLite (scud_orion_ai.db) - save_scud_to_db(df, processing_date_str) - log(f"[✓] Записи за {processing_date_str} успешно сохранены в SQLite!", "SUCCESS") + # Флаг 'Пришел' ставится в True только если был вход + df['Пришел'] = df['Статус'].str.contains('Присутствовал', case=False, na=False) & (~mask_anomaly) + + # 2. ПРЯМОЕ СОХРАНЕНИЕ В SQLite (scud_orion_ai.db) С METКОЙ СРЕЗА snapshot_time + save_scud_to_db(df, processing_date_str, snapshot_time=snapshot_time) + log(f"[✓] Записи за {processing_date_str} успешно сохранены в SQLite с меткой {snapshot_time}!", "SUCCESS") # 3. Сохранение в Excel (если включен флаг save_xlsx) if save_xlsx: diff --git a/update_imports.py b/update_imports.py index 17ec348..fbbf5c8 100644 --- a/update_imports.py +++ b/update_imports.py @@ -19,7 +19,7 @@ def update_file_imports(filepath): for mod in service_modules: new_content = new_content.replace(f"from modules.{mod}", f"from services.{mod}") new_content = new_content.replace(f"import modules.{mod}", f"import services.{mod}") - # Также на случай относительных или прямых импортов внутри сервисов друг на друга + # Замена для прямых импортов между модулями внутри папки services/ new_content = new_content.replace(f"from {mod}", f"from services.{mod}") if content != new_content: @@ -33,9 +33,9 @@ def run_update(): if "venv" in root or ".git" in root: continue for file in files: - if file.endswith(".py") and file != "migrate_structure.py" and file != "update_imports.py": + if file.endswith(".py") and file not in ["migrate_structure.py", "update_imports.py"]: update_file_imports(os.path.join(root, file)) - print("=== Обновление завершено ===") + print("=== Обновление импортов успешно завершено ===") if __name__ == "__main__": run_update() \ No newline at end of file