перед внедрением API и веб интерфейса.
This commit is contained in:
+37
-26
@@ -5,6 +5,8 @@ from config import (
|
||||
normalize_fio, clean_scud_fio_light, load_exceptions,
|
||||
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')
|
||||
|
||||
@@ -24,42 +26,51 @@ def is_excluded(fio, dept, pos, exceptions):
|
||||
|
||||
def load_staff_data(date_str):
|
||||
"""
|
||||
Загружает файл Штатного расписания 1С за конкретную дату.
|
||||
Если файла за эту дату нет — возвращает None без подмены на старые даты.
|
||||
Загружает файл Штатного расписания 1С за конкретную дату из папки 1c.
|
||||
"""
|
||||
filepath = find_dated_file("Штат", date_str)
|
||||
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)
|
||||
df_staff = df_raw.iloc[:, [1, 5, 12]].copy()
|
||||
df_staff.columns = ['ФИО', 'Подразделение', 'Должность']
|
||||
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
|
||||
try:
|
||||
df_raw = pd.read_excel(filepath, skiprows=8)
|
||||
|
||||
# Индексы столбцов в файле 1С: 1 — ФИО, 5 — Подразделение, 12 — Должность
|
||||
df_staff = df_raw.iloc[:, [1, 5, 12]].copy()
|
||||
df_staff.columns = ['ФИО', 'Подразделение', 'Должность']
|
||||
|
||||
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):
|
||||
"""
|
||||
Загружает файл Отсутствий сотрудников 1С за конкретную дату.
|
||||
Если файла за эту дату нет — возвращает None без подмены на старые даты.
|
||||
Загружает документально подтвержденные отсутствия сотрудников
|
||||
НАПРЯМУЮ из базы MS SQL 1С:ЗУП за указанную дату.
|
||||
"""
|
||||
filepath = find_dated_file("Отсутствия", date_str)
|
||||
if not filepath:
|
||||
try:
|
||||
df_absent = fetch_zup_absences_from_sql(date_str)
|
||||
|
||||
if df_absent is not None and not df_absent.empty:
|
||||
df_absent = df_absent.dropna(subset=['ФИО', 'Вид_отсутствия'])
|
||||
df_absent = df_absent[~df_absent['ФИО'].astype(str).str.contains('АО "ЛЕНМОРНИИПРОЕКТ"|Сотрудник', case=False, na=False)]
|
||||
df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio)
|
||||
return df_absent[['fio_clean', 'Вид_отсутствия']].dropna(subset=['fio_clean'])
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[⚠️] Ошибка SQL-выгрузки отсутствий за {date_str}: {e}")
|
||||
return None
|
||||
|
||||
df_raw = pd.read_excel(filepath, skiprows=3)
|
||||
df_absent = df_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[['ФИО', 'Вид_отсутствия']].dropna(subset=['ФИО', 'Вид_отсутствия'])
|
||||
df_absent = df_absent[~df_absent['ФИО'].astype(str).str.contains('АО "ЛЕНМОРНИИПРОЕКТ"|Сотрудник', case=False, na=False)]
|
||||
df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio)
|
||||
return df_absent[['fio_clean', 'Вид_отсутствия']].dropna(subset=['fio_clean'])
|
||||
|
||||
|
||||
def load_scud_data(target_date_type="today"):
|
||||
"""Загружает файл СКУД за 'today' или 'yesterday'."""
|
||||
@@ -100,7 +111,7 @@ def load_scud_data(target_date_type="today"):
|
||||
def load_all_data():
|
||||
"""
|
||||
Загружает слитные датасеты за Сегодня и за Вчера.
|
||||
Если файла 1С за Сегодня нет, соответствующие датасеты вернет как None.
|
||||
Если данных за Сегодня нет, возвращает для них None.
|
||||
"""
|
||||
df_scud_today = load_scud_data(target_date_type="today")
|
||||
try:
|
||||
|
||||
+25
-6
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import json
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from config import DATA_DIR
|
||||
@@ -94,21 +95,38 @@ def init_db():
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Автоматически загружаем базовые правила компании
|
||||
# Автоматически загружаем базовые правила и системный промпт
|
||||
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 = [
|
||||
system_ai_prompt,
|
||||
"ГЛОБАЛЬНОЕ ПРАВИЛО 1: Любое физическое присутствие сотрудника по СКУД при наличии в 1С документа отсутствия (отпуск, больничный, уход за ребенком и др.), КРОМЕ командировок и служебных поездок — ЯВЛЯЕТСЯ СТРОГОЙ ИСТИННОЙ АНОМАЛИЕЙ.",
|
||||
"ГЛОБАЛЬНОЕ ПРАВИЛО 2: Документы отсутствий из 1С:ЗУП имеют 100% высший приоритет над статическим реестром удалёнщиков.",
|
||||
"ГЛОБАЛЬНОЕ ПРАВИЛО 3: Удалённые сотрудники имеют право беспрепятственно работать в офисе по СКУД в любое время (это не является аномалией)."
|
||||
]
|
||||
|
||||
kb_path = os.path.join(os.path.dirname(DATA_DIR), "knowledge_base.json")
|
||||
if os.path.exists(kb_path):
|
||||
try:
|
||||
with open(kb_path, "r", encoding="utf-8") as f:
|
||||
@@ -124,11 +142,12 @@ def sync_knowledge_base_to_db():
|
||||
cursor = conn.cursor()
|
||||
for rule in rules_to_insert:
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO ai_knowledge_base (rule_text) VALUES (?)",
|
||||
(rule.strip(),)
|
||||
"INSERT OR IGNORE INTO ai_knowledge_base (rule_text, added_by) VALUES (?, ?)",
|
||||
(rule.strip(), "System_AI")
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
|
||||
def save_scud_to_db(df_scud, date_str):
|
||||
"""Оптимизированное сохранение логов СКУД за дату."""
|
||||
if df_scud.empty:
|
||||
|
||||
+50
-52
@@ -1,40 +1,39 @@
|
||||
import os
|
||||
import logging
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
import pyodbc
|
||||
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__)
|
||||
|
||||
# Конфигурация 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:
|
||||
"""Формирует строку подключения pyodbc к MS SQL Server."""
|
||||
"""Формирует строку подключения pyodbc к MS SQL Server из config.py."""
|
||||
return (
|
||||
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;"
|
||||
f"TrustServerCertificate={ZUP_SQL_CONFIG.get('trust_server_certificate', 'yes')};"
|
||||
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
|
||||
на указанную дату.
|
||||
на указанную дату (принимает как 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 = """
|
||||
DECLARE @TargetDate DATE = ?;
|
||||
|
||||
@@ -86,17 +85,21 @@ def fetch_zup_absences_from_sql(target_date: date) -> pd.DataFrame:
|
||||
ORDER BY [ФИО] ASC;
|
||||
"""
|
||||
|
||||
conn_str = get_zup_connection_string()
|
||||
with pyodbc.connect(conn_str) as conn:
|
||||
df = pd.read_sql(query, conn, params=[target_date])
|
||||
|
||||
if not df.empty:
|
||||
df['fio_clean'] = df['ФИО'].apply(normalize_fio)
|
||||
return df
|
||||
try:
|
||||
conn_str = get_zup_connection_string()
|
||||
with pyodbc.connect(conn_str, timeout=5) as conn:
|
||||
df = pd.read_sql(query, conn, params=[target_date])
|
||||
|
||||
if not df.empty:
|
||||
df['fio_clean'] = df['ФИО'].apply(normalize_fio)
|
||||
return df
|
||||
except Exception as e:
|
||||
logger.error(f"[❌] Ошибка SQL-выгрузки отсутствий за {target_date}: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
|
||||
def fetch_zup_staff_from_sql() -> pd.DataFrame:
|
||||
"""Извлекает весь актуальный штат из MS SQL 1С:ЗУП."""
|
||||
"""Резервная выгрузка штата из MS SQL."""
|
||||
query = """
|
||||
SELECT DISTINCT
|
||||
LTRIM(RTRIM(ref_emp._Description)) AS [ФИО],
|
||||
@@ -107,53 +110,48 @@ def fetch_zup_staff_from_sql() -> pd.DataFrame:
|
||||
AND ref_emp._Marked = 0x00
|
||||
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:
|
||||
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")
|
||||
os.makedirs(c_1c_dir, exist_ok=True)
|
||||
|
||||
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")
|
||||
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:
|
||||
# Пустой заголовок (8 строк) для эмуляции отчета 1С
|
||||
dummy_headers = pd.DataFrame([[""] * 13] * 8)
|
||||
dummy_headers.to_excel(writer, index=False, header=False)
|
||||
|
||||
# Данные
|
||||
df_staff[['ФИО', 'Подразделение', 'Должность']].to_excel(writer, startrow=8, index=False)
|
||||
|
||||
with pd.ExcelWriter(absences_excel_path, engine='openpyxl') as writer:
|
||||
# Пустой заголовок (3 строки) для эмуляции отчета 1С
|
||||
dummy_headers = pd.DataFrame([[""] * 3] * 3)
|
||||
dummy_headers.to_excel(writer, index=False, header=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
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user