""" =============================================================================== FILE: services/reports/simplified_builder.py ROLE: Генератор книги "Упрощенный отчет за ДД.ММ.ГГГГг..xlsx" на основе агрегированных данных СКУД и 1С. =============================================================================== """ import os import openpyxl from openpyxl.styles import Font, Alignment, Border, Side from datetime import datetime, time import pandas as pd from config import REPORTS_DIR from services.reports.styles import get_dated_reports_dir def format_fio_initials(full_fio: str) -> str: """Преобразует 'Иванов Иван Иванович' в 'Иванов И.И.'""" if not full_fio or pd.isna(full_fio): return "" parts = str(full_fio).strip().split() if len(parts) >= 3: return f"{parts[0]} {parts[1][0]}.{parts[2][0]}." elif len(parts) == 2: return f"{parts[0]} {parts[1][0]}." return str(full_fio).strip() def parse_time_cell(val, default_empty="Нет входа (0:00)"): """Преобразует строку времени СКУД в time(hh, mm) либо оставляет текстовую заглушку.""" if not val or pd.isna(val): return default_empty s = str(val).strip() if s in ["Нет входа", "—", "", "nan", "None", "00:00:00", "00:00"]: return default_empty if s in ["Нет выхода"]: return "Нет выхода (23:59)" # Если уже пришёл time if isinstance(val, time): return val try: parts = s.split(":") h = int(parts[0]) m = int(parts[1]) if len(parts) > 1 else 0 return time(h, m) except Exception: return s def generate_simplified_excel(df_merged: pd.DataFrame, date_str: str, filename: str = None) -> str: """ Генерирует Excel-файл упрощенного отчета точно по образцу Ориона. Структура: A: № | B: Подразделение | C: Сотрудник | D: Должность | E: Начало дня | F: Конец дня """ clean_date = str(date_str).replace('_', '.') if not filename: filename = f"Упрощенный отчет за {clean_date}г..xlsx" target_dir = get_dated_reports_dir(clean_date) output_path = os.path.join(target_dir, filename) # Исключаем тех, кто не принят на работу и исключения is_exc = df_merged.get('is_excluded', False) == True is_not_hired = df_merged.get('not_hired_yet', False) == True df_src = df_merged[(~is_exc) & (~is_not_hired)].copy() # Подготовка данных rows_to_sort = [] for _, r in df_src.iterrows(): fio_raw = r.get('Сотрудник', r.get('ФИО', '')) dept = str(r.get('Подразделение', '—')).strip() pos = str(r.get('Должность', '—')).strip() if pos.lower() in ['nan', 'none', '']: pos = '—' if dept.lower() in ['nan', 'none', '']: dept = '—' fio_short = format_fio_initials(fio_raw) t_in = parse_time_cell(r.get('Начало_дня'), default_empty="Нет входа (0:00)") t_out = parse_time_cell(r.get('Конец_дня'), default_empty="Нет выхода (23:59)") rows_to_sort.append({ 'dept': dept, 'fio_short': fio_short, 'pos': pos, 't_in': t_in, 't_out': t_out }) # Сортировка: Подразделение (А-Я), затем Сотрудник (А-Я) rows_sorted = sorted(rows_to_sort, key=lambda x: (x['dept'].lower(), x['fio_short'].lower())) # Создание книги openpyxl для точного соблюдения структуры образца wb = openpyxl.Workbook() ws = wb.active ws.title = "Лист1" # Стили по образцу font_main = Font(name="Calibri", size=11, bold=False) align_center = Alignment(horizontal="center", vertical="center") border_thin = Border( left=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin") ) # Заголовок ws["B1"] = f"Упрощенный отчет: c {clean_date} по {clean_date}" ws["B1"].font = font_main # Шапка таблицы (Строка 3) headers = [None, "Подразделение", "Сотрудник", "Должность", "Начало дня", "Конец дня"] for col_idx, h_text in enumerate(headers, start=1): cell = ws.cell(row=3, column=col_idx, value=h_text) cell.font = font_main cell.alignment = align_center cell.border = border_thin # Заполнение строк данных (с 4 строки) for idx, item in enumerate(rows_sorted, start=1): row_num = 3 + idx vals = [idx, item['dept'], item['fio_short'], item['pos'], item['t_in'], item['t_out']] for col_idx, val in enumerate(vals, start=1): cell = ws.cell(row=row_num, column=col_idx, value=val) cell.font = font_main cell.alignment = align_center cell.border = border_thin # Числовой формат времени h:mm if isinstance(val, time): cell.number_format = "h:mm" # Настройка ширины колонок по образцу widths = {'A': 13.0, 'B': 25.0, 'C': 27.0, 'D': 80.0, 'E': 21.5, 'F': 23.3} for col_letter, w in widths.items(): ws.column_dimensions[col_letter].width = w # Создание пустых Лист2 и Лист3 как в оригинальном шаблоне wb.create_sheet("Лист2") wb.create_sheet("Лист3") wb.save(output_path) return output_path