Compare commits

..

5 Commits

5 changed files with 197 additions and 67 deletions

14
TODO Normal file
View File

@@ -0,0 +1,14 @@
For bills, there is too much to guess, need to ask:
frequency:
then I could work out growth rate
MUCH LONGER/HARDER:
potentially for each bill_type, there are unique extras - e.g. THIS feels too hard:
water has 2 fixed charges (water & sewerage) and then a consumption charge (per ML)
elec has 1 fixe charge (daily) and then consumption (per kwh) BUT, also daily solar rate
gas has fixed charge and consumption
internet, kayo is monthly fixed (but can go up sometimes)
eweka is annual fixed
phone is messier again.

41
bills.py Normal file
View File

@@ -0,0 +1,41 @@
from db import get_bill_data, get_bill_types, set_bill_type_growth
def derive_bill_data():
bd=get_bill_data()
bt=get_bill_types()
water_id = None
for t in bt:
if t['name'] == "Water":
water_id = t['id']
if not water_id:
return
total={}
total[water_id]={}
for yr in [2022, 2023, 2024]:
print( f"water_id={water_id}")
total[water_id][yr] = 0
for b in bd:
if b['bill_type_id'] == water_id and str(yr) in b['bill_date']:
total[water_id][yr] += b['amount']
print( f"{yr} => {b['bill_date']} -- {b['amount']}" )
print( f"total for water in {yr} is {total[water_id][yr]}" )
# once we have all yr totals:
growth = {}
growth[water_id] = {}
max_growth = {}
avg_growth = {}
max_growth[water_id] = 0
avg_growth[water_id] = 0
count = 0
for yr in [2023, 2024]:
growth[water_id][yr] = (total[water_id][yr] - total[water_id][yr-1]) / total[water_id][yr-1] * 100
avg_growth[water_id] += growth[water_id][yr]
count += 1
if growth[water_id][yr] > max_growth[water_id]:
max_growth[water_id] = growth[water_id][yr]
print( f"growth from {yr} to {yr-1} = {growth}%")
print( f"Max growth was: {max_growth[water_id]}" )
print( f"Avg growth is: {avg_growth[water_id]/count}" )
set_bill_type_growth( water_id, avg_growth[water_id]/count )

41
db.py
View File

@@ -85,6 +85,14 @@ def init_db():
)''')
cur.execute('''CREATE TABLE IF NOT EXISTS bill_type (
id INTEGER PRIMARY KEY AUTOINCREMENT,
freq INTEGER,
name STRING,
ann_growth REAL,
FOREIGN KEY(freq) REFERENCES bill_freq(id)
)''')
cur.execute('''CREATE TABLE IF NOT EXISTS bill_freq (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name STRING
)''')
@@ -113,6 +121,9 @@ def init_db():
Inflation, Mich_present, Overseas_trip, Mark_reno, D_leave_owed_in_days, D_TLS_shares, M_TLS_shares, D_CBA_shares, TLS_price, CBA_price, Overseas_trip_date, Mark_reno_date, Car_buyout_date, Sell_shares, compare_to, Ioniq6_future)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(4762.29, 10, 24000, 620, 2412, 45824.68, 83738.74, 80000, 424875.26, 4.75, 2.4, 10000, 50000, 10000, 76.85, 1000, 750, 1111, 4.52, 163.32, '2025-06-01', '2025-09-01', '2025-02-20', 4, 0, 0))
cur.execute( "INSERT INTO bill_freq values ( 1, 'Annual' )" )
cur.execute( "INSERT INTO bill_freq values ( 2, 'Quarterly' )" )
cur.execute( "INSERT INTO bill_freq values ( 3, 'Monthly' )" )
conn.commit()
conn.close()
@@ -248,7 +259,7 @@ def get_comp_set_options(finance):
def get_bill_data():
conn = connect_db(True)
cur = conn.cursor()
cur.execute('SELECT bd.id, bt.id as bill_type_id, bt.name, bd.amount, bd.bill_date FROM bill_type bt, bill_data bd where bt.id = bd.bill_type order by bt.name, bd.bill_date')
cur.execute('SELECT bd.id, bt.id as bill_type_id, bt.name, bd.amount, bd.bill_date FROM bill_type bt, bill_data bd where bt.id = bd.bill_type order by bt.name, bd.bill_date desc')
bd = cur.fetchall()
conn.close()
return bd
@@ -256,11 +267,20 @@ def get_bill_data():
def get_bill_types():
conn = connect_db(True)
cur = conn.cursor()
cur.execute('SELECT id, name FROM bill_type order by name')
cur.execute('SELECT * FROM bill_type order by name')
bt = cur.fetchall()
conn.close()
return bt
def get_bill_freqs():
conn = connect_db(True)
cur = conn.cursor()
cur.execute('SELECT * FROM bill_freq order by name')
bf = cur.fetchall()
conn.close()
return bf
def new_bill( name, amount, bill_date ):
conn = connect_db(False)
cur = conn.cursor()
@@ -277,18 +297,19 @@ def update_bill_data( id, name, amount, bill_date ):
conn.close()
return
def insert_bill_type( bt ):
def insert_bill_type( bt, fq ):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"insert into bill_type ( 'name' ) values ( '{bt}' )" )
print( f"fq={fq}" )
cur.execute( f"insert into bill_type ( 'name', 'freq', 'ann_growth' ) values ( '{bt}', {fq}, 0 )" )
conn.commit()
conn.close()
return
def update_bill_type(id, bill_type):
def update_bill_type(id, bill_type, freq):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"update bill_type set name ='{bill_type}' where id = {id}" )
cur.execute( f"update bill_type set name ='{bill_type}', freq={freq} where id = {id}" )
conn.commit()
conn.close()
return
@@ -308,3 +329,11 @@ def delete_bill_type( id ):
conn.commit()
conn.close()
return
def set_bill_type_growth( id, g ):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"update bill_type set ann_growth ='{g}' where id = {id}" )
conn.commit()
conn.close()
return

11
main.py
View File

@@ -1,9 +1,10 @@
# main.py
from flask import Flask, render_template, request, redirect, url_for, Response, jsonify
from calc import calculate_savings_depletion
from db import init_db, get_finance_data, update_finance, get_budget_data, insert_cset, get_comp_set_data, get_comp_set_options
from db import init_db, get_finance_data, update_finance, get_budget_data, insert_cset, get_comp_set_data, get_comp_set_options, get_bill_freqs
from db import get_bill_data, new_bill, update_bill_data, delete_bill
from db import get_bill_types, insert_bill_type, update_bill_type, delete_bill_type
from bills import derive_bill_data
from collections import defaultdict, Counter
from datetime import datetime
import csv
@@ -142,19 +143,21 @@ def update():
def DisplayBillData():
bill_data = get_bill_data()
bill_types = get_bill_types()
bill_freqs = get_bill_freqs()
now=datetime.today().strftime('%Y-%m-%d')
return render_template('bills.html', now=now, bill_data=bill_data, bill_types=bill_types )
derive_bill_data()
return render_template('bills.html', now=now, bill_data=bill_data, bill_types=bill_types, bill_freqs=bill_freqs )
@app.route('/newbilltype', methods=['POST'])
def InsertBillType():
data = request.get_json()
insert_bill_type( data['bill_type'] )
insert_bill_type( data['bill_type'], data['freq'] )
return "200"
@app.route('/updatebilltype', methods=['POST'])
def UpdateBillType():
data = request.get_json()
update_bill_type( data['id'], data['bill_type'] )
update_bill_type( data['id'], data['bill_type'], data['freq'] )
return "200"
@app.route('/newbill', methods=['POST'])

View File

@@ -18,15 +18,57 @@
</style>
</head>
<body>
<div class="containerfluid row">
<div class="pt-2 containerfluid row">
<h3 align="center">Bill Details (go to <a href="/">Finance Tracker</a>)</h3>
<div class="col-6">
<div class="mt-4 col-6">
<div class="row align-items-center">
<button id="new-bill-data-button" class="px-0 offset-6 col-2 btn btn-success" onCLick="StartNewBillData()"><span class="bi bi-plus-lg"> New Bill</span></button>
<button id="new-bill-type-button" class="mb-3 px-0 offset-2 col-2 btn btn-success" onCLick="StartNewBillType()"><span class="bi bi-plus-lg"> New Bill Type</span></button>
<div class="new-bill-type-class px-0 col-2 d-none"> <input type="text" class="form-control text-end float-end border border-primary" id="new-bill-type-name"></div>
<div class="new-bill-type-class px-0 col-2 d-none"><select id="new-bill-type-freq" class="form-select text-center">
{% for bf in bill_freqs %}
<option value={{bf.id}}>{{bf.name}}</option>
{% endfor %}
</select>
</div>
<button id="save-bill-type" class="new-bill-type-class px-0 col-1 btn btn-success d-none" onClick="NewBillType()"><span class="bi bi-floppy"></span> Save</button>
<button id="canc-bill-type" class="new-bill-type-class px-0 col-1 btn btn-danger d-none" onClick="CancelNewBillType()"><span class="bi bi-trash3"> Cancel</span></button>
</div>
<div class="row">
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Name</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Frequency</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Annual Growth Est</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Actions</ ></div>
</div>
{% for bt in bill_types %}
<div class="row">
<div class="px-0 col-2"><input type="text" class="bill-type-{{bt.id}} form-control text-center bg-white" id="bill-type-name-{{bt.id}}" value="{{ bt.name }}" disabled> </div>
<!-- bind Enter to save this bill-type -->
<script>$("#bill-type-name-{{bt.id}}").keyup(function(event){ if(event.which == 13){ $('#bill-type-save-{{bt.id}}').click(); } event.preventDefault(); });</script>
<div class="px-0 col-2"><select id="bill-type-freq-{{bt.id}}" class="bill-type-{{bt.id}} form-select text-center bg-white" disabled>
{% for bf in bill_freqs %}
<option value={{bf.id}}>{{bf.name}}</option>
{% endfor %}
</select>
</div>
<script>console.log( 'set freq to {{bt.freq}}' ); $('#bill-type-freq-{{bt.id}}').val( {{bt.freq}} );</script>
<div class="px-0 col-2"><input type="text" class="form-control text-center" id="bill-type-grow-{{bt.id}}" value="{{'%.2f'|format(bt.ann_growth)}}%" disabled> </div>
<button id="bill-type-chg-{{bt.id}}" class="px-0 col-1 btn btn-success" onClick="StartUpdateBillType( {{bt.id}} )">Change</button>
<button id="bill-type-del-{{bt.id}}" class="px-0 col-1 btn btn-danger" onClick="DelBillType({{bt.id}})"><span class="bi bi-trash3"> Delete</button>
<button id="bill-type-save-{{bt.id}}" class="px-0 col-1 btn btn-success d-none" onClick="UpdateBillType( {{bt.id}} )">Save</button>
<button id="bill-type-canc-{{bt.id}}" class="px-0 col-1 btn btn-danger d-none" onClick="CancelUpdateBillType({{bt.id}}, '{{bt.name}}')"><span class="bi bi-trash3"> Cancel</button>
</div>
{% endfor %}
</div>
<!-- right-hand-side, bill types (e.g. gas, phone, etc.) -->
<div class="pt-4 col-6">
<div class="row align-items-center">
<button id="new-bill-data-button" class="mb-3 px-0 offset-6 col-2 btn btn-success" onCLick="StartNewBillData()"><span class="bi bi-plus-lg"> New Bill</span></button>
<div class="new-bill-data-class px-0 col-2 d-none"> <select id="new-bill-data-type" class="form-select text-end float-end border border-primary">
{% for el in bill_types %}
<option value={{el.id}}>{{el.name}}</option>
{% for bt in bill_types %}
<option value={{bt.id}}>{{bt.name}}</option>
{% endfor %}
</select>
</div>
@@ -36,54 +78,48 @@
<span class="bi bi-floppy"></span> Save </button>
<button class="new-bill-data-class px-0 col-1 btn btn-danger d-none" onClick="CancelNewBill()" ><span class="bi bi-trash3"> Cancel</span> </button>
</div>
<div class="row">
<div class="px-0 col-2"> <label class="form-control text-center border-0">Name</ > </div>
<div class="px-0 col-2"> <label class="form-control text-center border-0">Date</ > </div>
<div class="px-0 col-2"> <label class="form-control text-center border-0">Amount</ > </div>
</div>
{% for el in bill_data %}
<div class="row">
<div class="px-0 col-2"> <input type="text" class="form-control text-center bg-white" id="bill-data-type-{{el.id}}" value="{{ el.name }}" disabled> </div>
<div class="px-0 col-2"> <input type="date" class="form-control text-center bg-white" id="bill-data-date-{{el.id}}" value="{{ el.bill_date }}" disabled> </div>
<div class="px-0 col-2"> <input type="number" class="form-control text-center bg-white" id="bill-data-amount-{{el.id}}" value="{{ el.amount }}" disabled> </div>
<button id="bill-data-chg-{{el.id}}" class="px-0 col-1 btn btn-success" onClick="StartUpdateBill( {{el.id}} )">Change</button>
<button id="bill-data-del-{{el.id}}" class="px-0 col-1 btn btn-danger" onClick="DeleteBill( {{el.id }} )"><span class="bi bi-trash3"> Delete
<button id="bill-data-save-{{el.id}}" class="px-0 col-1 btn btn-success d-none" onClick="UpdateBill( {{el.id}} )">Save</button>
<button id="bill-data-canc-{{el.id}}" class="px-0 col-1 btn btn-danger d-none"
onClick="CancelUpdateBill({{el.id}}, '{{el.name}}', '{{el.bill_date}}', '{{el.amount}}')"> <span class="bi bi-trash3"> Cancel</button>
</button>
</div>
{% endfor %}
</div>
<!-- right-hand-side, bill types (e.g. gas, phone, etc.) -->
<div class="col-6">
<div class="row align-items-center">
<button id="new-bill-type-button" class="px-0 offset-2 col-2 btn btn-success" onCLick="StartNewBillType()"><span class="bi bi-plus-lg"> New Bill Type</span></button>
<div class="new-bill-type-class px-0 col-2 d-none"> <input type="text" class="form-control text-end float-end border border-primary" id="new-bill-type-name"></div>
<button id="save-bill-type" class="new-bill-type-class px-0 col-1 btn btn-success d-none" onClick="NewBillType()"><span class="bi bi-floppy"></span> Save</button>
<button id="canc-bill-type" class="new-bill-type-class px-0 col-1 btn btn-danger d-none" onClick="CancelNewBillType()"><span class="bi bi-trash3"> Cancel</span></button>
</div>
<div class="row">
<div class="px-0 col-2"><label class="form-control text-center border-0">Name</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0">Frequency</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0">Annual Growth Est</ ></div>
</div>
{% for el in bill_types %}
<div class="row">
<div class="px-0 col-2"><input type="text" class="bill-type-name form-control text-center bg-white" id="bill-type-name-{{el.id}}" value="{{ el.name }}" disabled> </div>
<!-- bind Enter to save this bill-type -->
<script>$("#bill-type-name-{{el.id}}").keyup(function(event){ if(event.which == 13){ $('#bill-type-save-{{el.id}}').click(); } event.preventDefault(); });</script>
<div class="px-0 col-2"><input type="text" class="bill-type-{{el.id}} form-control text-center bg-white" id="bill-type-freq-{{el.id}}" value="not yet" disabled> </div>
<div class="px-0 col-2"><input type="text" class="bill-type-{{el.id}} form-control text-center bg-white" id="bill-type-grow-{{el.id}}" value="not yet" disabled> </div>
<button id="bill-type-chg-{{el.id}}" class="px-0 col-1 btn btn-success" onClick="StartUpdateBillType( {{el.id}} )">Change</button>
<button id="bill-type-del-{{el.id}}" class="px-0 col-1 btn btn-danger" onClick="DelBillType({{el.id}})"><span class="bi bi-trash3"> Delete</button>
<button id="bill-type-save-{{el.id}}" class="px-0 col-1 btn btn-success d-none" onClick="UpdateBillType( {{el.id}} )">Save</button>
<button id="bill-type-canc-{{el.id}}" class="px-0 col-1 btn btn-danger d-none" onClick="CancelUpdateBillType({{el.id}}, '{{el.name}}')"><span class="bi bi-trash3"> Cancel</button>
</div>
<!-- create tabbed view for each bill type -->
<nav id="bills-nav" class="nav nav-tabs">
{% for bt in bill_types %}
<button class="nav-link" id="tab-{{bt.name}}" data-bs-toggle="tab" data-bs-target="#tab-{{bt.id}}" type="button" role="tab" aria-controls="tab1" aria-selected="true">{{bt.name}}</button>
{% endfor %}
</div>
</nav>
<div class="tab-content">
{% for bt in bill_types %}
{% if loop.first %}
<div id="tab-{{bt.id}}" class="tab-pane active">
{% else %}
<div id="tab-{{bt.id}}" class="tab-pane">
{% endif %}
{% for bd in bill_data %}
{% if loop.first %}
<div class="row pt-2">
<div class="p-0 col-2"> <label class="form-control text-center border-0 fw-bold bg-light rounded-0">Name</ > </div>
<div class="p-0 col-2"> <label class="form-control text-center border-0 fw-bold bg-light rounded-0">Date</ > </div>
<div class="p-0 col-2"> <label class="form-control text-center border-0 fw-bold bg-light rounded-0">Amount</ > </div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Actions</ ></div>
</div>
{% endif %}
{% if bd.bill_type_id == bt.id %}
<div class="row">
<div class="px-0 col-2"> <input type="text" class="form-control text-center bg-white" id="bill-data-type-{{bd.id}}" value="{{ bd.name }}" disabled> </div>
<div class="px-0 col-2"> <input type="date" class="form-control text-center bg-white" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
<div class="px-0 col-2"> <input type="number" class="form-control text-center bg-white" id="bill-data-amount-{{bd.id}}" value="{{ bd.amount }}" disabled> </div>
<button id="bill-data-chg-{{bd.id}}" class="px-0 col-1 btn btn-success" onClick="StartUpdateBill( {{bd.id}} )">Change</button>
<button id="bill-data-del-{{bd.id}}" class="px-0 col-1 btn btn-danger" onClick="DeleteBill( {{bd.id }} )"><span class="bi bi-trash3"> Delete
<button id="bill-data-save-{{bd.id}}" class="px-0 col-1 btn btn-success d-none" onClick="UpdateBill( {{bd.id}} )">Save</button>
<button id="bill-data-canc-{{bd.id}}" class="px-0 col-1 btn btn-danger d-none"
onClick="CancelUpdateBill({{bd.id}}, '{{bd.name}}', '{{bd.bill_date}}', '{{bd.amount}}')"> <span class="bi bi-trash3"> Cancel</button>
</button>
</div>
{% endif %}
{% endfor %}
</div>
{% endfor %}
</div>
<script>
function StartNewBillData()
{
@@ -177,12 +213,14 @@
$('.new-bill-type-class').addClass('d-none')
$('#new-bill-type-button').removeClass('d-none')
$('#new-bill-type-name').val('')
// reset select to first option
$('#new-bill-type-freq').val( $('#new-bill-type-freq option:first').attr('value') )
}
function NewBillType()
{
$.ajax( { type: 'POST', url: '/newbilltype',
contentType: 'application/json', data: JSON.stringify( { 'bill_type': $('#new-bill-type-name').val() } ),
contentType: 'application/json', data: JSON.stringify( { 'bill_type': $('#new-bill-type-name').val(), 'freq': $('#new-bill-type-freq').val() } ),
success: function() { window.location='bills' } } )
}
@@ -190,11 +228,11 @@
{
val=$('#bill-type-name-'+id).val()
// "disable" the freq & growth
$('.bill-type-'+id).addClass('bg-light text-secondary').removeClass('bg-white')
// "enable" fields for edits
$('.bill-type-'+id).prop('disabled', false)
// "enable" name for edits
$('#bill-type-name-'+id).prop('disabled', false).focus()
// put focus into name field
$('#bill-type-name-'+id).focus()
// move cursor to the end after 'focus()' above
$('#bill-type-name-'+id).val('').val( val )
@@ -209,7 +247,7 @@
function UpdateBillType(id)
{
$.ajax( { type: 'POST', url: '/updatebilltype',
contentType: 'application/json', data: JSON.stringify( { 'id': id, 'bill_type': $('#bill-type-name-'+id).val() } ),
contentType: 'application/json', data: JSON.stringify( { 'id': id, 'bill_type': $('#bill-type-name-'+id).val(), 'freq': $('#bill-type-freq-'+id).val() } ),
success: function() { window.location='bills' } } )
}
@@ -241,6 +279,11 @@
$("#new-bill-type-name").keyup(function(event){ if(event.which == 13){ $("#save-bill-type").click(); } event.preventDefault(); });
// note we also dynamically bound each bill-type-name to save on Enter when we create them in a loop
// force something to be active
$('#bills-nav .nav-link').first().addClass('active');
// now go back to last tab, as per something I dont know yet :)
$('#tab-Water').tab('show');
} )
</script>
</body>