147 lines
5.9 KiB
Python
147 lines
5.9 KiB
Python
"""
|
||
===============================================================================
|
||
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 |