feat(reports): stabilize on-demand generation, live presence and 1C fallback
This commit is contained in:
+69
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user