from tkinter import messagebox import customtkinter as ctk from app.database import get_connection class TransactionsFrame(ctk.CTkFrame): def __init__(self, master, refresh_callback) -> None: super().__init__(master) self.refresh_callback = refresh_callback self.account_map: dict[str, int] = {} self.category_map: dict[str, int] = {} self.grid_columnconfigure(1, weight=1) self.grid_rowconfigure(0, weight=1) self._build_form() self._build_list() def _build_form(self) -> None: form_frame = ctk.CTkFrame(self, corner_radius=18) form_frame.grid(row=0, column=0, sticky="ns", padx=(8, 12), pady=8) title = ctk.CTkLabel( form_frame, text="Операции", font=ctk.CTkFont(size=26, weight="bold"), ) title.pack(anchor="w", padx=18, pady=(18, 6)) subtitle = ctk.CTkLabel( form_frame, text="Добавляйте доходы и расходы по категориям.", text_color="gray75", ) subtitle.pack(anchor="w", padx=18, pady=(0, 18)) self.title_entry = ctk.CTkEntry( form_frame, width=300, placeholder_text="Название", ) self.title_entry.pack(padx=18, pady=8) self.amount_entry = ctk.CTkEntry( form_frame, width=300, placeholder_text="Сумма", ) self.amount_entry.pack(padx=18, pady=8) self.type_menu = ctk.CTkOptionMenu( form_frame, width=300, values=["Расход", "Доход"], command=self._on_type_change, ) self.type_menu.set("Расход") self.type_menu.pack(padx=18, pady=8) self.account_menu = ctk.CTkOptionMenu(form_frame, width=300, values=["Нет счетов"]) self.account_menu.pack(padx=18, pady=8) self.category_menu = ctk.CTkOptionMenu( form_frame, width=300, values=["Нет категорий"], ) self.category_menu.pack(padx=18, pady=8) add_button = ctk.CTkButton( form_frame, text="Добавить операцию", width=300, height=42, command=self.add_transaction, ) add_button.pack(padx=18, pady=(12, 18)) def _build_list(self) -> None: self.list_frame = ctk.CTkScrollableFrame(self, label_text="История операций") self.list_frame.grid(row=0, column=1, sticky="nsew", padx=(0, 8), pady=8) def _on_type_change(self, _value: str) -> None: self._load_category_menu() def _load_account_menu(self) -> None: with get_connection() as connection: rows = connection.execute( "SELECT id, name FROM accounts ORDER BY name" ).fetchall() if not rows: self.account_map = {} self.account_menu.configure(values=["Нет счетов"]) self.account_menu.set("Нет счетов") return values = [] self.account_map = {} for row in rows: label = f"{row['name']} (ID {row['id']})" values.append(label) self.account_map[label] = row["id"] self.account_menu.configure(values=values) self.account_menu.set(values[0]) def _load_category_menu(self) -> None: transaction_type = self.type_menu.get() with get_connection() as connection: rows = connection.execute( """ SELECT id, name FROM categories WHERE category_type = ? ORDER BY name """, (transaction_type,), ).fetchall() if not rows: self.category_map = {} self.category_menu.configure(values=["Нет категорий"]) self.category_menu.set("Нет категорий") return values = [] self.category_map = {} for row in rows: label = f"{row['name']} (ID {row['id']})" values.append(label) self.category_map[label] = row["id"] self.category_menu.configure(values=values) self.category_menu.set(values[0]) def add_transaction(self) -> None: title = self.title_entry.get().strip() amount_text = self.amount_entry.get().strip().replace(",", ".") transaction_type = self.type_menu.get() account_label = self.account_menu.get() category_label = self.category_menu.get() if not title or not amount_text: messagebox.showerror("Ошибка", "Заполните все поля.") return if account_label not in self.account_map: messagebox.showerror("Ошибка", "Сначала добавьте хотя бы один счёт.") return if category_label not in self.category_map: messagebox.showerror("Ошибка", "Сначала создайте подходящую категорию.") return try: amount = float(amount_text) except ValueError: messagebox.showerror("Ошибка", "Сумма должна быть числом.") return if amount <= 0: messagebox.showerror("Ошибка", "Сумма должна быть больше нуля.") return account_id = self.account_map[account_label] category_id = self.category_map[category_label] balance_delta = -amount if transaction_type == "Расход" else amount with get_connection() as connection: connection.execute( """ INSERT INTO transactions ( title, amount, transaction_type, account_id, category_id ) VALUES (?, ?, ?, ?, ?) """, (title, amount, transaction_type, account_id, category_id), ) connection.execute( "UPDATE accounts SET balance = balance + ? WHERE id = ?", (balance_delta, account_id), ) connection.commit() self.title_entry.delete(0, "end") self.amount_entry.delete(0, "end") self.refresh_callback() def delete_transaction(self, transaction_id: int) -> None: with get_connection() as connection: row = connection.execute( """ SELECT amount, transaction_type, account_id FROM transactions WHERE id = ? """, (transaction_id,), ).fetchone() if row is None: return reverse_delta = ( row["amount"] if row["transaction_type"] == "Расход" else -row["amount"] ) connection.execute( "UPDATE accounts SET balance = balance + ? WHERE id = ?", (reverse_delta, row["account_id"]), ) connection.execute("DELETE FROM transactions WHERE id = ?", (transaction_id,)) connection.commit() self.refresh_callback() def refresh_data(self) -> None: self._load_account_menu() self._load_category_menu() for widget in self.list_frame.winfo_children(): widget.destroy() with get_connection() as connection: rows = connection.execute( """ SELECT t.id, t.title, t.amount, t.transaction_type, t.created_at, a.name AS account_name, c.name AS category_name FROM transactions AS t JOIN accounts AS a ON a.id = t.account_id JOIN categories AS c ON c.id = t.category_id ORDER BY t.id DESC """ ).fetchall() if not rows: empty_label = ctk.CTkLabel( self.list_frame, text="Операций пока нет.", text_color="gray70", ) empty_label.pack(anchor="w", padx=10, pady=10) return for row in rows: card = ctk.CTkFrame(self.list_frame, corner_radius=16) card.pack(fill="x", padx=6, pady=6) title = ctk.CTkLabel( card, text=row["title"], font=ctk.CTkFont(size=18, weight="bold"), ) title.grid(row=0, column=0, sticky="w", padx=16, pady=(12, 4)) delete_button = ctk.CTkButton( card, text="Удалить", width=90, fg_color="#b33939", hover_color="#932f2f", command=lambda transaction_id=row["id"]: self.delete_transaction( transaction_id ), ) delete_button.grid(row=0, column=1, sticky="e", padx=16, pady=(12, 4)) amount_prefix = "-" if row["transaction_type"] == "Расход" else "+" info = ctk.CTkLabel( card, text=( f"{amount_prefix}{row['amount']:,.2f} ₽ | " f"{row['transaction_type']} | {row['account_name']} | " f"{row['category_name']} | {row['created_at'][:16]}" ).replace(",", " "), text_color="gray75", ) info.grid(row=1, column=0, columnspan=2, sticky="w", padx=16, pady=(0, 14))