12.08.2026 17:30 Логика формирования отчетов работает, все отсутствия учитываются, логика присутствия сотрудников в случае сбоя СКУД на работе настроена.
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
# 🛠 HOWTO: Помощь в технической диагностике системы (`scud_orion_ai_v2`)
|
||||||
|
|
||||||
|
Данный документ содержит руководство по работе со служебными диагностическими утилитами проекта. Скрипты предназначены для быстрой оценки состояния базы данных SQLite, анализа файловой структуры и формирования контекстного снимка кодовой базы для ИИ.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Содержимое папки `scripts/diagnostics/`
|
||||||
|
|
||||||
|
Рекомендуется размещать диагностические скрипты в отдельной директории `scripts/diagnostics/`:
|
||||||
|
|
||||||
|
1. **`inspect_db.py`** — Инспектор таблиц и записей базы данных SQLite.
|
||||||
|
2. **`inspect_files.py`** — Сканер дискового пространства и структуры файлов.
|
||||||
|
3. **`make_code_snapshot.py`** — Генератор единого текстового слепка кода для ИИ.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Назначение и запуск утилит
|
||||||
|
|
||||||
|
### 1. Инспектор базы данных SQLite (`inspect_db.py`)
|
||||||
|
|
||||||
|
* **Назначение:**
|
||||||
|
* Проверяет доступность базы данных (`data/scud_orion_ai.db`).
|
||||||
|
* Автоматически считывает список всех таблиц и их колонок.
|
||||||
|
* Подсчитывает количество записей в каждой таблице (`scud_logs`, `zup_staff`, `zup_absences` и др.).
|
||||||
|
* Выводит список всех активных правил из таблицы `ai_knowledge_base`.
|
||||||
|
|
||||||
|
* **Команда для запуска:**
|
||||||
|
```bash
|
||||||
|
python scripts/diagnostics/inspect_db.py
|
||||||
|
2. Сканер файловой структуры (inspect_files.py)
|
||||||
|
Назначение:
|
||||||
|
|
||||||
|
Обходит структуру каталогов проекта.
|
||||||
|
|
||||||
|
Показывает точный размер каждого файла в байтах и итоговый вес проекта в мегабайтах.
|
||||||
|
|
||||||
|
Автоматически игнорирует служебные и временные папки (.git, __pycache__, venv, .venv).
|
||||||
|
|
||||||
|
Команда для запуска:
|
||||||
|
|
||||||
|
Bash
|
||||||
|
python scripts/diagnostics/inspect_files.py
|
||||||
|
3. Генератор контекстного снимка кода (make_code_snapshot.py)
|
||||||
|
Назначение:
|
||||||
|
|
||||||
|
Собирает весь исходный код Python (.py), настройки (.json), инструкции (.md) и скрипты (.sh) в единый файл project_code_snapshot.md.
|
||||||
|
|
||||||
|
Исключает тяжелые архивы, логи и виртуальное окружение.
|
||||||
|
|
||||||
|
Сформированный файл передается нейросети (LLM) для быстрого погружения в контекст проекта без потери архитектурных деталей.
|
||||||
|
|
||||||
|
Команда для запуска:
|
||||||
|
|
||||||
|
Bash
|
||||||
|
python scripts/diagnostics/make_code_snapshot.py
|
||||||
|
🎯 Чек-лист применения при сбоях
|
||||||
|
Ошибка или несостыковка в отчетах: запустить inspect_db.py и проверить, заполнены ли таблицы scud_logs и zup_absences.
|
||||||
|
|
||||||
|
Передача задачи или кода ИИ-ассистенту: запустить make_code_snapshot.py и прикрепить итоговый project_code_snapshot.md к запросу.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import openpyxl
|
||||||
|
|
||||||
|
wb_good = openpyxl.load_workbook('output/5 августа 2026 отчет.xlsx') # или ваш путь к рабочему
|
||||||
|
wb_bad = openpyxl.load_workbook('output/10 августа 2026 отчет.xlsx')
|
||||||
|
|
||||||
|
ws_good = wb_good.active
|
||||||
|
ws_bad = wb_bad.active
|
||||||
|
|
||||||
|
print(f"Размер хорошего: {ws_good.max_row} строк, {ws_good.max_column} колонок")
|
||||||
|
print(f"Размер с ошибкой: {ws_bad.max_row} строк, {ws_bad.max_column} колонок")
|
||||||
|
|
||||||
|
# Сравним типы ячеек в колонках
|
||||||
|
for r in range(4, min(ws_good.max_row, ws_bad.max_row) + 1):
|
||||||
|
for c in range(1, 11):
|
||||||
|
v_g = ws_good.cell(row=r, column=c).value
|
||||||
|
v_b = ws_bad.cell(row=r, column=c).value
|
||||||
|
t_g = type(v_g)
|
||||||
|
t_b = type(v_b)
|
||||||
|
if t_g != t_b:
|
||||||
|
|
||||||
|
print(f"Строка {r}, колонка {c}: тип в хорошем {t_g}, в плохом {t_b}")
|
||||||
+70
-35
@@ -231,57 +231,73 @@ def get_latest_snapshot_time(date_str=None):
|
|||||||
|
|
||||||
def load_scud_from_db_by_snapshot(date_str, snapshot_param=None):
|
def load_scud_from_db_by_snapshot(date_str, snapshot_param=None):
|
||||||
"""
|
"""
|
||||||
Загружает логи СКУД по ID снапшота ('20260805-001', 'Y20260805-001') или по штампу времени.
|
Загружает логи СКУД по ID снапшота.
|
||||||
С автоматическим маппингом полей Подразделения и Должности.
|
Если snapshot_param не задан (или искался прошлый день), приоритетно выбирает
|
||||||
|
финальный вечерний снапшот, начинающийся с буквы 'Y' (срез на 22:00) за дату date_str.
|
||||||
"""
|
"""
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
df = pd.DataFrame()
|
df = pd.DataFrame()
|
||||||
|
|
||||||
|
# 1. Если передан конкретный ID снапшота (например, '20260810-001')
|
||||||
if snapshot_param:
|
if snapshot_param:
|
||||||
if "-" in str(snapshot_param) or str(snapshot_param).startswith('Y'):
|
|
||||||
df = pd.read_sql_query(
|
|
||||||
"SELECT * FROM scud_logs WHERE log_date = ? AND snapshot_id = ?",
|
|
||||||
conn, params=(date_str, str(snapshot_param))
|
|
||||||
)
|
|
||||||
if df.empty:
|
|
||||||
df = pd.read_sql_query(
|
df = pd.read_sql_query(
|
||||||
"SELECT * FROM scud_logs WHERE snapshot_id = ?",
|
"SELECT * FROM scud_logs WHERE snapshot_id = ?",
|
||||||
conn, params=(str(snapshot_param),)
|
conn, params=(str(snapshot_param),)
|
||||||
)
|
)
|
||||||
|
|
||||||
if df.empty:
|
# 2. Если по snapshot_param ничего не найдено ИЛИ snapshot_param=None (запрос за вчерашний день):
|
||||||
|
if df.empty and date_str:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# ⭐️ ПРИОРИТЕТ 1: Ищем срез, начальный символ которого 'Y' (вечерний зафиксированный Y-снапшот)
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT snapshot_id FROM scud_logs WHERE log_date = ? AND snapshot_id LIKE 'Y%' ORDER BY id DESC LIMIT 1",
|
||||||
|
(date_str,)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
# ⭐️ ПРИОРИТЕТ 2: Если Y-снапшот не найден, берем самый свежий обычный
|
||||||
|
if not row:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT snapshot_id FROM scud_logs WHERE log_date = ? ORDER BY id DESC LIMIT 1",
|
||||||
|
(date_str,)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if row and row[0]:
|
||||||
|
target_snap_id = row[0]
|
||||||
df = pd.read_sql_query(
|
df = pd.read_sql_query(
|
||||||
"SELECT * FROM scud_logs WHERE log_date = ? AND snapshot_time = ?",
|
"SELECT * FROM scud_logs WHERE snapshot_id = ?",
|
||||||
conn, params=(date_str, str(snapshot_param))
|
conn, params=(target_snap_id,)
|
||||||
)
|
|
||||||
|
|
||||||
if df.empty:
|
|
||||||
latest_time = get_latest_snapshot_time(date_str)
|
|
||||||
if latest_time:
|
|
||||||
df = pd.read_sql_query(
|
|
||||||
"SELECT * FROM scud_logs WHERE log_date = ? AND snapshot_time = ?",
|
|
||||||
conn, params=(date_str, latest_time)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Приводим названия колонок к стандарту
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df['Сотрудник'] = df['fio']
|
rename_map = {
|
||||||
df['Пришел'] = df['is_present'].astype(int) == 1 if 'is_present' in df.columns else False
|
'department': 'Подразделение',
|
||||||
df['Начало_дня'] = df['time_in']
|
'position': 'Должность',
|
||||||
df['Первая_активность'] = df['first_activity']
|
'fio': 'Сотрудник',
|
||||||
df['Конец_дня'] = df['time_out']
|
'time_in': 'Начало_дня',
|
||||||
df['Находился_в_здании'] = df['time_in_building']
|
'first_activity': 'Первая_активность',
|
||||||
|
'time_out': 'Конец_дня',
|
||||||
|
'duration': 'Находился_в_здании',
|
||||||
|
'is_present': 'Пришел'
|
||||||
|
}
|
||||||
|
# Переименовываем имеющиеся колонки
|
||||||
|
df = df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns})
|
||||||
|
|
||||||
# 💡 Маппинг колонок БД в стандартные русские наименования
|
# Заполняем недостающие колонки значениями по умолчанию, если их не было в БД
|
||||||
if 'department' in df.columns and 'Подразделение' not in df.columns:
|
required_cols = ['Пришел', 'Начало_дня', 'Первая_активность', 'Конец_дня', 'Находился_в_здании', 'anomaly_flag']
|
||||||
df['Подразделение'] = df['department']
|
for col in required_cols:
|
||||||
if 'position' in df.columns and 'Должность' not in df.columns:
|
if col not in df.columns:
|
||||||
df['Должность'] = df['position']
|
if col == 'Пришел':
|
||||||
|
df[col] = False
|
||||||
else:
|
else:
|
||||||
df = pd.DataFrame(columns=[
|
df[col] = '—'
|
||||||
'Сотрудник', 'fio_clean', 'Подразделение', 'Должность',
|
|
||||||
'Начало_дня', 'Первая_активность', 'Конец_дня',
|
# Приводим тип флага Пришел к bool
|
||||||
'Находился_в_здании', 'Пришел', 'anomaly_flag'
|
if 'Пришел' in df.columns:
|
||||||
])
|
df['Пришел'] = df['Пришел'].astype(bool)
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
@@ -443,3 +459,22 @@ def add_department_synonym_to_db(short_name, full_name):
|
|||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print(f"[✓] В базу SQLite добавлен новый синоним отдела: '{short_name}' ⟷ '{full_name}'")
|
print(f"[✓] В базу SQLite добавлен новый синоним отдела: '{short_name}' ⟷ '{full_name}'")
|
||||||
|
|
||||||
|
def load_staff_from_db(date_str):
|
||||||
|
"""Загружает Штатное расписание 1С из базы SQLite за указанную дату."""
|
||||||
|
with get_connection() as conn:
|
||||||
|
df = pd.read_sql_query(
|
||||||
|
"SELECT fio as 'ФИО', fio_clean, department as 'Подразделение', position as 'Должность' FROM zup_staff WHERE snapshot_date = ?",
|
||||||
|
conn, params=(date_str,)
|
||||||
|
)
|
||||||
|
return df if not df.empty else None
|
||||||
|
|
||||||
|
def load_absences_from_db(date_str):
|
||||||
|
"""Загружает документальные отсутствия 1С из базы SQLite за указанную дату."""
|
||||||
|
with get_connection() as conn:
|
||||||
|
df = pd.read_sql_query(
|
||||||
|
"SELECT fio as 'ФИО', fio_clean, absence_type as 'Вид_отсутствия' FROM zup_absences WHERE absence_date = ?",
|
||||||
|
conn, params=(date_str,)
|
||||||
|
)
|
||||||
|
return df if not df.empty else None
|
||||||
|
|
||||||
Binary file not shown.
@@ -3,7 +3,7 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ from config import DATE_TODAY, DATE_YESTERDAY, OUTPUT_DIR, DATA_DIR, normalize_f
|
|||||||
from services.scud_export import run_export
|
from services.scud_export import run_export
|
||||||
from services.share_copier import copy_1c_files_from_share
|
from services.share_copier import copy_1c_files_from_share
|
||||||
from services.data_validator import check_file_freshness
|
from services.data_validator import check_file_freshness
|
||||||
from services.data_loader import load_all_data, load_scud_data
|
from services.data_loader import load_1c_data_smart, load_scud_data
|
||||||
from services.excel_exporter import generate_summary_excel, generate_detailed_excel
|
from services.excel_exporter import generate_summary_excel, generate_detailed_excel
|
||||||
from services.ai_verifier import ai_verify_scud_against_staff, analyze_scud_mass_failure_ai
|
from services.ai_verifier import ai_verify_scud_against_staff, analyze_scud_mass_failure_ai
|
||||||
from services.text_reporter import generate_markdown_report
|
from services.text_reporter import generate_markdown_report
|
||||||
@@ -24,45 +24,12 @@ from core.database import (
|
|||||||
save_absences_to_db,
|
save_absences_to_db,
|
||||||
save_anomalies_to_db,
|
save_anomalies_to_db,
|
||||||
load_scud_from_db_by_snapshot,
|
load_scud_from_db_by_snapshot,
|
||||||
|
load_staff_from_db,
|
||||||
|
load_absences_from_db,
|
||||||
get_latest_snapshot_time,
|
get_latest_snapshot_time,
|
||||||
has_scud_logs_for_date
|
has_scud_logs_for_date
|
||||||
)
|
)
|
||||||
|
|
||||||
from services.ai_verifier import resolve_department_exception_ai
|
|
||||||
|
|
||||||
def apply_exceptions_from_json(df, exceptions_cfg):
|
|
||||||
"""
|
|
||||||
Размечает флаг is_excluded=True на основе правил из exceptions.json и БД SQLite.
|
|
||||||
Сверяет одновременно 1С, СКУД и использует интеллектуальный поиск отделов.
|
|
||||||
"""
|
|
||||||
if df is None or df.empty or not exceptions_cfg:
|
|
||||||
if df is not None:
|
|
||||||
df['is_excluded'] = False
|
|
||||||
return df
|
|
||||||
|
|
||||||
deps = exceptions_cfg.get("departments", [])
|
|
||||||
exact_pos = [p.lower() for p in exceptions_cfg.get("positions", [])]
|
|
||||||
pos_kw = [k.lower() for k in exceptions_cfg.get("position_keywords", [])]
|
|
||||||
exc_fios = [normalize_fio(f) for f in exceptions_cfg.get("fio", [])]
|
|
||||||
|
|
||||||
df['is_excluded'] = False
|
|
||||||
|
|
||||||
for idx, row in df.iterrows():
|
|
||||||
fio_clean = row.get('fio_clean', '')
|
|
||||||
dep_1c = row.get('Подразделение', '')
|
|
||||||
dep_scud = row.get('department_scud', row.get('department', ''))
|
|
||||||
pos = str(row.get('Должность', '')).lower()
|
|
||||||
|
|
||||||
is_fio_exc = fio_clean in exc_fios
|
|
||||||
is_pos_exc = (pos in exact_pos) or any(k in pos for k in pos_kw) if pos else False
|
|
||||||
|
|
||||||
# Двухуровневая интеллектуальная проверка отделов (1С + СКУД + ИИ-Кеш БД)
|
|
||||||
is_dep_exc = resolve_department_exception_ai(dep_1c, dep_scud, deps)
|
|
||||||
|
|
||||||
if is_fio_exc or is_dep_exc or is_pos_exc:
|
|
||||||
df.at[idx, 'is_excluded'] = True
|
|
||||||
|
|
||||||
return df
|
|
||||||
|
|
||||||
def load_exceptions_config():
|
def load_exceptions_config():
|
||||||
"""Загружает файл exceptions.json из корня проекта."""
|
"""Загружает файл exceptions.json из корня проекта."""
|
||||||
@@ -80,18 +47,18 @@ def load_exceptions_config():
|
|||||||
|
|
||||||
def apply_exceptions_from_json(df, exceptions_cfg):
|
def apply_exceptions_from_json(df, exceptions_cfg):
|
||||||
"""
|
"""
|
||||||
Размечает флаг is_excluded=True на основе правил из exceptions.json.
|
Быстрая и эффективная разметка флага is_excluded=True на основе exceptions.json.
|
||||||
Сверяет отдел одновременно по полному имени из 1С и по короткой аббревиатуре из СКУД.
|
Выполняется мгновенно без цикличных HTTP-запросов к ИИ.
|
||||||
"""
|
"""
|
||||||
if df is None or df.empty or not exceptions_cfg:
|
if df is None or df.empty or not exceptions_cfg:
|
||||||
if df is not None:
|
if df is not None:
|
||||||
df['is_excluded'] = False
|
df['is_excluded'] = False
|
||||||
return df
|
return df
|
||||||
|
|
||||||
deps = [d.lower() for d in exceptions_cfg.get("departments", [])]
|
deps = [d.strip().lower() for d in exceptions_cfg.get("departments", []) if d]
|
||||||
exact_pos = [p.lower() for p in exceptions_cfg.get("positions", [])]
|
exact_pos = [p.strip().lower() for p in exceptions_cfg.get("positions", []) if p]
|
||||||
pos_kw = [k.lower() for k in exceptions_cfg.get("position_keywords", [])]
|
pos_kw = [k.strip().lower() for k in exceptions_cfg.get("position_keywords", []) if k]
|
||||||
exc_fios = [normalize_fio(f) for f in exceptions_cfg.get("fio", [])]
|
exc_fios = [normalize_fio(f) for f in exceptions_cfg.get("fio", []) if f]
|
||||||
|
|
||||||
df['is_excluded'] = False
|
df['is_excluded'] = False
|
||||||
|
|
||||||
@@ -102,8 +69,12 @@ def apply_exceptions_from_json(df, exceptions_cfg):
|
|||||||
pos = str(row.get('Должность', '')).lower()
|
pos = str(row.get('Должность', '')).lower()
|
||||||
|
|
||||||
is_fio_exc = fio_clean in exc_fios
|
is_fio_exc = fio_clean in exc_fios
|
||||||
is_dep_exc = any(d in dep_1c or d == dep_scud for d in deps) if (dep_1c or dep_scud) else False
|
is_pos_exc = (pos in exact_pos) or any(k in pos for k in pos_kw if k) if pos else False
|
||||||
is_pos_exc = (pos in exact_pos) or any(k in pos for k in pos_kw) if pos else False
|
|
||||||
|
# Быстрая проверка отделов по подстрокам
|
||||||
|
is_dep_exc = False
|
||||||
|
if deps:
|
||||||
|
is_dep_exc = any(d in dep_1c or d in dep_scud for d in deps)
|
||||||
|
|
||||||
if is_fio_exc or is_dep_exc or is_pos_exc:
|
if is_fio_exc or is_dep_exc or is_pos_exc:
|
||||||
df.at[idx, 'is_excluded'] = True
|
df.at[idx, 'is_excluded'] = True
|
||||||
@@ -127,7 +98,6 @@ def filter_report_dataframe(merged_df):
|
|||||||
is_not_excluded = merged_df.get('is_excluded', False) == False
|
is_not_excluded = merged_df.get('is_excluded', False) == False
|
||||||
is_not_no_pass = merged_df.get('no_scud_pass', False) == False
|
is_not_no_pass = merged_df.get('no_scud_pass', False) == False
|
||||||
|
|
||||||
# В отчет попадают ТОЛЬКО офисные сотрудники с пропусками ИЛИ имеющие документ из 1С
|
|
||||||
filtered_df = merged_df[(is_not_excluded & is_not_no_pass) | has_1c_reason].copy()
|
filtered_df = merged_df[(is_not_excluded & is_not_no_pass) | has_1c_reason].copy()
|
||||||
return filtered_df
|
return filtered_df
|
||||||
|
|
||||||
@@ -150,7 +120,6 @@ def load_static_reason_workers():
|
|||||||
def detect_all_anomalies(merged_df, static_reasons_dict, kb_rules, scud_fios_set=None):
|
def detect_all_anomalies(merged_df, static_reasons_dict, kb_rules, scud_fios_set=None):
|
||||||
"""
|
"""
|
||||||
Автоматически выявляет истинные аномалии СКУД ⟷ 1С.
|
Автоматически выявляет истинные аномалии СКУД ⟷ 1С.
|
||||||
Исключения (ОВК/Клининг/Охрана) НЕ попадают в аномалии СКУД, если на них нет документа из 1С.
|
|
||||||
"""
|
"""
|
||||||
anomalies = []
|
anomalies = []
|
||||||
kb_rules_text = " ".join(kb_rules).lower() if kb_rules else ""
|
kb_rules_text = " ".join(kb_rules).lower() if kb_rules else ""
|
||||||
@@ -168,7 +137,6 @@ def detect_all_anomalies(merged_df, static_reasons_dict, kb_rules, scud_fios_set
|
|||||||
|
|
||||||
is_fio_whitelisted_in_kb = fio_clean.lower() in kb_rules_text
|
is_fio_whitelisted_in_kb = fio_clean.lower() in kb_rules_text
|
||||||
|
|
||||||
# 🚨 АНОМАЛИЯ 1: Физическое присутствие при документе отсутствия 1С
|
|
||||||
if is_present and has_1c_reason:
|
if is_present and has_1c_reason:
|
||||||
reason_lower = reason_1c.lower()
|
reason_lower = reason_1c.lower()
|
||||||
is_allowed_trip = any(kw in reason_lower for kw in ALLOWED_WORK_TRIP_KEYWORDS)
|
is_allowed_trip = any(kw in reason_lower for kw in ALLOWED_WORK_TRIP_KEYWORDS)
|
||||||
@@ -180,11 +148,9 @@ def detect_all_anomalies(merged_df, static_reasons_dict, kb_rules, scud_fios_set
|
|||||||
"details": f"Сотрудник пришел по СКУД, но в 1С оформлен документ: '{reason_1c}'"
|
"details": f"Сотрудник пришел по СКУД, но в 1С оформлен документ: '{reason_1c}'"
|
||||||
})
|
})
|
||||||
|
|
||||||
# ИСКЛЮЧЕНИЯ без документов 1С НЕ пропускаются дальше к аномалиям СКУД
|
|
||||||
if is_exc and not has_1c_reason:
|
if is_exc and not has_1c_reason:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 🚨 АНОМАЛИЯ 2: Перемещение внутри здания без отметки утреннего входа
|
|
||||||
if anom_flag == 'ANOMALY_NO_IN_HAS_ACTIVITY':
|
if anom_flag == 'ANOMALY_NO_IN_HAS_ACTIVITY':
|
||||||
first_act = row.get('Первая_активность', '—')
|
first_act = row.get('Первая_активность', '—')
|
||||||
anomalies.append({
|
anomalies.append({
|
||||||
@@ -193,7 +159,6 @@ def detect_all_anomalies(merged_df, static_reasons_dict, kb_rules, scud_fios_set
|
|||||||
"details": f"Отсутствует регистрация входа на КПП при зафиксированной первой активности в {first_act}"
|
"details": f"Отсутствует регистрация входа на КПП при зафиксированной первой активности в {first_act}"
|
||||||
})
|
})
|
||||||
|
|
||||||
# 🚨 АНОМАЛИЯ 3: Сотрудник есть в Штате 1С, но ПОЛНОСТЬЮ ОТСУТСТВУЕТ в базе СКУД Орион Pro
|
|
||||||
if scud_fios_set is not None:
|
if scud_fios_set is not None:
|
||||||
if fio_clean not in scud_fios_set and not has_1c_reason:
|
if fio_clean not in scud_fios_set and not has_1c_reason:
|
||||||
anomalies.append({
|
anomalies.append({
|
||||||
@@ -274,6 +239,8 @@ def main():
|
|||||||
else:
|
else:
|
||||||
print("[0/5] Пропуск прямого экспорта из MS SQL (чтение из базы SQLite)...")
|
print("[0/5] Пропуск прямого экспорта из MS SQL (чтение из базы SQLite)...")
|
||||||
|
|
||||||
|
# ⚡️ ПРОВЕРКА ШАРЫ ТОЛЬКО ДЛЯ ОБЫЧНОГО ДНЕВНОГО ЗАПУСКА
|
||||||
|
if not args.snapshot and not args.skip_export:
|
||||||
copy_1c_files_from_share()
|
copy_1c_files_from_share()
|
||||||
|
|
||||||
print("[1/5] Проверка актуальности и свежести входных данных...")
|
print("[1/5] Проверка актуальности и свежести входных данных...")
|
||||||
@@ -294,45 +261,62 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print("[✓] Проверка доступности данных успешно пройдена!\n")
|
print("[✓] Проверка доступности данных успешно пройдена!\n")
|
||||||
|
else:
|
||||||
|
print("[1/5] Пропуск проверки сетевой шары (все данные читаются из SQLite)...")
|
||||||
|
has_today_1c = True
|
||||||
|
|
||||||
print("[2/5] Загрузка данных из СКУД, 1С:ЗУП, реестра причин и исключений...")
|
print("[2/5] Загрузка данных из СКУД, 1С:ЗУП, реестра причин и исключений...")
|
||||||
|
|
||||||
|
# 🎯 ВЫЧИСЛЕНИЕ ДАТ СНАПШОТА И ДАТ НАКАНУНЕ
|
||||||
if args.snapshot or args.skip_export:
|
if args.snapshot or args.skip_export:
|
||||||
raw_scud_today_df = load_scud_from_db_by_snapshot(DATE_TODAY, snapshot_param)
|
raw_scud_today_df = load_scud_from_db_by_snapshot(None, snapshot_param=snapshot_param)
|
||||||
raw_scud_yesterday_df = load_scud_from_db_by_snapshot(DATE_YESTERDAY, snapshot_param)
|
|
||||||
|
|
||||||
if raw_scud_yesterday_df.empty:
|
if not raw_scud_today_df.empty and 'log_date' in raw_scud_today_df.columns:
|
||||||
raw_scud_yesterday_df = load_scud_from_db_by_snapshot(DATE_YESTERDAY)
|
target_date_str = str(raw_scud_today_df['log_date'].iloc[0])
|
||||||
|
|
||||||
(
|
|
||||||
_, _,
|
|
||||||
df_staff_today, df_absent_today,
|
|
||||||
df_staff_yesterday, df_absent_yesterday
|
|
||||||
) = load_all_data()
|
|
||||||
else:
|
else:
|
||||||
(
|
target_date_str = DATE_TODAY
|
||||||
raw_scud_today_df, raw_scud_yesterday_df,
|
|
||||||
df_staff_today, df_absent_today,
|
dt_target = datetime.strptime(target_date_str, "%d.%m.%Y")
|
||||||
df_staff_yesterday, df_absent_yesterday
|
if dt_target.weekday() == 0: # Понедельник -> Пятница
|
||||||
) = load_all_data()
|
dt_yesterday = dt_target - timedelta(days=3)
|
||||||
|
else:
|
||||||
|
dt_yesterday = dt_target - timedelta(days=1)
|
||||||
|
yesterday_date_str = dt_yesterday.strftime("%d.%m.%Y")
|
||||||
|
|
||||||
|
print(f"[📸] СНАПШОТ ОПРЕДЕЛЕН: Целевая дата = {target_date_str}, Накануне = {yesterday_date_str}")
|
||||||
|
|
||||||
|
raw_scud_yesterday_df = load_scud_from_db_by_snapshot(yesterday_date_str, snapshot_param=None)
|
||||||
|
|
||||||
|
# Гарантированная загрузка кадров 1С за обе даты (БД + локальный фолбэк)
|
||||||
|
df_staff_today, df_absent_today = load_1c_data_smart(target_date_str, use_db=True)
|
||||||
|
df_staff_yesterday, df_absent_yesterday = load_1c_data_smart(yesterday_date_str, use_db=True)
|
||||||
|
else:
|
||||||
|
target_date_str = DATE_TODAY
|
||||||
|
yesterday_date_str = DATE_YESTERDAY
|
||||||
|
|
||||||
|
raw_scud_today_df = load_scud_from_db_by_snapshot(target_date_str, snapshot_param=snapshot_param)
|
||||||
|
raw_scud_yesterday_df = load_scud_from_db_by_snapshot(yesterday_date_str, snapshot_param=None)
|
||||||
|
|
||||||
|
df_staff_today, df_absent_today = load_1c_data_smart(DATE_TODAY, use_db=False)
|
||||||
|
df_staff_yesterday, df_absent_yesterday = load_1c_data_smart(DATE_YESTERDAY, use_db=False)
|
||||||
|
|
||||||
if df_staff_yesterday is not None:
|
if df_staff_yesterday is not None:
|
||||||
save_staff_to_db(df_staff_yesterday, DATE_YESTERDAY)
|
save_staff_to_db(df_staff_yesterday, yesterday_date_str)
|
||||||
if df_absent_yesterday is not None:
|
if df_absent_yesterday is not None:
|
||||||
save_absences_to_db(df_absent_yesterday, DATE_YESTERDAY)
|
save_absences_to_db(df_absent_yesterday, yesterday_date_str)
|
||||||
|
|
||||||
if df_staff_today is not None:
|
if df_staff_today is not None:
|
||||||
save_staff_to_db(df_staff_today, DATE_TODAY)
|
save_staff_to_db(df_staff_today, target_date_str)
|
||||||
if df_absent_today is not None:
|
if df_absent_today is not None:
|
||||||
save_absences_to_db(df_absent_today, DATE_TODAY)
|
save_absences_to_db(df_absent_today, target_date_str)
|
||||||
|
|
||||||
staff_fios_yesterday_clean = df_staff_yesterday['fio_clean'].dropna().tolist() if df_staff_yesterday is not None else []
|
staff_fios_yesterday_clean = df_staff_yesterday['fio_clean'].dropna().tolist() if df_staff_yesterday is not None else []
|
||||||
static_reasons_dict = load_static_reason_workers()
|
static_reasons_dict = load_static_reason_workers()
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 🎯 ЧАСТЬ 1: ДЕТАЛЬНЫЙ ОТЧЕТ ЗА ВЧЕРА (ГАРАНТИРОВАННЫЙ РАСЧЕТ)
|
# 🎯 ЧАСТЬ 1: ДЕТАЛЬНЫЙ ОТЧЕТ ЗА ВЧЕРА (ДЕНЬ НАКАНУНЕ)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
print(f"\n[3/5] Обработка и построение детального отчета за ВЧЕРА ({DATE_YESTERDAY})...")
|
print(f"\n[3/5] Обработка и построение детального отчета за ВЧЕРА ({yesterday_date_str})...")
|
||||||
|
|
||||||
if not raw_scud_yesterday_df.empty and 'Пришел' not in raw_scud_yesterday_df.columns:
|
if not raw_scud_yesterday_df.empty and 'Пришел' not in raw_scud_yesterday_df.columns:
|
||||||
raw_scud_yesterday_df['Пришел'] = raw_scud_yesterday_df['is_present'].astype(int) == 1 if 'is_present' in raw_scud_yesterday_df.columns else False
|
raw_scud_yesterday_df['Пришел'] = raw_scud_yesterday_df['is_present'].astype(int) == 1 if 'is_present' in raw_scud_yesterday_df.columns else False
|
||||||
@@ -356,7 +340,9 @@ def main():
|
|||||||
|
|
||||||
raw_scud_yesterday_df = aggregate_scud_by_employee(raw_scud_yesterday_df, debug=DEBUG)
|
raw_scud_yesterday_df = aggregate_scud_by_employee(raw_scud_yesterday_df, debug=DEBUG)
|
||||||
|
|
||||||
merged_yesterday = df_staff_yesterday.copy() if df_staff_yesterday is not None else pd.DataFrame()
|
# ⭐️ ФОРМИРУЕМ МЕРДЖ ИСКЛЮЧИТЕЛЬНО НА БАЗЕ ШТАТА ЗА ПРОШЛЫЙ ДЕНЬ
|
||||||
|
merged_yesterday = df_staff_yesterday.copy() if (df_staff_yesterday is not None and not df_staff_yesterday.empty) else (df_staff_today.copy() if df_staff_today is not None else pd.DataFrame())
|
||||||
|
|
||||||
if not merged_yesterday.empty:
|
if not merged_yesterday.empty:
|
||||||
if not raw_scud_yesterday_df.empty:
|
if not raw_scud_yesterday_df.empty:
|
||||||
if 'Подразделение' in raw_scud_yesterday_df.columns:
|
if 'Подразделение' in raw_scud_yesterday_df.columns:
|
||||||
@@ -371,8 +357,16 @@ def main():
|
|||||||
on='fio_clean', how='left'
|
on='fio_clean', how='left'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if 'Вид_отсутствия' in merged_yesterday.columns:
|
||||||
|
merged_yesterday = merged_yesterday.drop(columns=['Вид_отсутствия'])
|
||||||
|
|
||||||
|
# Присоединяем отсутствия СТРОГО за дату yesterday_date_str
|
||||||
if df_absent_yesterday is not None and not df_absent_yesterday.empty:
|
if df_absent_yesterday is not None and not df_absent_yesterday.empty:
|
||||||
merged_yesterday = merged_yesterday.merge(df_absent_yesterday[['fio_clean', 'Вид_отсутствия']], on='fio_clean', how='left')
|
merged_yesterday = merged_yesterday.merge(
|
||||||
|
df_absent_yesterday[['fio_clean', 'Вид_отсутствия']],
|
||||||
|
on='fio_clean',
|
||||||
|
how='left'
|
||||||
|
)
|
||||||
|
|
||||||
if 'Пришел' not in merged_yesterday.columns:
|
if 'Пришел' not in merged_yesterday.columns:
|
||||||
merged_yesterday['Пришел'] = False
|
merged_yesterday['Пришел'] = False
|
||||||
@@ -399,7 +393,6 @@ def main():
|
|||||||
)
|
)
|
||||||
merged_yesterday.loc[mask_exc_yesterday, 'Вид_отсутствия'] = 'Исключение (ОВК/Подрядчики)'
|
merged_yesterday.loc[mask_exc_yesterday, 'Вид_отсутствия'] = 'Исключение (ОВК/Подрядчики)'
|
||||||
|
|
||||||
# Расчет флага no_scud_pass для вчера
|
|
||||||
scud_fios_yesterday_set = set(raw_scud_yesterday_df['fio_clean'].dropna().tolist()) if not raw_scud_yesterday_df.empty else set()
|
scud_fios_yesterday_set = set(raw_scud_yesterday_df['fio_clean'].dropna().tolist()) if not raw_scud_yesterday_df.empty else set()
|
||||||
merged_yesterday['no_scud_pass'] = (
|
merged_yesterday['no_scud_pass'] = (
|
||||||
(~merged_yesterday['fio_clean'].isin(scud_fios_yesterday_set)) &
|
(~merged_yesterday['fio_clean'].isin(scud_fios_yesterday_set)) &
|
||||||
@@ -407,28 +400,25 @@ def main():
|
|||||||
(merged_yesterday.get('is_excluded', False) == False)
|
(merged_yesterday.get('is_excluded', False) == False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 🚨 Рассчитываем аномалии за ВЧЕРА
|
|
||||||
anomalies_yesterday_list = detect_all_anomalies(merged_yesterday, static_reasons_dict, kb_rules, scud_fios_set=scud_fios_yesterday_set)
|
anomalies_yesterday_list = detect_all_anomalies(merged_yesterday, static_reasons_dict, kb_rules, scud_fios_set=scud_fios_yesterday_set)
|
||||||
save_anomalies_to_db(anomalies_yesterday_list, DATE_YESTERDAY)
|
save_anomalies_to_db(anomalies_yesterday_list, yesterday_date_str)
|
||||||
|
|
||||||
# ⚡️ Фильтрация: исключения НЕ попадают в вчерашний отчет (если нет отпуска из 1С)
|
|
||||||
filtered_yesterday = filter_report_dataframe(merged_yesterday)
|
filtered_yesterday = filter_report_dataframe(merged_yesterday)
|
||||||
generate_detailed_excel(merged_df=filtered_yesterday, date_str=DATE_YESTERDAY)
|
generate_detailed_excel(merged_df=filtered_yesterday, date_str=yesterday_date_str)
|
||||||
|
|
||||||
# Фиксируем вчерашний снапшот строго на 22:00:00
|
yesterday_dt_obj = datetime.strptime(yesterday_date_str, "%d.%m.%Y")
|
||||||
yesterday_dt_obj = datetime.strptime(DATE_YESTERDAY, "%d.%m.%Y")
|
|
||||||
yesterday_22_str = yesterday_dt_obj.strftime("%Y-%m-%d 22:00:00")
|
yesterday_22_str = yesterday_dt_obj.strftime("%Y-%m-%d 22:00:00")
|
||||||
save_scud_to_db(merged_yesterday, DATE_YESTERDAY, snapshot_time=yesterday_22_str, is_yesterday=True)
|
save_scud_to_db(merged_yesterday, yesterday_date_str, snapshot_time=yesterday_22_str, is_yesterday=True)
|
||||||
print(f"[✓] Детальный отчет за вчера сформирован и зафиксирован в SQLite со снапшотом на {yesterday_22_str}")
|
print(f"[✓] Детальный отчет за вчера сформирован и зафиксирован в SQLite за {yesterday_date_str}")
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 🎯 ЧАСТЬ 2: ЕЖЕДНЕВНАЯ СВОДКА ЗА СЕГОДНЯ
|
# 🎯 ЧАСТЬ 2: ЕЖЕДНЕВНАЯ СВОДКА (ЗА ЦЕЛЕВОЙ ДЕНЬ СНАПШОТА)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
if not args.snapshot and not args.skip_export:
|
if not args.snapshot and not args.skip_export:
|
||||||
save_scud_to_db(raw_scud_today_df, DATE_TODAY, snapshot_time=snapshot_param, is_yesterday=False)
|
save_scud_to_db(raw_scud_today_df, target_date_str, snapshot_time=snapshot_param, is_yesterday=False)
|
||||||
|
|
||||||
if has_today_1c and df_staff_today is not None and df_absent_today is not None:
|
if has_today_1c and df_staff_today is not None and df_absent_today is not None:
|
||||||
print("\n[4/5] Обработка данных за СЕГОДНЯ...")
|
print(f"\n[4/5] Обработка и построение Ежедневной сводки за {target_date_str}...")
|
||||||
staff_fios_today_clean = df_staff_today['fio_clean'].dropna().tolist()
|
staff_fios_today_clean = df_staff_today['fio_clean'].dropna().tolist()
|
||||||
|
|
||||||
if not raw_scud_today_df.empty and 'Пришел' not in raw_scud_today_df.columns:
|
if not raw_scud_today_df.empty and 'Пришел' not in raw_scud_today_df.columns:
|
||||||
@@ -493,7 +483,6 @@ def main():
|
|||||||
)
|
)
|
||||||
merged_today.loc[mask_exc_today, 'Вид_отсутствия'] = 'Исключение (ОВК/Подрядчики)'
|
merged_today.loc[mask_exc_today, 'Вид_отсутствия'] = 'Исключение (ОВК/Подрядчики)'
|
||||||
|
|
||||||
# Расчет флага no_scud_pass для сегодня
|
|
||||||
scud_fios_today_set = set(raw_scud_today_df['fio_clean'].dropna().tolist()) if not raw_scud_today_df.empty else set()
|
scud_fios_today_set = set(raw_scud_today_df['fio_clean'].dropna().tolist()) if not raw_scud_today_df.empty else set()
|
||||||
merged_today['no_scud_pass'] = (
|
merged_today['no_scud_pass'] = (
|
||||||
(~merged_today['fio_clean'].isin(scud_fios_today_set)) &
|
(~merged_today['fio_clean'].isin(scud_fios_today_set)) &
|
||||||
@@ -504,7 +493,6 @@ def main():
|
|||||||
scud_fios_set = set(raw_scud_today_df['fio_clean'].dropna().tolist()) if not raw_scud_today_df.empty else set()
|
scud_fios_set = set(raw_scud_today_df['fio_clean'].dropna().tolist()) if not raw_scud_today_df.empty else set()
|
||||||
anomalies_list = detect_all_anomalies(merged_today, static_reasons_dict, kb_rules, scud_fios_set=scud_fios_set)
|
anomalies_list = detect_all_anomalies(merged_today, static_reasons_dict, kb_rules, scud_fios_set=scud_fios_set)
|
||||||
|
|
||||||
# 🚨 ОПЕРАТИВНЫЙ МОНИТОРИНГ ОБОРУДОВАНИЯ СКУД ЗА СЕГОДНЯ:
|
|
||||||
mass_failure_today = analyze_scud_mass_failure_ai(raw_scud_today_df)
|
mass_failure_today = analyze_scud_mass_failure_ai(raw_scud_today_df)
|
||||||
if mass_failure_today and mass_failure_today.get("is_mass_failure"):
|
if mass_failure_today and mass_failure_today.get("is_mass_failure"):
|
||||||
print("\n" + "!" * 60)
|
print("\n" + "!" * 60)
|
||||||
@@ -512,9 +500,8 @@ def main():
|
|||||||
print(mass_failure_today["alert_text"])
|
print(mass_failure_today["alert_text"])
|
||||||
print("!" * 60 + "\n")
|
print("!" * 60 + "\n")
|
||||||
|
|
||||||
save_anomalies_to_db(anomalies_list, DATE_TODAY)
|
save_anomalies_to_db(anomalies_list, target_date_str)
|
||||||
|
|
||||||
# Флаги фильтрации для ИИ-репортера
|
|
||||||
is_no_pass_today = merged_today['no_scud_pass'] == True if 'no_scud_pass' in merged_today.columns else False
|
is_no_pass_today = merged_today['no_scud_pass'] == True if 'no_scud_pass' in merged_today.columns else False
|
||||||
is_exc_today = merged_today.get('is_excluded', False) == True
|
is_exc_today = merged_today.get('is_excluded', False) == True
|
||||||
|
|
||||||
@@ -524,7 +511,6 @@ def main():
|
|||||||
(~merged_today['Вид_отсутствия'].astype(str).str.startswith('Исключение'))
|
(~merged_today['Вид_отсутствия'].astype(str).str.startswith('Исключение'))
|
||||||
]
|
]
|
||||||
|
|
||||||
# Истинно неизвестные случаи (исключаем тех, у кого нет пропуска, и спецотделы ОВК/Охрана)
|
|
||||||
absent_unexplained = merged_today[
|
absent_unexplained = merged_today[
|
||||||
(merged_today['Пришел'] == False) &
|
(merged_today['Пришел'] == False) &
|
||||||
(merged_today['Вид_отсутствия'].isna() | (merged_today['Вид_отсутствия'].astype(str).str.strip() == '')) &
|
(merged_today['Вид_отсутствия'].isna() | (merged_today['Вид_отсутствия'].astype(str).str.strip() == '')) &
|
||||||
@@ -540,7 +526,6 @@ def main():
|
|||||||
]
|
]
|
||||||
|
|
||||||
print("[5/5] Запуск ИИ-аудитора и построение Ежедневной сводки...")
|
print("[5/5] Запуск ИИ-аудитора и построение Ежедневной сводки...")
|
||||||
# Фильтруем аномалии СКУД для текстового отчета (убираем аномалии "ОТСУТСТВУЕТ В СКУД", т.к. они ушли в раздел "Нет пропуска")
|
|
||||||
filtered_anomalies_list = [
|
filtered_anomalies_list = [
|
||||||
a for a in anomalies_list
|
a for a in anomalies_list
|
||||||
if "ОТСУТСТВУЕТ В СКУД" not in a.get('type', '')
|
if "ОТСУТСТВУЕТ В СКУД" not in a.get('type', '')
|
||||||
@@ -554,12 +539,12 @@ def main():
|
|||||||
anomalies_list=filtered_anomalies_list,
|
anomalies_list=filtered_anomalies_list,
|
||||||
raw_scud_df=raw_scud_today_df,
|
raw_scud_df=raw_scud_today_df,
|
||||||
raw_absent_df=df_absent_today,
|
raw_absent_df=df_absent_today,
|
||||||
date_str=DATE_TODAY
|
date_str=target_date_str
|
||||||
)
|
)
|
||||||
|
|
||||||
generate_summary_excel(merged_df=merged_today, date_str=DATE_TODAY)
|
generate_summary_excel(merged_df=merged_today, date_str=target_date_str)
|
||||||
|
|
||||||
md_report_path = os.path.join(OUTPUT_DIR, f"Сводка_контроллинга_{DATE_TODAY}.md")
|
md_report_path = os.path.join(OUTPUT_DIR, f"Сводка_контроллинга_{target_date_str}.md")
|
||||||
with open(md_report_path, "w", encoding="utf-8") as f:
|
with open(md_report_path, "w", encoding="utf-8") as f:
|
||||||
f.write(report_text)
|
f.write(report_text)
|
||||||
print(f"\n[✓] Текстовый отчет сохранен в: {md_report_path}")
|
print(f"\n[✓] Текстовый отчет сохранен в: {md_report_path}")
|
||||||
@@ -580,7 +565,7 @@ def main():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(report_text)
|
print(report_text)
|
||||||
else:
|
else:
|
||||||
print(f"\n[ℹ️] Формирование Ежедневной сводки за {DATE_TODAY} ПРОПУЩЕНО.")
|
print(f"\n[ℹ️] Формирование Ежедневной сводки за {target_date_str} ПРОПУЩЕНО.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+40
-9
@@ -71,7 +71,6 @@ def print_snapshots_list(date_str=None):
|
|||||||
snap_time = r[2] if r[2] else '—'
|
snap_time = r[2] if r[2] else '—'
|
||||||
count = r[3]
|
count = r[3]
|
||||||
|
|
||||||
# Извлекаем время (ЧЧ:ММ:СС) из snapshot_time, если оно есть
|
|
||||||
time_part = "—"
|
time_part = "—"
|
||||||
if snap_time and " " in snap_time:
|
if snap_time and " " in snap_time:
|
||||||
time_part = snap_time.split(" ")[1]
|
time_part = snap_time.split(" ")[1]
|
||||||
@@ -155,6 +154,30 @@ def inspect_scud(snapshot_id=None, date_str=None, export_xlsx=None):
|
|||||||
print("=" * 90 + "\n")
|
print("=" * 90 + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def print_absences(date_str=None):
|
||||||
|
"""Выводит список официально отсутствующих сотрудников из 1С:ЗУП за выбранный день."""
|
||||||
|
target_date = date_str if date_str else DATE_TODAY
|
||||||
|
print("\n" + "=" * 90)
|
||||||
|
print(f"📋 ОФИЦИАЛЬНЫЕ ОТСУТСТВИЯ ИЗ 1С:ЗУП ЗА ДАТУ [{target_date}]:")
|
||||||
|
print("=" * 90)
|
||||||
|
|
||||||
|
with get_connection() as conn:
|
||||||
|
df = pd.read_sql_query(
|
||||||
|
"SELECT fio as 'ФИО', absence_type as 'Причина отсутствия 1С' FROM zup_absences WHERE absence_date = ? ORDER BY absence_type, fio",
|
||||||
|
conn,
|
||||||
|
params=(target_date,)
|
||||||
|
)
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
print(f"Записи об отсутствиях 1С за {target_date} в базе не найдены.")
|
||||||
|
else:
|
||||||
|
print(f"Всего зафиксировано документов 1С: {len(df)}")
|
||||||
|
print("-" * 90)
|
||||||
|
print(df.to_string(index=False))
|
||||||
|
|
||||||
|
print("=" * 90 + "\n")
|
||||||
|
|
||||||
|
|
||||||
def print_anomalies():
|
def print_anomalies():
|
||||||
"""Выводит список аномалий СКУД из БД."""
|
"""Выводит список аномалий СКУД из БД."""
|
||||||
print("\n" + "=" * 80)
|
print("\n" + "=" * 80)
|
||||||
@@ -193,6 +216,7 @@ def dump_all_to_excel(out_filename="db_dump_full.xlsx"):
|
|||||||
df.to_excel(writer, sheet_name=table[:31], index=False)
|
df.to_excel(writer, sheet_name=table[:31], index=False)
|
||||||
print(f"[✓] Дамп успешно сохранен: {out_path}\n")
|
print(f"[✓] Дамп успешно сохранен: {out_path}\n")
|
||||||
|
|
||||||
|
|
||||||
def print_system_prompts():
|
def print_system_prompts():
|
||||||
"""Выводит все системные промпты из базы данных."""
|
"""Выводит все системные промпты из базы данных."""
|
||||||
print("\n" + "=" * 80)
|
print("\n" + "=" * 80)
|
||||||
@@ -211,6 +235,7 @@ def print_system_prompts():
|
|||||||
print(f"{r[4]}\n")
|
print(f"{r[4]}\n")
|
||||||
print("=" * 80 + "\n")
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
|
||||||
def print_session_states():
|
def print_session_states():
|
||||||
"""Выводит текущие активные сессии и превью (session_states)."""
|
"""Выводит текущие активные сессии и превью (session_states)."""
|
||||||
print("\n" + "=" * 80)
|
print("\n" + "=" * 80)
|
||||||
@@ -230,14 +255,14 @@ def print_session_states():
|
|||||||
print("=" * 80 + "\n")
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
HELP_TEXT = """
|
||||||
help_text = """
|
|
||||||
CLI-утилита инспекции и управления SQLite базой данных СКУД (scud_orion_ai.db)
|
CLI-утилита инспекции и управления SQLite базой данных СКУД (scud_orion_ai.db)
|
||||||
|
|
||||||
ДОСТУПНЫЕ КОМАНДЫ:
|
ДОСТУПНЫЕ КОМАНДЫ:
|
||||||
stats -- Общая статистика строк по всем таблицам БД
|
stats -- Общая статистика строк по всем таблицам БД
|
||||||
snapshots [ДД.ММ.ГГГГ] -- Посмотреть реестр снапшотов (опционально за конкретную дату)
|
snapshots [ДД.ММ.ГГГГ] -- Посмотреть реестр снапшотов (опционально за конкретную дату)
|
||||||
scud [ДД.ММ.ГГГГ] [--snapshot ID] [--export-xlsx NAME] -- Инспекция логов СКУД по дате/снапшоту и экспорт в Excel
|
scud [ДД.ММ.ГГГГ] [--snapshot ID] [--export-xlsx NAME] -- Инспекция логов СКУД по дате/снапшоту и экспорт в Excel
|
||||||
|
absences [ДД.ММ.ГГГГ] -- Посмотреть список официально отсутствующих из 1С:ЗУП
|
||||||
anomalies -- Посмотреть историю найденных аномалий СКУД ⟷ 1С
|
anomalies -- Посмотреть историю найденных аномалий СКУД ⟷ 1С
|
||||||
rules -- Посмотреть правила Базы Знаний ИИ из SQLite
|
rules -- Посмотреть правила Базы Знаний ИИ из SQLite
|
||||||
prompts -- Посмотреть системные промпты (system_prompts)
|
prompts -- Посмотреть системные промпты (system_prompts)
|
||||||
@@ -250,22 +275,26 @@ CLI-утилита инспекции и управления SQLite базой
|
|||||||
python scripts/db_cli.py snapshots 06.08.2026
|
python scripts/db_cli.py snapshots 06.08.2026
|
||||||
python scripts/db_cli.py scud 06.08.2026 --export-xlsx срез_четверг
|
python scripts/db_cli.py scud 06.08.2026 --export-xlsx срез_четверг
|
||||||
python scripts/db_cli.py scud --snapshot Y20260805-007
|
python scripts/db_cli.py scud --snapshot Y20260805-007
|
||||||
|
python scripts/db_cli.py absences 07.08.2026
|
||||||
python scripts/db_cli.py prompts
|
python scripts/db_cli.py prompts
|
||||||
python scripts/db_cli.py sessions
|
python scripts/db_cli.py sessions
|
||||||
python scripts/db_cli.py snapshot del Y20260805-007
|
python scripts/db_cli.py snapshot del Y20260805-007
|
||||||
python scripts/db_cli.py snapshot del --day 04.08.2026
|
python scripts/db_cli.py snapshot del --day 04.08.2026
|
||||||
python scripts/db_cli.py dump my_dump.xlsx
|
python scripts/db_cli.py dump my_dump.xlsx
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
if any(arg in sys.argv for arg in ['-h', '--help']):
|
if any(arg in sys.argv for arg in ['-h', '--help']):
|
||||||
print(help_text)
|
print(HELP_TEXT)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=help_text,
|
description=HELP_TEXT,
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
add_help=False # Отключаем стандартный вывод справки argparse
|
add_help=False
|
||||||
)
|
)
|
||||||
parser.add_argument('command', nargs='?', default=None, choices=['stats', 'snapshots', 'scud', 'anomalies', 'rules', 'prompts', 'sessions', 'dump', 'snapshot'], help="Основная команда")
|
parser.add_argument('command', nargs='?', default=None, choices=['stats', 'snapshots', 'scud', 'absences', 'anomalies', 'rules', 'prompts', 'sessions', 'dump', 'snapshot'], help="Основная команда")
|
||||||
parser.add_argument('action', nargs='?', default=None, help="Дополнительное действие (например, 'del')")
|
parser.add_argument('action', nargs='?', default=None, help="Дополнительное действие (например, 'del')")
|
||||||
parser.add_argument('param', nargs='?', default=None, help="Параметр (дата, ID снапшота или имя файла)")
|
parser.add_argument('param', nargs='?', default=None, help="Параметр (дата, ID снапшота или имя файла)")
|
||||||
parser.add_argument('--snapshot', type=str, default=None, help="ID конкретного снапшота для инспекции")
|
parser.add_argument('--snapshot', type=str, default=None, help="ID конкретного снапшота для инспекции")
|
||||||
@@ -278,7 +307,6 @@ CLI-утилита инспекции и управления SQLite базой
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Валидация команд
|
|
||||||
if args.command == 'stats':
|
if args.command == 'stats':
|
||||||
print_stats()
|
print_stats()
|
||||||
elif args.command == 'snapshots':
|
elif args.command == 'snapshots':
|
||||||
@@ -286,6 +314,9 @@ CLI-утилита инспекции и управления SQLite базой
|
|||||||
print_snapshots_list(date_str=date_val)
|
print_snapshots_list(date_str=date_val)
|
||||||
elif args.command == 'scud':
|
elif args.command == 'scud':
|
||||||
inspect_scud(snapshot_id=args.snapshot, date_str=args.param, export_xlsx=args.export_xlsx)
|
inspect_scud(snapshot_id=args.snapshot, date_str=args.param, export_xlsx=args.export_xlsx)
|
||||||
|
elif args.command == 'absences':
|
||||||
|
date_val = args.param or args.action
|
||||||
|
print_absences(date_str=date_val)
|
||||||
elif args.command == 'anomalies':
|
elif args.command == 'anomalies':
|
||||||
print_anomalies()
|
print_anomalies()
|
||||||
elif args.command == 'rules':
|
elif args.command == 'rules':
|
||||||
@@ -311,7 +342,7 @@ CLI-утилита инспекции и управления SQLite базой
|
|||||||
print("Используйте: python scripts/db_cli.py snapshot del [ID или --day 'ДД.ММ.ГГГГ']\n")
|
print("Используйте: python scripts/db_cli.py snapshot del [ID или --day 'ДД.ММ.ГГГГ']\n")
|
||||||
else:
|
else:
|
||||||
print(f"\n[❌] Ошибка: Неизвестная команда.")
|
print(f"\n[❌] Ошибка: Неизвестная команда.")
|
||||||
parser.print_help()
|
print(HELP_TEXT)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Автопоиск файла базы данных в проекте
|
||||||
|
db_path = 'data/scud_orion_ai.db' if os.path.exists('data/scud_orion_ai.db') else 'scud_orion_ai.db'
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"🔍 ДИАГНОСТИКА СУБД SQLITE: {db_path}")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
print(f"❌ Файл базы данных {db_path} не найден!")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 1. Список всех таблиц и колонок
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||||
|
tables = [t[0] for t in cursor.fetchall()]
|
||||||
|
|
||||||
|
print("\n📋 СТРУКТУРА ТАБЛИЦ И КОЛИЧЕСТВО ЗАПИСЕЙ:")
|
||||||
|
print("-" * 80)
|
||||||
|
for t_name in tables:
|
||||||
|
cursor.execute(f"PRAGMA table_info({t_name})")
|
||||||
|
cols = [c[1] for c in cursor.fetchall()]
|
||||||
|
|
||||||
|
cursor.execute(f"SELECT COUNT(*) FROM {t_name}")
|
||||||
|
count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
print(f"• [{t_name:<20}] — {count:>6} строк | Колонки: {cols}")
|
||||||
|
|
||||||
|
# 2. Просмотр правил Базы Знаний
|
||||||
|
if 'ai_knowledge_base' in tables:
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("🧠 АКТУАЛЬНЫЕ ПРАВИЛА БАЗЫ ЗНАНИЙ (ai_knowledge_base):")
|
||||||
|
print("=" * 80)
|
||||||
|
cursor.execute("SELECT id, rule_text, added_by FROM ai_knowledge_base ORDER BY id ASC")
|
||||||
|
rules = cursor.fetchall()
|
||||||
|
if not rules:
|
||||||
|
print("Таблица ai_knowledge_base пуста.")
|
||||||
|
else:
|
||||||
|
for r_id, r_text, r_author in rules:
|
||||||
|
print(f" {r_id}. [{r_author}] {r_text}\n")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("=" * 80)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("📂 ТЕКУЩЕЕ СОСТОЯНИЕ ФАЙЛОВ ПРОЕКТА (scud_orion_ai_v2)")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
total_files = 0
|
||||||
|
total_size = 0
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk('.'):
|
||||||
|
# Исключаем служебные каталоги
|
||||||
|
dirs[:] = [d for d in dirs if d not in ['.git', '__pycache__', 'venv', '.venv', 'extracted_project']]
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
p = os.path.join(root, f)
|
||||||
|
size = os.path.getsize(p)
|
||||||
|
total_files += 1
|
||||||
|
total_size += size
|
||||||
|
print(f"{p:<55} ({size:>10,} bytes)".replace(',', ' '))
|
||||||
|
|
||||||
|
print("-" * 80)
|
||||||
|
print(f"ИТОГО: файлов: {total_files} | Общий объем: {total_size / (1024 * 1024):.2f} MB")
|
||||||
|
print("=" * 80)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
OUTPUT_SNAPSHOT = "project_code_snapshot.md"
|
||||||
|
|
||||||
|
# Расширения файлов для включения в снимок
|
||||||
|
ALLOWED_EXTENSIONS = {'.py', '.json', '.md', '.sh', '.ini'}
|
||||||
|
EXCLUDE_DIRS = {'.git', '__pycache__', 'venv', '.venv', 'output', 'logs', 'extracted_project'}
|
||||||
|
EXCLUDE_FILES = {OUTPUT_SNAPSHOT, 'scud_orion_ai_v2.tar.gz', 'context_memory.db'}
|
||||||
|
|
||||||
|
print(f"🔄 Сборка полного контекстного слепка проекта в {OUTPUT_SNAPSHOT}...")
|
||||||
|
|
||||||
|
with open(OUTPUT_SNAPSHOT, 'w', encoding='utf-8') as out:
|
||||||
|
out.write("# 📦 ПОЛНЫЙ ИСХОДНЫЙ КОД И КОНФИГУРАЦИЯ ПРОЕКТА scud_orion_ai_v2\n\n")
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk('.'):
|
||||||
|
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
|
||||||
|
|
||||||
|
for file in sorted(files):
|
||||||
|
ext = os.path.splitext(file)[1].lower()
|
||||||
|
if ext in ALLOWED_EXTENSIONS and file not in EXCLUDE_FILES:
|
||||||
|
filepath = os.path.join(root, file)
|
||||||
|
out.write(f"## File: `{filepath}`\n")
|
||||||
|
out.write("```" + (ext.replace('.', '') if ext != '.md' else '') + "\n")
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
out.write(f.read())
|
||||||
|
except Exception as e:
|
||||||
|
out.write(f"// Ошибка чтения файла: {e}\n")
|
||||||
|
out.write("\n```\n\n")
|
||||||
|
|
||||||
|
print(f"✓ Успешно создан слепок проекта: {OUTPUT_SNAPSHOT} ({os.path.getsize(OUTPUT_SNAPSHOT):,} bytes)")
|
||||||
Binary file not shown.
+62
-23
@@ -5,9 +5,13 @@ 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С
|
# Импортируем прямую SQL-выгрузку отсутствий из 1С и функции работы с SQLite
|
||||||
from services.zup_extractor import fetch_zup_absences_from_sql
|
from services.zup_extractor import fetch_zup_absences_from_sql
|
||||||
from core.database import load_scud_from_db_by_snapshot
|
from core.database import (
|
||||||
|
load_scud_from_db_by_snapshot,
|
||||||
|
load_staff_from_db,
|
||||||
|
load_absences_from_db
|
||||||
|
)
|
||||||
|
|
||||||
warnings.filterwarnings('ignore', category=UserWarning, module='pandas')
|
warnings.filterwarnings('ignore', category=UserWarning, module='pandas')
|
||||||
|
|
||||||
@@ -56,8 +60,10 @@ def load_staff_data(date_str):
|
|||||||
def load_absent_data(date_str):
|
def load_absent_data(date_str):
|
||||||
"""
|
"""
|
||||||
Загружает документально подтвержденные отсутствия сотрудников
|
Загружает документально подтвержденные отсутствия сотрудников
|
||||||
НАПРЯМУЮ из базы MS SQL 1С:ЗУП за указанную дату.
|
НАПРЯМУЮ из базы MS SQL 1С:ЗУП за указанную дату,
|
||||||
|
а также автоматически обогащает их удаленщиками из static_reason_workers.csv.
|
||||||
"""
|
"""
|
||||||
|
df_absent = None
|
||||||
try:
|
try:
|
||||||
df_absent = fetch_zup_absences_from_sql(date_str)
|
df_absent = fetch_zup_absences_from_sql(date_str)
|
||||||
|
|
||||||
@@ -65,19 +71,63 @@ def load_absent_data(date_str):
|
|||||||
df_absent = df_absent.dropna(subset=['ФИО', 'Вид_отсутствия'])
|
df_absent = df_absent.dropna(subset=['ФИО', 'Вид_отсутствия'])
|
||||||
df_absent = df_absent[~df_absent['ФИО'].astype(str).str.contains('АО "ЛЕНМОРНИИПРОЕКТ"|Сотрудник', case=False, na=False)]
|
df_absent = df_absent[~df_absent['ФИО'].astype(str).str.contains('АО "ЛЕНМОРНИИПРОЕКТ"|Сотрудник', case=False, na=False)]
|
||||||
df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio)
|
df_absent['fio_clean'] = df_absent['ФИО'].apply(normalize_fio)
|
||||||
return df_absent[['fio_clean', 'Вид_отсутствия']].dropna(subset=['fio_clean'])
|
df_absent = df_absent[['fio_clean', 'Вид_отсутствия']].dropna(subset=['fio_clean'])
|
||||||
else:
|
else:
|
||||||
return None
|
df_absent = pd.DataFrame(columns=['fio_clean', 'Вид_отсутствия'])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[⚠️] Ошибка SQL-выгрузки отсутствий за {date_str}: {e}")
|
print(f"[⚠️] Ошибка SQL-выгрузки отсутствий за {date_str}: {e}")
|
||||||
return None
|
df_absent = pd.DataFrame(columns=['fio_clean', 'Вид_отсутствия'])
|
||||||
|
|
||||||
|
# ⭐️ ОБОГАЩЕНИЕ УДАЛЕНЩИКАМИ ИЗ static_reason_workers.csv
|
||||||
|
try:
|
||||||
|
from config import DATA_DIR
|
||||||
|
static_path = os.path.join(DATA_DIR, "static_reason_workers.csv")
|
||||||
|
if os.path.exists(static_path):
|
||||||
|
df_static = pd.read_csv(static_path, encoding='utf-8')
|
||||||
|
if 'fio' in df_static.columns and 'reason' in df_static.columns:
|
||||||
|
df_static['fio_clean'] = df_static['fio'].apply(normalize_fio)
|
||||||
|
existing_fios = set(df_absent['fio_clean'].dropna().tolist()) if not df_absent.empty else set()
|
||||||
|
|
||||||
|
new_rows = []
|
||||||
|
for _, s_row in df_static.iterrows():
|
||||||
|
if s_row['fio_clean'] not in existing_fios:
|
||||||
|
new_rows.append({
|
||||||
|
'fio_clean': s_row['fio_clean'],
|
||||||
|
'Вид_отсутствия': s_row['reason']
|
||||||
|
})
|
||||||
|
if new_rows:
|
||||||
|
df_absent = pd.concat([df_absent, pd.DataFrame(new_rows)], ignore_index=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[⚠️] Ошибка обогащения удаленщиками в load_absent_data: {e}")
|
||||||
|
|
||||||
|
return df_absent if not df_absent.empty else None
|
||||||
|
|
||||||
|
|
||||||
|
def load_1c_data_smart(date_str, use_db=False):
|
||||||
|
"""
|
||||||
|
Универсальный загрузчик данных 1С (Штат и Отсутствия).
|
||||||
|
Если use_db=True, приоритетно извлекает сохраненные данные из SQLite БД
|
||||||
|
за целевую дату указанного снапшота.
|
||||||
|
"""
|
||||||
|
df_staff = None
|
||||||
|
df_absent = None
|
||||||
|
|
||||||
|
if use_db:
|
||||||
|
df_staff = load_staff_from_db(date_str)
|
||||||
|
df_absent = load_absences_from_db(date_str)
|
||||||
|
|
||||||
|
# Если в базе нет записей за эту дату или use_db=False — загружаем из файлов/SQL
|
||||||
|
if df_staff is None:
|
||||||
|
df_staff = load_staff_data(date_str)
|
||||||
|
if df_absent is None:
|
||||||
|
df_absent = load_absent_data(date_str)
|
||||||
|
|
||||||
|
return df_staff, df_absent
|
||||||
|
|
||||||
|
|
||||||
def process_scud_anomalies(df_scud, exceptions=None):
|
def process_scud_anomalies(df_scud, exceptions=None):
|
||||||
"""
|
"""
|
||||||
Вычисляет 'Первую активность' и маркирует аномалии СКУД.
|
Вычисляет 'Первую активность' и маркирует аномалии СКУД.
|
||||||
Если 'Начало дня' == 'Нет входа', но есть любая физическая активность —
|
|
||||||
записывает первейшее время в 'Первая_активность', сбрасывает 'Пришел' в False и выставляет флаг аномалии.
|
|
||||||
"""
|
"""
|
||||||
if exceptions is None:
|
if exceptions is None:
|
||||||
exceptions = load_exceptions()
|
exceptions = load_exceptions()
|
||||||
@@ -100,7 +150,6 @@ def process_scud_anomalies(df_scud, exceptions=None):
|
|||||||
val_end = str(row.get(end_col, '')).strip() if end_col else ''
|
val_end = str(row.get(end_col, '')).strip() if end_col else ''
|
||||||
val_status = str(row.get(status_col, '')).strip().lower() if status_col else ''
|
val_status = str(row.get(status_col, '')).strip().lower() if status_col else ''
|
||||||
|
|
||||||
# Извлечение первого зафиксированного события из сырых логов (pLogData), если передавалось
|
|
||||||
raw_first_event = row.get('raw_first_event', None)
|
raw_first_event = row.get('raw_first_event', None)
|
||||||
if pd.isna(raw_first_event) or str(raw_first_event).strip() in ['', 'None', 'nan', '00:00:00']:
|
if pd.isna(raw_first_event) or str(raw_first_event).strip() in ['', 'None', 'nan', '00:00:00']:
|
||||||
raw_first_event = None
|
raw_first_event = None
|
||||||
@@ -110,18 +159,13 @@ def process_scud_anomalies(df_scud, exceptions=None):
|
|||||||
|
|
||||||
first_act = '—'
|
first_act = '—'
|
||||||
anom_flag = 'NONE'
|
anom_flag = 'NONE'
|
||||||
|
|
||||||
# Стандартная проверка присутствия
|
|
||||||
is_present = not ('отсутствовал' in val_status or has_no_in)
|
is_present = not ('отсутствовал' in val_status or has_no_in)
|
||||||
|
|
||||||
# ЛОГИКА ВЫЧИСЛЕНИЯ ПЕРВОЙ АКТИВНОСТИ И ДЕТЕКТИРОВАНИЯ АНОМАЛИИ
|
|
||||||
if has_no_in:
|
if has_no_in:
|
||||||
# Если нет входа, но есть проход внутри или зафиксирован выход
|
|
||||||
if raw_first_event is not None or not has_no_out:
|
if raw_first_event is not None or not has_no_out:
|
||||||
first_act = str(raw_first_event) if raw_first_event else val_end
|
first_act = str(raw_first_event) if raw_first_event else val_end
|
||||||
anom_flag = 'ANOMALY_NO_IN_HAS_ACTIVITY'
|
anom_flag = 'ANOMALY_NO_IN_HAS_ACTIVITY'
|
||||||
|
|
||||||
# ЖЕСТКАЯ ВАЛИДАЦИЯ: Если человек не исключен официально — сбрасываем фейковый Пришел = True
|
|
||||||
if not is_excluded(fio, dept, pos, exceptions):
|
if not is_excluded(fio, dept, pos, exceptions):
|
||||||
is_present = False
|
is_present = False
|
||||||
else:
|
else:
|
||||||
@@ -145,14 +189,13 @@ def load_scud_data(target_date_type="today", snapshot_param=None):
|
|||||||
"""
|
"""
|
||||||
target_date = DATE_TODAY if target_date_type == "today" else DATE_YESTERDAY
|
target_date = DATE_TODAY if target_date_type == "today" else DATE_YESTERDAY
|
||||||
|
|
||||||
# Загружаем данные строго из SQLite БД
|
|
||||||
df_scud = load_scud_from_db_by_snapshot(target_date, snapshot_param=snapshot_param)
|
df_scud = load_scud_from_db_by_snapshot(target_date, snapshot_param=snapshot_param)
|
||||||
|
|
||||||
if df_scud is None or df_scud.empty:
|
if df_scud is None or df_scud.empty:
|
||||||
# Fallback на случай отсутствия записей за вчера — пробуем взять за сегодня
|
|
||||||
df_scud = load_scud_from_db_by_snapshot(DATE_TODAY, snapshot_param=snapshot_param)
|
df_scud = load_scud_from_db_by_snapshot(DATE_TODAY, snapshot_param=snapshot_param)
|
||||||
if df_scud is None or df_scud.empty:
|
if df_scud is None or df_scud.empty:
|
||||||
raise FileNotFoundError(f"В базе SQLite не найдены логи СКУД за {target_date}!")
|
print(f"[ℹ️] В базе SQLite / файлах отсутствует СКУД за {target_date}. Возвращаем пустой набор.")
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
exceptions = load_exceptions()
|
exceptions = load_exceptions()
|
||||||
|
|
||||||
@@ -161,7 +204,6 @@ def load_scud_data(target_date_type="today", snapshot_param=None):
|
|||||||
df_scud['fio_raw'] = df_scud[fio_raw_col].astype(str)
|
df_scud['fio_raw'] = df_scud[fio_raw_col].astype(str)
|
||||||
df_scud['fio_clean'] = df_scud['fio_raw'].apply(clean_scud_fio_light)
|
df_scud['fio_clean'] = df_scud['fio_raw'].apply(clean_scud_fio_light)
|
||||||
|
|
||||||
# Нормализация колонок под единый стандарт отчета
|
|
||||||
if 'Начало_дня' not in df_scud.columns and 'time_in' in df_scud.columns:
|
if 'Начало_дня' not in df_scud.columns and 'time_in' in df_scud.columns:
|
||||||
df_scud['Начало_дня'] = df_scud['time_in']
|
df_scud['Начало_дня'] = df_scud['time_in']
|
||||||
if 'Конец_дня' not in df_scud.columns and 'time_out' in df_scud.columns:
|
if 'Конец_дня' not in df_scud.columns and 'time_out' in df_scud.columns:
|
||||||
@@ -188,11 +230,8 @@ def load_all_data():
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
df_scud_yesterday = df_scud_today.copy()
|
df_scud_yesterday = df_scud_today.copy()
|
||||||
|
|
||||||
df_staff_today = load_staff_data(DATE_TODAY)
|
df_staff_today, df_absent_today = load_1c_data_smart(DATE_TODAY)
|
||||||
df_absent_today = load_absent_data(DATE_TODAY)
|
df_staff_yesterday, df_absent_yesterday = load_1c_data_smart(DATE_YESTERDAY)
|
||||||
|
|
||||||
df_staff_yesterday = load_staff_data(DATE_YESTERDAY)
|
|
||||||
df_absent_yesterday = load_absent_data(DATE_YESTERDAY)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
df_scud_today,
|
df_scud_today,
|
||||||
|
|||||||
@@ -81,11 +81,13 @@ WITH DailyLogs AS (
|
|||||||
WHERE log.TimeVal BETWEEN @StartDate AND @EndDate
|
WHERE log.TimeVal BETWEEN @StartDate AND @EndDate
|
||||||
AND log.HozOrgan IS NOT NULL
|
AND log.HozOrgan IS NOT NULL
|
||||||
AND log.HozOrgan > 0
|
AND log.HozOrgan > 0
|
||||||
|
AND log.Event IN (26, 27, 28, 29, 32, 33, 54, 55, 64, 65)
|
||||||
),
|
),
|
||||||
Passages AS (
|
Passages AS (
|
||||||
SELECT
|
SELECT
|
||||||
EmployeeID,
|
EmployeeID,
|
||||||
MIN(TimeVal) AS FirstRawEvent,
|
MIN(TimeVal) AS FirstRawEvent,
|
||||||
|
MAX(TimeVal) AS LastRawEvent,
|
||||||
MIN(CASE WHEN Direction = 'IN' THEN TimeVal END) AS FirstIn,
|
MIN(CASE WHEN Direction = 'IN' THEN TimeVal END) AS FirstIn,
|
||||||
MAX(CASE WHEN Direction = 'OUT' THEN TimeVal END) AS LastOut,
|
MAX(CASE WHEN Direction = 'OUT' THEN TimeVal END) AS LastOut,
|
||||||
MAX(CASE WHEN RowNumDesc = 1 THEN Direction END) AS LastEventType
|
MAX(CASE WHEN RowNumDesc = 1 THEN Direction END) AS LastEventType
|
||||||
@@ -116,21 +118,23 @@ SELECT
|
|||||||
THEN N'Нет выхода'
|
THEN N'Нет выхода'
|
||||||
WHEN pass.LastOut IS NOT NULL AND pass.LastOut > pass.FirstIn
|
WHEN pass.LastOut IS NOT NULL AND pass.LastOut > pass.FirstIn
|
||||||
THEN CAST(CONVERT(VARCHAR(8), pass.LastOut, 108) AS NVARCHAR(20))
|
THEN CAST(CONVERT(VARCHAR(8), pass.LastOut, 108) AS NVARCHAR(20))
|
||||||
|
WHEN @TargetDate < CAST(GETDATE() AS DATE) AND pass.LastRawEvent IS NOT NULL AND pass.LastRawEvent > ISNULL(pass.FirstIn, pass.FirstRawEvent)
|
||||||
|
THEN CAST(CONVERT(VARCHAR(8), pass.LastRawEvent, 108) AS NVARCHAR(20))
|
||||||
ELSE N'Нет выхода'
|
ELSE N'Нет выхода'
|
||||||
END AS [Конец_дня],
|
END AS [Конец_дня],
|
||||||
CASE
|
CASE
|
||||||
WHEN pass.EmployeeID IS NOT NULL AND pass.FirstIn IS NOT NULL THEN
|
WHEN pass.EmployeeID IS NOT NULL AND (pass.FirstIn IS NOT NULL OR pass.FirstRawEvent IS NOT NULL) THEN
|
||||||
RIGHT('0' + CAST(DATEDIFF(MINUTE,
|
RIGHT('0' + CAST(DATEDIFF(MINUTE,
|
||||||
pass.FirstIn,
|
ISNULL(pass.FirstIn, pass.FirstRawEvent),
|
||||||
CASE
|
CASE
|
||||||
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE()
|
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE()
|
||||||
ELSE ISNULL(pass.LastOut, pass.FirstIn)
|
ELSE ISNULL(pass.LastOut, pass.LastRawEvent)
|
||||||
END) / 60 AS VARCHAR), 2) + ':' +
|
END) / 60 AS VARCHAR), 2) + ':' +
|
||||||
RIGHT('0' + CAST(DATEDIFF(MINUTE,
|
RIGHT('0' + CAST(DATEDIFF(MINUTE,
|
||||||
pass.FirstIn,
|
ISNULL(pass.FirstIn, pass.FirstRawEvent),
|
||||||
CASE
|
CASE
|
||||||
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE()
|
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE()
|
||||||
ELSE ISNULL(pass.LastOut, pass.FirstIn)
|
ELSE ISNULL(pass.LastOut, pass.LastRawEvent)
|
||||||
END) % 60 AS VARCHAR), 2)
|
END) % 60 AS VARCHAR), 2)
|
||||||
ELSE N'00:00'
|
ELSE N'00:00'
|
||||||
END AS [Находился_в_здании],
|
END AS [Находился_в_здании],
|
||||||
|
|||||||
Reference in New Issue
Block a user