Skip to content
This repository was archived by the owner on Apr 24, 2025. It is now read-only.

Commit 89fb477

Browse files
committed
Initial Implementatin of Expense Tracker GUI with CTk
TODO: Report Generators TODO: Income and Expense Summary Tabs
1 parent 4683c2c commit 89fb477

4 files changed

Lines changed: 520 additions & 0 deletions

File tree

projects/Expense-Tracker/app.py

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
import tkinter as tk
2+
from tkinter import ttk
3+
from tkinter import messagebox
4+
import customtkinter as ctk
5+
from datetime import datetime
6+
7+
from gui_widgets import *
8+
from item import ItemsDB
9+
10+
11+
class App(ctk.CTk):
12+
def __init__(self, db_path, *args, **kwargs):
13+
super().__init__(*args, **kwargs)
14+
15+
# Configure Main Window
16+
self.title("Expense Tracker")
17+
self.geometry("1500x730")
18+
19+
# Configure Layout
20+
self.grid_rowconfigure((0, 1, 2, 3), weight=2)
21+
self.grid_rowconfigure(3, weight=1)
22+
self.grid_columnconfigure((0, 1, 2), weight=1)
23+
24+
# ==============================================================================================================
25+
26+
# Table for the Expense Items
27+
self._items_db = ItemsDB(db_path)
28+
self.items_table = ItemsTable(self, self._items_db)
29+
self.items_table.grid(row=0, column=0, padx=5, pady=5, columnspan=3, sticky='nswe')
30+
self.items_table.bind('<ButtonRelease-1>', self.on_row_selected)
31+
32+
# ==============================================================================================================
33+
# Settings Tab
34+
# Unfortunately, Customtkinter does not support menu bar
35+
self.tab_view_settings = ctk.CTkTabview(self)
36+
self.tab_view_settings.grid(row=1, column=0, padx=5, pady=5, sticky='nswe')
37+
38+
# Edit Tab
39+
self.tab_view_settings.add('Edit')
40+
41+
button = ctk.CTkButton(self.tab_view_settings.tab('Edit'),
42+
text='Deselect Table',
43+
command=lambda: self.items_table.deselect(),
44+
)
45+
button.pack(pady=5)
46+
47+
# # Button for Clearing the Items in the Table (Not Deleting from Json)
48+
# self.btn_clear_all_items = ctk.CTkButton(self.tab_view_settings.tab('Edit'),
49+
# text='Clear Items',
50+
# command=lambda: self.items_table.clear_items(),
51+
# )
52+
# self.btn_clear_all_items.pack(pady=5)
53+
#
54+
# def lesson_option(choice):
55+
# lesson_number = int(choice) - 1
56+
# return lesson_number
57+
#
58+
# optionbox_var = ctk.StringVar(value="1")
59+
# optionbox = ctk.CTkOptionMenu(self.tab_view_settings.tab('Edit'),
60+
# values=['1', '2'],
61+
# variable=optionbox_var,
62+
# command=lesson_option)
63+
# optionbox.pack()
64+
65+
# Report Tab
66+
self.tab_view_settings.add('Report')
67+
68+
# TODO: Create Event-handler for Generating Excel Report
69+
self.btn_generate_excel_report = ctk.CTkButton(self.tab_view_settings.tab('Report'),
70+
text='Generate Excel Report',
71+
command=lambda: print('Generating Excel Report ...'),
72+
)
73+
self.btn_generate_excel_report.pack(pady=5)
74+
75+
# TODO: Create Event-handler for Generating PDF Report
76+
self.btn_generate_pdf_report = ctk.CTkButton(self.tab_view_settings.tab('Report'),
77+
text='Generate PDF Report',
78+
command=lambda: print('Generating PDF Report ...'),
79+
)
80+
self.btn_generate_pdf_report.pack(pady=5)
81+
82+
# Set Default Tab to 'Edit'
83+
self.tab_view_settings.set('Edit')
84+
85+
# ==============================================================================================================
86+
87+
# Frame for Buttons
88+
self.frame_buttons = ctk.CTkFrame(self,
89+
border_color='black'
90+
)
91+
self.frame_buttons.grid(row=1, column=1, padx=5, pady=5)
92+
93+
# Add Button
94+
self.btn_add_item = ctk.CTkButton(self.frame_buttons,
95+
text='Add',
96+
command=self.on_btn_add_clicked,
97+
)
98+
self.btn_add_item.pack(padx=5, pady=5)
99+
100+
# Update Button
101+
self.btn_update_item = ctk.CTkButton(self.frame_buttons,
102+
text='Update',
103+
command=self.on_btn_update_clicked,
104+
)
105+
self.btn_update_item.pack(padx=5, pady=5)
106+
107+
# Delete Button
108+
self.btn_delete_item = ctk.CTkButton(self.frame_buttons,
109+
text='Delete',
110+
command=self.on_btn_delete_clicked,
111+
)
112+
self.btn_delete_item.pack(padx=5, pady=5)
113+
114+
# Clear Form Button
115+
self.btn_clear_form = ctk.CTkButton(self.frame_buttons,
116+
text='Clear Form',
117+
command=self.clear_form,
118+
)
119+
self.btn_clear_form.pack(padx=5, pady=5)
120+
121+
# ==============================================================================================================
122+
123+
# Frame for Form
124+
self.frame_form = ctk.CTkFrame(self,
125+
border_color='black'
126+
)
127+
self.frame_form.grid(row=1, column=2, padx=5, pady=5, ipadx=10, sticky='nswe')
128+
self.frame_form.grid_rowconfigure([i for i in range(6)], weight=1)
129+
self.frame_form.grid_columnconfigure((0, 1), weight=1)
130+
131+
# Entry - Name
132+
label = ctk.CTkLabel(self.frame_form, text="Name: ", fg_color="transparent")
133+
label.grid(row=0, column=0, padx=5, pady=5, sticky='we')
134+
self.entry_name = ctk.CTkEntry(self.frame_form, placeholder_text="Name")
135+
self.entry_name.grid(row=0, column=1, padx=5, pady=5, sticky='we')
136+
137+
# Entry - Amount
138+
label = ctk.CTkLabel(self.frame_form, text="Amount: ", fg_color="transparent")
139+
label.grid(row=1, column=0, padx=5, pady=5, sticky='we')
140+
self.entry_amount = ctk.CTkEntry(self.frame_form, placeholder_text="Amount")
141+
self.entry_amount.grid(row=1, column=1, padx=5, pady=5, sticky='we')
142+
143+
# Entry - Description
144+
label = ctk.CTkLabel(self.frame_form, text="Description: ", fg_color="transparent")
145+
label.grid(row=2, column=0, padx=5, pady=5, sticky='we')
146+
self.entry_description = ctk.CTkEntry(self.frame_form, placeholder_text="Description")
147+
self.entry_description.grid(row=2, column=1, padx=5, pady=5, sticky='we')
148+
149+
# Entry - Date
150+
label = ctk.CTkLabel(self.frame_form, text="Date: ", fg_color="transparent")
151+
label.grid(row=3, column=0, padx=5, pady=5, sticky='we')
152+
self.entry_date = ctk.CTkEntry(self.frame_form, placeholder_text="YYYY-MM-dd")
153+
self.entry_date.grid(row=3, column=1, padx=5, pady=5, sticky='we')
154+
155+
# Entry - Category
156+
label = ctk.CTkLabel(self.frame_form, text="Category: ", fg_color="transparent")
157+
label.grid(row=4, column=0, padx=5, pady=5, sticky='we')
158+
self.entry_category = ctk.CTkEntry(self.frame_form, placeholder_text="Category")
159+
self.entry_category.grid(row=4, column=1, padx=5, pady=5, sticky='we')
160+
161+
# Entry - Subcategory
162+
label = ctk.CTkLabel(self.frame_form, text="Subcategory: ", fg_color="transparent")
163+
label.grid(row=5, column=0, padx=5, pady=5, sticky='we')
164+
self.entry_subcategory = ctk.CTkEntry(self.frame_form, placeholder_text="Subcategory")
165+
self.entry_subcategory.grid(row=5, column=1, padx=5, pady=5, sticky='we')
166+
167+
# ==============================================================================================================
168+
169+
# CTkTabview for Visualizations
170+
self.tab_view_visuals = ctk.CTkTabview(self)
171+
self.tab_view_visuals.grid(row=2, column=0, padx=5, pady=5, columnspan=3, sticky='nswe')
172+
self.tab_view_visuals.add("Expense Summary") # Expense Summary Tab
173+
self.tab_view_visuals.add("Income Summary") # add tab at the end
174+
self.tab_view_visuals.set("Expense Summary") # set currently visible tab
175+
176+
# TODO: Expense Summary Tab Contents
177+
label = ctk.CTkLabel(master=self.tab_view_visuals.tab('Expense Summary'), text="Under Construction",
178+
fg_color="red")
179+
label.pack()
180+
181+
# TODO: Income Summary Tab Contents
182+
183+
# ==============================================================================================================
184+
# Other Widget-Event-Callbacks Bindings
185+
self.protocol("WM_DELETE_WINDOW", self.on_window_close)
186+
187+
def on_window_close(self):
188+
# Reference: https://stackoverflow.com/questions/111155/how-do-i-handle-the-window-close-event-in-tkinter
189+
print('Window Closing ...')
190+
self.destroy()
191+
192+
def _get_form_values(self):
193+
"""Returns a Dictionary of the Form Values"""
194+
return {
195+
'name': self.entry_name.get(),
196+
'amount': self.entry_amount.get(),
197+
'description': self.entry_description.get(),
198+
'date': self.entry_date.get(),
199+
'category': self.entry_category.get(),
200+
'subcategory': self.entry_subcategory.get(),
201+
}
202+
203+
def _validate_form_values(self):
204+
# TODO: Add Form Entry Validations
205+
pass
206+
207+
def clear_form(self):
208+
self.entry_name.delete(0, last_index=len(self.entry_name.get()))
209+
self.entry_amount.delete(0, last_index=len(self.entry_amount.get()))
210+
self.entry_description.delete(0, last_index=len(self.entry_description.get()))
211+
self.entry_date.delete(0, last_index=len(self.entry_date.get()))
212+
self.entry_category.delete(0, last_index=len(self.entry_category.get()))
213+
self.entry_subcategory.delete(0, last_index=len(self.entry_subcategory.get()))
214+
215+
def fill_form(self, name, amount, description, date, category='None', subcategory='None'):
216+
self.clear_form()
217+
self.entry_name.insert(0, name)
218+
self.entry_amount.insert(0, amount)
219+
self.entry_date.insert(0, date)
220+
self.entry_description.insert(0, description)
221+
self.entry_category.insert(0, category)
222+
self.entry_subcategory.insert(0, subcategory)
223+
224+
# self.entry_name.configure(placeholder_text=name)
225+
# self.entry_amount.configure(placeholder_text=amount)
226+
# self.entry_date.configure(textvariable=date)
227+
# self.entry_description.configure(placeholder_text=description)
228+
# self.entry_category.configure(placeholder_text=category)
229+
# self.entry_subcategory.configure(placeholder_text=subcategory)
230+
231+
def on_btn_add_clicked(self):
232+
new_item = self._create_item_from_form()
233+
234+
if new_item is not None:
235+
self.items_table.add_item(new_item)
236+
else:
237+
messagebox.showerror('Error', 'Invalid Item Data')
238+
239+
self.clear_form()
240+
241+
def on_btn_update_clicked(self):
242+
# Get the (one) selected item
243+
selected_item = self.items_table.focus()
244+
item_dct = self.items_table.item(selected_item)
245+
values = item_dct['values']
246+
item_id = item_dct['text']
247+
row = [item_id] + list(values)
248+
249+
try:
250+
item_obj = self._create_item(row)
251+
except IndexError:
252+
self.items_table.deselect()
253+
messagebox.showerror('Error', 'Invalid Action')
254+
return
255+
256+
# Reconstruct the Forms
257+
try:
258+
item_obj.name = self.entry_name.get().strip()
259+
item_obj.amount = float(self.entry_amount.get().strip())
260+
item_obj.description = self.entry_description.get().strip()
261+
item_obj.date = datetime.strptime(self.entry_date.get().strip(), '%Y-%m-%d %H:%M:%S')
262+
item_obj.category = \
263+
None if self.entry_category.get() == 'None' else \
264+
Category(self.entry_category.get().strip()) if self.entry_subcategory.get().strip() == 'None' else \
265+
Category(self.entry_category.get().strip(), self.entry_subcategory.get().strip())
266+
267+
except ValueError as value_error:
268+
messagebox.showerror('Error', str(value_error))
269+
return
270+
271+
# Update Local Data and View
272+
self.items_table.update_item(item_obj)
273+
274+
self.clear_form()
275+
276+
def on_btn_delete_clicked(self):
277+
yes_or_no = messagebox.askyesno("Delete Item", "Do you want to continue?")
278+
279+
if not yes_or_no:
280+
self.items_table.deselect()
281+
return
282+
283+
self.clear_form()
284+
285+
if len(self.items_table.selection()) == 0:
286+
return
287+
288+
items = (self.items_table.item(item) for item in self.items_table.selection())
289+
items = ([item['text']] + list(item['values']) for item in items)
290+
291+
item_objs = [self._create_item(item) for item in items]
292+
293+
self.items_table.deletes_item(item_objs)
294+
295+
def _create_item(self, inp_item):
296+
# inp_item: List[str|int|float]
297+
try:
298+
item_id = inp_item[0]
299+
item_name = inp_item[1]
300+
item_amount = inp_item[2]
301+
item_description = inp_item[3]
302+
item_date = inp_item[4]
303+
item_category = inp_item[5]
304+
item_subcategory = inp_item[6]
305+
306+
# Clean Date
307+
item_date = datetime.strptime(item_date, '%Y-%m-%d %H:%M:%S')
308+
309+
if item_category in ['None', '']:
310+
category = None
311+
else:
312+
if item_subcategory in ['None', '']:
313+
category = Category(item_category)
314+
else:
315+
category = Category(item_category, item_subcategory)
316+
317+
item_obj = Item(
318+
item_id,
319+
item_name,
320+
item_amount,
321+
item_description,
322+
item_date,
323+
category
324+
)
325+
326+
return item_obj
327+
except ValueError:
328+
return None
329+
330+
def _create_item_from_form(self) -> Item | None:
331+
try:
332+
if self.entry_category.get().strip() in ['None', '']:
333+
category = None
334+
else:
335+
entry_category = self.entry_category.get().strip()
336+
if self.entry_subcategory.get() in ['None', '']:
337+
category = Category(entry_category)
338+
else:
339+
entry_subcategory = self.entry_subcategory.get().strip()
340+
category = Category(entry_category, entry_subcategory)
341+
342+
temp_item = Item.create(
343+
self.entry_name.get(),
344+
self.entry_amount.get(),
345+
self.entry_description.get(),
346+
self.entry_date.get(),
347+
category
348+
)
349+
return temp_item
350+
except ValueError as value_error:
351+
print(value_error)
352+
return None
353+
354+
def on_row_selected(self, e):
355+
# Reference: https://stackoverflow.com/questions/30614279/tkinter-treeview-get-selected-item-values
356+
357+
# Set the Selected Item for the TreeView
358+
selected_item = self.items_table.focus()
359+
item_dct = self.items_table.item(selected_item)
360+
361+
values = item_dct['values']
362+
item_id = item_dct['text']
363+
row = [item_id] + list(values)
364+
print(row)
365+
366+
self.fill_form(
367+
values[0],
368+
values[1],
369+
values[2],
370+
values[3],
371+
category=values[4],
372+
subcategory=values[5]
373+
)
374+
375+
# def on_rows_selected(self, e):
376+
# # Reference: https://stackoverflow.com/questions/48867800/tk-treeview-focus-how-do-i-get-multiple-selected-lines
377+
# selected_items = self.items_table.selection() # Gets the Selected Items from TreeView Table
378+
# # self.items_table.selection_set(selected_items)
379+
# selected_items = [self.items_table.item(selected_item) for selected_item in selected_items]

0 commit comments

Comments
 (0)