-
Notifications
You must be signed in to change notification settings - Fork 0
/
banktracker.py
286 lines (258 loc) · 12 KB
/
banktracker.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
import csv
import os.path
import tempfile
import eel
import tkinter as tk
import gspread
import gspread_formatting as gsf
import time
from decimal import *
from datetime import datetime as dt
# Variables for eel properties. This will allow user to change window size in the future
screen_width = tk.Tk().winfo_screenwidth()
screen_height = tk.Tk().winfo_screenheight()
win_width = 1280
win_height = 720
# Change Decimal module to have only 2 decimal places as these values are currency
getcontext().prec = 2
# Create empty lists to store transactions and each unique transaction category
categories = []
# Variables to store the name of the temp file and the OS temp directory
# For the sake of security Javascript cannot access all details of
# local storage. The workaround I have created is that the contents
# of the file added to the GUI are written to a temp file and then
# read by this script.
tmp_path = tempfile.gettempdir()
tmp_fileName = "tmp.csv"
completeName = os.path.join(tmp_path, tmp_fileName)
# Writing the content of the uploaded files to the temp file
def writeToTemp(fileContent):
lines = fileContent.splitlines()
print("Writing tmp file...")
f = open(completeName, "w")
# This overwrites the file everytime so new temp files don't
# need to be created.
f.seek(0)
for line in lines[:-1]:
f.write(line)
f.write('\n')
f.write(lines[-1])
f.close()
# The function that starts to process of sheet creation exposed
# to the Javascript of the web based GUI.
@eel.expose
def startSheetCreation(file, sheetName, isLast):
writeToTemp(file)
rows = readCSV(completeName)
eel.updateProgressBar(10, "Creating Sheet...")
createSheet(sheetName, rows, isLast)
# Function for reading the .csv file returns list of transactions
def readCSV(file):
transactions = []
with open(file, mode='r') as csv_file:
csv_reader = csv.reader(csv_file)
# Skip first line of CSV to avoid header
next(csv_reader)
# Iterate through each row of the .csv file
for row in csv_reader:
# Skip blank rows
if row == "":
continue
# Change the format of the date in the .csv to a more human readable format
# Date no longer needs to change due to format changes by bank
date = row[0]
name = row[1]
category = row[3]
# Store each unique category
if category not in categories:
categories.append(category)
# My .csv files format positive transactions with a double hyphen
# This if statement changes the double hypen to just be a positive number instead
# If the number is just a regular negative it is stored as a negative
if row[4][:2] == "--":
amount = Decimal(row[4][2:])
else:
amount = Decimal(row[4])
# Store all the data as a transaction element and add it to the transactions list
transaction = (date, name, category, str(amount))
transactions.append(transaction)
return transactions
# Function that creates a table of each unique category
# so that you can track which categories cost you the most
def insertCategories(wks, startRow, progress):
# This allows the loading bar on the GUI to updated
# a small percentage with each completed category insertion.
addedProgress = progress / len(categories)
print("Writing categories...")
# Offset is needed for formatting purposes.
offset = 0
for category in categories:
# time.sleep() prevents going over the maximum allowed
# API calls. Writes the category and formats it all at once.
wks.insert_row([category], int(startRow))
time.sleep(1)
wks.update(f'B2', f'=SUMIF(C:C,INDIRECT("A{(len(categories) + 1) - offset}"),D:D)', raw=False)
time.sleep(1)
# Calls updateProgressBar Javascript function to change
# the fill of the loading bar.
eel.updateProgressBar(addedProgress, "Inserting Categories...")
offset = offset + 1
time.sleep(2)
print("Categories done!")
# Any formatting for titles and the Totals section is done here
def updateFormatting(wks, rows):
# batch updating saves API calls allowing all formatting to be pushed at once
batch = gsf.batch_updater(sh)
# the formatting for a header
headerFormat = gsf.cellFormat(
backgroundColor=gsf.color(0.6, 0.6, 0.6),
textFormat=gsf.textFormat(bold=True, fontSize=14),
horizontalAlignment='CENTER',
borders=gsf.borders(bottom=gsf.border('SOLID'), top=gsf.border('SOLID'), right=gsf.border('SOLID'), left=gsf.border('SOLID'))
)
# currency format for any cells that contain a currecny value
currencyFormat = gsf.cellFormat(
numberFormat=gsf.numberFormat(type='CURRENCY', pattern='$#,##0.00'),
borders=gsf.borders(bottom=gsf.border('SOLID'), top=gsf.border('SOLID'), right=gsf.border('SOLID'), left=gsf.border('SOLID'))
)
# standardized date format
dateFormat = gsf.cellFormat(
numberFormat=gsf.numberFormat(type='DATE', pattern='yyyy-mm-dd'),
borders=gsf.borders(bottom=gsf.border('SOLID'), top=gsf.border('SOLID'), right=gsf.border('SOLID'), left=gsf.border('SOLID'))
)
# complete outline to all cells
borderFormat = gsf.cellFormat(
borders=gsf.borders(bottom=gsf.border('SOLID'), top=gsf.border('SOLID'), right=gsf.border('SOLID'), left=gsf.border('SOLID'))
)
# creating the totals table and adding formulas
wks.update('F6', 'Total Income')
wks.update('F7', f'=SUMIF(B2:B{len(categories) + 1}, ">0", B2:B{len(categories) + 1})', value_input_option='USER_ENTERED')
wks.update('G6', 'Total Expenses')
wks.update('G7', f'=SUMIF(B2:B{len(categories) + 1}, "<0", B2:B{len(categories) + 1})', value_input_option='USER_ENTERED')
wks.update('H6', 'Profit/ Loss')
wks.update('H7', '=SUM(F7+G7)', value_input_option='USER_ENTERED')
# All of these are conditional rules to add red / green background
catNeg = gsf.ConditionalFormatRule(
ranges=[gsf.GridRange.from_a1_range(f'B2:B{len(categories) + 1}', wks)],
booleanRule=gsf.BooleanRule(
condition=gsf.BooleanCondition('NUMBER_LESS', ['0']),
format=gsf.CellFormat(textFormat=gsf.textFormat(bold=False), backgroundColor=gsf.color(0.95,0.78,0.76))
)
)
catPos = gsf.ConditionalFormatRule(
ranges=[gsf.GridRange.from_a1_range(f'B2:B{len(categories) + 1}', wks)],
booleanRule=gsf.BooleanRule(
condition=gsf.BooleanCondition('NUMBER_GREATER', ['0']),
format=gsf.CellFormat(textFormat=gsf.textFormat(bold=False), backgroundColor=gsf.color(0.71,0.88,0.80))
)
)
rowNeg = gsf.ConditionalFormatRule(
ranges=[gsf.GridRange.from_a1_range(f'D{len(categories) + 4}:D{(len(categories) + 4) + (len(rows) - 1)}', wks)],
booleanRule=gsf.BooleanRule(
condition=gsf.BooleanCondition('NUMBER_LESS', ['0']),
format=gsf.CellFormat(textFormat=gsf.textFormat(bold=False), backgroundColor=gsf.color(0.95,0.78,0.76))
)
)
rowPos = gsf.ConditionalFormatRule(
ranges=[gsf.GridRange.from_a1_range(f'D{len(categories) + 4}:D{(len(categories) + 4) + (len(rows) - 1)}', wks)],
booleanRule=gsf.BooleanRule(
condition=gsf.BooleanCondition('NUMBER_GREATER', ['0']),
format=gsf.CellFormat(textFormat=gsf.textFormat(bold=False), backgroundColor=gsf.color(0.71,0.88,0.80))
)
)
totalNeg = gsf.ConditionalFormatRule(
ranges=[gsf.GridRange.from_a1_range(f'F7:H7', wks)],
booleanRule=gsf.BooleanRule(
condition=gsf.BooleanCondition('NUMBER_LESS', ['0']),
format=gsf.CellFormat(textFormat=gsf.textFormat(bold=False), backgroundColor=gsf.color(0.95,0.78,0.76))
)
)
totalPos = gsf.ConditionalFormatRule(
ranges=[gsf.GridRange.from_a1_range(f'F7:H7', wks)],
booleanRule=gsf.BooleanRule(
condition=gsf.BooleanCondition('NUMBER_GREATER', ['0']),
format=gsf.CellFormat(textFormat=gsf.textFormat(bold=False), backgroundColor=gsf.color(0.71,0.88,0.80))
)
)
# this adds all the conditional format rules to the sheet
rules = gsf.get_conditional_format_rules(wks)
rules.append(catNeg)
rules.append(catPos)
rules.append(rowNeg)
rules.append(rowPos)
rules.append(totalNeg)
rules.append(totalPos)
rules.save()
# batch formatting each change is queued up
batch.format_cell_range(wks, 'A1:B1', headerFormat)
batch.format_cell_range(wks, f'A{len(categories) + 3}:D{len(categories) + 3}', headerFormat)
batch.format_cell_range(wks, 'F6:H6', headerFormat)
batch.format_cell_range(wks, f'B2:B{len(categories) + 1}', currencyFormat)
batch.format_cell_range(wks, 'F7:H7', currencyFormat)
batch.format_cell_range(wks, f'A2:A{len(categories) + 1}', borderFormat)
batch.format_cell_range(wks, f'A{len(categories) + 4}:A{(len(categories) + 4) + (len(rows) - 1)}', dateFormat)
batch.format_cell_range(wks, f'B{len(categories) + 4}:B{(len(categories) + 4) + (len(rows) - 1)}', borderFormat)
batch.format_cell_range(wks, f'C{len(categories) + 4}:C{(len(categories) + 4) + (len(rows) - 1)}', borderFormat)
batch.format_cell_range(wks, f'D{len(categories) + 4}:D{(len(categories) + 4) + (len(rows) - 1)}', currencyFormat)
# width of columns was pre-determined
batch.set_column_width(wks, 'A', 160)
batch.set_column_width(wks, 'B', 320)
batch.set_column_width(wks, 'C', 160)
batch.set_column_width(wks, 'D', 150)
batch.set_column_width(wks, 'F', 125)
batch.set_column_width(wks, 'G', 150)
batch.set_column_width(wks, 'H', 115)
# sends all updates in one API call preventing the API from being overloaded
batch.execute()
# removes the temp file created to make this function
def cleanup():
os.remove(completeName)
time.sleep(2)
# Function that inputs all transactions into another table
def insertExpenses(wks, startRow, rows, progress):
addedProgress = progress / len(rows)
offset = 0
print("Writing expenses...")
for row in reversed(rows):
wks.insert_row([row[0], row[1], row[2], row[3]], int(startRow))
time.sleep(2)
wks.update(f'A{(len(categories) + 4)}', row[0], value_input_option='USER_ENTERED')
time.sleep(2)
wks.update(f'D{(len(categories) + 4)}', row[3], raw=False)
time.sleep(2)
eel.updateProgressBar(addedProgress, "Inserting Expenses...")
time.sleep(2)
print("Expenses done!")
# Function that creates a new sheet in the linked Google Sheets project
def createSheet(sheetName, rows, isLast):
eel.updateSheetTitle(sheetName)
worksheet = sh.add_worksheet(title=sheetName, rows=200, cols=20)
worksheet.insert_row(["Expense Name", "Total"], 1)
eel.updateProgressBar(10, "Inserting Categories...")
catRows = len(categories) + len(rows)
catProgress = 70 * (len(categories) / catRows)
rowProgress = 70 * (len(rows) / catRows)
insertCategories(worksheet, 2, catProgress)
worksheet.insert_row(["Date", "Description", "Category", "Amount (USD)"], len(categories) + 3)
eel.updateProgressBar(0, "Inserting Expenses...")
insertExpenses(worksheet, len(categories) + 4, rows, rowProgress)
eel.updateProgressBar(0, "Adding base formatting...")
updateFormatting(worksheet, rows)
eel.updateProgressBar(5, "Cleaning up...")
cleanup()
eel.updateProgressBar(5, "Done!")
print("Finished worksheet!")
time.sleep(2)
if isLast:
print("All worksheets Done!")
eel.done()
# Connect service account, open the correct Google Sheets project
sa = gspread.service_account()
sh = sa.open("Bank Tracker")
#def main():
# Create a sheet with name taken from second command line argument
#createSheet(sys.argv[2])
# Push all arguments into main function to be used if necessary
if __name__ == "__main__":
eel.init('web')
eel.start('index.html', size=(win_width,win_height), position=((screen_width / 2) - (win_width / 2), (screen_height / 2) - (win_height / 2)))