/
LKastornova
/
Python_lesson
Обзор
Документация
Войти
/
LKastornova
/
Python_lesson
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
gui.py
1 061 строка
53 KB
@lvkastor
Initial commit: Finance Manager 2026 application
11 янв 2026, 19:15
11 янв 2026, 19:15
209a1c3
Код
Авторство
О чём код?
""" Модуль графического интерфейса приложения Finance Manager 2026. Содержит класс FinanceApp, реализующий графический интерфейс с использованием Tkinter. Интерфейс включает вкладки для добавления операций, просмотра с фильтрацией и сортировкой, анализа данных, расширенного анализа с pandas и импорта/экспорта данных. """ import tkinter as tk from tkinter import messagebox, ttk, filedialog from tkcalendar import DateEntry from datetime import datetime, timedelta from models import FinancialOperation from storage import load_operations, save_operations, export_to_csv, export_to_json, import_from_csv, import_from_json from analysis import ( calculate_balance, analyze_expenses_by_category, analyze_income_by_category, calculate_period_totals, filter_operations_by_period, create_expense_pie_chart, create_income_expense_bar_chart, create_top_expenses_chart ) from utils import validate_amount, clean_comment import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg class FinanceApp: """Главный класс приложения, управляющий графическим интерфейсом.""" def __init__(self, root, operations): """ Инициализирует приложение Finance Manager. Args: root: Главное окно Tkinter operations: Список финансовых операций для отображения """ self.root = root self.root.title("Finance Manager 2026") self.operations = operations self.categories = { 'income': ['работа', 'аренда', 'прочее'], 'expense': ['развлечения', 'еда', 'здоровье', 'квартира', 'красота', 'одежда', 'подарки', 'отпуск', 'транспорт', 'штуки', 'счета', 'пряжа', 'дом (хозмаг)', 'машина'] } self.create_widgets() self.update_balance_display() def create_widgets(self): """Создает все элементы интерфейса с вкладками.""" self.notebook = ttk.Notebook(self.root) self.notebook.pack(fill="both", expand=True, padx=10, pady=10) # Вкладки self.add_frame = tk.Frame(self.notebook) self.notebook.add(self.add_frame, text="Добавить операцию") self.create_add_widgets(self.add_frame) self.view_frame = tk.Frame(self.notebook) self.notebook.add(self.view_frame, text="Просмотр операций") self.create_view_widgets(self.view_frame) self.analysis_frame = tk.Frame(self.notebook) self.notebook.add(self.analysis_frame, text="Анализ") self.create_analysis_widgets(self.analysis_frame) self.advanced_analysis_frame = tk.Frame(self.notebook) self.notebook.add(self.advanced_analysis_frame, text="Расширенный анализ") self.create_advanced_analysis_widgets(self.advanced_analysis_frame) self.import_export_frame = tk.Frame(self.notebook) self.notebook.add(self.import_export_frame, text="Импорт/Экспорт") self.create_import_export_widgets(self.import_export_frame) def create_add_widgets(self, parent): """Создает виджеты для добавления операций.""" self.balance_label = tk.Label(parent, text="", font=('Arial', 14, 'bold')) self.balance_label.grid(row=0, column=0, columnspan=3, pady=10) # Тип операции tk.Label(parent, text="Тип операции:").grid(row=1, column=0, sticky="w", padx=5, pady=5) self.operation_type_var = tk.StringVar(value="income") tk.Radiobutton(parent, text="Доход", variable=self.operation_type_var, value="income", command=self.update_categories).grid(row=1, column=1) tk.Radiobutton(parent, text="Расход", variable=self.operation_type_var, value="expense", command=self.update_categories).grid(row=1, column=2) # Категория tk.Label(parent, text="Категория:").grid(row=2, column=0, sticky="w", padx=5, pady=5) self.category_var = tk.StringVar() self.category_menu = ttk.Combobox(parent, textvariable=self.category_var, state="readonly") self.category_menu.grid(row=2, column=1, columnspan=2, sticky="ew", padx=5, pady=5) self.update_categories() # Остальные поля ввода tk.Label(parent, text="Сумма:").grid(row=3, column=0, sticky="w", padx=5, pady=5) self.amount_entry = tk.Entry(parent, validate="key") self.amount_entry.config(validatecommand=(self.amount_entry.register(self.validate_amount_input), '%P')) self.amount_entry.grid(row=3, column=1, columnspan=2, sticky="ew", padx=5, pady=5) tk.Label(parent, text="Подкатегория (опционально):").grid(row=4, column=0, sticky="w", padx=5, pady=5) self.subcategory_entry = tk.Entry(parent) self.subcategory_entry.grid(row=4, column=1, columnspan=2, sticky="ew", padx=5, pady=5) tk.Label(parent, text="Комментарий (опционально):").grid(row=5, column=0, sticky="w", padx=5, pady=5) self.comment_entry = tk.Entry(parent) self.comment_entry.grid(row=5, column=1, columnspan=2, sticky="ew", padx=5, pady=5) tk.Label(parent, text="Дата операции:").grid(row=6, column=0, sticky="w", padx=5, pady=5) self.date_entry = DateEntry(parent, date_pattern="yyyy-mm-dd", year=2026, month=1, day=1) self.date_entry.grid(row=6, column=1, columnspan=2, sticky="ew", padx=5, pady=5) # Кнопки tk.Button(parent, text="Добавить операцию", command=self.add_operation, bg="lightblue").grid(row=7, column=0, columnspan=3, pady=10, sticky="ew") tk.Button(parent, text="Очистить форму", command=self.clear_form, bg="lightgray").grid(row=8, column=0, columnspan=3, pady=5, sticky="ew") def create_view_widgets(self, parent): """Создает виджеты для просмотра операций с фильтрацией и сортировкой.""" filter_frame = tk.LabelFrame(parent, text="Фильтры и сортировка") filter_frame.pack(fill="x", padx=10, pady=10) # Период tk.Label(filter_frame, text="Период:").grid(row=0, column=0, padx=5, pady=5) self.period_var = tk.StringVar(value="all") periods = [("Все", "all"), ("Текущая неделя", "week"), ("Текущий месяц", "month"), ("Диапазон дат", "custom")] for idx, (text, value) in enumerate(periods): tk.Radiobutton(filter_frame, text=text, variable=self.period_var, value=value, command=self.on_period_changed).grid(row=0, column=idx+1) # Поля для диапазона дат (скрыты по умолчанию) self.custom_date_frame = tk.Frame(filter_frame) self.custom_date_frame.grid(row=1, column=0, columnspan=5, sticky="w", pady=5) self.custom_date_frame.grid_remove() # Скрываем по умолчанию tk.Label(self.custom_date_frame, text="С:").pack(side="left", padx=2) self.start_date_entry = DateEntry(self.custom_date_frame, date_pattern="yyyy-mm-dd", year=2026, month=1, day=1, width=10) self.start_date_entry.pack(side="left", padx=2) tk.Label(self.custom_date_frame, text="По:").pack(side="left", padx=2) self.end_date_entry = DateEntry(self.custom_date_frame, date_pattern="yyyy-mm-dd", year=2026, month=1, day=31, width=10) self.end_date_entry.pack(side="left", padx=2) tk.Button(self.custom_date_frame, text="Применить", command=self.update_operations_view, bg="lightblue").pack(side="left", padx=10) # Тип операции для фильтра tk.Label(filter_frame, text="Тип:").grid(row=2, column=0, padx=5, pady=5) self.filter_type_var = tk.StringVar(value="all") types = [("Все", "all"), ("Доходы", "income"), ("Расходы", "expense")] for idx, (text, value) in enumerate(types): tk.Radiobutton(filter_frame, text=text, variable=self.filter_type_var, value=value, command=self.update_operations_view).grid(row=2, column=idx+1) # Сортировка tk.Label(filter_frame, text="Сортировать по:").grid(row=3, column=0, padx=5, pady=5) self.sort_var = tk.StringVar(value="date_desc") sort_options = [ ("Дате (новые)", "date_desc"), ("Дате (старые)", "date_asc"), ("Сумме (убыв.)", "amount_desc"), ("Сумме (возр.)", "amount_asc"), ("Категории", "category") ] sort_frame = tk.Frame(filter_frame) sort_frame.grid(row=3, column=1, columnspan=4, sticky="w") for idx, (text, value) in enumerate(sort_options): tk.Radiobutton(sort_frame, text=text, variable=self.sort_var, value=value, command=self.update_operations_view).pack(side="left", padx=5) # Таблица операций table_frame = tk.Frame(parent) table_frame.pack(fill="both", expand=True, padx=10, pady=10) columns = ("Дата", "Тип", "Категория", "Подкатегория", "Сумма", "Комментарий") self.operations_table = ttk.Treeview(table_frame, columns=columns, show="headings", height=20) for col in columns: self.operations_table.heading(col, text=col) self.operations_table.column(col, anchor="center", width=100) self.operations_table.configure(selectmode='extended') scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=self.operations_table.yview) self.operations_table.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") self.operations_table.pack(side="left", fill="both", expand=True) # Итоги totals_frame = tk.Frame(parent) totals_frame.pack(fill="x", padx=10, pady=10) self.income_total_label = tk.Label(totals_frame, text="Доходы: 0.00", font=('Arial', 10)) self.income_total_label.pack(side="left", padx=20) self.expense_total_label = tk.Label(totals_frame, text="Расходы: 0.00", font=('Arial', 10)) self.expense_total_label.pack(side="left", padx=20) self.profit_label = tk.Label(totals_frame, text="Прибыль: 0.00", font=('Arial', 10, 'bold')) self.profit_label.pack(side="left", padx=20) tk.Button(parent, text="Удалить выбранные операции", command=self.delete_operation, bg="lightcoral").pack(pady=10) self.update_operations_view() def create_analysis_widgets(self, parent): """Создает виджеты для базового анализа с графиками.""" # Сохраняем текущий тип графика self.current_chart_type = 'pie' # Фрейм для фильтров filter_frame = tk.Frame(parent) filter_frame.pack(fill="x", padx=10, pady=10) tk.Label(filter_frame, text="Период анализа:").pack(side="left", padx=5) self.analysis_period_var = tk.StringVar(value="all") # Радиокнопки для стандартных периодов periods_frame = tk.Frame(filter_frame) periods_frame.pack(side="left", padx=5) analysis_periods = [("Все", "all"), ("Текущая неделя", "week"), ("Текущий месяц", "month"), ("Диапазон дат", "custom")] for text, value in analysis_periods: tk.Radiobutton(periods_frame, text=text, variable=self.analysis_period_var, value=value, command=self.on_analysis_period_changed).pack(side="left", padx=5) # Поля для диапазона дат (скрыты по умолчанию) self.analysis_custom_date_frame = tk.Frame(filter_frame) self.analysis_custom_date_frame.pack(side="left", padx=5) self.analysis_custom_date_frame.pack_forget() # Скрываем по умолчанию tk.Label(self.analysis_custom_date_frame, text="С:").pack(side="left", padx=2) self.analysis_start_date_entry = DateEntry(self.analysis_custom_date_frame, date_pattern="yyyy-mm-dd", year=2026, month=1, day=1, width=10) self.analysis_start_date_entry.pack(side="left", padx=2) tk.Label(self.analysis_custom_date_frame, text="По:").pack(side="left", padx=2) self.analysis_end_date_entry = DateEntry(self.analysis_custom_date_frame, date_pattern="yyyy-mm-dd", year=2026, month=1, day=31, width=10) self.analysis_end_date_entry.pack(side="left", padx=2) tk.Button(self.analysis_custom_date_frame, text="Применить", command=self.update_current_chart, bg="lightblue").pack(side="left", padx=10) # Кнопки для выбора типа графика btn_frame = tk.Frame(parent) btn_frame.pack(fill="x", padx=10, pady=10) tk.Button(btn_frame, text="Круговая диаграмма расходов", command=lambda: self.set_chart_type('pie'), bg="lightblue").pack(side="left", padx=5) tk.Button(btn_frame, text="Доходы/расходы по датам", command=lambda: self.set_chart_type('bar'), bg="lightgreen").pack(side="left", padx=5) tk.Button(btn_frame, text="Топ-10 расходов", command=lambda: self.set_chart_type('top'), bg="lightcoral").pack(side="left", padx=5) # Фрейм для графика self.graph_frame = tk.Frame(parent) self.graph_frame.pack(fill="both", expand=True, padx=10, pady=10) # Показываем начальный график self.show_chart('pie') def create_advanced_analysis_widgets(self, parent): """Создает виджеты для расширенного анализа с pandas.""" canvas = tk.Canvas(parent) scrollbar = ttk.Scrollbar(parent, orient="vertical", command=canvas.yview) scrollable_frame = tk.Frame(canvas) scrollable_frame.bind( "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) btn_frame = tk.Frame(scrollable_frame) btn_frame.pack(fill="x", padx=10, pady=10) # Сохраняем текущий тип анализа self.current_analysis_type = 'categories' # Функция для обновления анализа при изменении типа def set_analysis_type(analysis_type): self.current_analysis_type = analysis_type self.show_advanced_analysis(analysis_type) tk.Button(btn_frame, text="Категории (детально)", command=lambda: set_analysis_type('categories'), bg="lightblue").pack(side="left", padx=5, pady=5) tk.Button(btn_frame, text="Месячные тренды", command=lambda: set_analysis_type('trends'), bg="lightgreen").pack(side="left", padx=5, pady=5) tk.Button(btn_frame, text="Дни недели", command=lambda: set_analysis_type('weekdays'), bg="lightyellow").pack(side="left", padx=5, pady=5) tk.Button(btn_frame, text="Сводный отчет", command=lambda: set_analysis_type('report'), bg="lightcoral").pack(side="left", padx=5, pady=5) # Фрейм для фильтров period_frame = tk.LabelFrame(scrollable_frame, text="Период анализа") period_frame.pack(fill="x", padx=10, pady=10) self.advanced_period_var = tk.StringVar(value="all") # Радиокнопки для стандартных периодов periods_frame = tk.Frame(period_frame) periods_frame.pack(side="top", fill="x", pady=5) advanced_periods = [("Все", "all"), ("Текущая неделя", "week"), ("Текущий месяц", "month"), ("Диапазон дат", "custom")] for text, value in advanced_periods: tk.Radiobutton(periods_frame, text=text, variable=self.advanced_period_var, value=value, command=self.on_advanced_period_changed).pack(side="left", padx=5) # Поля для диапазона дат (скрыты по умолчанию) self.advanced_custom_date_frame = tk.Frame(period_frame) self.advanced_custom_date_frame.pack(side="top", fill="x", pady=5) self.advanced_custom_date_frame.pack_forget() # Скрываем по умолчанию tk.Label(self.advanced_custom_date_frame, text="С:").pack(side="left", padx=2) self.advanced_start_date_entry = DateEntry(self.advanced_custom_date_frame, date_pattern="yyyy-mm-dd", year=2026, month=1, day=1, width=10) self.advanced_start_date_entry.pack(side="left", padx=2) tk.Label(self.advanced_custom_date_frame, text="По:").pack(side="left", padx=2) self.advanced_end_date_entry = DateEntry(self.advanced_custom_date_frame, date_pattern="yyyy-mm-dd", year=2026, month=1, day=31, width=10) self.advanced_end_date_entry.pack(side="left", padx=2) tk.Button(self.advanced_custom_date_frame, text="Применить", command=lambda: self.show_advanced_analysis(self.current_analysis_type), bg="lightblue").pack(side="left", padx=10) self.advanced_result_frame = tk.Frame(scrollable_frame) self.advanced_result_frame.pack(fill="both", expand=True, padx=10, pady=10) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # Показываем начальный анализ self.show_advanced_analysis('categories') def create_import_export_widgets(self, parent): """Создает виджеты для импорта и экспорта данных.""" export_frame = tk.LabelFrame(parent, text="Экспорт данных") export_frame.pack(fill="x", padx=10, pady=10) tk.Button(export_frame, text="Экспорт в CSV", command=self.export_csv, bg="lightgreen").pack(pady=5, padx=10, fill="x") tk.Button(export_frame, text="Экспорт в JSON", command=self.export_json, bg="lightgreen").pack(pady=5, padx=10, fill="x") import_frame = tk.LabelFrame(parent, text="Импорт данных") import_frame.pack(fill="x", padx=10, pady=10) tk.Button(import_frame, text="Импорт из CSV", command=self.import_csv, bg="lightyellow").pack(pady=5, padx=10, fill="x") tk.Button(import_frame, text="Импорт из JSON", command=self.import_json, bg="lightyellow").pack(pady=5, padx=10, fill="x") # Обработчики событий для фильтров def on_period_changed(self): """Обработчик изменения периода в просмотре операций.""" if self.period_var.get() == "custom": self.custom_date_frame.grid() else: self.custom_date_frame.grid_remove() self.update_operations_view() def on_analysis_period_changed(self): """Обработчик изменения периода в базовом анализе.""" if self.analysis_period_var.get() == "custom": self.analysis_custom_date_frame.pack(side="left", padx=5) else: self.analysis_custom_date_frame.pack_forget() self.update_current_chart() def on_advanced_period_changed(self): """Обработчик изменения периода в расширенном анализе.""" if self.advanced_period_var.get() == "custom": self.advanced_custom_date_frame.pack(side="top", fill="x", pady=5) else: self.advanced_custom_date_frame.pack_forget() self.show_advanced_analysis(self.current_analysis_type) def set_chart_type(self, chart_type): """Устанавливает тип графика и обновляет его.""" self.current_chart_type = chart_type self.update_current_chart() def update_current_chart(self): """Обновляет текущий график.""" self.show_chart(self.current_chart_type) def validate_amount_input(self, value): """Валидация ввода суммы.""" if value == "": return True try: float(value) return True except ValueError: return False def update_balance_display(self): """Обновление отображения баланса.""" balance = calculate_balance(self.operations) color = "green" if balance >= 0 else "red" self.balance_label.config(text=f"Текущий баланс: {balance:.2f}", fg=color) def clear_form(self): """Очищает все поля ввода.""" self.operation_type_var.set("income") self.update_categories() self.amount_entry.delete(0, tk.END) self.subcategory_entry.delete(0, tk.END) self.comment_entry.delete(0, tk.END) self.date_entry.set_date(datetime.now()) def update_categories(self): """Обновляет список категорий в зависимости от выбранного типа операции.""" operation_type = self.operation_type_var.get() categories = self.categories[operation_type] self.category_menu['values'] = categories if categories: if not self.category_var.get(): self.category_var.set(categories[0]) elif self.category_var.get() not in categories: self.category_var.set(categories[0]) def add_operation(self): """Добавляет новую операцию.""" operation_type = self.operation_type_var.get() category = self.category_var.get() amount_str = self.amount_entry.get().strip() if not amount_str: messagebox.showerror("Ошибка", "Введите сумму!") return try: amount = float(amount_str) if amount <= 0: messagebox.showerror("Ошибка", "Сумма должна быть положительной!") return except ValueError: messagebox.showerror("Ошибка", "Некорректная сумма!") return comment = self.comment_entry.get().strip() if comment: comment = clean_comment(comment) subcategory = self.subcategory_entry.get().strip() or None date = self.date_entry.get_date() try: operation = FinancialOperation(operation_type, amount, category, subcategory, date, comment) self.operations.append(operation) if save_operations(self.operations): messagebox.showinfo("Успех", "Операция добавлена!") self.clear_form() self.update_balance_display() self.update_operations_view() else: messagebox.showerror("Ошибка", "Не удалось сохранить операцию") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось добавить операцию: {str(e)}") def update_operations_view(self): """Обновление отображения операций с фильтрацией и сортировкой.""" for item in self.operations_table.get_children(): self.operations_table.delete(item) filtered_ops = self.operations.copy() # Фильтрация по периоду period = self.period_var.get() if period != "all": if period == "custom": # Фильтрация по диапазону дат start_date = self.start_date_entry.get_date() end_date = self.end_date_entry.get_date() filtered_ops = [op for op in filtered_ops if start_date <= op.date.date() <= end_date] else: filtered_ops = self.filter_operations_by_period_custom(filtered_ops, period) filter_type = self.filter_type_var.get() if filter_type != "all": filtered_ops = [op for op in filtered_ops if op.operation_type == filter_type] sort_option = self.sort_var.get() if sort_option == "date_desc": filtered_ops.sort(key=lambda x: x.date, reverse=True) elif sort_option == "date_asc": filtered_ops.sort(key=lambda x: x.date, reverse=False) elif sort_option == "amount_desc": filtered_ops.sort(key=lambda x: x.amount, reverse=True) elif sort_option == "amount_asc": filtered_ops.sort(key=lambda x: x.amount, reverse=False) elif sort_option == "category": filtered_ops.sort(key=lambda x: x.category.lower()) total_income = 0 total_expense = 0 for operation in filtered_ops: if operation.operation_type == "income": total_income += operation.amount type_display = "Доход" else: total_expense += operation.amount type_display = "Расход" self.operations_table.insert("", "end", values=( operation.date.strftime("%Y-%m-%d"), type_display, operation.category, operation.subcategory or "", f"{operation.amount:.2f}", operation.comment or "" )) profit = total_income - total_expense self.income_total_label.config(text=f"Доходы: {total_income:.2f}") self.expense_total_label.config(text=f"Расходы: {total_expense:.2f}") self.profit_label.config(text=f"Прибыль: {profit:.2f}", fg="green" if profit >= 0 else "red") def filter_operations_by_period_custom(self, operations, period='month'): """ Фильтрация операций по периоду (исправленная версия). Args: operations: Список операций для фильтрации period: Период ('week', 'month') Returns: list: Отфильтрованные операции """ if not operations: return operations now = datetime.now() if period == 'week': # Начало текущей недели (понедельник) start_date = now - timedelta(days=now.weekday()) start_date = datetime(start_date.year, start_date.month, start_date.day, 0, 0, 0) # Конец недели (воскресенье) end_date = start_date + timedelta(days=6) end_date = datetime(end_date.year, end_date.month, end_date.day, 23, 59, 59) return [op for op in operations if start_date <= op.date <= end_date] elif period == 'month': start_date = datetime(now.year, now.month, 1, 0, 0, 0) # Конец месяца if now.month == 12: next_month = datetime(now.year + 1, 1, 1, 0, 0, 0) else: next_month = datetime(now.year, now.month + 1, 1, 0, 0, 0) end_date = next_month - timedelta(seconds=1) return [op for op in operations if start_date <= op.date <= end_date] else: return operations def delete_operation(self): """Удаление выбранных операций.""" selection = self.operations_table.selection() if not selection: messagebox.showwarning("Внимание", "Выберите операции для удаления") return selected_operations_info = [] operations_to_delete = [] for selected_item in selection: item_values = self.operations_table.item(selected_item)['values'] for i, operation in enumerate(self.operations): if (operation.date.strftime("%Y-%m-%d") == item_values[0] and operation.category == item_values[2] and abs(operation.amount - float(item_values[4])) < 0.01): selected_operations_info.append(f"{operation.date.strftime('%Y-%m-%d')} - {operation.category}: {operation.amount:.2f}") operations_to_delete.append((i, operation)) break if not operations_to_delete: messagebox.showwarning("Внимание", "Не удалось найти выбранные операции") return operations_text = "\n".join(selected_operations_info) if len(operations_to_delete) == 1: message = f"Удалить операцию?\n{operations_text}" else: message = f"Удалить {len(operations_to_delete)} операций?\n{operations_text}" if messagebox.askyesno("Подтверждение удаления", message): operations_to_delete.sort(key=lambda x: x[0], reverse=True) deleted_count = 0 for idx, operation in operations_to_delete: if 0 <= idx < len(self.operations): self.operations.pop(idx) deleted_count += 1 self.update_operations_view() try: save_operations(self.operations) if deleted_count == 1: messagebox.showinfo("Успех", "Операция удалена") else: messagebox.showinfo("Успех", f"{deleted_count} операций удалено") self.update_balance_display() except Exception as e: messagebox.showerror("Ошибка", f"Не удалось сохранить изменения: {str(e)}") def show_chart(self, chart_type): """Отображение графиков анализа.""" period = self.analysis_period_var.get() # Если выбран диапазон дат, создаем специальный параметр if period == "custom": start_date = self.analysis_start_date_entry.get_date() end_date = self.analysis_end_date_entry.get_date() # Формируем строку с диапазоном дат period_for_chart = f"{start_date.strftime('%d.%m.%Y')}-{end_date.strftime('%d.%m.%Y')}" # Фильтруем операции для графика filtered_ops = [op for op in self.operations if start_date <= op.date.date() <= end_date] else: period_for_chart = period filtered_ops = self.operations for widget in self.graph_frame.winfo_children(): widget.destroy() fig = None if chart_type == 'pie': fig = create_expense_pie_chart(filtered_ops, period_for_chart) elif chart_type == 'bar': fig = create_income_expense_bar_chart(filtered_ops, period_for_chart) elif chart_type == 'top': fig = create_top_expenses_chart(filtered_ops, top_n=10, period=period_for_chart) if fig: canvas = FigureCanvasTkAgg(fig, master=self.graph_frame) canvas.draw() canvas.get_tk_widget().pack(fill="both", expand=True) save_button = tk.Button(self.graph_frame, text="Сохранить график", command=lambda: self.save_chart(fig)) save_button.pack(pady=5) else: tk.Label(self.graph_frame, text="Нет данных для построения графика").pack() def save_chart(self, fig): """Сохранение графика в файл.""" filename = filedialog.asksaveasfilename( defaultextension=".png", filetypes=[("PNG files", "*.png"), ("PDF files", "*.pdf"), ("All files", "*.*")] ) if filename: try: fig.savefig(filename, dpi=300, bbox_inches='tight') messagebox.showinfo("Успех", f"График сохранен в {filename}") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось сохранить график: {str(e)}") def show_advanced_analysis(self, analysis_type): """Отображение расширенного анализа.""" for widget in self.advanced_result_frame.winfo_children(): widget.destroy() period = self.advanced_period_var.get() # Если выбран диапазон дат, фильтруем операции if period == "custom": start_date = self.advanced_start_date_entry.get_date() end_date = self.advanced_end_date_entry.get_date() filtered_ops = [op for op in self.operations if start_date <= op.date.date() <= end_date] period_for_analysis = f"{start_date.strftime('%d.%m.%Y')}-{end_date.strftime('%d.%m.%Y')}" else: filtered_ops = self.operations period_for_analysis = period try: if analysis_type == 'categories': self.show_category_statistics(filtered_ops, period_for_analysis) elif analysis_type == 'trends': self.show_monthly_trends(filtered_ops, period_for_analysis) elif analysis_type == 'weekdays': self.show_weekday_patterns(filtered_ops, period_for_analysis) elif analysis_type == 'report': self.show_financial_report(filtered_ops, period_for_analysis) except Exception as e: tk.Label(self.advanced_result_frame, text=f"Ошибка при анализе: {str(e)}", fg="red").pack(padx=10, pady=10) def show_category_statistics(self, operations, period='all'): """Отображение детальной статистики по категориям.""" try: from pandas_analysis import analyze_category_statistics # Проверяем, есть ли данные для выбранного периода filtered_ops = operations.copy() # Проверяем типы операций income_count = sum(1 for op in filtered_ops if op.operation_type == 'income') expense_count = sum(1 for op in filtered_ops if op.operation_type == 'expense') # ОТЛАДОЧНЫЙ ВЫВОД print(f"DEBUG show_category_statistics: period = '{period}'") print(f"DEBUG: Operations before filtering: {len(self.operations)}") print(f"DEBUG: Operations after filtering: {len(filtered_ops)}") print(f"DEBUG: Income operations: {income_count}") print(f"DEBUG: Expense operations: {expense_count}") if not filtered_ops: # Показываем информативное сообщение info_frame = tk.Frame(self.advanced_result_frame) info_frame.pack(pady=20) tk.Label(info_frame, text="Нет данных для анализа в выбранном периоде", fg="orange", font=('Arial', 12, 'bold')).pack() tk.Label(info_frame, text=f"Период: {period}", font=('Arial', 10)).pack() tk.Label(info_frame, text=f"Всего операций за период: {len(filtered_ops)}", font=('Arial', 10)).pack() tk.Label(info_frame, text=f"Доходы: {income_count}, Расходы: {expense_count}", font=('Arial', 10)).pack() return stats = analyze_category_statistics(filtered_ops, period) if not stats: # Показываем более информативное сообщение info_frame = tk.Frame(self.advanced_result_frame) info_frame.pack(pady=20) tk.Label(info_frame, text="Нет данных для анализа в выбранном периоде", fg="orange", font=('Arial', 12, 'bold')).pack() tk.Label(info_frame, text=f"Период: {period}", font=('Arial', 10)).pack() tk.Label(info_frame, text=f"Всего операций за период: {len(filtered_ops)}", font=('Arial', 10)).pack() tk.Label(info_frame, text=f"Доходы: {income_count}, Расходы: {expense_count}", font=('Arial', 10)).pack() # Если есть операции, но нет статистики, показываем их if filtered_ops: tk.Label(info_frame, text="\nОперации в периоде:", font=('Arial', 10, 'bold')).pack(pady=5) for op in filtered_ops[:5]: # Показываем первые 5 op_type = "Доход" if op.operation_type == 'income' else "Расход" tk.Label(info_frame, text=f" {op.date.date()}: {op_type} - {op.category} - {op.amount:.2f}", font=('Arial', 9)).pack() return columns = ("Категория", "Сумма", "Среднее", "Кол-во", "Макс", "Мин", "Доля %") tree = ttk.Treeview(self.advanced_result_frame, columns=columns, show="headings", height=15) for col in columns: tree.heading(col, text=col) tree.column(col, width=80, anchor="center") for category, data in stats.items(): if category != '_overall': tree.insert("", "end", values=( category, f"{data['total']:.2f}", f"{data['average']:.2f}", data['count'], f"{data['max']:.2f}", f"{data['min']:.2f}", f"{data['percentage']:.1f}" )) scrollbar = ttk.Scrollbar(self.advanced_result_frame, orient="vertical", command=tree.yview) tree.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") tree.pack(side="left", fill="both", expand=True) if '_overall' in stats: overall = stats['_overall'] summary_frame = tk.Frame(self.advanced_result_frame) summary_frame.pack(fill="x", pady=10) summary_text = f"Всего расходов: {overall['total_expenses']:.2f} руб. | " summary_text += f"Категорий: {overall['category_count']} | " summary_text += f"Самая дорогая: {overall['most_expensive_category']} | " summary_text += f"Самая частая: {overall['most_frequent_category']}" tk.Label(summary_frame, text=summary_text, font=('Arial', 10, 'bold')).pack() # Добавляем информацию о периоде period_label = f" (Период: {period})" if period != 'all' else "" tk.Label(summary_frame, text=f"Анализ расходов по категориям{period_label}", font=('Arial', 9, 'italic')).pack() except ImportError: tk.Label(self.advanced_result_frame, text="Модуль pandas_analysis не найден").pack() except Exception as e: tk.Label(self.advanced_result_frame, text=f"Ошибка при анализе: {str(e)}", fg="red").pack(padx=10, pady=10) def show_monthly_trends(self, operations, period=None): """Отображение месячных трендов.""" try: from pandas_analysis import analyze_monthly_trends # Передаем period в функцию анализа trends = analyze_monthly_trends(operations, months=6, period=period) except ImportError: tk.Label(self.advanced_result_frame, text="Модуль pandas_analysis не найден").pack() return if not trends: # Показываем информативное сообщение info_frame = tk.Frame(self.advanced_result_frame) info_frame.pack(pady=20) period_text = f" за период '{period}'" if period and period != 'all' else "" tk.Label(info_frame, text=f"Нет данных для анализа{period_text}", fg="orange", font=('Arial', 12, 'bold')).pack() return text_widget = tk.Text(self.advanced_result_frame, height=20, width=80) text_widget.pack(fill="both", expand=True) # Заголовок с указанием периода title = "МЕСЯЧНЫЕ ТРЕНДЫ (последние 6 месяцев)" if period and period != 'all': title += f" - Период: {period}" text_widget.insert("end", title + "\n") text_widget.insert("end", "=" * 50 + "\n\n") for month, data in trends.items(): text_widget.insert("end", f"{month}:\n") text_widget.insert("end", f" Доходы: {data['income']:.2f} руб.\n") text_widget.insert("end", f" Расходы: {data['expense']:.2f} руб.\n") text_widget.insert("end", f" Баланс: {data['balance']:.2f} руб.\n") if data['income_change'] is not None: change_icon = "↑" if data['income_change'] > 0 else "↓" text_widget.insert("end", f" Изменение доходов: {data['income_change']:.1f}% {change_icon}\n") if data['expense_change'] is not None: change_icon = "↑" if data['expense_change'] > 0 else "↓" text_widget.insert("end", f" Изменение расходов: {data['expense_change']:.1f}% {change_icon}\n") text_widget.insert("end", "\n") text_widget.config(state="disabled") tk.Button(self.advanced_result_frame, text="Сохранить отчет", command=lambda: self.save_text_report(text_widget.get("1.0", "end"))).pack(pady=5) def show_weekday_patterns(self, operations, period=None): """Отображение паттернов по дням недели.""" try: from pandas_analysis import analyze_daily_spending_patterns # Передаем period в функцию анализа patterns = analyze_daily_spending_patterns(operations, period) except ImportError: tk.Label(self.advanced_result_frame, text="Модуль pandas_analysis не найден").pack() return if not patterns: # Показываем информативное сообщение info_frame = tk.Frame(self.advanced_result_frame) info_frame.pack(pady=20) period_text = f" за период '{period}'" if period and period != 'all' else "" tk.Label(info_frame, text=f"Нет данных для анализа{period_text}", fg="orange", font=('Arial', 12, 'bold')).pack() return columns = ("День", "Сумма", "Среднее", "Кол-во", "Станд. откл.") tree = ttk.Treeview(self.advanced_result_frame, columns=columns, show="headings", height=10) for col in columns: tree.heading(col, text=col) tree.column(col, width=100, anchor="center") days_order = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'] for day in days_order: if day in patterns and day != '_summary': data = patterns[day] tree.insert("", "end", values=( day, f"{data['total']:.2f}", f"{data['average']:.2f}", data['count'], f"{data['std_dev']:.2f}" )) scrollbar = ttk.Scrollbar(self.advanced_result_frame, orient="vertical", command=tree.yview) tree.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") tree.pack(side="left", fill="both", expand=True) if '_summary' in patterns: summary = patterns['_summary'] summary_frame = tk.Frame(self.advanced_result_frame) summary_frame.pack(fill="x", pady=10) # Добавляем информацию о периоде period_text = f" (Период: {period})" if period and period != 'all' else "" tk.Label(summary_frame, text=f"Анализ расходов по дням недели{period_text}", font=('Arial', 9, 'italic')).pack() summary_text = f"Самый дорогой день: {summary['most_expensive_day']} ({summary['most_expensive_amount']:.2f} руб.) | " summary_text += f"Самый дешевый день: {summary['cheapest_day']} ({summary['cheapest_amount']:.2f} руб.)" tk.Label(summary_frame, text=summary_text, font=('Arial', 10, 'bold')).pack() def show_financial_report(self, operations, period=None): """Отображение сводного финансового отчета.""" try: from pandas_analysis import generate_financial_report report = generate_financial_report(operations, period) except ImportError: tk.Label(self.advanced_result_frame, text="Модуль pandas_analysis не найден").pack() return text_widget = tk.Text(self.advanced_result_frame, height=25, width=80) text_widget.pack(fill="both", expand=True) text_widget.insert("end", report) text_widget.config(state="disabled") btn_frame = tk.Frame(self.advanced_result_frame) btn_frame.pack(pady=10) tk.Button(btn_frame, text="Сохранить отчет", command=lambda: self.save_text_report(report), bg="lightgreen").pack(side="left", padx=5) tk.Button(btn_frame, text="Копировать", command=lambda: self.copy_to_clipboard(report), bg="lightblue").pack(side="left", padx=5) def save_text_report(self, text): """Сохранение текстового отчета в файл.""" filename = filedialog.asksaveasfilename( defaultextension=".txt", filetypes=[("Text files", "*.txt"), ("All files", "*.*")] ) if filename: try: with open(filename, 'w', encoding='utf-8') as f: f.write(text) messagebox.showinfo("Успех", f"Отчет сохранен в {filename}") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось сохранить отчет: {str(e)}") def copy_to_clipboard(self, text): """Копирование текста в буфер обмена.""" self.root.clipboard_clear() self.root.clipboard_append(text) messagebox.showinfo("Успех", "Текст скопирован в буфер обмена") def export_csv(self): """Экспорт данных в CSV.""" filename = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV files", "*.csv"), ("All files", "*.*")] ) if filename: try: export_to_csv(self.operations, filename) messagebox.showinfo("Успех", f"Данные экспортированы в {filename}") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось экспортировать данные: {str(e)}") def export_json(self): """Экспорт данных в JSON.""" filename = filedialog.asksaveasfilename( defaultextension=".json", filetypes=[("JSON files", "*.json"), ("All files", "*.*")] ) if filename: try: export_to_json(self.operations, filename) messagebox.showinfo("Успех", f"Данные экспортированы в {filename}") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось экспортировать данные: {str(e)}") def import_csv(self): """Импорт данных из CSV.""" filename = filedialog.askopenfilename( filetypes=[("CSV files", "*.csv"), ("All files", "*.*")] ) if filename: try: imported_ops = import_from_csv(filename) self.operations.extend(imported_ops) save_operations(self.operations) messagebox.showinfo("Успех", f"Данные импортированы из {filename}") self.update_balance_display() self.update_operations_view() except Exception as e: messagebox.showerror("Ошибка", f"Не удалось импортировать данные: {str(e)}") def import_json(self): """Импорт данных из JSON.""" filename = filedialog.askopenfilename( filetypes=[("JSON files", "*.json"), ("All files", "*.*")] ) if filename: try: imported_ops = import_from_json(filename) self.operations.extend(imported_ops) save_operations(self.operations) messagebox.showinfo("Успех", f"Данные импортированы из {filename}") self.update_balance_display() self.update_operations_view() except Exception as e: messagebox.showerror("Ошибка", f"Не удалось импортировать данные: {str(e)}")