feat(scud): добавление таблицы scud_events_raw и сохранение детальной ленты проходов
This commit is contained in:
@@ -237,4 +237,40 @@ def delete_snapshots_by_date(date_str: str) -> int:
|
|||||||
cursor.execute("DELETE FROM scud_logs WHERE log_date = ? OR snapshot_id LIKE ?", (date_str, f"%{date_str.replace('.', '')}%"))
|
cursor.execute("DELETE FROM scud_logs WHERE log_date = ? OR snapshot_id LIKE ?", (date_str, f"%{date_str.replace('.', '')}%"))
|
||||||
cnt = cursor.rowcount
|
cnt = cursor.rowcount
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cnt
|
return cnt
|
||||||
|
|
||||||
|
def save_raw_events_to_db(df_raw_events: pd.DataFrame, date_str: str) -> int:
|
||||||
|
"""
|
||||||
|
Сохраняет ленту сырых физических проходов турникетов в таблицу scud_events_raw.
|
||||||
|
Перезаписывает сырые события за указанную дату для исключения дубликатов.
|
||||||
|
"""
|
||||||
|
if df_raw_events is None or df_raw_events.empty:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
records = [
|
||||||
|
(
|
||||||
|
date_str,
|
||||||
|
str(r.get('TimeVal', '')),
|
||||||
|
int(r.get('HozOrgan', 0)),
|
||||||
|
str(r.get('Сотрудник', '')),
|
||||||
|
str(r.get('fio_clean', '')),
|
||||||
|
str(r.get('Подразделение', '')),
|
||||||
|
int(r.get('Event', 0)),
|
||||||
|
int(r.get('Mode', 0)),
|
||||||
|
str(r.get('Direction', 'OTHER'))
|
||||||
|
)
|
||||||
|
for _, r in df_raw_events.iterrows()
|
||||||
|
]
|
||||||
|
|
||||||
|
with get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("DELETE FROM scud_events_raw WHERE log_date = ?", (date_str,))
|
||||||
|
cursor.executemany("""
|
||||||
|
INSERT INTO scud_events_raw (
|
||||||
|
log_date, time_val, hoz_organ, fio, fio_clean,
|
||||||
|
department, event_code, mode, direction
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", records)
|
||||||
|
conn.commit()
|
||||||
|
return len(records)
|
||||||
@@ -38,6 +38,23 @@ def init_all_tables() -> None:
|
|||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# 1.1 Сырые физические события турникетов СКУД (для перекуров и оперативного статуса)
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS scud_events_raw (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
log_date TEXT NOT NULL,
|
||||||
|
time_val TEXT NOT NULL,
|
||||||
|
hoz_organ INTEGER NOT NULL,
|
||||||
|
fio TEXT NOT NULL,
|
||||||
|
fio_clean TEXT NOT NULL,
|
||||||
|
department TEXT,
|
||||||
|
event_code INTEGER NOT NULL,
|
||||||
|
mode INTEGER NOT NULL,
|
||||||
|
direction TEXT NOT NULL, -- 'IN' или 'OUT'
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
# 2. Кадровые реестры 1С
|
# 2. Кадровые реестры 1С
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS zup_staff (
|
CREATE TABLE IF NOT EXISTS zup_staff (
|
||||||
@@ -172,5 +189,7 @@ def init_all_tables() -> None:
|
|||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_scud_date ON scud_logs(log_date);")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_scud_date ON scud_logs(log_date);")
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_scud_fio ON scud_logs(fio_clean);")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_scud_fio ON scud_logs(fio_clean);")
|
||||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_mapping_scud ON person_identity_mapping(scud_fio);")
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_mapping_scud ON person_identity_mapping(scud_fio);")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_raw_events_date_hoz ON scud_events_raw(log_date, hoz_organ);")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_raw_events_date_time ON scud_events_raw(log_date, time_val);")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
+1
-1
@@ -101,7 +101,7 @@ def print_stats():
|
|||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
tables = [
|
tables = [
|
||||||
'scud_logs', 'zup_staff', 'zup_absences', 'anomalies_history',
|
'scud_logs', 'scud_events_raw', 'zup_staff', 'zup_absences', 'anomalies_history',
|
||||||
'ai_knowledge_base', 'chat_messages', 'session_states',
|
'ai_knowledge_base', 'chat_messages', 'session_states',
|
||||||
'system_prompt_nodes', 'tasks', 'exceptions_registry'
|
'system_prompt_nodes', 'tasks', 'exceptions_registry'
|
||||||
]
|
]
|
||||||
|
|||||||
+57
-13
@@ -24,6 +24,7 @@ import xlsxwriter
|
|||||||
|
|
||||||
from config import SCUD_DIR, clean_scud_fio_light, load_exceptions
|
from config import SCUD_DIR, clean_scud_fio_light, load_exceptions
|
||||||
from core.database import save_scud_to_db, has_scud_logs_for_date, has_yesterday_final_snapshot
|
from core.database import save_scud_to_db, has_scud_logs_for_date, has_yesterday_final_snapshot
|
||||||
|
from core.repositories.scud_repo import save_raw_events_to_db
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy connectable")
|
warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy connectable")
|
||||||
|
|
||||||
@@ -192,6 +193,41 @@ WHERE
|
|||||||
ORDER BY p.Name ASC;
|
ORDER BY p.Name ASC;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
SQL_RAW_EVENTS_QUERY = r"""
|
||||||
|
DECLARE @InputDate DATE = '{target_date}';
|
||||||
|
DECLARE @StartDate DATETIME = CAST(@InputDate AS DATETIME);
|
||||||
|
DECLARE @EndDate DATETIME = DATEADD(SECOND, -1, DATEADD(DAY, 1, @StartDate));
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
log.TimeVal,
|
||||||
|
log.HozOrgan,
|
||||||
|
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
|
||||||
|
)) AS [Сотрудник],
|
||||||
|
ISNULL(CAST(div.Name AS NVARCHAR(255)), N'Без подразделения') AS [Подразделение],
|
||||||
|
log.Event,
|
||||||
|
log.Mode,
|
||||||
|
CASE
|
||||||
|
WHEN log.Mode = 2 THEN 'OUT'
|
||||||
|
WHEN log.Mode = 1 THEN 'IN'
|
||||||
|
WHEN log.Event IN (2, 27, 29, 33, 55, 65) THEN 'OUT'
|
||||||
|
WHEN log.Event IN (1, 21, 26, 54, 64) THEN 'IN'
|
||||||
|
ELSE 'OTHER'
|
||||||
|
END AS Direction
|
||||||
|
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
|
||||||
|
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
|
||||||
|
ORDER BY log.TimeVal ASC;
|
||||||
|
"""
|
||||||
|
|
||||||
def save_df_to_clean_excel(df: pd.DataFrame, file_path: str, sheet_name: str = "Отчет"):
|
def save_df_to_clean_excel(df: pd.DataFrame, file_path: str, sheet_name: str = "Отчет"):
|
||||||
workbook = xlsxwriter.Workbook(file_path, {'constant_memory': False})
|
workbook = xlsxwriter.Workbook(file_path, {'constant_memory': False})
|
||||||
@@ -318,22 +354,30 @@ def run_export(input_date: str | None = None, save_xlsx: bool = True, debug: boo
|
|||||||
df['Пришел'] = df['Статус'].str.contains('Присутствовал', case=False, na=False) & (~mask_anomaly)
|
df['Пришел'] = df['Статус'].str.contains('Присутствовал', case=False, na=False) & (~mask_anomaly)
|
||||||
|
|
||||||
save_scud_to_db(df, processing_date_str, snapshot_time=snapshot_time, is_yesterday=is_yesterday)
|
save_scud_to_db(df, processing_date_str, snapshot_time=snapshot_time, is_yesterday=is_yesterday)
|
||||||
log(f"[✓] Записи за {processing_date_str} успешно сохранены в SQLite!", "SUCCESS")
|
|
||||||
|
|
||||||
if save_xlsx:
|
raw_sql = SQL_RAW_EVENTS_QUERY.format(target_date=processing_date.strftime("%Y-%m-%d"))
|
||||||
file_name = f"Сотрудники_{processing_date_str}.xlsx"
|
df_raw = pd.read_sql(raw_sql, connection)
|
||||||
file_path = os.path.join(SCUD_DIR, file_name)
|
if len(df_raw) > 0:
|
||||||
|
df_raw['fio_clean'] = df_raw['Сотрудник'].apply(clean_scud_fio_light)
|
||||||
|
inserted_count = save_raw_events_to_db(df_raw, processing_date_str)
|
||||||
|
log(f"[✓] В scud_events_raw сохранено {inserted_count} сырых событий проходов за {processing_date_str}!", "SUCCESS")
|
||||||
|
|
||||||
if os.path.exists(file_path):
|
log(f"[✓] Записи за {processing_date_str} успешно сохранены в SQLite!", "SUCCESS")
|
||||||
try:
|
|
||||||
os.remove(file_path)
|
|
||||||
except OSError as e:
|
|
||||||
log(f"ОШИБКА при удалении старого файла {file_name}: {e}", "ERROR")
|
|
||||||
|
|
||||||
save_df_to_clean_excel(df, file_path, sheet_name="Отчет")
|
if save_xlsx:
|
||||||
log(f"[✓] Успешно экспортирован файл: data/scud/{file_name}", "SUCCESS")
|
file_name = f"Сотрудники_{processing_date_str}.xlsx"
|
||||||
else:
|
file_path = os.path.join(SCUD_DIR, file_name)
|
||||||
log(f"Запрос за {processing_date_str} вернул 0 строк.", "WARNING")
|
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
except OSError as e:
|
||||||
|
log(f"ОШИБКА при удалении старого файла {file_name}: {e}", "ERROR")
|
||||||
|
|
||||||
|
save_df_to_clean_excel(df, file_path, sheet_name="Отчет")
|
||||||
|
log(f"[✓] Успешно экспортирован файл: data/scud/{file_name}", "SUCCESS")
|
||||||
|
else:
|
||||||
|
log(f"Запрос за {processing_date_str} вернул 0 строк.", "WARNING")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"🛑 ОШИБКА выгрузки СКУД за {processing_date_str}: {e}", "ERROR")
|
log(f"🛑 ОШИБКА выгрузки СКУД за {processing_date_str}: {e}", "ERROR")
|
||||||
|
|||||||
Reference in New Issue
Block a user