Загрузка данных
# Dozor/modules/excel/excel_aggregate_inter.py
import re
from tkinter import filedialog, messagebox, ttk
import tkinter as tk
from pathlib import Path
from modules.excel.excel_aggregate_work import AggregateWorkController
from modules.utils.window_behavior import LevelTwoBehavior
class ExpertAggregateInterface(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
behavior = LevelTwoBehavior()
behavior.setup_level_two(self.winfo_toplevel())
self.title("Агрегирование файлов")
# Оставляем размер из оригинала
self.geometry("470x250")
self.resizable(False, False)
self.controller = AggregateWorkController()
main_frame = ttk.Frame(self)
main_frame.pack(padx=10, pady=5, fill="both", expand=True)
# === Блок создания файла (как было раньше) ===
file_frame = ttk.Frame(main_frame)
file_frame.pack(fill="x", pady=(0, 10))
ttk.Label(file_frame, text="Название создаваемого файла:").pack(side="left")
self.new_file_name_entry = tk.Entry(file_frame, width=20)
self.new_file_name_entry.pack(side="left", padx=(5, 0), ipady=2)
create_file_button = ttk.Button(
file_frame, text="Создать файл", command=self.create_file_with_validation
)
create_button_packing_options = {"side": "right", "padx": (5, 0)}
create_file_button.pack(**create_button_packing_options)
# === Выбор файла Excel (как было раньше) ===
select_frame = ttk.Frame(main_frame)
select_frame.pack(fill="x", pady=(0, 10))
ttk.Label(select_frame, text="Выберите файл Excel:").pack(side="left")
self.file_path_entry = tk.Entry(select_frame, width=30)
self.file_path_entry.pack(side="left", padx=(5, 0), ipady=2)
browse_button = ttk.Button(
select_frame, text="Обзор...", command=self.select_file_for_aggregation
)
browse_button.pack(side="right", padx=(5, 0))
# === ПЕРЕМЕЩАЕМ СЮДА КНОПКУ ОБЪЕДИНЕНИЯ ===
aggregate_button = ttk.Button(
main_frame,
text="Объединить информацию",
command=self.aggregate_data
)
# В ДОНОРСКОМ файле она была во всю ширину и имела отступы сверху/слева
aggregate_button.pack(fill="x", pady=(5, 10), padx=(0, 0))
# === Проверка транзитивности (в точности как у вас) ===
sheet_frame = ttk.Frame(main_frame)
sheet_frame.pack(fill="x", pady=(0, 10))
# Верхняя строка: поле выбора листа
top_sheet_line = ttk.Frame(sheet_frame)
top_sheet_line.pack(fill="x")
label_sheet = ttk.Label(top_sheet_line, text="Выберите лист (группу):")
label_sheet.pack(side="left")
self.sheet_combobox = ttk.Combobox(top_sheet_line, state="readonly", width=20)
self.sheet_combobox.pack(side="left", padx=(5, 0), ipady=2)
self.sheet_combobox.bind(
"<<ComboboxSelected>>", lambda _: self.reset_transitivity_field()
)
# Нижняя строка: метка + кнопка проверки
bottom_check_line = ttk.Frame(sheet_frame)
bottom_check_line.pack(fill="x", pady=(5, 0))
check_label = ttk.Label(bottom_check_line, text="Проверить на транзитивность:")
check_label.pack(side="right", padx=(0, 5))
check_button = ttk.Button(
bottom_check_line, text="Проверить", command=self.check_transitivity
)
check_button.pack(side="right", padx=(5, 0))
# МЕТКА ПРОЦЕНТОВ (на самом правом краю, как было изначально!)
# Это атрибут! Без него не будет работать проверка
self.transitivity_percentage_label = tk.Label(
bottom_check_line,
text="", relief="groove",
width=8, bg="white"
)
self.transitivity_percentage_label.pack(side="right", padx=(5, 0))
# === НИЖНИЙ БЛОК (Кнопки действий) ===
# В вашем донорском файле они были НЕ растянутыми по ширине!
buttons_frame = ttk.Frame(main_frame)
buttons_frame.pack(fill="x", pady=15)
fix_button = ttk.Button(
buttons_frame,
text="Исправить транзитивность",
command=self.apply_transitivity_corrections,
)
# У вас был просто pack без расширений
fix_button.pack(side="left", padx=5)
send_to_db_button = ttk.Button(
buttons_frame,
text="Отправить в базу данных",
command=self.send_to_database,
)
send_to_db_button.pack(side="right", padx=5)
def create_file_with_validation(self):
filename = self.new_file_name_entry.get().strip()
invalid_chars_pattern = r'[\\/:*?"<>|]'
if not filename:
messagebox.showwarning("Предупреждение", "Имя файла не может быть пустым.")
return
if re.search(invalid_chars_pattern, filename):
messagebox.showerror("Ошибка", "Имя файла содержит недопустимые символы.")
return
reserved_names = [
"CON", "PRN", "AUX", "NUL", "COM1", "LPT1", "COM2", "LPT2",
"COM3", "LPT3", "COM4", "LPT4", "COM5", "LPT5"
]
base_name = filename.split(".")[0].upper()
if base_name in reserved_names:
messagebox.showerror("Ошибка", f"'{base_name}' — зарезервированное имя.")
return
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
full_path = experts_folder / filename
if not full_path.exists():
with open(full_path, 'w') as f:
pass
messagebox.showinfo("Готово", f"Файл '{full_path.name}' создан.")
else:
messagebox.showinfo("Готово", f"Файл '{full_path.name}' уже существует.")
def select_file_for_aggregation(self):
# askopenfilenames вернет кортеж строк
file_paths = filedialog.askopenfilenames(
title="Выберите файлы экспертов",
filetypes=[("Excel Files", "*.xlsx")]
)
if not file_paths:
return
# Сохраняем список во внутренний атрибут контроллера
self.controller.input_files = list(file_paths)
# Обновляем текстовое поле пути (покажем первый файл как пример)
display_path = str(Path(file_paths[0]).parent) + "/... (" + str(len(file_paths)) + " files)"
self.file_path_entry.delete(0, tk.END)
self.file_path_entry.insert(0, display_path)
# Пробуем загрузить листы первого файла для комбобокса
sheets = self.controller.load_sheets_from_file(file_paths[0])
self.sheet_combobox["values"] = sheets
if sheets:
self.sheet_combobox.current(0)
messagebox.showinfo("Выбор файлов", f"Выбрано {len(file_paths)} файлов.")
# === ИСПРАВЛЕНИЕ: возвращаем фокус нашему окну ===
self.lift()
self.focus_force()
def reset_transitivity_field(self):
self.transitivity_percentage_label.config(text="")
def check_transitivity(self):
file_path = self.file_path_entry.get()
selected_sheet = self.sheet_combobox.get()
if not file_path or not selected_sheet:
messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
return
result = self.controller.check_transitivity()
self.transitivity_percentage_label.config(text=result)
def apply_transitivity_corrections(self):
file_path = self.file_path_entry.get()
selected_sheet = self.sheet_combobox.get()
if not file_path or not selected_sheet:
messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
return
success = self.controller.apply_transitivity_corrections(file_path, selected_sheet)
if success:
messagebox.showinfo("Готово", "Транзитивность исправлена и файл сохранён.")
def aggregate_data(self):
output_file = self.new_file_name_entry.get().strip()
if not output_file:
messagebox.showwarning("Предупреждение", "Сначала создайте имя файла.")
return
if not output_file.endswith(".xlsx"):
output_file += ".xlsx"
# Передаем сохраненный ранее список файлов
input_files = getattr(self.controller, 'input_files', [])
success = self.controller.aggregate_data(output_file, input_files)
if success:
messagebox.showinfo("Готово", f"Данные объединены в {output_file}.")
# === ИСПРАВЛЕНИЕ: возвращаем фокус нашему окну ===
self.lift()
self.focus_force()
def send_to_database(self):
file_path = self.file_path_entry.get()
if not file_path:
messagebox.showwarning("Внимание", "Пожалуйста, выберите файл Excel!")
return
success = self.controller.send_to_database(file_path)
if success:
messagebox.showinfo("Готово", "Данные успешно отправлены в базу данных.")
# Dozor/modules/excel/excel_aggregate_work.py
import os
from pathlib import Path
from statistics import mode, StatisticsError
from openpyxl import load_workbook, Workbook
from collections import defaultdict
# --- ДОБАВЛЕНО ДЛЯ ИСПРАВЛЕНИЯ ОШИБКИ ---
try:
from tkinter import messagebox
except ImportError:
# Заглушка на случай запуска вне Tkinter
class _DummyMsg:
@staticmethod
def showwarning(*args, **kwargs): pass
@staticmethod
def showerror(*args, **kwargs): pass
@staticmethod
def showinfo(*args, **kwargs): pass
messagebox = _DummyMsg()
try:
from modules.data.database_module import get_db_connection
except ImportError:
def get_db_connection():
return None
def normalize_excel_value(value):
"""
Приводит любое значение из Excel к числу или строке для сравнения.
Убирает запятые, обрабатывает пустоту.
"""
if value is None or str(value).strip() == "":
return "EMPTY"
val_str = str(value).replace(',', '.').strip()
try:
num = float(val_str)
return num
except ValueError:
return val_str.upper()
def aggregate_matrices_by_mode(matrices):
"""
Принимает список матриц.
Возвращает одну матрицу согласно правилам:
- Нормализация через точку.
- Если мода есть (хотя бы 2 одинаковых значения) -> берем её.
- Если все три значения разные (3, 1, 0.333) -> ставим 1.
"""
if not matrices:
return []
max_rows = max(len(mat) for mat in matrices)
max_cols = max(len(row) for mat in matrices for row in mat)
result = [[None for _ in range(max_cols)] for _ in range(max_rows)]
for r in range(max_rows):
for c in range(max_cols):
cell_values = []
for matrix in matrices:
if r < len(matrix) and c < len(matrix[r]):
norm_val = normalize_excel_value(matrix[r][c])
if norm_val != "EMPTY":
cell_values.append(norm_val)
if not cell_values:
result[r][c] = ""
continue
# Считаем частоту каждого уникального значения
counts = {}
for v in cell_values:
counts[v] = counts.get(v, 0) + 1
# Находим максимальное количество повторений
max_count = max(counts.values())
# Правило: если хотя бы два значения совпали (max_count >= 2), это мода
if max_count >= 2:
candidates = [k for k, v in counts.items() if v == max_count]
final_val = candidates[0]
else:
# Правило: если все разные (например: 3.0, 1.0, 0.333) -> ставим 1
final_val = 1.0
# Если итоговое число целое (3.0), сохраняем как int для красоты
if isinstance(final_val, float) and abs(final_val - int(final_val)) < 1e-9:
result[r][c] = int(final_val)
else:
result[r][c] = round(float(final_val), 3) if isinstance(final_val, (int, float)) else final_val
return result
class AggregateWorkController:
def __init__(self):
self.input_files = []
def select_file(self):
file_paths = filedialog.askopenfilenames(
title="Выберите файлы экспертов",
filetypes=[("Excel Files", "*.xlsx")]
)
self.input_files = list(file_paths) if file_paths else []
return self.input_files
def load_sheets_from_file(self, file_path):
try:
wb = load_workbook(file_path, data_only=True)
return wb.sheetnames
except Exception as e:
print(f"[AGG CTRL] Ошибка чтения {file_path}: {e}")
return []
def check_transitivity(self):
return "N/A"
def apply_transitivity_corrections(self, file_path=None, sheet_name=None):
"""
Заглушка: просто показывает сообщение, чтобы кнопка работала.
Реальная логика правки матрицы должна быть здесь.
"""
messagebox.showinfo("Транзитивность", "Функция исправления транзитивности пока находится в разработке.")
def create_empty_excel_template(self, filename):
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
full_path = experts_folder / filename
wb = Workbook()
if "Sheet" in wb.sheetnames:
del wb["Sheet"]
wb.save(full_path)
def send_to_database(self, file_path):
pass
def aggregate_data(self, output_filename, input_files):
if not input_files or len(input_files) < 2:
messagebox.showwarning("Внимание", "Нужно выбрать минимум 2 файла.")
return False
all_matrices = []
conn = get_db_connection()
# --- ЭТАП 1: ЧТЕНИЕ ---
for file_path in input_files:
try:
wb = load_workbook(file_path, data_only=True)
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
matrix = []
for row in ws.iter_rows(min_row=2, min_col=4, values_only=True):
current_row = []
for cell in row:
current_row.append(cell)
if current_row:
matrix.append(current_row)
all_matrices.append({'name': sheet_name, 'data': matrix})
except Exception as e:
messagebox.showerror("Ошибка", f"Файл битый или защищен:\n{os.path.basename(file_path)}")
return False
# --- ЭТАП 2: ГРУППИРОВКА ПО ЛИСТАМ ---
grouped_by_sheet = defaultdict(list)
for item in all_matrices:
grouped_by_sheet[item['name']].append(item['data'])
# --- ЭТАП 3: ЗАПИСЬ ---
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
dest_path = experts_folder / output_filename
final_wb = Workbook()
if "Sheet" in final_wb.sheetnames:
del final_wb["Sheet"]
success = True
for sheet_name, matrices_list in grouped_by_sheet.items():
agg_matrix = aggregate_matrices_by_mode(matrices_list)
new_ws = final_wb.create_sheet(title=sheet_name)
# Пишем заголовки кодов (I-1, I-2...)
num_indicators = len(agg_matrix)
for col_idx in range(num_indicators):
new_ws.cell(row=1, column=col_idx + 4, value=f"I-{col_idx+1}")
# Пишем тело матрицы
for r_idx, row in enumerate(agg_matrix):
for c_idx, val in enumerate(row):
target_cell = new_ws.cell(row=r_idx + 2, column=c_idx + 4)
if val == "" or val is None:
target_cell.value = ""
elif isinstance(val, int):
target_cell.value = val
elif isinstance(val, float):
# Форматируем числа с точкой всегда
target_cell.value = round(val, 3)
else:
target_cell.value = str(val)
try:
final_wb.save(dest_path)
messagebox.showinfo("Готово", f"Сводный отчет создан:\n{dest_path}")
return True
except Exception as e:
messagebox.showerror("Ошибка сохранения", f"{e}")
return False
# Dozor/modules/excel/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/",
)
# Dozor/modules/excel/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
# Dozor/modules/excel/excel_ui.py
import tkinter as tk
from tkinter import ttk
from pathlib import Path
from modules.utils.window_behavior import LevelTwoBehavior
from modules.excel.excel_export_inter import ExpertExportInterface
from modules.excel.excel_aggregate_inter import ExpertAggregateInterface
class ExpertsFacade(tk.Toplevel):
def __init__(self, master, current_user):
super().__init__(master)
self.title("Dozor - Эксперты")
# Фиксируем минимальный размер, чтобы окно не схлопнулось при centering
self.minsize(400, 300)
self.resizable(False, False)
self.current_user = current_user
# --- СЕТКА ОКНА ---
self.grid_rowconfigure(0, weight=1) # Хедер
self.grid_rowconfigure(1, weight=5) # Тело (растягивается)
self.grid_columnconfigure(0, weight=1)
self._create_header()
self._create_body()
# Даем Tkinter время рассчитать размеры виджетов ДО перемещения окна
self.update_idletasks()
# Применяем поведение уровня 2 ПОСЛЕ создания контента
behavior = LevelTwoBehavior()
behavior.setup_level_two(self)
def _create_header(self):
header = tk.Frame(self, bg="#34495e", height=50)
header.grid(row=0, column=0, sticky="ew")
title = tk.Label(header, text="Раздел Эксперта", bg="#34495e", fg="white", font=("Arial", 16))
title.pack(side="left", padx=20, pady=10)
back_btn = tk.Button(header, text="← Назад", command=self.destroy, bg="#7f8c8d", fg="white", relief="flat", bd=0, activebackground="#95a5a6")
back_btn.pack(side="right", padx=20, pady=10)
def _create_body(self):
body = tk.Frame(self, bg="white")
body.grid(row=1, column=0, sticky="nsew", padx=30, pady=(30, 30)) # Увеличили нижний отступ для баланса
# Настройка сетки: пустое пространство сверху -> кнопка 1 -> кнопка 2 -> пустое снизу
body.grid_rowconfigure(0, weight=2) # Пустота сверху (тянется сильнее)
body.grid_rowconfigure(1, weight=1) # Кнопка экспорта
body.grid_rowconfigure(2, weight=1) # Кнопка агрегирования
body.grid_rowconfigure(3, weight=2) # Пустота снизу (тянется сильнее)
body.grid_columnconfigure(0, weight=1)
export_btn = tk.Button(
body, text="Создание Excel файлов", font=("Arial", 14), width=25, height=2,
command=self.open_export_window,
bg="#3498db", fg="white", relief="raised", bd=2, activebackground="#5dade2"
)
export_btn.grid(row=1, column=0, pady=5, sticky="ew")
aggregate_btn = tk.Button(
body, text="Агрегирование Excel файлов", font=("Arial", 14), width=25, height=2,
command=self.open_aggregate_window,
bg="#27ae60", fg="white", relief="raised", bd=2, activebackground="#52be80"
)
aggregate_btn.grid(row=2, column=0, pady=5, sticky="ew")
def open_export_window(self):
win = ExpertExportInterface(self)
self.wait_window(win)
def open_aggregate_window(self):
win = ExpertAggregateInterface(self)
self.wait_window(win)