PhilMiles

Personal Finance App w. adapted currencies

Aug 15th, 2025 (edited)
65
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.12 KB | Source Code | 0 0
  1. import tkinter as tk
  2. from tkinter import ttk
  3. from datetime import datetime
  4. from datetime import timedelta
  5. import json
  6. import os
  7. import requests
  8. from matplotlib.figure import Figure
  9. from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
  10.  
  11.  
  12. root = tk.Tk()
  13. root.title("Personal Finance Tracker")
  14. root.geometry("700x500")
  15.  
  16. transactions = []
  17. currencies = ["USD", "EUR", "CAD", "JPY", "CNY", "GBP"]
  18. exchange_rates = {}
  19. BASE_CURRENCY = "USD"
  20.  
  21.  
  22. # --- Functions ---
  23. def fetch_exchange_rates(base=BASE_CURRENCY):
  24.     url = f"https://api.frankfurter.app/latest?from={base}"
  25.     try:
  26.         response = requests.get(url)
  27.         data = response.json()
  28.         return data["rates"]
  29.     except Exception as e:
  30.         print("Failed to fetch exchange rates:", e)
  31.         return {}
  32.  
  33. exchange_rates.update(fetch_exchange_rates("USD"))
  34.  
  35. def save_transactions():
  36.     with open("transactions.json", "w") as f:
  37.         json.dump(transactions, f, indent=4)
  38.  
  39. def load_transactions():
  40.     if os.path.exists("transactions.json"):
  41.         with open("transactions.json", "r") as f:
  42.             loaded = json.load(f)
  43.             transactions.extend(loaded)
  44.             filter_and_display()
  45.  
  46.  
  47. def add_transactions():
  48.     description = desc_entry.get()
  49.     amount = amount_entry.get()
  50.     type = type_var.get()
  51.     category = category_var.get()
  52.     currency = currency_var.get()
  53.     date = datetime.now().strftime("%Y-%m-%d") # %d-%m-%Y
  54.  
  55.     try:
  56.         amount = float(amount)
  57.     except ValueError:
  58.         result_label.config(text="Amount must be a number!", fg="red")
  59.         return
  60.  
  61.     if not description or not category or type not in ["Income", "Expense"]:
  62.         result_label.config(text="Please fill all the fields.", fg="red")
  63.         return
  64.  
  65.     # Store transaction and update table
  66.     transactions.append([date, description, category, f"{amount:.2f}", currency, type])
  67.  
  68.     # tree.insert("", "end", values=(date, description, category, f"{amount:.2f}", currency, type))
  69.     # update_summary()
  70.     filter_and_display()
  71.  
  72.     clear_fields()
  73.     save_transactions()
  74.     result_label.config(text="Transaction added!", fg="green")
  75.  
  76. def update_summary():
  77.     target_currency = default_currency_var.get()
  78.     rates = exchange_rates
  79.  
  80.     income = 0
  81.     expense = 0
  82.  
  83.     displayed = [tree.item(i, "values") for i in tree.get_children()]
  84.     for t in displayed:
  85.         amount = float(t[3])
  86.         currency = t[4]
  87.  
  88.         rate_currency = 1.0 if currency == BASE_CURRENCY else rates.get(currency)
  89.         rate_target = 1.0 if target_currency == BASE_CURRENCY else rates.get(target_currency)
  90.  
  91.         if rate_currency is None or rate_target is None:
  92.             continue
  93.  
  94.         converted = amount / rate_currency * rate_target
  95.  
  96.         if t[5] == "Income":
  97.             income += converted
  98.         else:
  99.             expense += converted
  100.  
  101.     balance = income - expense
  102.     summary_label.config(
  103.         text=f"Income: {target_currency} {income:.2f}    "
  104.              f"Expense: {target_currency} {expense:.2f}    "
  105.              f"Balance: {target_currency} {balance:.2f}"
  106.     )
  107.  
  108. def convert_currency(amount, from_cur, to_cur, rates, base):
  109.     if from_cur == to_cur:
  110.         return float(amount)
  111.  
  112.     # rate relative to base; 1 base -> r units of cur
  113.     def rate_vs_base(cur):
  114.         if cur == base:
  115.             return 1.0
  116.         r = rates.get(cur)
  117.         if r is None:
  118.             raise ValueError(f"Missing FX rate for {cur} relative to {base}")
  119.         return float(r)
  120.  
  121.     # A -> base
  122.     if from_cur == base:
  123.         amt_in_base = float(amount)
  124.     else:
  125.         r_from = rate_vs_base(from_cur)
  126.         amt_in_base = float(amount) / r_from  # because r_from = units of 'from_cur' per 1 base
  127.  
  128.     # base -> B
  129.     if to_cur == base:
  130.         return amt_in_base
  131.     r_to = rate_vs_base(to_cur)
  132.     return amt_in_base * r_to
  133.  
  134. def compute_category_totals(transactions_list, selected_currency, rates, base):
  135.     totals = {}
  136.     for row in transactions_list:
  137.         try:
  138.             t_date, _desc, category, amount_str, from_cur, t_type = row
  139.         except Exception:
  140.             continue
  141.  
  142.         if str(t_type).lower() != "expense":
  143.             continue
  144.  
  145.         try:
  146.             amt = float(amount_str)
  147.         except (TypeError, ValueError):
  148.             continue
  149.  
  150.         try:
  151.             amt_conv = convert_currency(amt, from_cur, selected_currency, rates, base)
  152.         except Exception as e:
  153.             print(f"[FX WARN] {e}; skipping row: {row}")
  154.             continue
  155.  
  156.         totals[category] = totals.get(category, 0.0) + amt_conv
  157.     return totals
  158.  
  159. def update_charts():
  160.     # clear charts
  161.     pie_ax.clear()
  162.     bar_ax.clear()
  163.  
  164.     # get displayed transactions straight from the table
  165.     displayed = [tree.item(i, "values") for i in tree.get_children()]
  166.  
  167.     # convert expenses to the currently selected summary currency
  168.     target_currency = default_currency_var.get()
  169.     category_totals = compute_category_totals(displayed, target_currency, exchange_rates, BASE_CURRENCY)
  170.  
  171.     if not category_totals:
  172.         pie_ax.text(0.5, 0.5, 'No expenses', ha='center', va='center', fontsize=12)
  173.         bar_ax.text(0.5, 0.5, 'No expenses', ha='center', va='center', fontsize=12)
  174.     else:
  175.         # pie chart
  176.         pie_ax.pie(category_totals.values(), labels=category_totals.keys(), autopct="%1.1f%%")
  177.         pie_ax.set_title(f"Expenses by Category ({target_currency})")
  178.  
  179.         # bar chart
  180.         bar_ax.bar(category_totals.keys(), category_totals.values(), color="skyblue")
  181.         bar_ax.set_ylabel(f"Amount ({target_currency})")
  182.         bar_ax.set_xlabel("Expenses by Category")
  183.         bar_ax.tick_params(axis='x', rotation=45)
  184.  
  185.     chart_canvas.draw()
  186.  
  187.  
  188. def clear_fields():
  189.     desc_entry.delete(0, tk.END)
  190.     amount_entry.delete(0, tk.END)
  191.     type_var.set("Income")
  192.     category_var.set("General")
  193.     currency_var.set("USD")
  194.  
  195. def delete_transaction():
  196.     selected_item = tree.selection()
  197.     if not selected_item:
  198.         result_label.config(text="No transaction selected.", fg="red")
  199.         return
  200.  
  201.     item = selected_item[0]
  202.     values = list(tree.item(item, "values"))
  203.  
  204.     try:
  205.         transactions.remove(values)
  206.         tree.delete(item)
  207.         save_transactions()
  208.         update_summary()
  209.         update_charts()  # keep charts in sync after deletion
  210.         result_label.config(text="Transaction deleted.", fg="green")
  211.     except ValueError:
  212.         for i, t in enumerate(transactions):
  213.             print(f"{i}: {t==values} - {t} vs {values}")
  214.         result_label.config(text="Could not delete transaction.", fg="red")
  215.  
  216. def filter_and_display():
  217.     for transaction in tree.get_children():
  218.         tree.delete(transaction)
  219.  
  220.     now = datetime.now()
  221.     selected = filter_var.get()
  222.     filtered = []
  223.  
  224.     # ["Today", "last 3 days", "last 7 days", "last 30 days", "all transactions"]
  225.     if selected == "Today":
  226.         cutoff = now.replace(hour=0, minute=0, second=0, microsecond=0)
  227.     elif selected == "last 3 days":
  228.         cutoff = now - timedelta(days=3)
  229.     elif selected == "last 7 days":
  230.         cutoff = now - timedelta(days=7)
  231.     elif selected == "last 30 days":
  232.         cutoff = now - timedelta(days=30)
  233.     elif selected == "all transactions":
  234.         cutoff = datetime.min
  235.  
  236.     for t in transactions:
  237.         t_date = datetime.strptime(t[0], "%Y-%m-%d")
  238.  
  239.         if t_date >= cutoff:
  240.             filtered.append(t)
  241.  
  242.     for t in filtered:
  243.         tree.insert("", "end", values=t)
  244.  
  245.     update_summary()
  246.     update_charts()
  247.  
  248. # --- Input Form ---
  249. form_frame = tk.Frame(root, pady=10)
  250. form_frame.pack()
  251.  
  252. # Row 0 – Description and Type
  253. tk.Label(form_frame, text="Description").grid(row=0, column=0, padx=5, sticky="e")
  254. desc_entry = tk.Entry(form_frame, width=25)
  255. desc_entry.grid(row=0, column=1, padx=5)
  256.  
  257. tk.Label(form_frame, text="Type").grid(row=0, column=2, padx=5, sticky="e")
  258. type_var = tk.StringVar(value="Income")
  259. ttk.Combobox(form_frame, textvariable=type_var, values=["Income", "Expense"], width=10).grid(row=0, column=3, padx=5)
  260.  
  261. # Row 1 – Amount
  262. tk.Label(form_frame, text="Amount").grid(row=1, column=0, padx=5, sticky="e")
  263. amount_entry = tk.Entry(form_frame, width=25)
  264. amount_entry.grid(row=1, column=1, padx=5, sticky="w")
  265.  
  266. # Row 2 – Currency below Amount
  267. tk.Label(form_frame, text="Currency").grid(row=2, column=0, padx=5, sticky="e")
  268. currency_var = tk.StringVar(value="USD")
  269. ttk.Combobox(form_frame, textvariable=currency_var, values=currencies, width=10).grid(row=2, column=1, padx=5, sticky="w")
  270.  
  271. # Row 2 – Category and Add button
  272. tk.Label(form_frame, text="Category").grid(row=1, column=2, padx=5, sticky="e")
  273. category_var = tk.StringVar(value="General")
  274. ttk.Combobox(form_frame, textvariable=category_var, values=["Meals & Dining", "Transport", "Salary", "Bills", "General"], width=10).grid(row=1, column=3, padx=5)
  275.  
  276. # Add Button
  277. tk.Button(form_frame, text="Add", width=20, command=add_transactions).grid(row=2, column=2, columnspan=2, padx=10)
  278.  
  279. # Delete Button
  280. tk.Button(form_frame, text="Delete Selected", width=20, command=delete_transaction).grid(row=3, column=2, columnspan=2, padx=10, pady=5)
  281.  
  282. # Result label
  283. result_label = tk.Label(form_frame, text="", fg="green")
  284. result_label.grid(row=3, column=0, columnspan=5, pady=5)
  285.  
  286. # --- Transaction Table ---
  287. table_frame = tk.Frame(root)
  288. table_frame.pack(pady=10)
  289.  
  290. columns = ("Date", "Description", "Category", "Amount", "Currency", "Type")
  291. tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=10)
  292.  
  293. for col in columns:
  294.     tree.heading(col, text=col)
  295.     tree.column(col, anchor=tk.CENTER, width=130)
  296.  
  297. tree.pack()
  298.  
  299. currency_frame = tk.Frame(root)
  300. currency_frame.pack(pady=5)
  301.  
  302. tk.Label(currency_frame, text="Display summary in:").pack(side="left", padx=5)
  303. default_currency_var = tk.StringVar(value="USD")
  304. default_currency_menu = ttk.Combobox(currency_frame, textvariable=default_currency_var, values=currencies, width=10)
  305. default_currency_menu.pack(side="left", padx=10)
  306.  
  307. # transaction history
  308. tk.Label(currency_frame, text="Display : ").pack(side="left", padx=5)
  309. filter_var = tk.StringVar(value="last 7 days")
  310. time_options = ["Today", "last 3 days", "last 7 days", "last 30 days", "all transactions"]
  311. filter_menu = ttk.Combobox(currency_frame, textvariable=filter_var, values=time_options, width=10)
  312. filter_menu.pack(side="left")
  313.  
  314.  
  315. def on_currency_change(*args):
  316.     update_summary()
  317.     update_charts()  # ensure charts follow selected currency
  318.  
  319. default_currency_var.trace_add("write", on_currency_change)
  320. filter_var.trace_add("write", lambda *args: filter_and_display())
  321.  
  322. # --- Summary ---
  323. summary_label = tk.Label(root, text="Income: USD 0.00   Expense: USD 0.00   Balance:  USD 0.00", font=("Arial", 12, "bold"))
  324. summary_label.pack(pady=10)
  325.  
  326. # --- Chart Area ---
  327. chart_frame = tk.Frame(root)
  328. chart_frame.pack(fill="both", expand=True, pady=10)
  329.  
  330. fig = Figure(figsize=(7, 3), dpi=100)
  331. pie_ax = fig.add_subplot(121)
  332. bar_ax = fig.add_subplot(122)
  333.  
  334. chart_canvas = FigureCanvasTkAgg(fig, master=chart_frame)
  335. chart_canvas.get_tk_widget().pack(fill="both", expand=True)
  336.  
  337. # --- Run the App ---
  338. load_transactions()
  339. root.mainloop()
  340.  
Advertisement
Add Comment
Please, Sign In to add comment