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

Commit 4672d90

Browse files
committed
feat: completed initial prototype for expense-tracker
todo - Currency Converter todo - local settings and ui todo - separation of expense and income
1 parent 89fb477 commit 4672d90

5 files changed

Lines changed: 191 additions & 87 deletions

File tree

projects/Expense-Tracker/app.py

Lines changed: 38 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
from tkinter import ttk
33
from tkinter import messagebox
44
import customtkinter as ctk
5+
56
from datetime import datetime
7+
from pathlib import Path
68

7-
from gui_widgets import *
8-
from item import ItemsDB
9+
from gui_widgets import ItemsTable, SummaryByCategoryPivotTable
10+
from item import ItemsDB, Category, Item
11+
from expense_income_stats import ExcelReport, PdfReport
912

1013

1114
class App(ctk.CTk):
@@ -44,38 +47,24 @@ def __init__(self, db_path, *args, **kwargs):
4447
)
4548
button.pack(pady=5)
4649

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()
50+
button = ctk.CTkButton(self.tab_view_settings.tab('Edit'),
51+
text='Update Summaries',
52+
command=self.update_pivot_tables,
53+
)
54+
button.pack(pady=5)
6455

6556
# Report Tab
6657
self.tab_view_settings.add('Report')
6758

68-
# TODO: Create Event-handler for Generating Excel Report
6959
self.btn_generate_excel_report = ctk.CTkButton(self.tab_view_settings.tab('Report'),
7060
text='Generate Excel Report',
71-
command=lambda: print('Generating Excel Report ...'),
61+
command=self.generate_excel_report,
7262
)
7363
self.btn_generate_excel_report.pack(pady=5)
7464

75-
# TODO: Create Event-handler for Generating PDF Report
7665
self.btn_generate_pdf_report = ctk.CTkButton(self.tab_view_settings.tab('Report'),
7766
text='Generate PDF Report',
78-
command=lambda: print('Generating PDF Report ...'),
67+
command=self.generate_pdf_report,
7968
)
8069
self.btn_generate_pdf_report.pack(pady=5)
8170

@@ -169,21 +158,32 @@ def __init__(self, db_path, *args, **kwargs):
169158
# CTkTabview for Visualizations
170159
self.tab_view_visuals = ctk.CTkTabview(self)
171160
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
175161

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()
162+
expense_tab = self.tab_view_visuals.add(
163+
"Summary By Category") # Summary By Category Tab
164+
165+
self.tab_view_visuals.set("Summary By Category") # set currently visible tab
180166

181-
# TODO: Income Summary Tab Contents
167+
# Summary By Category Tab Contents
168+
self.pivot_table = SummaryByCategoryPivotTable(self.tab_view_visuals.tab('Summary By Category'))
182169

183170
# ==============================================================================================================
184171
# Other Widget-Event-Callbacks Bindings
185172
self.protocol("WM_DELETE_WINDOW", self.on_window_close)
186173

174+
def update_pivot_tables(self):
175+
self.pivot_table.update_pivot_table()
176+
177+
def generate_excel_report(self):
178+
excel_path = str(Path(__file__).resolve().parent / 'report.xlsx')
179+
report = ExcelReport(excel_path, self._items_db)
180+
report.generate_report()
181+
182+
def generate_pdf_report(self):
183+
pdf_path = str(Path(__file__).resolve().parent / 'report.pdf')
184+
report = PdfReport(pdf_path, self._items_db)
185+
report.generate_report()
186+
187187
def on_window_close(self):
188188
# Reference: https://stackoverflow.com/questions/111155/how-do-i-handle-the-window-close-event-in-tkinter
189189
print('Window Closing ...')
@@ -200,10 +200,6 @@ def _get_form_values(self):
200200
'subcategory': self.entry_subcategory.get(),
201201
}
202202

203-
def _validate_form_values(self):
204-
# TODO: Add Form Entry Validations
205-
pass
206-
207203
def clear_form(self):
208204
self.entry_name.delete(0, last_index=len(self.entry_name.get()))
209205
self.entry_amount.delete(0, last_index=len(self.entry_amount.get()))
@@ -221,13 +217,6 @@ def fill_form(self, name, amount, description, date, category='None', subcategor
221217
self.entry_category.insert(0, category)
222218
self.entry_subcategory.insert(0, subcategory)
223219

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-
231220
def on_btn_add_clicked(self):
232221
new_item = self._create_item_from_form()
233222

@@ -259,10 +248,9 @@ def on_btn_update_clicked(self):
259248
item_obj.amount = float(self.entry_amount.get().strip())
260249
item_obj.description = self.entry_description.get().strip()
261250
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())
251+
item_obj.category = None if self.entry_category.get() == 'None' else Category(
252+
self.entry_category.get().strip()) if self.entry_subcategory.get().strip() == 'None' else Category(
253+
self.entry_category.get().strip(), self.entry_subcategory.get().strip())
266254

267255
except ValueError as value_error:
268256
messagebox.showerror('Error', str(value_error))
@@ -341,7 +329,7 @@ def _create_item_from_form(self) -> Item | None:
341329

342330
temp_item = Item.create(
343331
self.entry_name.get(),
344-
self.entry_amount.get(),
332+
float(self.entry_amount.get()),
345333
self.entry_description.get(),
346334
self.entry_date.get(),
347335
category
@@ -354,14 +342,16 @@ def _create_item_from_form(self) -> Item | None:
354342
def on_row_selected(self, e):
355343
# Reference: https://stackoverflow.com/questions/30614279/tkinter-treeview-get-selected-item-values
356344

345+
# Reference: For Multiple Selected Rows
346+
# https://stackoverflow.com/questions/48867800/tk-treeview-focus-how-do-i-get-multiple-selected-lines
347+
357348
# Set the Selected Item for the TreeView
358349
selected_item = self.items_table.focus()
359350
item_dct = self.items_table.item(selected_item)
360351

361352
values = item_dct['values']
362353
item_id = item_dct['text']
363354
row = [item_id] + list(values)
364-
print(row)
365355

366356
self.fill_form(
367357
values[0],
@@ -371,9 +361,3 @@ def on_row_selected(self, e):
371361
category=values[4],
372362
subcategory=values[5]
373363
)
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]

projects/Expense-Tracker/expense_income_stats.py

Lines changed: 101 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from abc import ABC, abstractmethod
22
from item import ItemsDB
3+
import pandas as pd
34
import statistics
5+
from reportlab.pdfgen import canvas
46

57

68
class ExpenseIncomeStats:
@@ -9,22 +11,6 @@ def __init__(self, db_path: str, start: str = None, end: str = None):
911
self.start = start
1012
self.end = end
1113

12-
# if start is None and end is None:
13-
# # Get all Items
14-
# self._items = self._items_db.get_all_items()
15-
# pass
16-
# elif start is None and end is not None:
17-
# # Start from the Oldest Item(s) to the given end date string
18-
# oldest = min(item.date for item in self._items_db.get_all_items())
19-
# self._items = self._items_db.get_items_by_date_range(oldest.strftime("%Y-%m-%d"), end)
20-
# elif start is not None and end is None:
21-
# # Start from the given start date string to the latest date
22-
# latest = max(item.date for item in self._items_db.get_all_items())
23-
# self._items = self._items_db.get_items_by_date_range(start, latest.strftime("%Y-%m-%d"))
24-
# else:
25-
# # Get item from given date range
26-
# self._items = self._items_db.get_items_by_date_range(start, end)
27-
2814
@property
2915
def items(self):
3016
return self._items_db.get_all_items()
@@ -92,26 +78,115 @@ def get_stats_by_category_with_subcategories(self) -> dict:
9278

9379
return stats_dict
9480

95-
@staticmethod
96-
def currency_converter(base_currency: str, quote_currency: str) -> float:
97-
# TODO: Implement currency_converter() method.
98-
pass
99-
10081

10182
class Report(ABC):
83+
def __init__(self, file_path: str, items_db: ItemsDB):
84+
self.file_path = file_path
85+
self.items_db = items_db
86+
10287
@abstractmethod
10388
def generate_report(self, *args, **kwargs):
10489
pass
10590

91+
@staticmethod
92+
def to_dataframe(items) -> pd.DataFrame:
93+
# items: List[Item]
10694

107-
class ExcelReport(Report):
108-
def __init__(self, file_path: str):
109-
self.file_path = file_path
95+
rows = []
96+
for item in items:
97+
category = item.category
98+
row = {k: v for k, v in item.__dict__.items() if k != 'category'}
99+
100+
if category is not None:
101+
row['category'] = category.name
102+
if category.subcategory is not None:
103+
row['subcategory'] = category.subcategory
104+
105+
rows.append(row)
106+
107+
return pd.DataFrame(rows)
110108

109+
110+
class ExcelReport(Report):
111111
def generate_report(self):
112-
pass
112+
# TODO: Enhance - Include the Items without Category or Subcategory
113+
items = self.items_db.get_all_items()
114+
df = self.to_dataframe(items)
115+
116+
with pd.ExcelWriter(self.file_path, engine='xlsxwriter') as writer:
117+
workbook = writer.book
118+
119+
# Raw Data Tab
120+
df.to_excel(
121+
writer,
122+
'Data', # worksheet name
123+
index=False # index does not contain relevant information
124+
)
125+
summary_sheet = writer.sheets['Data'] # Assigning a variable to the sheet allows formatting
126+
127+
# Pivot Table Tab
128+
pivot_table = df.pivot_table(
129+
values='amount',
130+
index=['category', 'subcategory'],
131+
aggfunc={
132+
'amount': ['mean', 'max', 'min'],
133+
}
134+
)
135+
136+
# Flatten the hierarchical column index
137+
pivot_table.columns = [f'{agg}_amount' for agg in pivot_table.columns]
138+
139+
pivot_table.to_excel(
140+
writer,
141+
'Summary By Category', # worksheet name
142+
index=True # index does not contain relevant information
143+
)
144+
summary_sheet = writer.sheets['Summary By Category']
113145

114146

115147
class PdfReport(Report):
116148
def generate_report(self):
117-
pass
149+
items = self.items_db.get_all_items()
150+
df = self.to_dataframe(items)
151+
152+
from reportlab.lib import colors
153+
from reportlab.lib.pagesizes import letter
154+
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
155+
156+
# Pivot Table Tab
157+
pivot_table = df.pivot_table(
158+
values='amount',
159+
index=['category', 'subcategory'],
160+
aggfunc={
161+
'amount': ['mean', 'max', 'min'],
162+
}
163+
)
164+
pivot_table = pivot_table.reset_index()
165+
166+
# Convert DataFrame to a list of lists for the table
167+
table_data = [list(pivot_table.columns)] + pivot_table.values.tolist()
168+
169+
# Create PDF
170+
pdf_filename = self.file_path
171+
doc = SimpleDocTemplate(pdf_filename, pagesize=letter)
172+
elements = []
173+
174+
# Create table
175+
table = Table(table_data)
176+
177+
# Style the table
178+
style = TableStyle([('BACKGROUND', (0, 0), (-1, 0), colors.gray),
179+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
180+
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
181+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
182+
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
183+
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
184+
('GRID', (0, 0), (-1, -1), 1, colors.black)])
185+
186+
table.setStyle(style)
187+
188+
# Add table to elements
189+
elements.append(table)
190+
191+
# Build PDF
192+
doc.build(elements)

0 commit comments

Comments
 (0)