перед внедрением API и веб интерфейса.

This commit is contained in:
2026-08-04 17:43:00 +03:00
parent d667b2d846
commit 7eea236417
11 changed files with 357 additions and 87 deletions
Binary file not shown.
+12
View File
@@ -23,6 +23,7 @@ if NOW.weekday() == 0:
else: else:
DATE_YESTERDAY = (NOW - timedelta(days=1)).strftime("%d.%m.%Y") DATE_YESTERDAY = (NOW - timedelta(days=1)).strftime("%d.%m.%Y")
# --- Настройки LLM / Ollama ---
OLLAMA_URL = "http://192.168.11.3:11434/api/generate" OLLAMA_URL = "http://192.168.11.3:11434/api/generate"
OLLAMA_MODEL = "qwen2.5:14b" OLLAMA_MODEL = "qwen2.5:14b"
MODEL_NAME = OLLAMA_MODEL MODEL_NAME = OLLAMA_MODEL
@@ -30,6 +31,17 @@ MODEL_NAME = OLLAMA_MODEL
KNOWLEDGE_BASE_PATH = os.path.join(DATA_DIR, "knowledge_base.json") KNOWLEDGE_BASE_PATH = os.path.join(DATA_DIR, "knowledge_base.json")
EXCEPTIONS_PATH = os.path.join(BASE_DIR, "exceptions.json") EXCEPTIONS_PATH = os.path.join(BASE_DIR, "exceptions.json")
# --- Настройки подключения к MS SQL Server (1С:ЗУП 3.1) ---
ZUP_SQL_CONFIG = {
"driver": "{ODBC Driver 18 for SQL Server}", # или "{ODBC Driver 17 for SQL Server}"
"server": os.getenv("ZUP_SQL_SERVER", "ACCOUNT-01"),
"database": os.getenv("ZUP_SQL_DB", "ZUP30"), # Замените на точное имя базы ЗУП на ACCOUNT-01
"user": os.getenv("ZUP_SQL_USER", "scud_reader"), # Логин SQL Server
"password": os.getenv("ZUP_SQL_PASS", "Rhfcysq90"), # Пароль SQL Server
"trust_server_certificate": "yes", # Доверять сертификату сервера
"encrypt": "no" # "yes", если на MS SQL включено обязательное SSL-шифрование
}
def find_dated_file(prefix, date_str, search_dirs=[ZUP_1C_DIR, SCUD_DIR, DATA_DIR, "."]): def find_dated_file(prefix, date_str, search_dirs=[ZUP_1C_DIR, SCUD_DIR, DATA_DIR, "."]):
""" """
Binary file not shown.
+37 -26
View File
@@ -5,6 +5,8 @@ from config import (
normalize_fio, clean_scud_fio_light, load_exceptions, normalize_fio, clean_scud_fio_light, load_exceptions,
DATE_TODAY, DATE_YESTERDAY, find_dated_file, ZUP_1C_DIR, SCUD_DIR DATE_TODAY, DATE_YESTERDAY, find_dated_file, ZUP_1C_DIR, SCUD_DIR
) )
# Импортируем прямую SQL-выгрузку отсутствий из 1С
from modules.zup_extractor import fetch_zup_absences_from_sql
warnings.filterwarnings('ignore', category=UserWarning, module='pandas') warnings.filterwarnings('ignore', category=UserWarning, module='pandas')
@@ -24,41 +26,50 @@ def is_excluded(fio, dept, pos, exceptions):
def load_staff_data(date_str): def load_staff_data(date_str):
""" """
Загружает файл Штатного расписания 1С за конкретную дату. Загружает файл Штатного расписания 1С за конкретную дату из папки 1c.
Если файла за эту дату нет — возвращает None без подмены на старые даты.
""" """
filepath = find_dated_file("Штат", date_str) filepath = find_dated_file("Штат", date_str)
if not filepath: if not filepath:
return None fallback_path = os.path.join(ZUP_1C_DIR, "штат.xlsx")
if os.path.exists(fallback_path):
filepath = fallback_path
else:
return None
df_raw = pd.read_excel(filepath, skiprows=8) try:
df_staff = df_raw.iloc[:, [1, 5, 12]].copy() df_raw = pd.read_excel(filepath, skiprows=8)
df_staff.columns = ['ФИО', 'Подразделение', 'Должность']
df_staff = df_staff.dropna(subset=['ФИО']).reset_index(drop=True) # Индексы столбцов в файле 1С: 1 — ФИО, 5 — Подразделение, 12 — Должность
df_staff = df_staff[~df_staff['ФИО'].astype(str).str.contains('Всего|Организация|Сотрудник|ФИО', case=False, na=False)] df_staff = df_raw.iloc[:, [1, 5, 12]].copy()
df_staff['fio_clean'] = df_staff['ФИО'].apply(normalize_fio) df_staff.columns = ['ФИО', 'Подразделение', 'Должность']
return df_staff
df_staff = df_staff.dropna(subset=['ФИО']).reset_index(drop=True)
df_staff = df_staff[~df_staff['ФИО'].astype(str).str.contains('Всего|Организация|Сотрудник|ФИО', case=False, na=False)]
df_staff['fio_clean'] = df_staff['ФИО'].apply(normalize_fio)
return df_staff
except Exception as e:
print(f"[❌] Ошибка загрузки штата из {filepath}: {e}")
return None
def load_absent_data(date_str): def load_absent_data(date_str):
""" """
Загружает файл Отсутствий сотрудников 1С за конкретную дату. Загружает документально подтвержденные отсутствия сотрудников
Если файла за эту дату нет — возвращает None без подмены на старые даты. НАПРЯМУЮ из базы MS SQL 1С:ЗУП за указанную дату.
""" """
filepath = find_dated_file("Отсутствия", date_str) try:
if not filepath: df_absent = fetch_zup_absences_from_sql(date_str)
return None
df_raw = pd.read_excel(filepath, skiprows=3) if df_absent is not None and not df_absent.empty:
df_absent = df_raw[1:].copy().reset_index(drop=True) df_absent = df_absent.dropna(subset=['ФИО', 'Вид_отсутствия'])
df_absent = df_absent.rename(columns={ df_absent = df_absent[~df_absent['ФИО'].astype(str).str.contains('АО "ЛЕНМОРНИИПРОЕКТ"|Сотрудник', case=False, na=False)]
df_absent.columns[0]: 'ФИО', df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio)
df_absent.columns[2]: 'Вид_отсутствия' return df_absent[['fio_clean', 'Вид_отсутствия']].dropna(subset=['fio_clean'])
}) else:
df_absent = df_absent[['ФИО', 'Вид_отсутствия']].dropna(subset=['ФИО', 'Вид_отсутствия']) return None
df_absent = df_absent[~df_absent['ФИО'].astype(str).str.contains('АО "ЛЕНМОРНИИПРОЕКТ"|Сотрудник', case=False, na=False)] except Exception as e:
df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio) print(f"[⚠️] Ошибка SQL-выгрузки отсутствий за {date_str}: {e}")
return df_absent[['fio_clean', 'Вид_отсутствия']].dropna(subset=['fio_clean']) return None
def load_scud_data(target_date_type="today"): def load_scud_data(target_date_type="today"):
@@ -100,7 +111,7 @@ def load_scud_data(target_date_type="today"):
def load_all_data(): def load_all_data():
""" """
Загружает слитные датасеты за Сегодня и за Вчера. Загружает слитные датасеты за Сегодня и за Вчера.
Если файла 1С за Сегодня нет, соответствующие датасеты вернет как None. Если данных за Сегодня нет, возвращает для них None.
""" """
df_scud_today = load_scud_data(target_date_type="today") df_scud_today = load_scud_data(target_date_type="today")
try: try:
+24 -5
View File
@@ -1,5 +1,6 @@
import os import os
import sqlite3 import sqlite3
import json
import pandas as pd import pandas as pd
from datetime import datetime from datetime import datetime
from config import DATA_DIR from config import DATA_DIR
@@ -94,21 +95,38 @@ def init_db():
conn.commit() conn.commit()
# Автоматически загружаем базовые правила компании # Автоматически загружаем базовые правила и системный промпт
sync_knowledge_base_to_db() sync_knowledge_base_to_db()
def sync_knowledge_base_to_db(): def sync_knowledge_base_to_db():
""" """
Автоматически мигрирует все правила из knowledge_base.json в таблицу ai_knowledge_base SQLite. Автоматически мигрирует правила из knowledge_base.json и системный промпт в таблицу ai_knowledge_base SQLite.
""" """
kb_path = os.path.join(os.path.dirname(DATA_DIR), "knowledge_base.json") system_ai_prompt = (
"СИСТЕМНЫЙ ПРОМПТ ИИ (MATCHING & VERIFICATION):\n"
"Ты — экспертный ИИ-аудитор СКУД и кадрового учета 1С. Твоя задача — проводить интеллектуальный анализ аномалий "
"и сопоставление (Fuzzy Matching) нераспознанных ФИО из СКУД с эталонным Штатным расписанием 1С.\n\n"
"1. ЗАЩИТА ОТ ПОЛНЫХ ТЁЗОК (FULL NAME COLLISION):\n"
" - Если в эталонном штате 1С присутствуют два или более сотрудников с ОДИНАКОВЫМИ ФИО (полные тёзки), "
"ты ОБЯЗАН учитывать контекст: Подразделение и Должность.\n"
" - Ни в коем случае не объединяй записи разных людей! Если контекст не позволяет однозначно определить, кто именно из тёзок совершил проход, ПОМЕТЬ СЛУЧАЙ КАК НЕОДНОЗНАЧНЫЙ и НЕ делай автоматическую подмену.\n\n"
"2. ПРАВИЛА СОПОСТАВЛЕНИЯ (MATCHING RULES):\n"
" - Допускаются исправления мелких опечаток, перестановки порядка (Имя Фамилия -> Фамилия Имя), замена букв Е/Ё, И/Й.\n"
" - Учитывай сокращения инициалов (например, 'Иванов И.В.' -> 'Иванов Иван Васильевич').\n"
" - Если уверенность в совпадении меньше 85% — НЕ сопоставляй, оставляй запись как нераспознанную.\n\n"
"3. ФОРМАТ ВЫХОДНЫХ ДАННЫХ:\n"
" Верни строго JSON-объект без вводных слов и Markdown-разметки: {\"mappings\": [...], \"unmatched\": [...]}"
)
rules_to_insert = [ rules_to_insert = [
system_ai_prompt,
"ГЛОБАЛЬНОЕ ПРАВИЛО 1: Любое физическое присутствие сотрудника по СКУД при наличии в 1С документа отсутствия (отпуск, больничный, уход за ребенком и др.), КРОМЕ командировок и служебных поездок — ЯВЛЯЕТСЯ СТРОГОЙ ИСТИННОЙ АНОМАЛИЕЙ.", "ГЛОБАЛЬНОЕ ПРАВИЛО 1: Любое физическое присутствие сотрудника по СКУД при наличии в 1С документа отсутствия (отпуск, больничный, уход за ребенком и др.), КРОМЕ командировок и служебных поездок — ЯВЛЯЕТСЯ СТРОГОЙ ИСТИННОЙ АНОМАЛИЕЙ.",
"ГЛОБАЛЬНОЕ ПРАВИЛО 2: Документы отсутствий из 1С:ЗУП имеют 100% высший приоритет над статическим реестром удалёнщиков.", "ГЛОБАЛЬНОЕ ПРАВИЛО 2: Документы отсутствий из 1С:ЗУП имеют 100% высший приоритет над статическим реестром удалёнщиков.",
"ГЛОБАЛЬНОЕ ПРАВИЛО 3: Удалённые сотрудники имеют право беспрепятственно работать в офисе по СКУД в любое время (это не является аномалией)." "ГЛОБАЛЬНОЕ ПРАВИЛО 3: Удалённые сотрудники имеют право беспрепятственно работать в офисе по СКУД в любое время (это не является аномалией)."
] ]
kb_path = os.path.join(os.path.dirname(DATA_DIR), "knowledge_base.json")
if os.path.exists(kb_path): if os.path.exists(kb_path):
try: try:
with open(kb_path, "r", encoding="utf-8") as f: with open(kb_path, "r", encoding="utf-8") as f:
@@ -124,11 +142,12 @@ def sync_knowledge_base_to_db():
cursor = conn.cursor() cursor = conn.cursor()
for rule in rules_to_insert: for rule in rules_to_insert:
cursor.execute( cursor.execute(
"INSERT OR IGNORE INTO ai_knowledge_base (rule_text) VALUES (?)", "INSERT OR IGNORE INTO ai_knowledge_base (rule_text, added_by) VALUES (?, ?)",
(rule.strip(),) (rule.strip(), "System_AI")
) )
conn.commit() conn.commit()
def save_scud_to_db(df_scud, date_str): def save_scud_to_db(df_scud, date_str):
"""Оптимизированное сохранение логов СКУД за дату.""" """Оптимизированное сохранение логов СКУД за дату."""
if df_scud.empty: if df_scud.empty:
+49 -51
View File
@@ -1,40 +1,39 @@
import os import os
import logging import logging
from datetime import date from datetime import date, datetime
import pyodbc import pyodbc
import pandas as pd import pandas as pd
from config import DATA_DIR, normalize_fio from config import DATA_DIR, normalize_fio, ZUP_SQL_CONFIG
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Конфигурация MS SQL Server 1С:ЗУП 3.1
ZUP_SQL_CONFIG = {
"driver": "{ODBC Driver 18 for SQL Server}",
"server": os.getenv("ZUP_MS_SQL_SERVER", "192.168.1.10"), # Укажите IP MS SQL 1С
"database": os.getenv("ZUP_MS_SQL_DB", "zup_db"),
"user": os.getenv("ZUP_MS_SQL_USER", "sa"),
"password": os.getenv("ZUP_MS_SQL_PASS", "your_password"),
}
def get_zup_connection_string() -> str: def get_zup_connection_string() -> str:
"""Формирует строку подключения pyodbc к MS SQL Server.""" """Формирует строку подключения pyodbc к MS SQL Server из config.py."""
return ( return (
f"DRIVER={ZUP_SQL_CONFIG['driver']};" f"DRIVER={ZUP_SQL_CONFIG['driver']};"
f"SERVER={ZUP_SQL_CONFIG['server']};" f"SERVER={ZUP_SQL_CONFIG['server']};"
f"DATABASE={ZUP_SQL_CONFIG['database']};" f"DATABASE={ZUP_SQL_CONFIG['database']};"
f"UID={ZUP_SQL_CONFIG['user']};" f"UID={ZUP_SQL_CONFIG['user']};"
f"PWD={ZUP_SQL_CONFIG['password']};" f"PWD={ZUP_SQL_CONFIG['password']};"
f"TrustServerCertificate=yes;" f"TrustServerCertificate={ZUP_SQL_CONFIG.get('trust_server_certificate', 'yes')};"
f"Encrypt=no;" f"Encrypt={ZUP_SQL_CONFIG.get('encrypt', 'no')};"
) )
def fetch_zup_absences_from_sql(target_date: date) -> pd.DataFrame: def fetch_zup_absences_from_sql(target_date) -> pd.DataFrame:
""" """
Извлекает оперативные отсутствия и действующие декреты из MS SQL 1С:ЗУП 3.1 Извлекает оперативные отсутствия и действующие декреты из MS SQL 1С:ЗУП 3.1
на указанную дату. на указанную дату (принимает как datetime.date, так и строку 'DD.MM.YYYY' / 'DD_MM_YYYY').
""" """
if isinstance(target_date, str):
clean_date_str = target_date.replace('_', '.')
try:
target_date = datetime.strptime(clean_date_str, "%d.%m.%Y").date()
except ValueError:
logger.error(f"[❌] Неверный формат даты для SQL-запроса: {target_date}. Ожидался DD.MM.YYYY")
return pd.DataFrame()
query = """ query = """
DECLARE @TargetDate DATE = ?; DECLARE @TargetDate DATE = ?;
@@ -86,17 +85,21 @@ def fetch_zup_absences_from_sql(target_date: date) -> pd.DataFrame:
ORDER BY [ФИО] ASC; ORDER BY [ФИО] ASC;
""" """
conn_str = get_zup_connection_string() try:
with pyodbc.connect(conn_str) as conn: conn_str = get_zup_connection_string()
df = pd.read_sql(query, conn, params=[target_date]) with pyodbc.connect(conn_str, timeout=5) as conn:
df = pd.read_sql(query, conn, params=[target_date])
if not df.empty: if not df.empty:
df['fio_clean'] = df['ФИО'].apply(normalize_fio) df['fio_clean'] = df['ФИО'].apply(normalize_fio)
return df return df
except Exception as e:
logger.error(f"[❌] Ошибка SQL-выгрузки отсутствий за {target_date}: {e}")
return pd.DataFrame()
def fetch_zup_staff_from_sql() -> pd.DataFrame: def fetch_zup_staff_from_sql() -> pd.DataFrame:
"""Извлекает весь актуальный штат из MS SQL 1С:ЗУП.""" """Резервная выгрузка штата из MS SQL."""
query = """ query = """
SELECT DISTINCT SELECT DISTINCT
LTRIM(RTRIM(ref_emp._Description)) AS [ФИО], LTRIM(RTRIM(ref_emp._Description)) AS [ФИО],
@@ -107,53 +110,48 @@ def fetch_zup_staff_from_sql() -> pd.DataFrame:
AND ref_emp._Marked = 0x00 AND ref_emp._Marked = 0x00
ORDER BY [ФИО] ASC; ORDER BY [ФИО] ASC;
""" """
conn_str = get_zup_connection_string()
with pyodbc.connect(conn_str) as conn:
df = pd.read_sql(query, conn)
if not df.empty:
df['fio_clean'] = df['ФИО'].apply(normalize_fio)
return df
def sync_zup_to_excel(target_date: date) -> bool:
"""
Извлекает данные из MS SQL и автоматически создает локальные Excel-файлы 1С:
data/1c/Штат_DD_MM_YYYY.xlsx
data/1c/Отсутствия_DD_MM_YYYY.xlsx
"""
try: try:
date_str_file = target_date.strftime("%d_%m_%Y") conn_str = get_zup_connection_string()
with pyodbc.connect(conn_str, timeout=5) as conn:
df = pd.read_sql(query, conn)
if not df.empty:
df['fio_clean'] = df['ФИО'].apply(normalize_fio)
return df
except Exception as e:
logger.error(f"[❌] Ошибка выгрузки штата из MS SQL: {e}")
return pd.DataFrame()
def sync_zup_to_excel(target_date) -> bool:
"""Создает дамп в Excel при необходимости."""
try:
if isinstance(target_date, str):
clean_date_str = target_date.replace('_', '.')
target_date_obj = datetime.strptime(clean_date_str, "%d.%m.%Y").date()
else:
target_date_obj = target_date
date_str_file = target_date_obj.strftime("%d_%m_%Y")
c_1c_dir = os.path.join(DATA_DIR, "1c") c_1c_dir = os.path.join(DATA_DIR, "1c")
os.makedirs(c_1c_dir, exist_ok=True) os.makedirs(c_1c_dir, exist_ok=True)
df_staff = fetch_zup_staff_from_sql() df_staff = fetch_zup_staff_from_sql()
df_absences = fetch_zup_absences_from_sql(target_date) df_absences = fetch_zup_absences_from_sql(target_date_obj)
logger.info(f"[1С Direct SQL] Выгрузка за {target_date.strftime('%d.%m.%Y')}: Штат ({len(df_staff)} чел.), Отсутствия ({len(df_absences)} чел.)")
# Сохранение в формате, который ожидает data_loader.py
staff_excel_path = os.path.join(c_1c_dir, f"Штат_{date_str_file}.xlsx") staff_excel_path = os.path.join(c_1c_dir, f"Штат_{date_str_file}.xlsx")
absences_excel_path = os.path.join(c_1c_dir, f"Отсутствия_{date_str_file}.xlsx") absences_excel_path = os.path.join(c_1c_dir, f"Отсутствия_{date_str_file}.xlsx")
# Формируем структуру Excel с нужным количеством пропущенных строк (skiprows)
with pd.ExcelWriter(staff_excel_path, engine='openpyxl') as writer: with pd.ExcelWriter(staff_excel_path, engine='openpyxl') as writer:
# Пустой заголовок (8 строк) для эмуляции отчета 1С
dummy_headers = pd.DataFrame([[""] * 13] * 8) dummy_headers = pd.DataFrame([[""] * 13] * 8)
dummy_headers.to_excel(writer, index=False, header=False) dummy_headers.to_excel(writer, index=False, header=False)
# Данные
df_staff[['ФИО', 'Подразделение', 'Должность']].to_excel(writer, startrow=8, index=False) df_staff[['ФИО', 'Подразделение', 'Должность']].to_excel(writer, startrow=8, index=False)
with pd.ExcelWriter(absences_excel_path, engine='openpyxl') as writer: with pd.ExcelWriter(absences_excel_path, engine='openpyxl') as writer:
# Пустой заголовок (3 строки) для эмуляции отчета 1С
dummy_headers = pd.DataFrame([[""] * 3] * 3) dummy_headers = pd.DataFrame([[""] * 3] * 3)
dummy_headers.to_excel(writer, index=False, header=False) dummy_headers.to_excel(writer, index=False, header=False)
# Данные
df_absences[['ФИО', 'Вид_отсутствия']].to_excel(writer, startrow=3, index=False) df_absences[['ФИО', 'Вид_отсутствия']].to_excel(writer, startrow=3, index=False)
logger.info(f"[✓] Успешно созданы файлы 1С за {target_date.strftime('%d.%m.%Y')}: {staff_excel_path}, {absences_excel_path}")
return True return True
except Exception as e: except Exception as e:
+9 -3
View File
@@ -76,10 +76,16 @@ if __name__ == "__main__":
print(df.to_string(index=False) if not df.empty else "Аномалий в базе пока нет.") print(df.to_string(index=False) if not df.empty else "Аномалий в базе пока нет.")
elif cmd == "rules": elif cmd == "rules":
df = query_db("SELECT * FROM ai_knowledge_base ORDER BY id ASC") df = query_db("SELECT id, added_by, created_at, rule_text FROM ai_knowledge_base ORDER BY id ASC")
if df is not None: if df is not None and not df.empty:
print("\n🧠 ПРАВИЛА БАЗЫ ЗНАНИЙ ИИ (ОБУЧЕНИЕ):") print("\n🧠 ПРАВИЛА БАЗЫ ЗНАНИЙ ИИ (ОБУЧЕНИЕ):")
print(df.to_string(index=False) if not df.empty else "База знаний пуста.") print("=" * 80)
for _, row in df.iterrows():
print(f"📌 [ID: {row['id']}] | Добавил: {row['added_by']} | Дата: {row['created_at']}")
print(f"{row['rule_text']}")
print("-" * 80)
else:
print("\n🧠 База знаний пуста.")
elif cmd == "scud" and len(sys.argv) > 2: elif cmd == "scud" and len(sys.argv) > 2:
date_str = sys.argv[2] date_str = sys.argv[2]
+56
View File
@@ -0,0 +1,56 @@
import sys
import pyodbc
from config import ZUP_SQL_CONFIG
def test_connection():
print("=" * 60)
print("🧪 ПРОВЕРКА ПОДКЛЮЧЕНИЯ К MS SQL SERVER (1С:ЗУП)")
print("=" * 60)
print(f" • Сервер: {ZUP_SQL_CONFIG['server']}")
print(f" • База: {ZUP_SQL_CONFIG['database']}")
print(f" • Логин: {ZUP_SQL_CONFIG['user']}")
print(f" • Драйвер: {ZUP_SQL_CONFIG['driver']}")
print("-" * 60)
conn_str = (
f"DRIVER={ZUP_SQL_CONFIG['driver']};"
f"SERVER={ZUP_SQL_CONFIG['server']};"
f"DATABASE={ZUP_SQL_CONFIG['database']};"
f"UID={ZUP_SQL_CONFIG['user']};"
f"PWD={ZUP_SQL_CONFIG['password']};"
f"TrustServerCertificate={ZUP_SQL_CONFIG['trust_server_certificate']};"
f"Encrypt={ZUP_SQL_CONFIG['encrypt']};"
)
try:
print("⏳ Установка соединения...")
with pyodbc.connect(conn_str, timeout=10) as conn:
cursor = conn.cursor()
# 1. Проверка версии SQL Сервера
cursor.execute("SELECT @@VERSION;")
db_version = cursor.fetchone()[0]
print("✅ Подключение успешно установлено!\n")
print(f"📋 Версия MS SQL Server:\n{db_version.splitlines()[0]}\n")
# 2. Проверка доступа к таблице физических лиц _Reference299
cursor.execute("SELECT COUNT(*) FROM dbo._Reference299 WITH (NOLOCK);")
count_phys = cursor.fetchone()[0]
print(f"📊 Записей в таблице физлиц (_Reference299): {count_phys} шт.")
# 3. Проверка доступа к регистру кадровых состояний _InfoRg16921
cursor.execute("SELECT COUNT(*) FROM dbo._InfoRg16921 WITH (NOLOCK);")
count_states = cursor.fetchone()[0]
print(f"📊 Записей в регистре состояний (_InfoRg16921): {count_states} шт.")
print("\n🎉 Все проверки успешно пройдены!")
except pyodbc.Error as ex:
sqlstate = ex.args[0] if ex.args else "UNKNOWN"
print("\n❌ ОШИБКА ПОДКЛЮЧЕНИЯ!")
print(f"Код ошибки: {sqlstate}")
print(f"Детали:\n{ex}")
sys.exit(1)
if __name__ == "__main__":
test_connection()
+45
View File
@@ -0,0 +1,45 @@
import sqlite3
import os
from config import DATA_DIR
DB_PATH = os.path.join(DATA_DIR, "scud_orion_ai.db")
system_ai_prompt = (
"СИСТЕМНЫЙ ПРОМПТ ИИ (MATCHING & VERIFICATION):\n"
"Ты — экспертный ИИ-аудитор СКУД и кадрового учета 1С. Твоя задача — проводить интеллектуальный анализ аномалий "
"и сопоставление (Fuzzy Matching) нераспознанных ФИО из СКУД с эталонным Штатным расписанием 1С.\n\n"
"1. ЗАЩИТА ОТ ПОЛНЫХ ТЁЗОК (FULL NAME COLLISION):\n"
" - Если в эталонном штате 1С присутствуют два или более сотрудников с ОДИНАКОВЫМИ ФИО (полные тёзки), "
"ты ОБЯЗАН учитывать контекст: Подразделение и Должность.\n"
" - Ни в коем случае не объединяй записи разных людей! Если контекст не позволяет однозначно определить, кто именно из тёзок совершил проход, ПОМЕТЬ СЛУЧАЙ КАК НЕОДНОЗНАЧНЫЙ и НЕ делай автоматическую подмену.\n\n"
"2. ПРАВИЛА СОПОСТАВЛЕНИЯ (MATCHING RULES):\n"
" - Допускаются исправления мелких опечаток, перестановки порядка (Имя Фамилия -> Фамилия Имя), замена букв Е/Ё, И/Й.\n"
" - Учитывай сокращения инициалов (например, 'Иванов И.В.' -> 'Иванов Иван Васильевич').\n"
" - Если уверенность в совпадении меньше 85% — НЕ сопоставляй, оставляй запись как нераспознанную.\n\n"
"3. ФОРМАТ ВЫХОДНЫХ ДАННЫХ:\n"
" Верни строго JSON-объект без вводных слов и Markdown-разметки: {\"mappings\": [...], \"unmatched\": [...]}"
)
rules = [
(system_ai_prompt, "System_AI"),
("ГЛОБАЛЬНОЕ ПРАВИЛО 1: Любое физическое присутствие сотрудника по СКУД при наличии в 1С документа отсутствия (отпуск, больничный, уход за ребенком и др.), КРОМЕ командировок и служебных поездок — ЯВЛЯЕТСЯ СТРОГОЙ ИСТИННОЙ АНОМАЛИЕЙ.", "Human"),
("ГЛОБАЛЬНОЕ ПРАВИЛО 2: Документы отсутствий из 1С:ЗУП имеют 100% высший приоритет над статическим реестром удалёнщиков.", "Human"),
("ГЛОБАЛЬНОЕ ПРАВИЛО 3: Удалённые сотрудники имеют право беспрепятственно работать в офисе по СКУД в любое время (это не является аномалией).", "Human")
]
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Очищаем таблицу перед записью, чтобы избежать дублей и UNIQUE constraint error
cursor.execute("DELETE FROM ai_knowledge_base;")
for rule_text, added_by in rules:
cursor.execute(
"INSERT INTO ai_knowledge_base (rule_text, added_by) VALUES (?, ?)",
(rule_text, added_by)
)
conn.commit()
conn.close()
print("✅ База знаний успешно перезаписана с включением системного промпта ИИ!")
+63
View File
@@ -0,0 +1,63 @@
import pyodbc
from config import ZUP_SQL_CONFIG
print("="*60)
print("🕵️‍♂️ ZUP DETECTIVE: ПОИСК СТАТУСА 'УВОЛЕН'")
print("="*60)
conn_str = (
f"DRIVER={ZUP_SQL_CONFIG['driver']};"
f"SERVER={ZUP_SQL_CONFIG['server']};"
f"DATABASE={ZUP_SQL_CONFIG['database']};"
f"UID={ZUP_SQL_CONFIG['user']};"
f"PWD={ZUP_SQL_CONFIG['password']};"
f"TrustServerCertificate=yes;"
)
with pyodbc.connect(conn_str) as conn:
cursor = conn.cursor()
# 1. Получаем ID наших подопытных
cursor.execute("SELECT TOP 1 _IDRRef FROM dbo._Reference299 WHERE _Description LIKE '%Шумский Алексей Анатольевич%'")
id_work = cursor.fetchone()[0]
cursor.execute("SELECT TOP 1 _IDRRef FROM dbo._Reference299 WHERE _Description LIKE '%Щелканов Дмитрий Викторович%'")
id_fired = cursor.fetchone()[0]
# 2. Сканируем два самых больших кадровых регистра, которые нашел наш сканер
for table in ['_InfoRg16684', '_InfoRg32796']:
print(f"\n🔍 АНАЛИЗ РЕГИСТРА: {table}")
# Ищем все колонки-ссылки
cursor.execute(f"SELECT name FROM sys.columns WHERE object_id = OBJECT_ID('{table}') AND system_type_id = 173 AND max_length = 16")
cols_bin = [r[0] for r in cursor.fetchall()]
found = False
for col in cols_bin:
cursor.execute(f"SELECT COUNT(*) FROM dbo.{table} WITH (NOLOCK) WHERE {col} = ?", (id_work,))
if cursor.fetchone()[0] > 0:
print(f"✅ Найдена колонка связи с Сотрудником: {col}")
# Забираем полные строки обоих людей
cursor.execute(f"SELECT * FROM dbo.{table} WITH (NOLOCK) WHERE {col} = ?", (id_work,))
data_work = cursor.fetchone()
all_cols = [c[0] for c in cursor.description]
cursor.execute(f"SELECT * FROM dbo.{table} WITH (NOLOCK) WHERE {col} = ?", (id_fired,))
data_fired = cursor.fetchone()
if data_work and data_fired:
print("\n⚖️ ОТЛИЧИЯ В ДАННЫХ (Работает vs Уволен):")
for i, c in enumerate(all_cols):
# Выводим только те поля, где есть отличия
if data_work[i] != data_fired[i]:
print(f"Поле [{c}]:")
print(f" 🟢 Шумский: {data_work[i]}")
print(f" 🔴 Щелканов: {data_fired[i]}\n")
found = True
break
if found:
break
print("✅ Расследование завершено!")
+60
View File
@@ -0,0 +1,60 @@
import pyodbc
from config import ZUP_SQL_CONFIG
print("="*60)
print("🔍 ЗАПУСК СКАНЕРА СТРУКТУРЫ 1С:ЗУП 3.1")
print("="*60)
conn_str = (
f"DRIVER={ZUP_SQL_CONFIG['driver']};"
f"SERVER={ZUP_SQL_CONFIG['server']};"
f"DATABASE={ZUP_SQL_CONFIG['database']};"
f"UID={ZUP_SQL_CONFIG['user']};"
f"PWD={ZUP_SQL_CONFIG['password']};"
f"TrustServerCertificate=yes;"
f"Encrypt=no;"
)
with pyodbc.connect(conn_str) as conn:
cursor = conn.cursor()
print("⏳ Анализ справочников...")
cursor.execute("SELECT name FROM sys.tables WHERE name LIKE '_Reference%'")
for row in cursor.fetchall():
t = row[0]
try:
cursor.execute(f"SELECT TOP 5 CAST(_Description AS NVARCHAR(255)) FROM {t} WITH (NOLOCK) WHERE _Description <> ''")
vals = [r[0].strip() for r in cursor.fetchall() if r[0]]
if not vals: continue
text = " ".join(vals).lower()
# Ищем сотрудников (исключая физлиц _Reference299)
if t != '_Reference299' and any(w in text for w in ["александров", "иванов", "смирнов", "петрова", "сергеев"]):
print(f"👤 СОТРУДНИКИ ({t}): {vals[:3]}")
# Ищем должности
elif any(w in text for w in ["инженер", "специалист", "директор", "уборщик"]):
print(f"💼 ДОЛЖНОСТИ ({t}): {vals[:3]}")
# Ищем подразделения
elif any(w in text for w in ["отдел", "управление", "служба"]):
print(f"🏢 ПОДРАЗДЕЛЕНИЯ ({t}): {vals[:3]}")
except:
pass
print("\n⏳ Анализ регистров кадровой истории...")
cursor.execute("SELECT name FROM sys.tables WHERE name LIKE '_InfoRg%'")
for row in cursor.fetchall():
t = row[0]
try:
# Ищем широкие регистры (Текущие кадровые данные имеют много колонок)
cursor.execute(f"SELECT COUNT(*) FROM sys.columns WHERE object_id = OBJECT_ID('{t}')")
col_count = cursor.fetchone()[0]
cursor.execute(f"SELECT COUNT(*) FROM {t} WITH (NOLOCK)")
row_count = cursor.fetchone()[0]
if col_count > 15 and row_count > 500:
print(f"📋 ШИРОКИЙ РЕГИСТР КАДРОВ ({t}) -> Колонок: {col_count}, Строк: {row_count}")
except:
pass
print("\n✅ Сканирование завершено!")