From 260362709865e33a35aeb3ac98d57bbe1c404102 Mon Sep 17 00:00:00 2001 From: manoraga Date: Tue, 28 Jul 2026 16:41:26 +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=D0=BE.=20=D0=A0=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=B5=D1=82=20=D0=B2=D1=81=D0=B5,=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BD=D0=BE=20=D1=84=D0=BE=D1=80=D0=BC=D0=B8?= =?UTF-8?q?=D1=80=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D0=B2=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=88=D0=BD=D0=B8=D0=B9=20=D0=BE=D1=82=D1=87=D0=B5=D1=82?= =?UTF-8?q?=20(=D0=BD=D0=B5=D1=82=20=D0=B2=D1=8B=D1=85=D0=BE=D0=B4=D0=B0).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.py | 98 +++++------ .../1c/Отсутствия сотрудников - выгрузка.xlsx | Bin 0 -> 10330 bytes data/1c/Штатные сотрудники - ОУП.xlsx | Bin 0 -> 29918 bytes data/knowledge_base.json | 3 +- data/scud/Сотрудники_27.07.2026.xlsx | Bin 0 -> 23399 bytes data/scud/Сотрудники_28.07.2026.xlsx | Bin 0 -> 23907 bytes logs/export_28.07.2026.log | 27 +++ main.py | 166 ++++++++++++------ modules/ai_verifier.py | 35 ++-- modules/data_loader.py | 20 +-- modules/data_validator.py | 60 +++---- modules/scud_export.py | 102 +++-------- modules/text_reporter.py | 69 ++++---- 13 files changed, 297 insertions(+), 283 deletions(-) create mode 100644 data/1c/Отсутствия сотрудников - выгрузка.xlsx create mode 100644 data/1c/Штатные сотрудники - ОУП.xlsx create mode 100644 data/scud/Сотрудники_27.07.2026.xlsx create mode 100644 data/scud/Сотрудники_28.07.2026.xlsx create mode 100644 logs/export_28.07.2026.log diff --git a/config.py b/config.py index 56935a0..f5fd21d 100644 --- a/config.py +++ b/config.py @@ -1,78 +1,64 @@ import os -import re -import json -import pandas as pd from datetime import datetime, timedelta -# 📁 Пути к директориям BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") + +# 📂 ПОДПАПКИ DАТА +SCUD_DIR = os.path.join(DATA_DIR, "scud") +ZUP_1C_DIR = os.path.join(DATA_DIR, "1c") OUTPUT_DIR = os.path.join(BASE_DIR, "output") -os.makedirs(DATA_DIR, exist_ok=True) -os.makedirs(OUTPUT_DIR, exist_ok=True) +# Создаем директории при их отсутствии +for folder in [DATA_DIR, SCUD_DIR, ZUP_1C_DIR, OUTPUT_DIR]: + os.makedirs(folder, exist_ok=True) -# 📅 Динамический расчет дат (СЕГОДНЯ и ВЧЕРА) -def get_controlling_dates(): - now = datetime.now() - if now.weekday() == 0: # Если Понедельник -> Вчера = Пятница - yesterday_dt = now - timedelta(days=3) - else: - yesterday_dt = now - timedelta(days=1) - - date_today_str = now.strftime("%d.%m.%Y") - date_yesterday_str = yesterday_dt.strftime("%d.%m.%Y") - return date_today_str, date_yesterday_str +# 📅 Даты +NOW = datetime.now() +DATE_TODAY = NOW.strftime("%d.%m.%Y") -DATE_TODAY, DATE_YESTERDAY = get_controlling_dates() +if NOW.weekday() == 0: # Понедельник -> Вчера = Пятница + DATE_YESTERDAY = (NOW - timedelta(days=3)).strftime("%d.%m.%Y") +else: + DATE_YESTERDAY = (NOW - timedelta(days=1)).strftime("%d.%m.%Y") # 📄 Пути к файлам -SCUD_FILE_TODAY = os.path.join(DATA_DIR, f"Сотрудники_{DATE_TODAY}.xlsx") -SCUD_FILE_YESTERDAY = os.path.join(DATA_DIR, f"Сотрудники_{DATE_YESTERDAY}.xlsx") -SCUD_FILE = SCUD_FILE_TODAY if os.path.exists(SCUD_FILE_TODAY) else SCUD_FILE_YESTERDAY +SCUD_FILE_TODAY = os.path.join(SCUD_DIR, f"Сотрудники_{DATE_TODAY}.xlsx") +SCUD_FILE_YESTERDAY = os.path.join(SCUD_DIR, f"Сотрудники_{DATE_YESTERDAY}.xlsx") +SCUD_FILE = os.path.join(SCUD_DIR, "Сотрудники.xlsx") -STAFF_FILE = os.path.join(DATA_DIR, "Штатные сотрудники - ОУП.xlsx") -ABSENT_FILE = os.path.join(DATA_DIR, "Отсутствия сотрудников - выгрузка.xlsx") -EXCEPTIONS_FILE = os.path.join(BASE_DIR, "exceptions.json") +STAFF_FILE_1C = os.path.join(ZUP_1C_DIR, "Штатные сотрудники - ОУП.xlsx") +ABSENT_FILE_1C = os.path.join(ZUP_1C_DIR, "Отсутствия сотрудников - выгрузка.xlsx") -# 🤖 Настройки Ollama -OLLAMA_URL = "http://10.121.18.227:11434/api/chat" +# Настройки Ollama / RAG (объявляем оба имени для совместимости) +OLLAMA_URL = "http://10.121.18.227:11434/api/generate" OLLAMA_MODEL = "qwen2.5:14b" +MODEL_NAME = OLLAMA_MODEL + +KNOWLEDGE_BASE_PATH = os.path.join(DATA_DIR, "knowledge_base.json") +EXCEPTIONS_PATH = os.path.join(BASE_DIR, "exceptions.json") -# 🛠 Очистка и нормализация данных - -def normalize_fio(val): - """Строгая нормализация ФИО для точности сопоставления 1С и СКУД""" - if pd.isna(val): +def normalize_fio(fio): + if not fio or not isinstance(fio, str): return "" - val = str(val).strip().lower() - val = re.sub(r'\(.*?\)', '', val) - trans = str.maketrans('aceopxyABCEHKMOPTX', 'асеорхуАВСЕНКМОРТХ') - val = val.translate(trans).replace('ё', 'е') - return re.sub(r'\s+', ' ', val).strip() + parts = fio.strip().split() + return " ".join(parts).title() -def clean_scud_fio_light(val): - """Легкая очистка ФИО для СКУД""" - if pd.isna(val): +def clean_scud_fio_light(fio_str): + if not isinstance(fio_str, str): return "" - val = str(val).strip().lower() - val = re.sub(r'\(.*?\)', '', val).replace('ё', 'е') - return re.sub(r'\s+', ' ', val).strip() + fio_clean = fio_str.replace("(сводная)", "").strip() + return normalize_fio(fio_clean) -def load_exceptions(file_path=EXCEPTIONS_FILE): - """Загрузка списка исключений из JSON""" - if not os.path.exists(file_path): - return {"departments": [], "positions": [], "fio": [], "position_keywords": []} - with open(file_path, "r", encoding="utf-8") as f: - data = json.load(f) - return { - "departments": [str(d).strip().upper() for d in data.get("departments", [])], - "positions": [str(p).strip().lower() for p in data.get("positions", [])], - "fio": [normalize_fio(f) for f in data.get("fio", [])], - "position_keywords": [str(k).strip().lower() for k in data.get("position_keywords", [])] - } - -# тестовый комментарий \ No newline at end of file +def load_exceptions(): + import json + if os.path.exists(EXCEPTIONS_PATH): + try: + with open(EXCEPTIONS_PATH, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + pass + return {"fio": [], "departments": [], "positions": [], "position_keywords": []} \ No newline at end of file diff --git a/data/1c/Отсутствия сотрудников - выгрузка.xlsx b/data/1c/Отсутствия сотрудников - выгрузка.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a4ef5d0440723d551871dac62aca86ee50b661cd GIT binary patch literal 10330 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 HUjFw#>oxq= literal 0 HcmV?d00001 diff --git a/data/1c/Штатные сотрудники - ОУП.xlsx b/data/1c/Штатные сотрудники - ОУП.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4994621ff25d9fcfd8244e61a4d5d6eb7a8829f4 GIT binary patch literal 29918 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 Z=07vOfFFv)%fSD2;MYr8z{H$6{{z!n{sjO4 literal 0 HcmV?d00001 diff --git a/data/knowledge_base.json b/data/knowledge_base.json index c4fa635..6403666 100644 --- a/data/knowledge_base.json +++ b/data/knowledge_base.json @@ -1,7 +1,8 @@ { "rules": [ "Кондрашова Елена Владимировна имеет 2 пропуска в СКУД. Если один 'Отсутствовал', а второй с временем входа — считать ПРИШЕДШЕЙ.", - "Сотрудники подразделения ОА с должностью 'Инженер-программист' часто являются внутренними совместителями." + "Сотрудники подразделения ОА с должностью 'Инженер-программист' часто являются внутренними совместителями.", + "Сотрудник Незнанова Валерия Игоревна: Незнанова иногда присутствует в офисе, не считать аномалией. Когда в офисе, из удаленщиков в отчете исключать!" ], "fio_corrections": { "кондрашава елена владимировна": "Кондрашова Елена Владимировна" diff --git a/data/scud/Сотрудники_27.07.2026.xlsx b/data/scud/Сотрудники_27.07.2026.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..403a440b5b12fe521d54cf998c06a9d7eab61ee2 GIT binary patch literal 23399 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 literal 0 HcmV?d00001 diff --git a/data/scud/Сотрудники_28.07.2026.xlsx b/data/scud/Сотрудники_28.07.2026.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..76809980b6b09c094b4a2c7a40c6355252b8a086 GIT binary patch literal 23907 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 literal 0 HcmV?d00001 diff --git a/logs/export_28.07.2026.log b/logs/export_28.07.2026.log new file mode 100644 index 0000000..de17c64 --- /dev/null +++ b/logs/export_28.07.2026.log @@ -0,0 +1,27 @@ +[2026-07-28 15:35:20] [INFO] === [ЭТАП 0] Выгрузка свежих данных СКУД напрямую из БД Орион === +[2026-07-28 15:35:20] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 15:35:45] [INFO] База вернула 272 строк за 27.07.2026 +[2026-07-28 15:35:45] [INFO] [SUCCESS] [✓] Успешно экспортирован файл: data/scud/Сотрудники_27.07.2026.xlsx +[2026-07-28 15:35:45] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 15:36:07] [INFO] База вернула 272 строк за 28.07.2026 +[2026-07-28 15:36:07] [INFO] [SUCCESS] [✓] Успешно экспортирован файл: data/scud/Сотрудники_28.07.2026.xlsx +[2026-07-28 15:36:07] [INFO] === Выгрузка СКУД завершена === + +[2026-07-28 15:38:34] [INFO] === [ЭТАП 0] Выгрузка свежих данных СКУД напрямую из БД Орион === +[2026-07-28 15:38:34] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 15:38:54] [INFO] База вернула 272 строк за 27.07.2026 +[2026-07-28 15:38:54] [INFO] [SUCCESS] [✓] Успешно экспортирован файл: data/scud/Сотрудники_27.07.2026.xlsx +[2026-07-28 15:38:54] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 15:39:21] [INFO] База вернула 272 строк за 28.07.2026 +[2026-07-28 15:39:21] [INFO] [SUCCESS] [✓] Успешно экспортирован файл: data/scud/Сотрудники_28.07.2026.xlsx +[2026-07-28 15:39:21] [INFO] === Выгрузка СКУД завершена === + +[2026-07-28 15:41:14] [INFO] === [ЭТАП 0] Выгрузка свежих данных СКУД напрямую из БД Орион === +[2026-07-28 15:41:14] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 15:41:33] [INFO] База вернула 272 строк за 27.07.2026 +[2026-07-28 15:41:33] [INFO] [SUCCESS] [✓] Успешно экспортирован файл: data/scud/Сотрудники_27.07.2026.xlsx +[2026-07-28 15:41:33] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 15:41:58] [INFO] База вернула 272 строк за 28.07.2026 +[2026-07-28 15:41:58] [INFO] [SUCCESS] [✓] Успешно экспортирован файл: data/scud/Сотрудники_28.07.2026.xlsx +[2026-07-28 15:41:58] [INFO] === Выгрузка СКУД завершена === + diff --git a/main.py b/main.py index 0c1c9e9..2104bdb 100644 --- a/main.py +++ b/main.py @@ -6,20 +6,17 @@ import pandas as pd sys.path.append(os.path.dirname(os.path.abspath(__file__))) from config import DATE_TODAY, DATE_YESTERDAY, OUTPUT_DIR, DATA_DIR, normalize_fio +from modules.scud_export import run_export from modules.data_validator import check_file_freshness from modules.data_loader import load_all_data from modules.excel_exporter import generate_summary_excel, generate_detailed_excel from modules.ai_verifier import ai_verify_scud_against_staff from modules.text_reporter import generate_markdown_report from modules.feedback_loop import review_ai_decisions +from modules.knowledge_base import load_knowledge_base def load_static_reason_workers(): - """ - Загружает реестр сотрудников со статическими/особыми причинами - из static_reason_workers.csv. - Возвращает словарь: {fio_clean: reason} - """ static_path = os.path.join(DATA_DIR, "static_reason_workers.csv") if not os.path.exists(static_path): return {} @@ -33,12 +30,60 @@ def load_static_reason_workers(): return {} +def detect_all_anomalies(merged_df, static_reasons_dict, kb_rules): + """ + Автоматически находит логические конфликты между СКУД, 1С, статической удалёнкой и исключениями + с учётом динамических правил из базы знаний (knowledge_base.json). + """ + 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) + reason_1c = str(row.get('Вид_отсутствия', '')).strip() + has_1c_reason = pd.notna(row.get('Вид_отсутствия')) and reason_1c != '' + is_excluded = row.get('is_excluded', False) + has_static_reason = fio_clean in static_reasons_dict + + is_fio_whitelisted_in_kb = fio_clean.lower() in kb_rules_text + + # 🚨 АНОМАЛИЯ 1: Пришел по СКУД, но в 1С числится отсутствие + if is_present and has_1c_reason: + if not is_fio_whitelisted_in_kb: + anomalies.append({ + "type": "ФИЗИЧЕСКОЕ ПРИСУТСТВИЕ ПРИ ОФИЦИАЛЬНОМ ОТСУТСТВИИ", + "fio": fio, + "details": f"Сотрудник пришел по СКУД, но в 1С оформлено: '{reason_1c}'" + }) + + # 🚨 АНОМАЛИЯ 2: Удаленщик физически пришел в офис + if is_present and has_static_reason: + static_val = static_reasons_dict[fio_clean] + if 'удален' in str(static_val).lower(): + if not is_fio_whitelisted_in_kb: + anomalies.append({ + "type": "УДАЛЁНЩИК В ОФИСЕ", + "fio": fio, + "details": f"Сотрудник числится как '{static_val}' в реестре, но сегодня зафиксирован вход по СКУД." + }) + + # 🚨 АНОМАЛИЯ 3: Конфликт 1С и Статического реестра + if has_static_reason and has_1c_reason: + static_val = static_reasons_dict[fio_clean] + if reason_1c.lower() != static_val.lower() and not is_present: + if not is_fio_whitelisted_in_kb: + anomalies.append({ + "type": "КОНФЛИКТ СПРАВОЧНИКОВ (1С vs РЕЕСТР)", + "fio": fio, + "details": f"В стат. реестре указано '{static_val}', а в 1С оформлен документ '{reason_1c}'" + }) + + return anomalies + + def aggregate_scud_by_employee(df, debug=False): - """ - Агрегирует записи СКУД по сотруднику (fio_clean). - Устраняет дубликаты карт/табельных номеров (например, Кондрашова Е.В.): - Если человек пришел хотя бы по одной карте — Пришел = True. - """ if df.empty or 'fio_clean' not in df.columns: return df @@ -53,10 +98,6 @@ def aggregate_scud_by_employee(df, debug=False): best_row = group.iloc[0].to_dict() best_row['Пришел'] = is_present - - if debug and len(group) > 1: - print(f" [DEBUG] Найдено дубликатов карт/записей для {fio_clean}: {len(group)} шт. Статус присутствия: {is_present}") - aggregated.append(best_row) return pd.DataFrame(aggregated) @@ -64,7 +105,8 @@ def aggregate_scud_by_employee(df, debug=False): def main(): parser = argparse.ArgumentParser(description="Система модульного контроллинга СКУД ⟷ 1С") - parser.add_argument('-d', '--debug', action='store_true', help="Запуск в режиме отладки (подробный вывод)") + parser.add_argument('-d', '--debug', action='store_true', help="Запуск в режиме отладки") + parser.add_argument('--skip-export', action='store_true', help="Пропустить выгрузку СКУД из MS SQL") args = parser.parse_args() DEBUG = args.debug @@ -73,8 +115,22 @@ def main(): print(f"ЗАПУСК СИСТЕМЫ МОДУЛЬНОГО КОНТРОЛЛИНГА СКУД ⟷ 1С {'[DEBUG MODE]' if DEBUG else ''}") print("=" * 60) - # 0. Валидация файлов - print("[0/5] Проверка актуальности и свежести входных файлов...") + # Загружаем существующие правила из Базы Знаний + kb_data = load_knowledge_base() + kb_rules = kb_data.get("rules", []) + + # 0. Выгрузка из БД + if not args.skip_export: + print("[0/5] Экспорт свежих данных из базы СКУД 'Орион Pro'...") + try: + run_export(debug=DEBUG) + except Exception as e: + print(f"[⚠️] Ошибка автоэкспорта из БД: {e}. Используем файлы в data/scud.") + else: + print("[0/5] Пропуск прямого экспорта из БД (--skip-export)...") + + # 1. Валидация + print("[1/5] Проверка актуальности и свежести входных файлов...") is_valid, warnings, errors = check_file_freshness() if warnings: @@ -93,40 +149,43 @@ def main(): print("[✓] Проверка актуальности успешно пройдена!\n") - # 1. Загрузка данных - print("[1/5] Загрузка данных СКУД, 1С, реестра статических причин и исключений...") + # 2. Загрузка данных + print("[2/5] Загрузка данных из data/scud, data/1c, реестра причин и исключений...") raw_scud_df, staff_fios_clean, df_absent = load_all_data() static_reasons_dict = load_static_reason_workers() - if DEBUG: - print(f" [DEBUG] Загружено записей СКУД: {len(raw_scud_df)}") - print(f" [DEBUG] Загружено сотрудников из 1С: {len(staff_fios_clean)}") - print(f" [DEBUG] Загружено докум. отсутствий 1С: {len(df_absent)}") - print(f" [DEBUG] Загружено статических причин: {len(static_reasons_dict)}") - - # 2. ИИ-сверка ФИО - print("[2/5] Проверка опечаток и сопоставление ФИО через ИИ...") + # 3. ИИ-сверка ФИО (СКУД + 1С) + print("[3/5] Проверка опечаток и сопоставление ФИО через ИИ...") scud_unrecognized = raw_scud_df[~raw_scud_df['fio_clean'].isin(staff_fios_clean)]['fio_clean'].tolist() - fio_mapping = ai_verify_scud_against_staff(scud_unrecognized, staff_fios_clean) + fio_mapping_scud = ai_verify_scud_against_staff(scud_unrecognized, staff_fios_clean) - if fio_mapping: - print(f" [ИИ] Найдено и исправлено совпадений/опечаток: {len(fio_mapping)}") + if fio_mapping_scud: + print(f" [ИИ] Найдено совпадений в СКУД: {len(fio_mapping_scud)}") raw_scud_df['fio_clean'] = raw_scud_df['fio_clean'].apply( - lambda x: fio_mapping[x]['staff_fio'] if x in fio_mapping else x + lambda x: fio_mapping_scud[x]['staff_fio'] if x in fio_mapping_scud else x ) - # 2.1 Агрегация нескольких карт/дубликатов + absent_unrecognized = df_absent[~df_absent['fio_clean'].isin(staff_fios_clean)]['fio_clean'].tolist() + if absent_unrecognized: + fio_mapping_absent = ai_verify_scud_against_staff(absent_unrecognized, staff_fios_clean) + if fio_mapping_absent: + print(f" [ИИ] Найдено совпадений в 1С Отсутствиях: {len(fio_mapping_absent)}") + df_absent['fio_clean'] = df_absent['fio_clean'].apply( + lambda x: fio_mapping_absent[x]['staff_fio'] if x in fio_mapping_absent else x + ) + raw_scud_df = aggregate_scud_by_employee(raw_scud_df, debug=DEBUG) - # 3. Объединение данных и иерархия приоритетов - print("[3/5] Объединение реестров 1С, СКУД, статических причин и исключений...") + # 4. Объединение и детекция + print("[4/5] Объединение реестров 1С, СКУД, статических причин и исключений...") merged = raw_scud_df[raw_scud_df['fio_clean'].isin(staff_fios_clean)].copy() - # 🎯 ПРИОРИТЕТ 1: Выгрузка докум. отсутствий 1С:ЗУП (Отсутствия сотрудников - выгрузка.xlsx) merged = merged.merge(df_absent, on='fio_clean', how='left') - # 🎯 ПРИОРИТЕТ 2: Статические причины из static_reason_workers.csv - # Применяются ТОЛЬКО если в 1С:ЗУП документа нет и сотрудник отсутствует в СКУД + # Детекция аномалий С УЧЁТОМ НАКОПЛЕННОЙ БАЗЫ ЗНАНИЙ + anomalies_list = detect_all_anomalies(merged, static_reasons_dict, kb_rules) + + # Применение статических причин (ТОЛЬКО ДЛЯ ТЕХ, КТО НЕ ПРИШЕЛ В ОФИС) if static_reasons_dict: for fio_clean, reason_val in static_reasons_dict.items(): mask_apply_static = ( @@ -136,9 +195,7 @@ def main(): ) merged.loc[mask_apply_static, 'Вид_отсутствия'] = reason_val - # 🎯 ПРИОРИТЕТ 3: Исключения по должности/департаменту/ФИО (exceptions.json) - # Если сотрудник попал в исключения (is_excluded = True), НЕ пришел в СКУД и НЕ имеет документа в 1С: - # Присваиваем ему статус "Исключение (ОВК/Подрядчики)", чтобы он НЕ ПОПАДАЛ в неизвестные! + # Применение исключений mask_apply_exceptions = ( (merged['Пришел'] == False) & (merged['Вид_отсутствия'].isna() | (merged['Вид_отсутствия'].astype(str).str.strip() == '')) & @@ -153,43 +210,42 @@ def main(): if DEBUG: print("\n--- [DEBUG STATS] ---") print(f" Всего по списку: {len(merged)}") - print(f" Присутствуют на рабочем месте: {len(merged[merged['Пришел'] == True])}") - print(f" Официально отсутствуют (1С + стат. причины + исключения): {len(absent_explained)}") + is_working_mask = (merged['Пришел'] == True) | ( + merged['Вид_отсутствия'].astype(str).str.lower().str.contains('командировк|удален|дистанцион|разъездн', regex=True, na=False) + ) + print(f" Работают (офис / удаленка / командировки): {len(merged[is_working_mask])}") + print(f" Официально отсутствуют: {len(absent_explained)}") print(f" Истинно неизвестные случаи: {len(absent_unexplained)}") - if not absent_unexplained.empty: - print(" Список неизвестных:") - for fio in absent_unexplained['Сотрудник'].dropna().tolist(): - print(f" - {fio}") + print(f" Обнаружено комплексных аномалий: {len(anomalies_list)}") print("---------------------\n") - # 4. ИИ-анализ и интерактивная проверка (HITL) - print("[4/5] Запуск ИИ-аудитора и проверка аномалий...") + # 5. ИИ-аудитор + print("[5/5] Запуск ИИ-аудитора и глубокий анализ аномалий...") report_text = generate_markdown_report( merged_df=merged, absent_explained=absent_explained, absent_unexplained=absent_unexplained, scud_present_but_absent_in_1c=scud_present_but_absent_in_1c, + anomalies_list=anomalies_list, raw_scud_df=raw_scud_df, date_str=DATE_TODAY ) suspicious_cases = [] - if not scud_present_but_absent_in_1c.empty: - for _, row in scud_present_but_absent_in_1c.iterrows(): + if anomalies_list: + for a in anomalies_list: suspicious_cases.append({ - 'fio_target': row.get('Сотрудник', ''), - 'reason': f"Пришел по СКУД, но в 1С зафиксировано: {row.get('Вид_отсутствия', '')}" + 'fio_target': a['fio'], + 'reason': f"{a['type']}: {a['details']}" }) review_ai_decisions(report_text, suspicious_cases) - # 5. Формирование Excel и Markdown отчетов - print("[5/5] Формирование итоговых Excel и Markdown отчетов...") generate_summary_excel(merged_df=merged, date_str=DATE_TODAY) generate_detailed_excel(merged_df=merged, date_str=DATE_YESTERDAY) print("\n" + "=" * 60) - print("ГОТОВАЯ ТЕКСТОВАЯ СВОДКА ИИ:") + print("ГОТОВАЯ ТЕКСТОВАЯ СВОДКА ИИС-АУДИТОРА:") print("=" * 60) print(report_text) diff --git a/modules/ai_verifier.py b/modules/ai_verifier.py index 4dec35a..2f70819 100644 --- a/modules/ai_verifier.py +++ b/modules/ai_verifier.py @@ -3,27 +3,34 @@ import re import requests from config import OLLAMA_URL, OLLAMA_MODEL -def ask_ollama(prompt, model=OLLAMA_MODEL, system_prompt=None): - """Базовая функция отправки запросов к Ollama""" - if system_prompt is None: - system_prompt = "Ты — офисный аудитор безопасности и кадрового учета." - +def ask_ollama(prompt, system_prompt=None): + """ + Универсальная функция отправки запросов к Ollama (поддерживает /api/generate и /api/chat) + """ payload = { - "model": model, - "messages": [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": prompt} - ], + "model": OLLAMA_MODEL, + "prompt": f"{system_prompt}\n\n{prompt}" if system_prompt else prompt, "stream": False } + try: - response = requests.post(OLLAMA_URL, json=payload, timeout=60) + response = requests.post(OLLAMA_URL, json=payload, timeout=120) if response.status_code == 200: - return response.json()['message']['content'] + data = response.json() + # Проверяем формат /api/generate + if "response" in data: + return data["response"].strip() + # Проверяем формат /api/chat + elif "message" in data and "content" in data["message"]: + return data["message"]["content"].strip() + else: + return f"⚠️ Неизвестная структура ответа Ollama: {data}" else: - return f"Ошибка API Ollama: {response.status_code}" + return f"⚠️ Ошибка Ollama (Код {response.status_code}): {response.text}" + except requests.exceptions.ConnectionError: + return "⚠️ Не удалось подключиться к Ollama. Проверьте, запущен ли сервис (ollama run qwen2.5:14b)." except Exception as e: - return f"Ошибка подключения к Ollama: {e}" + return f"⚠️ Ошибка при обращении к Ollama: {e}" def ai_verify_scud_against_staff(unrecognized_scud_fios, staff_fios): """ИИ-сверка опечаток и несовпадений ФИО между СКУД и 1С:ЗУП""" diff --git a/modules/data_loader.py b/modules/data_loader.py index 24cc1c4..dd50cea 100644 --- a/modules/data_loader.py +++ b/modules/data_loader.py @@ -3,7 +3,8 @@ import pandas as pd import warnings from config import ( normalize_fio, clean_scud_fio_light, load_exceptions, - DATA_DIR, SCUD_FILE, SCUD_FILE_TODAY, SCUD_FILE_YESTERDAY + DATA_DIR, SCUD_FILE, SCUD_FILE_TODAY, SCUD_FILE_YESTERDAY, + STAFF_FILE_1C, ABSENT_FILE_1C ) warnings.filterwarnings('ignore', category=UserWarning, module='pandas') @@ -20,19 +21,17 @@ def is_excluded(fio, dept, pos, exceptions): return False def load_staff_data(): - staff_path = os.path.join(DATA_DIR, "Штатные сотрудники - ОУП.xlsx") - if not os.path.exists(staff_path): - raise FileNotFoundError(f"Файл штатного расписания не найден: {staff_path}") - df_staff = pd.read_excel(staff_path, skiprows=7) + if not os.path.exists(STAFF_FILE_1C): + raise FileNotFoundError(f"Файл штатного расписания не найден по пути: {STAFF_FILE_1C}") + df_staff = pd.read_excel(STAFF_FILE_1C, skiprows=7) df_staff = df_staff.rename(columns={df_staff.columns[1]: 'ФИО'}) df_staff['fio_clean'] = df_staff['ФИО'].apply(normalize_fio) return df_staff def load_absent_data(): - absent_path = os.path.join(DATA_DIR, "Отсутствия сотрудников - выгрузка.xlsx") - if not os.path.exists(absent_path): - raise FileNotFoundError(f"Файл отсутствий не найден: {absent_path}") - df_absent_raw = pd.read_excel(absent_path, skiprows=3) + if not os.path.exists(ABSENT_FILE_1C): + raise FileNotFoundError(f"Файл отсутствий не найден по пути: {ABSENT_FILE_1C}") + df_absent_raw = pd.read_excel(ABSENT_FILE_1C, skiprows=3) df_absent = df_absent_raw[1:].copy().reset_index(drop=True) df_absent = df_absent.rename(columns={df_absent.columns[0]: 'ФИО', df_absent.columns[2]: 'Вид_отсутствия'}) df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio) @@ -41,7 +40,7 @@ def load_absent_data(): def load_scud_data(): scud_path = SCUD_FILE_TODAY if os.path.exists(SCUD_FILE_TODAY) else (SCUD_FILE_YESTERDAY if os.path.exists(SCUD_FILE_YESTERDAY) else SCUD_FILE) if not os.path.exists(scud_path): - raise FileNotFoundError(f"Файл СКУД не найден в папке {DATA_DIR}") + raise FileNotFoundError(f"Файл СКУД не найден в папке data/scud: {scud_path}") df_scud = pd.read_excel(scud_path) exceptions = load_exceptions() @@ -63,7 +62,6 @@ def load_scud_data(): 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/modules/data_validator.py b/modules/data_validator.py index ce005d3..da812a2 100644 --- a/modules/data_validator.py +++ b/modules/data_validator.py @@ -1,46 +1,34 @@ import os -import pandas as pd -from datetime import datetime, timedelta -from config import DATA_DIR, DATE_TODAY, DATE_YESTERDAY +import time +from datetime import datetime +from config import SCUD_FILE_TODAY, STAFF_FILE_1C, ABSENT_FILE_1C -def check_file_freshness(): + +def check_file_freshness(max_age_hours=24): """ - Проверяет наличие и актуальность файлов СКУД и 1С. - Возвращает (is_valid, warnings_list, errors_list) + Проверяет существование и свежесть входных файлов. """ - errors = [] warnings = [] + errors = [] + now = time.time() + max_age_sec = max_age_hours * 3600 - # Ожидаемые имена файлов - scud_today_file = os.path.join(DATA_DIR, f"Сотрудники_{DATE_TODAY}.xlsx") - scud_yesterday_file = os.path.join(DATA_DIR, f"Сотрудники_{DATE_YESTERDAY}.xlsx") - staff_file = os.path.join(DATA_DIR, "Штатные сотрудники - ОУП.xlsx") - absent_file = os.path.join(DATA_DIR, "Отсутствия сотрудников - выгрузка.xlsx") - - now = datetime.now() - - # 1. Проверка файла СКУД за СЕГОДНЯ - if not os.path.exists(scud_today_file): - errors.append(f"❌ КРИТИЧЕСКАЯ ОШИБКА: Отсутствует актуальный файл СКУД за сегодня: 'Сотрудники_{DATE_TODAY}.xlsx'!") - else: - # Проверяем дату изменения файла (mtime) - file_mtime = datetime.fromtimestamp(os.path.getmtime(scud_today_file)) - if file_mtime.date() < now.date(): - errors.append(f"⚠️ ОШИБКА АКТУАЛЬНОСТИ: Файл 'Сотрудники_{DATE_TODAY}.xlsx' существует, но изменялся прошлым днем ({file_mtime.strftime('%d.%m.%Y %H:%M')})!") - - # 2. Проверка файла СКУД за ВЧЕРА - if not os.path.exists(scud_yesterday_file): - warnings.append(f"⚠️ ПРЕДУПРЕЖДЕНИЕ: Файл СКУД за вчера ({DATE_YESTERDAY}) не найден. Детальный отчет за вчера может быть неполным.") - - # 3. Проверка файлов 1С (Штат и Отсутствия) - for f_path, f_name in [(staff_file, "Штатные сотрудники"), (absent_file, "Выгрузка отсутствий")]: - if not os.path.exists(f_path): - errors.append(f"❌ КРИТИЧЕСКАЯ ОШИБКА: Файл 1С '{f_name}' не найден в папке data/!") + # 1. Проверка файлов 1С в data/1c + for file_path, label in [(STAFF_FILE_1C, "Штатное расписание 1С"), (ABSENT_FILE_1C, "Отсутствия 1С")]: + if not os.path.exists(file_path): + errors.append(f"Файл '{label}' не найден по пути: {file_path}") else: - file_mtime = datetime.fromtimestamp(os.path.getmtime(f_path)) - days_old = (now - file_mtime).days - if days_old >= 2: - warnings.append(f"⚠️ УСТАРЕВШИЕ ДАННЫЕ 1С: Файл '{os.path.basename(f_path)}' не обновлялся {days_old} дн. (Дата: {file_mtime.strftime('%d.%m.%Y')})") + mtime = os.path.getmtime(file_path) + age_hours = (now - mtime) / 3600 + if age_hours > max_age_hours: + warnings.append( + f"Файл '{label}' обновлялся давно ({age_hours:.1f} ч. назад). " + f"Дата изменения: {datetime.fromtimestamp(mtime).strftime('%d.%m.%Y %H:%M')}" + ) + + # 2. Проверка файла СКУД в data/scud + if not os.path.exists(SCUD_FILE_TODAY): + warnings.append(f"Сегодняшний файл СКУД еще не сформирован: {SCUD_FILE_TODAY}") is_valid = len(errors) == 0 return is_valid, warnings, errors \ No newline at end of file diff --git a/modules/scud_export.py b/modules/scud_export.py index 4d1842e..2d47d9f 100644 --- a/modules/scud_export.py +++ b/modules/scud_export.py @@ -1,11 +1,5 @@ """ -Скрипт экспорта данных СКУД (Orion) в Excel. -Оптимизированная супер-быстрая версия (использует ROW_NUMBER() вместо подзапросов). - -Запуск: - python scud_export.py - python scud_export.py -d - python scud_export.py --date 28.07.2026 -d +Модуль автоматического экспорта данных СКУД (Orion) из MS SQL Server в Excel. """ import argparse @@ -19,13 +13,12 @@ import pandas as pd import pyodbc from openpyxl.utils import get_column_letter +from config import SCUD_DIR + warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy connectable") -# ============================================================================== -# 1. Настройки путей и логов -# ============================================================================== SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -LOG_DIR = os.path.join(SCRIPT_DIR, "logs") +LOG_DIR = os.path.join(SCRIPT_DIR, "..", "logs") os.makedirs(LOG_DIR, exist_ok=True) TODAY_DATE_STR = datetime.now().strftime("%d.%m.%Y") @@ -34,13 +27,14 @@ LOG_FILE = os.path.join(LOG_DIR, f"export_{TODAY_DATE_STR}.log") logger = logging.getLogger("scud_export") logger.setLevel(logging.INFO) -_file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8") -_console_handler = logging.StreamHandler(sys.stdout) -_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S") -_file_handler.setFormatter(_formatter) -_console_handler.setFormatter(_formatter) -logger.addHandler(_file_handler) -logger.addHandler(_console_handler) +if not logger.handlers: + _file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8") + _console_handler = logging.StreamHandler(sys.stdout) + _formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S") + _file_handler.setFormatter(_formatter) + _console_handler.setFormatter(_formatter) + logger.addHandler(_file_handler) + logger.addHandler(_console_handler) def log(message: str, level: str = "INFO"): @@ -56,21 +50,12 @@ def log(message: str, level: str = "INFO"): logger.log(level_map.get(level, logging.INFO), message) -# ============================================================================== -# 2. Настройки базы данных и папки -# ============================================================================== SERVER_NAME = r"192.168.1.253\SQL" DATABASE_NAME = "Orion-14.01.21-1" SQL_USER = "sa" SQL_PASSWORD = "123456" - -FOLDER_PATH = r"C:\scud_exchsnge" ODBC_DRIVER = "SQL Server Native Client 10.0" - -# ============================================================================== -# 3. СУПЕР-ОПТИМИЗИРОВАННЫЙ SQL-ЗАПРОС (Мгновенное выполнение) -# ============================================================================== SQL_QUERY_TEMPLATE = r""" DECLARE @InputDate DATE = '{target_date}'; DECLARE @TargetDate DATE = @InputDate; @@ -78,7 +63,6 @@ DECLARE @TargetDate DATE = @InputDate; DECLARE @StartDate DATETIME = CAST(@TargetDate AS DATETIME); DECLARE @EndDate DATETIME = DATEADD(SECOND, -1, DATEADD(DAY, 1, @StartDate)); --- 1. Фильтруем логи только за нужные 24 часа за один проход WITH DailyLogs AS ( SELECT log.HozOrgan AS EmployeeID, @@ -95,7 +79,6 @@ WITH DailyLogs AS ( AND log.HozOrgan > 0 AND (log.Event IN (28, 29, 26, 27, 32, 33) OR log.Mode IN (1, 2)) ), --- 2. Агрегируем времена входа/выхода и определяем последнее событие Passages AS ( SELECT EmployeeID, @@ -167,9 +150,6 @@ ORDER BY p.Name ASC; """ -# ============================================================================== -# 4. Вспомогательные функции -# ============================================================================== def auto_fit_columns(file_path: str, sheet_name: str, padding: float = 2.0, min_width: float = 8.0, max_width: float = 60.0): from openpyxl import load_workbook @@ -196,7 +176,6 @@ def get_targets(input_date: str | None): try: parsed = datetime.strptime(input_date, "%d.%m.%Y").date() targets.append({"name": "Указанная дата", "date": parsed}) - log(f"Запущен ручной режим для даты: {input_date}") except ValueError: log(f"ОШИБКА: Неверный формат даты '{input_date}'. Используйте ДД.ММ.ГГГГ", "ERROR") sys.exit(1) @@ -211,16 +190,13 @@ def get_targets(input_date: str | None): return targets -# ============================================================================== -# 5. Основная функция выгрузки -# ============================================================================== -def run_export(input_date: str | None, debug: bool = False): +def run_export(input_date: str | None = None, debug: bool = False): if debug: logger.setLevel(logging.DEBUG) log("=== ВКЛЮЧЕН РЕЖИМ ОТЛАДКИ (DEBUG MODE) ===", "WARNING") - log("=== Запуск скрипта экспорта СКУД ===") - os.makedirs(FOLDER_PATH, exist_ok=True) + log("=== [ЭТАП 0] Выгрузка свежих данных СКУД напрямую из БД Орион ===") + os.makedirs(SCUD_DIR, exist_ok=True) targets = get_targets(input_date) @@ -234,6 +210,7 @@ def run_export(input_date: str | None, debug: bool = False): f"Encrypt=no;" ) + success = True for target in targets: processing_date = target["date"] processing_date_str = processing_date.strftime("%d.%m.%Y") @@ -242,15 +219,13 @@ def run_export(input_date: str | None, debug: bool = False): log(f"--- Обработка периода: {period_label} ({processing_date_str}) ---") file_name = f"Сотрудники_{processing_date_str}.xlsx" - file_path = os.path.join(FOLDER_PATH, file_name) + file_path = os.path.join(SCUD_DIR, file_name) if os.path.exists(file_path): - log(f"Файл {file_name} существует. Перезаписываем...") try: os.remove(file_path) except OSError as e: - log(f"ОШИБКА при удалении файла {file_name}: {e}", "ERROR") - continue + log(f"ОШИБКА при удалении старого файла {file_name}: {e}", "ERROR") sql_query = SQL_QUERY_TEMPLATE.format(target_date=processing_date.strftime("%Y-%m-%d")) @@ -261,51 +236,28 @@ def run_export(input_date: str | None, debug: bool = False): log(f"База вернула {len(df)} строк за {processing_date_str}") - if debug: - present_df = df[df["Начало_дня"] != "Нет входа"] - still_inside_df = df[(df["Начало_дня"] != "Нет входа") & (df["Конец_дня"] == "Нет выхода")] - left_df = df[(df["Начало_дня"] != "Нет входа") & (df["Конец_дня"] != "Нет выхода")] - - print("\n" + "=" * 50) - print(f"[DEBUG STATS] Дата: {processing_date_str}") - print(f" Всего активных сотрудников в БД: {len(df)}") - print(f" Присутствуют / были сегодня: {len(present_df)}") - print(f" Физически находятся в здании: {len(still_inside_df)} (Конец_дня = 'Нет выхода')") - print(f" Завершили рабочий день (Ушли): {len(left_df)}") - - if not still_inside_df.empty: - print("\n Первые 5 сотрудников, находящихся сейчас в здании:") - for _, r in still_inside_df.head(5).iterrows(): - print(f" • {r['Сотрудник']} | Вход: {r['Начало_дня']} | В здании: {r['Находился_в_здании']}") - print("=" * 50 + "\n") - if len(df) > 0: df.to_excel(file_path, sheet_name="Отчет", index=False, engine="openpyxl") auto_fit_columns(file_path, sheet_name="Отчет") - log(f"Успешно создан файл {file_path}", "SUCCESS") + log(f"[✓] Успешно экспортирован файл: data/scud/{file_name}", "SUCCESS") + else: + log(f"Запрос за {processing_date_str} вернул 0 строк.", "WARNING") except Exception as e: - log(f"ОШИБКА при обработке даты {processing_date_str}: {e}", "ERROR") + log(f"🛑 ОШИБКА выгрузки СКУД за {processing_date_str}: {e}", "ERROR") + success = False finally: if connection is not None: connection.close() - log("=== Завершение работы скрипта ===") + log("=== Выгрузка СКУД завершена ===\n") + return success if __name__ == "__main__": parser = argparse.ArgumentParser(description="Экспорт данных СКУД в Excel") - parser.add_argument( - "--date", - dest="input_date", - default=None, - help='Дата в формате ДД.ММ.ГГГГ (например, "28.07.2026")', - ) - parser.add_argument( - "-d", "--debug", - action="store_true", - help="Запуск в режиме отладки (вывод детальной статистики)", - ) + parser.add_argument("--date", dest="input_date", default=None) + parser.add_argument("-d", "--debug", action="store_true") args = parser.parse_args() run_export(args.input_date, debug=args.debug) \ No newline at end of file diff --git a/modules/text_reporter.py b/modules/text_reporter.py index cf3e13b..6c5e2f8 100644 --- a/modules/text_reporter.py +++ b/modules/text_reporter.py @@ -2,7 +2,8 @@ import json from modules.ai_verifier import ask_ollama from modules.knowledge_base import load_knowledge_base -def generate_markdown_report(merged_df, absent_explained, absent_unexplained, scud_present_but_absent_in_1c, raw_scud_df=None, raw_staff_df=None, date_str="27.07.2026"): + +def generate_markdown_report(merged_df, absent_explained, absent_unexplained, scud_present_but_absent_in_1c, anomalies_list=None, raw_scud_df=None, raw_staff_df=None, date_str="28.07.2026"): kb = load_knowledge_base() custom_rules = kb.get("rules", []) custom_rules_str = "\n".join([f"- {r}" for r in custom_rules]) if custom_rules else "Специфических правил компании пока нет." @@ -25,67 +26,65 @@ def generate_markdown_report(merged_df, absent_explained, absent_unexplained, sc detailed_cases_for_ai.append({ "fio_clean": fio_clean, - "staff_records_count": len(staff_matches), "staff_records_1C": staff_matches, "scud_records_LOGS": scud_matches }) - cols_crit = [c for c in ['Сотрудник', 'Подразделение', 'Должность', 'Вид_отсутствия'] if c in scud_present_but_absent_in_1c.columns] - critical_list = scud_present_but_absent_in_1c[cols_crit].to_dict(orient='records') - - # Исходные точные цифры Python total_staff = len(merged_df) - present_cnt = len(merged_df[merged_df['Пришел'] == True]) + is_working_mask = (merged_df['Пришел'] == True) | ( + merged_df['Вид_отсутствия'].astype(str).str.lower().str.contains('командировк|удален|дистанцион|разъездн', regex=True, na=False) + ) + present_cnt = len(merged_df[is_working_mask]) explained_cnt = len(absent_explained) unexplained_cnt = len(absent_unexplained) + anomalies_cnt = len(anomalies_list) if anomalies_list else 0 prompt = f""" -Ты — старший аудитор кадровой безопасности и систем СКУД. Проанализируй данные за {date_str} и составь итоговый отчет. +Ты — старший аудитор кадровой безопасности и контроллинга СКУД. +Проанализируй аномалии и пересечения данных за {date_str}. -📌 ДИНАМИЧЕСКИЕ ПРАВИЛА И НАКОПЛЕННЫЙ ОПЫТ ПРЕДПРИЯТИЯ: +📌 НАКОПЛЕННЫЕ ПРАВИЛА ПРЕДПРИЯТИЯ И ИСКЛЮЧЕНИЯ: {custom_rules_str} -🚨 СТРОГИЕ ПРАВИЛА МАТЕМАТИКИ И КАТЕГОРИЙ: -1. Цифры сводки ДОЛЖНЫ СТРОГО СОВПАДАТЬ с первично посчитанным массивом: - - Всего офисных сотрудников: {total_staff} - - Присутствуют на рабочем месте: {present_cnt} - - Официально отсутствуют: {explained_cnt} - - Неизвестно (истинно неотмеченные): {unexplained_cnt} чел. -2. Заголовок "#### Неизвестные случаи: {unexplained_cnt}" должен иметь ровно ту же цифру {unexplained_cnt}. -3. Нумерованный список ниже ДОЛЖЕН содержать ровно {unexplained_cnt} сотрудников из массива `detailed_cases_for_ai` без пропусков. +🚨 ВХОДНЫЕ МЕТРИКИ: +- Всего сотрудников: {total_staff} +- На работе / Удаленно / Командировка: {present_cnt} +- Официально отсутствуют (1С/Реестры): {explained_cnt} +- Неизвестно (требуют выяснения): {unexplained_cnt} +- Выявлено аномалий и конфликтов реестров: {anomalies_cnt} --- -📊 ВХОДНЫЕ МИКРО-ДАННЫЕ НЕИЗВЕСТНЫХ ({unexplained_cnt} чел): -{json.dumps(detailed_cases_for_ai, ensure_ascii=False, indent=2)} +🚨 ОБНАРУЖЕННЫЕ АНОМАЛИИ И КОНФЛИКТЫ МЕЖДУ СКУД, 1С, СТАТИЧЕСКИМИ РЕЕСТРАМИ И ИСКЛЮЧЕНИЯМИ ({anomalies_cnt} шт.): +{json.dumps(anomalies_list, ensure_ascii=False, indent=2)} -🚨 СПИСОК КРИТИЧЕСКИХ АНОМАЛИЙ: -{json.dumps(critical_list, ensure_ascii=False, indent=2)} +--- +📊 СПИСОК НЕИЗВЕСТНЫХ СЛУЧАЕВ ({unexplained_cnt} чел): +{json.dumps(detailed_cases_for_ai, ensure_ascii=False, indent=2)} СТРОГИЙ ШАБЛОН ОТВЕТА: -**Сводка контроллинга СКУД и 1С:ЗУП на {date_str} в 12:00** +**Сводка контроллинга СКУД и 1С:ЗУП на {date_str}** - Всего офисных сотрудников: **{total_staff}** -- Присутствуют на рабочем месте: **{present_cnt}** +- Работают (офис / удаленка / командировки): **{present_cnt}** - Официально отсутствуют: **{explained_cnt}** -- Неизвестно (истинно неотмеченные после проверки ИИ): **{unexplained_cnt}** чел. +- Неизвестно (истинно неотмеченные): **{unexplained_cnt}** чел. +- Выявлено аномалий/конфликтов реестров: **{anomalies_cnt}** шт. -#### ⚠️ Подозрения на ложные срабатывания СКУД / Совмещения: -(Укажи сотрудников с аномалиями/вторыми проходами, например Кондрашову Е.В., если у нее дубликат карты) +#### 🚨 Выявленные ИИ аномалии и конфликты источников ({anomalies_cnt}): +(Подробно разбери каждый конфликт из `anomalies_list`. Опиши суть нарушения. Если аномалий нет — напиши "Аномалий и конфликтов не обнаружено.") -#### Критические аномалии (Работа в отпуске / на больничном): -(Детально опиши сотрудников из списка КРИТИЧЕСКИХ АНОМАЛИЙ. Если их нет — напиши "Критических нарушений не выявлено.") +#### ⚠️ Подозрения на ложные срабатывания / Совмещения: +(Укажи сотрудников с повторными проходами или несколькими картами, если такие есть) #### Неизвестные случаи: {unexplained_cnt} -(Выведи НУМЕРОВАННЫЙ СПИСОК ВСЕХ {unexplained_cnt} человек строго в формате: -1. ФИО — [Аббревиатура], Должность -2. ФИО — [Аббревиатура], Должность -... -{unexplained_cnt}. ФИО — [Аббревиатура], Должность) +(Выведи НУМЕРОВАННЫЙ СПИСОК ВСЕХ {unexplained_cnt} человек в формате: +1. ФИО — Подразделение, Должность +2. ФИО — Подразделение, Должность) ### Рекомендации: -... +(Сформулируй 2-3 общие системные рекомендации для кадровой службы. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО перечислять конкретные ФИО сотрудников в скобках или тексте рекомендаций — они уже перечислены выше.) """ - sys_prompt = "Ты — кадровый аудитор. Выводи ровно то количество людей в списке, которое указано в переменной unexplained_cnt." + sys_prompt = "Ты — эксперт кадрового аудита. Пиши сжато, лаконично, без дублирования ФИО сотрудников в блоке рекомендаций." return ask_ollama(prompt, system_prompt=sys_prompt) \ No newline at end of file