Compare commits

...

19 Commits

Author SHA1 Message Date
3b33390c3e updated ME bank amount in help, but mainly fixed year start being hard-coded to 2025, now works for any year 2025-12-26 11:09:01 +11:00
f309dfa947 redid bills UI, you can now choose CPI or FLAT X% as well, via a drop-down not a set of buttons. The changing of inflation also tweaks any bills using CPI, all works 2025-12-23 18:35:52 +11:00
ce20c57d11 added health check, forgot to add template/cset <- allows deletion of comparison sets in earlier commit, and added a new TODO 2025-11-05 01:13:22 +11:00
d4662b9051 update TODO 2025-11-04 11:36:56 +11:00
0a9a50f9a1 added ability to delete comparison sets, also made future bills recalc for Hydunday/D_quit dependent future bills, this is not effectively functional -v1.0 :) 2025-11-04 11:36:36 +11:00
9cc907fb62 added a basic comparison set page, with a table of the data that changes only (just to fit it in) and allowing them to be deleted 2025-11-01 22:48:21 +11:00
4bb336645a have added quick re-estimate button 2025-10-29 22:57:50 +11:00
227e95cab7 add a button to recalc bills by removing all esimated bills and rebuild them 2025-10-17 21:22:07 +11:00
bf66e9fa7c redo D_quit logic (tax and when we can sell shares) 2025-10-17 21:20:22 +11:00
5ce614ed28 added note around tax and quitting 2025-10-17 21:19:50 +11:00
b6b396342f fix up annotation bug where we put daily amt, not bill amt in annot 2025-10-05 12:09:21 +11:00
fb2fffea7b noting need to tie bills/finance page items to recalc (change date of quit, need to redo bills future) 2025-10-05 12:08:33 +11:00
252dc23364 make payment annotations show as negatives, use this to make annotations of adding go above graph, and generally payment annotaions below the line. Then switch to have the last few annoations above the graph regardless as we run out of room at that end of the graph 2025-09-16 23:24:19 +10:00
a75db565ee Created tabbed interface for the front page, update the TODO to match 2025-09-16 22:51:23 +10:00
b1614760a6 more thoughts/how to make this more usable longer-term 2025-09-16 12:57:11 +10:00
670a63cfd7 updated TODO, removed old ones, added new around UI changes to have tabbed lower data/graphs 2025-09-16 12:54:16 +10:00
1c112e6f6b move future bills into bills.py, away from calc.py for file content consistency 2025-09-15 22:17:17 +10:00
8274da0ce0 fix up containerfluid to container-fluid, and add some margin for left/right on bills 2025-09-15 22:16:40 +10:00
6618dd16b4 use warning instead of info for consistency 2025-09-11 21:19:25 +10:00
10 changed files with 762 additions and 310 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 :( * 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 - something to do with missing year of data in quarterly bills - still an issue

23
TODO
View File

@@ -1,11 +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 calc - slides around with tax brackets in future
UI: UI:
* use key_dates to highlight when I quit, when we own car * 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: For bills:
[DONE] * calc quit date based on finance data
[DONE] * calc date of car lease end or buyout
* use this to populate bill estimates, so this allows totals / year and simplifies calc.py
* might need to be able to mark a specific bill as an outlier: * 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) - 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 - and even electricity, water, etc. for when we were away in Europe but mostly gas/elec

199
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 from defines import END_YEAR
import datetime import datetime
import re
from datetime import date, timedelta from datetime import date, timedelta
@@ -88,6 +90,18 @@ def find_next_bill( bill_type, bill_info, bill_date ):
return None return None
# see if this bill exists (used to prevent adding more than once in future
# estimated bills)
def find_this_bill( bill_type, bill_info, bill_date ):
yr = int(bill_date[:4])
if not bill_type in bill_info or not 'year' in bill_info[bill_type] or not yr in bill_info[bill_type]['year']:
return None
for b in bill_info[bill_type]['year'][yr]:
if bill_type == b['bill_type'] and bill_date == b['bill_date']:
return b
return None
# find the bill just before the date given # find the bill just before the date given
def find_previous_bill( bill_type, bill_info, bill_date ): def find_previous_bill( bill_type, bill_info, bill_date ):
wanted_year = int(bill_date[:4]) wanted_year = int(bill_date[:4])
@@ -139,6 +153,7 @@ def new_estimated_bill( bill_info, yr, bill_type, amt, new_date ):
bill={} bill={}
bill['bill_date']=new_date bill['bill_date']=new_date
bill['amount']=amt bill['amount']=amt
bill['bill_type']=bill_type
bill['estimated']=1 bill['estimated']=1
# need to insert(0,) to add this "newest" bill to start of the data for {yr} so that find_previous_bill can work - only need the above 3 fields # need to insert(0,) to add this "newest" bill to start of the data for {yr} so that find_previous_bill can work - only need the above 3 fields
bill_info[bill_type]['year'][yr].insert(0,bill) bill_info[bill_type]['year'][yr].insert(0,bill)
@@ -224,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 # 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 ): 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 # 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 # 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:] dd = bill_info[bill_type]['first_bill']['bill_date'][8:]
@@ -281,8 +298,18 @@ def get_growth_value( bt, bill_type ):
return el['ann_growth_min'] return el['ann_growth_min']
elif which == 'simple': elif which == 'simple':
return el['ann_growth_simple'] return el['ann_growth_simple']
else: elif which == 'max':
return el['ann_growth_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
################################################################################ ################################################################################
@@ -295,6 +322,7 @@ def process_bill_data(bd, bt, bf, key_dates):
# this maps a bill id to a freq id (e.g. bill #34 - has a frequency of #2 (which might be quarterly) # this maps a bill id to a freq id (e.g. bill #34 - has a frequency of #2 (which might be quarterly)
bt_id_freq = {row["id"]: row["freq"] for row in bt} bt_id_freq = {row["id"]: row["freq"] for row in bt}
bt_id_ann_growth_avg = {row["id"]: row["ann_growth_avg"] for row in bt} 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) # 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} bf_id_num = {row["id"]: row["num_bills_per_annum"] for row in bf}
@@ -304,11 +332,23 @@ def process_bill_data(bd, bt, bf, key_dates):
# want to proces all bill data into easier to maniuplate structure, so make # want to proces all bill data into easier to maniuplate structure, so make
# a bill_info[bill_id] with first_bill, last_bill, [yr] with matching bills to process # a bill_info[bill_id] with first_bill, last_bill, [yr] with matching bills to process
bill_info={} bill_info={}
future_car_bills=[]
future_D_quit_bills=[]
for bill in bd: for bill in bd:
bill_type = bill['bill_type'] bill_type = bill['bill_type']
if bill['bill_date'] == 'future': if bill['bill_date'] == 'future':
print("Having a future data - skip this one") # 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 )
bill_info[bill_type]={}
bill_info[bill_type]['future'] = 1
bill_info[bill_type]['freq'] = bf_id_name[bt_id_freq[bill_type]]
bill_info[bill_type]['growth'] = get_growth_value( bt, bill_type )
bill_info[bill_type]['num_ann_bills'] = bf_id_num[bt_id_freq[bill_type]]
bill_info[bill_type]['year']={}
continue continue
yr= int(bill['bill_date'][:4]) yr= int(bill['bill_date'][:4])
@@ -339,6 +379,8 @@ def process_bill_data(bd, bt, bf, key_dates):
# now process the bill_info from yr of first bill to yr of last bill # now process the bill_info from yr of first bill to yr of last bill
for bill_type in bill_info: for bill_type in bill_info:
if 'future' in bill_info[bill_type]:
continue
# find freq id based on bill_type id, then use that to find num bills by freq id # find freq id based on bill_type id, then use that to find num bills by freq id
num = bf_id_num[bt_id_freq[bill_type]] num = bf_id_num[bt_id_freq[bill_type]]
@@ -357,9 +399,67 @@ 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: 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 continue
add_missing_bills_for_yr( bill_type, bill_info, yr ) add_missing_bills_for_yr( bill_type, bill_info, yr )
derive_ann_growth( bill_type, bill_info ) 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 )
return bill_info return bill_info
################################################################################
# deal_with_future_car_bills - just add these estimate bills based on when we
# own the car (data can change if I buy it out)
################################################################################
def deal_with_future_car_bills( key_dates, future_car_bills, bill_info ):
car_yr=key_dates['D_hyundai_owned'][0:4]
car_mmdd=key_dates['D_hyundai_owned'][5:]
for fb in future_car_bills:
# deal with future bills due to their starting dates being dynamic
amt=fb['amount']
bt=fb['bill_type']
# factor in growth for next bill
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']:
new_estimated_bill( bill_info, yr, fb['bill_type'], amt, new_date )
amt += amt * bill_info[bt]['growth']/100
################################################################################
# deal_with_future_D_quit_bills - just add these estimate bills based on when I
# quit
################################################################################
def deal_with_future_D_quit_bills( key_dates, future_D_quit_bills, bill_info ):
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:]
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']
if bill_info[bt]['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
if not find_this_bill( bt, bill_info, 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:
# 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 ):
new_estimated_bill( bill_info, yr, bt, amt, new_date )
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 ):
new_estimated_bill( bill_info, yr, bt, amt, new_date )
################################################################################ ################################################################################
# add_missing_bills_for_yr -- wrapper to call right func based on bill freq # add_missing_bills_for_yr -- wrapper to call right func based on bill freq
################################################################################ ################################################################################
@@ -400,7 +500,7 @@ def ProportionQtrlyData( bill_type, bill_info ):
# terms of min/avg/max - uses qtr data for qtrly bills, or just normal totals # terms of min/avg/max - uses qtr data for qtrly bills, or just normal totals
# for other bill types # for other bill types
################################################################################ ################################################################################
def derive_ann_growth( bill_type, bill_info ): 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) # just do up to now so we stop earlier than looking at other estimated (just an optimisation)
now_yr = datetime.date.today().year now_yr = datetime.date.today().year
@@ -433,7 +533,6 @@ def derive_ann_growth( bill_type, bill_info ):
# use new derived qtr, slightly more accurate # use new derived qtr, slightly more accurate
total[yr]=tot total[yr]=tot
# once we have all yr totals: # once we have all yr totals:
growth = {} growth = {}
min_growth = 999 min_growth = 999
@@ -496,3 +595,89 @@ def calc_future_totals(bill_info, bill_types):
total[bt['id']][yr] = round( total[bt['id']][yr], 2 ) total[bt['id']][yr] = round( total[bt['id']][yr], 2 )
return total 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

130
calc.py
View File

@@ -6,13 +6,11 @@ from defines import END_YEAR
LEASE = 0 LEASE = 0
# Dates that don't change # Dates that don't change
car_balloon_date = datetime(2026, 11, 15)
new_fin_year_25 = datetime(2025, 7, 1)
new_fin_year_26 = datetime(2026, 7, 1)
end_date = datetime(END_YEAR, 4, 15)
school_fees_date = datetime(2025, 12, 5)
mich_present_date = datetime(2026,10,15)
first_pay_date = datetime(2025,1,8) first_pay_date = datetime(2025,1,8)
school_fees_date = datetime(2025, 12, 5)
car_balloon_date = datetime(2026, 11, 15)
mich_present_date = datetime(2026,10,15)
end_date = datetime(END_YEAR, 4, 15)
def bill_amount_today(finance, day, bill_data, bt_id_name, total ): def bill_amount_today(finance, day, bill_data, bt_id_name, total ):
amt=0 amt=0
@@ -24,8 +22,8 @@ def bill_amount_today(finance, day, bill_data, bt_id_name, total ):
amt += b['amount'] amt += b['amount']
if b['amount'] > 1000: if b['amount'] > 1000:
n=bt_id_name[ b['bill_type'] ] n=bt_id_name[ b['bill_type'] ]
print( f"bill_amt_today {n} for {day_str} has amt={amt}" ) print( f"bill_amt_today {n} for {day_str} has amt={b['amount']}" )
add_annotation(finance, day, total-amt, amt, f"Pay {n}" ) add_annotation(finance, day, total-b['amount'], -b['amount'], f"Pay {n}" )
# bills are desc order so if the bill is before the day we are after then stop looking # bills are desc order so if the bill is before the day we are after then stop looking
if b['bill_date'] < day_str: if b['bill_date'] < day_str:
return amt return amt
@@ -84,22 +82,18 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
payout = 83115.84 payout = 83115.84
print( f"leave payout gross={payout}" ) print( f"leave payout gross={payout}" )
# as the leave is just on top of my existing earnings and if in 2024 fin year, just take tax at 37% for the extra leave amount
# hardcoded 6 represents the 12 weeks or 6 fornights of pay owed to me when I give notice or they sack me
D_leave_after_tax = payout * (1-0.37)
# However, if I quit in the next fin year - tax for 2025 will be: $4,288 plus 30c for each $1 over $45,000 # However, if I quit in the next fin year - tax for 2025 will be: $4,288 plus 30c for each $1 over $45,000
# (assuming the 7830.42 * ~90/bus_days_in_fortnight = ~ $64k - > 45k and < $135k bracket is 30%) # (assuming the 7830.42 * ~90/bus_days_in_fortnight = ~ $64k - > 45k and < $135k bracket is 30%)
# Given, I probably can't stop Deakin doing PAYG deductions, I won't get # Given, I probably can't stop Deakin doing PAYG deductions, I won't get
# the tax back until the end of the financial year, so work out the # the tax back until the end of the financial year, so work out the
# amount of tax I will get back info: tax_diff_D_leave # amount of tax I will get back info: tax_diff_D_leave
tax_on_leave = (payout - 45000)*.37 + 4288 tax_on_leave = (payout - 45000)*.37 + 4288
D_leave_after_tax_new_fin_year = payout - tax_on_leave D_leave_after_tax = payout - tax_on_leave
# just use redunancy calc... # just use redunancy calc...
D_leave_after_tax_new_fin_year = 56518.77 D_leave_after_tax = 56518.77
tax_diff_D_leave = payout - D_leave_after_tax_new_fin_year tax_diff_D_leave = payout - D_leave_after_tax
print( f"tax_diff_D_leave: {tax_diff_D_leave}") print( f"tax_diff_D_leave: {tax_diff_D_leave}")
@@ -107,7 +101,6 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
# convenience vars to make it easier to read conditional leave tax/payment logic below # convenience vars to make it easier to read conditional leave tax/payment logic below
D_has_quit = False D_has_quit = False
D_quit_year = 0
claim_tax_on_leave = False claim_tax_on_leave = False
# Constants for interest calculations # Constants for interest calculations
@@ -117,40 +110,22 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
# main loop range -- start from now, and simulate till D is 60 (April 2031) # main loop range -- start from now, and simulate till D is 60 (April 2031)
current_date = datetime.today() current_date = datetime.today()
# work out which bill_types relate to future bills
for bt in bill_type:
if 'Hyundai' in bt['name']:
if 'Car Ins' in bt['name']:
ioniq6_ins_bt = bt
if 'Car Rego' in bt['name']:
ioniq6_rego_bt = bt
if 'Health Ins' in bt['name']:
health_ins_bt = bt
if 'Phone' in bt['name'] and 'Damien' in bt['name']:
phone_d_bt = bt
# TODO: need to refactor Living_Expenses to exclude bills # refactor Living_Expenses to exclude bills (as we have detailed future projections for them that usually exceed inflation)
total=0 total=0
yr=str(current_date.year) yr=str(current_date.year)
for b in bill_data: for b in bill_data:
if b['bill_type'] == ioniq6_rego_bt['id']:
ioniq6_rego = b['amount']
if b['bill_type'] == ioniq6_ins_bt['id']:
ioniq6_ins = b['amount']
if b['bill_type'] == health_ins_bt['id']:
health_ins = b['amount']
if b['bill_type'] == phone_d_bt['id']:
phone_d = b['amount']
if yr in b['bill_date']: if yr in b['bill_date']:
total += b['amount'] total += b['amount']
print( f"this yr={current_date.year} - total={total} -- hi={health_ins}, phone_d={phone_d}" ) print( f"this yr={current_date.year} - total={total}" )
Living_Expenses -= total Living_Expenses -= total
print( f"LE is now={Living_Expenses}" ) print( f"LE is now={Living_Expenses}" )
# Calculate daily living expenses # Calculate daily living expenses
daily_living_expenses = Living_Expenses / 365 daily_living_expenses = Living_Expenses / 365
print( f"daily LE starts at={daily_living_expenses}" )
print( f"fortnightly LE starts at={daily_living_expenses*14}" )
# Start the calculation # Start the calculation
current_savings = Savings current_savings = Savings
@@ -187,19 +162,6 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
# if we have a bill for today, pay for it # if we have a bill for today, pay for it
current_savings -= bill_amount_today( finance, current_date, bill_data, bt_id_name, current_savings ) current_savings -= bill_amount_today( finance, current_date, bill_data, bt_id_name, current_savings )
# KLUDGE for future bills for now:
# if I have quit, pay monthly future bills - assume 1st day of month for health ins
if D_has_quit and current_date.day == 1:
health_ins = health_ins * 1+((health_ins_bt['ann_growth_simple'])/100)*12
current_savings -= health_ins
# once a year from when I quit, pay the phone bill (year I quit comes below)
if D_has_quit and current_date.month == D_quit_date.month and current_date.day == D_quit_date.day:
amt=phone_d
for y in range( D_quit_date.year, current_date.year):
amt = amt * (1+phone_d_bt['ann_growth_simple']/100)
current_savings -= amt
# Calculate daily interest but apply at the end of the month # Calculate daily interest but apply at the end of the month
monthly_interest += current_savings * daily_interest_rate monthly_interest += current_savings * daily_interest_rate
@@ -214,32 +176,16 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
current_savings -= Car_loan_via_pay current_savings -= Car_loan_via_pay
print( f"{current_date}: making car loan pay as pre-tax lease: ${Car_loan_via_pay}" ) print( f"{current_date}: making car loan pay as pre-tax lease: ${Car_loan_via_pay}" )
# no more pay and if leave after tax > 0 this is the day I quit
if D_Num_fortnights_pay == 0 and D_leave_after_tax > 0: if D_Num_fortnights_pay == 0 and D_leave_after_tax > 0:
D_has_quit = True D_has_quit = True
D_quit_date = current_date D_quit_date = current_date
D_quit_year = current_date.year # going to pay tax on payout, so claim it back next year
# okay, if we leave before Jun 30th 2024, then I pay full tax, otherwise I get 'extra', but have to await end of next fin year claim_tax_on_leave = True
if current_date > new_fin_year_25: print(f"{current_date}: D has resigned in new year- get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax}" )
claim_tax_on_leave = True current_savings += D_leave_after_tax
print(f"{current_date}: D has resigned in new year- get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax_new_fin_year}" ) add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
current_savings += D_leave_after_tax_new_fin_year
add_annotation(finance, current_date, current_savings, D_leave_after_tax_new_fin_year, "D quit" )
else:
claim_tax_on_leave = False
print(f"{current_date}: D has resigned - get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax}" )
current_savings += D_leave_after_tax
add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
D_leave_after_tax = 0 D_leave_after_tax = 0
# pay for 1st year of phone for Damien
current_savings -= phone_d
# its end of 'next' fin year, if tax_diff > 0, then ddp quit after new tax year and gets back the overpaid tax
if current_date > new_fin_year_26 and claim_tax_on_leave:
current_savings += tax_diff_D_leave
print( f"I quit last fin year, so now its 1st July {current_date.year}, get tax back of {tax_diff_D_leave}" )
add_annotation(finance, current_date, current_savings, tax_diff_D_leave, "D quit - tax back" )
# can only claim the tax back once :)
claim_tax_on_leave=False
if fortnight_income: if fortnight_income:
print(f"{current_date}: salary paid by Deakin - adding: {fortnight_income}" ) print(f"{current_date}: salary paid by Deakin - adding: {fortnight_income}" )
@@ -248,6 +194,14 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
savings_per_fortnight.append((current_date.strftime("%Y-%m-%d"), round(current_savings, 2))) savings_per_fortnight.append((current_date.strftime("%Y-%m-%d"), round(current_savings, 2)))
# its end of fin year, if claim_tax_on_leave > 0 then get tax back
if current_date.month == 7 and current_date.day == 1 and claim_tax_on_leave:
current_savings += tax_diff_D_leave
print( f"I quit last fin year, so now its 1st July {current_date.year}, get tax back of {tax_diff_D_leave}" )
add_annotation(finance, current_date, current_savings, tax_diff_D_leave, "D quit - tax back" )
# can only claim the tax back once :)
claim_tax_on_leave=False
# if I have quit, then car lease payments are made on the 15th of the month for full Car_loan # if I have quit, then car lease payments are made on the 15th of the month for full Car_loan
if D_has_quit and current_date.day == 15: if D_has_quit and current_date.day == 15:
if Ioniq6_future == LEASE and current_date <= car_balloon_date: if Ioniq6_future == LEASE and current_date <= car_balloon_date:
@@ -265,7 +219,7 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
# monthly increase living expenses by a monthly inflation multiplier # monthly increase living expenses by a monthly inflation multiplier
Living_Expenses += (Inflation/100.0)/12 * Living_Expenses Living_Expenses += (Inflation/100.0)/12 * Living_Expenses
daily_living_expenses = Living_Expenses / 365 daily_living_expenses = Living_Expenses / 365
#print(f"{current_date}: Living Exp inceased - ${Living_Expenses}") # print(f"{current_date}: Living Exp inceased - ${Living_Expenses}")
if current_date.date() == school_fees_date.date(): if current_date.date() == school_fees_date.date():
current_savings -= School_Fees current_savings -= School_Fees
@@ -281,30 +235,6 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
add_annotation(finance, current_date, current_savings, -Car_buyout, "car buyout") add_annotation(finance, current_date, current_savings, -Car_buyout, "car buyout")
print(f"{current_date}: car buyout - ${Car_buyout}" ) print(f"{current_date}: car buyout - ${Car_buyout}" )
# Anniversary of Car purchase/balloon so potentially insurance/rego
# when I quit, the if we haven't paid the car outright, then need to add rego, but not insurance
# if we pay-out the car, then add insurace and rego
if current_date.month == car_balloon_date.month and current_date.day == car_balloon_date.day:
# staying with the lease (0), if I have quit, then pay monthly rego only up to lease date, but full cost after car balloon date
if Ioniq6_future == LEASE:
if current_date.year >= car_balloon_date.year:
ins_amt=ioniq6_ins
for y in range( car_balloon_date.year, current_date.year):
ins_amt = ins_amt * (1+ioniq6_ins_bt['ann_growth_simple']/100)
rego_amt=ioniq6_rego
for y in range( car_balloon_date.year, current_date.year):
rego_amt = rego_amt * (1+ioniq6_rego_bt['ann_growth_simple']/100)
current_savings -= (ins_amt+rego_amt)
add_annotation(finance, current_date, current_savings, -(ins_amt+rego_amt), "IONIQ 6 ins/rego" )
# if we buy car outright, then as long as this anniversary is after buyout date, pay ins and rego
elif current_date.year >= car_buyout_date.year:
current_savings -= (ioniq6_ins + ioniq6_rego)
add_annotation(finance, current_date, current_savings, -(ioniq6_ins+ioniq6_rego), "IONIQ 6 ins/rego" )
# if current_date.date() == overseas_trip_date.date():
# current_savings -= Overseas_trip
# add_annotation(finance, current_date, current_savings, -Overseas_trip, "O/S trip")
if current_date.date() == mich_present_date.date(): if current_date.date() == mich_present_date.date():
current_savings -= Mich_present current_savings -= Mich_present
add_annotation(finance, current_date, current_savings, -Mich_present, "Mich's present" ) add_annotation(finance, current_date, current_savings, -Mich_present, "Mich's present" )
@@ -328,7 +258,7 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
# if selling shares, and its 1st of July... # if selling shares, and its 1st of July...
# BUT not if D quits before end of financial year - as I won't be able to sell CBA shares for no cap gains # BUT not if D quits before end of financial year - as I won't be able to sell CBA shares for no cap gains
# so wait until the following year # so wait until the following year
if current_date.month == 7 and current_date.day == 1 and D_has_quit and Sell_shares>0 and (current_date.year > D_quit_year or current_date.year == D_quit_year and claim_tax_on_leave == False): if current_date.month == 7 and current_date.day == 1 and D_has_quit and Sell_shares>0 and (D_quit_date.month<7 or D_quit_date.year < current_date.year ):
# 2024 Govt. value # 2024 Govt. value
tax_threshold = 18200 tax_threshold = 18200
# cap-gains is 50% of profit (lazy profit calc here, just assume its all profit) # cap-gains is 50% of profit (lazy profit calc here, just assume its all profit)
@@ -358,7 +288,6 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
finance['CBA']=D_CBA_shares finance['CBA']=D_CBA_shares
finance['TLS']=D_TLS_shares+M_TLS_shares finance['TLS']=D_TLS_shares+M_TLS_shares
return depletion_date, savings_per_fortnight, current_savings return depletion_date, savings_per_fortnight, current_savings
################################################################################ ################################################################################
@@ -381,6 +310,5 @@ def calc_key_dates( finance ):
else: else:
key_dates['D_hyundai_owned'] = finance['Car_buyout_date'] key_dates['D_hyundai_owned'] = finance['Car_buyout_date']
print( f"kd={key_dates}" )
return key_dates return key_dates

101
db.py
View File

@@ -84,6 +84,12 @@ def init_db():
FOREIGN KEY(comparison_set_id) REFERENCES comparison_set(id) 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 ( cur.execute('''CREATE TABLE IF NOT EXISTS bill_type (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
freq INTEGER, freq INTEGER,
@@ -95,12 +101,6 @@ def init_db():
FOREIGN KEY(freq) REFERENCES bill_freq(id) 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 ( cur.execute('''CREATE TABLE IF NOT EXISTS bill_data (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
bill_type INTEGER, bill_type INTEGER,
@@ -116,28 +116,56 @@ def init_db():
show_estimated INTEGER 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') cur.execute('SELECT COUNT(*) FROM finance')
if cur.fetchone()[0] == 0: 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, 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) 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', 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)) (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 ( 1, 'Annual', 1 )" )
cur.execute( "INSERT INTO bill_freq values ( 2, 'Quarterly', 4 )" ) 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 ( 3, 'Quarterly (forced)', 4 )" )
cur.execute( "INSERT INTO bill_freq values ( 4, 'Monthly', 12 )" ) cur.execute( "INSERT INTO bill_freq values ( 4, 'Monthly', 12 )" )
# start with no specific Tab/bill_type to show, and dont show_estimated # 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 )" ) 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.commit()
conn.close() conn.close()
@@ -376,6 +404,14 @@ def set_bill_type_growth( id, min_g, avg_g, max_g, simple_g ):
conn.close() conn.close()
return 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(): def get_bill_ui():
conn = connect_db(True) conn = connect_db(True)
cur = conn.cursor() cur = conn.cursor()
@@ -395,3 +431,36 @@ def save_ui(data):
conn.commit() conn.commit()
conn.close() conn.close()
return 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()
cur.execute( "delete from bill_data where estimated=1" )
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

70
main.py
View File

@@ -1,16 +1,19 @@
# main.py # main.py
from flask import Flask, render_template, request, redirect, url_for, Response, jsonify from flask import Flask, render_template, request, redirect, url_for, Response, jsonify
from calc import calculate_savings_depletion, calc_key_dates 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 init_db, get_finance_data, update_finance, get_budget_data
from db import get_bill_data, new_bill, update_bill_data, delete_bill 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_ui, save_ui
from db import get_bill_types, insert_bill_type, update_bill_type, delete_bill_type, use_growth 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 defines import END_YEAR
from collections import defaultdict, Counter from collections import defaultdict, Counter
from datetime import datetime, date from datetime import datetime, date
import csv import csv
import io import io
import requests
from disp import FP_VAR from disp import FP_VAR
app = Flask(__name__) app = Flask(__name__)
@@ -113,6 +116,8 @@ def save():
@app.route('/update', methods=['POST']) @app.route('/update', methods=['POST'])
def update(): def update():
old_finance_data = get_finance_data()
finance_data = ( finance_data = (
request.form['D_Salary'], request.form['D_Salary'],
request.form['D_Num_fortnights_pay'], request.form['D_Num_fortnights_pay'],
@@ -142,6 +147,26 @@ def update():
request.form['Ioniq6_future'] request.form['Ioniq6_future']
) )
update_finance(finance_data) update_finance(finance_data)
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')) return redirect(url_for('index'))
@app.route('/bills') @app.route('/bills')
@@ -153,13 +178,14 @@ def DisplayBillData():
bill_types = get_bill_types() bill_types = get_bill_types()
bill_freqs = get_bill_freqs() bill_freqs = get_bill_freqs()
bill_ui = get_bill_ui() 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, # 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) 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) # get an array of the total costs of bills each year - purely cosmetic (using bill_info)
total=calc_future_totals(bill_info, bill_types) total=calc_future_totals(bill_info, bill_types)
# update/re-get bill_data now that new estimated bills have been added # update/re-get bill_data now that new estimated bills have been added
bill_data = get_bill_data("order_by_bill_type_then_date") 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']) @app.route('/newbilltype', methods=['POST'])
def InsertBillType(): def InsertBillType():
@@ -214,6 +240,42 @@ def SaveUI():
save_ui( data ) save_ui( data )
return "200" return "200"
@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 # Main program
if __name__ == '__main__': if __name__ == '__main__':

View File

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

View File

@@ -18,7 +18,7 @@
</style> </style>
</head> </head>
<body> <body>
<div class="pt-2 containerfluid row"> <div class="pt-2 mx-2 container-fluid row">
<h3 align="center">Bill Details (go to <a href="/">Finance Tracker</a>)</h3> <h3 align="center">Bill Details (go to <a href="/">Finance Tracker</a>)</h3>
{# DEBUG totals if needed {# DEBUG totals if needed
@@ -36,7 +36,7 @@
{% endfor %} {% endfor %}
</table> </table>
#} #}
<div class="mt-4 col-7"> <div class="col-8">
<div class="row"> <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">Bill Type</div>
<div class="col-2 form-control-inline d-none new-bill-type-class">Frequency</div> <div class="col-2 form-control-inline d-none new-bill-type-class">Frequency</div>
@@ -52,68 +52,95 @@
</div> </div>
<button id="save-bill-type" class="new-bill-type-class px-0 col-1 btn btn-success bg-success-subtle text-success d-none" onClick="NewBillType()"><span class="bi bi-floppy"></span> Save</button> <button id="save-bill-type" class="new-bill-type-class px-0 col-1 btn btn-success bg-success-subtle text-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 bg-danger-subtle text-danger d-none" onClick="CancelNewBillType()"><span class="bi bi-x"> Cancel</span></button> <button id="canc-bill-type" class="new-bill-type-class px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelNewBillType()"><span class="bi bi-x"> Cancel</span></button>
<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>
<div class="row"> <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"><label class="form-control d-flex
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Frequency</ ></div> align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></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"><label class="form-control d-flex
<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> align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Frequency</ ></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">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> </div>
{% for bt in bill_types %} {% for bt in bill_types %}
<div class="row"> <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 --> <!-- 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> <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 %} {% for bf in bill_freqs %}
<option value={{bf.id}}>{{bf.name}}</option> <option value={{bf.id}}>{{bf.name}}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<script>$('#bill-type-freq-{{bt.id}}').val( {{bt.freq}} );</script> <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"> <div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="min-{{bt.id}}" autocomplete="off" <select id="{{bt.id}}_growth" class="form-select col" onChange="UseGrowth({{bt.id}})">
onChange="UseGrowth({{bt.id}}, 'min')" {% if bt.which_growth == 'min' %}checked{% endif %}> {% for gt in growth %}
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="min-{{bt.id}}" style="width: 6ch;"> {% if gt.name == 'Min' %}
{% if bt.ann_growth_min> 0 and bt.ann_growth_min < 10 %} &nbsp; {% endif %} <option value='min'
{{'%.2f'|format(bt.ann_growth_min)}} {% if bt.which_growth == 'min' %}selected{% endif %}
</label> >{{'%.2f'|format(bt.ann_growth_min)}}% {{gt.name}}</option>
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="avg-{{bt.id}}" autocomplete="off" {% elif gt.name == 'Avg' %}
onChange="UseGrowth({{bt.id}}, 'avg')" {% if bt.which_growth == 'avg' %}checked{% endif %}> <option value='avg'
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="avg-{{bt.id}}" style="width: 6ch;"> {% if bt.which_growth == 'avg' %}selected{% endif %}
{% if bt.ann_growth_avg < 10 %} &nbsp; {% endif %} >{{'%.2f'|format(bt.ann_growth_avg)}}% {{gt.name}}</option>
{{'%.2f'|format(bt.ann_growth_avg)}} {% elif gt.name == 'Max' %}
</label> <option value='max'
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="max-{{bt.id}}" autocomplete="off" {% if bt.which_growth == 'max' %}selected{% endif %}
onChange="UseGrowth({{bt.id}}, 'max')" {% if bt.which_growth == 'max' %}checked{% endif %}> >{{'%.2f'|format(bt.ann_growth_max)}}% {{gt.name}}</option>
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="max-{{bt.id}}" style="width: 6ch;"> {% elif gt.name == 'Simple' %}
{% if bt.ann_growth_max < 10 %} &nbsp; {% endif %} <option value='simple'
{{'%.2f'|format(bt.ann_growth_max)}} {% if bt.which_growth == 'simple' %}selected{% endif %}
</label> >{{'%.2f'|format(bt.ann_growth_simple)}}% {{gt.name}}</option>
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="simple-{{bt.id}}" autocomplete="off" {% elif gt.name == 'CPI' %}
onChange="UseGrowth({{bt.id}}, 'simple')" {% if bt.which_growth == 'simple' %}checked{% endif %}> <option value='cpi'
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="simple-{{bt.id}}" style="width: 6ch;"> {% if bt.which_growth == 'cpi' %}selected{% endif %}
{% if bt.ann_growth_simple < 10 %} &nbsp; {% endif %} >{{'%.2f'|format(cpi)}}% {{gt.name}}</option>
{{'%.2f'|format(bt.ann_growth_simple)}} {% else %}
</label> <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> </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> {% for yr in range( 2025, 2032 ) %}
<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> <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>
<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> {% endfor %}
<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-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-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> <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> </div>
{% endfor %} {% 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 ) %} {% set tot=namespace( sum=0 ) %}
{% for bt in bill_types %} {% for bt in bill_types %}
{% if bt.id in total %} {% if bt.id in total %}
{% set tot.sum = tot.sum + total[bt.id][yr] %} {% set tot.sum = tot.sum + total[bt.id][yr] %}
{% endif %} {% endif %}
{% endfor %} {% 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" %} {% set markup="h5" %}
{% if yr == this_year %} {% if yr == this_year %}
{% set markup="h4 pt-4" %} {% set markup="h4 pt-4" %}
@@ -126,12 +153,16 @@
${{'%.2f'|format(tot.sum)}} ${{'%.2f'|format(tot.sum)}}
</div> </div>
</div> </div>
#}
{% endfor %} {% endfor %}
<div class="px-0 col"></div>
<div class="px-0 col"></div>
</div>
</div> </div>
<!-- right-hand-side, bill types (e.g. gas, phone, etc.) --> <!-- right-hand-side, bill types (e.g. gas, phone, etc.) -->
<div class="pt-4 col-5"> <div class="col-4">
<div class="row"> <div class="row">
<div class="col-2 form-control-inline d-none new-bill-data-class">Bill Type</div> <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> <div id="new-bill-data-date-label" class="col-4 form-control-inline d-none new-bill-data-class">Date</div>
@@ -185,8 +216,9 @@
<div class="row pt-2"> <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">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">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="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-4"><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">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> </div>
{% endif %} {% endif %}
{% if bd.bill_type == bt.id %} {% if bd.bill_type == bt.id %}
@@ -202,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> <div class="px-0 col-2"> <input type="text" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
{% else %} {% else %}
<div class="px-0 col-2"> <input type="date" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div> <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 %} {% 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 %} {% 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-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-2 btn btn-danger bg-danger-subtle text-danger" onClick="DeleteBill( {{bd.id }} )"><span class="bi bi-trash3"> Delete <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-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-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-2 btn btn-danger bg-danger-subtle text-danger d-none" <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> onClick="CancelUpdateBill({{bd.id}}, '{{bd.name}}', '{{bd.bill_date}}', '{{bd.amount}}')"> <span class="bi bi-x"> Cancel</button>
</button> </button>
{% else %}
<div class="px-0 col"></div>
<div class="px-0 col"></div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
@@ -402,10 +443,11 @@
data: JSON.stringify( { 'id': id } ), success: function() { window.location='bills' } } ) 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', $.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 ) function SaveTab( last_tab )
@@ -466,7 +508,18 @@
}); });
}); });
function ForceRecalcBills()
{
$.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> </script>
</body> </body>
</html> </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 charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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 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> <title>Finance Form</title>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
@@ -18,8 +20,31 @@
</style> </style>
</head> </head>
<body> <body>
<div class="containerfluid"> <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"> <form id="vals_form" class="ms-3 mt-3" action="/update" method="POST">
{% for r in DISP %} {% for r in DISP %}
@@ -78,105 +103,127 @@
</div> </div>
{% endfor %} {% endfor %}
<h5 align="center" class="mt-4">Fortnighthly Savings data: <!-- create tabbed view for each bill type -->
{% if COMP %} <nav id="ft-nav" class="nav nav-tabs pt-3">
{# get comparison date so we can use it below in loop to know when to print it out #} <button class="nav-link" id="tab-but-findata" data-bs-toggle="tab" data-bs-target="#tab-findata" type="button" role="tab" aria-controls="tab1" aria-selected="true" >Fortnighthly Savings data</button>
{% set comp_yr=COMP['date'][:4] %} <button class="nav-link" id="tab-but-graph" data-bs-toggle="tab" data-bs-target="#tab-graph" type="button" role="tab" aria-controls="tab2" aria-selected="true" >Graph of savings over time</button>
{% set comp_mon=COMP['date'][5:7] %} {% if COMP %}
{% set comp_day=COMP['date'][8:10 ] %} <button class="nav-link" id="tab-but-compgraph" data-bs-toggle="tab" data-bs-target="#tab-compgraph" type="button" role="tab" aria-controls="tab3" aria-selected="true" >Comparison graph</button>
{% set comp_done=namespace( val=0 ) %}
{% else %}
{# we dont need to do a comparison, so consider it done before we begin #}
{% set comp_done=namespace( val=1 ) %}
{% endif %}
</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>
{# inside started if below, we add blank lines to the start of the year so the dates line up #}
{% for _ in range( 0, padding ) %}
<br>
{% endfor %}
{% set car_done=namespace( val=0 ) %}
{% for date, dollars in savings %}
{% set yr=date[:4] %}
{% set mon=date[5:7] %}
{% set day=date[8:10 ] %}
{% set car_yr=key_dates['D_hyundai_owned'][:4] %}
{% set car_mon=key_dates['D_hyundai_owned'][5:7] %}
{% set car_day=key_dates['D_hyundai_owned'][8:10 ] %}
{% if yr|int > first_yr|int and mon == '01' and day|int <= 14 %}
</div><div class="col-auto">
<div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">{{yr}}</div>
{% endif %}
{% if date == key_dates['D_quit_date'] %}
<font class="text-info">
<label data-bs-toggle="tooltip" title="D quits">
{% elif (yr == car_yr and mon == car_mon and day >= car_day and car_done.val == 0) %}
{%set car_done.val=1 %}
<font class="text-info">
<label data-bs-toggle="tooltip" title="We own car">
{% else %}
<font class="text-secondary">
<label>
{% endif %}
{{ date }}:&nbsp;
{{ '$%0.2f' % dollars|float }}<br>
</label>
</font><br>
{% if comp_done.val == 0 and yr == comp_yr and mon == comp_mon and day|int >= comp_day|int %}
<font class="text-info">
{{ COMP['date'] }}:&nbsp;
{{ '$%0.2f' % COMP['amount']|float }}<br>
</font>
{% set comp_done.val=1 %}
{% endif %}
{% endfor %}
{% if depletion_date %}
<div class="alert alert-danger">Run out of $'s:<br>{{depletion_date}}</div>
{% else %} {% else %}
<div class="alert alert-success">Super kicks in!!!</div> <button class="nav-link" id="tab-but-compgraph" data-bs-toggle="tab" data-bs-target="#tab-compgraph" type="button" role="tab" aria-controls="tab3" aria-selected="true" disabled>Comparison graph</button>
{% endif %} {% endif %}
</div> </nav>
<div class="col-auto">
<div class="alert alert-warning"> <div class="tab-content">
<h6 class="alert-heading">SUMMARY/BUDGET</h6> <div id="tab-findata" class="tab-pane">
{% for label, value in BUDGET %} <h5 align="center" class="mt-4">Fortnighthly Savings data:
<div> {% if COMP %}
{{label}} {{value}} {# get comparison date so we can use it below in loop to know when to print it out #}
</div> {% set comp_yr=COMP['date'][:4] %}
{% set comp_mon=COMP['date'][5:7] %}
{% set comp_day=COMP['date'][8:10 ] %}
{% set comp_done=namespace( val=0 ) %}
{% else %}
{# we dont need to do a comparison, so consider it done before we begin #}
{% set comp_done=namespace( val=1 ) %}
{% endif %}
</h5>
<div class="row">
<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 ) %}
<br>
{% endfor %} {% endfor %}
</div>
<div id="comp_col" class="col-auto"> {% set car_done=namespace( val=0 ) %}
<div class="input-group">
<button type="button" class="btn btn-info me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save</button> {% for date, dollars in savings %}
<button type="submit" class="disabled btn btn-info rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button> {% set yr=date[:4] %}
<select class="form-select border border-info text-info" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()"> {% set mon=date[5:7] %}
{% for el in finance['COMP_SETS'] %} {% set day=date[8:10 ] %}
<option value="{{el[0]}}">{{el[1]}}</option>
{% endfor %} {% set car_yr=key_dates['D_hyundai_owned'][:4] %}
</select> {% set car_mon=key_dates['D_hyundai_owned'][5:7] %}
</div> {% set car_day=key_dates['D_hyundai_owned'][8:10 ] %}
{% if COMP %}
<div style="display:none" id="comp_alert" class="alert alert-info mt-2">Note: {{ '$%0.2f' % COMP['amount']|float }} is the final value of the compared to data (with a buffer of: {{ '$%0.2f' % COMP['buffer']|float }}). Hover over blue labels above to see what compared to values differed</div> {% if yr|int > first_yr|int and mon == '01' and day|int <= 14 %}
</div><div class="col-auto">
<div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">{{yr}}</div>
{% endif %}
{% if date == key_dates['D_quit_date'] %}
<font class="text-warning">
<label data-bs-toggle="tooltip" title="D quits">
{% elif (yr == car_yr and mon == car_mon and day >= car_day and car_done.val == 0) %}
{%set car_done.val=1 %}
<font class="text-warning">
<label data-bs-toggle="tooltip" title="We own car">
{% else %}
<font class="text-secondary">
<label>
{% endif %}
{{ date }}:&nbsp;
{{ '$%0.2f' % dollars|float }}<br>
</label>
</font><br>
{% if comp_done.val == 0 and yr == comp_yr and mon == comp_mon and day|int >= comp_day|int %}
<font class="text-info">
{{ COMP['date'] }}:&nbsp;
{{ '$%0.2f' % COMP['amount']|float }}<br>
</font>
{% set comp_done.val=1 %}
{% endif %}
{% endfor %}
{% if depletion_date %}
<div class="alert alert-danger">Run out of $'s:<br>{{depletion_date}}</div>
{% else %}
<div class="alert alert-success">Super kicks in!!!</div>
{% endif %} {% endif %}
</div> </div>
</div> <div class="col-auto">
<div class="alert alert-warning">
<h6 class="alert-heading">SUMMARY/BUDGET</h6>
{% for label, value in BUDGET %}
<div>
{{label}} {{value}}
</div>
{% endfor %}
</div>
<div id="comp_col" class="col-auto">
<div class="input-group">
<button type="button" class="btn btn-info me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save Comparison set</button>
<button type="submit" class="disabled btn btn-info rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button>
<select class="form-select border border-info text-info" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
{% for el in finance['COMP_SETS'] %}
<option value="{{el[0]}}">{{el[1]}}</option>
{% endfor %}
</select>
</div>
{% if COMP %}
<div style="display:none" id="comp_alert" class="alert alert-info mt-2">Note: {{ '$%0.2f' % COMP['amount']|float }} is the final value of the compared to data (with a buffer of: {{ '$%0.2f' % COMP['buffer']|float }}). Hover over blue labels above to see what compared to values differed</div>
{% endif %}
</div>
</div>
</div>
</div> </div>
</form> </form>
<div class="row mt-4 highcharts-dark" id="container" style="width:100%; height:800px;"></div>
<div id="tab-graph" class="tab-pane">
<div class="row mt-4 highcharts-dark" id="graph" style="width:100%; height:800px;"></div>
</div>
<div id="tab-compgraph" class="tab-pane">
<div class="row mt-4 highcharts-dark" id="graph-comp" style="width:100%; height:800px;"></div>
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">
// make these global so we can also use them in the /save route (via modal) // make these global so we can also use them in the /save route (via modal)
const savingsData = JSON.parse('{{ savings | tojson }}'); const savingsData = JSON.parse('{{ savings | tojson }}');
const vars = JSON.parse('{{ finance | tojson }}'); const vars = JSON.parse('{{ finance | tojson }}');
$(function() { $('[data-bs-toggle="popover"]').popover(); });
window.onload = function() { window.onload = function() {
$('#Sell_shares').val( {{finance['Sell_shares']}} ) $('#Sell_shares').val( {{finance['Sell_shares']}} )
$('#compare_to').val( {{finance['compare_to']}} ) $('#compare_to').val( {{finance['compare_to']}} )
@@ -209,6 +256,7 @@
}; };
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
$('#tab-but-findata').click()
// Parse the savings_data from Flask // Parse the savings_data from Flask
const chartData = savingsData.map(entry => [new Date(entry[0]).getTime(), parseFloat(entry[1])]); const chartData = savingsData.map(entry => [new Date(entry[0]).getTime(), parseFloat(entry[1])]);
{% if COMP %} {% if COMP %}
@@ -250,33 +298,62 @@
}); });
const annotations = []; const annotations = [];
// the al, x, offset are used to make the altenrate annotations be on slightly different vertical offsets (size is based on $'s) // offset is used to make the next annotation be on slightly different vertical offsets (size is based on $'s)
// al alternates every 2 annotations left / right (so 2 left, then 2 right), x is just used to also move the label more left/right to get the connecting line // HACK: start at 13, later we adjust in steps of 50s allowing 4 steps, then we go back to top
var offset=13 var offset=13
{% if not COMP %} // Add annotations for changes greater than 5000
// Add annotations for changes greater than 5000 {% for a in finance['annotations'] %}
{% for a in finance['annotations'] %} annotations.push({
annotations.push({ labels: [{
labels: [{ point: {
point: { x: {{a['x']}},
x: {{a['x']}}, y: {{a['y']}},
y: {{a['y']}}, crop: true,
crop: true, xAxis: 0,
xAxis: 0, yAxis: 0
yAxis: 0 },
}, x: -70,
x: -70, {% if '-$' in a['label'] %}
y: offset, y: offset,
text: '{{a['label']}}' {% else %}
}], labelOptions: { allowOverlap: true } y: -20,
}); {% endif %}
text: '{{a['label']}}'
}], labelOptions: { allowOverlap: true }
});
{% if a['y'] > 200000 %}
offset = ({{loop.index}} * 50 % 200) +50 offset = ({{loop.index}} * 50 % 200) +50
{% endfor %} {% else %}
document.keep = annotations offset = -100
{% endif %} {% endif %}
{% endfor %}
document.keep = annotations
// Highcharts configuration // Highcharts configuration
Highcharts.chart('container', { Highcharts.chart('graph', {
chart: { type: 'line' },
colors: [ 'orange' ],
title: { text: 'Savings Over Time' },
xAxis: {
type: 'datetime',
title: { text: 'Date' },
plotBands: plotBands // Alternating background for years
},
yAxis: { title: { text: 'Amount ($)' } },
tooltip: {
pointFormatter: function ()
{
if( this.series.symbol == 'circle' ) { s='\u25CF' } else { s='\u2B25' }
return '<span style="color:' + this.point.color + '">' + s + '</span> <b>' + this.point.y + ':</b> ' + this.series.name + '<br>'
}, shared:true
},
annotations: annotations, // Add annotations
series: [ { name: "Savings", data: chartData, marker: { radius: 2 } } ]
});
{% if COMP %}
// Highcharts configuration
Highcharts.chart('graph-comp', {
chart: { type: 'line' }, chart: { type: 'line' },
colors: [ colors: [
'orange', // Custom color 1 'orange', // Custom color 1
@@ -289,21 +366,19 @@
plotBands: plotBands // Alternating background for years plotBands: plotBands // Alternating background for years
}, },
yAxis: { title: { text: 'Amount ($)' } }, yAxis: { title: { text: 'Amount ($)' } },
tooltip: { tooltip: {
pointFormatter: function () pointFormatter: function ()
{ {
if( this.series.symbol == 'circle' ) { s='\u25CF' } else { s='\u2B25' } if( this.series.symbol == 'circle' ) { s='\u25CF' } else { s='\u2B25' }
return '<span style="color:' + this.point.color + '">' + s + '</span> <b>' + this.point.y + ':</b> ' + this.series.name + '<br>' return '<span style="color:' + this.point.color + '">' + s + '</span> <b>' + this.point.y + ':</b> ' + this.series.name + '<br>'
}, shared:true }, shared:true
}, },
annotations: annotations, // Add annotations
series: [ series: [
{ name: "Savings", data: chartData, marker: { radius: 2 } } { name: "Savings", data: chartData, marker: { radius: 2 } }
{% if COMP %}
,{ name: "{{COMP['vars']['name']}}", data: compChartData, marker: { radius: 2 } } ,{ name: "{{COMP['vars']['name']}}", data: compChartData, marker: { radius: 2 } }
{% endif %}
] ]
}); });
{% endif %}
}); });
</script> </script>
<div id="save_modal" class="modal modal-lg" tabindex="-1"> <div id="save_modal" class="modal modal-lg" tabindex="-1">
@@ -340,4 +415,3 @@
</div> </div>
</body> </body>
</html> </html>