Добавлены главная панель, подписки, бюджеты и отчеты

This commit is contained in:
2026-03-30 23:53:45 +03:00
parent 8ff14e5090
commit 7dcb1a23a7
4 changed files with 780 additions and 0 deletions

175
app/ui/reports_frame.py Normal file
View File

@@ -0,0 +1,175 @@
from tkinter import filedialog, messagebox
import customtkinter as ctk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from app.database import get_connection
class ReportsFrame(ctk.CTkFrame):
def __init__(self, master, refresh_callback) -> None:
super().__init__(master)
self.refresh_callback = refresh_callback
self.last_report_text = ""
self.grid_columnconfigure((0, 1), weight=1)
self.grid_rowconfigure(1, weight=1)
self._build_header()
self._build_left_panel()
self._build_right_panel()
def _build_header(self) -> None:
header_frame = ctk.CTkFrame(self, fg_color="transparent")
header_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=8, pady=(8, 12))
header_frame.grid_columnconfigure(0, weight=1)
title = ctk.CTkLabel(
header_frame,
text="Аналитика и отчёты",
font=ctk.CTkFont(size=28, weight="bold"),
)
title.grid(row=0, column=0, sticky="w")
export_button = ctk.CTkButton(
header_frame,
text="Сохранить отчёт в TXT",
command=self.export_report,
)
export_button.grid(row=0, column=1, padx=(12, 0))
def _build_left_panel(self) -> None:
left_frame = ctk.CTkFrame(self, corner_radius=18)
left_frame.grid(row=1, column=0, sticky="nsew", padx=(8, 12), pady=8)
left_frame.grid_rowconfigure(1, weight=1)
left_frame.grid_columnconfigure(0, weight=1)
info_label = ctk.CTkLabel(
left_frame,
text="Текстовый отчёт за текущий месяц",
font=ctk.CTkFont(size=20, weight="bold"),
)
info_label.grid(row=0, column=0, sticky="w", padx=18, pady=(18, 8))
self.report_box = ctk.CTkTextbox(left_frame)
self.report_box.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
def _build_right_panel(self) -> None:
right_frame = ctk.CTkFrame(self, corner_radius=18)
right_frame.grid(row=1, column=1, sticky="nsew", padx=(0, 8), pady=8)
right_frame.grid_rowconfigure(1, weight=1)
right_frame.grid_columnconfigure(0, weight=1)
chart_label = ctk.CTkLabel(
right_frame,
text="Структура расходов по категориям",
font=ctk.CTkFont(size=20, weight="bold"),
)
chart_label.grid(row=0, column=0, sticky="w", padx=18, pady=(18, 8))
self.chart_container = ctk.CTkFrame(right_frame, fg_color="transparent")
self.chart_container.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.figure = Figure(figsize=(5, 4), dpi=100)
self.canvas = FigureCanvasTkAgg(self.figure, master=self.chart_container)
self.canvas.get_tk_widget().pack(fill="both", expand=True)
def refresh_data(self) -> None:
with get_connection() as connection:
cursor = connection.cursor()
summary = cursor.execute(
"""
SELECT
COALESCE(SUM(CASE WHEN transaction_type = 'Доход' THEN amount END), 0) AS income,
COALESCE(SUM(CASE WHEN transaction_type = 'Расход' THEN amount END), 0) AS expense
FROM transactions
WHERE strftime('%Y-%m', created_at) = strftime('%Y-%m', 'now')
"""
).fetchone()
balance = cursor.execute(
"SELECT COALESCE(SUM(balance), 0) AS total FROM accounts"
).fetchone()["total"]
subscription_total = cursor.execute(
"""
SELECT COALESCE(
SUM(
CASE
WHEN period = 'Год' THEN amount / 12.0
ELSE amount
END
),
0
) AS total
FROM subscriptions
WHERE status = 'Активна'
"""
).fetchone()["total"]
category_rows = cursor.execute(
"""
SELECT c.name,
SUM(t.amount) AS total
FROM transactions AS t
JOIN categories AS c ON c.id = t.category_id
WHERE t.transaction_type = 'Расход'
AND strftime('%Y-%m', t.created_at) = strftime('%Y-%m', 'now')
GROUP BY c.name
HAVING total > 0
ORDER BY total DESC
"""
).fetchall()
net_result = summary["income"] - summary["expense"]
top_category = category_rows[0]["name"] if category_rows else "Нет данных"
top_category_value = category_rows[0]["total"] if category_rows else 0
report_text = (
"ОТЧЁТ ЗА ТЕКУЩИЙ МЕСЯЦ\n\n"
f"Доходы: {summary['income']:,.2f}\n"
f"Расходы: {summary['expense']:,.2f}\n"
f"Финансовый результат: {net_result:,.2f}\n"
f"Общий баланс по всем счетам: {balance:,.2f}\n"
f"Активные подписки в пересчёте на месяц: {subscription_total:,.2f}\n"
f"Самая затратная категория: {top_category} ({top_category_value:,.2f} ₽)\n"
).replace(",", " ")
self.last_report_text = report_text
self.report_box.delete("1.0", "end")
self.report_box.insert("1.0", report_text)
self.figure.clear()
axis = self.figure.add_subplot(111)
axis.set_title("Расходы по категориям")
if category_rows:
labels = [row["name"] for row in category_rows]
values = [row["total"] for row in category_rows]
axis.pie(values, labels=labels, autopct="%1.1f%%")
else:
axis.text(0.5, 0.5, "Нет данных для диаграммы", ha="center", va="center")
axis.axis("off")
self.canvas.draw()
def export_report(self) -> None:
if not self.last_report_text:
messagebox.showinfo("Информация", "Сначала обновите отчёт.")
return
file_path = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Text files", "*.txt")],
title="Сохранить отчёт",
)
if not file_path:
return
with open(file_path, "w", encoding="utf-8") as file:
file.write(self.last_report_text)
messagebox.showinfo("Готово", "Отчёт сохранён.")