Compare commits

...

7 Commits

9 changed files with 457 additions and 100 deletions

2
BUGS
View File

@@ -1,2 +1,4 @@
* kayo bills are wrong in between normal bills
* added an electricity bill by accident for 2018, that kills lots :(
- something to do with missing year of data in quarterly bills - still an issue

25
TODO
View File

@@ -1,19 +1,24 @@
bills html, and growth types are very lame... could I do something more like:
{% for gt in growth %}
{% if gt.name == 'Min' %}
<option value='min'
{% if bt.which_growth == gt.name %}selected{% endif %}
>{{'%.2f'|format(bt.ann_growth_min)}}% {{gt.name}}</option>
CALC:
* if I quit at different times of the financial year, technically the amount I earn will be taxed
differently, but hard to cal - slides around with tax brackets in future
differently, but hard to calc - slides around with tax brackets in future
UI:
* have a comparison management capability - somehow, at least be able to delete csets (can I put an x inside the menu? OR, have a manage button and pop-up?)
* maybe a help/note/set of links somehow, to common things: e.g. macquarie int rate page, inflation source, etc.
- also then could effectively take those "notes" out of db.py and incorporate, so that when I update stuff it gets done right
- in fact, I *COULD* have a basic date check/workflow, e.g. its been more than X days since Y was updated, click here
and it walks me through the manual update steps and when I finish its datestamped until next time
* to allow <yr> total on bill summary, need to re-work annual growth to a drop-down
- [DONE] mock-up
- need to change 'which growth' to accommodate new growth models (cpi, override*)
- [DONE] do this in DB with new table - then don't do crazy pattern matching/making up overrides in the UI code
- get UI to use bill_growth_types table
- get UseGrowth() to use bill_growth_types table
For bills:
* quick hack, Force Re-estimate (delete estimates and reload bill page)
* if we change certain items on the finance page, need to re-do bills, e.g.
- change D # pays to quit, or Car lease/buy out
* might need to be able to mark a specific bill as an outlier:
- so we ignore the data somehow (think Gas is messing with my bills)
- and even electricity, water, etc. for when we were away in Europe but mostly gas/elec

121
bills.py
View File

@@ -1,6 +1,8 @@
from db import set_bill_type_growth, new_bill
from db import set_bill_type_growth, new_bill, deleteFutureEstimates, get_finance_data, get_bill_data, get_bill_types, get_bill_freqs
from calc import calc_key_dates
from defines import END_YEAR
import datetime
import re
from datetime import date, timedelta
@@ -237,6 +239,8 @@ def actually_add_estimated_new_quarter_bill_forced( bill_type, bill_info, yr, q
# NOTE: ALWAYS called for first year - don't always add bills/see below
def add_missing_monthly_bills_in_yr( bill_type, bill_info, yr ):
print( f"add_missing_monthly_bills_in_yr for ( bt={bill_type} -- yr={yr} )" )
# start date arithmetic from first bill (this is possibly an issue if monthly is not
# really perfectly the same each month, but its only for an estimate so should be ok
dd = bill_info[bill_type]['first_bill']['bill_date'][8:]
@@ -294,8 +298,18 @@ def get_growth_value( bt, bill_type ):
return el['ann_growth_min']
elif which == 'simple':
return el['ann_growth_simple']
else:
elif which == 'max':
return el['ann_growth_max']
elif which == 'cpi':
finance_data = get_finance_data()
return finance_data['Inflation']
else:
match = re.match("flat-(\d+)", which )
if match:
return int(match.group(1))
else:
print( f"FAILED TO GET_GROWTH_VALUE --> which={which}" )
return 0
################################################################################
@@ -310,7 +324,6 @@ def process_bill_data(bd, bt, bf, key_dates):
bt_id_ann_growth_avg = {row["id"]: row["ann_growth_avg"] for row in bt}
bt_id_name = {row["id"]: row["name"] for row in bt}
# this maps freq to bills per annum (e.g. id=2 to 4 bills per annum)
bf_id_num = {row["id"]: row["num_bills_per_annum"] for row in bf}
# and allows me a way to see if the bill is quarterly but also fixed or seasonal
@@ -386,7 +399,7 @@ def process_bill_data(bd, bt, bf, key_dates):
if yr in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr]) >= bill_info[bill_type]['num_ann_bills'] and bill_info[bill_type]['num_ann_bills'] !=4:
continue
add_missing_bills_for_yr( bill_type, bill_info, yr )
derive_ann_growth( bill_type, bill_info, key_dates, future_car_bills, future_D_quit_bills )
derive_ann_growth( bill_type, bill_info, key_dates )
deal_with_future_car_bills( key_dates, future_car_bills, bill_info )
deal_with_future_D_quit_bills( key_dates, future_D_quit_bills, bill_info )
@@ -405,11 +418,10 @@ def deal_with_future_car_bills( key_dates, future_car_bills, bill_info ):
amt=fb['amount']
bt=fb['bill_type']
# factor in growth for next bill
for yr in range( int(car_yr), END_YEAR ):
for yr in range( int(car_yr), END_YEAR+1 ):
new_date=f"{yr}-{car_mmdd}"
# if we dont already have an annual bill for this year (all car bills are annual)
if yr not in bill_info[bt]['year']:
print( f"amt of a car fb is: {amt}, car_yr={car_yr}, yr={yr}, nd={new_date}")
new_estimated_bill( bill_info, yr, fb['bill_type'], amt, new_date )
amt += amt * bill_info[bt]['growth']/100
@@ -429,27 +441,23 @@ def deal_with_future_D_quit_bills( key_dates, future_D_quit_bills, bill_info ):
bt=fb['bill_type']
if bill_info[bt]['num_ann_bills'] == 1:
# factor in growth for next bill
for yr in range( int(D_quit_yr), END_YEAR ):
for yr in range( int(D_quit_yr), END_YEAR+1 ):
new_date=f"{yr}-{dq_mm}-{dq_dd}"
# if we dont already have an annual bill for this year
if not find_this_bill( bt, bill_info, new_date ):
print( f"amt of a D_quit fb is: {amt}, dq_yr={D_quit_yr}, yr={yr}, nd={new_date}")
new_estimated_bill( bill_info, yr, bt, amt, new_date )
amt += amt * bill_info[bt]['growth']/100
elif bill_info[bt]['num_ann_bills'] == 12:
print( f"should be adding monthly future bill" )
# do rest of this year, then next years
for m in range( int(dq_mm), 13):
new_date=f"{D_quit_yr}-{m:02d}-{dq_dd}"
if not find_this_bill( bt, bill_info, new_date ):
print( f"amt of a D_quit fb is: {amt}, dq_yr={D_quit_yr}, nd={new_date}")
new_estimated_bill( bill_info, yr, bt, amt, new_date )
for yr in range( int(D_quit_yr)+1, END_YEAR ):
for yr in range( int(D_quit_yr)+1, END_YEAR+1 ):
amt += amt * bill_info[bt]['growth']/100
for m in range( 1, 13):
new_date=f"{yr}-{m:02d}-{dq_dd}"
if not find_this_bill( bt, bill_info, new_date ):
print( f"amt of a D_quit fb is: {amt}, dq_yr={D_quit_yr}, yr={yr}, nd={new_date}")
new_estimated_bill( bill_info, yr, bt, amt, new_date )
################################################################################
@@ -492,7 +500,7 @@ def ProportionQtrlyData( bill_type, bill_info ):
# terms of min/avg/max - uses qtr data for qtrly bills, or just normal totals
# for other bill types
################################################################################
def derive_ann_growth( bill_type, bill_info, key_dates, future_car_bills, future_D_quit_bills ):
def derive_ann_growth( bill_type, bill_info, key_dates ):
# just do up to now so we stop earlier than looking at other estimated (just an optimisation)
now_yr = datetime.date.today().year
@@ -586,3 +594,90 @@ def calc_future_totals(bill_info, bill_types):
# had to round to 2 decimal here to get sensible totals
total[bt['id']][yr] = round( total[bt['id']][yr], 2 )
return total
################################################################################
# When we change the day D_quits, or we buyout the car, then future bills need
# to change/rebuild estimates, convenience routine used to find future bills -
# rather than go through them as we render /bills
################################################################################
def getFutureBills(bd,bt,future_car_bills, future_D_quit_bills):
# this maps a bill id to a name
bt_id_name = {row["id"]: row["name"] for row in bt}
for bill in bd:
bill_type = bill['bill_type']
if bill['bill_date'] == 'future':
# Future bills, deal with them at the end - they have dynamic start dates
if 'Hyundai' in bt_id_name[bill_type]:
future_car_bills.insert( 0, bill )
else:
future_D_quit_bills.insert( 0, bill )
return
################################################################################
# When we change the day D_quits, or we buyout the car, then future bills need
# to change/rebuild estimates, convenience routine used to handle this
################################################################################
def recalcFutureBills():
future_car_bills=[]
future_D_quit_bills=[]
print("Recalculating future bills as we changed a key date" )
finance_data = get_finance_data()
key_dates = calc_key_dates( finance_data )
bill_data = get_bill_data("order_by_date_only")
bill_types = get_bill_types()
bill_freqs = get_bill_freqs()
bt_id_freq = {row["id"]: row["freq"] for row in bill_types}
# this maps freq to bills per annum (e.g. id=2 to 4 bills per annum)
bf_id_num = {row["id"]: row["num_bills_per_annum"] for row in bill_freqs}
getFutureBills(bill_data, bill_types, future_car_bills, future_D_quit_bills)
deleteFutureEstimates()
# deal with future car bills
car_yr=key_dates['D_hyundai_owned'][0:4]
car_mmdd=key_dates['D_hyundai_owned'][5:]
for fb in future_car_bills:
amt=fb['amount']
bt=fb['bill_type']
# only can use simple growth as its a future bill
growth=bill_types[bt]['ann_growth_simple']
# factor in growth for next bills
for yr in range( int(car_yr), END_YEAR+1 ):
new_date=f"{yr}-{car_mmdd}"
new_bill( fb['bill_type'], amt, new_date, 1 )
amt += amt * growth/100
# deal with future D_Quit bills
D_quit_yr = key_dates['D_quit_date'][0:4]
dq_mm=key_dates['D_quit_date'][5:7]
dq_dd=key_dates['D_quit_date'][8:]
# avoid feb 29+ :)
if int(dq_dd) > 28: dq_dd=28
for fb in future_D_quit_bills:
# deal with future bills due to their starting dates being dynamic
amt=fb['amount']
bt=fb['bill_type']
growth=bill_types[bt]['ann_growth_simple']
num_ann_bills= bf_id_num[bt_id_freq[bt]]
if num_ann_bills == 1:
# factor in growth for next bill
for yr in range( int(D_quit_yr), END_YEAR+1 ):
new_date=f"{yr}-{dq_mm}-{dq_dd}"
# if we dont already have an annual bill for this year
new_bill( fb['bill_type'], amt, new_date, 1 )
amt += amt * growth/100
elif num_ann_bills == 12:
# do rest of this year, then next years
for m in range( int(dq_mm), 13):
new_date=f"{D_quit_yr}-{m:02d}-{dq_dd}"
new_bill( fb['bill_type'], amt, new_date, 1 )
for yr in range( int(D_quit_yr)+1, END_YEAR+1 ):
amt += amt * growth/100
for m in range( 1, 13):
new_date=f"{yr}-{m:02d}-{dq_dd}"
new_bill( fb['bill_type'], amt, new_date, 1 )
return

92
db.py
View File

@@ -84,6 +84,12 @@ def init_db():
FOREIGN KEY(comparison_set_id) REFERENCES comparison_set(id)
)''')
cur.execute('''CREATE TABLE IF NOT EXISTS bill_freq (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name STRING,
num_bills_per_annum INTEGER
)''')
cur.execute('''CREATE TABLE IF NOT EXISTS bill_type (
id INTEGER PRIMARY KEY AUTOINCREMENT,
freq INTEGER,
@@ -95,12 +101,6 @@ def init_db():
FOREIGN KEY(freq) REFERENCES bill_freq(id)
)''')
cur.execute('''CREATE TABLE IF NOT EXISTS bill_freq (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name STRING,
num_bills_per_annum INTEGER
)''')
cur.execute('''CREATE TABLE IF NOT EXISTS bill_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bill_type INTEGER,
@@ -116,28 +116,56 @@ def init_db():
show_estimated INTEGER
)''')
# Check if table is empty, if so insert default values
cur.execute('''CREATE TABLE IF NOT EXISTS bill_growth_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name string,
value INTEGER
)''')
# Check if finance table is empty, if so insert default values
cur.execute('SELECT COUNT(*) FROM finance')
if cur.fetchone()[0] == 0:
###
# For now manually update below on the fortnight of the original pay shcedule to compare saved version vs. our reality. Update:
# Savings (Macq+me bank) -- noting ME bank is: $2001.19, nab is -5200
# TLS/CBA prices
# Interest rate
# D_leave_owed_in_days
# maybe quarterly update Inflation? (this is harder to appreciate, seems much lower officialy than Savings, but which inflation:
# I've decided to use RBA Trimmed Mean CPI YoY -- https://tradingeconomics.com/australia/inflation-cpi
###
cur.execute('''INSERT INTO finance (D_Salary, D_Num_fortnights_pay, School_Fees, Car_loan_via_pay, Car_loan, Car_balloon, Car_buyout, Living_Expenses, Savings, Interest_Rate,
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))
# Check if bill_freq table is empty, if so insert default values
cur.execute('SELECT COUNT(*) FROM bill_freq')
if cur.fetchone()[0] == 0:
cur.execute( "INSERT INTO bill_freq values ( 1, 'Annual', 1 )" )
cur.execute( "INSERT INTO bill_freq values ( 2, 'Quarterly', 4 )" )
cur.execute( "INSERT INTO bill_freq values ( 3, 'Quarterly (forced)', 4 )" )
cur.execute( "INSERT INTO bill_freq values ( 4, 'Monthly', 12 )" )
# start with no specific Tab/bill_type to show, and dont show_estimated
# Check if bill_ui table is empty, if so insert default values
cur.execute('SELECT COUNT(*) FROM bill_ui')
if cur.fetchone()[0] == 0:
cur.execute( "INSERT INTO bill_ui values ( 1, null, 0 )" )
# Check if bill_growth_types table is empty, if so insert default values
cur.execute('SELECT COUNT(*) FROM bill_growth_types')
if cur.fetchone()[0] == 0:
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Min', 0 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Avg', 0 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Max', 0 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Simple', 0 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'CPI', 0 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 0', 0 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 1', 1 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 2', 2 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 3', 3 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 4', 4 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 5', 5 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 6', 6 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 7', 7 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 8', 8 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 9', 9 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 10', 10 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 12', 12 )" )
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 15', 15 )" )
conn.commit()
conn.close()
@@ -376,6 +404,14 @@ def set_bill_type_growth( id, min_g, avg_g, max_g, simple_g ):
conn.close()
return
def get_bill_growth_types():
conn = connect_db(True)
cur = conn.cursor()
cur.execute('SELECT * FROM bill_growth_types')
growth = cur.fetchall()
conn.close()
return growth
def get_bill_ui():
conn = connect_db(True)
cur = conn.cursor()
@@ -397,6 +433,14 @@ def save_ui(data):
return
def deleteFutureEstimates():
conn = connect_db(False)
cur = conn.cursor()
cur.execute( "delete from bill_data where bill_date != 'future' and bill_type in ( select bill_type from bill_data where bill_date='future')" )
conn.commit()
conn.close()
return
def delete_estimated_bills():
conn = connect_db(False)
cur = conn.cursor()
@@ -404,3 +448,19 @@ def delete_estimated_bills():
conn.commit()
conn.close()
return
def delete_estimated_bills_for(bt_id):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"delete from bill_data where estimated=1 and bill_type = {bt_id}" )
conn.commit()
conn.close()
return
def delete_cset(id):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"delete from comparison_set where id = '{id}'" )
conn.commit()
conn.close()
return

67
main.py
View File

@@ -1,16 +1,19 @@
# main.py
from flask import Flask, render_template, request, redirect, url_for, Response, jsonify
from calc import calculate_savings_depletion, calc_key_dates
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, delete_estimated_bills
from db import init_db, get_finance_data, update_finance, get_budget_data
from db import insert_cset, get_comp_set_data, get_comp_set_options, delete_cset
from db import get_bill_freqs, get_bill_growth_types
from db import get_bill_data, new_bill, update_bill_data, delete_bill, delete_estimated_bills, delete_estimated_bills_for
from db import get_bill_ui, save_ui
from db import get_bill_types, insert_bill_type, update_bill_type, delete_bill_type, use_growth
from bills import process_bill_data, calc_future_totals, set_bill_type_growth
from bills import process_bill_data, calc_future_totals, set_bill_type_growth, recalcFutureBills
from defines import END_YEAR
from collections import defaultdict, Counter
from datetime import datetime, date
import csv
import io
import requests
from disp import FP_VAR
app = Flask(__name__)
@@ -113,6 +116,8 @@ def save():
@app.route('/update', methods=['POST'])
def update():
old_finance_data = get_finance_data()
finance_data = (
request.form['D_Salary'],
request.form['D_Num_fortnights_pay'],
@@ -142,7 +147,26 @@ def update():
request.form['Ioniq6_future']
)
update_finance(finance_data)
# FIXME: need code here to delete/rebuild future bills if we change "D # Pays to quit "
new_finance_data = get_finance_data()
# changed Ioniq6_future, Car_buyout_date or D_Num_fortnights_pay, so lets force recalc key_dates, and therefore estimated bills
if old_finance_data['D_Num_fortnights_pay'] != new_finance_data['D_Num_fortnights_pay'] or old_finance_data['Ioniq6_future'] != new_finance_data['Ioniq6_future'] or old_finance_data['Car_buyout_date'] != new_finance_data['Car_buyout_date']:
recalcFutureBills()
if old_finance_data['Inflation'] != new_finance_data['Inflation']:
# need to check if any bill type is using CPI, if so, force those future bills to be recalculated
bill_types = get_bill_types()
for bt in bill_types:
if bt['which_growth'] == 'cpi':
print( f"OK, changed inflation and need to redo bills for bt_id={bt['id']}" )
delete_estimated_bills_for( bt['id'] )
#recalc_estimated_bills_for( bt['id'] )
# okay, now go through code to recalc bills...
base=request.url_root
response = requests.get(f"{base}/bills")
if response.status_code == 200:
print("ALL GOOD")
else:
print("FFS")
return redirect(url_for('index'))
@app.route('/bills')
@@ -154,13 +178,14 @@ def DisplayBillData():
bill_types = get_bill_types()
bill_freqs = get_bill_freqs()
bill_ui = get_bill_ui()
bill_growth_types = get_bill_growth_types()
# take bill data, AND work out estimated future bills - process this into the bill_info array,
bill_info=process_bill_data(bill_data, bill_types, bill_freqs, key_dates)
# get an array of the total costs of bills each year - purely cosmetic (using bill_info)
total=calc_future_totals(bill_info, bill_types)
# update/re-get bill_data now that new estimated bills have been added
bill_data = get_bill_data("order_by_bill_type_then_date")
return render_template('bills.html', bill_data=bill_data, bill_types=bill_types, bill_freqs=bill_freqs, bill_ui=bill_ui, this_year=datetime.today().year, END_YEAR=END_YEAR, total=total, key_dates=key_dates )
return render_template('bills.html', bill_data=bill_data, bill_types=bill_types, bill_freqs=bill_freqs, bill_ui=bill_ui, this_year=datetime.today().year, END_YEAR=END_YEAR, total=total, key_dates=key_dates, growth=bill_growth_types, cpi=finance_data['Inflation'] )
@app.route('/newbilltype', methods=['POST'])
def InsertBillType():
@@ -218,8 +243,40 @@ def SaveUI():
@app.route('/force_recalc_bills', methods=['POST'])
def force_recalc_bills():
delete_estimated_bills()
recalcFutureBills()
return "200"
@app.route('/cset')
def cset():
finance_data = get_finance_data()
get_comp_set_options(finance_data)
comp_data={}
for el in finance_data['COMP_SETS']:
comp_data[el[0]] = get_comp_set_data( el[0] )
# delete items not that helpful (same for all, not that interesting)
if el[0]:
del comp_data[el[0]]['vars']['Car_loan_via_pay']
del comp_data[el[0]]['vars']['Mark_reno']
del comp_data[el[0]]['vars']['Mark_reno_date']
del comp_data[el[0]]['vars']['Overseas_trip_date']
del comp_data[el[0]]['vars']['Car_balloon']
del comp_data[el[0]]['vars']['Mich_present']
del comp_data[el[0]]['vars']['D_TLS_shares']
del comp_data[el[0]]['vars']['M_TLS_shares']
return render_template('cset.html', finance=finance_data, comp_data=comp_data )
@app.route('/delcset', methods=['POST'])
def DeleteCSet():
data = request.get_json()
delete_cset( data['id'] )
return "200"
# quick health route so traefik knows we are up
@app.route('/health')
def health():
return {"status": "ok"}, 200
# Main program
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -8,4 +8,4 @@ pysqlite3
Werkzeug
flask-compress
gunicorn
requests

View File

@@ -36,7 +36,7 @@
{% endfor %}
</table>
#}
<div class="col-7">
<div class="col-8">
<div class="row">
<div class="col-2 form-control-inline d-none new-bill-type-class">Bill Type</div>
<div class="col-2 form-control-inline d-none new-bill-type-class">Frequency</div>
@@ -55,66 +55,92 @@
<button id="recalc-bills" class="mt-4 col-2 offset-3 btn btn-warning bg-warning-subtle text-warning" onClick="ForceRecalcBills()"><span class="bi bi-repeat"> Recalculate</span></button>
</div>
<div class="row">
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Frequency</ ></div>
<div class="px-0 col-4"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Annual Growth Est (min/avg/max/simple)</ ></div>
<div class="px-0 col-1"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">{{this_year}} Total</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></div>
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Frequency</ ></div>
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold
bg-body-tertiary rounded-0">Annual Growth</ ></div>
{% for yr in range( 2025, 2032 ) %}
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0
fw-bold bg-body-tertiary rounded-0">{{yr}} Total</ ></div>
{% endfor %}
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
{# spacer to get header line right now we don't use forced col widths #}
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0"> </ ></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" id="bill-type-name-{{bt.id}}" value="{{ bt.name }}" disabled> </div>
<div class="px-0 col-1"><input type="text" class="bill-type-{{bt.id}} form-control text-center" 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" disabled>
<div class="px-0 col-1"><select id="bill-type-freq-{{bt.id}}" class="bill-type-{{bt.id}} form-select text-center" disabled>
{% for bf in bill_freqs %}
<option value={{bf.id}}>{{bf.name}}</option>
{% endfor %}
</select>
</div>
<script>$('#bill-type-freq-{{bt.id}}').val( {{bt.freq}} );</script>
<div class="px-0 col-4">
<div class="px-0 col">
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="min-{{bt.id}}" autocomplete="off"
onChange="UseGrowth({{bt.id}}, 'min')" {% if bt.which_growth == 'min' %}checked{% endif %}>
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="min-{{bt.id}}" style="width: 6ch;">
{% if bt.ann_growth_min> 0 and bt.ann_growth_min < 10 %} &nbsp; {% endif %}
{{'%.2f'|format(bt.ann_growth_min)}}
</label>
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="avg-{{bt.id}}" autocomplete="off"
onChange="UseGrowth({{bt.id}}, 'avg')" {% if bt.which_growth == 'avg' %}checked{% endif %}>
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="avg-{{bt.id}}" style="width: 6ch;">
{% if bt.ann_growth_avg < 10 %} &nbsp; {% endif %}
{{'%.2f'|format(bt.ann_growth_avg)}}
</label>
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="max-{{bt.id}}" autocomplete="off"
onChange="UseGrowth({{bt.id}}, 'max')" {% if bt.which_growth == 'max' %}checked{% endif %}>
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="max-{{bt.id}}" style="width: 6ch;">
{% if bt.ann_growth_max < 10 %} &nbsp; {% endif %}
{{'%.2f'|format(bt.ann_growth_max)}}
</label>
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="simple-{{bt.id}}" autocomplete="off"
onChange="UseGrowth({{bt.id}}, 'simple')" {% if bt.which_growth == 'simple' %}checked{% endif %}>
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="simple-{{bt.id}}" style="width: 6ch;">
{% if bt.ann_growth_simple < 10 %} &nbsp; {% endif %}
{{'%.2f'|format(bt.ann_growth_simple)}}
</label>
<select id="{{bt.id}}_growth" class="form-select col" onChange="UseGrowth({{bt.id}})">
{% for gt in growth %}
{% if gt.name == 'Min' %}
<option value='min'
{% if bt.which_growth == 'min' %}selected{% endif %}
>{{'%.2f'|format(bt.ann_growth_min)}}% {{gt.name}}</option>
{% elif gt.name == 'Avg' %}
<option value='avg'
{% if bt.which_growth == 'avg' %}selected{% endif %}
>{{'%.2f'|format(bt.ann_growth_avg)}}% {{gt.name}}</option>
{% elif gt.name == 'Max' %}
<option value='max'
{% if bt.which_growth == 'max' %}selected{% endif %}
>{{'%.2f'|format(bt.ann_growth_max)}}% {{gt.name}}</option>
{% elif gt.name == 'Simple' %}
<option value='simple'
{% if bt.which_growth == 'simple' %}selected{% endif %}
>{{'%.2f'|format(bt.ann_growth_simple)}}% {{gt.name}}</option>
{% elif gt.name == 'CPI' %}
<option value='cpi'
{% if bt.which_growth == 'cpi' %}selected{% endif %}
>{{'%.2f'|format(cpi)}}% {{gt.name}}</option>
{% else %}
<option value='flat-{{gt.value}}'
{% if bt.which_growth == 'flat-'+gt.value|string %}selected{% endif %}
>{{'%.2f'|format(gt.value)}}% {{gt.name}}
</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="px-0 col-1"><input type="text" class="bill-type-total-{{bt.id}} form-control text-center" id="bill-type-total-{{bt.id}}" value="${{'%.2f'|format(total[bt.id][this_year])}}" disabled> </div>
<button id="bill-type-chg-{{bt.id}}" class="px-0 col-1 btn btn-success bg-success-subtle text-success" onClick="StartUpdateBillType( {{bt.id}} )"><span class="bi bi-pencil-square"> Change</button>
<button id="bill-type-del-{{bt.id}}" class="px-0 col-1 btn btn-danger bg-danger-subtle text-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 bg-success-subtle text-success d-none" onClick="UpdateBillType( {{bt.id}} )"><spam class="bi bi-floppy"> Save</button>
<button id="bill-type-canc-{{bt.id}}" class="px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelUpdateBillType({{bt.id}}, '{{bt.name}}')"><span class="bi bi-x"> Cancel</button>
{% for yr in range( 2025, 2032 ) %}
<div class="px-0 col"><input type="text" class="bill-type-total-{{bt.id}} form-control text-center" id="bill-type-total-{{bt.id}}" value="${{'%.2f'|format(total[bt.id][yr])}}" disabled> </div>
{% endfor %}
<button id="bill-type-chg-{{bt.id}}" class="px-0 col btn btn-success bg-success-subtle text-success" onClick="StartUpdateBillType( {{bt.id}} )"><span class="bi bi-pencil-square"> Change</button>
<button id="bill-type-del-{{bt.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger" onClick="DelBillType({{bt.id}})"><span class="bi bi-trash3"> Delete</button>
<button id="bill-type-save-{{bt.id}}" class="px-0 col btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBillType( {{bt.id}} )"><spam class="bi bi-floppy"> Save</button>
<button id="bill-type-canc-{{bt.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelUpdateBillType({{bt.id}}, '{{bt.name}}')"><span class="bi bi-x"> Cancel</button>
</div>
{% endfor %}
{% for yr in range( this_year, END_YEAR) %}
<div class="row">
<div class="px-0 col"></div>
<div class="px-0 col"></div>
<div class="px-0 col"><input type="text" class="form-control text-end text-primary fs-5" value="TOTAL:"></div>
{% for yr in range( this_year, END_YEAR+1) %}
{% set tot=namespace( sum=0 ) %}
{% for bt in bill_types %}
{% if bt.id in total %}
{% set tot.sum = tot.sum + total[bt.id][yr] %}
{% endif %}
{% endfor %}
<div class="px-0 col"><input type="text" class="form-control text-center text-primary bg-dark fs-5" value="${{'%.2f'|format(tot.sum)}}" disabled></div>
{#
{% set markup="h5" %}
{% if yr == this_year %}
{% set markup="h4 pt-4" %}
@@ -127,12 +153,16 @@
${{'%.2f'|format(tot.sum)}}
</div>
</div>
#}
{% endfor %}
<div class="px-0 col"></div>
<div class="px-0 col"></div>
</div>
</div>
<!-- right-hand-side, bill types (e.g. gas, phone, etc.) -->
<div class="col-5">
<div class="col-4">
<div class="row">
<div class="col-2 form-control-inline d-none new-bill-data-class">Bill Type</div>
<div id="new-bill-data-date-label" class="col-4 form-control-inline d-none new-bill-data-class">Date</div>
@@ -186,8 +216,9 @@
<div class="row pt-2">
<div class="p-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></div>
<div class="p-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Date</ ></div>
<div class="p-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Amount</ ></div>
<div class="px-0 col-4"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
<div class="p-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Amount</ ></div>
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"></ ></div>
</div>
{% endif %}
{% if bd.bill_type == bt.id %}
@@ -203,15 +234,24 @@
<div class="px-0 col-2"> <input type="text" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
{% else %}
<div class="px-0 col-2"> <input type="date" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
<script>
if( typeof future_id !== 'undefined' && future_id>0) {
first_col_id={{bd.id}}
future_id=0
}
</script>
{% endif %}
<div class="px-0 col-2"> <input type="number" class="{{classes}}" id="bill-data-amount-{{bd.id}}" value="{{ bd.amount }}" disabled> </div>
<div class="px-0 col"> <input type="number" class="{{classes}}" id="bill-data-amount-{{bd.id}}" value="{{ bd.amount }}" disabled> </div>
{% if bd.estimated == 0 %}
<button id="bill-data-chg-{{bd.id}}" class="px-0 col-2 btn btn-success bg-success-subtle text-success" onClick="StartUpdateBill( {{bd.id}} )"><span class="bi bi-pencil-square"> Change</button>
<button id="bill-data-del-{{bd.id}}" class="px-0 col-2 btn btn-danger bg-danger-subtle text-danger" onClick="DeleteBill( {{bd.id }} )"><span class="bi bi-trash3"> Delete
<button id="bill-data-save-{{bd.id}}" class="px-0 col-2 btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBill( {{bd.id}} )"><span class="bi bi-floppy"> Save</button>
<button id="bill-data-canc-{{bd.id}}" class="px-0 col-2 btn btn-danger bg-danger-subtle text-danger d-none"
onClick="CancelUpdateBill({{bd.id}}, '{{bd.name}}', '{{bd.bill_date}}', '{{bd.amount}}')"> <span class="bi bi-x"> Cancel</button>
</button>
<button id="bill-data-chg-{{bd.id}}" class="px-0 col btn btn-success bg-success-subtle text-success" onClick="StartUpdateBill( {{bd.id}} )"><span class="bi bi-pencil-square"> Change</button>
<button id="bill-data-del-{{bd.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger" onClick="DeleteBill( {{bd.id }} )"><span class="bi bi-trash3"> Delete
<button id="bill-data-save-{{bd.id}}" class="px-0 col btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBill( {{bd.id}} )"><span class="bi bi-floppy"> Save</button>
<button id="bill-data-canc-{{bd.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger d-none"
onClick="CancelUpdateBill({{bd.id}}, '{{bd.name}}', '{{bd.bill_date}}', '{{bd.amount}}')"> <span class="bi bi-x"> Cancel</button>
</button>
{% else %}
<div class="px-0 col"></div>
<div class="px-0 col"></div>
{% endif %}
</div>
{% endif %}
@@ -403,10 +443,11 @@
data: JSON.stringify( { 'id': id } ), success: function() { window.location='bills' } } )
}
function UseGrowth( bt, which )
function UseGrowth( bt_id )
{
which = $('#'+bt_id+ '_growth option:selected').val()
$.ajax( { type: 'POST', url: '/usegrowth', contentType: 'application/json',
data: JSON.stringify( { 'bill_type': bt, 'which_growth': which } ), success: function() { window.location='bills' } } )
data: JSON.stringify( { 'bill_type': bt_id, 'which_growth': which } ), success: function() { window.location='bills' } } )
}
function SaveTab( last_tab )
@@ -472,7 +513,13 @@
$.ajax( { type: 'POST', url: '/force_recalc_bills', contentType: 'application/json', success: function() { window.location='bills' } } )
}
/*
$(document.ready() {
for( bt in future_ids ) {
$('#bill-data-date-'+future_ids[bt]).width( $('#bill-data-date-'+first_col_id[bt]).width() )
}
}
*/
</script>
</body>
</html>

66
templates/cset.html Normal file
View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<title>Finance Form (Comparison Sets)</title>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/annotations.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
<style>
.col-form-label { width:140px; }
html { font-size: 80%; }
</style>
</head>
<body>
<div class="pt-2 mx-2 container-fluid row">
<h3 align="center">Comparison Sets (go to <a href="/">Finance Tracker</a>)</h3>
<div class="row">
</div>
<table class="table table-sm table-dark table-striped">
<thead>
<tr>
<th class="text-center">Action</th>
{% for d in comp_data[finance['COMP_SETS'][1][0]]['vars'] %}
{% if d != 'id' %}
<th class="text-center">{{ d|replace('_', '<br>')|safe }}</th>
{% endif %}
{% endfor %}
</tr>
</thead>
{% for c in finance['COMP_SETS'] %}
{% if c[0] != 0 %}
<tr>
<td><button class="btn btn-danger bg-danger-subtle text-danger"
onClick="DelCSet({{c[0]}})"><span class="bi bi-trash3"> Delete</button></td>
{% for d in comp_data[c[0]]['vars'] %}
{% if d != 'id' %}
{% if d == 'name' %}
<td class="align-middle">{{comp_data[c[0]]['vars'][d]}}</td>
{% else %}
<td class="text-center align-middle">{{comp_data[c[0]]['vars'][d]}}</td>
{% endif %}
{% endif %}
{% endfor %}
</tr>
{% endif %}
{% endfor %}
</table>
<script>
function DelCSet( id )
{
// POST to a delete, success should just reload this page
$.ajax( { type: 'POST', url: '/delcset', contentType: 'application/json', data: JSON.stringify( { 'id': id } ), success: function() { window.location='cset' } } )
}
</script>
</body>
</html>

View File

@@ -4,6 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<title>Finance Form</title>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
@@ -19,7 +21,30 @@
</head>
<body>
<div class="container-fluid">
<h3 align="center">Finance Tracker (go to <a href="bills">Bills</a>)</h3>
<div class="d-flex align-items-center justify-content-center position-relative">
<h3 align="center">Finance Tracker (go to <a href="bills">Bills</a> or <a href="cset">Comparison Sets</a>)</h3>
<!-- Clickable Question Mark Icon -->
<a href="#" tabindex="0"
class="position-absolute end-0 me-3"
data-bs-toggle="popover"
data-bs-trigger="click"
data-bs-placement="right"
data-bs-content="For now manually update the itmes below on day aftter original pay shcedule to compare saved version vs. our reality:
<ul>
<li>Savings (<a href='https://online.macquarie.com.au/personal/#/login'>Macquarie</a>
+<a href='https://ib.mebank.com.au/authR5/ib/login.jsp'>ME bank</a>
+<a href='https://ib.nab.com.au/login'>NAB</a>) -- noting ME bank is: $1000</li>
<li><a href='https://www.google.com/search?q=asx+tls'>TLS</a>/<a href='https://www.google.com/search?q=asx+cba'>CBA</a> prices</li>
<li>Macq <a href='https://www.macquarie.com.au/everyday-banking/savings-account.html'>Interest rate</a></li>
<li><a href='https://deakinpeople.deakin.edu.au/psc/HCMP/EMPLOYEE/HRMS/c/NUI_FRAMEWORK.PT_AGSTARTPAGE_NUI.GBL?CONTEXTIDPARAMS=TEMPLATE_ID%3aPTPPNAVCOL&scname=ADMN_LEAVE&PTPPB_GROUPLET_ID=DU_LEAVE&CRefName=ADMN_NAVCOLL_3'>D_leave_owed_in_days</a> by: {{key_dates['D_quit_date']}}</li>
<li>update Inflation - using <a href='https://tradingeconomics.com/australia/core-inflation-rate'>RBA Trimmed Mean CPI YoY</a></li></li>
</ul>"
data-bs-html="true">
<i class="bi bi-question-circle" style="font-size: 2.0rem;"></i>
</a>
</div>
</div>
<form id="vals_form" class="ms-3 mt-3" action="/update" method="POST">
{% for r in DISP %}
@@ -105,7 +130,7 @@
</h5>
<div class="row">
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">2025</div>
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">{{savings[0][0][:4]}}</div>
{# inside started if below, we add blank lines to the start of the year so the dates line up #}
{% for _ in range( 0, padding ) %}
@@ -198,6 +223,7 @@
const savingsData = JSON.parse('{{ savings | tojson }}');
const vars = JSON.parse('{{ finance | tojson }}');
$(function() { $('[data-bs-toggle="popover"]').popover(); });
window.onload = function() {
$('#Sell_shares').val( {{finance['Sell_shares']}} )
$('#compare_to').val( {{finance['compare_to']}} )
@@ -389,4 +415,3 @@
</div>
</body>
</html>