12.08.2026 17:30 Логика формирования отчетов работает, все отсутствия учитываются, логика присутствия сотрудников в случае сбоя СКУД на работе настроена.

This commit is contained in:
2026-08-12 17:33:48 +03:00
parent ffb0e18635
commit 8897b14197
13 changed files with 467 additions and 192 deletions
+62 -23
View File
@@ -5,9 +5,13 @@ 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С
# Импортируем прямую SQL-выгрузку отсутствий из 1С и функции работы с SQLite
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')
@@ -56,8 +60,10 @@ def load_staff_data(date_str):
def load_absent_data(date_str):
"""
Загружает документально подтвержденные отсутствия сотрудников
НАПРЯМУЮ из базы MS SQL 1С:ЗУП за указанную дату.
НАПРЯМУЮ из базы MS SQL 1С:ЗУП за указанную дату,
а также автоматически обогащает их удаленщиками из static_reason_workers.csv.
"""
df_absent = None
try:
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[~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'])
df_absent = df_absent[['fio_clean', 'Вид_отсутствия']].dropna(subset=['fio_clean'])
else:
return None
df_absent = pd.DataFrame(columns=['fio_clean', 'Вид_отсутствия'])
except Exception as 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):
"""
Вычисляет 'Первую активность' и маркирует аномалии СКУД.
Если 'Начало дня' == 'Нет входа', но есть любая физическая активность —
записывает первейшее время в 'Первая_активность', сбрасывает 'Пришел' в False и выставляет флаг аномалии.
"""
if exceptions is None:
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_status = str(row.get(status_col, '')).strip().lower() if status_col else ''
# Извлечение первого зафиксированного события из сырых логов (pLogData), если передавалось
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']:
raw_first_event = None
@@ -110,18 +159,13 @@ def process_scud_anomalies(df_scud, exceptions=None):
first_act = '—'
anom_flag = 'NONE'
# Стандартная проверка присутствия
is_present = not ('отсутствовал' in val_status or has_no_in)
# ЛОГИКА ВЫЧИСЛЕНИЯ ПЕРВОЙ АКТИВНОСТИ И ДЕТЕКТИРОВАНИЯ АНОМАЛИИ
if has_no_in:
# Если нет входа, но есть проход внутри или зафиксирован выход
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
anom_flag = 'ANOMALY_NO_IN_HAS_ACTIVITY'
# ЖЕСТКАЯ ВАЛИДАЦИЯ: Если человек не исключен официально — сбрасываем фейковый Пришел = True
if not is_excluded(fio, dept, pos, exceptions):
is_present = False
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
# Загружаем данные строго из SQLite БД
df_scud = load_scud_from_db_by_snapshot(target_date, snapshot_param=snapshot_param)
if df_scud is None or df_scud.empty:
# Fallback на случай отсутствия записей за вчера — пробуем взять за сегодня
df_scud = load_scud_from_db_by_snapshot(DATE_TODAY, snapshot_param=snapshot_param)
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()
@@ -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_clean'] = df_scud['fio_raw'].apply(clean_scud_fio_light)
# Нормализация колонок под единый стандарт отчета
if 'Начало_дня' not in df_scud.columns and 'time_in' in df_scud.columns:
df_scud['Начало_дня'] = df_scud['time_in']
if 'Конец_дня' not in df_scud.columns and 'time_out' in df_scud.columns:
@@ -188,11 +230,8 @@ def load_all_data():
except FileNotFoundError:
df_scud_yesterday = df_scud_today.copy()
df_staff_today = load_staff_data(DATE_TODAY)
df_absent_today = load_absent_data(DATE_TODAY)
df_staff_yesterday = load_staff_data(DATE_YESTERDAY)
df_absent_yesterday = load_absent_data(DATE_YESTERDAY)
df_staff_today, df_absent_today = load_1c_data_smart(DATE_TODAY)
df_staff_yesterday, df_absent_yesterday = load_1c_data_smart(DATE_YESTERDAY)
return (
df_scud_today,