""" =============================================================================== FILE: services/reports/otchet_builder.py ROLE: Генератор книги Детального суточного отчета со сверкой 1С:ЗУП. =============================================================================== """ import os import math import pandas as pd import xlsxwriter from services.reports.styles import get_dated_reports_dir, format_date_ru, safe_close_workbook, create_xlsx_format from services.reports.calculators import calculate_deviation def generate_detailed_excel(merged_df, date_str="20.08.2026", filename=None): date_clean = str(date_str).replace('_', '.') if not filename: filename = f"{format_date_ru(date_clean)} отчет.xlsx" df_export = merged_df[merged_df.get('is_excluded', False) == False].copy() if merged_df is not None and not merged_df.empty else pd.DataFrame() target_dir = get_dated_reports_dir(date_clean) output_path = os.path.join(target_dir, filename) wb = xlsxwriter.Workbook(output_path) ws = wb.add_worksheet("Детальный_отчет") fmt_date_lbl = wb.add_format({'font_name': 'Arial', 'font_size': 10, 'bold': True}) ws.write(1, 1, "Дата:", fmt_date_lbl) ws.write(1, 3, date_clean, fmt_date_lbl) headers = [ "№", "ФИО", "Подразделение", "время входа", "первая активность", "время выхода", "находился в здании", "причина отсутствия", "норма", "отклонение от нормы" ] fmt_hdr = create_xlsx_format(wb, font_name='Arial', font_size=10, bg_color='#D9E1F2', bold=True, align="center", wrap=True) ws.set_row(3, 26) for col_idx, h_text in enumerate(headers): ws.write(3, col_idx, h_text, fmt_hdr) start_col = 'Начало дня' if 'Начало дня' in df_export.columns else 'Начало_дня' end_col = 'Конец дня' if 'Конец дня' in df_export.columns else 'Конец_дня' hours_col = 'Часы' if 'Часы' in df_export.columns else 'Находился_в_здании' chars_per_line_h = 24 for idx, row in df_export.reset_index(drop=True).iterrows(): row_num = 4 + idx is_present = row.get('Пришел', False) absence_reason = row.get('Вид_отсутствия', '') has_reason = pd.notna(absence_reason) and str(absence_reason).strip() != '' in_val = str(row.get(start_col, 'Нет входа')).strip() out_val = str(row.get(end_col, 'Нет выхода')).strip() in_building_str = str(row.get(hours_col, '00:00')) first_act_val = str(row.get('Первая_активность', '—')).strip() has_first_act = first_act_val not in ['—', '', 'None', 'nan'] deviation_val = calculate_deviation(in_building_str, reason=absence_reason if has_reason else "", norm_hours=8, lunch_minutes=30) dept_scud_val = row.get('department_scud', row.get('department', row.get('Подразделение', ''))) row_color = None if is_present and has_reason: row_color = '#E2EFDA' elif not is_present and has_reason: row_color = '#FFF2CC' elif not is_present and not has_reason and not has_first_act: row_color = '#FCE4D6' val_h_str = str(absence_reason) if has_reason else "" lines_count = math.ceil(len(val_h_str) / chars_per_line_h) if len(val_h_str) > chars_per_line_h else 1 ws.set_row(row_num, max(lines_count * 18, 20)) values = [ (idx + 1, 'center', False), (row.get('Сотрудник', ''), 'left', False), (dept_scud_val, 'center', False), (in_val, 'center', False), (first_act_val, 'center', False), (out_val, 'center', False), (in_building_str, 'center', False), (absence_reason if has_reason else '', 'left', True), (8, 'center', False), (deviation_val, 'center', False) ] for col_idx, (val, align_type, is_wrap) in enumerate(values): fmt = create_xlsx_format(wb, font_name='Arial', font_size=10, bg_color=row_color, align=align_type, wrap=is_wrap) ws.write(row_num, col_idx, val, fmt) col_widths = {0: 4, 1: 33, 2: 13, 3: 11, 4: 11, 5: 11, 6: 12, 7: 24, 8: 6, 9: 11} for col_idx, width in col_widths.items(): ws.set_column(col_idx, col_idx, width) safe_close_workbook(wb, output_path, target_dir, filename)