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

176 lines
6.7 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 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("Готово", "Отчёт сохранён.")