Files
Finance-Control/app/ui/transactions_frame.py

292 lines
9.7 KiB
Python

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))