Compare commits
No commits in common. "d986461019cb7aa7ed64e1bf7355efd533fa3eae" and "ac6763df1b6987f7b6b39a4d0ddf379f0633919a" have entirely different histories.
d986461019
...
ac6763df1b
23
.vscode/launch.json
vendored
23
.vscode/launch.json
vendored
@ -11,28 +11,7 @@
|
|||||||
"program": "main.py",
|
"program": "main.py",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
"args": [
|
"args": ["C:\\Users\\saram\\Downloads\\Statement_082022_4653.pdf"]
|
||||||
// "C:\\Users\\saram\\Downloads\\Statement_072022_4653.pdf",
|
|
||||||
// "C:\\Users\\saram\\Downloads\\Statement_082022_4653.pdf",
|
|
||||||
"C:\\Users\\saram\\Downloads\\Statement_092022_4653.pdf",
|
|
||||||
"C:\\Users\\saram\\Downloads\\Statement_102022_4653.pdf",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create Database",
|
|
||||||
"type": "python",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "database.py",
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"justMyCode": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Flask",
|
|
||||||
"type": "python",
|
|
||||||
"request": "launch",
|
|
||||||
"program": "app.py",
|
|
||||||
"console": "integratedTerminal",
|
|
||||||
"justMyCode": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
144
app.py
144
app.py
@ -1,144 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from flask import Flask, render_template, request, abort
|
|
||||||
import peewee
|
|
||||||
from playhouse.shortcuts import model_to_dict
|
|
||||||
|
|
||||||
import database
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config["DEBUG"] = True
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def before_request():
|
|
||||||
database.instance.connect()
|
|
||||||
|
|
||||||
@app.after_request
|
|
||||||
def after_request(response):
|
|
||||||
database.instance.close()
|
|
||||||
return response
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def home():
|
|
||||||
puppies = ['hollie', 'grace', 'loki']
|
|
||||||
return render_template('index.html', puppies=puppies)
|
|
||||||
|
|
||||||
@app.route("/users")
|
|
||||||
def users():
|
|
||||||
args = request.args.to_dict()
|
|
||||||
if 'username' not in args:
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
username = args['username'].strip()
|
|
||||||
try:
|
|
||||||
return model_to_dict(database.User.get(database.User.username == username))
|
|
||||||
except peewee.DoesNotExist:
|
|
||||||
abort(404)
|
|
||||||
except:
|
|
||||||
abort(500)
|
|
||||||
|
|
||||||
@app.route("/transactions", methods=["GET", "POST"])
|
|
||||||
def transactions():
|
|
||||||
if request.method == "GET":
|
|
||||||
args = request.args.to_dict()
|
|
||||||
if 'user_id' not in args:
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
user_id = args['user_id'].strip()
|
|
||||||
try:
|
|
||||||
july_2022 = datetime(2022, 7, 1, 0)
|
|
||||||
transactions = database.Transaction.select().where(
|
|
||||||
(database.Transaction.user == user_id) &
|
|
||||||
(database.Transaction.transaction_date > july_2022)
|
|
||||||
)
|
|
||||||
return [model_to_dict(t) for t in transactions]
|
|
||||||
except peewee.DoesNotExist:
|
|
||||||
abort(404)
|
|
||||||
except:
|
|
||||||
abort(500)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
body = request.get_json()
|
|
||||||
try:
|
|
||||||
user = database.User.get(database.User.uuid == body['user_id'])
|
|
||||||
category = database.TransactionCategory.get(database.TransactionCategory.name == body['category'])
|
|
||||||
database.Transaction.create(
|
|
||||||
user=user,
|
|
||||||
subcategory=category,
|
|
||||||
transaction_date=body['transaction_date'],
|
|
||||||
description=body['description'],
|
|
||||||
amount=body['amount'],
|
|
||||||
type=body['type'],
|
|
||||||
notes=body['notes']
|
|
||||||
)
|
|
||||||
return ('', 200)
|
|
||||||
except peewee.DoesNotExist:
|
|
||||||
abort(404)
|
|
||||||
except KeyError:
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
# Default if no method is hit
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
@app.route("/transactions/<uuid>", methods=["PUT"])
|
|
||||||
def transaction(uuid=None):
|
|
||||||
if request.method == "PUT":
|
|
||||||
if not uuid:
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
body = request.get_json()
|
|
||||||
if 'category' not in body or 'notes' not in body:
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
try:
|
|
||||||
category = database.TransactionCategory.get(database.TransactionCategory.name == body['category'])
|
|
||||||
transaction = database.Transaction.get(database.Transaction.primary_key == uuid)
|
|
||||||
transaction.subcategory = category
|
|
||||||
transaction.notes = body['notes']
|
|
||||||
transaction.save()
|
|
||||||
return ('', 200)
|
|
||||||
except peewee.DoesNotExist:
|
|
||||||
abort(404)
|
|
||||||
except:
|
|
||||||
abort(500)
|
|
||||||
|
|
||||||
# Default if no method is hit
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
@app.route("/categories", methods=["GET", "POST"])
|
|
||||||
def categories():
|
|
||||||
if request.method == "GET":
|
|
||||||
try:
|
|
||||||
raw_categories = database.TransactionCategory.select()
|
|
||||||
categories = {}
|
|
||||||
for category in raw_categories:
|
|
||||||
if not category.parent:
|
|
||||||
categories[category.name] = []
|
|
||||||
|
|
||||||
for category in raw_categories:
|
|
||||||
if category.parent:
|
|
||||||
categories[category.parent.name].append(category.name)
|
|
||||||
|
|
||||||
return categories
|
|
||||||
except peewee.DoesNotExist:
|
|
||||||
abort(404)
|
|
||||||
except:
|
|
||||||
abort(500)
|
|
||||||
if request.method == "POST":
|
|
||||||
body = request.get_json()
|
|
||||||
try:
|
|
||||||
if 'parent' in body:
|
|
||||||
parent = database.TransactionCategory.get(database.TransactionCategory.name == body['parent'])
|
|
||||||
database.TransactionCategory.create(name=body['name'], parent=parent)
|
|
||||||
else:
|
|
||||||
database.TransactionCategory.create(name=body['name'])
|
|
||||||
return ('', 200)
|
|
||||||
except peewee.DoesNotExist:
|
|
||||||
abort(404)
|
|
||||||
except:
|
|
||||||
abort(500)
|
|
||||||
|
|
||||||
# Should never get here.
|
|
||||||
abort(500)
|
|
||||||
|
|
||||||
app.run()
|
|
@ -53,16 +53,13 @@ class Transactions(Regex):
|
|||||||
|
|
||||||
date = result[0]
|
date = result[0]
|
||||||
description = " ".join(result[1].split())
|
description = " ".join(result[1].split())
|
||||||
is_credit = '-' in result[2]
|
is_payment = '-' in result[2]
|
||||||
amount = float(result[3].replace(',', ''))
|
amount = float(result[3].replace(',', ''))
|
||||||
|
|
||||||
if is_credit:
|
if is_payment:
|
||||||
if "AUTOPAY" in description:
|
|
||||||
print(f"Skipping payment: {amount}")
|
print(f"Skipping payment: {amount}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
amount *= -1
|
|
||||||
|
|
||||||
transactions.append([date, description, amount])
|
transactions.append([date, description, amount])
|
||||||
|
|
||||||
return transactions
|
return transactions
|
||||||
|
59
database.py
59
database.py
@ -1,5 +1,4 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import uuid
|
|
||||||
from peewee import *
|
from peewee import *
|
||||||
|
|
||||||
DATABASE = 'budget.db'
|
DATABASE = 'budget.db'
|
||||||
@ -8,67 +7,59 @@ instance = SqliteDatabase(DATABASE, pragmas=[('foreign_keys', 'on')])
|
|||||||
def create_tables():
|
def create_tables():
|
||||||
"""Helper function to create database tables. Should be called manually."""
|
"""Helper function to create database tables. Should be called manually."""
|
||||||
with instance:
|
with instance:
|
||||||
instance.create_tables([User, TransactionCategory, Transaction])
|
instance.create_tables([User, TransactionCategory, Transaction, Source])
|
||||||
|
|
||||||
def add_user():
|
|
||||||
# Make my user.
|
# Make my user.
|
||||||
instance.connect()
|
instance.connect()
|
||||||
User.create(username='ciphercules')
|
User.create(username='ciphercules')
|
||||||
instance.close()
|
instance.close()
|
||||||
|
|
||||||
def add_default_categories():
|
def add_default_categories():
|
||||||
"""Helper function to add default categories. Should be called manually."""
|
|
||||||
categories = [
|
categories = [
|
||||||
("food", ["snacks", "fast_food", "groceries", "restaurant"]),
|
{"food": ["snacks", "fast_food", "groceries", "restaurant"]},
|
||||||
("clothing", []),
|
{"clothing": []},
|
||||||
("event", ["event_food", "birthday", "movie_theater"]),
|
{"event": ["event_food", "birthday", "movie_theater"]},
|
||||||
("finance", ["interest"]),
|
{"finance": ["interest"]},
|
||||||
("hobby", ["gym", "game_development", "projects", "education"]),
|
{"hobby": ["gym", "game_development", "projects", "education"]},
|
||||||
("home_improvement", []),
|
{"home_improvement": []},
|
||||||
("pet", ["dog_health", "dog_food"]),
|
{"pet": ["health", "dog_food"]},
|
||||||
("media", ["book", "music", "television", "video_game"]),
|
{"media": ["book", "music", "television", "video_game"]},
|
||||||
("health", ["medicine"]),
|
{"health": ["medicine"]},
|
||||||
("transit", ["car_insurance", "car_registration", "gas", "parking", "taxi", "car_maintenance"]),
|
{"transit": ["car_insurance", "car_registration", "gas", "parking", "taxi", "car_maintenance"]},
|
||||||
("utilities", ["electricity", "natural_gas", "laundry", "cell_phone", "trash", "water"]),
|
{"utilities": ["electricity", "gas", "laundry", "cell_phone", "trash", "water"]},
|
||||||
("rent", []),
|
{"rent": []},
|
||||||
]
|
]
|
||||||
|
|
||||||
instance.connect()
|
instance.connect()
|
||||||
with instance.atomic():
|
with instance.atomic():
|
||||||
for category in categories:
|
for parent, children in categories.items():
|
||||||
parent, children = category
|
|
||||||
parent_db = TransactionCategory.create(name=parent)
|
parent_db = TransactionCategory.create(name=parent)
|
||||||
for child in children:
|
for child in children:
|
||||||
TransactionCategory.create(name=child, parent=parent_db)
|
TransactionCategory.create(name=child, parent=parent_db)
|
||||||
|
|
||||||
|
|
||||||
def delete_transactions():
|
|
||||||
instance.connect()
|
|
||||||
# august_2022 = datetime.datetime(2022, 8, 18, 0)
|
|
||||||
# delete_query = Transaction.delete().where(Transaction.transaction_date > august_2022)
|
|
||||||
delete_query = Transaction.delete().where(Transaction.primary_key == 83)
|
|
||||||
delete_query.execute()
|
|
||||||
instance.close()
|
|
||||||
|
|
||||||
class BaseModel(Model):
|
class BaseModel(Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
database = instance
|
database = instance
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
uuid = UUIDField(unique=True, primary_key=True, default=uuid.uuid4())
|
username = CharField(unique=True, primary_key=True)
|
||||||
username = CharField(unique=True)
|
|
||||||
|
|
||||||
class TransactionCategory(BaseModel):
|
class TransactionCategory(BaseModel):
|
||||||
primary_key = AutoField(primary_key=True)
|
primary_key = AutoField(primary_key=True)
|
||||||
name = CharField(unique=True)
|
name = CharField(unique=True)
|
||||||
parent = ForeignKeyField('self', null=True, backref='children')
|
parent = ForeignKeyField('self', null=True, backref='children')
|
||||||
|
|
||||||
|
class Source(BaseModel):
|
||||||
|
filename=CharField(unique=True, primary_key=True)
|
||||||
|
type = IntegerField()
|
||||||
|
created_date = DateTimeField(default=datetime.datetime.now)
|
||||||
|
user = ForeignKeyField(User, backref='transactions')
|
||||||
|
|
||||||
class Transaction(BaseModel):
|
class Transaction(BaseModel):
|
||||||
# Metadata
|
# Metadata
|
||||||
primary_key = AutoField(primary_key=True)
|
primary_key = AutoField(primary_key=True)
|
||||||
|
source = ForeignKeyField(Source, backref='transactions')
|
||||||
created_date = DateTimeField(default=datetime.datetime.now)
|
created_date = DateTimeField(default=datetime.datetime.now)
|
||||||
source_file=CharField(null=True)
|
|
||||||
type = IntegerField()
|
|
||||||
user = ForeignKeyField(User, backref='transactions')
|
user = ForeignKeyField(User, backref='transactions')
|
||||||
|
|
||||||
# Real data
|
# Real data
|
||||||
@ -76,9 +67,3 @@ class Transaction(BaseModel):
|
|||||||
description = CharField()
|
description = CharField()
|
||||||
amount = FloatField()
|
amount = FloatField()
|
||||||
subcategory = ForeignKeyField(TransactionCategory, backref='+', null=True)
|
subcategory = ForeignKeyField(TransactionCategory, backref='+', null=True)
|
||||||
notes = CharField(null=True)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
create_tables()
|
|
||||||
add_user()
|
|
||||||
add_default_categories()
|
|
18
main.py
18
main.py
@ -9,14 +9,10 @@ parser = argparse.ArgumentParser(prog="BudgetBear", description="Calculate a bud
|
|||||||
parser.add_argument('files', type=str, nargs='+', help='File to parse transactions from.')
|
parser.add_argument('files', type=str, nargs='+', help='File to parse transactions from.')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Get my user.
|
username = 'ciphercules'
|
||||||
database.instance.connect()
|
|
||||||
user = database.User.select().where(database.User.username == 'ciphercules').get()
|
|
||||||
database.instance.close()
|
|
||||||
|
|
||||||
file_parsers = [capital_one.Parser()]
|
file_parsers = [capital_one.Parser()]
|
||||||
for f in args.files:
|
for f in args.files:
|
||||||
filename=os.path.basename(f),
|
|
||||||
for file_parser in file_parsers:
|
for file_parser in file_parsers:
|
||||||
# Use the first successful parser.
|
# Use the first successful parser.
|
||||||
transactions = file_parser.parse(f)
|
transactions = file_parser.parse(f)
|
||||||
@ -26,16 +22,22 @@ for f in args.files:
|
|||||||
# Add to database
|
# Add to database
|
||||||
database.instance.connect()
|
database.instance.connect()
|
||||||
with database.instance.atomic():
|
with database.instance.atomic():
|
||||||
|
# Add source file first.
|
||||||
|
source = database.Source.create(
|
||||||
|
filename=os.path.basename(f),
|
||||||
|
type=file_parser.source,
|
||||||
|
user=username
|
||||||
|
)
|
||||||
|
|
||||||
# Add each transaction
|
# Add each transaction
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
date, description, amount = transaction
|
date, description, amount = transaction
|
||||||
database.Transaction.create(
|
database.Transaction.create(
|
||||||
user=user,
|
user=username,
|
||||||
transaction_date=date,
|
transaction_date=date,
|
||||||
description=description,
|
description=description,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
source_file=filename,
|
source=source
|
||||||
type=file_parser.source
|
|
||||||
)
|
)
|
||||||
database.instance.close()
|
database.instance.close()
|
||||||
|
|
||||||
|
2
model.py
2
model.py
@ -12,5 +12,3 @@ class BaseParser:
|
|||||||
class TransactionSource:
|
class TransactionSource:
|
||||||
"""Enum of possible transaction sources"""
|
"""Enum of possible transaction sources"""
|
||||||
CAPITAL_ONE = 1
|
CAPITAL_ONE = 1
|
||||||
SPLITWISE = 2
|
|
||||||
COMPANY = 3
|
|
@ -1,522 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>BudgetBear</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<style>
|
|
||||||
body {font-family: Arial, Helvetica, sans-serif;}
|
|
||||||
/* The Modal (background) */
|
|
||||||
.modal {
|
|
||||||
display: none; /* Hidden by default */
|
|
||||||
position: fixed; /* Stay in place */
|
|
||||||
z-index: 1; /* Sit on top */
|
|
||||||
padding-top: 50px; /* Location of the box */
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%; /* Full width */
|
|
||||||
height: 100%; /* Full height */
|
|
||||||
overflow: auto; /* Enable scroll if needed */
|
|
||||||
background-color: rgb(0,0,0); /* Fallback color */
|
|
||||||
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Content */
|
|
||||||
.modal-content {
|
|
||||||
background-color: #fefefe;
|
|
||||||
margin: auto;
|
|
||||||
padding: 30px;
|
|
||||||
border: 1px solid #888;
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The Close Button */
|
|
||||||
.close {
|
|
||||||
float: left;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The Next Button */
|
|
||||||
.next {
|
|
||||||
float: right;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>BudgetBear</h1>
|
|
||||||
<p>The app for bears on a budget</p>
|
|
||||||
<div id="login_form">
|
|
||||||
<label for="login_input">Login: </label>
|
|
||||||
<input id="login_input" name="login">
|
|
||||||
<input id="login_submit" type="submit" value="Submit">
|
|
||||||
</div>
|
|
||||||
<div id="login_success" style="display:none">
|
|
||||||
<p>Logged in as: <span id="login_username"></span></p>
|
|
||||||
</div>
|
|
||||||
<div id="login_error" style="display:none">
|
|
||||||
<p style="color:red">Failed to login</p>
|
|
||||||
</div>
|
|
||||||
<h2>Transactions</h2>
|
|
||||||
<input id="transactions_refresh" type="submit" value="Refresh" disabled>
|
|
||||||
<!-- Transaction Modal -->
|
|
||||||
<button id="transaction_modal_btn">Let's Process!</button>
|
|
||||||
<div id="transaction_error" style="display:none">
|
|
||||||
<p style="color:blue">No transactions to process!</p>
|
|
||||||
</div>
|
|
||||||
<div id="transaction_modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h3>Transaction ID <span id="transaction_id"></span></h3>
|
|
||||||
<p><b>Remaining:</b> <span id="transactions_unprocessed"></span></p>
|
|
||||||
<p><b>Transaction date:</b> <span id="transaction_date"></span></p>
|
|
||||||
<p><b>Description:</b> <span id="transaction_description"></span></p>
|
|
||||||
<p><b>Amount:</b> $<span id="transaction_amount"></span></p>
|
|
||||||
<label for="transaction_category">Category</label>
|
|
||||||
<select id="transaction_category"></select>
|
|
||||||
<label for="transaction_subcategory">Subcategory</label>
|
|
||||||
<select id="transaction_subcategory" disabled></select>
|
|
||||||
<br><br>
|
|
||||||
<label for="transaction_notes">Notes:</label>
|
|
||||||
<textarea id="transaction_notes" rows="4" cols="50"></textarea>
|
|
||||||
<br><br>
|
|
||||||
<label for="transaction_split">Split with others?</label>
|
|
||||||
<input type="checkbox" id="transaction_split" value="true">
|
|
||||||
<br><br>
|
|
||||||
<div id="transaction_split_content" style="display:none">
|
|
||||||
<h3>Split Transaction</h3>
|
|
||||||
<label for="transaction_split_amount">Amount reimbursed:</label>
|
|
||||||
<input type="number" id="transaction_split_amount">
|
|
||||||
<br><br>
|
|
||||||
<label for="transaction_split_type">Type: </label>
|
|
||||||
<select id="transaction_split_type">
|
|
||||||
<option value="2">Splitwise</option>
|
|
||||||
<option value="3">SpaceX</option>
|
|
||||||
</select>
|
|
||||||
<br><br>
|
|
||||||
<label for="transaction_split_notes">Notes: </label>
|
|
||||||
<textarea id="transaction_split_notes" rows="4" cols="50"></textarea>
|
|
||||||
<br><br>
|
|
||||||
</div>
|
|
||||||
<button id="modal_close" class="close">Exit</button>
|
|
||||||
<button id="modal_next" class="next">Submit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2>Add category</h2>
|
|
||||||
<label for="category_parent">Parent: </label>
|
|
||||||
<select id="category_parent"></select>
|
|
||||||
<label for="category_name">Name: </label>
|
|
||||||
<input id="category_name">
|
|
||||||
<button id="category_add">Add</button>
|
|
||||||
<!-- Transaction Modal End -->
|
|
||||||
</body>
|
|
||||||
<script>
|
|
||||||
// Login HTML elements.
|
|
||||||
const login =
|
|
||||||
{
|
|
||||||
form : document.getElementById('login_form'),
|
|
||||||
input : document.getElementById('login_input'),
|
|
||||||
submit : document.getElementById('login_submit'),
|
|
||||||
success : document.getElementById('login_success'),
|
|
||||||
error : document.getElementById('login_error'),
|
|
||||||
username : document.getElementById('login_username'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Transaction HTML elements.
|
|
||||||
const x_action =
|
|
||||||
{
|
|
||||||
refresh : document.getElementById('transactions_refresh'),
|
|
||||||
modal_btn : document.getElementById('transaction_modal_btn'),
|
|
||||||
error : document.getElementById('transaction_error'),
|
|
||||||
modal : document.getElementById('transaction_modal'),
|
|
||||||
id : document.getElementById('transaction_id'),
|
|
||||||
exit: document.getElementById('modal_close'),
|
|
||||||
submit: document.getElementById('modal_next'),
|
|
||||||
unprocessed : document.getElementById('transactions_unprocessed'),
|
|
||||||
date: document.getElementById('transaction_date'),
|
|
||||||
description: document.getElementById('transaction_description'),
|
|
||||||
amount: document.getElementById('transaction_amount'),
|
|
||||||
category: document.getElementById('transaction_category'),
|
|
||||||
subcategory: document.getElementById('transaction_subcategory'),
|
|
||||||
notes: document.getElementById('transaction_notes'),
|
|
||||||
split:
|
|
||||||
{
|
|
||||||
enabled: document.getElementById('transaction_split'),
|
|
||||||
container: document.getElementById('transaction_split_content'),
|
|
||||||
notes: document.getElementById('transaction_split_notes'),
|
|
||||||
amount: document.getElementById('transaction_split_amount'),
|
|
||||||
type: document.getElementById('transaction_split_type'),
|
|
||||||
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const category =
|
|
||||||
{
|
|
||||||
name_input: document.getElementById('category_name'),
|
|
||||||
parent_select: document.getElementById('category_parent'),
|
|
||||||
add_button: document.getElementById('category_add'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Global variables.
|
|
||||||
const global_data =
|
|
||||||
{
|
|
||||||
current_user: null,
|
|
||||||
transaction:
|
|
||||||
{
|
|
||||||
unprocessed_indices: [],
|
|
||||||
list: [],
|
|
||||||
process_index: 0,
|
|
||||||
unprocessed_cnt: function()
|
|
||||||
{
|
|
||||||
const diff = this.unprocessed_indices.length - 1 - this.process_index
|
|
||||||
return diff >= 0 ? diff : 0;
|
|
||||||
},
|
|
||||||
get_current: function()
|
|
||||||
{
|
|
||||||
if (this.process_index < this.unprocessed_indices.length)
|
|
||||||
{
|
|
||||||
return this.list[this.unprocessed_indices[this.process_index]]
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
categories: {},
|
|
||||||
update: function()
|
|
||||||
{
|
|
||||||
let new_indices = []
|
|
||||||
for (let i = 0; i < this.transaction.list.length; i++)
|
|
||||||
{
|
|
||||||
if (this.transaction.list[i].subcategory === null)
|
|
||||||
{
|
|
||||||
// Save index of first unprocessed transaction.
|
|
||||||
new_indices.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.transaction.unprocessed_indices = new_indices;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
login.input.onkeypress = function(event) {
|
|
||||||
if (event.key === "Enter")
|
|
||||||
{
|
|
||||||
event.preventDefault();
|
|
||||||
login.submit.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
login.submit.onclick = function(_) {
|
|
||||||
const username = login.input.value;
|
|
||||||
const request_url = "/users?username=" + username;
|
|
||||||
const handler =
|
|
||||||
{
|
|
||||||
success: function (data) {
|
|
||||||
// Cache data in browser.
|
|
||||||
global_data.current_user = data;
|
|
||||||
|
|
||||||
// Retrieve transactions.
|
|
||||||
get_transactions(global_data.current_user.uuid);
|
|
||||||
|
|
||||||
// Update UI.
|
|
||||||
login.form.style.display = 'none';
|
|
||||||
login.error.style.display = 'none';
|
|
||||||
login.username.innerText = data.username;
|
|
||||||
login.success.style.display = 'block';
|
|
||||||
},
|
|
||||||
error: function () {
|
|
||||||
// Update UI.
|
|
||||||
login.error.style.display = 'block';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
http_request(request_url, handler);
|
|
||||||
};
|
|
||||||
|
|
||||||
x_action.refresh.onclick = function() {
|
|
||||||
get_transactions(global_data.current_user.uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
x_action.modal_btn.onclick = function() {
|
|
||||||
if (global_data.transaction.unprocessed_cnt() === 0)
|
|
||||||
{
|
|
||||||
x_action.error.style.display = "block";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
update_transaction_ui(global_data.transaction.get_current());
|
|
||||||
x_action.error.style.display = "none";
|
|
||||||
x_action.modal.style.display = "block";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
x_action.exit.onclick = function()
|
|
||||||
{
|
|
||||||
reset_transaction_ui();
|
|
||||||
}
|
|
||||||
|
|
||||||
x_action.submit.onclick = function()
|
|
||||||
{
|
|
||||||
const body = {}
|
|
||||||
if (x_action.subcategory.value !== "")
|
|
||||||
{
|
|
||||||
body.category = x_action.subcategory.value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
body.category = x_action.category.value;
|
|
||||||
}
|
|
||||||
body.notes = x_action.notes.value;
|
|
||||||
const transaction = global_data.transaction.get_current()
|
|
||||||
const url = "/transactions/" + transaction.primary_key;
|
|
||||||
const handler =
|
|
||||||
{
|
|
||||||
success: function() {
|
|
||||||
console.log("Updated transaction successfully!");
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
alert("Failed to update transaction");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
http_request(url, handler, "PUT", body);
|
|
||||||
|
|
||||||
const is_split = x_action.split.enabled.checked;
|
|
||||||
if (is_split)
|
|
||||||
{
|
|
||||||
const split_body = {}
|
|
||||||
split_body.category = body.category;
|
|
||||||
split_body.notes = x_action.split.notes.value;
|
|
||||||
split_body.amount = -1 * parseFloat(x_action.split.amount.value);
|
|
||||||
split_body.type = parseInt(x_action.split.type.value);
|
|
||||||
split_body.transaction_date = transaction.transaction_date;
|
|
||||||
split_body.description = transaction.description;
|
|
||||||
split_body.user_id = global_data.current_user.uuid;
|
|
||||||
|
|
||||||
const url = "/transactions";
|
|
||||||
const handler =
|
|
||||||
{
|
|
||||||
success: function() {
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
alert("Failed to add split transaction");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
http_request(url, handler, "POST", split_body);
|
|
||||||
}
|
|
||||||
|
|
||||||
global_data.transaction.process_index++;
|
|
||||||
if (global_data.transaction.get_current() !== null)
|
|
||||||
{
|
|
||||||
update_transaction_ui(global_data.transaction.get_current());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reset_transaction_ui();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
x_action.split.enabled.onclick = function()
|
|
||||||
{
|
|
||||||
if (x_action.split.enabled.checked == true)
|
|
||||||
{
|
|
||||||
x_action.split.container.style.display = 'block';
|
|
||||||
const transaction = global_data.transaction.get_current();
|
|
||||||
x_action.split.type.onchange();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x_action.split.container.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
x_action.split.type.onchange = function()
|
|
||||||
{
|
|
||||||
const selected = x_action.split.type.value;
|
|
||||||
const transaction = global_data.transaction.get_current();
|
|
||||||
if (selected == 2) // Splitwise
|
|
||||||
{
|
|
||||||
x_action.split.amount.value = transaction.amount / 2;
|
|
||||||
}
|
|
||||||
else if (selected == 3) //SpaceX
|
|
||||||
{
|
|
||||||
x_action.split.amount.value = transaction.amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
x_action.category.onchange = function()
|
|
||||||
{
|
|
||||||
x_action.subcategory.innerHTML = '';
|
|
||||||
const selected = x_action.category.value;
|
|
||||||
const subcategories = global_data.categories[selected]
|
|
||||||
if (subcategories.length == 0)
|
|
||||||
{
|
|
||||||
x_action.subcategory.disabled = true
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x_action.subcategory.disabled = undefined
|
|
||||||
for (let i = 0; i < subcategories.length; i++)
|
|
||||||
{
|
|
||||||
const opt = document.createElement("option");
|
|
||||||
opt.value = subcategories[i];
|
|
||||||
opt.innerText = subcategories[i];
|
|
||||||
|
|
||||||
x_action.subcategory.appendChild(opt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
category.add_button.onclick = function()
|
|
||||||
{
|
|
||||||
const body = {};
|
|
||||||
body.name = category.name_input.value;
|
|
||||||
const selected = category.parent_select.value;
|
|
||||||
if (selected != "none" )
|
|
||||||
{
|
|
||||||
body.parent = selected;
|
|
||||||
}
|
|
||||||
const url = "/categories";
|
|
||||||
const handler =
|
|
||||||
{
|
|
||||||
success: function()
|
|
||||||
{
|
|
||||||
get_categories();
|
|
||||||
},
|
|
||||||
error: function()
|
|
||||||
{
|
|
||||||
alert("Failed to add category");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
http_request(url, handler, "POST", body);
|
|
||||||
|
|
||||||
category.parent_select.value = "none";
|
|
||||||
category.name_input.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
category.name_input.onkeypress = function(event) {
|
|
||||||
if (event.key === "Enter")
|
|
||||||
{
|
|
||||||
event.preventDefault();
|
|
||||||
category.add_button.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function reset_transaction_ui(close_modal=true)
|
|
||||||
{
|
|
||||||
// Reset all stateful inputs.
|
|
||||||
if (close_modal)
|
|
||||||
{
|
|
||||||
x_action.modal.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
x_action.split.container.style.display = 'none';
|
|
||||||
x_action.split.enabled.checked = false;
|
|
||||||
x_action.split.notes.value = "";
|
|
||||||
x_action.category.innerHTML = "";
|
|
||||||
x_action.subcategory.innerHTML = "";
|
|
||||||
x_action.subcategory.disabled = "true";
|
|
||||||
x_action.notes.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_transaction_ui(transaction)
|
|
||||||
{
|
|
||||||
// Reset old state.
|
|
||||||
reset_transaction_ui(false);
|
|
||||||
|
|
||||||
// Global updates first.
|
|
||||||
x_action.unprocessed.innerText = global_data.transaction.unprocessed_cnt();
|
|
||||||
for (key in global_data.categories)
|
|
||||||
{
|
|
||||||
const opt = document.createElement("option");
|
|
||||||
opt.value = key;
|
|
||||||
opt.innerText = key;
|
|
||||||
|
|
||||||
x_action.category.appendChild(opt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction specific updates.
|
|
||||||
x_action.id.innerText = transaction.primary_key;
|
|
||||||
x_action.date.innerText = transaction.transaction_date;
|
|
||||||
x_action.description.innerText = transaction.description;
|
|
||||||
x_action.amount.innerText = transaction.amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_transactions(uuid) {
|
|
||||||
const request_url = "/transactions?user_id=" + uuid;
|
|
||||||
const handler =
|
|
||||||
{
|
|
||||||
success: function(data) {
|
|
||||||
global_data.transaction.list = data;
|
|
||||||
x_action.refresh.disabled = undefined;
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
console.error("Failed to get transactions");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
http_request(request_url, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_categories() {
|
|
||||||
const request_url = "/categories";
|
|
||||||
const handler =
|
|
||||||
{
|
|
||||||
success: function(data) {
|
|
||||||
global_data.categories = data
|
|
||||||
update_category_ui();
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
console.error("Failed to get categories");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
http_request(request_url, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_category_ui()
|
|
||||||
{
|
|
||||||
function add_opt(key)
|
|
||||||
{
|
|
||||||
const opt = document.createElement("option");
|
|
||||||
opt.value = key;
|
|
||||||
opt.innerText = key;
|
|
||||||
|
|
||||||
category.parent_select.appendChild(opt);
|
|
||||||
}
|
|
||||||
|
|
||||||
category.parent_select.innerHTML = "";
|
|
||||||
add_opt("none");
|
|
||||||
for (key in global_data.categories) add_opt(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
function http_request(url, response_handler, method="GET", body=null)
|
|
||||||
{
|
|
||||||
let request = new XMLHttpRequest();
|
|
||||||
request.addEventListener("load", function(){
|
|
||||||
if (request.status !== 200)
|
|
||||||
{
|
|
||||||
response_handler.error();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
let response_body = ""
|
|
||||||
if (request.responseText !== "")
|
|
||||||
{
|
|
||||||
response_body = JSON.parse(request.responseText);
|
|
||||||
}
|
|
||||||
response_handler.success(response_body);
|
|
||||||
global_data.update();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const async = true;
|
|
||||||
request.open(method, url, async);
|
|
||||||
if (method == "POST" || method == "PUT")
|
|
||||||
{
|
|
||||||
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
|
||||||
request.send(JSON.stringify(body));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
request.send(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do every page load.
|
|
||||||
get_categories();
|
|
||||||
</script>
|
|
||||||
</html>
|
|
12
todo.txt
12
todo.txt
@ -4,11 +4,11 @@ Every week, I need to:
|
|||||||
2. Update SplitWise for shared purchases
|
2. Update SplitWise for shared purchases
|
||||||
|
|
||||||
# Tasks
|
# Tasks
|
||||||
xParse capital one statement
|
|
||||||
x Given a PDF bank statement from capital one, extract the transaction date, description, and amount from each transaction
|
## Parse capital one statement
|
||||||
xStore the data in a SQL lite database
|
x Given a PDF bank statement from capital one, extract the transaction date, description, and amount from each transaction
|
||||||
|
Store the data in a SQL lite database
|
||||||
x Create database schema
|
x Create database schema
|
||||||
x Automatically add capital one transactions to database
|
x Automatically add capital one transactions to database
|
||||||
xAdd web page to categorize transactions
|
Add web page to add categories
|
||||||
xAdd web page to add categories
|
Add web page to categorize transactions
|
||||||
Visualize spend for each month
|
|
Loading…
Reference in New Issue
Block a user