Files
scud_orion_ai_v2/modules/zup_extractor.py
T

161 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import logging
from datetime import date
import pyodbc
import pandas as pd
from config import DATA_DIR, normalize_fio
logger = logging.getLogger(__name__)
# Конфигурация MS SQL Server 1С:ЗУП 3.1
ZUP_SQL_CONFIG = {
"driver": "{ODBC Driver 18 for SQL Server}",
"server": os.getenv("ZUP_MS_SQL_SERVER", "192.168.1.10"), # Укажите IP MS SQL 1С
"database": os.getenv("ZUP_MS_SQL_DB", "zup_db"),
"user": os.getenv("ZUP_MS_SQL_USER", "sa"),
"password": os.getenv("ZUP_MS_SQL_PASS", "your_password"),
}
def get_zup_connection_string() -> str:
"""Формирует строку подключения pyodbc к MS SQL Server."""
return (
f"DRIVER={ZUP_SQL_CONFIG['driver']};"
f"SERVER={ZUP_SQL_CONFIG['server']};"
f"DATABASE={ZUP_SQL_CONFIG['database']};"
f"UID={ZUP_SQL_CONFIG['user']};"
f"PWD={ZUP_SQL_CONFIG['password']};"
f"TrustServerCertificate=yes;"
f"Encrypt=no;"
)
def fetch_zup_absences_from_sql(target_date: date) -> pd.DataFrame:
"""
Извлекает оперативные отсутствия и действующие декреты из MS SQL 1С:ЗУП 3.1
на указанную дату.
"""
query = """
DECLARE @TargetDate DATE = ?;
-- 1. Оперативные отсутствия (Отпуска, Командировки, Больничные, Отгулы)
SELECT
LTRIM(RTRIM(ref_emp._Description)) AS [ФИО],
CASE state._Fld16925RRef
WHEN 0x9C10B2452D414FDF4A90E2B2AB81D3F7 THEN N'Отпуск основной'
WHEN 0xBA63FCF94B4AD0664ED369D2E6505D67 THEN N'Командировка'
WHEN 0x8C3B61F23954155A40EB0108FC0932DB THEN N'Болезнь'
WHEN 0x853001C18D0965EE4B2702405C94054A THEN N'Отпуск неоплачиваемый по разрешению работодателя'
WHEN 0xB7335AEFD8708C3E462861FC59489A38 THEN N'Отпуск по беременности и родам'
ELSE N'Другое отсутствие'
END AS [Вид_отсутствия]
FROM dbo._InfoRg16921 state WITH (NOLOCK)
INNER JOIN dbo._Reference299 ref_emp WITH (NOLOCK)
ON state._Fld16922RRef = ref_emp._IDRRef
WHERE @TargetDate BETWEEN CAST(CASE WHEN YEAR(state._Fld16926) > 3000 THEN DATEADD(YEAR, -2000, state._Fld16926) ELSE state._Fld16926 END AS DATE)
AND CAST(CASE WHEN YEAR(state._Fld16927) > 3000 THEN DATEADD(YEAR, -2000, state._Fld16927) ELSE state._Fld16927 END AS DATE)
UNION ALL
-- 2. Динамический выбор ДЕЙСТВУЮЩИХ декретниц по уходу за ребенком
SELECT
active_state.fio AS [ФИО],
N'Отпуск по уходу за ребенком' AS [Вид_отсутствия]
FROM (
SELECT
LTRIM(RTRIM(ref_emp._Description)) AS fio,
all_states._Fld16925RRef AS state_guid,
CAST(CASE WHEN YEAR(all_states._Fld16926) > 3000 THEN DATEADD(YEAR, -2000, all_states._Fld16926) ELSE all_states._Fld16926 END AS DATE) AS date_start,
ROW_NUMBER() OVER (
PARTITION BY all_states._Fld16922RRef
ORDER BY all_states._Fld16926 DESC
) AS rn
FROM dbo._InfoRg16921 all_states WITH (NOLOCK)
INNER JOIN dbo._Reference299 ref_emp WITH (NOLOCK)
ON all_states._Fld16922RRef = ref_emp._IDRRef
WHERE CAST(CASE WHEN YEAR(all_states._Fld16926) > 3000 THEN DATEADD(YEAR, -2000, all_states._Fld16926) ELSE all_states._Fld16926 END AS DATE) <= @TargetDate
) active_state
WHERE active_state.rn = 1
AND active_state.state_guid = 0xA4FBA038663B3C2A48DA151C262855E1
AND active_state.date_start >= DATEADD(YEAR, -3, @TargetDate)
ORDER BY [ФИО] ASC;
"""
conn_str = get_zup_connection_string()
with pyodbc.connect(conn_str) as conn:
df = pd.read_sql(query, conn, params=[target_date])
if not df.empty:
df['fio_clean'] = df['ФИО'].apply(normalize_fio)
return df
def fetch_zup_staff_from_sql() -> pd.DataFrame:
"""Извлекает весь актуальный штат из MS SQL 1С:ЗУП."""
query = """
SELECT DISTINCT
LTRIM(RTRIM(ref_emp._Description)) AS [ФИО],
N'Организация' AS [Подразделение],
N'Сотрудник' AS [Должность]
FROM dbo._Reference299 ref_emp WITH (NOLOCK)
WHERE ref_emp._Description <> ''
AND ref_emp._Marked = 0x00
ORDER BY [ФИО] ASC;
"""
conn_str = get_zup_connection_string()
with pyodbc.connect(conn_str) as conn:
df = pd.read_sql(query, conn)
if not df.empty:
df['fio_clean'] = df['ФИО'].apply(normalize_fio)
return df
def sync_zup_to_excel(target_date: date) -> bool:
"""
Извлекает данные из MS SQL и автоматически создает локальные Excel-файлы 1С:
data/1c/Штат_DD_MM_YYYY.xlsx
data/1c/Отсутствия_DD_MM_YYYY.xlsx
"""
try:
date_str_file = target_date.strftime("%d_%m_%Y")
c_1c_dir = os.path.join(DATA_DIR, "1c")
os.makedirs(c_1c_dir, exist_ok=True)
df_staff = fetch_zup_staff_from_sql()
df_absences = fetch_zup_absences_from_sql(target_date)
logger.info(f"[1С Direct SQL] Выгрузка за {target_date.strftime('%d.%m.%Y')}: Штат ({len(df_staff)} чел.), Отсутствия ({len(df_absences)} чел.)")
# Сохранение в формате, который ожидает data_loader.py
staff_excel_path = os.path.join(c_1c_dir, f"Штат_{date_str_file}.xlsx")
absences_excel_path = os.path.join(c_1c_dir, f"Отсутствия_{date_str_file}.xlsx")
# Формируем структуру Excel с нужным количеством пропущенных строк (skiprows)
with pd.ExcelWriter(staff_excel_path, engine='openpyxl') as writer:
# Пустой заголовок (8 строк) для эмуляции отчета 1С
dummy_headers = pd.DataFrame([[""] * 13] * 8)
dummy_headers.to_excel(writer, index=False, header=False)
# Данные
df_staff[['ФИО', 'Подразделение', 'Должность']].to_excel(writer, startrow=8, index=False)
with pd.ExcelWriter(absences_excel_path, engine='openpyxl') as writer:
# Пустой заголовок (3 строки) для эмуляции отчета 1С
dummy_headers = pd.DataFrame([[""] * 3] * 3)
dummy_headers.to_excel(writer, index=False, header=False)
# Данные
df_absences[['ФИО', 'Вид_отсутствия']].to_excel(writer, startrow=3, index=False)
logger.info(f"[✓] Успешно созданы файлы 1С за {target_date.strftime('%d.%m.%Y')}: {staff_excel_path}, {absences_excel_path}")
return True
except Exception as e:
logger.error(f"[❌] Ошибка прямого импорта из MS SQL 1С: {e}")
return False