Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import tkinter as tk
- from tkinter import ttk
- from datetime import datetime
- from datetime import timedelta
- import json
- import os
- import requests
- from matplotlib.figure import Figure
- from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
- root = tk.Tk()
- root.title("Personal Finance Tracker")
- root.geometry("700x500")
- transactions = []
- currencies = ["USD", "EUR", "CAD", "JPY", "CNY", "GBP"]
- exchange_rates = {}
- BASE_CURRENCY = "USD"
- # --- Functions ---
- def fetch_exchange_rates(base=BASE_CURRENCY):
- url = f"https://api.frankfurter.app/latest?from={base}"
- try:
- response = requests.get(url)
- data = response.json()
- return data["rates"]
- except Exception as e:
- print("Failed to fetch exchange rates:", e)
- return {}
- exchange_rates.update(fetch_exchange_rates("USD"))
- def save_transactions():
- with open("transactions.json", "w") as f:
- json.dump(transactions, f, indent=4)
- def load_transactions():
- if os.path.exists("transactions.json"):
- with open("transactions.json", "r") as f:
- loaded = json.load(f)
- transactions.extend(loaded)
- filter_and_display()
- def add_transactions():
- description = desc_entry.get()
- amount = amount_entry.get()
- type = type_var.get()
- category = category_var.get()
- currency = currency_var.get()
- date = datetime.now().strftime("%Y-%m-%d") # %d-%m-%Y
- try:
- amount = float(amount)
- except ValueError:
- result_label.config(text="Amount must be a number!", fg="red")
- return
- if not description or not category or type not in ["Income", "Expense"]:
- result_label.config(text="Please fill all the fields.", fg="red")
- return
- # Store transaction and update table
- transactions.append([date, description, category, f"{amount:.2f}", currency, type])
- # tree.insert("", "end", values=(date, description, category, f"{amount:.2f}", currency, type))
- # update_summary()
- filter_and_display()
- clear_fields()
- save_transactions()
- result_label.config(text="Transaction added!", fg="green")
- def update_summary():
- target_currency = default_currency_var.get()
- rates = exchange_rates
- income = 0
- expense = 0
- displayed = [tree.item(i, "values") for i in tree.get_children()]
- for t in displayed:
- amount = float(t[3])
- currency = t[4]
- rate_currency = 1.0 if currency == BASE_CURRENCY else rates.get(currency)
- rate_target = 1.0 if target_currency == BASE_CURRENCY else rates.get(target_currency)
- if rate_currency is None or rate_target is None:
- continue
- converted = amount / rate_currency * rate_target
- if t[5] == "Income":
- income += converted
- else:
- expense += converted
- balance = income - expense
- summary_label.config(
- text=f"Income: {target_currency} {income:.2f} "
- f"Expense: {target_currency} {expense:.2f} "
- f"Balance: {target_currency} {balance:.2f}"
- )
- def convert_currency(amount, from_cur, to_cur, rates, base):
- if from_cur == to_cur:
- return float(amount)
- # rate relative to base; 1 base -> r units of cur
- def rate_vs_base(cur):
- if cur == base:
- return 1.0
- r = rates.get(cur)
- if r is None:
- raise ValueError(f"Missing FX rate for {cur} relative to {base}")
- return float(r)
- # A -> base
- if from_cur == base:
- amt_in_base = float(amount)
- else:
- r_from = rate_vs_base(from_cur)
- amt_in_base = float(amount) / r_from # because r_from = units of 'from_cur' per 1 base
- # base -> B
- if to_cur == base:
- return amt_in_base
- r_to = rate_vs_base(to_cur)
- return amt_in_base * r_to
- def compute_category_totals(transactions_list, selected_currency, rates, base):
- totals = {}
- for row in transactions_list:
- try:
- t_date, _desc, category, amount_str, from_cur, t_type = row
- except Exception:
- continue
- if str(t_type).lower() != "expense":
- continue
- try:
- amt = float(amount_str)
- except (TypeError, ValueError):
- continue
- try:
- amt_conv = convert_currency(amt, from_cur, selected_currency, rates, base)
- except Exception as e:
- print(f"[FX WARN] {e}; skipping row: {row}")
- continue
- totals[category] = totals.get(category, 0.0) + amt_conv
- return totals
- def update_charts():
- # clear charts
- pie_ax.clear()
- bar_ax.clear()
- # get displayed transactions straight from the table
- displayed = [tree.item(i, "values") for i in tree.get_children()]
- # convert expenses to the currently selected summary currency
- target_currency = default_currency_var.get()
- category_totals = compute_category_totals(displayed, target_currency, exchange_rates, BASE_CURRENCY)
- if not category_totals:
- pie_ax.text(0.5, 0.5, 'No expenses', ha='center', va='center', fontsize=12)
- bar_ax.text(0.5, 0.5, 'No expenses', ha='center', va='center', fontsize=12)
- else:
- # pie chart
- pie_ax.pie(category_totals.values(), labels=category_totals.keys(), autopct="%1.1f%%")
- pie_ax.set_title(f"Expenses by Category ({target_currency})")
- # bar chart
- bar_ax.bar(category_totals.keys(), category_totals.values(), color="skyblue")
- bar_ax.set_ylabel(f"Amount ({target_currency})")
- bar_ax.set_xlabel("Expenses by Category")
- bar_ax.tick_params(axis='x', rotation=45)
- chart_canvas.draw()
- def clear_fields():
- desc_entry.delete(0, tk.END)
- amount_entry.delete(0, tk.END)
- type_var.set("Income")
- category_var.set("General")
- currency_var.set("USD")
- def delete_transaction():
- selected_item = tree.selection()
- if not selected_item:
- result_label.config(text="No transaction selected.", fg="red")
- return
- item = selected_item[0]
- values = list(tree.item(item, "values"))
- try:
- transactions.remove(values)
- tree.delete(item)
- save_transactions()
- update_summary()
- update_charts() # keep charts in sync after deletion
- result_label.config(text="Transaction deleted.", fg="green")
- except ValueError:
- for i, t in enumerate(transactions):
- print(f"{i}: {t==values} - {t} vs {values}")
- result_label.config(text="Could not delete transaction.", fg="red")
- def filter_and_display():
- for transaction in tree.get_children():
- tree.delete(transaction)
- now = datetime.now()
- selected = filter_var.get()
- filtered = []
- # ["Today", "last 3 days", "last 7 days", "last 30 days", "all transactions"]
- if selected == "Today":
- cutoff = now.replace(hour=0, minute=0, second=0, microsecond=0)
- elif selected == "last 3 days":
- cutoff = now - timedelta(days=3)
- elif selected == "last 7 days":
- cutoff = now - timedelta(days=7)
- elif selected == "last 30 days":
- cutoff = now - timedelta(days=30)
- elif selected == "all transactions":
- cutoff = datetime.min
- for t in transactions:
- t_date = datetime.strptime(t[0], "%Y-%m-%d")
- if t_date >= cutoff:
- filtered.append(t)
- for t in filtered:
- tree.insert("", "end", values=t)
- update_summary()
- update_charts()
- # --- Input Form ---
- form_frame = tk.Frame(root, pady=10)
- form_frame.pack()
- # Row 0 – Description and Type
- tk.Label(form_frame, text="Description").grid(row=0, column=0, padx=5, sticky="e")
- desc_entry = tk.Entry(form_frame, width=25)
- desc_entry.grid(row=0, column=1, padx=5)
- tk.Label(form_frame, text="Type").grid(row=0, column=2, padx=5, sticky="e")
- type_var = tk.StringVar(value="Income")
- ttk.Combobox(form_frame, textvariable=type_var, values=["Income", "Expense"], width=10).grid(row=0, column=3, padx=5)
- # Row 1 – Amount
- tk.Label(form_frame, text="Amount").grid(row=1, column=0, padx=5, sticky="e")
- amount_entry = tk.Entry(form_frame, width=25)
- amount_entry.grid(row=1, column=1, padx=5, sticky="w")
- # Row 2 – Currency below Amount
- tk.Label(form_frame, text="Currency").grid(row=2, column=0, padx=5, sticky="e")
- currency_var = tk.StringVar(value="USD")
- ttk.Combobox(form_frame, textvariable=currency_var, values=currencies, width=10).grid(row=2, column=1, padx=5, sticky="w")
- # Row 2 – Category and Add button
- tk.Label(form_frame, text="Category").grid(row=1, column=2, padx=5, sticky="e")
- category_var = tk.StringVar(value="General")
- ttk.Combobox(form_frame, textvariable=category_var, values=["Meals & Dining", "Transport", "Salary", "Bills", "General"], width=10).grid(row=1, column=3, padx=5)
- # Add Button
- tk.Button(form_frame, text="Add", width=20, command=add_transactions).grid(row=2, column=2, columnspan=2, padx=10)
- # Delete Button
- tk.Button(form_frame, text="Delete Selected", width=20, command=delete_transaction).grid(row=3, column=2, columnspan=2, padx=10, pady=5)
- # Result label
- result_label = tk.Label(form_frame, text="", fg="green")
- result_label.grid(row=3, column=0, columnspan=5, pady=5)
- # --- Transaction Table ---
- table_frame = tk.Frame(root)
- table_frame.pack(pady=10)
- columns = ("Date", "Description", "Category", "Amount", "Currency", "Type")
- tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=10)
- for col in columns:
- tree.heading(col, text=col)
- tree.column(col, anchor=tk.CENTER, width=130)
- tree.pack()
- currency_frame = tk.Frame(root)
- currency_frame.pack(pady=5)
- tk.Label(currency_frame, text="Display summary in:").pack(side="left", padx=5)
- default_currency_var = tk.StringVar(value="USD")
- default_currency_menu = ttk.Combobox(currency_frame, textvariable=default_currency_var, values=currencies, width=10)
- default_currency_menu.pack(side="left", padx=10)
- # transaction history
- tk.Label(currency_frame, text="Display : ").pack(side="left", padx=5)
- filter_var = tk.StringVar(value="last 7 days")
- time_options = ["Today", "last 3 days", "last 7 days", "last 30 days", "all transactions"]
- filter_menu = ttk.Combobox(currency_frame, textvariable=filter_var, values=time_options, width=10)
- filter_menu.pack(side="left")
- def on_currency_change(*args):
- update_summary()
- update_charts() # ensure charts follow selected currency
- default_currency_var.trace_add("write", on_currency_change)
- filter_var.trace_add("write", lambda *args: filter_and_display())
- # --- Summary ---
- summary_label = tk.Label(root, text="Income: USD 0.00 Expense: USD 0.00 Balance: USD 0.00", font=("Arial", 12, "bold"))
- summary_label.pack(pady=10)
- # --- Chart Area ---
- chart_frame = tk.Frame(root)
- chart_frame.pack(fill="both", expand=True, pady=10)
- fig = Figure(figsize=(7, 3), dpi=100)
- pie_ax = fig.add_subplot(121)
- bar_ax = fig.add_subplot(122)
- chart_canvas = FigureCanvasTkAgg(fig, master=chart_frame)
- chart_canvas.get_tk_widget().pack(fill="both", expand=True)
- # --- Run the App ---
- load_transactions()
- root.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment