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