Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fc417f3a7 | ||
|
|
2603627098 |
+40
-7
@@ -1,9 +1,42 @@
|
|||||||
# Входные и сгенерированные файлы
|
# --- 1. Python и виртуальное окружение ---
|
||||||
data/*.xlsx
|
|
||||||
output/*
|
|
||||||
|
|
||||||
# Системные и временные файлы Python
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
venv/
|
||||||
.venv/
|
.venv/
|
||||||
.vscode/
|
env/
|
||||||
|
.env
|
||||||
|
|
||||||
|
# --- 2. Настройки IDE и редакторов ---
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# --- 3. Логи скриптов и выгрузок ---
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# --- 4. Временные и выходные файлы отчетов ---
|
||||||
|
output/
|
||||||
|
*.tmp
|
||||||
|
~$*.xlsx
|
||||||
|
|
||||||
|
# --- 5. Рабочие данные СКУД и 1С (Выгрузки) ---
|
||||||
|
data/scud/*
|
||||||
|
data/1c/*
|
||||||
|
!data/scud/.gitkeep
|
||||||
|
!data/1c/.gitkeep
|
||||||
|
|
||||||
|
# Исключаем сырые Excel-файлы в корне и папке data
|
||||||
|
*.xlsx
|
||||||
|
*.xls
|
||||||
|
*.csv
|
||||||
|
|
||||||
|
# Разрешаем отслеживать только статический справочник причин, если он нужен
|
||||||
|
!data/static_reason_workers.csv
|
||||||
|
!data/knowledge_base.json
|
||||||
|
|
||||||
|
# --- 6. Системные файлы ОС ---
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -1,78 +1,64 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import pandas as pd
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
# 📁 Пути к директориям
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
DATA_DIR = os.path.join(BASE_DIR, "data")
|
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")
|
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()
|
||||||
now = datetime.now()
|
DATE_TODAY = NOW.strftime("%d.%m.%Y")
|
||||||
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
|
|
||||||
|
|
||||||
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_TODAY = os.path.join(SCUD_DIR, f"Сотрудники_{DATE_TODAY}.xlsx")
|
||||||
SCUD_FILE_YESTERDAY = os.path.join(DATA_DIR, f"Сотрудники_{DATE_YESTERDAY}.xlsx")
|
SCUD_FILE_YESTERDAY = os.path.join(SCUD_DIR, f"Сотрудники_{DATE_YESTERDAY}.xlsx")
|
||||||
SCUD_FILE = SCUD_FILE_TODAY if os.path.exists(SCUD_FILE_TODAY) else SCUD_FILE_YESTERDAY
|
SCUD_FILE = os.path.join(SCUD_DIR, "Сотрудники.xlsx")
|
||||||
|
|
||||||
STAFF_FILE = os.path.join(DATA_DIR, "Штатные сотрудники - ОУП.xlsx")
|
STAFF_FILE_1C = os.path.join(ZUP_1C_DIR, "Штатные сотрудники - ОУП.xlsx")
|
||||||
ABSENT_FILE = os.path.join(DATA_DIR, "Отсутствия сотрудников - выгрузка.xlsx")
|
ABSENT_FILE_1C = os.path.join(ZUP_1C_DIR, "Отсутствия сотрудников - выгрузка.xlsx")
|
||||||
EXCEPTIONS_FILE = os.path.join(BASE_DIR, "exceptions.json")
|
|
||||||
|
|
||||||
# 🤖 Настройки Ollama
|
# Настройки Ollama / RAG (объявляем оба имени для совместимости)
|
||||||
OLLAMA_URL = "http://10.121.18.227:11434/api/chat"
|
OLLAMA_URL = "http://10.121.18.227:11434/api/generate"
|
||||||
OLLAMA_MODEL = "qwen2.5:14b"
|
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(fio):
|
||||||
|
if not fio or not isinstance(fio, str):
|
||||||
def normalize_fio(val):
|
|
||||||
"""Строгая нормализация ФИО для точности сопоставления 1С и СКУД"""
|
|
||||||
if pd.isna(val):
|
|
||||||
return ""
|
return ""
|
||||||
val = str(val).strip().lower()
|
parts = fio.strip().split()
|
||||||
val = re.sub(r'\(.*?\)', '', val)
|
return " ".join(parts).title()
|
||||||
trans = str.maketrans('aceopxyABCEHKMOPTX', 'асеорхуАВСЕНКМОРТХ')
|
|
||||||
val = val.translate(trans).replace('ё', 'е')
|
|
||||||
return re.sub(r'\s+', ' ', val).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def clean_scud_fio_light(val):
|
def clean_scud_fio_light(fio_str):
|
||||||
"""Легкая очистка ФИО для СКУД"""
|
if not isinstance(fio_str, str):
|
||||||
if pd.isna(val):
|
|
||||||
return ""
|
return ""
|
||||||
val = str(val).strip().lower()
|
fio_clean = fio_str.replace("(сводная)", "").strip()
|
||||||
val = re.sub(r'\(.*?\)', '', val).replace('ё', 'е')
|
return normalize_fio(fio_clean)
|
||||||
return re.sub(r'\s+', ' ', val).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def load_exceptions(file_path=EXCEPTIONS_FILE):
|
def load_exceptions():
|
||||||
"""Загрузка списка исключений из JSON"""
|
import json
|
||||||
if not os.path.exists(file_path):
|
if os.path.exists(EXCEPTIONS_PATH):
|
||||||
return {"departments": [], "positions": [], "fio": [], "position_keywords": []}
|
try:
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
with open(EXCEPTIONS_PATH, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
return json.load(f)
|
||||||
return {
|
except Exception:
|
||||||
"departments": [str(d).strip().upper() for d in data.get("departments", [])],
|
pass
|
||||||
"positions": [str(p).strip().lower() for p in data.get("positions", [])],
|
return {"fio": [], "departments": [], "positions": [], "position_keywords": []}
|
||||||
"fio": [normalize_fio(f) for f in data.get("fio", [])],
|
|
||||||
"position_keywords": [str(k).strip().lower() for k in data.get("position_keywords", [])]
|
|
||||||
}
|
|
||||||
|
|
||||||
# тестовый комментарий
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"rules": [
|
"rules": [
|
||||||
"Кондрашова Елена Владимировна имеет 2 пропуска в СКУД. Если один 'Отсутствовал', а второй с временем входа — считать ПРИШЕДШЕЙ.",
|
"Кондрашова Елена Владимировна имеет 2 пропуска в СКУД. Если один 'Отсутствовал', а второй с временем входа — считать ПРИШЕДШЕЙ.",
|
||||||
"Сотрудники подразделения ОА с должностью 'Инженер-программист' часто являются внутренними совместителями."
|
"Сотрудники подразделения ОА с должностью 'Инженер-программист' часто являются внутренними совместителями.",
|
||||||
|
"Сотрудник Незнанова Валерия Игоревна: Незнанова иногда присутствует в офисе, не считать аномалией. Когда в офисе, из удаленщиков в отчете исключать!"
|
||||||
],
|
],
|
||||||
"fio_corrections": {
|
"fio_corrections": {
|
||||||
"кондрашава елена владимировна": "Кондрашова Елена Владимировна"
|
"кондрашава елена владимировна": "Кондрашова Елена Владимировна"
|
||||||
|
|||||||
@@ -6,20 +6,17 @@ import pandas as pd
|
|||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
from config import DATE_TODAY, DATE_YESTERDAY, OUTPUT_DIR, DATA_DIR, normalize_fio
|
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_validator import check_file_freshness
|
||||||
from modules.data_loader import load_all_data
|
from modules.data_loader import load_all_data
|
||||||
from modules.excel_exporter import generate_summary_excel, generate_detailed_excel
|
from modules.excel_exporter import generate_summary_excel, generate_detailed_excel
|
||||||
from modules.ai_verifier import ai_verify_scud_against_staff
|
from modules.ai_verifier import ai_verify_scud_against_staff
|
||||||
from modules.text_reporter import generate_markdown_report
|
from modules.text_reporter import generate_markdown_report
|
||||||
from modules.feedback_loop import review_ai_decisions
|
from modules.feedback_loop import review_ai_decisions
|
||||||
|
from modules.knowledge_base import load_knowledge_base
|
||||||
|
|
||||||
|
|
||||||
def load_static_reason_workers():
|
def load_static_reason_workers():
|
||||||
"""
|
|
||||||
Загружает реестр сотрудников со статическими/особыми причинами
|
|
||||||
из static_reason_workers.csv.
|
|
||||||
Возвращает словарь: {fio_clean: reason}
|
|
||||||
"""
|
|
||||||
static_path = os.path.join(DATA_DIR, "static_reason_workers.csv")
|
static_path = os.path.join(DATA_DIR, "static_reason_workers.csv")
|
||||||
if not os.path.exists(static_path):
|
if not os.path.exists(static_path):
|
||||||
return {}
|
return {}
|
||||||
@@ -33,12 +30,60 @@ def load_static_reason_workers():
|
|||||||
return {}
|
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):
|
def aggregate_scud_by_employee(df, debug=False):
|
||||||
"""
|
|
||||||
Агрегирует записи СКУД по сотруднику (fio_clean).
|
|
||||||
Устраняет дубликаты карт/табельных номеров (например, Кондрашова Е.В.):
|
|
||||||
Если человек пришел хотя бы по одной карте — Пришел = True.
|
|
||||||
"""
|
|
||||||
if df.empty or 'fio_clean' not in df.columns:
|
if df.empty or 'fio_clean' not in df.columns:
|
||||||
return df
|
return df
|
||||||
|
|
||||||
@@ -53,10 +98,6 @@ def aggregate_scud_by_employee(df, debug=False):
|
|||||||
best_row = group.iloc[0].to_dict()
|
best_row = group.iloc[0].to_dict()
|
||||||
|
|
||||||
best_row['Пришел'] = is_present
|
best_row['Пришел'] = is_present
|
||||||
|
|
||||||
if debug and len(group) > 1:
|
|
||||||
print(f" [DEBUG] Найдено дубликатов карт/записей для {fio_clean}: {len(group)} шт. Статус присутствия: {is_present}")
|
|
||||||
|
|
||||||
aggregated.append(best_row)
|
aggregated.append(best_row)
|
||||||
|
|
||||||
return pd.DataFrame(aggregated)
|
return pd.DataFrame(aggregated)
|
||||||
@@ -64,7 +105,8 @@ def aggregate_scud_by_employee(df, debug=False):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Система модульного контроллинга СКУД ⟷ 1С")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
DEBUG = args.debug
|
DEBUG = args.debug
|
||||||
@@ -73,8 +115,22 @@ def main():
|
|||||||
print(f"ЗАПУСК СИСТЕМЫ МОДУЛЬНОГО КОНТРОЛЛИНГА СКУД ⟷ 1С {'[DEBUG MODE]' if DEBUG else ''}")
|
print(f"ЗАПУСК СИСТЕМЫ МОДУЛЬНОГО КОНТРОЛЛИНГА СКУД ⟷ 1С {'[DEBUG MODE]' if DEBUG else ''}")
|
||||||
print("=" * 60)
|
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()
|
is_valid, warnings, errors = check_file_freshness()
|
||||||
|
|
||||||
if warnings:
|
if warnings:
|
||||||
@@ -93,40 +149,43 @@ def main():
|
|||||||
|
|
||||||
print("[✓] Проверка актуальности успешно пройдена!\n")
|
print("[✓] Проверка актуальности успешно пройдена!\n")
|
||||||
|
|
||||||
# 1. Загрузка данных
|
# 2. Загрузка данных
|
||||||
print("[1/5] Загрузка данных СКУД, 1С, реестра статических причин и исключений...")
|
print("[2/5] Загрузка данных из data/scud, data/1c, реестра причин и исключений...")
|
||||||
raw_scud_df, staff_fios_clean, df_absent = load_all_data()
|
raw_scud_df, staff_fios_clean, df_absent = load_all_data()
|
||||||
static_reasons_dict = load_static_reason_workers()
|
static_reasons_dict = load_static_reason_workers()
|
||||||
|
|
||||||
if DEBUG:
|
# 3. ИИ-сверка ФИО (СКУД + 1С)
|
||||||
print(f" [DEBUG] Загружено записей СКУД: {len(raw_scud_df)}")
|
print("[3/5] Проверка опечаток и сопоставление ФИО через ИИ...")
|
||||||
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] Проверка опечаток и сопоставление ФИО через ИИ...")
|
|
||||||
scud_unrecognized = raw_scud_df[~raw_scud_df['fio_clean'].isin(staff_fios_clean)]['fio_clean'].tolist()
|
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:
|
if fio_mapping_scud:
|
||||||
print(f" [ИИ] Найдено и исправлено совпадений/опечаток: {len(fio_mapping)}")
|
print(f" [ИИ] Найдено совпадений в СКУД: {len(fio_mapping_scud)}")
|
||||||
raw_scud_df['fio_clean'] = raw_scud_df['fio_clean'].apply(
|
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)
|
raw_scud_df = aggregate_scud_by_employee(raw_scud_df, debug=DEBUG)
|
||||||
|
|
||||||
# 3. Объединение данных и иерархия приоритетов
|
# 4. Объединение и детекция
|
||||||
print("[3/5] Объединение реестров 1С, СКУД, статических причин и исключений...")
|
print("[4/5] Объединение реестров 1С, СКУД, статических причин и исключений...")
|
||||||
merged = raw_scud_df[raw_scud_df['fio_clean'].isin(staff_fios_clean)].copy()
|
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')
|
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:
|
if static_reasons_dict:
|
||||||
for fio_clean, reason_val in static_reasons_dict.items():
|
for fio_clean, reason_val in static_reasons_dict.items():
|
||||||
mask_apply_static = (
|
mask_apply_static = (
|
||||||
@@ -136,9 +195,7 @@ def main():
|
|||||||
)
|
)
|
||||||
merged.loc[mask_apply_static, 'Вид_отсутствия'] = reason_val
|
merged.loc[mask_apply_static, 'Вид_отсутствия'] = reason_val
|
||||||
|
|
||||||
# 🎯 ПРИОРИТЕТ 3: Исключения по должности/департаменту/ФИО (exceptions.json)
|
# Применение исключений
|
||||||
# Если сотрудник попал в исключения (is_excluded = True), НЕ пришел в СКУД и НЕ имеет документа в 1С:
|
|
||||||
# Присваиваем ему статус "Исключение (ОВК/Подрядчики)", чтобы он НЕ ПОПАДАЛ в неизвестные!
|
|
||||||
mask_apply_exceptions = (
|
mask_apply_exceptions = (
|
||||||
(merged['Пришел'] == False) &
|
(merged['Пришел'] == False) &
|
||||||
(merged['Вид_отсутствия'].isna() | (merged['Вид_отсутствия'].astype(str).str.strip() == '')) &
|
(merged['Вид_отсутствия'].isna() | (merged['Вид_отсутствия'].astype(str).str.strip() == '')) &
|
||||||
@@ -153,43 +210,42 @@ def main():
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
print("\n--- [DEBUG STATS] ---")
|
print("\n--- [DEBUG STATS] ---")
|
||||||
print(f" Всего по списку: {len(merged)}")
|
print(f" Всего по списку: {len(merged)}")
|
||||||
print(f" Присутствуют на рабочем месте: {len(merged[merged['Пришел'] == True])}")
|
is_working_mask = (merged['Пришел'] == True) | (
|
||||||
print(f" Официально отсутствуют (1С + стат. причины + исключения): {len(absent_explained)}")
|
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)}")
|
print(f" Истинно неизвестные случаи: {len(absent_unexplained)}")
|
||||||
if not absent_unexplained.empty:
|
print(f" Обнаружено комплексных аномалий: {len(anomalies_list)}")
|
||||||
print(" Список неизвестных:")
|
|
||||||
for fio in absent_unexplained['Сотрудник'].dropna().tolist():
|
|
||||||
print(f" - {fio}")
|
|
||||||
print("---------------------\n")
|
print("---------------------\n")
|
||||||
|
|
||||||
# 4. ИИ-анализ и интерактивная проверка (HITL)
|
# 5. ИИ-аудитор
|
||||||
print("[4/5] Запуск ИИ-аудитора и проверка аномалий...")
|
print("[5/5] Запуск ИИ-аудитора и глубокий анализ аномалий...")
|
||||||
report_text = generate_markdown_report(
|
report_text = generate_markdown_report(
|
||||||
merged_df=merged,
|
merged_df=merged,
|
||||||
absent_explained=absent_explained,
|
absent_explained=absent_explained,
|
||||||
absent_unexplained=absent_unexplained,
|
absent_unexplained=absent_unexplained,
|
||||||
scud_present_but_absent_in_1c=scud_present_but_absent_in_1c,
|
scud_present_but_absent_in_1c=scud_present_but_absent_in_1c,
|
||||||
|
anomalies_list=anomalies_list,
|
||||||
raw_scud_df=raw_scud_df,
|
raw_scud_df=raw_scud_df,
|
||||||
date_str=DATE_TODAY
|
date_str=DATE_TODAY
|
||||||
)
|
)
|
||||||
|
|
||||||
suspicious_cases = []
|
suspicious_cases = []
|
||||||
if not scud_present_but_absent_in_1c.empty:
|
if anomalies_list:
|
||||||
for _, row in scud_present_but_absent_in_1c.iterrows():
|
for a in anomalies_list:
|
||||||
suspicious_cases.append({
|
suspicious_cases.append({
|
||||||
'fio_target': row.get('Сотрудник', ''),
|
'fio_target': a['fio'],
|
||||||
'reason': f"Пришел по СКУД, но в 1С зафиксировано: {row.get('Вид_отсутствия', '')}"
|
'reason': f"{a['type']}: {a['details']}"
|
||||||
})
|
})
|
||||||
|
|
||||||
review_ai_decisions(report_text, suspicious_cases)
|
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_summary_excel(merged_df=merged, date_str=DATE_TODAY)
|
||||||
generate_detailed_excel(merged_df=merged, date_str=DATE_YESTERDAY)
|
generate_detailed_excel(merged_df=merged, date_str=DATE_YESTERDAY)
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("ГОТОВАЯ ТЕКСТОВАЯ СВОДКА ИИ:")
|
print("ГОТОВАЯ ТЕКСТОВАЯ СВОДКА ИИС-АУДИТОРА:")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(report_text)
|
print(report_text)
|
||||||
|
|
||||||
|
|||||||
+21
-14
@@ -3,27 +3,34 @@ import re
|
|||||||
import requests
|
import requests
|
||||||
from config import OLLAMA_URL, OLLAMA_MODEL
|
from config import OLLAMA_URL, OLLAMA_MODEL
|
||||||
|
|
||||||
def ask_ollama(prompt, model=OLLAMA_MODEL, system_prompt=None):
|
def ask_ollama(prompt, system_prompt=None):
|
||||||
"""Базовая функция отправки запросов к Ollama"""
|
"""
|
||||||
if system_prompt is None:
|
Универсальная функция отправки запросов к Ollama (поддерживает /api/generate и /api/chat)
|
||||||
system_prompt = "Ты — офисный аудитор безопасности и кадрового учета."
|
"""
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": model,
|
"model": OLLAMA_MODEL,
|
||||||
"messages": [
|
"prompt": f"{system_prompt}\n\n{prompt}" if system_prompt else prompt,
|
||||||
{"role": "system", "content": system_prompt},
|
|
||||||
{"role": "user", "content": prompt}
|
|
||||||
],
|
|
||||||
"stream": False
|
"stream": False
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(OLLAMA_URL, json=payload, timeout=60)
|
response = requests.post(OLLAMA_URL, json=payload, timeout=120)
|
||||||
if response.status_code == 200:
|
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:
|
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:
|
except Exception as e:
|
||||||
return f"Ошибка подключения к Ollama: {e}"
|
return f"⚠️ Ошибка при обращении к Ollama: {e}"
|
||||||
|
|
||||||
def ai_verify_scud_against_staff(unrecognized_scud_fios, staff_fios):
|
def ai_verify_scud_against_staff(unrecognized_scud_fios, staff_fios):
|
||||||
"""ИИ-сверка опечаток и несовпадений ФИО между СКУД и 1С:ЗУП"""
|
"""ИИ-сверка опечаток и несовпадений ФИО между СКУД и 1С:ЗУП"""
|
||||||
|
|||||||
+9
-11
@@ -3,7 +3,8 @@ import pandas as pd
|
|||||||
import warnings
|
import warnings
|
||||||
from config import (
|
from config import (
|
||||||
normalize_fio, clean_scud_fio_light, load_exceptions,
|
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')
|
warnings.filterwarnings('ignore', category=UserWarning, module='pandas')
|
||||||
@@ -20,19 +21,17 @@ def is_excluded(fio, dept, pos, exceptions):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def load_staff_data():
|
def load_staff_data():
|
||||||
staff_path = os.path.join(DATA_DIR, "Штатные сотрудники - ОУП.xlsx")
|
if not os.path.exists(STAFF_FILE_1C):
|
||||||
if not os.path.exists(staff_path):
|
raise FileNotFoundError(f"Файл штатного расписания не найден по пути: {STAFF_FILE_1C}")
|
||||||
raise FileNotFoundError(f"Файл штатного расписания не найден: {staff_path}")
|
df_staff = pd.read_excel(STAFF_FILE_1C, skiprows=7)
|
||||||
df_staff = pd.read_excel(staff_path, skiprows=7)
|
|
||||||
df_staff = df_staff.rename(columns={df_staff.columns[1]: 'ФИО'})
|
df_staff = df_staff.rename(columns={df_staff.columns[1]: 'ФИО'})
|
||||||
df_staff['fio_clean'] = df_staff['ФИО'].apply(normalize_fio)
|
df_staff['fio_clean'] = df_staff['ФИО'].apply(normalize_fio)
|
||||||
return df_staff
|
return df_staff
|
||||||
|
|
||||||
def load_absent_data():
|
def load_absent_data():
|
||||||
absent_path = os.path.join(DATA_DIR, "Отсутствия сотрудников - выгрузка.xlsx")
|
if not os.path.exists(ABSENT_FILE_1C):
|
||||||
if not os.path.exists(absent_path):
|
raise FileNotFoundError(f"Файл отсутствий не найден по пути: {ABSENT_FILE_1C}")
|
||||||
raise FileNotFoundError(f"Файл отсутствий не найден: {absent_path}")
|
df_absent_raw = pd.read_excel(ABSENT_FILE_1C, skiprows=3)
|
||||||
df_absent_raw = pd.read_excel(absent_path, skiprows=3)
|
|
||||||
df_absent = df_absent_raw[1:].copy().reset_index(drop=True)
|
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 = df_absent.rename(columns={df_absent.columns[0]: 'ФИО', df_absent.columns[2]: 'Вид_отсутствия'})
|
||||||
df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio)
|
df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio)
|
||||||
@@ -41,7 +40,7 @@ def load_absent_data():
|
|||||||
def load_scud_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)
|
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):
|
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)
|
df_scud = pd.read_excel(scud_path)
|
||||||
exceptions = load_exceptions()
|
exceptions = load_exceptions()
|
||||||
@@ -63,7 +62,6 @@ def load_scud_data():
|
|||||||
|
|
||||||
df_scud['Пришел'] = df_scud.apply(is_person_present, axis=1)
|
df_scud['Пришел'] = df_scud.apply(is_person_present, axis=1)
|
||||||
|
|
||||||
# Отмечаем флаг исключений, но НЕ удаляем сразу
|
|
||||||
df_scud['is_excluded'] = df_scud.apply(
|
df_scud['is_excluded'] = df_scud.apply(
|
||||||
lambda r: is_excluded(r.get('Сотрудник', ''), r.get('Подразделение', ''), r.get('Должность', ''), exceptions),
|
lambda r: is_excluded(r.get('Сотрудник', ''), r.get('Подразделение', ''), r.get('Должность', ''), exceptions),
|
||||||
axis=1
|
axis=1
|
||||||
|
|||||||
+24
-36
@@ -1,46 +1,34 @@
|
|||||||
import os
|
import os
|
||||||
import pandas as pd
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from config import DATA_DIR, DATE_TODAY, DATE_YESTERDAY
|
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 = []
|
warnings = []
|
||||||
|
errors = []
|
||||||
|
now = time.time()
|
||||||
|
max_age_sec = max_age_hours * 3600
|
||||||
|
|
||||||
# Ожидаемые имена файлов
|
# 1. Проверка файлов 1С в data/1c
|
||||||
scud_today_file = os.path.join(DATA_DIR, f"Сотрудники_{DATE_TODAY}.xlsx")
|
for file_path, label in [(STAFF_FILE_1C, "Штатное расписание 1С"), (ABSENT_FILE_1C, "Отсутствия 1С")]:
|
||||||
scud_yesterday_file = os.path.join(DATA_DIR, f"Сотрудники_{DATE_YESTERDAY}.xlsx")
|
if not os.path.exists(file_path):
|
||||||
staff_file = os.path.join(DATA_DIR, "Штатные сотрудники - ОУП.xlsx")
|
errors.append(f"Файл '{label}' не найден по пути: {file_path}")
|
||||||
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/!")
|
|
||||||
else:
|
else:
|
||||||
file_mtime = datetime.fromtimestamp(os.path.getmtime(f_path))
|
mtime = os.path.getmtime(file_path)
|
||||||
days_old = (now - file_mtime).days
|
age_hours = (now - mtime) / 3600
|
||||||
if days_old >= 2:
|
if age_hours > max_age_hours:
|
||||||
warnings.append(f"⚠️ УСТАРЕВШИЕ ДАННЫЕ 1С: Файл '{os.path.basename(f_path)}' не обновлялся {days_old} дн. (Дата: {file_mtime.strftime('%d.%m.%Y')})")
|
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
|
is_valid = len(errors) == 0
|
||||||
return is_valid, warnings, errors
|
return is_valid, warnings, errors
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
[2026-07-28 14:45:26] [INFO] === Запуск скрипта экспорта СКУД ===
|
|
||||||
[2026-07-28 14:45:26] [INFO] --- Обработка периода: Вчера (27.07.2026) ---
|
|
||||||
[2026-07-28 14:45:26] [INFO] Файл Сотрудники_27.07.2026.xlsx уже существует. Удаляем для перезаписи...
|
|
||||||
[2026-07-28 14:45:26] [ERROR] ОШИБКА при обработке даты 27.07.2026: ('IM002', '[IM002] [Microsoft][Диспетчер драйверов ODBC] Источник данных не найден и не указан драйвер, используемый по умолчанию (0) (SQLDriverConnect)')
|
|
||||||
[2026-07-28 14:45:26] [INFO] --- Обработка периода: Сегодня (28.07.2026) ---
|
|
||||||
[2026-07-28 14:45:26] [INFO] Файл Сотрудники_28.07.2026.xlsx уже существует. Удаляем для перезаписи...
|
|
||||||
[2026-07-28 14:45:26] [ERROR] ОШИБКА при обработке даты 28.07.2026: ('IM002', '[IM002] [Microsoft][Диспетчер драйверов ODBC] Источник данных не найден и не указан драйвер, используемый по умолчанию (0) (SQLDriverConnect)')
|
|
||||||
[2026-07-28 14:45:26] [INFO] === Завершение работы скрипта ===
|
|
||||||
[2026-07-28 14:47:46] [INFO] === Запуск скрипта экспорта СКУД ===
|
|
||||||
[2026-07-28 14:47:46] [INFO] --- Обработка периода: Вчера (27.07.2026) ---
|
|
||||||
[2026-07-28 14:48:17] [INFO] База вернула 272 строк за 27.07.2026
|
|
||||||
[2026-07-28 14:48:17] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx
|
|
||||||
[2026-07-28 14:48:17] [INFO] --- Обработка периода: Сегодня (28.07.2026) ---
|
|
||||||
[2026-07-28 14:48:44] [INFO] База вернула 272 строк за 28.07.2026
|
|
||||||
[2026-07-28 14:48:44] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx
|
|
||||||
[2026-07-28 14:48:44] [INFO] === Завершение работы скрипта ===
|
|
||||||
[2026-07-28 14:49:55] [INFO] === Запуск скрипта экспорта СКУД ===
|
|
||||||
[2026-07-28 14:49:55] [INFO] --- Обработка периода: Вчера (27.07.2026) ---
|
|
||||||
[2026-07-28 14:49:55] [INFO] Файл Сотрудники_27.07.2026.xlsx уже существует. Удаляем для перезаписи...
|
|
||||||
[2026-07-28 14:50:17] [INFO] База вернула 272 строк за 27.07.2026
|
|
||||||
[2026-07-28 14:50:18] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx
|
|
||||||
[2026-07-28 14:50:18] [INFO] --- Обработка периода: Сегодня (28.07.2026) ---
|
|
||||||
[2026-07-28 14:50:18] [INFO] Файл Сотрудники_28.07.2026.xlsx уже существует. Удаляем для перезаписи...
|
|
||||||
[2026-07-28 14:50:50] [INFO] База вернула 272 строк за 28.07.2026
|
|
||||||
[2026-07-28 14:50:50] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx
|
|
||||||
[2026-07-28 14:50:50] [INFO] === Завершение работы скрипта ===
|
|
||||||
[2026-07-28 14:53:08] [INFO] === Запуск скрипта экспорта СКУД ===
|
|
||||||
[2026-07-28 14:53:08] [INFO] --- Обработка периода: Вчера (27.07.2026) ---
|
|
||||||
[2026-07-28 14:53:08] [INFO] Файл Сотрудники_27.07.2026.xlsx уже существует. Удаляем для перезаписи...
|
|
||||||
[2026-07-28 14:53:30] [INFO] База вернула 272 строк за 27.07.2026
|
|
||||||
[2026-07-28 14:53:30] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx
|
|
||||||
[2026-07-28 14:53:30] [INFO] --- Обработка периода: Сегодня (28.07.2026) ---
|
|
||||||
[2026-07-28 14:53:30] [INFO] Файл Сотрудники_28.07.2026.xlsx уже существует. Удаляем для перезаписи...
|
|
||||||
[2026-07-28 14:53:52] [INFO] База вернула 272 строк за 28.07.2026
|
|
||||||
[2026-07-28 14:53:52] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx
|
|
||||||
[2026-07-28 14:53:52] [INFO] === Завершение работы скрипта ===
|
|
||||||
[2026-07-28 15:12:05] [INFO] === Запуск скрипта экспорта СКУД ===
|
|
||||||
[2026-07-28 15:12:05] [INFO] --- Обработка периода: Вчера (27.07.2026) ---
|
|
||||||
[2026-07-28 15:12:05] [INFO] Файл Сотрудники_27.07.2026.xlsx существует. Перезаписываем...
|
|
||||||
[2026-07-28 15:12:05] [ERROR] ОШИБКА при обработке даты 27.07.2026: ('42000', '[42000] [Microsoft][SQL Server Native Client 10.0][SQL Server]Неправильный синтаксис около конструкции "me". (102) (SQLExecDirectW)')
|
|
||||||
[2026-07-28 15:12:05] [INFO] --- Обработка периода: Сегодня (28.07.2026) ---
|
|
||||||
[2026-07-28 15:12:05] [INFO] Файл Сотрудники_28.07.2026.xlsx существует. Перезаписываем...
|
|
||||||
[2026-07-28 15:12:05] [ERROR] ОШИБКА при обработке даты 28.07.2026: ('42000', '[42000] [Microsoft][SQL Server Native Client 10.0][SQL Server]Неправильный синтаксис около конструкции "me". (102) (SQLExecDirectW)')
|
|
||||||
[2026-07-28 15:12:05] [INFO] === Завершение работы скрипта ===
|
|
||||||
[2026-07-28 15:13:31] [INFO] === Запуск скрипта экспорта СКУД ===
|
|
||||||
[2026-07-28 15:13:31] [INFO] --- Обработка периода: Вчера (27.07.2026) ---
|
|
||||||
[2026-07-28 15:15:02] [INFO] База вернула 272 строк за 27.07.2026
|
|
||||||
[2026-07-28 15:15:03] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx
|
|
||||||
[2026-07-28 15:15:03] [INFO] --- Обработка периода: Сегодня (28.07.2026) ---
|
|
||||||
[2026-07-28 15:16:20] [INFO] База вернула 272 строк за 28.07.2026
|
|
||||||
[2026-07-28 15:16:21] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx
|
|
||||||
[2026-07-28 15:16:21] [INFO] === Завершение работы скрипта ===
|
|
||||||
[2026-07-28 15:17:22] [INFO] === Запуск скрипта экспорта СКУД ===
|
|
||||||
[2026-07-28 15:17:22] [INFO] --- Обработка периода: Вчера (27.07.2026) ---
|
|
||||||
[2026-07-28 15:17:22] [INFO] Файл Сотрудники_27.07.2026.xlsx существует. Перезаписываем...
|
|
||||||
[2026-07-28 15:17:46] [INFO] База вернула 272 строк за 27.07.2026
|
|
||||||
[2026-07-28 15:17:47] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx
|
|
||||||
[2026-07-28 15:17:47] [INFO] --- Обработка периода: Сегодня (28.07.2026) ---
|
|
||||||
[2026-07-28 15:17:47] [INFO] Файл Сотрудники_28.07.2026.xlsx существует. Перезаписываем...
|
|
||||||
[2026-07-28 15:18:11] [INFO] База вернула 272 строк за 28.07.2026
|
|
||||||
[2026-07-28 15:18:12] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx
|
|
||||||
[2026-07-28 15:18:12] [INFO] === Завершение работы скрипта ===
|
|
||||||
+27
-75
@@ -1,11 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Скрипт экспорта данных СКУД (Orion) в Excel.
|
Модуль автоматического экспорта данных СКУД (Orion) из MS SQL Server в Excel.
|
||||||
Оптимизированная супер-быстрая версия (использует ROW_NUMBER() вместо подзапросов).
|
|
||||||
|
|
||||||
Запуск:
|
|
||||||
python scud_export.py
|
|
||||||
python scud_export.py -d
|
|
||||||
python scud_export.py --date 28.07.2026 -d
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -19,13 +13,12 @@ import pandas as pd
|
|||||||
import pyodbc
|
import pyodbc
|
||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
|
from config import SCUD_DIR
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy connectable")
|
warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy connectable")
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# 1. Настройки путей и логов
|
|
||||||
# ==============================================================================
|
|
||||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
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)
|
os.makedirs(LOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
TODAY_DATE_STR = datetime.now().strftime("%d.%m.%Y")
|
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 = logging.getLogger("scud_export")
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
_file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
|
if not logger.handlers:
|
||||||
_console_handler = logging.StreamHandler(sys.stdout)
|
_file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
|
||||||
_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
_console_handler = logging.StreamHandler(sys.stdout)
|
||||||
_file_handler.setFormatter(_formatter)
|
_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||||
_console_handler.setFormatter(_formatter)
|
_file_handler.setFormatter(_formatter)
|
||||||
logger.addHandler(_file_handler)
|
_console_handler.setFormatter(_formatter)
|
||||||
logger.addHandler(_console_handler)
|
logger.addHandler(_file_handler)
|
||||||
|
logger.addHandler(_console_handler)
|
||||||
|
|
||||||
|
|
||||||
def log(message: str, level: str = "INFO"):
|
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)
|
logger.log(level_map.get(level, logging.INFO), message)
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# 2. Настройки базы данных и папки
|
|
||||||
# ==============================================================================
|
|
||||||
SERVER_NAME = r"192.168.1.253\SQL"
|
SERVER_NAME = r"192.168.1.253\SQL"
|
||||||
DATABASE_NAME = "Orion-14.01.21-1"
|
DATABASE_NAME = "Orion-14.01.21-1"
|
||||||
SQL_USER = "sa"
|
SQL_USER = "sa"
|
||||||
SQL_PASSWORD = "123456"
|
SQL_PASSWORD = "123456"
|
||||||
|
|
||||||
FOLDER_PATH = r"C:\scud_exchsnge"
|
|
||||||
ODBC_DRIVER = "SQL Server Native Client 10.0"
|
ODBC_DRIVER = "SQL Server Native Client 10.0"
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# 3. СУПЕР-ОПТИМИЗИРОВАННЫЙ SQL-ЗАПРОС (Мгновенное выполнение)
|
|
||||||
# ==============================================================================
|
|
||||||
SQL_QUERY_TEMPLATE = r"""
|
SQL_QUERY_TEMPLATE = r"""
|
||||||
DECLARE @InputDate DATE = '{target_date}';
|
DECLARE @InputDate DATE = '{target_date}';
|
||||||
DECLARE @TargetDate DATE = @InputDate;
|
DECLARE @TargetDate DATE = @InputDate;
|
||||||
@@ -78,7 +63,6 @@ DECLARE @TargetDate DATE = @InputDate;
|
|||||||
DECLARE @StartDate DATETIME = CAST(@TargetDate AS DATETIME);
|
DECLARE @StartDate DATETIME = CAST(@TargetDate AS DATETIME);
|
||||||
DECLARE @EndDate DATETIME = DATEADD(SECOND, -1, DATEADD(DAY, 1, @StartDate));
|
DECLARE @EndDate DATETIME = DATEADD(SECOND, -1, DATEADD(DAY, 1, @StartDate));
|
||||||
|
|
||||||
-- 1. Фильтруем логи только за нужные 24 часа за один проход
|
|
||||||
WITH DailyLogs AS (
|
WITH DailyLogs AS (
|
||||||
SELECT
|
SELECT
|
||||||
log.HozOrgan AS EmployeeID,
|
log.HozOrgan AS EmployeeID,
|
||||||
@@ -95,7 +79,6 @@ WITH DailyLogs AS (
|
|||||||
AND log.HozOrgan > 0
|
AND log.HozOrgan > 0
|
||||||
AND (log.Event IN (28, 29, 26, 27, 32, 33) OR log.Mode IN (1, 2))
|
AND (log.Event IN (28, 29, 26, 27, 32, 33) OR log.Mode IN (1, 2))
|
||||||
),
|
),
|
||||||
-- 2. Агрегируем времена входа/выхода и определяем последнее событие
|
|
||||||
Passages AS (
|
Passages AS (
|
||||||
SELECT
|
SELECT
|
||||||
EmployeeID,
|
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):
|
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
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
@@ -196,7 +176,6 @@ def get_targets(input_date: str | None):
|
|||||||
try:
|
try:
|
||||||
parsed = datetime.strptime(input_date, "%d.%m.%Y").date()
|
parsed = datetime.strptime(input_date, "%d.%m.%Y").date()
|
||||||
targets.append({"name": "Указанная дата", "date": parsed})
|
targets.append({"name": "Указанная дата", "date": parsed})
|
||||||
log(f"Запущен ручной режим для даты: {input_date}")
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log(f"ОШИБКА: Неверный формат даты '{input_date}'. Используйте ДД.ММ.ГГГГ", "ERROR")
|
log(f"ОШИБКА: Неверный формат даты '{input_date}'. Используйте ДД.ММ.ГГГГ", "ERROR")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -211,16 +190,13 @@ def get_targets(input_date: str | None):
|
|||||||
return targets
|
return targets
|
||||||
|
|
||||||
|
|
||||||
# ==============================================================================
|
def run_export(input_date: str | None = None, debug: bool = False):
|
||||||
# 5. Основная функция выгрузки
|
|
||||||
# ==============================================================================
|
|
||||||
def run_export(input_date: str | None, debug: bool = False):
|
|
||||||
if debug:
|
if debug:
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
log("=== ВКЛЮЧЕН РЕЖИМ ОТЛАДКИ (DEBUG MODE) ===", "WARNING")
|
log("=== ВКЛЮЧЕН РЕЖИМ ОТЛАДКИ (DEBUG MODE) ===", "WARNING")
|
||||||
|
|
||||||
log("=== Запуск скрипта экспорта СКУД ===")
|
log("=== [ЭТАП 0] Выгрузка свежих данных СКУД напрямую из БД Орион ===")
|
||||||
os.makedirs(FOLDER_PATH, exist_ok=True)
|
os.makedirs(SCUD_DIR, exist_ok=True)
|
||||||
|
|
||||||
targets = get_targets(input_date)
|
targets = get_targets(input_date)
|
||||||
|
|
||||||
@@ -234,6 +210,7 @@ def run_export(input_date: str | None, debug: bool = False):
|
|||||||
f"Encrypt=no;"
|
f"Encrypt=no;"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
success = True
|
||||||
for target in targets:
|
for target in targets:
|
||||||
processing_date = target["date"]
|
processing_date = target["date"]
|
||||||
processing_date_str = processing_date.strftime("%d.%m.%Y")
|
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}) ---")
|
log(f"--- Обработка периода: {period_label} ({processing_date_str}) ---")
|
||||||
|
|
||||||
file_name = f"Сотрудники_{processing_date_str}.xlsx"
|
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):
|
if os.path.exists(file_path):
|
||||||
log(f"Файл {file_name} существует. Перезаписываем...")
|
|
||||||
try:
|
try:
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
log(f"ОШИБКА при удалении файла {file_name}: {e}", "ERROR")
|
log(f"ОШИБКА при удалении старого файла {file_name}: {e}", "ERROR")
|
||||||
continue
|
|
||||||
|
|
||||||
sql_query = SQL_QUERY_TEMPLATE.format(target_date=processing_date.strftime("%Y-%m-%d"))
|
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}")
|
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:
|
if len(df) > 0:
|
||||||
df.to_excel(file_path, sheet_name="Отчет", index=False, engine="openpyxl")
|
df.to_excel(file_path, sheet_name="Отчет", index=False, engine="openpyxl")
|
||||||
auto_fit_columns(file_path, sheet_name="Отчет")
|
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:
|
except Exception as e:
|
||||||
log(f"ОШИБКА при обработке даты {processing_date_str}: {e}", "ERROR")
|
log(f"🛑 ОШИБКА выгрузки СКУД за {processing_date_str}: {e}", "ERROR")
|
||||||
|
success = False
|
||||||
finally:
|
finally:
|
||||||
if connection is not None:
|
if connection is not None:
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
log("=== Завершение работы скрипта ===")
|
log("=== Выгрузка СКУД завершена ===\n")
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Экспорт данных СКУД в Excel")
|
parser = argparse.ArgumentParser(description="Экспорт данных СКУД в Excel")
|
||||||
parser.add_argument(
|
parser.add_argument("--date", dest="input_date", default=None)
|
||||||
"--date",
|
parser.add_argument("-d", "--debug", action="store_true")
|
||||||
dest="input_date",
|
|
||||||
default=None,
|
|
||||||
help='Дата в формате ДД.ММ.ГГГГ (например, "28.07.2026")',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-d", "--debug",
|
|
||||||
action="store_true",
|
|
||||||
help="Запуск в режиме отладки (вывод детальной статистики)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
run_export(args.input_date, debug=args.debug)
|
run_export(args.input_date, debug=args.debug)
|
||||||
+34
-35
@@ -2,7 +2,8 @@ import json
|
|||||||
from modules.ai_verifier import ask_ollama
|
from modules.ai_verifier import ask_ollama
|
||||||
from modules.knowledge_base import load_knowledge_base
|
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()
|
kb = load_knowledge_base()
|
||||||
custom_rules = kb.get("rules", [])
|
custom_rules = kb.get("rules", [])
|
||||||
custom_rules_str = "\n".join([f"- {r}" for r in custom_rules]) if custom_rules else "Специфических правил компании пока нет."
|
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({
|
detailed_cases_for_ai.append({
|
||||||
"fio_clean": fio_clean,
|
"fio_clean": fio_clean,
|
||||||
"staff_records_count": len(staff_matches),
|
|
||||||
"staff_records_1C": staff_matches,
|
"staff_records_1C": staff_matches,
|
||||||
"scud_records_LOGS": scud_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)
|
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)
|
explained_cnt = len(absent_explained)
|
||||||
unexplained_cnt = len(absent_unexplained)
|
unexplained_cnt = len(absent_unexplained)
|
||||||
|
anomalies_cnt = len(anomalies_list) if anomalies_list else 0
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Ты — старший аудитор кадровой безопасности и систем СКУД. Проанализируй данные за {date_str} и составь итоговый отчет.
|
Ты — старший аудитор кадровой безопасности и контроллинга СКУД.
|
||||||
|
Проанализируй аномалии и пересечения данных за {date_str}.
|
||||||
|
|
||||||
📌 ДИНАМИЧЕСКИЕ ПРАВИЛА И НАКОПЛЕННЫЙ ОПЫТ ПРЕДПРИЯТИЯ:
|
📌 НАКОПЛЕННЫЕ ПРАВИЛА ПРЕДПРИЯТИЯ И ИСКЛЮЧЕНИЯ:
|
||||||
{custom_rules_str}
|
{custom_rules_str}
|
||||||
|
|
||||||
🚨 СТРОГИЕ ПРАВИЛА МАТЕМАТИКИ И КАТЕГОРИЙ:
|
🚨 ВХОДНЫЕ МЕТРИКИ:
|
||||||
1. Цифры сводки ДОЛЖНЫ СТРОГО СОВПАДАТЬ с первично посчитанным массивом:
|
- Всего сотрудников: {total_staff}
|
||||||
- Всего офисных сотрудников: {total_staff}
|
- На работе / Удаленно / Командировка: {present_cnt}
|
||||||
- Присутствуют на рабочем месте: {present_cnt}
|
- Официально отсутствуют (1С/Реестры): {explained_cnt}
|
||||||
- Официально отсутствуют: {explained_cnt}
|
- Неизвестно (требуют выяснения): {unexplained_cnt}
|
||||||
- Неизвестно (истинно неотмеченные): {unexplained_cnt} чел.
|
- Выявлено аномалий и конфликтов реестров: {anomalies_cnt}
|
||||||
2. Заголовок "#### Неизвестные случаи: {unexplained_cnt}" должен иметь ровно ту же цифру {unexplained_cnt}.
|
|
||||||
3. Нумерованный список ниже ДОЛЖЕН содержать ровно {unexplained_cnt} сотрудников из массива `detailed_cases_for_ai` без пропусков.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
📊 ВХОДНЫЕ МИКРО-ДАННЫЕ НЕИЗВЕСТНЫХ ({unexplained_cnt} чел):
|
🚨 ОБНАРУЖЕННЫЕ АНОМАЛИИ И КОНФЛИКТЫ МЕЖДУ СКУД, 1С, СТАТИЧЕСКИМИ РЕЕСТРАМИ И ИСКЛЮЧЕНИЯМИ ({anomalies_cnt} шт.):
|
||||||
{json.dumps(detailed_cases_for_ai, ensure_ascii=False, indent=2)}
|
{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}**
|
- Всего офисных сотрудников: **{total_staff}**
|
||||||
- Присутствуют на рабочем месте: **{present_cnt}**
|
- Работают (офис / удаленка / командировки): **{present_cnt}**
|
||||||
- Официально отсутствуют: **{explained_cnt}**
|
- Официально отсутствуют: **{explained_cnt}**
|
||||||
- Неизвестно (истинно неотмеченные после проверки ИИ): **{unexplained_cnt}** чел.
|
- Неизвестно (истинно неотмеченные): **{unexplained_cnt}** чел.
|
||||||
|
- Выявлено аномалий/конфликтов реестров: **{anomalies_cnt}** шт.
|
||||||
|
|
||||||
#### ⚠️ Подозрения на ложные срабатывания СКУД / Совмещения:
|
#### 🚨 Выявленные ИИ аномалии и конфликты источников ({anomalies_cnt}):
|
||||||
(Укажи сотрудников с аномалиями/вторыми проходами, например Кондрашову Е.В., если у нее дубликат карты)
|
(Подробно разбери каждый конфликт из `anomalies_list`. Опиши суть нарушения. Если аномалий нет — напиши "Аномалий и конфликтов не обнаружено.")
|
||||||
|
|
||||||
#### Критические аномалии (Работа в отпуске / на больничном):
|
#### ⚠️ Подозрения на ложные срабатывания / Совмещения:
|
||||||
(Детально опиши сотрудников из списка КРИТИЧЕСКИХ АНОМАЛИЙ. Если их нет — напиши "Критических нарушений не выявлено.")
|
(Укажи сотрудников с повторными проходами или несколькими картами, если такие есть)
|
||||||
|
|
||||||
#### Неизвестные случаи: {unexplained_cnt}
|
#### Неизвестные случаи: {unexplained_cnt}
|
||||||
(Выведи НУМЕРОВАННЫЙ СПИСОК ВСЕХ {unexplained_cnt} человек строго в формате:
|
(Выведи НУМЕРОВАННЫЙ СПИСОК ВСЕХ {unexplained_cnt} человек в формате:
|
||||||
1. ФИО — [Аббревиатура], Должность
|
1. ФИО — Подразделение, Должность
|
||||||
2. ФИО — [Аббревиатура], Должность
|
2. ФИО — Подразделение, Должность)
|
||||||
...
|
|
||||||
{unexplained_cnt}. ФИО — [Аббревиатура], Должность)
|
|
||||||
|
|
||||||
### Рекомендации:
|
### Рекомендации:
|
||||||
...
|
(Сформулируй 2-3 общие системные рекомендации для кадровой службы. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО перечислять конкретные ФИО сотрудников в скобках или тексте рекомендаций — они уже перечислены выше.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sys_prompt = "Ты — кадровый аудитор. Выводи ровно то количество людей в списке, которое указано в переменной unexplained_cnt."
|
sys_prompt = "Ты — эксперт кадрового аудита. Пиши сжато, лаконично, без дублирования ФИО сотрудников в блоке рекомендаций."
|
||||||
return ask_ollama(prompt, system_prompt=sys_prompt)
|
return ask_ollama(prompt, system_prompt=sys_prompt)
|
||||||
Reference in New Issue
Block a user