feat(reports): stabilize on-demand generation, live presence and 1C fallback

This commit is contained in:
2026-09-25 13:00:11 +03:00
parent 2f8cc1bffd
commit 90656944c5
42 changed files with 13410 additions and 6778 deletions
+69 -12
View File
@@ -3,7 +3,8 @@
FILE: services/scud_export.py
ROLE: Прямой экспорт данных СКУД Орион (MS SQL) в SQLite и чистый Excel (XlsxWriter).
Корректная фильтрация транзитных проходов турникетов парковки и двора.
Учет только левого PERCo (DoorIndex = 1) и факта физического прохода (Event = 32).
Учет только левого PERCo (DoorIndex = 1), правого PERCo (DoorIndex = 2)
и входа через Флигель (DoorIndex = 23).
===============================================================================
"""
@@ -109,6 +110,12 @@ WITH PercoPassages AS (
log.DoorIndex = 2
AND ({turnstile_filter_sql})
)
OR
-- Контур 3: Флигель 1 эт. разрешен только для реестра флигеля
(
log.DoorIndex = 23
AND ({fligel_filter_sql})
)
)
),
Passages AS (
@@ -226,11 +233,24 @@ SELECT
FROM pLogData log WITH (NOLOCK)
INNER JOIN pList p WITH (NOLOCK) ON log.HozOrgan = p.ID
LEFT JOIN PDivision div WITH (NOLOCK) ON p.Section = div.ID
LEFT JOIN PPost post WITH (NOLOCK) ON p.Post = post.ID
WHERE log.TimeVal BETWEEN @StartDate AND @EndDate
AND log.HozOrgan IS NOT NULL
AND log.HozOrgan > 0
AND log.Event IN (28, 32)
-- ⭐️ Фильтрация сотрудников Ленморниипроект (исключение арендаторов и гостей)
AND ISNULL(p.StatusRecord, 0) = 0
AND p.DateTimeInArchive IS NULL
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'Аренд%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT IN (N'Без подразделения', N'')
AND p.Name NOT LIKE N'бр.%'
AND p.Name NOT LIKE N'Гость%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT IN (N'БГИ', N'КНР')
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'Рабоч%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'Врем%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'Практика%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'тест%'
AND ISNULL(CAST(post.Name AS NVARCHAR(255)), N'') NOT LIKE N'Практикант%'
ORDER BY log.TimeVal ASC;
"""
@@ -313,6 +333,26 @@ def run_export(input_date: str | None = None, input_time: str | None = None, sav
log("=== [ЭТАП 0] Выгрузка свежих данных СКУД напрямую из БД Орион ===")
os.makedirs(SCUD_DIR, exist_ok=True)
# ⭐️ 1. ПОЛУЧАСОВАЯ СИНХРОНИЗАЦИЯ 1С:ЗУП (Файлы с шары + База данных)
try:
log("[1C] Проверка сетевой шары и синхронизация файлов 1С...")
copy_1c_files_from_share()
target_sync_date = input_date or datetime.now().strftime("%d.%m.%Y")
# Прямой опрос свежих отсутствий и сохранение в SQLite
df_fresh_abs = load_absent_data(target_sync_date)
if df_fresh_abs is not None and not df_fresh_abs.empty:
save_absences_to_db(df_fresh_abs, target_sync_date)
log(f"[1C] [✓] Актуализировано {len(df_fresh_abs)} отсутствий в SQLite за {target_sync_date}", "SUCCESS")
# Если на шаре появился свежий штат — также обновляем его в базе
df_fresh_staff = load_staff_data(target_sync_date)
if df_fresh_staff is not None and not df_fresh_staff.empty:
save_staff_to_db(df_fresh_staff, target_sync_date)
log(f"[1C] [✓] Актуализирован штат ({len(df_fresh_staff)} чел.) в SQLite за {target_sync_date}", "SUCCESS")
except Exception as e:
log(f"[1C] ⚠️ Ошибка фоновой синхронизации 1С: {e}", "WARNING")
targets = get_targets(input_date, input_time)
conn_str = (
f"DRIVER={{{ODBC_DRIVER}}};"
@@ -338,22 +378,23 @@ def run_export(input_date: str | None = None, input_time: str | None = None, sav
log(f"[ℹ️] День ({processing_date_str}) уже зафиксирован финишным снапшотом _FINAL. Пропускаем.")
continue
# 1. Формируем фильтр правого турникета из exceptions_registry
exc_data = load_exceptions()
# 1. Формируем фильтр правого турникета (DoorIndex = 2)
t_fios = [f.replace("'", "''") for f in exc_data.get('turnstile_fio', []) if f]
t_depts = [d.replace("'", "''") for d in exc_data.get('turnstile_departments', []) if d]
full_fio_sql = (
"LTRIM(RTRIM("
"ISNULL(CAST(p.Name AS NVARCHAR(255)), N'') + "
"CASE WHEN p.FirstName IS NOT NULL AND CAST(p.FirstName AS NVARCHAR(255)) <> '' THEN N' ' + CAST(p.FirstName AS NVARCHAR(255)) ELSE N'' END + "
"CASE WHEN p.MidName IS NOT NULL AND CAST(p.MidName AS NVARCHAR(255)) <> '' THEN N' ' + CAST(p.MidName AS NVARCHAR(255)) ELSE N'' END"
"))"
)
conditions = []
if t_fios:
fio_in = ", ".join([f"N'{f}'" for f in t_fios])
# Склеиваем Фамилию + Имя + Отчество для точного сравнения с реестром ФИО
full_fio_sql = (
"LTRIM(RTRIM("
"ISNULL(CAST(p.Name AS NVARCHAR(255)), N'') + "
"CASE WHEN p.FirstName IS NOT NULL AND CAST(p.FirstName AS NVARCHAR(255)) <> '' THEN N' ' + CAST(p.FirstName AS NVARCHAR(255)) ELSE N'' END + "
"CASE WHEN p.MidName IS NOT NULL AND CAST(p.MidName AS NVARCHAR(255)) <> '' THEN N' ' + CAST(p.MidName AS NVARCHAR(255)) ELSE N'' END"
"))"
)
conditions.append(f"{full_fio_sql} IN ({fio_in})")
if t_depts:
@@ -362,7 +403,22 @@ def run_export(input_date: str | None = None, input_time: str | None = None, sav
turnstile_filter_sql = " OR ".join(conditions) if conditions else "1 = 0"
# 2. Безопасное математическое определение @EndDate через DATEADD (независимо от локали сервера)
# 1.1 Формируем фильтр Флигеля (DoorIndex = 23)
fl_fios = [f.replace("'", "''") for f in exc_data.get('fligel_fio', []) if f]
fl_depts = [d.replace("'", "''") for d in exc_data.get('fligel_departments', []) if d]
fl_conditions = []
if fl_fios:
fl_fio_in = ", ".join([f"N'{f}'" for f in fl_fios])
fl_conditions.append(f"{full_fio_sql} IN ({fl_fio_in})")
if fl_depts:
fl_dept_in = ", ".join([f"N'{d}'" for d in fl_depts])
fl_conditions.append(f"ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') IN ({fl_dept_in})")
fligel_filter_sql = " OR ".join(fl_conditions) if fl_conditions else "1 = 0"
# 2. Безопасное математическое определение @EndDate через DATEADD
if target_time:
t_clean = target_time.strip()
t_parts = t_clean.split(":")
@@ -387,7 +443,8 @@ def run_export(input_date: str | None = None, input_time: str | None = None, sav
sql_query = SQL_QUERY_TEMPLATE.format(
target_date=processing_date.strftime("%Y-%m-%d"),
end_datetime_sql=end_datetime_sql,
turnstile_filter_sql=turnstile_filter_sql
turnstile_filter_sql=turnstile_filter_sql,
fligel_filter_sql=fligel_filter_sql
)
connection = None