219 lines
11 KiB
Python
219 lines
11 KiB
Python
"""
|
||
===============================================================================
|
||
FILE: services/reports/svodka_builder.py
|
||
ROLE: Генератор книги Ежедневной сводки (иерархические группировки XlsxWriter).
|
||
===============================================================================
|
||
"""
|
||
|
||
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
|
||
|
||
|
||
def generate_summary_excel(merged_df, date_str="21.08.2026", filename=None):
|
||
date_clean = str(date_str).replace('_', '.')
|
||
if not filename:
|
||
filename = f"{format_date_ru(date_clean)} сводка.xlsx"
|
||
|
||
target_dir = get_dated_reports_dir(date_clean)
|
||
output_path = os.path.join(target_dir, filename)
|
||
|
||
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)
|
||
|
||
fmt_hdr_l = create_xlsx_format(wb, bg_color='#D9E1F2', bold=True, align="left")
|
||
fmt_hdr_r = create_xlsx_format(wb, bg_color='#D9E1F2', bold=True, align="right")
|
||
fmt_tot_l = create_xlsx_format(wb, bg_color='#F2F2F2', bold=True, align="left")
|
||
fmt_tot_r = create_xlsx_format(wb, bg_color='#F2F2F2', bold=True, align="right")
|
||
fmt_empty = create_xlsx_format(wb)
|
||
|
||
ws.set_row(0, 20)
|
||
ws.write(0, 0, "Сводка на", fmt_hdr_l)
|
||
ws.write(0, 1, date_clean, fmt_hdr_r)
|
||
|
||
ws.set_row(1, 20)
|
||
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, staff_total, fmt_tot_r)
|
||
|
||
current_row = 3
|
||
|
||
# 1. Неизвестно
|
||
unexplained = merged_df[
|
||
(~is_not_hired) &
|
||
(merged_df['Пришел'] == False) &
|
||
(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")
|
||
fmt_unexp_hr = create_xlsx_format(wb, bg_color='#FCE4D6', bold=True, align="right")
|
||
fmt_unexp_rl = create_xlsx_format(wb, bg_color='#FCE4D6', bold=False, align="left")
|
||
fmt_unexp_rr = create_xlsx_format(wb, bg_color='#FCE4D6', bold=False, align="right")
|
||
|
||
ws.set_row(current_row, 20)
|
||
ws.write(current_row, 0, "неизвестно", fmt_unexp_hl)
|
||
ws.write(current_row, 1, len(unexplained), fmt_unexp_hr)
|
||
current_row += 1
|
||
|
||
for fio in sorted(unexplained['Сотрудник'].dropna().unique()):
|
||
ws.set_row(current_row, 20, None, {'level': 1, 'hidden': False})
|
||
ws.write(current_row, 0, str(fio), fmt_unexp_rl)
|
||
ws.write(current_row, 1, "", fmt_unexp_rr)
|
||
current_row += 1
|
||
|
||
# 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, 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, str(fio), fmt_np_rl)
|
||
ws.write(current_row, 1, "", fmt_np_rr)
|
||
current_row += 1
|
||
|
||
# 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('Исключение')) &
|
||
(~is_remote_reason)
|
||
]
|
||
absent_groups = absent_only.groupby('Вид_отсутствия')
|
||
pastels = ['#FFF2CC', '#E1D5E7', '#E1F5FE', '#FFF0F5', '#FCF3CF']
|
||
|
||
for idx_cat, (cat_name, group) in enumerate(absent_groups):
|
||
hex_c = pastels[idx_cat % len(pastels)]
|
||
fmt_cat_hl = create_xlsx_format(wb, bg_color=hex_c, bold=True, align="left")
|
||
fmt_cat_hr = create_xlsx_format(wb, bg_color=hex_c, bold=True, align="right")
|
||
fmt_cat_rl = create_xlsx_format(wb, bg_color=hex_c, bold=False, align="left")
|
||
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, str(cat_name), fmt_cat_hl)
|
||
ws.write(current_row, 1, len(group), fmt_cat_hr)
|
||
current_row += 1
|
||
|
||
is_other_category = (str(cat_name).strip().lower() == "иное")
|
||
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, str(fio), fmt_cat_rl)
|
||
ws.write(current_row, 1, str(detail_val), fmt_cat_rr)
|
||
current_row += 1
|
||
|
||
# 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")
|
||
fmt_pres_hr = create_xlsx_format(wb, bg_color='#E2EFDA', bold=True, align="right")
|
||
|
||
ws.set_row(current_row, 20)
|
||
ws.write(current_row, 0, "Итого на работе", fmt_pres_hl)
|
||
ws.write(current_row, 1, total_present_count, fmt_pres_hr)
|
||
current_row += 1
|
||
|
||
# 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")
|
||
fmt_rem_rr = create_xlsx_format(wb, bg_color='#E8F8F5', bold=False, align="right")
|
||
|
||
ws.set_row(current_row, 20)
|
||
ws.write(current_row, 0, "В том числе на удаленной работе", fmt_rem_hl)
|
||
ws.write(current_row, 1, len(remote_home), fmt_rem_hr)
|
||
current_row += 1
|
||
|
||
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, str(fio), fmt_rem_rl)
|
||
ws.write(current_row, 1, "", fmt_rem_rr)
|
||
current_row += 1
|
||
|
||
# 7. Аномалии СКУД и 1С
|
||
anomalies = merged_df[
|
||
(~is_not_hired) & (~is_exc) & (
|
||
((merged_df['Пришел'] == True) & (merged_df['Вид_отсутствия'].notna()) &
|
||
(~merged_df['Вид_отсутствия'].astype(str).str.startswith('Исключение')) &
|
||
(~is_remote_reason) &
|
||
(~reason_clean.str.contains('командировк', na=False))) |
|
||
(merged_df.get('anomaly_flag', 'NONE') == 'ANOMALY_NO_IN_HAS_ACTIVITY')
|
||
)
|
||
]
|
||
fmt_anom_hl = create_xlsx_format(wb, bg_color='#FCE4D6', bold=True, align="left")
|
||
fmt_anom_hr = create_xlsx_format(wb, bg_color='#FCE4D6', bold=True, align="right")
|
||
fmt_anom_rl = create_xlsx_format(wb, bg_color='#FCE4D6', bold=False, align="left")
|
||
fmt_anom_rr = create_xlsx_format(wb, bg_color='#FCE4D6', bold=False, align="left", wrap=True)
|
||
|
||
ws.set_row(current_row, 20)
|
||
ws.write(current_row, 0, "Аномалии СКУД и 1С", fmt_anom_hl)
|
||
ws.write(current_row, 1, len(anomalies), fmt_anom_hr)
|
||
current_row += 1
|
||
|
||
chars_per_line_b = 30
|
||
if not anomalies.empty:
|
||
for _, row in anomalies.iterrows():
|
||
fio = row.get('Сотрудник', '')
|
||
anom_flag = row.get('anomaly_flag', 'NONE')
|
||
reason_text = f"🚨 АНОМАЛИЯ СКУД: Нет входа (первая активность: {row.get('Первая_активность', '—')})" if anom_flag == 'ANOMALY_NO_IN_HAS_ACTIVITY' else f"В 1С: {row.get('Вид_отсутствия', '')}"
|
||
|
||
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, 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)
|
||
ws.set_column(1, 1, 38)
|
||
safe_close_workbook(wb, output_path, target_dir, filename) |