feat(reports): add simplified report builder, fix nan/inf crash and separate unhired/no-pass staff

This commit is contained in:
2026-09-24 09:25:57 +03:00
parent 9c89b6592e
commit 30651672ab
10 changed files with 437 additions and 89 deletions
+52 -24
View File
@@ -20,7 +20,7 @@ def generate_summary_excel(merged_df, date_str="21.08.2026", filename=None):
target_dir = get_dated_reports_dir(date_clean)
output_path = os.path.join(target_dir, filename)
wb = xlsxwriter.Workbook(output_path)
wb = xlsxwriter.Workbook(output_path, {'nan_inf_to_errors': True})
ws = wb.add_worksheet("Лист_1")
ws.outline_settings(visible=True, symbols_below=False, symbols_right=False, auto_style=False)
@@ -38,18 +38,24 @@ def generate_summary_excel(merged_df, date_str="21.08.2026", filename=None):
ws.write(1, 0, "", fmt_empty)
ws.write(1, 1, "", fmt_empty)
is_not_hired = merged_df.get('not_hired_yet', False) == True
is_no_pass = merged_df.get('no_scud_pass', False) == True
is_exc = merged_df.get('is_excluded', False) == True
# "По списку" строго по официальному штату 1С
staff_total = len(merged_df[~is_not_hired])
ws.set_row(2, 20)
ws.write(2, 0, "По списку", fmt_tot_l)
ws.write(2, 1, len(merged_df), fmt_tot_r)
ws.write(2, 1, staff_total, fmt_tot_r)
current_row = 3
is_no_pass = merged_df['no_scud_pass'] == True if 'no_scud_pass' in merged_df.columns else False
is_exc = merged_df.get('is_excluded', False) == True
# 1. Неизвестно
unexplained = merged_df[
(~is_not_hired) &
(merged_df['Пришел'] == False) &
(merged_df['Вид_отсутствия'].isna() | (merged_df['Вид_отсутствия'].astype(str).str.strip() == '')) &
(merged_df['Вид_отсутствия'].isna() | (merged_df['Вид_отсутствия'].astype(str).str.strip().isin(['', 'nan', 'None']))) &
(~is_no_pass) & (~is_exc)
]
fmt_unexp_hl = create_xlsx_format(wb, bg_color='#FCE4D6', bold=True, align="left")
@@ -64,34 +70,54 @@ def generate_summary_excel(merged_df, date_str="21.08.2026", filename=None):
for fio in sorted(unexplained['Сотрудник'].dropna().unique()):
ws.set_row(current_row, 20, None, {'level': 1, 'hidden': False})
ws.write(current_row, 0, fio, fmt_unexp_rl)
ws.write(current_row, 0, str(fio), fmt_unexp_rl)
ws.write(current_row, 1, "", fmt_unexp_rr)
current_row += 1
# 2. Нет пропуска
no_pass_df = merged_df[is_no_pass & (~is_exc)] if 'no_scud_pass' in merged_df.columns else pd.DataFrame()
# 2. Нет пропуска (в штате 1С есть, но карты СКУД нет)
no_pass_df = merged_df[is_no_pass & (~is_exc) & (~is_not_hired)]
fmt_np_hl = create_xlsx_format(wb, bg_color='#E1F5FE', bold=True, align="left")
fmt_np_hr = create_xlsx_format(wb, bg_color='#E1F5FE', bold=True, align="right")
fmt_np_rl = create_xlsx_format(wb, bg_color='#E1F5FE', bold=False, align="left")
fmt_np_rr = create_xlsx_format(wb, bg_color='#E1F5FE', bold=False, align="right")
ws.set_row(current_row, 20)
ws.write(current_row, 0, "Нет пропуска", fmt_np_hl)
ws.write(current_row, 0, "Нет в СКУД", fmt_np_hl)
ws.write(current_row, 1, len(no_pass_df), fmt_np_hr)
current_row += 1
if not no_pass_df.empty:
for fio in sorted(no_pass_df['Сотрудник'].dropna().unique()):
ws.set_row(current_row, 20, None, {'level': 1, 'hidden': False})
ws.write(current_row, 0, fio, fmt_np_rl)
ws.write(current_row, 0, str(fio), fmt_np_rl)
ws.write(current_row, 1, "", fmt_np_rr)
current_row += 1
# 3. Официальные отсутствия
# 3. Не приняты на работу (в СКУД есть, но в 1С приказа еще нет)
not_hired_df = merged_df[is_not_hired & (~is_exc)]
fmt_nh_hl = create_xlsx_format(wb, bg_color='#FFF0F5', bold=True, align="left")
fmt_nh_hr = create_xlsx_format(wb, bg_color='#FFF0F5', bold=True, align="right")
fmt_nh_rl = create_xlsx_format(wb, bg_color='#FFF0F5', bold=False, align="left")
fmt_nh_rr = create_xlsx_format(wb, bg_color='#FFF0F5', bold=False, align="right")
ws.set_row(current_row, 20)
ws.write(current_row, 0, "Нет в ЗУП", fmt_nh_hl)
ws.write(current_row, 1, len(not_hired_df), fmt_nh_hr)
current_row += 1
if not not_hired_df.empty:
for fio in sorted(not_hired_df['Сотрудник'].dropna().unique()):
ws.set_row(current_row, 20, None, {'level': 1, 'hidden': False})
ws.write(current_row, 0, str(fio), fmt_nh_rl)
ws.write(current_row, 1, "", fmt_nh_rr)
current_row += 1
# 4. Официальные отсутствия
reason_clean = merged_df['Вид_отсутствия'].astype(str).str.lower()
is_remote_reason = reason_clean.str.contains('удален|дистанцион', regex=True, na=False)
absent_only = merged_df[
(~is_not_hired) &
(merged_df['Пришел'] == False) &
(merged_df['Вид_отсутствия'].notna()) &
(~merged_df['Вид_отсутствия'].astype(str).str.startswith('Исключение')) &
@@ -108,7 +134,7 @@ def generate_summary_excel(merged_df, date_str="21.08.2026", filename=None):
fmt_cat_rr = create_xlsx_format(wb, bg_color=hex_c, bold=False, align="right")
ws.set_row(current_row, 20)
ws.write(current_row, 0, cat_name, fmt_cat_hl)
ws.write(current_row, 0, str(cat_name), fmt_cat_hl)
ws.write(current_row, 1, len(group), fmt_cat_hr)
current_row += 1
@@ -116,14 +142,16 @@ def generate_summary_excel(merged_df, date_str="21.08.2026", filename=None):
for _, row in group.sort_values(by='Сотрудник').iterrows():
fio = row.get('Сотрудник', '')
detail_val = row.get('detailed_reason', row.get('comment', '')) if is_other_category else ""
if pd.isna(detail_val):
detail_val = ""
ws.set_row(current_row, 20, None, {'level': 1, 'hidden': True, 'collapsed': True})
ws.write(current_row, 0, fio, fmt_cat_rl)
ws.write(current_row, 1, detail_val, fmt_cat_rr)
ws.write(current_row, 0, str(fio), fmt_cat_rl)
ws.write(current_row, 1, str(detail_val), fmt_cat_rr)
current_row += 1
# 4. Итого на работе
exc_without_doc = merged_df[is_exc & (merged_df['Вид_отсутствия'].isna() | (merged_df['Вид_отсутствия'].astype(str).str.strip().isin(['', 'nan', 'Исключение'])))]
present_scud = merged_df[(merged_df['Пришел'] == True) & (~is_exc)]
# 5. Итого на работе (только принятые)
exc_without_doc = merged_df[(~is_not_hired) & is_exc & (merged_df['Вид_отсутствия'].isna() | (merged_df['Вид_отсутствия'].astype(str).str.strip().isin(['', 'nan', 'Исключение'])))]
present_scud = merged_df[(~is_not_hired) & (merged_df['Пришел'] == True) & (~is_exc)]
total_present_count = len(present_scud) + len(exc_without_doc)
fmt_pres_hl = create_xlsx_format(wb, bg_color='#E2EFDA', bold=True, align="left")
@@ -134,8 +162,8 @@ def generate_summary_excel(merged_df, date_str="21.08.2026", filename=None):
ws.write(current_row, 1, total_present_count, fmt_pres_hr)
current_row += 1
# 5. Удаленная работа
remote_home = merged_df[(merged_df['Пришел'] == False) & is_remote_reason & (~is_exc)]
# 6. Удаленная работа
remote_home = merged_df[(~is_not_hired) & (merged_df['Пришел'] == False) & is_remote_reason & (~is_exc)]
fmt_rem_hl = create_xlsx_format(wb, bg_color='#E8F8F5', bold=True, align="left")
fmt_rem_hr = create_xlsx_format(wb, bg_color='#E8F8F5', bold=True, align="right")
fmt_rem_rl = create_xlsx_format(wb, bg_color='#E8F8F5', bold=False, align="left")
@@ -149,13 +177,13 @@ def generate_summary_excel(merged_df, date_str="21.08.2026", filename=None):
if not remote_home.empty:
for fio in sorted(remote_home['Сотрудник'].dropna().unique()):
ws.set_row(current_row, 20, None, {'level': 1, 'hidden': True, 'collapsed': True})
ws.write(current_row, 0, fio, fmt_rem_rl)
ws.write(current_row, 0, str(fio), fmt_rem_rl)
ws.write(current_row, 1, "", fmt_rem_rr)
current_row += 1
# 6. Аномалии СКУД и 1С
# 7. Аномалии СКУД и 1С
anomalies = merged_df[
(~is_exc) & (
(~is_not_hired) & (~is_exc) & (
((merged_df['Пришел'] == True) & (merged_df['Вид_отсутствия'].notna()) &
(~merged_df['Вид_отсутствия'].astype(str).str.startswith('Исключение')) &
(~is_remote_reason) &
@@ -182,8 +210,8 @@ def generate_summary_excel(merged_df, date_str="21.08.2026", filename=None):
lines_count = math.ceil(len(reason_text) / chars_per_line_b) if len(reason_text) > chars_per_line_b else 1
ws.set_row(current_row, max(lines_count * 18, 20), None, {'level': 1, 'hidden': True, 'collapsed': True})
ws.write(current_row, 0, fio, fmt_anom_rl)
ws.write(current_row, 1, reason_text, fmt_anom_rr)
ws.write(current_row, 0, str(fio), fmt_anom_rl)
ws.write(current_row, 1, str(reason_text), fmt_anom_rr)
current_row += 1
ws.set_column(0, 0, 45)