Загрузка данных
# Dozor/modules/excel/excel_aggregate_inter.py
import os
import re
from tkinter import filedialog, messagebox, ttk
import tkinter as tk
from pathlib import Path
from modules.excel.excel_aggregate_work import AggregateWorkController
from modules.utils.window_behavior import LevelTwoBehavior
from openpyxl import load_workbook
class ExpertAggregateInterface(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
behavior = LevelTwoBehavior()
behavior.setup_level_two(self.winfo_toplevel())
self.title("Агрегирование файлов")
# Уменьшили высоту на 5px для компенсации плотности элементов
self.geometry("470x265")
self.resizable(False, False)
self.controller = AggregateWorkController()
main_frame = ttk.Frame(self)
main_frame.pack(padx=10, pady=5, fill="both", expand=True, anchor="center")
# === Блок создания файла ===
file_frame = ttk.Frame(main_frame)
file_frame.pack(fill="x", pady=(0, 10))
ttk.Label(file_frame, text="Название создаваемого файла:").pack(side="left")
self.new_file_name_entry = tk.Entry(file_frame, width=20)
self.new_file_name_entry.pack(side="left", padx=(5, 0), ipady=2)
create_file_button = ttk.Button(
file_frame, text="Создать файл", command=self.create_file_with_validation
)
create_button_packing_options = {"side": "right", "padx": (5, 0)}
create_file_button.pack(**create_button_packing_options)
# === Выбор файла Excel ===
select_frame = ttk.Frame(main_frame)
select_frame.pack(fill="x", pady=(0, 10))
ttk.Label(select_frame, text="Выберите файл Excel:").pack(side="left")
self.file_path_entry = tk.Entry(select_frame, width=30)
self.file_path_entry.pack(side="left", padx=(5, 2), ipady=2) # Подтянули чуть выше
browse_button = ttk.Button(
select_frame, text="Обзор...", command=self.select_file_for_aggregation
)
browse_button.pack(side="right", padx=(5, 0))
aggregate_button = ttk.Button(
main_frame,
text="Объединить информацию",
command=self.aggregate_data
)
aggregate_button.pack(fill="x", pady=(5, 10), padx=(0, 0))
# === ЦЕНТРИРОВАННЫЙ БЛОК ТРАНЗИТИВНОСТИ ===
sheet_frame = ttk.Frame(main_frame)
sheet_frame.pack(anchor="center", pady=(0, 10))
top_sheet_line = ttk.Frame(sheet_frame)
top_sheet_line.pack(fill="x", pady=(0, 5))
label_sheet = ttk.Label(top_sheet_line, text="Выберите лист (группу):")
label_sheet.pack(side="left")
self.sheet_combobox = ttk.Combobox(top_sheet_line, state="readonly", width=20)
self.sheet_combobox.pack(side="left", padx=(5, 0), ipady=2)
self.sheet_combobox.bind(
"<<ComboboxSelected>>", lambda _: self.reset_transitivity_field()
)
bottom_check_line = ttk.Frame(sheet_frame)
bottom_check_line.pack(fill="x", pady=(0, 5))
# КНОПКА СПРАВА, ПРОЦЕНТЫ СЛЕВА (для визуального центра)
check_button = ttk.Button(
bottom_check_line, text="Проверить на транзитивность", command=self.check_transitivity
)
check_button.pack(side="right", padx=(5, 0))
self.transitivity_percentage_label = tk.Label(
bottom_check_line,
text="", relief="groove",
width=8, bg="white"
)
self.transitivity_percentage_label.pack(side="right", padx=(5, 0))
# === НИЖНИЙ БЛОК (Кнопки действий) - Плотная упаковка ===
action_buttons_frame = ttk.Frame(main_frame)
# Уменьшили отступ сверху с 15 до 10px
action_buttons_frame.pack(anchor="center", pady=10)
fix_button = ttk.Button(
action_buttons_frame,
text="Исправить транзитивность",
command=self.apply_transitivity_corrections,
)
# Убрали лишние вертикальные отступы вокруг кнопок
fix_button.pack()
send_to_db_button = ttk.Button(
action_buttons_frame,
text="Нормализовать данные",
command=self.send_to_database,
)
send_to_db_button.pack()
def create_file_with_validation(self):
filename = self.new_file_name_entry.get().strip()
invalid_chars_pattern = r'[\\/:*?"<>|]'
if not filename:
messagebox.showwarning("Предупреждение", "Имя файла не может быть пустым.")
return
if re.search(invalid_chars_pattern, filename):
messagebox.showerror("Ошибка", "Имя файла содержит недопустимые символы.")
return
reserved_names = [
"CON", "PRN", "AUX", "NUL", "COM1", "LPT1", "COM2", "LPT2",
"COM3", "LPT3", "COM4", "LPT4", "COM5", "LPT5"
]
base_name = filename.split(".")[0].upper()
if base_name in reserved_names:
messagebox.showerror("Ошибка", f"'{base_name}' — зарезервированное имя.")
return
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
full_path = experts_folder / filename
if not full_path.exists():
with open(full_path, 'w') as f:
pass
messagebox.showinfo("Готово", f"Файл '{full_path.name}' создан.")
else:
messagebox.showinfo("Готово", f"Файл '{full_path.name}' уже существует.")
def select_file_for_aggregation(self):
print("[GUI SELECT] Нажат Обзор...")
from tkinter import filedialog
file_paths = filedialog.askopenfilenames(
title="Выберите файлы экспертов",
filetypes=[("Excel Files", "*.xlsx")]
)
if not file_paths:
print("[GUI SELECT] Выбор отменен пользователем.")
return
# Сохраняем пути для агрегации
self.controller.input_files = list(file_paths)
# Читаем листы ТОЛЬКО из первого файла для комбобокса
first_fp = file_paths[0]
sheet_names = []
try:
print(f"[GUI SELECT] Читаю листы из '{os.path.basename(first_fp)}'...")
temp_wb = load_workbook(filename=first_fp, data_only=True)
sheet_names = temp_wb.sheetnames
print(f"[GUI SELECT OK] Найдено листов: {sheet_names}")
# Помещаем первый файл в постоянный кэш контроллера
main_wb = load_workbook(filename=first_fp, data_only=True)
self.controller._loaded_wbs[first_fp] = main_wb
except Exception as e:
print(f"[GUI SELECT FATAL] Не удалось прочитать {first_fp}: {e}")
messagebox.showerror("Ошибка Excel", f"Файл поврежден или занят:\n{e}")
return
# === ЗАПОЛНЕНИЕ ВЫПАДАЮЩЕГО СПИСКА ===
self.sheet_combobox['values'] = sheet_names
if sheet_names:
self.sheet_combobox.current(0) # Выбираем первый лист автоматически
print("[GUI SELECT] Комбобокс обновлен.")
else:
print("[GUI SELECT WARN] Листы не найдены даже в первом файле.")
def reset_transitivity_field(self):
self.transitivity_percentage_label.config(text="")
def check_transitivity(self):
file_path = self.file_path_entry.get()
selected_sheet = self.sheet_combobox.get()
if not file_path or not selected_sheet:
messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
return
result = self.controller.check_transitivity()
self.transitivity_percentage_label.config(text=result)
def apply_transitivity_corrections(self):
file_path = self.file_path_entry.get()
selected_sheet = self.sheet_combobox.get()
if not file_path or not selected_sheet:
messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
return
success = self.controller.apply_transitivity_corrections(file_path, selected_sheet)
if success:
messagebox.showinfo("Готово", "Транзитивность исправлена и файл сохранён.")
def aggregate_data(self):
print("[GUI] Кнопка нажата")
output_file = self.new_file_name_entry.get().strip()
if not output_file:
messagebox.showwarning("Предупреждение", "Сначала создайте имя файла.")
return
if not output_file.endswith(".xlsx"):
output_file += ".xlsx"
input_files = getattr(self.controller, 'input_files', [])
if not input_files or len(input_files) == 0:
messagebox.showwarning("Внимание", "Сначала выберите файлы кнопкой 'Обзор...'.")
return
workbook = self.controller.aggregate_data(output_file, input_files)
if workbook is None:
messagebox.showerror("Ошибка данных", "Не удалось собрать данные.\nСмотрите консоль.")
return
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
dest_path = experts_folder / output_file
print(f"[GUI SAVE TARGET] Путь назначения: {dest_path}")
try:
# === ИСПРАВЛЕНО: Передаем СТРОКУ пути, а не объект файла! ===
workbook.save(str(dest_path))
# Закрываем книгу для освобождения памяти Windows
workbook.close()
print("[GUI SAVE SUCCESS]")
# Обновляем интерфейс перед показом окна сообщения
self.update_idletasks()
messagebox.showinfo("Готово", f"Сводный отчет создан:\\n{dest_path}")
# Принудительный вынос окна поверх всех остальных окон ОС
self.lift()
self.attributes("-topmost", True)
self.after_idle(self.attributes, "-topmost", False)
except Exception as e:
print(f"[GUI FATAL ERROR]: {e}")
messagebox.showerror("Ошибка сохранения", f"Не удалось сохранить файл:\n{e}")
return None
def send_to_database(self):
file_path = self.file_path_entry.get()
if not file_path:
messagebox.showwarning("Внимание", "Пожалуйста, выберите файл Excel!")
return
success = self.controller.send_to_database(file_path)
if success:
messagebox.showinfo("Готово", "Данные успешно отправлены в базу данных.")
# Dozor/modules/excel/excel_aggregate_work.py
import os
from pathlib import Path
from statistics import mean, mode, StatisticsError
from openpyxl import load_workbook, Workbook
from collections import defaultdict
def normalize_cell(cell):
if cell is None or str(cell).strip() == "":
return ""
val_str = str(cell).replace(',', '.').strip().upper()
try:
num = float(val_str)
# Если число целое (например, 5.0), сохраняем как int
return int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
except ValueError:
return val_str
class AggregateWorkController:
def __init__(self):
self.input_files = [] # Здесь храним РЕАЛЬНЫЕ ПУТИ
def select_file(self):
from tkinter import filedialog
file_paths = filedialog.askopenfilenames(
title="Выберите файлы экспертов",
filetypes=[("Excel Files", "*.xlsx")]
)
if not file_paths:
print("[GUI SELECT] Выбор отменен.")
return []
# Сохраняем ЧИСТЫЕ пути в память контроллера
self.input_files = list(file_paths)
# Обновляем видимое поле (для пользователя)
display_path = f"{Path(file_paths[0]).parent}/... ({len(file_paths)} files)"
# ВАЖНО: обновляем entry только если он существует
if hasattr(self, 'file_path_entry'):
self.file_path_entry.delete(0, len(self.file_path_entry.get()))
self.file_path_entry.insert(0, display_path)
messagebox.showinfo("Выбор файлов", f"Выбрано {len(file_paths)} файлов.")
return self.input_files
def aggregate_data(self, output_filename, input_files):
print("[AGG START]")
if not input_files or len(input_files) < 2:
messagebox.showwarning("Внимание", "Нужно выбрать минимум 2 файла.")
return None
grouped_by_sheet = defaultdict(list)
for fp in input_files:
try:
wb = load_workbook(fp, data_only=True)
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
matrix = []
for row in ws.iter_rows(values_only=True):
current_row = [normalize_cell(c) for c in row]
matrix.append(current_row)
has_data = False
for mat_r in matrix:
for v in mat_r:
if isinstance(v, (int, float)):
has_data = True; break
if has_data: break
if has_data and matrix:
grouped_by_sheet[sheet_name].append(matrix)
except Exception as e:
print(f"[AGG READ ERROR]: {e}")
continue
final_wb = Workbook()
if "Sheet" in final_wb.sheetnames:
del final_wb["Sheet"]
names = list(grouped_by_sheet.keys())
if not names:
messagebox.showinfo("Результат", "Нет валидных данных для объединения.")
return None
from statistics import mode, StatisticsError
for s_idx, sheet_name in enumerate(names):
matrices_list = grouped_by_sheet[sheet_name]
max_r = 0; max_c = 0
for mat in matrices_list:
if len(mat) > max_r: max_r = len(mat)
for row in mat:
if len(row) > max_c: max_c = len(row)
result_mat = [["" for _ in range(max_c)] for _ in range(max_r)]
for r in range(max_r):
new_row = []
for c in range(max_c):
vals = []
for mat in matrices_list:
if r < len(mat) and c < len(mat[r]):
norm_val = normalize_cell(mat[r][c])
if isinstance(norm_val, (int, float)):
vals.append(norm_val)
if not vals:
new_row.append("")
continue
val = ""
try:
val = mode(vals)
except StatisticsError:
numeric_vals = [float(x) for x in vals if isinstance(x, (int, float))]
if numeric_vals:
val = round(mean(numeric_vals), 3)
new_row.append(val)
result_mat.append(new_row)
new_ws = final_wb.create_sheet(title=sheet_name)
# Шапка I-1...
for col_idx in range(max_c):
new_ws.cell(row=1, column=col_idx + 4, value=f"I-{col_idx+1}")
# Тело матрицы
for r_idx in range(len(result_mat)):
row = result_mat[r_idx]
for c_idx in range(len(row)):
val = row[c_idx]
target_row = r_idx + 2
target_col = c_idx + 4
if isinstance(val, (int, float)):
new_ws.cell(row=r_idx + 1, column=c_idx + 1, value=str(r_idx + 1))
new_ws.cell(row=r_idx + 1, column=c_idx + 2, value="ID")
new_ws.cell(row=target_row, column=target_col, value=val)
elif val != "":
new_ws.cell(row=r_idx + 1, column=c_idx + 1, value=str(r_idx + 1))
new_ws.cell(row=r_idx + 1, column=c_idx + 2, value="ID")
new_ws.cell(row=target_row, column=target_col, value=str(val))
# Нижняя половина (симметрия)
for i in range(len(result_mat)):
for j in range(i + 1, len(result_mat[i])):
upper_val = result_mat[i][j]
target_row = j + 1
target_col = i + 4
target_cell = new_ws.cell(row=target_row, column=target_col)
if isinstance(upper_val, (int, float)) and upper_val != 0:
lower_val = round(1 / upper_val, 3)
new_ws.cell(row=target_row, column=target_col, value=lower_val)
else:
new_ws.cell(row=target_row, column=target_col, value="")
dest_path = Path(__file__).resolve().parents[2] / "experts" / output_filename
try:
final_wb.save(str(dest_path))
final_wb.close()
print("[GUI SAVE SUCCESS]")
return final_wb
except Exception as e:
print(f"[GUI FATAL ERROR]: {e}")
return None
def check_transitivity(self):
print("[TRANS CHECK]")
input_files = getattr(self, 'input_files', [])
if not input_files: return "N/A"
first_fp = input_files[0]
wb = self._loaded_wbs.get(first_fp)
if not wb: return "Ошибка"
ws = wb.active; n = ws.max_row - 1
values = [[0.0]*n for _ in range(n)]
for i in range(1, n+1):
for j in range(1, n+1):
cell = ws.cell(row=i+1, column=j+3).value
norm = normalize_cell(cell)
values[i-1][j-1] = norm if isinstance(norm, (int, float)) else 0.0
total = consistent = 0
for i in range(n):
for j in range(n):
for k in range(n):
a,b,c = values[i][j], values[j][k], values[i][k]
if all(isinstance(x, (int, float)) for x in [a,b,c]) and a>0 and b>0:
total += 1
if a > b and b > c and a >= c: consistent += 1
return f"{int((consistent/total)*100)}%" if total else "0%"
def apply_transitivity_corrections(self, file_path=None, sheet_name=None):
print("[TRANS FIX]")
if not file_path or not sheet_name: return False
wb = self._loaded_wbs.get(file_path)
if not wb:
try: wb = load_workbook(file_path); self._loaded_wbs[file_path] = wb
except Exception as e: print(f"[FIX LOAD ERR]: {e}"); return False
if sheet_name not in wb.sheetnames: return False
ws = wb[sheet_name]; n = ws.max_row - 1
values = [[0.0]*n for _ in range(n)]
for i in range(1, n+1):
for j in range(1, n+1):
cell = ws.cell(row=i+1, column=j+3).value
norm = normalize_cell(cell)
values[i-1][j-1] = norm if isinstance(norm, (int, float)) else 0.0
corrected = False
for i in range(n):
for j in range(i + 1, n):
a = values[i][j]; inv_a = values[j][i]
if isinstance(a, (int, float)) and isinstance(inv_a, (int, float)):
if abs(a * inv_a - 1) > 0.01:
new_inv = round(1 / a, 3)
ws.cell(row=j+1, column=i+3, value=new_inv)
corrected = True
if corrected:
try: wb.save(file_path); print("[FIX SAVED]"); return True
except Exception as e: print(f"[FIX SAVE ERR]: {e}"); return False
else: print("[FIX NONE]"); return True