Добавлены главная панель, подписки, бюджеты и отчеты
This commit is contained in:
175
app/ui/reports_frame.py
Normal file
175
app/ui/reports_frame.py
Normal 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("Готово", "Отчёт сохранён.")
|
||||
Reference in New Issue
Block a user