Загрузка данных


При  проведении объединения файлов Эксель в один сводный файл выдало ошибку:

После заполнения матриц все сработало. Думаю надо ставить проверку и предупреждение

Traceback (most recent call last):
  File "E:\Anaconda3\Lib\tkinter\__init__.py", line 2068, in __call__
    return self.func(*args)
           ~~~~~~~~~^^^^^^^
  File "E:\dozor_env\Lib\site-packages\customtkinter\windows\widgets\ctk_button.py", line 564, in _on_release
    self._command()
    ~~~~~~~~~~~~~^^
  File "E:\dukora\modules\experts\aggregate_tab_inter.py", line 153, in aggregate_data
    success = self.controller.aggregate_data(output_file_path, input_files)
  File "E:\dukora\modules\experts\aggregate_tab_work.py", line 101, in aggregate_data
    messagebox.showerror("Ошибка данных", "В выбранных файлах нет матриц.")
    ^^^^^^^^^^
NameError: name 'messagebox' is not defined

Я вайб кодер. Но мне кажется здесь ошибка была или из-за того что матрица не оказалась строго квадратной. Когда я добавил для квадрата одну строку и заполнил матрицу - все сработало без ошибки. Нужно сделать в коде исправление на проверку того что я написал, чтобы программа не падала, а предупреждала, что матрица не соответствует квадрату.

Кроме того, при проверке на согласование транзитивности выдало следующую ошибку - 
(dozor_env) (base) E:\dukora>python main.py
[INFO] Пользователь '1' успешно авторизован.
[GUI SELECT] Нажат Обзор...
[PREPARE FAIL]: name 'np' is not defined

Для устранения этих ошибок показываю код.




# ============================================
# dukora\modules\experts\aggregate_tab_inter.ру
# ============================================
import customtkinter as ctk
from tkinter import messagebox
from pathlib import Path
# Исправлено: указываем правильную подпапку "excel"
from modules.utils.resources import resource_path

from modules.experts.aggregate_tab_work import AggregateWorkController

class AggregateExpertTab(ctk.CTkFrame):
    def __init__(self, master):
        super().__init__(master)
        self.controller = AggregateWorkController()
        
        # Основной контейнер с отступами
        main_container = ctk.CTkFrame(self, fg_color="transparent")
        main_container.pack(fill="both", expand=True, padx=10, pady=10)
        
        # --- СЕТКА ИНТЕРФЕЙСА ---
        # Колонки: 0-Заголовок (фикс), 1-Поле ввода (растягивается), 2-Кнопка Обзор (фикс)
        main_container.grid_columnconfigure(0, weight=0, minsize=140)  
        main_container.grid_columnconfigure(1, weight=1)               
        main_container.grid_columnconfigure(2, weight=0, minsize=100)   
        
        # Строка 0: Заголовок блока
        lbl_create = ctk.CTkLabel(main_container, text="АГРЕГИРОВАНИЕ ФАЙЛОВ:", font=ctk.CTkFont(size=16, weight="bold"))
        lbl_create.grid(row=0, column=0, columnspan=3, sticky="w", pady=(0, 10))
        
        # Строка 1: Блок создания файла и указания пути (вложенная сетка)
        frame_file = ctk.CTkFrame(main_container, fg_color="transparent")
        frame_file.grid(row=1, column=0, columnspan=3, sticky="ew", pady=5)
        
        frame_file.grid_columnconfigure(0, weight=0, minsize=140)
        frame_file.grid_columnconfigure(1, weight=1)
        frame_file.grid_columnconfigure(2, weight=0, minsize=100)
        
        t_lbl_1 = ctk.CTkLabel(frame_file, text="Создать файл:", anchor="w")
        t_lbl_1.grid(row=0, column=0, sticky="w", padx=(0, 5), pady=5)
        
        self.new_file_name_entry = ctk.CTkEntry(frame_file, width=250)
        self.new_file_name_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
        
        btn_create = ctk.CTkButton(frame_file, text="Создать", command=self.handle_create_file_request, width=100)
        btn_create.grid(row=0, column=2, sticky="e", padx=5, pady=5)
        
        t_lbl_2 = ctk.CTkLabel(frame_file, text="Указать путь:", anchor="w")
        t_lbl_2.grid(row=1, column=0, sticky="w", padx=(0, 5), pady=5)
        
        self.target_file_entry = ctk.CTkEntry(frame_file, width=250)
        self.target_file_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
        
        browse_target_button = ctk.CTkButton(frame_file, text="Обзор...", command=self.select_target_file, width=100)
        browse_target_button.grid(row=1, column=2, sticky="e", padx=5, pady=5)
        
        # Строка 2: Выбор исходных файлов экспертов (вложенная сетка)
        frame_select = ctk.CTkFrame(main_container, fg_color="transparent")
        frame_select.grid(row=2, column=0, columnspan=3, sticky="ew", pady=5)
        
        frame_select.grid_columnconfigure(0, weight=0, minsize=140)
        frame_select.grid_columnconfigure(1, weight=1)
        frame_select.grid_columnconfigure(2, weight=0, minsize=100)
        
        t_lbl_3 = ctk.CTkLabel(frame_select, text="Выбрать файлы:", anchor="w")
        t_lbl_3.grid(row=0, column=0, sticky="w", padx=(0, 5), pady=5)
        
        self.file_path_entry = ctk.CTkEntry(frame_select, width=250)
        self.file_path_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
        
        browse_source_button = ctk.CTkButton(frame_select, text="Обзор...", command=self.select_files_for_aggregation, width=100)
        browse_source_button.grid(row=0, column=2, sticky="e", padx=5, pady=5)
        
        # Строка 3: Кнопка объединения во всю ширину
        aggregate_button = ctk.CTkButton(
            main_container,
            text="Объединить информацию",
            command=self.aggregate_data,
            fg_color="#28a745", hover_color="#218838",
            font=ctk.CTkFont(size=13, weight="bold"),
            height=45
        )
        aggregate_button.grid(row=3, column=0, columnspan=3, sticky="ew", pady=(15, 15))
        
        # Настройка растягивания строк внутри вложенных фреймов для адаптивности
        for frm in [frame_file, frame_select]:
            frm.grid_rowconfigure(0, weight=1)
            frm.grid_rowconfigure(1, weight=1)
            
        # Настройка весов строк основного контейнера
        main_container.grid_rowconfigure(1, weight=0)
        main_container.grid_rowconfigure(2, weight=0)
        main_container.grid_rowconfigure(3, weight=0)

    # === МЕТОДЫ УПРАВЛЕНИЯ СОБЫТИЯМИ ИНТЕРФЕЙСА ===

    def handle_create_file_request(self):
        """
        Вызывается при нажатии кнопки "Создать".
        Передает имя в контроллер и отображает полный путь созданного файла.
        """
        filename = self.new_file_name_entry.get().strip()
        result_path = self.controller.create_empty_excel_file(filename)
        if result_path:
             # Очищаем поле целевого пути и вставляем туда только что созданный файл
             self.target_file_entry.delete(0, "end")
             self.target_file_entry.insert(0, str(result_path))
             
    def select_target_file(self):
        """
        Открывает проводник для выбора существующего Excel-файла 
        (куда будет сохранен результат или который будет перезаписан).
        """
        from tkinter import filedialog
        initial_dir = resource_path("excel") 
        file_path = filedialog.askopenfilename(title="Укажите целевой Excel-файл", initialdir=str(initial_dir), filetypes=[("Excel Files", "*.xlsx")])
        if file_path:
            self.target_file_entry.delete(0, "end")
            self.target_file_entry.insert(0, file_path)
            
    def select_files_for_aggregation(self):
        """
        Открывает множественный выбор файлов-источников (экспертных оценок).
        Путь к папке по умолчанию ведет в /experts от корня проекта.
        """
        print("[GUI SELECT] Нажат Обзор...")
        from tkinter import filedialog
        initial_dir = resource_path("experts")
        try:
            file_paths = filedialog.askopenfilenames(title="Выберите файлы экспертов", initialdir=str(initial_dir), filetypes=[("Excel Files", "*.xlsx")])
            if not file_paths: 
                return
            # Сохраняем список путей внутрь контроллера бизнес-логики
            self.controller.input_files = list(file_paths)
            display_text = f"... ({len(file_paths)} files)"
            self.file_path_entry.delete(0, "end")
            self.file_path_entry.insert(0, display_text)
        except Exception as e:
            print(f"[GUI FATAL ERROR]: {e}")
            messagebox.showerror("Критическая ошибка", f"Не удалось открыть проводник.\n{e}")
            
    def aggregate_data(self):
        """
        Главная кнопка действия. Проверяет заполненность полей и запускает логику сведения данных.
        """
        output_file_path = self.target_file_entry.get().strip()
        if not output_file_path:
            messagebox.showwarning("Предупреждение", "Сначала укажите целевой файл!")
            return
            
        input_files = getattr(self.controller, 'input_files', [])
        if not input_files or len(input_files) == 0:
            messagebox.showwarning("Внимание", "Сначала выберите исходные файлы кнопкой 'Обзор...'.")
            return
            
        success = self.controller.aggregate_data(output_file_path, input_files)
        if success:
            messagebox.showinfo("Готово", f"Сводный отчет успешно сохранен в:\n{output_file_path}")
# ===========================================            
            
            





# ===========================================
# dukora\modules\experts\aggregate_tab_work.py
# ===========================================
from openpyxl import load_workbook, Workbook
import os
import re
import zipfile
import shutil
import tempfile
from statistics import multimode, mean
from collections import defaultdict
from pathlib import Path
from modules.utils.resources import resource_path

class AggregateWorkController:
    def __init__(self):
        self.input_files = []
        self._loaded_wbs = {}
        
    def create_empty_excel_file(self, filename):
        clean_filename = filename.strip()
        if not clean_filename.endswith('.xlsx'):
            clean_filename += '.xlsx'
        if not clean_filename:
             messagebox.showwarning("Предупреждение", "Имя файла не может быть пустым.")
             return None
             
        invalid_chars_pattern = r'[\\/:*?"<>|]'
        if re.search(invalid_chars_pattern, clean_filename):
            messagebox.showerror('Ошибка', 'Имя файла содержит недопустимые символы \\ / : * ? " < > |')
            return None
            
        experts_folder = resource_path("excel")
        try:
            os.makedirs(experts_folder, exist_ok=True)
        except OSError as e:
             messagebox.showerror("Ошибка доступа", f"Не удалось создать папку 'excel':\n{e}")
             return None
        
        full_path = experts_folder / clean_filename
        wb = Workbook()
        if "Sheet" in wb.sheetnames:
            del wb["Sheet"]
        wb.create_sheet(title="Данные")
        wb.save(full_path)
        wb.close()
        return full_path
        
    def _load_all_workbooks(self):
        errors = []
        loaded_count = 0
        for fp in self.input_files:
            try:
                # Читаем значения формул (data_only=True), но игнорируем типы данных
                wb = load_workbook(fp, data_only=True, read_only=False)
                self._loaded_wbs[fp] = wb
                loaded_count += 1
            except Exception as e:
                errors.append((Path(fp).name, str(e)))
        return True if loaded_count > 0 else False, errors

    def _get_mode_or_neutral(self, values):
        numeric_values = [v for v in values if isinstance(v, (int, float))]
        if not numeric_values:
            return 1.0
            
        modes = multimode(numeric_values)
        if len(modes) == 1:
            return modes[0]
        return mean(modes)
        
    def aggregate_data(self, output_file_path, input_files):
        self._loaded_wbs.clear()
        
        all_loaded, errors = self._load_all_workbooks()
        if not all_loaded:
            messagebox.showerror("Ошибка загрузки", f"Ни один файл не был загружен.\nПримеры ошибок: {errors[:3]}")
            return False
            
        sheets_by_name = defaultdict(list)
        sheet_dimensions = {}
        
        # --- 1. ПРОХОД: Ищем размеры ---
        for fp, wb in self._loaded_wbs.items():
            for sheet_name in wb.sheetnames:
                ws = wb[sheet_name]
                n_indicators = 0
                while True:
                    cell_val = ws.cell(row=2, column=4 + n_indicators).value
                    
                    s = "" if cell_val is None else str(cell_val).strip().replace(',', '.')
                    
                    try:
                        float(s)
                        n_indicators += 1
                    except ValueError:
                        break
                        
                if n_indicators >= 2:
                    sheets_by_name[sheet_name].append(ws)
                    sheet_dimensions[sheet_name] = n_indicators
                    
        if not sheets_by_name:
            messagebox.showerror("Ошибка данных", "В выбранных файлах нет матриц.")
            return False
            
        out_wb = Workbook()
        if "Sheet" in out_wb.sheetnames:
            del out_wb["Sheet"]
            
        matrix_data_store = {}
        
        # --- 2. ПРОХОД: Считаем данные ---
        for group_name, worksheets in sheets_by_name.items():
            n = sheet_dimensions[group_name]
            
            # === ВОТ ЭТИ СТРОКИ БЫЛИ ПРОПУЩЕНЫ (фикс ошибки) ===
            MATRIX_START_ROW = 2
            MATRIX_START_COL = 4
            # ==================================================
            
            current_matrix = [[None] * n for _ in range(n)]
            
            for i in range(n):
                for j in range(n):
                    if i == j:
                        val = 1.0
                    elif i < j:
                        values_list = []
                        for ws in worksheets:
                            raw = ws.cell(row=MATRIX_START_ROW + i, column=MATRIX_START_COL + j).value
                            
                            clean_s = "" if raw is None else str(raw).strip().replace(',', '.').replace(' ', '')
                            
                            try:
                                num = float(clean_s)
                                values_list.append(num)
                            except ValueError:
                                continue
                                
                        val = self._get_mode_or_neutral(values_list)
                    else:
                        val = None
                        
                    current_matrix[i][j] = val
            
            # Заполняем низ обратными величинами
            EPSILON = 1e-9
            for i in range(n):
                for j in range(i):
                    upper = current_matrix[j][i]
                    if upper is None or abs(upper) < EPSILON:
                        current_matrix[i][j] = 1.0
                    else:
                        current_matrix[i][j] = round(1.0 / upper, 6)
                        
            matrix_data_store[group_name] = current_matrix
            
        # --- ЗАПИСЬ РЕЗУЛЬТАТА ---
        temp_fd, temp_path = tempfile.mkstemp(suffix='.xlsx')
        os.close(temp_fd)
        
        try:
            for group_name, matrix in matrix_data_store.items():
                n = len(matrix)
                out_ws = out_wb.create_sheet(title=group_name[:31])
                
                source_ws = sheets_by_name[group_name][0]
                
                headers = ["№ п/п", "Наименование показателя", "Код"]
                for col_idx, header in enumerate(headers, start=1):
                    out_ws.cell(row=1, column=col_idx, value=header)
                for row_idx in range(1, n + 1):
                    out_ws.cell(row=row_idx + 1, column=1, value=str(row_idx))
                    out_ws.cell(row=row_idx + 1, column=2, value=source_ws.cell(row=row_idx + 1, column=2).value)
                    out_ws.cell(row=row_idx + 1, column=3, value=source_ws.cell(row=row_idx + 1, column=3).value)
                
                codes = [source_ws.cell(row=1, column=4 + c).value for c in range(n)]
                for c_idx, code in enumerate(codes):
                    out_ws.cell(row=1, column=4 + c_idx, value=code)

                # Записываем числа
                for i in range(n):
                    for j in range(n):
                        out_ws.cell(row=MATRIX_START_ROW + i, column=MATRIX_START_COL + j, value=float(matrix[i][j]))
            
            out_wb.save(temp_path)
            out_wb.close()
            
            target_path = Path(output_file_path)
            os.makedirs(target_path.parent, exist_ok=True)
            shutil.move(temp_path, target_path)
            
        except Exception as e:
            messagebox.showerror("Ошибка записи", f"Технический сбой: {e}")
            if os.path.exists(temp_path): 
                os.remove(temp_path)
            return False

        for wb in self._loaded_wbs.values():
            wb.close()
        self._loaded_wbs.clear()
            
        return True
        
# =========================================        








# =========================================
# dukora\modules\experts\check_tab_inter.py
# =========================================
import customtkinter as ctk
from tkinter import messagebox # Для работы showerror/showinfo
from pathlib import Path

# Используем ресурсный путь до корня экспертов
from modules.utils.resources import resource_path

from modules.experts.check_tab_work import CheckWorkController

class CheckExpertTab(ctk.CTkFrame):
    def __init__(self, master):
        super().__init__(master)
        self.controller = CheckWorkController()
        
        main_container = ctk.CTkFrame(self, fg_color="transparent")
        main_container.pack(fill="both", expand=True, padx=10, pady=10)
        
        main_container.grid_columnconfigure(0, weight=0, minsize=160)  
        main_container.grid_columnconfigure(1, weight=1)             
        main_container.grid_columnconfigure(2, weight=0, minsize=140)
        
        FONT_LABEL = ctk.CTkFont(size=13)
        
        lbl_file = ctk.CTkLabel(main_container, text="Выбрать файл:", anchor="w", font=FONT_LABEL)
        lbl_file.grid(row=0, column=0, sticky="w", ipady=2, pady=(0, 15))
        
        self.file_path_entry = ctk.CTkEntry(main_container, width=400)
        self.file_path_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=(0, 15)) # Исправлено на padx/pady
        
        browse_btn = ctk.CTkButton(main_container, text="Обзор...", command=self.select_check_file, width=120)
        browse_btn.grid(row=0, column=2, sticky="e", ipady=2, pady=(0, 15))
        
        lbl_group = ctk.CTkLabel(main_container, text="Выбрать группу (лист):", anchor="w", font=FONT_LABEL)
        lbl_group.grid(row=1, column=0, sticky="w", ipady=2, pady=(0, 15))
        
        self.group_cb = ctk.CTkComboBox(master=main_container, state="readonly", width=400)
        # Подписываемся на событие изменения выбора листа для контроллера
        self.group_cb.bind("<<ComboboxSelected>>", lambda e: setattr(self.controller, "current_sheet_name", self.group_cb.get()))
        self.group_cb.grid(row=1, column=1, sticky="ew", padx=5, ipady=2, pady=(0, 15))
        
        check_btn = ctk.CTkButton(main_container, text="Проверить на транзитивность", command=self.run_transitivity_check, width=200)
        check_btn.grid(row=2, column=0, sticky="w", padx=(0, 5), ipady=2, pady=(0, 15)) 
        
        self.trans_var = ctk.StringVar(value="")
        result_entry = ctk.CTkEntry(
            master=main_container,
            textvariable=self.trans_var,
            justify="center",
            width=100,
            height=35,
            state="disabled" # Чтобы пользователь не мог редактировать результат руками
        )
        result_entry.grid(row=2, column=1, sticky="ew", padx=5, pady=(0, 15))
        
        percent_lbl = ctk.CTkLabel(main_container, text="%", anchor="w", font=FONT_LABEL)
        percent_lbl.grid(row=2, column=2, sticky="w", padx=5, ipady=2, pady=(0, 15)) # Исправлена опечатка lbg -> lbl
        
        btn_fix_norm_frame = ctk.CTkFrame(main_container, fg_color="transparent")
        btn_fix_norm_frame.grid(row=3, column=0, columnspan=3, sticky="ew", pady=(20, 0))
        
        fix_btn = ctk.CTkButton(btn_fix_norm_frame, text="Исправить транзитивность", fg_color="#28a745", hover_color="#218838", command=self.fix_transitivity)
        fix_btn.pack(side="left", expand=True, fill="x", padx=5, ipady=2) 

        norm_btn = ctk.CTkButton(btn_fix_norm_frame, text="Нормализовать", fg_color="#dc3545", hover_color="#c82333", command=self.normalize_data)
        norm_btn.pack(side="left", expand=True, fill="x", padx=5, ipady=2)

        prepare_btn = ctk.CTkButton(
            master=btn_fix_norm_frame, 
            text="Привести к согласованию",
            fg_color="#ff9900", 
            hover_color="#cc7a00", 
            command=self.prepare_and_normalize
        )
        prepare_btn.pack(side="right", expand=True, fill="x", padx=5, ipady=2)

        for r in [0, 1, 2, 3]:
            main_container.grid_rowconfigure(r, weight=0)
            
        # Изначально список листов пуст. Он заполняется при выборе файла.
        self.group_cb.configure(values=[])
    
    # === МЕТОДЫ УПРАВЛЕНИЯ ===
    
    def refresh_groups(self):
        sheets = []
        if self.controller.current_file_path:
            sheets = self.controller.get_sheet_names(self.controller.current_file_path)
        self.group_cb.configure(values=sheets)
        if sheets:
            self.group_cb.set(sheets[0])
    
    def select_check_file(self):
        initial_dir = str(resource_path("experts"))
        file_path = ctk.filedialog.askopenfilename(title="Выберите Excel-файл", initialdir=initial_dir, filetypes=[("Excel Files", "*.xlsx")])
        if file_path:
            self.file_path_entry.delete(0, "end")
            self.file_path_entry.insert(0, file_path)
            self.controller.current_file_path = file_path
            # Обновляем список листов сразу после выбора файла
            self.refresh_groups()
    
    def run_transitivity_check(self):
        selected_sheet = self.group_cb.get()
        file_path = self.file_path_entry.get().strip()
        
        if not file_path or not selected_sheet or selected_sheet == "Нет листов":
            messagebox.showwarning("Внимание", "Сначала выберите файл!")
            return
        
        self.trans_var.set("") # Очищаем поле перед проверкой
        
        percent_raw = self.controller.check_transitivity(file_path, selected_sheet)
        self.trans_var.set(percent_raw)
    
    def fix_transitivity(self):
        selected_sheet = self.group_cb.get()
        file_path = self.file_path_entry.get().strip()
        
        if not file_path or not selected_sheet or selected_sheet == "Нет листов":
            messagebox.showwarning("Внимание", "Сначала выберите файл!")
            return
        
        success = self.controller.apply_transitivity_corrections(file_path, selected_sheet)
        
        if success:
            # Сообщаем пользователю о сохранении изменений.
            # Поле процентов обнуляется, так как текущее значение устарело.
            # Пользователь должен нажать "Проверить" снова.
            messagebox.showinfo("Готово", f"Нарушения транзитивности исправлены.\nДля точного процента нажмите 'Проверить'.")
        else:
            messagebox.showerror("Ошибка", "Не удалось исправить данные.")
    
    def normalize_data(self):
        selected_sheet = self.group_cb.get()
        file_path = self.file_path_entry.get().strip()
        
        if not file_path or not selected_sheet or selected_sheet == "Нет листов":
            messagebox.showwarning("Внимание", "Сначала выберите файл!")
            return
        
        self.controller.current_file_path = file_path
        self.controller.current_sheet_name = selected_sheet
        
        success, message = self.controller.normalize_group_data()
        
        if success:
            messagebox.showinfo("Готово", message)
        else:
            msg_type = messagebox.showerror
            if "уже проведена" in message or "очистить" in message or "квадратн" in message:
                msg_type = messagebox.showwarning
            msg_type("Статус операции", message)
    
    def prepare_and_normalize(self):
        selected_sheet = self.group_cb.get()
        file_path = self.file_path_entry.get().strip()
        
        if not file_path or not selected_sheet or selected_sheet == "Нет листов":
            messagebox.showwarning("Внимание", "Сначала выберите файл!")
            return
        
        self.trans_var.set("")
        
        success_prepare = self.controller.prepare_matrix_for_consistency(file_path, selected_sheet)
        
        if success_prepare:
            success_norm, message = self.controller.normalize_group_data()
            
            if success_norm:
                messagebox.showinfo("Готово", f"{message}\n\nМатрица перестроена для достижения высокой согласованности. Нажмите 'Проверить', чтобы увидеть точный процент транзитивности.")
            else:
                messagebox.showerror("Ошибка при записи:", message)
        else:
            messagebox.showerror("Ошибка", "Не удалось подготовить данные.")
            
# =========================================











# =====================================
# dukora\modules\experts\check_tab_work
# =====================================
from openpyxl import load_workbook, Workbook
import os
from pathlib import Path

class CheckWorkController:
    def __init__(self):
        self.current_file_path = ""
        self.current_sheet_name = ""
        
    # === НОВЫЙ МЕТОД: Жесткая привязка к шкале Саати ===
    def _quantize_value(self, val):
        """
        Приводит любое число к ближайшей точке нашей шкалы: 0.33, 1.0 или 3.0
        """
        if val is None:
            return None
            
        v = float(val)
        
        # Если значение очень близко к 3 (например 2.95 - 3.04)
        if abs(v - 3.0) < 0.1:
            return 3.0
            
        # Если значение очень близко к 1 (например 0.95 - 1.04)
        if abs(v - 1.0) < 0.1:
            return 1.0
            
        # Если значение очень близко к 0.33 (обратное к 3)
        if abs(v - 0.33) < 0.05:
            return 0.33
            
        # Если вдруг попало что-то другое (ошибка ввода), возвращаем как есть
        return v

    def get_sheet_names(self, file_path):
        try:
            wb = load_workbook(file_path, read_only=True)
            names = wb.sheetnames
            wb.close()
            return names
        except Exception as e:
            print(f"[LOAD SHEETS ERROR]: {e}")
            return []
        
    def _parse_cell_value(self, raw_val):
        if raw_val is None:
            return None
        s = str(raw_val).strip().upper()
        if s == "":
            return None
        cleaned = "".join([c for c in s if c.isdigit() or c in ".-"])
        try:
            num = float(cleaned)
            # Сразу нормализуем считанное число
            return self._quantize_value(num)
        except ValueError:
            return None

    def check_transitivity(self, file_path, sheet_name):
        """
        Проверка транзитивности через построение рейтинга.
        Гарантирует корректную оценку после применения радикального исправления.
        """
        n = 0
        
        try:
            wb = load_workbook(file_path, data_only=True)
            if sheet_name not in wb.sheetnames:
                 return "Ошибка"
            ws = wb[sheet_name]
            
            # Определяем размер матрицы
            while True:
                cell_val = ws.cell(row=2, column=4 + n).value
                parsed = self._parse_cell_value(cell_val)
                if parsed is None:
                    break
                n += 1
                    
            if n < 3:
                wb.close()
                return "100%"
                
            MATRIX_START_ROW = 2
            MATRIX_START_COL = 4
            values = [[None] * n for _ in range(n)]
            
            # Считываем всю матрицу в память как числа из нашей шкалы {0.33, 1, 3}
            for i in range(n):
                for j in range(n):
                    cell = ws.cell(row=MATRIX_START_ROW + i, column=j+MATRIX_START_COL).value
                    values[i][j] = self._parse_cell_value(cell)
                    
            total_triads = 0
            consistent_triads = 0

            # === НОВАЯ ЛОГИКА ПРОВЕРКИ ПО РАНГАМ ===
            # Строим рейтинг показателей прямо здесь, в момент проверки.
            # Суммируем строки (как при нормализации).
            row_sums = [sum([val or 0 for val in row]) for row in values]

            # Сортируем индексы от самого сильного показателя к слабому.
            ranking = sorted(range(len(row_sums)), key=lambda x: -row_sums[x])

            # Проверка всех возможных треугольников
            for i in range(n):
                for j in range(n):
                    if i == j: continue
                    for k in range(n):
                        if k == i or k == j: continue

                        a = values[i][j]
                        b = values[j][k]
                        c = values[i][k]

                        total_triads += 1

                        # Логика простая: смотрим на позиции в рейтинге.
                        # Если позиция i выше позиции k в рейтинге,
                        # то значение в ячейке i-k ДОЛЖНО быть большим (>= 1).
                        # Иначе оно должно быть малым (< 1).
                        should_be_greater = (ranking.index(i) < ranking.index(k))
                        
                        # Значение в ячейке действительно большое?
                        is_greater = (a >= 1)

                        # Транзитивность соблюдена, если ранг соответствует значению ячейки.
                        if should_be_greater == is_greater:
                            consistent_triads += 1

            wb.close()
            
            if total_triads == 0: 
                return "100%"
                
            percent = int((consistent_triads / total_triads) * 100)
            return f"{percent}%"
            
        except Exception as e:
            print(f"[CHECK TRANS CRITICAL FAIL]: {e}")
            return "Ошибка"
            
    def apply_transitivity_corrections(self, file_path, sheet_name):
        """
        Радикальное исправление транзитивности через построение идеальной иерархии 
        на основе нормализованных весов (аналогично методу "Нормализовать").
        Гарантирует высокую согласованность.
        """
        try:
            wb = load_workbook(file_path)
            
            if sheet_name not in wb.sheetnames:
                print("[ERROR] Лист не найден.")
                return False

            ws = wb[sheet_name]

            # Определяем размер матрицы
            n = 0
            while True:
                cell_val = ws.cell(row=2, column=4 + n).value
                parsed = self._parse_cell_value(cell_val)
                if parsed is None:
                    break
                n += 1

            MATRIX_START_ROW = 2
            MATRIX_START_COL = 4

            # Считываем текущую матрицу в память
            matrix = [[self._parse_cell_value(ws.cell(row=i+MATRIX_START_ROW, column=j+MATRIX_START_COL).value) for j in range(n)] for i in range(n)]

            ### Шаг 1: Нормализация (тот же принцип, что в кнопке "Нормализовать")
            # Вычисляем сумму каждой строки
            row_sums = [sum([val or 0 for val in row]) for row in matrix]

            # Проверка на нулевую сумму (на всякий случай)
            total_sum = sum(row_sums)
            if abs(total_sum) < 1e-9:
                raise ValueError("Сумма всех оценок равна нулю!")

            # Создаём нормализированную матрицу
            normalized_matrix = []
            for i in range(n):
                row = []
                for j in range(n):
                    numerator = matrix[i][j] or 0
                    denominator = row_sums[j] or 1  # Защита от деления на ноль
                    row.append(numerator / denominator)
                normalized_matrix.append(row)

            # Теперь вычисляем итоговый вес каждого критерия как сумму его СТОЛБЦА в нормализированной матрице
            criterion_weights = [sum(col) for col in zip(*normalized_matrix)]

            ### Шаг 2: Построение рейтинга
            # Сортируем индексы показателей по убыванию их веса
            ranking = sorted(range(len(criterion_weights)), key=lambda x: -criterion_weights[x])

            ### Шаг 3: Построение идеально согласованной матрицы
            fixed_matrix = []
            for i in range(n):
                row = []
                for j in range(n):
                    if i == j:
                        # Диагональ всегда единица
                        row.append(1.0)
                    elif ranking[i] < ranking[j]:
                        # Строка важнее столбца -> ставим максимум из шкалы
                        row.append(3.0)
                    else:
                        # Столбец важнее строки или они равны -> ставим минимум
                        row.append(1/3)
                fixed_matrix.append(row)

            ### Шаг 4: Запись новой матрицы в файл
            for i in range(n):
                for j in range(n):
                    ws.cell(row=i+MATRIX_START_ROW, column=j+MATRIX_START_COL, value=fixed_matrix[i][j])

            # Сохраняем изменения
            wb.save(file_path)
            wb.close()

            return True

        except Exception as e:
            print(f"[FIX LOAD/SAVE CRITICAL FAIL]: {e}")
            return False

    import numpy as np  # Для работы с матрицей

    def prepare_matrix_for_consistency(self, file_path, sheet_name):
        """
        Подготавливает матрицу к состоянию максимального согласия методом Саати.
        Алгоритм:
        1. Находит главный собственный вектор текущей матрицы.
        2. Строит новую матрицу, где каждое отношение является отношением этих весов.
        3. Округляет результаты до нашей шкалы {0.33, 1, 3}.
        4. Записывает результат в файл.
        """
        try:
            wb = load_workbook(file_path)
            
            if sheet_name not in wb.sheetnames:
                print("[ERROR] Лист не найден.")
                return False

            ws = wb[sheet_name]

            # Определяем размер матрицы
            n = 0
            while True:
                cell_val = ws.cell(row=2, column=4 + n).value
                parsed = self._parse_cell_value(cell_val)
                if parsed is None:
                    break
                n += 1

            MATRIX_START_ROW = 2
            MATRIX_START_COL = 4

            # Считываем текущую матрицу в память
            matrix = []
            for i in range(n):
                row = []
                for j in range(n):
                    raw_val = ws.cell(row=i+MATRIX_START_ROW, column=j+MATRIX_START_COL).value
                    # Парсер должен возвращать только наши числа или None
                    num = self._parse_cell_value(raw_val)
                    # Защищаемся от случайных текстовых ячеек
                    row.append(num if isinstance(num, (int, float)) else 0)
                matrix.append(row)

            # Преобразуем её в NumPy-массив для линейной алгебры
            np_matrix = np.array(matrix)

            # Шаг 1: Находим главный собственный вектор
            eigenvalues, eigenvectors = np.linalg.eig(np_matrix)

            # Собственные числа могут быть комплексными из-за погрешностей округления,
            # поэтому берём их абсолютные значения.
            max_eigenvalue_idx = np.argmax(np.abs(eigenvalues))
            eigenvector = eigenvectors[:, max_eigenvalue_idx].real

            # Шаг 2: НОРМАЛИЗУЕМ вектор (делим каждый элемент на сумму всех элементов)
            weights = eigenvector / np.sum(eigenvector)

            # Теперь у нас есть идеальный набор весов [w1, w2, ..., wn].
            # Идеальная матрица парных сравнений строится как M[i][j] = wi / wj.

            # Шаг 3: Строим новую матрицу
            fixed_matrix = []
            for i in range(n):
                row = []
                for j in range(n):
                    # Диагональ всегда единица
                    if i == j:
                        value = 1.0
                    else:
                        # Отношение весов
                        ratio = weights[i] / weights[j]
                        
                        # Округляем до нашей шкалы
                        # Здесь можно выбрать стратегию:
                        # Вариант A: Оставлять дробное число (лучше сохраняет информацию эксперта)
                        # Вариант B: Принудительно ставить 0.33/1/3 (более жёсткая логика)

                        # Я выбрал вариант B, так как он лучше подходит под твою задачу.
                        if ratio > 1.5:   # Точка переключения между 1 и 3
                            value = 3.0
                        elif ratio < 0.67: # Точка переключения между 1 и 0.33
                            value = 0.33
                        else:
                            value = 1.0
                        
                    row.append(value)
                fixed_matrix.append(row)

            # Шаг 4: Запись новой матрицы в файл
            for i in range(n):
                for j in range(n):
                    ws.cell(row=i+MATRIX_START_ROW, column=j+MATRIX_START_COL, value=fixed_matrix[i][j])

            # Сохранение изменений
            wb.save(file_path)
            wb.close()

            return True

        except Exception as e:
            print(f"[PREPARE FAIL]: {e}")
            return False
            
    def normalize_group_data(self):
        if not self.current_file_path or not self.current_sheet_name:
            return False, "Файл не выбран"
            
        try:
            wb = load_workbook(self.current_file_path)
            if self.current_sheet_name not in wb.sheetnames:
                return False, "Лист не найден"
                
            ws = wb[self.current_sheet_name]
            n = 0
            while True:
                cell_val = ws.cell(row=2, column=4 + n).value
                if self._parse_cell_value(cell_val) is not None:
                    n += 1
                else:
                    break
                    
            if n < 2:
                wb.close()
                return False, "Недостаточно данных (менее 2x2)"
                
            MATRIX_START_COL = 4
            HEADER_ROW = 1      
            DATA_START_ROW = 2  
            
            target_col = MATRIX_START_COL + n
            max_search_limit = MATRIX_START_COL + n + 20
            
            while target_col < max_search_limit:
                header_cell = ws.cell(row=HEADER_ROW, column=target_col).value
                if header_cell is None or ("нормализ" in str(header_cell).lower()):
                    break
                target_col += 1
                
            if target_col >= max_search_limit:
                wb.close()
                return False, "Нет свободного места для колонки весов."
                
            ws.cell(row=HEADER_ROW, column=target_col, value="Нормализованный вес")
            
            row_sums = []
            for i in range(n):
                row_sum = 0.0
                for j in range(n):
                    val = self._parse_cell_value(ws.cell(row=DATA_START_ROW + i, column=MATRIX_START_COL + j).value)
                    row_sum += val if val is not None else 0.0
                row_sums.append(row_sum)
                
            total_sum = sum(row_sums)
            if abs(total_sum) < 1e-9:
                wb.close()
                return False, "Сумма всех оценок равна нулю."
                
            for i in range(n):
                weight = round((row_sums[i] / total_sum), 4)
                ws.cell(row=DATA_START_ROW + i, column=target_col, value=weight)
                
            wb.save(self.current_file_path)
            wb.close()
            
            col_letter = ws.cell(row=HEADER_ROW, column=target_col).column_letter
            return True, f"Веса успешно рассчитаны (колонка {col_letter})"
            
        except Exception as e:
            print(f"[NORM ERROR]: {e}")
            return False, f"Ошибка при записи: {str(e)}"
            
# ==============================================================









# ========================================
# dukora\modules\experts\experts_window.py
# ========================================
import customtkinter as ctk
# Абсолютные импорты согласно вашему правилу
from modules.experts.create_tab_inter import CreateExpertTab
from modules.experts.aggregate_tab_inter import AggregateExpertTab
from modules.experts.check_tab_inter import CheckExpertTab


class ExpertsWindow(ctk.CTkToplevel):
    def __init__(self, master):
        super().__init__(master)
        
        self.title("Эксперты")
        window_width = 650
        initial_height = 355

        # Горизонтальный центр экрана
        screen_w = self.winfo_screenwidth()
        x_cord = int((screen_w - window_width) / 2)

        # Фиксированный отступ от верхнего края (под панель задач Windows)
        taskbar_offset_top = 40 
        y_cord = taskbar_offset_top
        
        self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
        self.resizable(True, False)
        self.grab_set() 
        self.transient(master)

        main_container = ctk.CTkFrame(self, corner_radius=10)
        main_container.pack(padx=10, pady=10, fill="both", expand=True)

        title = ctk.CTkLabel(main_container, text="Модуль экспертов", font=ctk.CTkFont(size=20, weight="bold"))
        title.pack(pady=(0, 15))

        accordion_frame = ctk.CTkFrame(main_container)
        accordion_frame.pack(fill="both", expand=True, pady=(10, 0))

        button_frame = ctk.CTkFrame(main_container, fg_color="transparent")

        self.accordion_data = {}
        self.currently_open_button = None

        # Заголовки аккордеона
        create_item = self._create_accordion_header(accordion_frame, "Создание файлов для экспертов")
        agg_item = self._create_accordion_header(accordion_frame, "Агрегирование файлов")
        check_item = self._create_accordion_header(accordion_frame, "Проверка файлов")

        # Наполнение контентом через наши новые файлы
        self._fill_create_section(create_item["content"])
        self._fill_aggregate_section(agg_item["content"])
        self._fill_check_section(check_item["content"])

        for item in [create_item, agg_item, check_item]:
            btn = item["button"]
            content = item["content"]
            self.accordion_data[btn] = {"content": content}
            btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))

        self.toggle_section(create_item["button"], create_item["content"])
        
        button_frame.pack(fill="x", side="bottom")
        close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
        close_btn.pack(side="right", padx=10, pady=10)

    def toggle_section(self, button, content):
        is_currently_open = (self.currently_open_button == button)
        if self.currently_open_button and not is_currently_open:
            prev_content = self.accordion_data[self.currently_open_button]["content"]
            prev_content.pack_forget()
            self.currently_open_button.configure(fg_color="#34495e")
            
        if is_currently_open:
            content.pack_forget()
            button.configure(fg_color="#34495e")
            self.currently_open_button = None
            new_geo = self._calculate_window_geometry(collapsed=True)
        else:
            content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
            button.configure(fg_color="#2c3e50")
            self.currently_open_button = button
            new_geo = self._calculate_window_geometry(collapsed=False)
        
        self.after_idle(lambda g=new_geo: self.geometry(g))

    def _calculate_window_geometry(self, collapsed=False):
        current_width = self.winfo_width()
        if current_width == 1:
            current_width = 650
            
        header_height = (50 * len(self.accordion_data)) + 30 
        BOTTOM_RESERVED_SPACE = 70 

        if collapsed or not self.currently_open_button:
            target_height = 355
        else:
            data = self.accordion_data[self.currently_open_button]
            content_widget = data["content"]
            self.update_idletasks()
            open_content_height = content_widget.winfo_reqheight()
            target_height = header_height + open_content_height + 120 + BOTTOM_RESERVED_SPACE

        max_h = self.winfo_screenheight() - 50
        final_height = min(target_height, max_h)
        return f"{current_width}x{final_height}"

    def _create_accordion_header(self, parent, title):
        item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
        item_container.pack(fill="x", pady=5)
        header_btn = ctk.CTkButton(
            item_container,
            text=title,
            height=45,
            font=ctk.CTkFont(weight="bold", size=16),
            anchor="w",
            fg_color="#34495e",
            hover_color="#3c5a72",
        )
        header_btn.pack(fill="x")
        content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
        return {"button": header_btn, "content": content_frame}

    def _fill_create_section(self, frame):
        panel = CreateExpertTab(frame)
        panel.pack(fill="both", expand=True, padx=10, pady=10)

    def _fill_aggregate_section(self, frame):
        panel = AggregateExpertTab(frame)
        panel.pack(fill="both", expand=True, padx=10, pady=10)

    def _fill_check_section(self, frame):
        panel = CheckExpertTab(frame)
        panel.pack(fill="both", expand=True, padx=10, pady=10)

# ============================================================