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

256 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from tkinter import filedialog, messagebox
import customtkinter as ctk
from app.ui.base_frame import BaseAppFrame
from app.utils.formatters import today_text, validate_date
from app.utils.validators import normalize_text, parse_positive_float
class TransactionsFrame(BaseAppFrame):
"""Screen for adding, deleting and filtering transactions."""
def __init__(self, master, refresh_callback) -> None:
super().__init__(master, 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=20)
form_frame.grid(row=0, column=0, sticky="ns", padx=(8, 12), pady=8)
self.title_label(form_frame, "Операции").pack(
anchor="w", padx=18, pady=(18, 4)
)
self.subtitle_label(
form_frame,
"Добавляйте доходы и расходы с автоматическим изменением баланса.",
).pack(anchor="w", padx=18, pady=(0, 18))
self.title_entry = ctk.CTkEntry(
form_frame,
width=330,
placeholder_text="Название операции",
)
self.title_entry.pack(padx=18, pady=7)
self.amount_entry = ctk.CTkEntry(
form_frame,
width=330,
placeholder_text="Сумма",
)
self.amount_entry.pack(padx=18, pady=7)
self.type_menu = ctk.CTkOptionMenu(
form_frame,
width=330,
values=["Расход", "Доход"],
command=lambda _: self._load_category_menu(),
)
self.type_menu.set("Расход")
self.type_menu.pack(padx=18, pady=7)
self.account_menu = ctk.CTkOptionMenu(
form_frame,
width=330,
values=["Нет счетов"],
)
self.account_menu.pack(padx=18, pady=7)
self.category_menu = ctk.CTkOptionMenu(
form_frame,
width=330,
values=["Нет категорий"],
)
self.category_menu.pack(padx=18, pady=7)
self.date_entry = ctk.CTkEntry(
form_frame,
width=330,
placeholder_text="Дата: ГГГГ-ММ-ДД",
)
self.date_entry.insert(0, today_text())
self.date_entry.pack(padx=18, pady=7)
self.add_button = self.primary_button(
form_frame,
"Добавить операцию",
lambda: self.run_safe(self.add_transaction, self.add_button),
)
self.add_button.pack(fill="x", padx=18, pady=(12, 8))
self.export_button = ctk.CTkButton(
form_frame,
text="Экспорт операций в CSV",
height=38,
command=lambda: self.run_safe(self.export_csv, self.export_button),
)
self.export_button.pack(fill="x", padx=18, pady=(0, 18))
def _build_list(self) -> None:
right = ctk.CTkFrame(self, fg_color="transparent")
right.grid(row=0, column=1, sticky="nsew", padx=(0, 8), pady=8)
right.grid_rowconfigure(1, weight=1)
right.grid_columnconfigure(0, weight=1)
filter_frame = ctk.CTkFrame(right, corner_radius=18)
filter_frame.grid(row=0, column=0, sticky="ew", pady=(0, 8))
filter_frame.grid_columnconfigure(0, weight=1)
self.search_entry = ctk.CTkEntry(
filter_frame,
placeholder_text="Поиск по названию операции",
)
self.search_entry.grid(row=0, column=0, sticky="ew", padx=12, pady=12)
self.search_entry.bind("<KeyRelease>", lambda _: self.refresh_data())
self.filter_type_menu = ctk.CTkOptionMenu(
filter_frame,
width=140,
values=["Все", "Расход", "Доход"],
command=lambda _: self.refresh_data(),
)
self.filter_type_menu.set("Все")
self.filter_type_menu.grid(row=0, column=1, padx=12, pady=12)
self.list_frame = ctk.CTkScrollableFrame(right, label_text="История операций")
self.list_frame.grid(row=1, column=0, sticky="nsew")
def _load_account_menu(self) -> None:
rows = self.service.list_account_options()
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']}{self.money(row['balance'])}"
values.append(label)
self.account_map[label] = row["id"]
self.account_menu.configure(values=values)
if self.account_menu.get() not in values:
self.account_menu.set(values[0])
def _load_category_menu(self) -> None:
rows = self.service.list_categories(self.type_menu.get())
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 = row["name"]
values.append(label)
self.category_map[label] = row["id"]
self.category_menu.configure(values=values)
if self.category_menu.get() not in values:
self.category_menu.set(values[0])
def add_transaction(self) -> None:
account_label = self.account_menu.get()
category_label = self.category_menu.get()
if account_label not in self.account_map:
raise ValueError("Сначала добавьте хотя бы один счёт.")
if category_label not in self.category_map:
raise ValueError("Для выбранного типа операции нет категории.")
title = normalize_text(self.title_entry.get(), "Название")
amount = parse_positive_float(self.amount_entry.get(), "Сумма")
transaction_type = self.type_menu.get()
created_at = validate_date(self.date_entry.get())
self.service.add_transaction(
title=title,
amount=amount,
transaction_type=transaction_type,
account_id=self.account_map[account_label],
category_id=self.category_map[category_label],
created_at=created_at,
)
self.title_entry.delete(0, "end")
self.amount_entry.delete(0, "end")
self.date_entry.delete(0, "end")
self.date_entry.insert(0, today_text())
self.refresh_callback()
def delete_transaction(self, transaction_id: int) -> None:
if not messagebox.askyesno("Подтверждение", "Удалить выбранную операцию?"):
return
self.service.delete_transaction(transaction_id)
self.refresh_callback()
def export_csv(self) -> None:
file_path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV files", "*.csv")],
title="Сохранить операции",
)
if not file_path:
return
self.service.export_transactions_csv(file_path)
messagebox.showinfo("Готово", "Операции сохранены в CSV.")
def refresh_data(self) -> None:
self._load_account_menu()
self._load_category_menu()
self.clear(self.list_frame)
rows = self.service.list_transactions(
search=self.search_entry.get().strip() if hasattr(self, "search_entry") else "",
transaction_type=self.filter_type_menu.get()
if hasattr(self, "filter_type_menu")
else "Все",
)
if not rows:
ctk.CTkLabel(
self.list_frame,
text="Операций пока нет или фильтр ничего не нашёл.",
text_color="gray70",
).pack(anchor="w", padx=10, pady=10)
return
for row in rows:
self._render_row(row)
def _render_row(self, row) -> None:
card = ctk.CTkFrame(self.list_frame, corner_radius=16)
card.pack(fill="x", padx=6, pady=6)
card.grid_columnconfigure(0, weight=1)
sign = "+" if row["transaction_type"] == "Доход" else "-"
title_text = f"{row['title']} {sign}{self.money(row['amount'])}"
ctk.CTkLabel(
card,
text=title_text,
font=ctk.CTkFont(size=18, weight="bold"),
).grid(row=0, column=0, sticky="w", padx=16, pady=(12, 4))
self.danger_button(
card,
"Удалить",
lambda transaction_id=row["id"]: self.run_safe(
lambda: self.delete_transaction(transaction_id)
),
).grid(row=0, column=1, sticky="e", padx=16, pady=(12, 4))
info = (
f"{row['created_at'][:10]} | {row['transaction_type']} | "
f"{row['account_name']} | {row['category_name']}"
)
ctk.CTkLabel(card, text=info, text_color="gray70").grid(
row=1,
column=0,
columnspan=2,
sticky="w",
padx=16,
pady=(0, 14),
)