Загрузка данных
'''
Я, вайб кодер - ты, профессиональный программист. Ты делаешь - я вставляю, заменяю.
Кусками не работаем - только целыми фрагментами - метод на метод, файл на файл, от комментария до комментария, от комментария до конца и т.д. - по такому принципу.
- Файл main_interface.ру принципиально не менять, это только витрина и кнопки.
- Программа будет собираться руинсталлером, поэтому это надо иметь ввиду.
- В программе уже реализованы авторизация, первая кнопка главного меню АДМИНИСТРАТОР, вторая кнопка главного меню МЕНЕДЖЕР БАЗЫ.
- Необходимо подключить в "аккордеоне" третью кнопку главного меню ЭКСПЕРТЫ. Ниже будут представлены файлы-доноры для этой кнопки. Они реализованы в ткинкере. Мы делаем рефакторинг и все должно стать в кастомткинкере. Логика в файлах рабочая. По образцу этих файлов и надо организовать по принципу - один заголовок Аккордеона - 2 файла, из которых один для интерфейса, а второй с функционалом. Раньше в старом формате этот функционал был в двух окнах, теперь решили разделить на 3 окна.
- все файлы для работы ЭКСПЕРТЫ должны лежать по адресу dukora\modules\experts\.
- файл для работы с базами database_module.ру расположен по адресу dukora\modules\data\.
- если какие то вопросы есть - задавай.
Ниже представляю файлы, которые могут пригодиться как доноры, либо уже присутствующие как реальные файлы приложения.
dukora\main.py
dukora\gui\main_interface.py
dukora\modules\admin\users_tab.py
dukora\modules\admin\admin_accordion_window.py
dukora\modules\experts\experts_window.py
dukora\modules\data\database_module.py
excel_aggregate_inter.ру - файл – донор
excel_aggregate_work.ру - файл – донор
excel_export_inter.ру - файл – донор
excel_export_work.ру - файл - донор
==================================================================
Итогом работы должна стать полностью рабочая кнопка ЭКСПЕРТЫ - при нажатии на нее должны открываться в аккордеоне три заголовка а в них окна, оформленных в новом интерфейсе кастомткинкера с рабочим функционалом.
Новый интерфейс это и кастомткинкер и расположение элементов управления.
===================================================================
Заголовки и окна организовать так.
СОЗДАНИЕ ФАЙЛОВ ДЛЯ ЭКСПЕРТОВ:
Вверху
- Выбрать группу: поле с выпадающим списком групп
- Выбранные группы: поле для названия выбранных групп в 5 строк
- Количество экспертов: поле для указания числа экспертов
Заголовки выровнять по левому краю окна. Поля сделать одного размера по ширине, кроме числа экспертов - его поле на 3 знака. Поля должны быть выровнены по вертикальной линии.
Внизу в одну строчку равномерно на всю ширину окна 3 кнопки:
- Добавить (синего цвета)
- Удалить (красного цвета)
- Экспортировать в Excel (черного цвета)
===================================================================
АГРЕГИРОВАНИЕ ФАЙЛОВ:
Создать файл: поле для отображения адреса кнопка Создать (синего цвета)
Указать путь: поле для отображения адреса кнопка Обзор...(зеленого цвета)
Кнопка Объединить информацию на всю ширину (черного цвета)
===================================================================
ПРОВЕРКА ФАЙЛОВ:
Выбрать файл: поле для отображения адреса кнопка Обзор (синего цвета)
Выбрать группу: выпадающий список для выбора группы
Кнопка Проверить на транзитивность и поле на 5 знаков
Две кнопки в одну строку Исправить транзитивность (зеленого цвета) и Нормализовать (черного цвета)
'''
#================================================
#================================================
# dukora\main.py - реальный запускающий файл
#================================================
import customtkinter as ctk
from gui.main_interface import MainApp
from auth_window import AuthWindow
def start_application():
app = MainApp()
# Окно авторизации поверх главного
auth_win = AuthWindow(app)
app.wait_window(auth_win)
app.mainloop()
if __name__ == "__main__":
try:
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")
except Exception:
pass
start_application()
#===================================================
#====================================================
# dukora\gui\main_interface.py - реальный главный интерфейсный файл
#====================================================
import os
import customtkinter as ctk
from PIL import Image
import sys
from modules.manager.manager_window import ManagerWindow
from modules.experts.experts_window import ExpertsWindow
from modules.project.project_window import ProjectWindow
from modules.forecasting.forecast_window import ForecastWindow
from modules.utils.resources import resource_path
class MainApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Dozor System")
try:
if sys.platform == "win32":
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
target_w, target_h = 422, 335
self.geometry(f"{target_w}x{target_h}")
self.resizable(False, False)
bg_color = "#F0F0F0"
self.configure(fg_color=bg_color)
main_bg = ctk.CTkFrame(self, fg_color=bg_color)
main_bg.pack(fill="both", expand=True, padx=1, pady=1)
# === СЕТКА ИНТЕРФЕЙСА ===
main_bg.grid_columnconfigure(0, minsize=210, weight=0)
main_bg.grid_columnconfigure(1, weight=1)
for i in range(6):
main_bg.grid_rowconfigure(i, weight=0)
main_bg.grid_rowconfigure(6, weight=1)
button_font = ("Arial", 13, "bold")
btn_height = 45
# === ЛЕВАЯ ПАНЕЛЬ МЕНЮ (КНОПКИ ВЫЗОВА) ===
sidebar = ctk.CTkFrame(main_bg, fg_color="#EEEEEE", width=210)
sidebar.grid(row=0, column=0, rowspan=7, sticky="nsew", padx=(5, 5), pady=5)
sidebar.grid_columnconfigure(0, weight=1)
actions = [
("Администратор", self.open_admin_module),
("Менеджер базы", self.open_manager_module),
("Эксперты", self.open_experts_module),
("Проект", self.open_project_module),
("Прогнозирование", self.open_forecasting_module)
]
# Создание физических кнопок на экране
for i, (text, command_func) in enumerate(actions):
btn = ctk.CTkButton(
master=sidebar,
text=text,
font=button_font,
height=btn_height,
command=command_func
)
btn.grid(row=i, column=0, padx=10, pady=5, sticky="ew")
# === ПРАВАЯ ЧАСТЬ ВИТРИНЫ ===
content_area = ctk.CTkFrame(main_bg, fg_color=bg_color)
content_area.grid(row=0, column=1, rowspan=7, sticky="nsew", padx=(0, 10), pady=10)
content_area.grid_columnconfigure(0, weight=1)
content_area.grid_rowconfigure(0, weight=0)
content_area.grid_rowconfigure(1, minsize=2, weight=0)
content_area.grid_rowconfigure(2, minsize=27, weight=0)
content_area.grid_rowconfigure(3, weight=0)
pic_container = ctk.CTkFrame(content_area, fg_color="#d9ead3", width=220, height=130)
pic_container.grid(row=0, column=0, pady=(5, 0))
pic_container.grid_propagate(False)
img_loaded = False
try:
img_path = resource_path('images/logo1.png')
pil_img = Image.open(img_path)
target_w_i, target_h_i = 210, 110
ratio = pil_img.width / pil_img.height
box_ratio = target_w_i / target_h_i
if ratio > box_ratio:
new_w, new_h = target_w_i, int(target_w_i / ratio)
else:
new_w, new_h = int(target_h_i * ratio), target_h_i
resized_img = pil_img.resize((new_w, new_h), resample=Image.LANCZOS)
tk_image = ctk.CTkImage(light_image=resized_img, size=(new_w, new_h))
lbl_pic = ctk.CTkLabel(pic_container, image=tk_image, text="", fg_color="#d9ead3")
lbl_pic.place(relx=0.5, rely=0.5, anchor='center', x=0, y=-3)
img_loaded = True
except Exception as e:
fail_lbl = ctk.CTkLabel(pic_container, text="Сова\nне найдена", font=("Arial", 14), fg_color="#d9ead3")
fail_lbl.place(relx=0.5, rely=0.5, anchor="center")
bottom_btns_frame = ctk.CTkFrame(content_area, fg_color=bg_color)
bottom_btns_frame.grid(row=3, column=0, sticky="ew", padx=0)
bottom_btns_frame.grid_columnconfigure(0, minsize=195, weight=0)
ctk.CTkButton(
bottom_btns_frame,
text="Руководство",
font=button_font,
height=btn_height,
command=self.open_guide
).grid(row=0, column=0, sticky="ew", pady=(0, 8))
ctk.CTkButton(
bottom_btns_frame,
text="О программе",
font=button_font,
height=btn_height,
command=self.show_about
).grid(row=1, column=0, sticky="ew")
footer_frame = ctk.CTkFrame(main_bg, fg_color="#0078D7", height=30)
footer_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(10, 0))
footer_label = ctk.CTkLabel(footer_frame,
text="Copyright © Минск, НИИ, 2027 год",
text_color="#FF8C00",
fg_color="#0078D7",
font=("Arial", 13, "bold"))
footer_label.pack(expand=True, ipady=4)
self.update_idletasks()
self.tk.call('wm', 'geometry', self._w, f"{target_w}x{target_h}")
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
pos_x = (screen_w - target_w) // 2
pos_y = (screen_h - target_h) // 2
self.geometry(f"+{pos_x}+{pos_y}")
# === ОБРАБОТЧИК КНОПКИ АДМИНИСТРАТОРА (с принудительным логом) ===
def open_admin_module(self):
if hasattr(self, '_admin_win') and self._admin_win.winfo_exists():
self._admin_win.lift()
return
try:
from modules.admin.admin_accordion_window import AdminAccordionWindow
self._admin_win = AdminAccordionWindow(self)
self.wait_window(self._admin_win)
except Exception as e:
messagebox.showerror("Фатальный сбой", f"Не удалось открыть раздел Администратора.\n\n{e}")
import traceback
traceback.print_exc()
def open_manager_module(self):
if hasattr(self, '_manager_win') and self._manager_win.winfo_exists():
self._manager_win.lift()
return
self._manager_win = ManagerWindow(master=self)
self.wait_window(self._manager_win)
def open_experts_module(self):
if hasattr(self, '_experts_win') and self._experts_win.winfo_exists():
self._experts_win.lift()
return
self._experts_win = ExpertsWindow(master=self)
self.wait_window(self._experts_win)
def open_project_module(self):
if hasattr(self, '_project_win') and self._project_win.winfo_exists():
self._project_win.lift()
return
self._project_win = ProjectWindow(master=self)
self.wait_window(self._project_win)
def open_forecasting_module(self):
if hasattr(self, '_forecast_win') and self._forecast_win.winfo_exists():
self._forecast_win.lift()
return
self._forecast_win = ForecastWindow(master=self)
self.wait_window(self._forecast_win)
def open_guide(self):
file_name = "manual.chm"
full_path = resource_path(file_name)
print(f"Пытаюсь открыть: {full_path}")
if os.path.exists(full_path):
try:
os.startfile(full_path)
except Exception as e:
print(f"Система отказалась открывать файл: {e}")
else:
print("Ошибка: ФАЙЛА НЕТ в этой папке!")
def show_about(self):
from tkinter import messagebox
info_text = (
"Система Dozor V1.0\n"
"Разработано: Минск, НИИ\n"
"Версия сборки: 31.07.2026\n\n"
"(c) Все права защищены."
)
messagebox.showinfo(title="О программе", message=info_text)
if __name__ == "__main__":
app = MainApp()
app.mainloop()
#===================================================
#====================================================
# dukora\modules\admin\users_tab.py - реальный файл,
# для примера прописи адресов
#====================================================
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
from hashlib import sha256
from pathlib import Path
# Импорт нашей утилиты путей
from modules.utils.resources import resource_path
class AdminUsersPanel(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master)
container = ctk.CTkFrame(self)
container.pack(fill="both", expand=True, padx=10, pady=10)
style = ttk.Style()
style.theme_use("default")
style.configure("mystyle.Treeview", background="#2b2b2b", fieldbackground="#2b2b2b", foreground="white", borderwidth=0, font=("Arial", 10))
style.map('mystyle.Treeview', background=[('selected', '#4a4a4a')])
table_frame = ctk.CTkFrame(container)
table_frame.pack(fill="both", expand=True, side="top")
self.tree = ttk.Treeview(table_frame, columns=("ID", "Логин", "ФИО", "Роль"), show="headings", style="mystyle.Treeview")
self.tree.heading("ID", text="ID")
self.tree.heading("Логин", text="Логин")
self.tree.heading("ФИО", text="ФИО")
self.tree.heading("Роль", text="Роль")
self.tree.column("ID", width=40)
self.tree.column("Логин", width=100)
self.tree.column("ФИО", width=180)
self.tree.column("Роль", width=80)
self.tree.pack(side="left", fill="both", expand=True)
scrollbar = ctk.CTkScrollbar(table_frame, command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
form_frame = ctk.CTkFrame(container)
form_frame.pack(fill="x", pady=(10, 0))
self.login_entry = ctk.CTkEntry(form_frame, placeholder_text="Логин", width=100)
self.login_entry.grid(row=0, column=0, padx=5, pady=5)
self.fio_entry = ctk.CTkEntry(form_frame, placeholder_text="ФИО", width=180)
self.fio_entry.grid(row=0, column=1, padx=5, pady=5)
self.pass_entry = ctk.CTkEntry(form_frame, placeholder_text="Пароль", width=120, show="*")
self.pass_entry.grid(row=0, column=2, padx=5, pady=5)
self.role_cb = ctk.CTkComboBox(form_frame, values=["Админ", "Эксперт", "Пользователь"], width=100)
self.role_cb.grid(row=0, column=3, padx=5, pady=5)
self.role_cb.set("Эксперт")
btn_frame = ctk.CTkFrame(container)
btn_frame.pack(pady=10)
add_btn = ctk.CTkButton(btn_frame, text="Добавить", command=self.add_user)
add_btn.pack(side="left", padx=5)
edit_btn = ctk.CTkButton(btn_frame, text="Изменить", command=self.edit_user)
edit_btn.pack(side="left", padx=5)
del_btn = ctk.CTkButton(btn_frame, text="Удалить", command=self.delete_user, fg_color="#d9534f", hover_color="#c9302c")
del_btn.pack(side="left", padx=5)
# Загружаем базу сразу при создании панели
self.load_users()
def load_users(self):
self.tree.delete(*self.tree.get_children())
db_path: Path = resource_path('db/users.db')
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
fio TEXT NOT NULL,
role TEXT NOT NULL
)
''')
conn.commit()
cursor.execute('SELECT id, login, fio, role FROM users ORDER BY id')
rows = cursor.fetchall()
for index, row in enumerate(rows, start=1):
self.tree.insert("", "end", values=(index, row[1], row[2], row[3]), tags=(str(row[0]),))
except Exception as e:
messagebox.showerror("Ошибка БД", f"Не удалось загрузить пользователей:\n{e}\nПуть: {db_path}")
finally:
if 'conn' in locals():
conn.close()
def get_form_data(self):
return {
"login": self.login_entry.get(),
"fio": self.fio_entry.get(),
"password": self.pass_entry.get(),
"role": self.role_cb.get()
}
def clear_form(self):
self.login_entry.delete(0, "end")
self.fio_entry.delete(0, "end")
self.pass_entry.delete(0, "end")
self.role_cb.set("Эксперт")
def add_user(self):
data = self.get_form_data()
if not all([data["login"], data["fio"], data["password"]]):
messagebox.showwarning("Ошибка", "Заполните Логин, ФИО и Пароль")
return
password_hash = sha256(data["password"].encode()).hexdigest()
# === ВЫЗОВ РЕСУРСА ===
db_path: Path = resource_path('db/users.db')
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute('INSERT INTO users (login, password_hash, fio, role) VALUES (?, ?, ?, ?)', (data["login"], password_hash, data["fio"], data["role"]))
conn.commit()
messagebox.showinfo("Успех", f"Пользователь {data['login']} добавлен")
self.clear_form()
self.load_users()
except sqlite3.IntegrityError:
messagebox.showwarning("Ошибка", "Пользователь с таким логином уже существует.")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сохранить:\n{e}")
finally:
if 'conn' in locals():
conn.close()
def _get_selected_db_id(self):
selected = self.tree.selection()
if not selected:
return None
item = self.tree.item(selected)
db_id_str = item['tags'][0]
return int(db_id_str)
def delete_user(self):
db_id = self._get_selected_db_id()
login = self.tree.item(self.tree.selection())['values'][1]
if not db_id:
messagebox.showwarning("Ошибка", "Выберите строку")
return
# === ВЫЗОВ РЕСУРСА ===
db_path: Path = resource_path('db/users.db')
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute("DELETE FROM users WHERE id=?", (db_id,))
conn.commit()
messagebox.showinfo("Успех", f"Пользователь '{login}' удален")
self.clear_form()
self.load_users()
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось удалить:\n{e}")
finally:
if 'conn' in locals():
conn.close()
def edit_user(self):
db_id = self._get_selected_db_id()
if not db_id:
messagebox.showwarning("Ошибка", "Выберите строку")
return
# === ВЫЗОВ РЕСУРСА ===
db_path: Path = resource_path('db/users.db')
try:
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute('SELECT login, fio, role FROM users WHERE id=?', (db_id,))
user_data = cursor.fetchone()
if user_data:
self.login_entry.delete(0, "end")
self.login_entry.insert(0, user_data[0])
self.login_entry.configure(state="disabled")
self.fio_entry.delete(0, "end")
self.fio_entry.insert(0, user_data[1])
self.role_cb.set(user_data[2])
self.pass_entry.delete(0, "end")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось прочитать данные:\n{e}")
finally:
if 'conn' in locals():
conn.close()
#===================================================
#====================================================
# dukora\modules\admin\admin_accordion_window.py - реальный файл,
# для примера
#====================================================
import customtkinter as ctk
from tkinter import ttk, messagebox
# modules/admin/admin_accordion_window.py
from .users_tab import AdminUsersPanel
from .db_manager_panel import DBManagerPanel
from .migration_panel import MigrationPanel
class AdminAccordionWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.parent_main = master
self.title("Администрирование")
# --- 1. БАЗОВЫЕ ГАБАРИТЫ ОКНА ---
window_width = 650
initial_height = 380
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
x_cord = int((screen_w - window_width) / 2)
y_cord = int((screen_h - initial_height) / 2)
self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
# === 2. ЖЕСТКИЕ ОГРАНИЧЕНИЯ РАЗМЕРА ===
self.minsize(width=window_width, height=250)
self.maxsize(width=900, height=screen_h - 50)
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_lbl = ctk.CTkLabel(main_container, text="Панель администратора", font=ctk.CTkFont(size=20, weight="bold"))
title_lbl.pack(pady=(0, 15))
accordion_frame = ctk.CTkFrame(main_container)
accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
self.accordion_data = {}
self.currently_open_button = None
# === ЗАГОЛОВКИ АККОРДЕОНА ===
users_item = self._create_accordion_header(accordion_frame, "Управление пользователями")
security_item = self._create_accordion_header(accordion_frame, "Безопасность")
migration_item = self._create_accordion_header(accordion_frame, "Миграция")
# Наполняем секции контентом
self._fill_users_section(users_item["content"])
self._fill_security_section(security_item["content"])
self._fill_migration_section(migration_item["content"])
# Привязка команд переключения
for item in [users_item, security_item, migration_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(users_item["button"], users_item["content"])
# Кнопки внизу
button_frame = ctk.CTkFrame(main_container)
button_frame.pack(fill="x", pady=(10, 0))
close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
close_btn.pack(side="right", padx=10, pady=10)
self.protocol("WM_DELETE_WINDOW", self.on_closing)
# === 3. ЦЕНТРИРОВАНИЕ ПОСЛЕ ПОЛНОЙ ОТРИСТОВКИ ===
self.after(100, self._center_over_parent)
def _center_over_parent(self):
if not self.parent_main or not self.parent_main.winfo_exists():
return
self.update_idletasks()
w = self.winfo_width()
h = self.winfo_height()
root_win = self.parent_main
p_x = root_win.winfo_rootx()
p_y = root_win.winfo_rooty()
final_x = int(p_x + (root_win.winfo_width() - w) / 2)
final_y = int(p_y + (root_win.winfo_height() - h) / 2)
self.geometry(f"+{final_x}+{final_y}")
# === МЕТОД toggle_section (твой оригинальный) ===
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.geometry(new_geo)
# === РАСЧЕТ ГЕОМЕТРИИ (Исправлено!) ===
def _calculate_window_geometry(self, collapsed=False):
current_width = self.winfo_width()
# Если ширина еще не посчиталась (например, при первом запуске)
if current_width == 1:
current_width = 650
header_height = 50 * 3 + 30
# === ФИКС ДЛЯ КНОПОК: ДОБАВИЛИ ОТСТУП ПОД НИЖНЮЮ ПАНЕЛЬ ===
button_panel_height = 70 # Высота рамки с кнопкой "Закрыть" + её отступы
if collapsed or not self.currently_open_button:
target_height = header_height + 80 + button_panel_height
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 + button_panel_height
max_h = self.winfo_screenheight() - 50
min_h = 250
final_height = min(target_height, max_h) # Не выше экрана
final_height = max(final_height, min_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_users_section(self, frame):
panel = AdminUsersPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_security_section(self, frame):
panel = DBManagerPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_migration_section(self, frame):
panel = MigrationPanel(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def on_closing(self):
try:
self.after_cancel("all")
except Exception:
pass
try:
self.destroy()
except Exception:
pass
#===================================================
#====================================================
# dukora\modules\experts\experts_window.py - реальный файл
#====================================================
import customtkinter as ctk
from tkinter import ttk
class ExpertsWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.title("Эксперты")
window_width = 650
initial_height = 355
x_cord = int((self.winfo_screenwidth() / 2) - (window_width / 2))
y_cord = int((self.winfo_screenheight() / 2) - (initial_height / 2))
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
# ТВОИ ЗАГОЛОВКИ (3 штуки)
create_item = self._create_accordion_header(accordion_frame, "Создание файлов для экспертов")
aggregate_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(aggregate_item["content"])
self._fill_check_section(check_item["content"])
# Привязка команд
for item in [create_item, aggregate_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))
# Упаковка кнопки вниз
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()
# Динамический расчет высоты заголовков (их здесь 3)
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):
placeholder = ctk.CTkLabel(frame, text="Инструменты генерации экспертных файлов.", font=("Arial", 12))
placeholder.pack(expand=True)
def _fill_aggregate_section(self, frame):
placeholder = ctk.CTkLabel(frame, text="Объединение данных из разных источников.", font=("Arial", 12))
placeholder.pack(expand=True)
def _fill_check_section(self, frame):
placeholder = ctk.CTkLabel(frame, text="Валидация структуры и контрольных сумм.", font=("Arial", 12))
placeholder.pack(expand=True)
#===================================================
#====================================================
# dukora/modules/data/database_module.py - реальный файл,
# для работы с базой данных
#====================================================
# -*- coding: utf-8 -*-
import sys
import sqlite3
from pathlib import Path
try:
# Определяем корень проекта надежно:
# Если мы находимся внутри подпапок dukora, идем вверх до корня репозитория.
# __file__ дает путь к текущему .py файлу.
current_file_path = Path(__file__).resolve()
# Ищем папку 'dukora' в родителях
try:
dukora_parent = current_file_path.parents[[p.name for p in current_file_path.parents].index('dukora')]
except ValueError:
# Если по какой-то причине не нашли (запуск из другой директории), берем CWD
dukora_parent = Path.cwd()
BASE_DIR = dukora_parent
DB_FOLDER = BASE_DIR / "db"
ACTIVE_FLAG_PATH = DB_FOLDER / "active.flag"
DEFAULT_DB_PATH = DB_FOLDER / "database.db"
MASTERDATA_PATH = DB_FOLDER / "masterdata.db"
except Exception as e:
print(f"[FATAL] Не удалось вычислить путь к базе данных: {e}")
sys.exit(1)
def _determine_active_path():
try:
if ACTIVE_FLAG_PATH.exists():
with open(ACTIVE_FLAG_PATH, 'r', encoding='utf-8') as f:
name = f.read().strip()
if name == "masterdata":
return MASTERDATA_PATH
except Exception:
pass
return DEFAULT_DB_PATH
DB_PATH = _determine_active_path()
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session as SqlAlchemySession, declarative_base
engine = create_engine(f"sqlite:///{DB_PATH}", echo=False, connect_args={"check_same_thread": False})
Base = declarative_base()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db_session = SessionLocal()
try:
yield db_session
finally:
db_session.close()
def get_db_connection():
active_path = _determine_active_path()
global DB_PATH
DB_PATH = active_path
# === КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ ===
# Сначала проверяем наличие папки и создаем её ДО попытки открыть БД
if not DB_FOLDER.exists():
print(f"[ACTION] Создаю папку для базы: {DB_FOLDER}")
try:
DB_FOLDER.mkdir(parents=True, exist_ok=True)
except PermissionError as pe:
print(f"[FATAL] Нет прав на создание папки '{DB_FOLDER}'. Запустите терминал от имени Администратора.")
raise SystemExit(1)
conn = sqlite3.connect(active_path)
conn.execute("PRAGMA foreign_keys = ON;")
return conn
def _create_tables(cursor):
cursor.execute('''
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT NOT NULL UNIQUE,
gain_group REAL DEFAULT 0.0
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS indicators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
code TEXT NOT NULL UNIQUE,
group_code TEXT NOT NULL,
FOREIGN KEY (group_code) REFERENCES groups(code) ON DELETE CASCADE
)
''')
def _populate_test_data(conn):
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM groups")
count = cursor.fetchone()[0]
if count == 0:
print("[INFO] База данных пуста. Заполняю тестовыми данными...")
groups_data = [("Клубы", "KB", 1.5), ("Фанаты", "FAN", 2.0)]
cursor.executemany("INSERT INTO groups (name, code, gain_group) VALUES (?, ?, ?)", groups_data)
indicators_data = [
("ЦСКА", "KB-1", "KB"), ("Спартак", "KB-2", "KB"), ("Зенит", "KB-3", "KB"),
("Мужчины", "FAN-1", "FAN"), ("Женщины", "FAN-2", "FAN"), ("Дети", "FAN-3", "FAN")
]
cursor.executemany("INSERT INTO indicators (name, code, group_code) VALUES (?, ?, ?)", indicators_data)
conn.commit()
print("[SUCCESS] Тестовые данные успешно записаны.")
else:
print("[INFO] База данных уже содержит данные. Пропускаю инициализацию.")
def init_database():
print("[INIT] Проверка наличия рабочих баз данных...")
conn = None
try:
conn = get_db_connection()
cursor = conn.cursor()
_create_tables(cursor)
_populate_test_data(conn)
except sqlite3.Error as e:
print(f"[ERROR] Ошибка при инициализации DATABASE.DB: {e}")
if conn:
conn.rollback()
finally:
if conn:
conn.close()
master_conn = None
try:
if not MASTERDATA_PATH.exists():
print(f"[ACTION] База мастер-данных не найдена. Создаю новую: {MASTERDATA_PATH.name}")
master_conn = sqlite3.connect(MASTERDATA_PATH)
master_conn.execute("PRAGMA foreign_keys = ON;")
master_cursor = master_conn.cursor()
_create_tables(master_cursor)
_populate_test_data(master_conn)
except sqlite3.Error as e:
print(f"[ERROR] Ошибка при инициализации MASTERDATA.DB: {e}")
if master_conn:
master_conn.rollback()
finally:
if master_conn:
master_conn.close()
class Session:
def __init__(self):
pass
# --- ФУНКЦИИ ДЛЯ ИМПОРТА (лежат строго вне класса Session) ---
def list_groups():
"""Функция-заглушка для прямого импорта"""
return [(1, "Клубы"), (2, "Фанаты")]
def get_all_indicators_by_group(group_code):
"""Функция-заглушка для прямого импорта"""
mock_data = {
"KB": [(1, "ЦСКА"), (2, "Спартак"), (3, "Зенит")],
"FAN": [(4, "Мужчины"), (5, "Женщины"), (6, "Дети")]
}
return mock_data.get(group_code, [])
if __name__ == "__main__":
init_database()
#===================================================
Ниже идут файлы - доноры
#====================================================
# excel_aggregate_inter.py - файл - донор
#====================================================
from openpyxl import load_workbook
import os
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
class ExpertAggregateInterface(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
behavior = LevelTwoBehavior()
behavior.setup_level_two(self.winfo_toplevel())
self.title("Агрегирование файлов")
self.geometry("400x340")
self.resizable(False, False)
self.controller = AggregateWorkController()
main_frame = ttk.Frame(self)
main_frame.pack(padx=10, pady=10, fill="both", expand=True, anchor="center")
label_width = 15
unified_button_width = 25
field_input_width = 34
combobox_width = 30
file_create_frame = ttk.Frame(main_frame)
file_create_frame.pack(fill="x", pady=(0, 5))
ttk.Label(file_create_frame, text="Создать файл:", width=label_width, anchor="w").pack(side="left")
self.new_file_name_entry = tk.Entry(file_create_frame, width=field_input_width + 1)
self.new_file_name_entry.pack(side="left", padx=(5, 0), ipady=2)
create_file_button = ttk.Button(
file_create_frame, text="Создать", command=self.handle_create_file_request
)
create_file_button.pack(side="right", padx=(5, 0))
target_file_frame = ttk.Frame(main_frame)
target_file_frame.pack(fill="x", pady=(0, 5))
ttk.Label(target_file_frame, text="Указать путь:", width=label_width, anchor="w").pack(side="left")
self.target_file_entry = tk.Entry(target_file_frame, width=field_input_width + 1)
self.target_file_entry.pack(side="left", padx=(5, 2), ipady=2)
browse_target_button = ttk.Button(
target_file_frame, text="Обзор...", command=self.select_target_file
)
browse_target_button.pack(side="right", padx=(5, 0))
select_frame = ttk.Frame(main_frame)
select_frame.pack(fill="x", pady=(0, 15))
ttk.Label(select_frame, text="Выбрать файлы:", width=label_width, anchor="w").pack(side="left")
self.file_path_entry = tk.Entry(select_frame, width=field_input_width + 1)
self.file_path_entry.pack(side="left", padx=(5, 2), ipady=2)
browse_source_button = ttk.Button(
select_frame, text="Обзор...", command=self.select_files_for_aggregation
)
browse_source_button.pack(side="right", padx=(5, 0))
aggregate_button = ttk.Button(
main_frame,
text="Объединить информацию",
command=self.aggregate_data,
style="Big.TButton"
)
aggregate_button.pack(fill="x", pady=(10, 15))
style = ttk.Style(self)
style.configure("Big.TButton", font=('TkDefaultFont', 10, 'bold'))
self.opened_file_path_var = tk.StringVar()
self.transitivity_check_percent_var = tk.StringVar(value="")
self.transitivity_fix_percent_var = tk.StringVar(value="")
self.current_sheet_name = ""
row_5_frame = ttk.Frame(main_frame)
row_5_frame.pack(fill="x", pady=(0, 5), anchor="w")
open_file_button = ttk.Button(
row_5_frame, text="Открыть файл", command=self.select_open_target_file, width=unified_button_width
)
open_file_button.pack(side="left", padx=(0, 5))
opened_file_entry = tk.Entry(row_5_frame, textvariable=self.opened_file_path_var, state='readonly', width=field_input_width)
opened_file_entry.pack(side="left", ipady=2)
row_6_frame = ttk.Frame(main_frame)
row_6_frame.pack(fill="x", pady=(0, 5), anchor="w")
group_label_button = ttk.Button(
row_6_frame,
text="Выбрать группу:",
width=unified_button_width,
state="disabled"
)
group_label_button.pack(side="left", padx=(0, 5))
self.sheet_combobox = ttk.Combobox(row_6_frame, state="readonly", width=combobox_width)
self.sheet_combobox.pack(side="left", ipady=2)
self.sheet_combobox.bind("<<ComboboxSelected>>", self._on_sheet_select)
row_7_frame = ttk.Frame(main_frame)
row_7_frame.pack(fill="x", pady=(0, 5), anchor="w")
check_button = ttk.Button(row_7_frame, text="Проверить транзитивность", command=self.check_transitivity, width=unified_button_width)
check_button.pack(side="left", padx=(0, 10))
self.transitivity_percentage_label = tk.Label(
row_7_frame,
textvariable=self.transitivity_check_percent_var,
relief="groove",
width=8, bg="white", anchor="center"
)
self.transitivity_percentage_label.pack(side="left")
row_8_frame = ttk.Frame(main_frame)
row_8_frame.pack(fill="x", pady=(0, 5), anchor="w")
fix_button = ttk.Button(row_8_frame, text="Исправить транзитивность", command=self.apply_transitivity_corrections, width=unified_button_width)
fix_button.pack(side="left", padx=(0, 10))
self.fix_percentage_label = tk.Label(
row_8_frame,
textvariable=self.transitivity_fix_percent_var,
relief="groove",
width=8, bg="white", anchor="center"
)
self.fix_percentage_label.pack(side="left")
row_9_frame = ttk.Frame(main_frame)
row_9_frame.pack(fill="x", pady=(0, 5), anchor="w")
send_to_db_button = ttk.Button(
row_9_frame,
text="Нормализовать данные",
command=self.send_to_database,
width=unified_button_width
)
send_to_db_button.pack(side="left", padx=(0, 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:
messagebox.showinfo("Готово", f"Файл создан по пути:\n{result_path}")
def select_target_file(self):
initial_dir = str(Path(__file__).resolve().parents[2] / "experts")
file_path = filedialog.askopenfilename(title="Укажите целевой Excel-файл", initialdir=initial_dir, filetypes=[("Excel Files", "*.xlsx")])
if file_path:
self.target_file_entry.delete(0, tk.END)
self.target_file_entry.insert(0, file_path)
def select_files_for_aggregation(self):
print("[GUI SELECT] Нажат Обзор...")
try:
file_paths = filedialog.askopenfilenames(title="Выберите файлы экспертов", filetypes=[("Excel Files", "*.xlsx")])
if not file_paths: return
self.controller.input_files = list(file_paths)
first_fp = file_paths[0]
sheet_names = []
try:
temp_wb = load_workbook(first_fp, read_only=True, data_only=True)
sheet_names = temp_wb.sheetnames
temp_wb.close()
self.sheet_combobox['values'] = sheet_names
if sheet_names:
self.sheet_combobox.current(0)
self.current_sheet_name = sheet_names[0]
except Exception as e:
messagebox.showerror("Ошибка Excel", f"Не удалось прочитать листы:\n{e}")
return
display_text = f"... ({len(file_paths)} files)"
self.file_path_entry.delete(0, tk.END)
self.file_path_entry.insert(0, display_text)
except Exception as e:
print(f"[GUI FATAL ERROR]: Не удалось открыть диалоговое окно!")
print(f"Тип ошибки: {type(e).__name__}")
print(f"Описание: {e}")
messagebox.showerror("Критическая ошибка", f"Не удалось открыть проводник.\n{e}")
def _on_sheet_select(self, event):
selected = event.widget.get()
self.current_sheet_name = selected
self.reset_transitivity_field()
def reset_transitivity_field(self):
self.transitivity_check_percent_var.set("")
self.transitivity_fix_percent_var.set("")
def reset_check_field(self):
self.transitivity_check_percent_var.set("")
def reset_fix_field(self):
self.transitivity_fix_percent_var.set("")
def check_transitivity(self):
"""
ЛОГИКА ПРОВЕРКИ НАХОДИТСЯ ЗДЕСЬ.
При нажатии очищает поле ИСПРАВИТЬ.
"""
self.reset_fix_field()
selected_sheet = self.current_sheet_name
file_path = self.opened_file_path_var.get()
if not selected_sheet or not file_path:
messagebox.showwarning("Внимание", "Сначала выберите файл и лист!")
return
try:
wb = load_workbook(file_path, data_only=True)
if selected_sheet not in wb.sheetnames:
messagebox.showerror("Ошибка", "Лист не найден.")
return
ws = wb[selected_sheet]
n = 0
while True:
cell_val = ws.cell(row=2, column=4 + n).value
norm_val = ""
if cell_val is not None and str(cell_val).strip() != "":
s = str(cell_val).replace(',', '.').strip().upper()
try:
num = float(s)
norm_val = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
except ValueError:
norm_val = s
if isinstance(norm_val, (int, float)) or str(norm_val).strip() != "":
n += 1
else:
break
if n < 3:
self.transitivity_check_percent_var.set("100%")
wb.close()
return
values = [[None] * n for _ in range(n)]
for i in range(n):
for j in range(n):
cell = ws.cell(row=2 + i, column=4 + j).value
v = None
if cell is not None and str(cell).strip() != "":
s = str(cell).replace(',', '.').strip().upper()
try:
num = float(s)
v = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
except ValueError:
v = s
values[i][j] = v
total_triads = 0
consistent_triads = 0
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, b, c = values[i][j], values[j][k], values[i][k]
if all(isinstance(x, (int, float)) for x in [a, b, c]):
total_triads += 1
is_consistent = False
if a > b and b > c:
if a > c: is_consistent = True
elif a == b and b > c:
if a > c: is_consistent = True
elif a > b and b == c:
if a > c: is_consistent = True
elif a == b and b == c:
if a == c: is_consistent = True
if is_consistent:
consistent_triads += 1
wb.close()
if total_triads == 0:
percent_str = "100%"
else:
percent = int((consistent_triads / total_triads) * 100)
percent_str = f"{percent}%"
self.transitivity_check_percent_var.set(percent_str)
except Exception as e:
print(f"[GUI TRANS CHECK CRITICAL FAIL]: {e}")
self.transitivity_check_percent_var.set("Ошибка")
def apply_transitivity_corrections(self):
"""
Вызывает контроллер для правки данных.
При нажатии очищает поле ПРОВЕРИТЬ.
Берет "Старое" значение напрямую из файла перед правкой.
Если процент не вырос — сообщает эксперту о необходимости ручной переоценки.
"""
self.reset_check_field()
selected_sheet = self.current_sheet_name
file_path = self.opened_file_path_var.get()
if not selected_sheet or not file_path:
messagebox.showwarning("Внимание", "Сначала выберите файл и лист!")
return
# === 1. СЧИТЫВАЕМ РЕАЛЬНЫЙ СТАТУС "ДО" НАПРЯМУЮ ИЗ ФАЙЛА ===
old_status_raw = "Ошибка"
try:
wb_before = load_workbook(file_path, data_only=True)
ws_before = wb_before[selected_sheet]
n = 0
while True:
cell_val = ws_before.cell(row=2, column=4 + n).value
if cell_val is not None and str(cell_val).strip() != "":
n += 1
else:
break
if n < 3:
old_status_raw = "100%"
total_triads = 0
else:
values = [[None] * n for _ in range(n)]
for i in range(n):
for j in range(n):
cell = ws_before.cell(row=2 + i, column=4 + j).value
v = None
if cell is not None and str(cell).strip() != "":
s = str(cell).replace(',', '.').strip().upper()
try:
num = float(s)
v = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
except ValueError:
v = s
values[i][j] = v
total_triads = 0
consistent_triads = 0
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, b, c = values[i][j], values[j][k], values[i][k]
if all(isinstance(x, (int, float)) for x in [a, b, c]):
total_triads += 1
is_consistent = False
if a > b and b > c:
if a > c: is_consistent = True
elif a == b and b > c:
if a > c: is_consistent = True
elif a > b and b == c:
if a > c: is_consistent = True
elif a == b and b == c:
if a == c: is_consistent = True
if is_consistent:
consistent_triads += 1
if total_triads == 0:
old_status_raw = "100%"
else:
percent = int((consistent_triads / total_triads) * 100)
old_status_raw = f"{percent}%"
wb_before.close()
except Exception as e:
print(f"[GUI PRE-CHECK ERR]: {e}")
old_status_raw = "Ошибка"
# Готовим числа для сравнения
try:
old_percent = int(old_status_raw.strip('%')) if old_status_raw else 0
except ValueError:
old_percent = 0
# === 2. ЗАПУСКАЕМ ИСПРАВЛЕНИЕ ===
self.controller.current_file_path = file_path
self.controller.current_sheet_name = selected_sheet
success = self.controller.apply_transitivity_corrections()
if success:
# === 3. ПРОВЕРЯЕМ ПОСЛЕ (ЛОКАЛЬНО, БЕЗ КОНТРОЛЛЕРА) ===
current_status_raw = "Ошибка"
try:
wb_after = load_workbook(file_path, data_only=True)
ws_after = wb_after[selected_sheet]
n = 0
while True:
cell_val = ws_after.cell(row=2, column=4 + n).value
if cell_val is not None and str(cell_val).strip() != "":
n += 1
else:
break
if n < 3:
current_status_raw = "100%"
total_triads = 0
else:
values = [[None] * n for _ in range(n)]
for i in range(n):
for j in range(n):
cell = ws_after.cell(row=2 + i, column=4 + j).value
v = None
if cell is not None and str(cell).strip() != "":
s = str(cell).replace(',', '.').strip().upper()
try:
num = float(s)
v = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
except ValueError:
v = s
values[i][j] = v
total_triads = 0
consistent_triads = 0
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, b, c = values[i][j], values[j][k], values[i][k]
if all(isinstance(x, (int, float)) for x in [a, b, c]):
total_triads += 1
is_consistent = False
if a > b and b > c:
if a > c: is_consistent = True
elif a == b and b > c:
if a > c: is_consistent = True
elif a > b and b == c:
if a > c: is_consistent = True
elif a == b and b == c:
if a == c: is_consistent = True
if is_consistent:
consistent_triads += 1
if total_triads == 0:
current_status_raw = "100%"
else:
percent = int((consistent_triads / total_triads) * 100)
current_status_raw = f"{percent}%"
wb_after.close()
except Exception as e:
print(f"[GUI POST-CHECK ERR]: {e}")
current_status_raw = "Ошибка"
# Извлекаем число для логики окон
try:
current_percent = int(current_status_raw.strip('%')) if current_status_raw else 0
except ValueError:
current_percent = 0
self.transitivity_fix_percent_var.set("OK")
# ЛОГИКА СООБЩЕНИЙ (ТЕПЕРЬ OLD_PERCENT ЧЕСТНЫЙ)
if current_status_raw == "Ошибка":
messagebox.showerror("Ошибка", "Не удалось перепроверить данные после исправления.")
elif current_percent == old_percent and current_percent < 100:
# Теперь это сработает корректно
messagebox.showwarning(
"Требуется внимание эксперта",
f"Автоматическое исправление завершено.\nБыло: {old_status_raw}\nСтало: {current_status_raw}\n\n"
"Оценки экспертов содержат логические противоречия, которые нельзя исправить алгоритмически "
"без изменения смысла оценок.\nРекомендуется перепроверить матрицу вручную."
)
elif current_percent >= 95:
messagebox.showinfo(
"Отлично!",
f"Транзитивность восстановлена до максимума.\nБыло: {old_status_raw}\nСтало: {current_status_raw}"
)
else:
messagebox.showinfo(
"Готово",
f"Транзитивность исправлена.\nБыло: {old_status_raw}\nСтало: {current_status_raw}"
)
# Обновляем визуальное поле проверки актуальным значением
self.transitivity_check_percent_var.set(current_status_raw)
else:
messagebox.showerror("Ошибка", "Не удалось записать данные в файл.")
def select_open_target_file(self):
initial_dir = str(Path(__file__).resolve().parents[2] / "experts")
file_path = filedialog.askopenfilename(title="Откройте Excel-файл для обработки", initialdir=initial_dir, filetypes=[("Excel Files", "*.xlsx")])
if file_path:
self.current_file_path = file_path
self.controller.current_file_path = file_path
self.opened_file_path_var.set(file_path)
self.target_file_entry.delete(0, tk.END)
self.target_file_entry.insert(0, file_path)
try:
sheet_names = self.controller.get_sheet_names(file_path)
self.sheet_combobox['values'] = sheet_names
if sheet_names:
self.sheet_combobox.current(0)
self.current_sheet_name = sheet_names[0]
else:
self.sheet_combobox.set('')
self.current_sheet_name = ""
except Exception as e:
messagebox.showerror("Ошибка Excel", f"Не удалось прочитать структуру файла:\n{e}")
self.sheet_combobox.set('')
self.current_sheet_name = ""
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
workbook = self.controller.aggregate_data(output_file_path, input_files)
if workbook is None:
messagebox.showerror("Ошибка данных", "Не удалось собрать данные.\nСмотрите консоль.")
return
messagebox.showinfo("Готово", f"Сводный отчет успешно сохранен в:\n{output_file_path}")
def send_to_database(self):
"""
КНОПКА: Нормализовать данные.
"""
file_path = self.opened_file_path_var.get()
selected_sheet = self.current_sheet_name
if not file_path or not 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)
#===================================================
Ниже идут файлы - доноры
#====================================================
# excel_aggregate_work.py - файл - донор
#====================================================
from openpyxl import load_workbook, Workbook
import os
import re
from statistics import mean, mode, StatisticsError
from collections import defaultdict
from copy import copy
import tkinter.messagebox as messagebox
from pathlib import Path
class AggregateWorkController:
def __init__(self):
self.input_files = []
self._loaded_wbs = {}
self.current_file_path = ""
self.current_sheet_name = ""
# МЕТОД НОРМАЛИЗАЦИИ УДАЛЕН ПОЛНОСТЬЮ СОГЛАСНО ТЗ (теперь он часть normalize_group_data)
def get_sheet_names(self, file_path):
try:
wb = load_workbook(file_path, read_only=True, data_only=True)
sheet_names = wb.sheetnames
wb.close()
return sheet_names
except Exception as e:
print(f"[CTRL ERROR] Не удалось прочитать листы из {file_path}: {e}")
return []
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
try:
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
try:
os.makedirs(experts_folder, exist_ok=True)
except OSError as e:
messagebox.showerror("Ошибка доступа", f"Не удалось создать папку 'experts':\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()
messagebox.showinfo("Готово", f"Файл '{clean_filename}' успешно создан.")
return str(full_path)
except Exception as e:
print(f"[CREATE FILE ERROR]: {e}")
messagebox.showerror("Ошибка", f"Не удалось создать файл:\n{e}")
return None
def aggregate_data(self, output_file_path, input_files):
pass
def apply_transitivity_corrections(self):
"""
Исправляет нарушения нестрогой транзитивности (A>=B, B>=C -> A>=C).
Работает автономно без normalize_cell.
Матрица начинается со строки 2.
"""
print("[TRANS FIX]")
if not self.current_file_path or not self.current_sheet_name:
return False
EPSILON = 0.001
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 по СТРОКЕ 2
n = 0
while True:
cell_val = ws.cell(row=2, column=4 + n).value
if cell_val is not None and str(cell_val).strip() != "":
n += 1
else:
break
if n < 3:
return True
MATRIX_START_COL = 4
DATA_START_ROW = 2
matrix = [[None] * n for _ in range(n)]
for i in range(n):
for j in range(n):
cell = ws.cell(row=DATA_START_ROW + i, column=MATRIX_START_COL + j).value
val = None
if cell is not None and str(cell).strip() != "":
s = str(cell).replace(',', '.').strip().upper()
try:
num = float(s)
val = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
except ValueError:
val = s
matrix[i][j] = val
fixed_count = 0
# Перебираем уникальные индексы (диагональ исключена!)
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, b, c = matrix[i][j], matrix[j][k], matrix[i][k]
if all(isinstance(x, (int, float)) for x in [a, b, c]):
violation_found = False
new_c_value = c
# Нарушение прямой транзитивности >
if a > b and b > c and a <= c:
new_c_value = round(a + EPSILON, 3)
violation_found = True
# Случай равенства спереди, но финал меньше
elif a == b and b > c and a < c:
new_c_value = round(a + EPSILON, 3)
violation_found = True
# Строгое убывание, но финал догнал
elif a > b and b > c and a == c:
new_c_value = round(a + EPSILON, 3)
violation_found = True
# Полное равенство спереди, а финал ушел вниз
elif a == b and b == c and a < c:
new_c_value = round(a + EPSILON, 3)
violation_found = True
if violation_found:
ws.cell(row=DATA_START_ROW + i, column=MATRIX_START_COL + k).value = new_c_value
matrix[i][k] = new_c_value
fixed_count += 1
if fixed_count > 0:
wb.save(self.current_file_path)
print(f"[FIX SAVED] Пофиксили {fixed_count} связей.")
wb.close()
return True
except Exception as e:
print(f"[FIX LOAD/SAVE CRITICAL FAIL]: {e}")
return False
def normalize_group_data(self):
"""
Проводит нормализацию по сумме строк.
Чем выше сумма оценок показателя (строки), тем выше его итоговый вес.
Записывает результат справа от матрицы в первой свободной колонке.
Заголовки в строке 1, данные начинаются со строки 2.
"""
print("[NORMALIZE]")
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 по СТРОКЕ 2
n = 0
while True:
cell_val = ws.cell(row=2, column=4 + n).value
if cell_val is not None and str(cell_val).strip() != "":
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 = 4 + n
occupied = True
while occupied and target_col < 4 + n + 10:
header_cell = ws.cell(row=HEADER_ROW, column=target_col).value
if header_cell is None or (isinstance(header_cell, str) and "нормализ" in header_cell.lower()):
occupied = False
else:
target_col += 1
if occupied:
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):
cell = ws.cell(row=DATA_START_ROW + i, column=MATRIX_START_COL + j).value
val = 0.0
if cell is not None and str(cell).strip() != "":
s = str(cell).replace(',', '.').strip().upper()
try:
num = float(s)
val = int(num) if abs(num - int(num)) < 1e-9 else round(num, 3)
except ValueError:
val = 0.0
row_sum += val
row_sums.append(row_sum)
total_sum = sum(row_sums)
if total_sum == 0:
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=1, column=target_col).column_letter
return True, f"Веса успешно рассчитаны (колонка {col_letter})"
except Exception as e:
print(f"[NORM ERROR]: {e}")
return False, f"Ошибка при записи: {str(e)}"
#===================================================
Ниже идут файлы - доноры
#====================================================
# excel_export_inter.py - файл - донор
#====================================================
import tkinter as tk
from tkinter import ttk, messagebox
from modules.excel.excel_export_work import ExportWorkController
from modules.utils.window_behavior import LevelTwoBehavior
class ExpertExportInterface(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
behavior = LevelTwoBehavior()
behavior.setup_level_two(self.winfo_toplevel())
self.title("Создание файла для эксперта")
self.geometry("450x200")
self.resizable(False, False)
self.controller = ExportWorkController()
main_frame = ttk.Frame(self)
main_frame.pack(padx=10, pady=10, fill="both", expand=True)
ttk.Label(main_frame, text="Группа:").grid(row=0, column=0, sticky="w")
self.group_var = tk.StringVar()
self.combobox = ttk.Combobox(
main_frame, textvariable=self.group_var, state="readonly", width=25
)
self.combobox.grid(row=0, column=1, padx=(5, 10), sticky="ew")
self.refresh_groups_list()
add_group_btn = ttk.Button(
main_frame, text="Добавить группу", command=self.add_group
)
add_group_btn.grid(row=0, column=2, padx=(0, 10), sticky="e")
ttk.Label(main_frame, text="Выбранные группы:").grid(row=1, column=0, sticky="w")
self.selected_groups_text = tk.Text(main_frame, width=30, height=4)
self.selected_groups_text.grid(
row=1, column=1, columnspan=2, pady=(5, 10), sticky="ew"
)
ttk.Label(main_frame, text="Количество экспертов:").grid(
row=2, column=0, sticky="w"
)
self.num_experts = tk.IntVar(value=1)
self.experts_count_entry = ttk.Entry(main_frame, textvariable=self.num_experts, width=5)
self.experts_count_entry.grid(row=2, column=1, sticky="w")
remove_group_btn = ttk.Button(
main_frame, text="Удалить группу", command=self.remove_group
)
remove_group_btn.grid(row=2, column=2, sticky="e")
export_btn = ttk.Button(
main_frame, text="Экспортировать в Excel", command=self.export_to_excel
)
export_btn.grid(row=3, column=1, pady=(10, 0))
def refresh_groups_list(self):
self.controller.refresh_groups()
self.combobox["values"] = self.controller.groups
if self.controller.groups:
self.combobox.current(0)
def add_group(self):
group_name = self.group_var.get()
self.controller.add_group(group_name)
self._update_selected_text()
def remove_group(self):
self.controller.remove_group()
self._update_selected_text()
def _update_selected_text(self):
current_text = "\n".join(self.controller.selected_groups)
self.selected_groups_text.delete("1.0", tk.END)
self.selected_groups_text.insert(tk.END, current_text)
def export_to_excel(self):
selected_group_names = (
self.selected_groups_text.get("1.0", tk.END).strip().split("\n")
)
try:
num_experts = int(self.num_experts.get())
if num_experts <= 0:
raise ValueError
except Exception:
messagebox.showerror(
"Ошибка", "Количество экспертов должно быть положительным числом."
)
return
success = self.controller.export_to_excel(selected_group_names, num_experts)
if success:
messagebox.showinfo(
title="Экспорт завершён",
message=f"Успешно создано {num_experts} файлов.\n\nРасположение:\n.../experts/",
)
#===================================================
Ниже идут файлы - доноры
#====================================================
# excel_export_work.py - файл - донор
#====================================================
import os
from pathlib import Path
from openpyxl import Workbook
try:
from modules.data.database_module import get_db_connection
except ImportError:
print("[EXPORT WORK] Не найден database_module.")
def get_db_connection():
return None
def create_excel_template(groups_objects, output_filename):
wb = Workbook()
if "Sheet" in wb.sheetnames:
del wb["Sheet"]
created_any_sheet = False
for group_data in groups_objects:
indicators_list = group_data.get('indicators', [])
if not indicators_list:
continue
sheet_title = f"{group_data['name']}"
if len(sheet_title) > 31:
sheet_title = sheet_title[:31]
ws = wb.create_sheet(title=sheet_title)
created_any_sheet = True
# --- ЗАГОЛОВКИ ЛЕВОЙ ЧАСТИ ---
headers = ["№ п/п", "Наименование показателя", "Код"]
for col_idx, header in enumerate(headers, start=1):
ws.cell(row=1, column=col_idx, value=header)
# --- ДАННЫЕ ЛЕВОЙ ЧАСТИ ---
for idx, ind_tuple in enumerate(indicators_list, start=1):
name, code = ind_tuple[0], ind_tuple[1]
ws.cell(row=idx + 1, column=1, value=str(idx))
ws.cell(row=idx + 1, column=2, value=name)
ws.cell(row=idx + 1, column=3, value=code)
# --- ШАПКА МАТРИЦЫ (ВЕРХ) ---
matrix_start_col = 4
matrix_start_row = 1
for col_offset, ind_tuple in enumerate(indicators_list):
code = ind_tuple[1]
ws.cell(row=matrix_start_row, column=matrix_start_col + col_offset, value=code)
# --- ТЕЛО МАТРИЦЫ ---
size = len(indicators_list)
for r in range(size):
for c in range(size):
cell = ws.cell(
row=matrix_start_row + r + 1,
column=matrix_start_col + c
)
if r == c:
cell.value = 1.0
else:
cell.value = ""
if not created_any_sheet:
ws = wb.create_sheet(title="Пусто")
ws['A1'] = 'Нет индикаторов.'
try:
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
full_path = experts_folder / output_filename
wb.save(full_path)
except Exception as e:
print(f"[EXPORT WORK ERROR] {e}")
class ExportWorkController:
def __init__(self):
self.groups = []
self.selected_groups = []
def refresh_groups(self):
"""Читает названия групп из БД."""
conn = get_db_connection()
if not conn:
self.groups = ["Ошибка БД"]
return
cursor = conn.cursor()
cursor.execute("SELECT id, name FROM groups ORDER BY name")
rows = cursor.fetchall()
self.groups = [row[1] for row in rows]
# Сохраняем ID групп для экспорта
self._gid_cache = {row[1]: row[0] for row in rows}
conn.close()
# === НЕДОСТАЮЩИЕ МЕТОДЫ ДЛЯ ИНТЕРФЕЙСА ===
def add_group(self, group_name):
"""Добавляет группу в список выбранных (вызывается при нажатии кнопки)."""
if group_name and group_name.strip() and group_name not in self.selected_groups:
self.selected_groups.append(group_name)
def remove_group(self):
"""Удаляет последнюю добавленную группу (кнопка Удалить)."""
if len(self.selected_groups) > 0:
self.selected_groups.pop(-1)
def export_to_excel(self, selected_group_names, num_experts):
if int(num_experts) <= 0 or not selected_group_names:
return False
conn = get_db_connection()
if not conn:
return False
cursor = conn.cursor()
enriched_groups = []
for gname in selected_group_names:
gid = self._gid_cache.get(gname)
if not gid:
continue
cursor.execute("""
SELECT name, code
FROM indicators
WHERE group_code IN (
SELECT code FROM groups WHERE id = ?
)
ORDER BY name
""", (gid,))
indicators = cursor.fetchall()
enriched_groups.append({
'name': gname,
'indicators': indicators
})
conn.close()
if not enriched_groups:
return False
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
success = True
for expert_num in range(1, int(num_experts) + 1):
filename = f"Эксперт_{expert_num}.xlsx"
full_path = experts_folder / filename
create_excel_template(enriched_groups, str(filename))
if not full_path.exists():
success = False
return success