Загрузка данных
В МЕНЕДЖЕРЕ БАЗЫ вроде бы все работает, и в то же время не работает.
После ввода названия группы она не сразу появляется.
При выборе из списка группы - список показателей не обновляется.
Бывает нажимаешь на группу в списке - она не открывается.
Проверь файлы на ошибки и дай нужные исправления.
Учти, что я вайб кодер, замену кусками не давай - желательно метод на метод.
#==============================================
# dukora\modules\manager\manager_group_panel.py
#==============================================
import customtkinter as ctk
from tkinter import ttk, messagebox
from modules.manager.manager_group_work import list_groups, add_group, edit_group, delete_group
class ManagerGroupPanel(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master)
self._current_editing_id = None # Для хранения ID редактируемой группы
# === ГЛАВНАЯ СЕТКА ОКНА ===
self.grid_columnconfigure(0, weight=2)
self.grid_columnconfigure(1, weight=1)
self.grid_columnconfigure(2, weight=1)
self.grid_columnconfigure(3, weight=3)
self.grid_rowconfigure(0, weight=0)
self.grid_rowconfigure(1, weight=1)
# === 1. ЛЕВАЯ ЧАСТЬ: ФОРМА ВВОДА ===
form_frame = ctk.CTkFrame(self, corner_radius=10)
form_frame.grid(row=0, column=0, sticky="nsew", padx=(10, 5), pady=10)
form_frame.grid_columnconfigure(0, weight=0)
form_frame.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(form_frame, text="Название:", anchor="w").grid(row=0, column=0, sticky="w", padx=5, pady=(5, 0))
self.name_entry = ctk.CTkEntry(form_frame)
self.name_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
ctk.CTkLabel(form_frame, text="Код:", anchor="w").grid(row=1, column=0, sticky="w", padx=5, pady=(5, 0))
self.code_entry = ctk.CTkEntry(form_frame)
self.code_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
gain_frame = ctk.CTkFrame(form_frame)
gain_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
ctk.CTkLabel(gain_frame, text="Усиление:").pack(side="left")
self.gain_var = ctk.StringVar(value="1.0")
ctk.CTkEntry(gain_frame, textvariable=self.gain_var, width=60).pack(side="left", padx=(5, 0))
# === 2. ПРАВАЯ ЧАСТЬ: КНОПКИ УПРАВЛЕНИЯ ===
action_frame = ctk.CTkFrame(self)
action_frame.grid(row=0, column=2, sticky="n", padx=5, pady=10)
action_frame.grid_columnconfigure(0, weight=1)
# Кнопка Сохранить (создаем сразу, но прячем)
self.save_btn = ctk.CTkButton(action_frame, text="Сохранить", fg_color="#8B4513", hover_color="#A0522D", command=self.save_group, state="disabled")
self.save_btn.pack(fill="x", padx=5, pady=5)
# Остальные кнопки
ctk.CTkButton(action_frame, text="Добавить", fg_color="#3B82F6", command=self.add_group).pack(fill="x", padx=5, pady=5)
ctk.CTkButton(action_frame, text="Редактировать", fg_color="#10B981", command=self.edit_group_select).pack(fill="x", padx=5, pady=5)
ctk.CTkButton(action_frame, text="Удалить", fg_color="#EF4444", command=self.delete_group).pack(fill="x", padx=5, pady=5)
# === 3. НИЖНЯЯ ЧАСТЬ: ТАБЛИЦА ===
tree_container = ctk.CTkFrame(self)
tree_container.grid(row=1, column=0, columnspan=4, sticky="nsew", padx=10, pady=(0, 10))
tree_container.grid_columnconfigure(0, weight=1)
tree_container.grid_rowconfigure(0, weight=1)
columns = ("code", "name", "gain")
self.tree = ttk.Treeview(tree_container, columns=columns, show="headings")
self.tree.heading("code", text="Код"); self.tree.column("code", width=12, anchor="center")
self.tree.heading("gain", text="Усиление"); self.tree.column("gain", width=7, anchor="center")
self.tree.heading("name", text="Название"); self.tree.column("name", anchor="w")
self.tree.pack(fill="both", expand=True, side="left")
scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.tree.yview)
scrollbar.pack(side="right", fill="y")
self.tree.configure(yscrollcommand=scrollbar.set)
self.update_tree()
# --- МЕТОДЫ ПОДДЕРЖКИ ---
def update_tree(self):
for item in self.tree.get_children():
self.tree.delete(item)
try:
groups = list_groups()
for group in groups:
gain_text = f"{group.gain_group:.1f}" if group.gain_group is not None else ""
self.tree.insert("", "end", iid=str(group.id), values=(group.code, group.name, gain_text))
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
def get_selected(self):
selected = self.tree.selection()
if not selected:
return None
code, name, gain = self.tree.item(selected[0])["values"]
db_id_str = selected[0]
try:
db_id = int(db_id_str)
except ValueError:
return None
return {"id": db_id, "code": code, "name": name, "gain": gain}
def clear_form(self):
self.name_entry.delete(0, "end")
self.code_entry.delete(0, "end")
self.gain_var.set("1.0")
self.code_entry.configure(state="normal") # Разблокируем при очистке
self.reset_ui_mode()
def reset_ui_mode(self):
"""Возвращает UI в состояние 'по умолчанию'"""
self._current_editing_id = None
self.save_btn.configure(state="disabled")
# Показываем Редактировать обратно
for child in self.save_btn.master.winfo_children():
if isinstance(child, ctk.CTkButton) and child.cget("text") == "Редактировать":
child.pack(fill="x", padx=5, pady=5)
self.save_btn.pack_forget() # Прячем Сохранить
# --- ЛОГИКА КНОПОК ---
def add_group(self):
name = self.name_entry.get().strip()
code = self.code_entry.get().strip()
try:
gain_val = float(self.gain_var.get().replace(',', '.'))
except ValueError:
gain_val = 0.0
if not all([name, code]):
messagebox.showwarning("Ошибка", "Заполните название и код.")
return
try:
add_group(name, code, gain_val)
messagebox.showinfo("Успех", "Группа добавлена.")
self.update_tree(); self.clear_form()
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
def edit_group_select(self):
data = self.get_selected()
if not data:
messagebox.showwarning("Ошибка", "Выберите группу."); return
# Заполняем форму для редактирования
self.name_entry.delete(0, "end"); self.name_entry.insert(0, data['name'])
self.code_entry.delete(0, "end"); self.code_entry.insert(0, data['code'])
# Блокируем Код
self.code_entry.configure(state="disabled")
# Усиление
val = data['gain']
self.gain_var.set(val if val else "1.0")
# Меняем режим интерфейса
self._current_editing_id = data['id']
# Скрываем кнопку Редактировать и показываем Сохранить
for child in self.save_btn.master.winfo_children():
if isinstance(child, ctk.CTkButton) and child.cget("text") == "Редактировать":
child.pack_forget()
self.save_btn.configure(state="normal")
self.save_btn.pack(fill="x", padx=5, pady=5)
def save_group(self):
if not self._current_editing_id:
return
name = self.name_entry.get().strip()
# Код берем из поля, даже если оно disabled, либо храним его отдельно
current_data = self.get_selected()
if not current_data: return
code = current_data['code']
# Безопасный парсинг усиления
try:
gain_val = float(self.gain_var.get().replace(',', '.'))
except ValueError:
messagebox.showwarning("Ошибка", "Усиление должно быть числом. Значение сброшено.")
self.gain_var.set("1.0")
return
if not name:
messagebox.showwarning("Ошибка", "Название не может быть пустым."); return
try:
edit_group(self._current_editing_id, name, code, gain_val)
messagebox.showinfo("Готово", "Группа обновлена.")
self.update_tree()
self.clear_form()
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
def delete_group(self):
data = self.get_selected()
if not data:
messagebox.showwarning("Ошибка", "Выберите группу."); return
if messagebox.askyesno("Подтверждение", f"Удалить группу {data['name']}?"):
try:
delete_group(data["id"])
messagebox.showinfo("Успех", "Группа удалена.")
self.update_tree(); self.clear_form()
except Exception as e:
messagebox.showerror("Ошибка БД", str(e))
#===========================================================
# ==============================================
# dukora/modules/manager/manager_group_work.py
# ==============================================
import sqlite3
from typing import List
from contextlib import contextmanager
from modules.data.database_module import get_db_connection
class GroupDTO:
def __init__(self, id: int, code: str, name: str, gain_group: float = None):
self.id = id
self.code = code
self.name = name
self.gain_group = gain_group
@contextmanager
def _get_cursor():
conn = get_db_connection()
try:
cursor = conn.cursor()
yield cursor
# Принудительная фиксация СРАЗУ ЖЕ
conn.commit()
except Exception:
conn.rollback()
raise
finally:
# Принудительное закрытие соединения, чтобы сбросить все локи
conn.close()
def list_groups() -> List[GroupDTO]:
with _get_cursor() as 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("SELECT id, code, name, gain_group FROM groups ORDER BY name")
rows = cursor.fetchall()
return [GroupDTO(id=row[0], code=row[1], name=row[2], gain_group=row[3]) for row in rows]
def add_group(name: str, code: str, gain_group: float = 0.0) -> GroupDTO:
with _get_cursor() as cursor:
cursor.execute(
"INSERT INTO groups (name, code, gain_group) VALUES (?, ?, ?)",
(name, code, gain_group)
)
new_id = cursor.lastrowid
return GroupDTO(id=new_id, code=code, name=name, gain_group=gain_group)
def edit_group(group_id: int, new_name: str, new_code: str, new_gain: float):
conn = get_db_connection()
cursor = conn.cursor()
try:
cursor.execute(
"UPDATE groups SET name = ?, code = ?, gain_group = ? WHERE id = ?",
(new_name, new_code, new_gain, group_id)
)
if cursor.rowcount == 0:
raise Exception("Запись не обновлена. Проверьте уникальность кода/названия.")
conn.commit()
except sqlite3.IntegrityError as e:
conn.rollback()
raise Exception(f"Нарушение целостности данных: {e}")
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
def delete_group(group_id: int):
with _get_cursor() as cursor:
cursor.execute("DELETE FROM groups WHERE id = ?", (group_id,))
#==============================================
# ==============================================
# dukora/modules/manager/manager_metrics_panel.py
# ==============================================
import customtkinter as ctk
from tkinter import ttk, messagebox, filedialog
import tkinter as tk # Импортируем базовый tkinter для Text виджета
import pyperclip
from docx import Document
from modules.manager.manager_metrics_work import (
get_all_indicators_by_group, insert_indicator, delete_indicator_by_code,
IndicatorDTO, generate_unique_code, save_docx_template, update_indicator_name
)
from modules.manager.manager_group_work import list_groups as group_list
class ManagerMetricsPanel(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master)
self.parent_main = master
self.group_names_to_codes = {}
self._current_editing_code = None
# Переменная для хранения активного тултипа, чтобы не плодить их и иметь возможность закрыть
self._active_tooltip = None
# === СЕТКА ОКНА ===
self.grid_columnconfigure(0, weight=1); self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=0); self.grid_rowconfigure(1, weight=0)
self.grid_rowconfigure(2, weight=0); self.grid_rowconfigure(3, weight=10)
self.grid_rowconfigure(4, weight=0)
# === СТРОКА 0: ВЫБОР ГРУППЫ ===
ctk.CTkLabel(self, text="Группа:").grid(row=0, column=0, sticky="w", padx=(10, 5), pady=(10, 0))
self.group_combobox = ctk.CTkComboBox(self, state="readonly")
self.group_combobox.grid(row=0, column=0, columnspan=2, sticky="ew", padx=10, pady=(10, 0))
# === СТРОКА 1: ПОЛЕ ВВОДА НАЗВАНИЯ (БЕЛЫЙ ЦВЕТ) ===
input_frame = ctk.CTkFrame(self)
input_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=10, pady=(5, 10))
input_frame.grid_columnconfigure(0, weight=1)
input_frame.grid_columnconfigure(1, weight=0)
ctk.CTkLabel(input_frame, text="Название показателя:", anchor="w").grid(row=0, column=0, sticky="w")
# Поле сделано БЕЛЫМ с черным текстом для удобства чтения
self.name_entry = tk.Text(
input_frame, height=4, width=1, wrap="word", bd=1, relief="solid",
bg="white", fg="black", insertbackground="black", font=("Segoe UI", 10)
)
self.name_entry.grid(row=1, column=0, sticky="ew", padx=(0, 5), pady=5)
# Разрешаем правый клик мыши
self.name_entry.bind("<Button-3>", lambda e: "break")
# Разрешаем вставку через Ctrl+V / Shift+Insert
self.name_entry.bind("<Control-v>", lambda e: self._paste_via_pyperclip())
self.name_entry.bind("<Shift-Insert>", lambda e: self._paste_via_pyperclip())
paste_btn = ctk.CTkButton(input_frame, text="Вставить", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self._paste_via_pyperclip)
paste_btn.grid(row=1, column=1, sticky="ns", padx=(5, 0), pady=5, ipadx=10)
# === СТРОКА 2: КНОПКИ УПРАВЛЕНИЯ (ОДНА СТРОКА, БЕЗ СКРОЛЛА) ===
action_frame = ctk.CTkFrame(self)
action_frame.grid(row=2, column=0, columnspan=2, sticky="ew", padx=10, pady=5)
# Распределяем пространство поровну между 4 кнопками
for i in range(4):
action_frame.grid_columnconfigure(i, weight=1, uniform="actions")
ctk.CTkButton(action_frame, text="Добавить", fg_color="#3B82F6", command=self.add_indicator).pack(side="left", fill="both", expand=True, padx=2, pady=2)
ctk.CTkButton(action_frame, text="Редактировать", fg_color="#10B981", command=self.edit_indicator_select).pack(side="left", fill="both", expand=True, padx=2, pady=2)
# Изменен цвет на коричневый (#8B4513 - SaddleBrown)
self.save_btn = ctk.CTkButton(action_frame, text="Сохранить", state="disabled", fg_color="#8B4513", hover_color="#A0522D", command=self.update_indicator_name)
self.save_btn.pack(side="left", fill="both", expand=True, padx=2, pady=2)
ctk.CTkButton(action_frame, text="Удалить", fg_color="#EF4444", command=self.delete_indicator).pack(side="left", fill="both", expand=True, padx=2, pady=2)
# === СТРОКА 3: ТАБЛИЦА ДАННЫХ ===
tree_container = ctk.CTkFrame(self, fg_color="transparent")
tree_container.grid(row=3, column=0, columnspan=2, sticky="nsew", padx=10, pady=(0, 10))
tree_container.grid_columnconfigure(0, weight=1); tree_container.grid_rowconfigure(0, weight=1)
style = ttk.Style()
try:
theme_name = "clam" if "dark" in ctk.get_appearance_mode() else "default"
style.theme_use(theme_name)
style.configure("Custom.Treeview", background="#2b2b2b", fieldbackground="#2b2b2b", foreground="white", rowheight=30)
style.configure("Custom.Treeview.Heading", background="#34495e", foreground="white")
style.map("Custom.Treeview", background=[("selected", "#3c5a72")])
except Exception: pass
columns = ("number", "name", "code")
self.indicators_tree = ttk.Treeview(tree_container, columns=columns, show="headings", selectmode="browse", style="Custom.Treeview")
self.indicators_tree.grid(row=0, column=0, sticky="nsew")
scrollbar = ctk.CTkScrollbar(tree_container, orientation="vertical", command=self.indicators_tree.yview)
scrollbar.grid(row=0, column=1, sticky="ns", padx=(0, 2))
self.indicators_tree.configure(yscrollcommand=scrollbar.set)
self.indicators_tree.heading("number", text="№"); self.indicators_tree.column("number", width=50, anchor="center", stretch=False)
self.indicators_tree.heading("name", text="Название");
self.indicators_tree.column("name", width=0, minwidth=250, anchor="w", stretch=True)
self.indicators_tree.heading("code", text="Код"); self.indicators_tree.column("code", width=110, anchor="center", stretch=False)
self.indicators_tree.bind("<Button-1>", self._on_tree_click, add="+")
# === СТРОКА 4: ЭКСПОРТ ===
bottom_frame = ctk.CTkFrame(self)
bottom_frame.grid(row=4, column=0, columnspan=2, sticky="e", padx=10, pady=(0, 10))
ctk.CTkButton(bottom_frame, text="Экспорт в WORD", fg_color="#2B2B2B", hover_color="#3C3C3C", command=self.print_to_docx).pack(side="right", padx=5)
# Закрытие тултипа при потере фокуса окном (Задача 3)
self.bind("<FocusOut>", self._on_window_focus_out)
self.update_group_combobox()
self.update_indicators_list()
# --- МЕТОДЫ ПОДДЕРЖКИ ---
def _paste_via_pyperclip(self, event=None):
try:
clipboard_text = pyperclip.paste()
if clipboard_text:
self.name_entry.delete("sel.first", "sel.last")
self.name_entry.insert("insert", clipboard_text)
except Exception as e:
pass
return None
def update_group_combobox(self):
groups = group_list(); self.group_names_to_codes.clear()
for g in groups: self.group_names_to_codes[g.name] = getattr(g, 'code', '')
names = [g.name for g in groups]; self.group_combobox.configure(values=names)
if names: self.group_combobox.set(names[0]); self.update_indicators_list()
def update_indicators_list(self, event=None):
selected_group_code = self.group_names_to_codes.get(self.group_combobox.get())
indicators = get_all_indicators_by_group(selected_group_code) if selected_group_code else []
for item in self.indicators_tree.get_children(): self.indicators_tree.delete(item)
last_iid = None
for idx, ind in enumerate(indicators, start=1):
val_name = getattr(ind, 'name', '--')
val_code = getattr(ind, 'code', '--')
last_iid = val_code
self.indicators_tree.insert("", "end", iid=val_code, values=(f"{idx}.", val_name, val_code))
# Прокрутка к последнему добавленному элементу (Задача 4)
if last_iid:
self.indicators_tree.see(last_iid)
# --- ЛОГИКА КНОПОК И ИНТЕРАКТИВА ---
def add_indicator(self):
group_code = self.group_names_to_codes.get(self.group_combobox.get())
indicator_name = self.name_entry.get("1.0", "end").rstrip("\n").strip()
if not group_code or not indicator_name: messagebox.showwarning("Ошибка", "Выберите группу и введите название!"); return
new_code = generate_unique_code(group_code)
try:
insert_indicator(new_code, indicator_name, group_code)
messagebox.showinfo("Готово", "Индикатор добавлен."); self.update_indicators_list(); self.name_entry.delete("1.0", "end")
except Exception as e: messagebox.showerror("Ошибка", str(e))
def edit_indicator_select(self):
selected = self.indicators_tree.selection()
if not selected: messagebox.showwarning("Ошибка", "Выберите показатель в таблице."); return
code = selected[0]
item = self.indicators_tree.item(code)
current_name = item["values"][1].replace('\n', ' ')
self.name_entry.delete("1.0", "end"); self.name_entry.insert("1.0", current_name)
self._current_editing_code = code
self.save_btn.configure(state="normal")
def update_indicator_name(self):
if not self._current_editing_code: messagebox.showwarning("Ошибка", "Сначала выберите элемент через 'Редактировать'."); return
new_name = self.name_entry.get("1.0", "end").rstrip("\n").strip()
if not new_name: messagebox.showwarning("Ошибка", "Название не может быть пустым."); return
success = update_indicator_name(self._current_editing_code, new_name)
if success:
messagebox.showinfo("Готово", "Название обновлено.")
self.update_indicators_list()
self.save_btn.configure(state="disabled"); delattr(self, '_current_editing_code')
else: messagebox.showerror("Ошибка БД", "Не удалось обновить данные.")
def delete_indicator(self):
selected = self.indicators_tree.selection()
if not selected: messagebox.showwarning("Ошибка", "Выберите показатель."); return
code = selected[0]
if messagebox.askyesno("Подтверждение", f"Удалить этот показатель?"):
try:
delete_indicator_by_code(code)
messagebox.showinfo("Готово", "Удалено."); self.update_indicators_list(); self.name_entry.delete("1.0", "end")
except Exception as e: messagebox.showerror("Ошибка", str(e))
def _on_tree_click(self, event):
region = self.indicators_tree.identify_region(event.x, event.y)
if region == "cell":
column_id = self.indicators_tree.identify_column(event.x)
if column_id == "#2":
item_id = self.indicators_tree.identify_row(event.y)
if item_id:
full_text = self.indicators_tree.item(item_id)['values'][1]
# Задача 3: Если тултип уже открыт — убиваем его перед созданием нового
if self._active_tooltip and self._active_tooltip.winfo_exists():
self._active_tooltip.destroy()
tooltip = ctk.CTkToplevel(self)
self._active_tooltip = tooltip
tooltip.attributes("-topmost", True)
tooltip.overrideredirect(True)
x = self.winfo_rootx() + event.x + 15
y = self.winfo_rooty() + event.y + 15
tooltip.geometry(f"+{x}+{y}")
label = ctk.CTkLabel(tooltip, text=full_text, wraplength=400, justify="left")
label.pack(padx=10, pady=10)
def on_leave(_):
if self._active_tooltip and self._active_tooltip.winfo_exists():
self._active_tooltip.destroy()
self._active_tooltip = None
tooltip.bind("<Leave>", on_leave)
def keep_open(_): return "break"
tooltip.bind("<Button-1>", keep_open)
def print_to_docx(self):
group_name = self.group_combobox.get()
if not group_name: messagebox.showwarning("Ошибка", "Выберите группу."); return
group_code = self.group_names_to_codes.get(group_name)
indicators = get_all_indicators_by_group(group_code)
if not indicators: messagebox.showwarning("Ошибка", "Нет данных для экспорта."); return
from datetime import date
filename = f"{group_name.replace(' ', '_')}_{date.today().strftime('%Y-%m-%d')}.docx"
path = filedialog.asksaveasfilename(initialfile=filename, defaultextension=".docx", filetypes=[("Word files", "*.docx"), ("All files", "*.*")])
if not path: return
success = save_docx_template(group_name, indicators, path)
if success: messagebox.showinfo("Успех", f"Сохранено:\n{path}")
else: messagebox.showerror("Ошибка записи", "Проверьте консоль или права доступа к папке.")
def _on_window_focus_out(self, event):
"""Закрывает всплывающую подсказку, если фокус ушел из основного окна."""
if self._active_tooltip and self._active_tooltip.winfo_exists():
self._active_tooltip.destroy()
self._active_tooltip = None
def on_close(self):
# Чистим за собой при закрытии панели
if self._active_tooltip and self._active_tooltip.winfo_exists():
self._active_tooltip.destroy()
self.destroy()
#==============================================
# ==============================================
# dukora/modules/manager/manager_metrics_work.py
# ==============================================
import os
import sqlite3
from typing import List, Any
from contextlib import contextmanager
from modules.data.database_module import get_db_connection
from typing import List, Any
class IndicatorDTO:
def __init__(self, id: int, code: str, name: str, group_code: str):
self.id = id
self.code = code
self.name = name
self.group_code = group_code
@contextmanager
def _get_cursor():
conn = get_db_connection()
try:
cursor = conn.cursor()
yield cursor
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def insert_indicator(code: str, name: str, group_code: str) -> IndicatorDTO:
print(f"[WORK LOGIC] INSERT | Код: {code}, Имя: {name}")
with _get_cursor() as cursor:
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
)
''')
cursor.execute("INSERT INTO indicators (code, name, group_code) VALUES (?, ?, ?)",
(code, name, group_code))
new_id = cursor.lastrowid
return IndicatorDTO(id=new_id, code=code, name=name, group_code=group_code)
def delete_indicator_by_code(code: str):
print(f"[WORK LOGIC] DELETE | Код: {code}")
with _get_cursor() as cursor:
cursor.execute("DELETE FROM indicators WHERE code = ?", (code,))
def get_all_indicators_by_group(group_code: str) -> List[IndicatorDTO]:
print(f"[WORK LOGIC] SELECT | Группа: {group_code}")
with _get_cursor() as cursor:
cursor.execute("SELECT id, code, name, group_code FROM indicators WHERE group_code = ?", (group_code,))
rows = cursor.fetchall()
return [IndicatorDTO(id=row[0], code=row[1], name=row[2], group_code=row[3]) for row in rows]
# === ИСПРАВЛЕННАЯ ФУНКЦИЯ ОБНОВЛЕНИЯ ДЛЯ SQLITE ===
def update_indicator_name(code: str, new_name: str) -> bool:
"""
Обновляет название индикатора по его уникальному коду через SQLite.
"""
try:
with _get_cursor() as cursor:
# Сначала проверяем, существует ли такой индикатор
cursor.execute("SELECT name FROM indicators WHERE code = ?", (code,))
result = cursor.fetchone()
if not result:
print(f"[DB] Индикатор с кодом {code} не найден.")
return False
old_name = result[0]
# Выполняем обновление
cursor.execute("UPDATE indicators SET name = ? WHERE code = ?", (new_name.strip(), code))
# commit произойдет автоматически благодаря @contextmanager
print(f"[DB] Название обновлено: '{old_name}' -> '{new_name.strip()}'")
return True
except Exception as e:
# Rollback сработает автоматически благодаря декораторам контекста
print(f"[DB FATAL] Ошибка при обновлении индикатора: {repr(e)}")
return False
def generate_unique_code(group_code: str) -> str:
"""
Генерирует уникальный код показателя внутри группы по шаблону ГРУППА-N.
Например: Эко-1, Эко-2, ИТ-1.
"""
with _get_cursor() as cursor:
cursor.execute("""
SELECT MAX(CAST(substr(code, instr(code, '-') + 1) AS INTEGER))
FROM indicators WHERE code LIKE ?
""", (f"{group_code}-%",))
row = cursor.fetchone()[0]
if row is None:
next_num = 1
else:
next_num = row + 1
return f"{group_code}-{next_num}"
def save_docx_template(group_name: str, indicators: List[Any], path: str) -> bool:
if not indicators:
return False
try:
from docx import Document
from docx.shared import Cm, Pt
from docx.enum.section import WD_ORIENTATION
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
doc = Document()
# --- СТРАНИЦА ---
section = doc.sections[0]
section.orientation = WD_ORIENTATION.LANDSCAPE
section.page_width = Cm(29.7)
section.page_height = Cm(21.0)
section.left_margin = Cm(3.0)
section.right_margin = Cm(2.0)
section.top_margin = Cm(2.0)
section.bottom_margin = Cm(2.0)
# --- СТИЛЬ ---
style = doc.styles['Normal']
font = style.font
font.name = 'Times New Roman'
font.size = Pt(12)
# --- ЗАГОЛОВОК ---
p = doc.add_paragraph(f'Показатели группы: {group_name}')
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.runs[0].font.bold = True
p.runs[0].font.size = Pt(14)
doc.add_paragraph(' ')
# --- ТАБЛИЦА ---
table = doc.add_table(rows=len(indicators) + 1, cols=3)
table.style = 'Table Grid'
table.autofit = False
table.alignment = WD_TABLE_ALIGNMENT.CENTER
col_widths_cm = [2.0, 2.0, 20.5]
for i in range(3):
for cell in table.columns[i].cells:
cell.width = Cm(col_widths_cm[i])
# Шапка
hdr_cells = table.rows[0].cells
titles = ['№ п/п', 'Код', 'Наименование']
for i in range(3):
hdr_cells[i].text = titles[i]
para = hdr_cells[i].paragraphs[0]
para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = para.runs[0]
run.bold = True
run.font.size = Pt(14)
run.font.name = 'Times New Roman'
# Данные
for idx, ind in enumerate(indicators, start=1):
row_cells = table.rows[idx].cells
for i, cell in enumerate(row_cells):
cell.width = Cm(col_widths_cm[i])
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.TOP
row_cells[0].text = str(idx)
row_cells[1].text = ind.code
row_cells[2].text = ind.name.replace('\\n', '\n')
for cell in row_cells:
for para in cell.paragraphs:
for run in para.runs:
run.font.name = 'Times New Roman'
run.font.size = Pt(12)
doc.save(path)
return True
except Exception as e:
print(f"[WORK LOGIC FATAL] Ошибка: {repr(e)}")
return False
#==============================================
#==============================================
# dukora/modules/data/database_module.py
#==============================================
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()