Compare commits

...

70 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
4594630b9e simple visual treatment of key dates 2025-09-11 20:45:34 +10:00
45d173e236 actually nope, not a bug, I cant count :) 2025-09-11 17:55:44 +10:00
5ca99ca1f4 update to note fortnightly pay dates are wrong on year rollover and so I am paying myself a week early at those times 2025-09-11 17:53:56 +10:00
4389045ed5 moved some hard-coded dates to top of calc.py for ease of use in multiple functions, but also just for code readability, they are more like constants than variables. Code now works out key_dates for use in dealing with future bills / next steps 2025-09-11 17:52:28 +10:00
b69ec82510 just made graph taller for now, might one day make it smaller or larger depending on whether we are comparing or not? 2025-09-11 17:50:54 +10:00
2bd39ab24c updated TODO with progress and clarifying next steps 2025-09-11 17:50:19 +10:00
c49520af7a handle simple future bills and their growth all now done 2025-09-05 16:32:16 +10:00
1a56f80cca more clean ups 2025-09-05 16:20:09 +10:00
6ae1023f6e removed debugs, add automatic annotations if bill amount is > $1000, factor in growth for ioniq6*, added debug too 2025-09-05 16:19:53 +10:00
fc1746d749 also update to reflect bill_type_id change to bill_type 2025-09-05 16:19:01 +10:00
489fb3ee2b update comment 2025-09-05 16:13:51 +10:00
c5cfc00793 put back ioniq 6 future bills, but use bill data to set values - assumption at present is they are yearly bills, could do better, but good enough for now. This commit also changes selects to return bill_type not bill_type_id and removed some debugs for bill amt in calc loop 2025-09-05 12:20:08 +10:00
ebac4aaf66 incorporate bills for dates/amounts into calculations, still need to do future bills on triggers 2025-09-03 22:35:24 +10:00
4b63b8bd44 complete the future bill handling, added new UI to match a need for it, also tighten up other TODO items 2025-09-02 23:03:48 +10:00
a0d9ac45cd remove debug, and handle (by skipping) future dated bills 2025-09-02 23:03:03 +10:00
d80cffa0dd fix up mistaken col-4 for wrong Date header, move from name to bill_type for new/update bill and support future dates by showing future in the text, rather than show an actual input type=date 2025-09-02 23:01:00 +10:00
2459dc6ea1 allow handling creating future bills - for when I quit, and will help with when switch to owning Ioniq 6 2025-09-02 22:59:45 +10:00
95d792e72f added a set of titles when adding new bill / new bill types, allows to toggle date to be when quit or normal date, with normal date we use data, with when quit, we have growth we will use for simple growth and then date(s) can be factored in based on when I quit which is changable in the main financial data 2025-09-02 22:05:16 +10:00
5914f3fdd4 clarifying next TODO 2025-08-31 16:52:33 +10:00
c21bda8da0 the 5 is unnecessary, as the width of 6ch does the sizing anyway 2025-08-31 16:48:40 +10:00
f4490e937a with dark mode, using info instead of primary feels easier on the eye and allows for consistency with graph colours 2025-08-31 16:41:40 +10:00
e373dd0009 Change approach with Qtr bills, all are simple based on last qtr - when we have normal Qtr bills (freq 'Quarterly') we just use last qtrs data for growth much easier. For GAS, we have the 'Quaterly (forced)' freq. which uses the forced/calc. amount per qtr for growth, its good enough for Gas bills - which dont always have 4 bills a year 2025-08-31 16:32:18 +10:00
3a5b77f12d Change approach with Qtr bills, all are simple based on last qtr - when we have normal Qtr bills (freq 'Quarterly') we just use last qtrs data for growth much easier. For GAS, we have the 'Quaterly (forced)' freq. which uses the forced/calc. amount per qtr for growth, its good enough for Gas bills - which dont always have 4 bills a year 2025-08-31 16:32:00 +10:00
c74383f89e added a commented out simple debug of totals to help understand bills for now - prob will make this into real table somehow in future 2025-08-31 16:29:29 +10:00
4a7080787b new item 2025-08-31 16:28:36 +10:00
07f2a321ec added support for Quarterly (seasonal) and Quarterly (fixed), also updated live DB to match 2025-08-31 11:07:08 +10:00
f67ca61cc7 working through future bills, we need to do quarterly future estimates differently for seasonal/fixed bills 2025-08-31 11:06:29 +10:00
9ad5089ac5 updated README, todo & bugs are now in own files 2025-08-31 11:05:57 +10:00
17f2534056 bug fixed where we had a bill deleted it then growth was still trying to be calc when it no longer could 2025-08-31 10:39:58 +10:00
392daa1deb fixed total not showing in %.2f BUG 2025-08-31 10:33:08 +10:00
2937866617 improve how we find monthly bill in a month, dont use exact date just use yy-mm, also fix BUG where kayo used estimate in jan to project for the next 5years, rather than real bill in feb (all in the future) to estimate 2025-08-31 10:24:46 +10:00
0ab0a112e4 new BUGs 2025-08-31 10:08:22 +10:00
4c96a9b576 with monthly bills and < 12 in first year, wont have full yr total so cant do simple growth - fix for this 2025-08-31 10:08:14 +10:00
7ad767759f remove debug 2025-08-31 10:07:32 +10:00
24c581a35a use totals to calc bill totals - should be less work, also improved cosmetics of totals display 2025-08-30 14:29:31 +10:00
3749c01e93 hacky better formatting of Totals 2025-08-30 14:10:56 +10:00
c41048ab82 made lhs wider, shrank rhs, put this_year total per bill type in lhs, altered totals formatting at bottom 2025-08-30 14:00:14 +10:00
7422321227 call calc_future_totals to pass data onto html so we can show per bill type annual amount in this year 2025-08-30 13:59:29 +10:00
338b63aa06 now the UI shows annual costs, updated the comments and bills figure again to be more accurate 2025-08-30 13:58:56 +10:00
de32bdc7ff now have a calc_future_totals func that is used to allow html to show the bills as a simple annualised cost per year 2025-08-30 13:58:29 +10:00
4a2dd4d2da want to tweak formating and have current year totals included 2025-08-30 13:14:03 +10:00
aee8916471 updating comment / bills total 2025-08-30 13:13:43 +10:00
8f69023ffd format totals per year and based on dates/END_YEAR 2025-08-28 21:19:58 +10:00
d2bf472845 clean up TODO 2025-08-28 20:49:20 +10:00
e84faffd79 when choosing a Tab, make the new bill drop-down be of that type. When we save a new bill, change the last-tab to be the type of bill we just added 2025-08-28 20:47:12 +10:00
2bdd1348b8 fixed a few bugs, annual growth was just broken, dont add another estimate bill when we have one for that year or in that quarter, removed lots of debugs, fixed a few bugs where the first data point in a new year/qtr would not have arrays initialised properly first, apportion quarterly data in future real bills - it happens with Rates 2025-08-28 19:49:41 +10:00
91ebc227b6 add support for simple growth, also remove all estimated bills when we add a new real bill 2025-08-28 19:46:51 +10:00
89fe874c5c added simple growth, changed column widths and header formats to work better. Added a quick and dirty Total bills in 2025 section 2025-08-28 19:46:23 +10:00
dda3a3e3fe now we have dark mode, use different colors for lines on graphs 2025-08-28 19:45:21 +10:00
706aee6947 add a quick TODO to make UI slicker when adding bills 2025-08-28 19:45:02 +10:00
742911ec1b still this bug - adding a qtrly bill years before the rest 2025-08-28 19:44:40 +10:00
11 changed files with 1125 additions and 373 deletions

4
BUGS
View File

@@ -0,0 +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

12
README
View File

@@ -1,18 +1,8 @@
TODO:
* fix BUGs
* convert code over to use bill_type instead of name in bills.html
CONSIDER in code:
* when we time the payment of GMHBA / HCF (and at what cadence) and include it in calcs better
- it kicks in after pay stops, and could be paid monthly say, but it is higher than if we pay yearly (I think)
* could make bills be paid quarterly rather than as 'daily' living expenses
- also could be more painful with bill increases, they seem to go up more than CPI
CONSIDER in real-world:
* moving > $250k into say ING, then rabo-bank -- 4 months interest higher in each -- maybe to another provider after that
while the balance is > $250k it offsets individual bank risk
* maybe buying shares in something like berkshire-hathaway, or vanguard ETFs?
* pay out car if the diff is negligible to reduce the exposure to > $250k in bank
To run the code:

69
TODO
View File

@@ -1,51 +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:
* 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:
* gas bills are a mess and more than 4 per year... *SIGH* try this:
- if len(bills) > num_ann_bills (effectively too many bills for what we expect)
- normalise by looping over *EVERY* year of bills
- for each bill in yr:
- pb=find_prev_bill( bill )...
- calc days between pb and bill to get daily cost
- work out qtr() and take # days from bill in Qtr X and add it to Q[X]
- if # days i X+1 add those to Q[X+1]
- then need to be careful when working out totals/growth to use Q[...] not bill['year']
* growth for internet/monthly is a bit skewed.... Really think min/avg/max might need to be smarter
- at least max, its 114-134 BUT, not in 1 year, really that changed from 2022 to 2025, so 3 years... (or max = 18% over 3 years, or 6%)
-- so ann_growth_avg or max needs to factor in years of same prices
[DONE] -- Pragmatical growth before I bonkers:
[DONE] - monthly, prob. just flat by default (simple 1 or 12 bills for the year each year and I can get a growth rate from that)
[DONE] - and apply monthly growth - annually 12 + months from last bill each year
[DONE] - quarterly - should be able to take last qtr-1 ... qtr-4 and then grow them all by growth
[DONE]- annual easy
* once auto-filled bills exist:
[DONE]- calc growth
[DONE] - project out to I am 60 (A/Q/M) - A/Q done, M to go
[DONE] - probably need to allow a toggle to: allow show manual, show auto-filled past, show auto-filled future, show all
- remove bills from Living_Expenses (carefully - but by hand)
- fold future bills into calc so they are taken out in a more time and growth appropriate way
- inflation can then be put to a more realistic quarterly figure
LONGER/HARDER:
* need to work out 'first bill' and 'last bill' to auto-fill missing bills based on
-- all missing bills follow varying growth models & its by choice -- therefore I need this in DB
- ANN: flat, min, avg, max, manual
- QTR: flat, qtrly seasonal: min/avg/max/manual, qtrly simple: min/avg/max/manual, annual: min/avg/max/manual
- MON: flat, monthly: min/avg/max/manual, annual: min/avg/max/manual
-- use this logic to add missing bills (date):
-- ANN: missing annual bill, find date based on MM-DD and add new year - given we start with first_bill anyway, will only be used for future bill predictions
-- QTR: missing quarterly bill, find date based on MM-DD and ??? - can have missing bilsl in first year
-- MON: missing monthly bills, find date based on DD and put in each missing month
-- use this logic to add missing bills (amount):
-- ANN: future only, so add ann_growth (based on drop-down) for each future year
-- QTR: add growth (based on drop-down) for each future year
-- MON: add growth (based on drop-down) for each future year
MUCH LONGER/HARDER:
potentially for each bill_type, there are unique extras - e.g. THIS feels too hard:
water has 2 fixed charges (water & sewerage) and then a consumption charge (per ML)
elec has 1 fixe charge (daily) and then consumption (per kwh) BUT, also daily solar rate
gas has fixed charge and consumption
internet, kayo is monthly fixed (but can go up sometimes)
eweka is annual fixed
phone is messier again.
* 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

329
bills.py
View File

@@ -1,6 +1,8 @@
from db import get_bill_data, get_bill_types, get_bill_freqs, 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
@@ -51,6 +53,8 @@ def allocate_by_quarter( bill_info, bill_type, yr, prev_bill, bill):
bill_info[bill_type]['qtr'][q_start.year] = {}
for i in range(1,5):
bill_info[bill_type]['qtr'][q_start.year][i]=0
if q not in bill_info[bill_type]['qtr'][q_start.year]:
bill_info[bill_type]['qtr'][q_start.year][q]=0
bill_info[bill_type]['qtr'][q_start.year][q] += days*cost_per_day
# next quarter
cur = q_end + timedelta(days=1)
@@ -86,6 +90,18 @@ def find_next_bill( bill_type, bill_info, bill_date ):
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
def find_previous_bill( bill_type, bill_info, bill_date ):
wanted_year = int(bill_date[:4])
@@ -108,6 +124,10 @@ def find_previous_bill( bill_type, bill_info, bill_date ):
# okay, we have the previous billing year, and we wanted one for a year in the future,
# just return the last one in this year as its the most recent
if wanted_year > yr:
# small chance of future bills having estimates and reals (kayo did this)
for tmp in bill_info[bill_type]['year'][yr]:
if tmp['estimated'] == 0:
return tmp
return bill_info[bill_type]['year'][yr][0]
else:
# lets go through the newest to oldest of these bills
@@ -133,20 +153,24 @@ def new_estimated_bill( bill_info, yr, bill_type, amt, new_date ):
bill={}
bill['bill_date']=new_date
bill['amount']=amt
bill['bill_type']=bill_type
bill['estimated']=1
# need this for find_previous_bill to work but 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)
if bill_info[bill_type]['num_ann_bills'] == 4:
q = qtr( new_date )
# new bill in this qtr of this year, so set arrays up
if yr not in bill_info[bill_type]['qtr']:
bill_info[bill_type]['qtr'][yr]={}
pb = find_previous_bill( bill_type, bill_info, new_date )
if pb['estimated'] == 0:
print( f" FIXFIXFIX - have a prev real bill={pb['bill_date']} & this is first est - likely need to better apportion this bill into the quarters" )
allocate_by_quarter( bill_info, bill_type, yr, pb, bill )
bill_info[bill_type]['qtr'][yr][q]=amt
else:
if not q in bill_info[bill_type]['qtr'][yr]:
# first in this year, just init it...
bill_info[bill_type]['qtr'][yr][q]=0
bill_info[bill_type]['qtr'][yr][q]+=amt
return
@@ -155,12 +179,16 @@ def new_estimated_bill( bill_info, yr, bill_type, amt, new_date ):
# NOTE: only ever called when there is a need to add a new bill
def add_missing_annual_bill_in_yr( bill_type, bill_info, yr ):
mm_dd = bill_info[bill_type]['last_bill']['bill_date'][5:]
new_date= f'{yr}-{mm_dd}'
pb=find_previous_bill( bill_type, bill_info, new_date )
if pb:
amt = pb['amount']
else:
amt = bill_info[bill_type]['last_bill']['amount']
# okay the missing bill is before the first bill...
for i in range( bill_info[bill_type]['last_bill_year'], yr ):
amt += amt * bill_info[bill_type]['growth']/100
new_estimated_bill( bill_info, yr, bill_type, amt, f'{yr}-{mm_dd}' )
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
return
# missing quarterly bill, find date based on MM-DD and ??? - can have missing bilsl in first year
@@ -177,15 +205,32 @@ def add_missing_quarter_bills_in_yr( bill_type, bill_info, yr ):
else:
r=range(1,5)
for q in r:
# amt is total of last year's qtr bill proportion
amt = bill_info[bill_type]['qtr'][yr-1][q]*(1+bill_info[bill_type]['growth']/100)
# just make new bills first of last month of a qtr (good as any date for GAS, they move anyway)
new_date = f'{yr}-{q*3:02d}-01'
# SANITY CHECK: we might be adding a bill estimate we already have (due to stupid gas bills /qtrly code)
if yr in bill_info[bill_type]['year']:
for b in bill_info[bill_type]['year'][yr]:
if b['bill_date'] == new_date:
if 'forced' in bill_info[bill_type]['freq']:
actually_add_estimated_new_quarter_bill_forced(bill_type, bill_info, yr, q)
else:
actually_add_estimated_new_quarter_bill(bill_type, bill_info, yr, q)
return
################################################################################
# func take a qtr in a year, finds equiv from previous year, calcs new based on
# it (same 'day' with amt * growth)
################################################################################
def actually_add_estimated_new_quarter_bill( bill_type, bill_info, yr, q ):
# amt is total of last year's qtr bill (NOTE: use 4-q, bills are in desc order)
last_yrs_bill_in_this_q = bill_info[bill_type]['year'][yr-1][4-q]
amt = last_yrs_bill_in_this_q['amount']*(1+bill_info[bill_type]['growth']/100)
# make new qtr bill same 'day' (mm-dd) as last year, just chg (yr)
mmdd=last_yrs_bill_in_this_q['bill_date'][5:]
new_date = f'{yr}-{mmdd}'
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
return
def actually_add_estimated_new_quarter_bill_forced( bill_type, bill_info, yr, q ):
last_yrs_qtr_amount = bill_info[bill_type]['qtr'][yr-1][q]
amt=last_yrs_qtr_amount*(1+bill_info[bill_type]['growth']/100)
new_date = f'{yr}-{q*3:02d}-01'
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
return
@@ -194,6 +239,8 @@ def add_missing_quarter_bills_in_yr( bill_type, bill_info, yr ):
# 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:]
@@ -210,12 +257,13 @@ def add_missing_monthly_bills_in_yr( bill_type, bill_info, yr ):
for i in range( start_m+1, 13 ):
bill_found=False
new_date = f'{yr}-{i:02d}-{dd}'
new_date_yymm=f'{yr}-{i:02d}'
if yr in bill_info[bill_type]['year']:
for b in bill_info[bill_type]['year'][yr]:
# this bill exists, skip adding it (this occurs when called to
# add bills as there are < 12 bills in first_year, BUT, we
# don't fill before first_bill so the < 12 ALWAYS triggers
if str(b['bill_date']) == new_date:
if new_date_yymm in str(b['bill_date']):
bill_found=True
break
if not bill_found:
@@ -248,8 +296,20 @@ def get_growth_value( bt, bill_type ):
return el['ann_growth_avg']
elif which == 'min':
return el['ann_growth_min']
else:
elif which == 'simple':
return el['ann_growth_simple']
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
################################################################################
@@ -258,24 +318,44 @@ def get_growth_value( bt, bill_type ):
# and I didn't want to input 12 of them at the same price), and it always
# occurs for future bills
################################################################################
def process_bill_data(bd, bt, bf):
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)
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_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
bf_id_name = {row["id"]: row["name"] for row in bf}
# 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
bill_info={}
future_car_bills=[]
future_D_quit_bills=[]
for bill in bd:
bill_type = bill['bill_type_id']
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 )
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
yr= int(bill['bill_date'][:4])
# new bill type
if not bill_type in bill_info:
bill_info[bill_type]={}
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]['first_bill']={}
@@ -294,11 +374,13 @@ def process_bill_data(bd, bt, bf):
bill_info[bill_type]['first_bill_year']=int(bill['bill_date'][:4])
if not 'last_real_bill_year' in bill_info[bill_type] and not bill['estimated']:
bill_info[bill_type]['last_real_bill_year']=int(bill['bill_date'][:4])
# add this bill to list for this year
# append this bill to list for this year
bill_info[bill_type]['year'][yr].append(bill)
# now process the bill_info from yr of first bill to yr of last bill
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
num = bf_id_num[bt_id_freq[bill_type]]
@@ -313,10 +395,70 @@ def process_bill_data(bd, bt, bf):
# go from first_bill year until reach end year
for yr in range( yr_min, END_YEAR+1 ):
# we have all the bills needed for yr - but dont be cute with qtrly, gas bills suck can have missing with 4 bills
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:
# > can occur when we add a real bill "on top of" an estimate.
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 )
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
################################################################################
# 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
@@ -333,7 +475,7 @@ def add_missing_bills_for_yr( bill_type, bill_info, yr ):
################################################################################
# Takes qtrly bills and start from 2nd year of bills (so we can estimate growth)
# and go through each bill allocating hte proportion of each bill to each
# and go through each bill allocating the proportion of each bill to each
# relevant quarter - to build more accurate totals. Would be mostly marginal
# accept when Gas qtrly bills have 6 per year, and we need to guess say qtr4 in
# the future, we can't easily find corresponding bill form previous year, so
@@ -344,7 +486,8 @@ def ProportionQtrlyData( bill_type, bill_info ):
now_yr = datetime.date.today().year
# FIX UP CRAPPY QUARTERLY BILLING PROPORTIONS (only useful as some gas bills are 6 / year!)
if bill_info[bill_type]['num_ann_bills']==4:
for yr in range( bill_info[bill_type]['first_bill_year'], now_yr+1):
for yr in range( bill_info[bill_type]['first_bill_year'], END_YEAR+1):
if yr in bill_info[bill_type]['year']:
for b in bill_info[bill_type]['year'][yr]:
pb = find_previous_bill( bill_type, bill_info, b['bill_date'] )
if not pb:
@@ -357,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 ):
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
@@ -368,7 +511,7 @@ def derive_ann_growth( bill_type, bill_info ):
continue;
# just going to make sure we dont use estimated data in the last year of real data - can skew growths
if yr == bill_info[bill_type]['last_real_bill_year']:
if yr == bill_info[bill_type]['last_real_bill_year'] or bill_info[bill_type]['num_ann_bills'] ==1:
skip_yr=False
for b in bill_info[bill_type]['year'][yr]:
if b['estimated']:
@@ -390,31 +533,151 @@ def derive_ann_growth( bill_type, bill_info ):
# use new derived qtr, slightly more accurate
total[yr]=tot
# once we have all yr totals:
growth = {}
min_growth = 999
avg_growth = 0
max_growth = 0
count = 0
simple_first_yr=0
simple_last_yr=0
# start from year after first bill, so we can see annual growth from the following year onwards
for yr in range( bill_info[bill_type]['first_bill_year']+1, now_yr+1):
# if full data sets for consecutive years, work out annual growth stats
if yr-1 in total and yr in total:
if simple_first_yr==0:
simple_first_yr=yr
growth = (total[yr] - total[yr-1]) / total[yr-1] * 100
avg_growth += growth
count += 1
simple_last_yr=yr
if growth < min_growth:
min_growth = growth
if growth > max_growth:
max_growth = growth
# data to work with
if count:
## print( f"Before sanity check, min={min_growth}, avg={avg_growth/count}, max_growth={max_growth}" )
# HACK FOR SANITY SAKE NOW - bills wont decrease normally, and 10% is unlikely for sustained growth
if min_growth< 0: min_growth=0
if avg_growth< 0 or avg_growth > 10: avg_growth = 3*count
if max_growth>10 : max_growth = 9.99
set_bill_type_growth( bill_type, min_growth, avg_growth/count, max_growth )
# strt with 0, set it if we can below
simple_growth=0
if simple_first_yr != simple_last_yr:
# calculate a simple growth with full year consecutive totals -> last - first / years
simple_growth=( ((total[simple_last_yr]-total[simple_first_yr])/(simple_last_yr-simple_first_yr)) / total[simple_first_yr] )*100.0
else:
# calculate a simple growth based on last - first / years - only 1 consecutive year I guess, so can't use it, use real first/last
if bill_info[bill_type]['first_bill_year'] != bill_info[bill_type]['last_real_bill_year'] and bill_info[bill_type]['first_bill_year'] in total and bill_info[bill_type]['last_real_bill_year'] in total:
simple_growth=( ((total[bill_info[bill_type]['last_real_bill_year']]-total[bill_info[bill_type]['first_bill_year']])/(bill_info[bill_type]['last_real_bill_year']-bill_info[bill_type]['first_bill_year'])) / total[bill_info[bill_type]['first_bill_year']] )*100.0
set_bill_type_growth( bill_type, min_growth, avg_growth/count, max_growth, simple_growth )
else:
# okay use last - first / years to get a simple_growth, just need bills from different years
# if there are totals for them (may not be set with monthly and < 12 bills in 1st year)
if 'last_real_bill_year' in bill_info[bill_type] and bill_info[bill_type]['first_bill_year'] != bill_info[bill_type]['last_real_bill_year'] and bill_info[bill_type]['first_bill_year'] in total and bill_info[bill_type]['last_real_bill_year'] in total:
simple_growth=( ((total[bill_info[bill_type]['last_real_bill_year']]-total[bill_info[bill_type]['first_bill_year']])/(bill_info[bill_type]['last_real_bill_year']-bill_info[bill_type]['first_bill_year'])) / total[bill_info[bill_type]['first_bill_year']] )*100.0
set_bill_type_growth( bill_type, 0, 0, 0, simple_growth )
else:
# failsafe (just in case fill bills failed to add enough bills to average out)
print( f"{bill_type}: Unable to calculate growth!" )
################################################################################
# just go through this year to END_YEAR, total any bills for each year up
# so we can display the annual estimated bills onwards...
################################################################################
def calc_future_totals(bill_info, bill_types):
total={}
now_yr = datetime.date.today().year
for bt in bill_types:
total[bt['id']]={}
for yr in range( now_yr, END_YEAR+1):
total[bt['id']][yr]=0.0
if bt['id'] in bill_info and yr in bill_info[bt['id']]['year']:
for b in bill_info[bt['id']]['year'][yr]:
total[bt['id']][yr] += b['amount']
# 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

144
calc.py
View File

@@ -5,6 +5,31 @@ from defines import END_YEAR
# GLOBAL CONSTANTS
LEASE = 0
# Dates that don't change
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 ):
amt=0
day_str = day.strftime("%Y-%m-%d")
for b in bill_data:
# there may be more than one bill on this day, keep add amount and keep going in loop
if b['bill_date'] == day_str:
amt += b['amount']
if b['amount'] > 1000:
n=bt_id_name[ b['bill_type'] ]
print( f"bill_amt_today {n} for {day_str} has amt={b['amount']}" )
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
if b['bill_date'] < day_str:
return amt
#failsafe, doubt this even can occur with bills older than today
return amt
def add_annotation(finance, dt, total, delta, text):
# dont add an annotation for small changes (jic)
tm = dt.timestamp() * 1000
@@ -15,7 +40,7 @@ def add_annotation(finance, dt, total, delta, text):
finance['annotations'].append( { 'label': text, 'x': tm, 'y': total } )
return
def calculate_savings_depletion(finance):
def calculate_savings_depletion(finance, bill_data, bill_type):
# Extract all the financial data from the database
D_Salary = finance['D_Salary']
D_Num_fortnights_pay = finance['D_Num_fortnights_pay']
@@ -57,22 +82,18 @@ def calculate_savings_depletion(finance):
payout = 83115.84
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
# (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
# 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
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...
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}")
@@ -80,10 +101,7 @@ def calculate_savings_depletion(finance):
# convenience vars to make it easier to read conditional leave tax/payment logic below
D_has_quit = False
D_quit_year = 0
claim_tax_on_leave = False
new_fin_year_25 = datetime(2025, 7, 1)
new_fin_year_26 = datetime(2026, 7, 1)
# Constants for interest calculations
annual_interest_rate = Interest_Rate / 100.0
@@ -91,25 +109,29 @@ def calculate_savings_depletion(finance):
# main loop range -- start from now, and simulate till D is 60 (April 2031)
current_date = datetime.today()
end_date = datetime(END_YEAR, 4, 15)
# refactor Living_Expenses to exclude bills (as we have detailed future projections for them that usually exceed inflation)
total=0
yr=str(current_date.year)
for b in bill_data:
if yr in b['bill_date']:
total += b['amount']
print( f"this yr={current_date.year} - total={total}" )
Living_Expenses -= total
print( f"LE is now={Living_Expenses}" )
# Calculate daily living expenses
daily_living_expenses = Living_Expenses / 365
# take a stab at future rego and insurance on the Ioniq 6 when we finish the lease - paid every anniversary of the Car balloon payment date
ioniq6_rego = 800
ioniq6_ins = 2200
print( f"daily LE starts at={daily_living_expenses}" )
print( f"fortnightly LE starts at={daily_living_expenses*14}" )
# Start the calculation
current_savings = Savings
depletion_date = None
savings_per_fortnight = []
# significant dates that are non-changeable
school_fees_date = datetime(2025, 12, 5)
car_balloon_date = datetime(2026, 11, 15)
mich_present_date = datetime(2026,10,15)
# significant dates - but who knows when? :)
overseas_trip_date = datetime.strptime( finance['Overseas_trip_date'], "%Y-%m-%d")
mark_reno_date = datetime.strptime( finance['Mark_reno_date'], "%Y-%m-%d")
@@ -126,6 +148,9 @@ def calculate_savings_depletion(finance):
# (key is date, text is for larger spend items by hand)
finance['annotations']=[]
#quick convenience lookup of bill types name for annotations.
bt_id_name = {row["id"]: row["name"] for row in bill_type}
while current_date <= end_date:
#paid on 8th or 22nd of Jan (so 8th day of fortnight)
is_fortnight = (days_count % 14 == 7)
@@ -134,6 +159,9 @@ def calculate_savings_depletion(finance):
# Subtract daily living expenses
current_savings -= daily_living_expenses
# 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 )
# Calculate daily interest but apply at the end of the month
monthly_interest += current_savings * daily_interest_rate
@@ -148,29 +176,17 @@ def calculate_savings_depletion(finance):
current_savings -= 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:
D_has_quit = True
D_quit_year = current_date.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
if current_date > new_fin_year_25:
D_quit_date = current_date
# going to pay tax on payout, so claim it back next year
claim_tax_on_leave = True
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}" )
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}" )
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}" )
current_savings += D_leave_after_tax
add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
D_leave_after_tax = 0
# 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
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:
print(f"{current_date}: salary paid by Deakin - adding: {fortnight_income}" )
current_savings += fortnight_income
@@ -178,6 +194,14 @@ def calculate_savings_depletion(finance):
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 D_has_quit and current_date.day == 15:
if Ioniq6_future == LEASE and current_date <= car_balloon_date:
@@ -191,10 +215,11 @@ def calculate_savings_depletion(finance):
current_savings += monthly_interest
#print(f"{current_date}: interest paid - ${monthly_interest}")
monthly_interest = 0
# monthly increase living expenses by a monthly inflation multiplier
Living_Expenses += (Inflation/100.0)/12 * Living_Expenses
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():
current_savings -= School_Fees
@@ -210,24 +235,6 @@ def calculate_savings_depletion(finance):
add_annotation(finance, current_date, current_savings, -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:
current_savings -= (ioniq6_ins + ioniq6_rego)
add_annotation(finance, current_date, current_savings, -(ioniq6_ins+ioniq6_rego), "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():
current_savings -= Mich_present
add_annotation(finance, current_date, current_savings, -Mich_present, "Mich's present" )
@@ -251,7 +258,7 @@ def calculate_savings_depletion(finance):
# 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
# 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
tax_threshold = 18200
# cap-gains is 50% of profit (lazy profit calc here, just assume its all profit)
@@ -281,6 +288,27 @@ def calculate_savings_depletion(finance):
finance['CBA']=D_CBA_shares
finance['TLS']=D_TLS_shares+M_TLS_shares
return depletion_date, savings_per_fortnight, current_savings
################################################################################
# work out the date D quits and when we own the car, so we can then use it to
# handle future bills
################################################################################
def calc_key_dates( finance ):
key_dates={}
now = datetime.today()
# this will be 0 to 13 days - how far into this fortnights pay cycle are we now
days_in_pay_fortnight= ( now - first_pay_date ).days % 14
# add 1 less fortnight than we continue to work, then add rest of pay cycle (14-days_in_pay_fortnight)
key_dates['D_quit_date'] = (now+timedelta(weeks=2*(finance['D_Num_fortnights_pay']-1))+timedelta(days=(14-days_in_pay_fortnight))).strftime('%Y-%m-%d')
# use lease date
if finance['Ioniq6_future'] == LEASE:
key_dates['D_hyundai_owned'] = car_balloon_date.strftime('%Y-%m-%d')
# use buyout date
else:
key_dates['D_hyundai_owned'] = finance['Car_buyout_date']
return key_dates

131
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,
@@ -91,15 +97,10 @@ def init_db():
ann_growth_min REAL,
ann_growth_avg REAL,
ann_growth_max REAL,
ann_growth_simple REAL,
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,
@@ -115,27 +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: $1876.19, nab is 2727.95
# 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, 'Monthly', 12 )" )
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()
@@ -148,8 +178,9 @@ def get_finance_data():
return dict(finance)
def get_budget_data(finance_data):
# annual bills - health ins (5k), rates (2.4), electricity (1.5), gas (2), internet (1.6), car insurance (.7), rego (.8), house insurance (2.4), GFC (2.2), phones (.5), melb. pollen (.03), nabu casa (.1), eweka (.1) --- noting phone is elevated presuming I also go onto Aldi plan, but that there is no family discount
bills = 19330
# annual bills - health ins (5k), rates (2.6), electricity (1.2), gas (2.1) - but 1.4 in 2025 due to EU trip, internet (1.6), car insurance (.7), rego (.8), house insurance (2.4), GFC (2.6), water (1.1), eweka (.1), phones (.5), melb. pollen (.03), nabu casa (.1) --- noting phone is elevated presuming I also go onto Aldi plan, but that there is no family discount, and health will be extra after stop working
# fudging below - its more like 15.2 + health, and really gas will be more than 2.1 than 1.4, so about 16+5
bills = 21000
BUDGET=[]
BUDGET.append( ('Bills', f"${bills:,.2f}") )
BUDGET.append( ('Buffer', f"${finance_data['CBA']*finance_data['CBA_price']+finance_data['TLS']*finance_data['TLS_price']:,.2f}") )
@@ -268,10 +299,15 @@ def get_comp_set_options(finance):
conn.close()
return
def get_bill_data():
def get_bill_data(order_by):
conn = connect_db(True)
cur = conn.cursor()
cur.execute('''SELECT bd.id, bt.id as bill_type_id, bt.name, bd.amount, bd.bill_date, bd.estimated
if order_by == "order_by_date_only":
cur.execute('''SELECT bd.id, bt.id as bill_type, bt.name, bd.amount, bd.bill_date, bd.estimated
FROM bill_type bt, bill_data bd
where bt.id = bd.bill_type order by bd.bill_date desc''')
else:
cur.execute('''SELECT bd.id, bt.id as bill_type, bt.name, bd.amount, bd.bill_date, bd.estimated
FROM bill_type bt, bill_data bd
where bt.id = bd.bill_type order by bt.name, bd.bill_date desc''')
bd = cur.fetchall()
@@ -308,6 +344,12 @@ def get_bill_freqs():
def new_bill( bill_type, amount, bill_date, estimated ):
conn = connect_db(False)
cur = conn.cursor()
# if we are a real bill added by UI
if not estimated:
# delete old estimates as new bill will potentially change them/growth, etc.
cur.execute( f"delete from bill_data where estimated=1" )
# force the next /bills load to show the tab for the bill we are adding
cur.execute( f"update bill_ui set last_tab='{bill_type}'" )
cur.execute( f"insert into bill_data ( 'bill_type', 'amount', 'bill_date', 'estimated' ) values ( '{bill_type}', '{float(amount):.2f}', '{bill_date}', {estimated} )" )
conn.commit()
conn.close()
@@ -325,7 +367,7 @@ def insert_bill_type( bt, fq ):
conn = connect_db(False)
cur = conn.cursor()
print( f"fq={fq}" )
cur.execute( f"insert into bill_type ( 'name', 'freq', 'ann_growth_min', 'ann_growth_avg', 'ann_growth_max' ) values ( '{bt}', {fq}, 0, 0, 0 )" )
cur.execute( f"insert into bill_type ( 'name', 'freq', 'ann_growth_min', 'ann_growth_avg', 'ann_growth_max', 'ann_growth_simple' ) values ( '{bt}', {fq}, 0, 0, 0, 0 )" )
conn.commit()
conn.close()
return
@@ -354,14 +396,22 @@ def delete_bill_type( id ):
conn.close()
return
def set_bill_type_growth( id, min_g, avg_g, max_g ):
def set_bill_type_growth( id, min_g, avg_g, max_g, simple_g ):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"update bill_type set ann_growth_min='{min_g}', ann_growth_avg ='{avg_g}', ann_growth_max='{max_g}' where id = {id}" )
cur.execute( f"update bill_type set ann_growth_min={min_g}, ann_growth_avg ={avg_g}, ann_growth_max={max_g}, ann_growth_simple= {simple_g} where id = {id}" )
conn.commit()
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()
@@ -381,3 +431,36 @@ def save_ui(data):
conn.commit()
conn.close()
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

103
main.py
View File

@@ -1,15 +1,19 @@
# main.py
from flask import Flask, render_template, request, redirect, url_for, Response, jsonify
from calc import calculate_savings_depletion
from db import init_db, get_finance_data, update_finance, get_budget_data, insert_cset, get_comp_set_data, get_comp_set_options, get_bill_freqs
from db import get_bill_data, new_bill, update_bill_data, delete_bill
from calc import calculate_savings_depletion, calc_key_dates
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
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
from datetime import datetime, date
import csv
import io
import requests
from disp import FP_VAR
app = Flask(__name__)
@@ -24,7 +28,9 @@ init_db()
def index():
finance_data = get_finance_data()
get_comp_set_options(finance_data)
depletion_date, savings_per_fortnight, final_savings = calculate_savings_depletion(finance_data)
bill_data = get_bill_data("order_by_date_only")
bill_types = get_bill_types()
depletion_date, savings_per_fortnight, final_savings = calculate_savings_depletion(finance_data, bill_data, bill_types)
BUDGET=get_budget_data(finance_data)
if depletion_date:
@@ -99,7 +105,8 @@ def index():
# now work out how much padding we need in the first year to align the last dates for all years
padding=second_count - first_count
return render_template('index.html', now=now, first_yr=first_yr, padding=padding, finance=finance_data, depletion_date=depletion_date, savings=savings_per_fortnight, BUDGET=BUDGET, COMP=COMP, DISP=DISP)
key_dates = calc_key_dates( finance_data )
return render_template('index.html', now=now, first_yr=first_yr, padding=padding, finance=finance_data, depletion_date=depletion_date, savings=savings_per_fortnight, BUDGET=BUDGET, COMP=COMP, DISP=DISP, key_dates=key_dates)
@app.route('/save', methods=['POST'])
def save():
@@ -109,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'],
@@ -138,17 +147,45 @@ def update():
request.form['Ioniq6_future']
)
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'))
@app.route('/bills')
def DisplayBillData():
bill_data = get_bill_data()
finance_data = get_finance_data()
# work out when D quits, when car is owned
key_dates = calc_key_dates( finance_data )
bill_data = get_bill_data("order_by_bill_type_then_date")
bill_types = get_bill_types()
bill_freqs = get_bill_freqs()
bill_ui = get_bill_ui()
process_bill_data(bill_data, bill_types, bill_freqs)
bill_data = get_bill_data()
return render_template('bills.html', bill_data=bill_data, bill_types=bill_types, bill_freqs=bill_freqs, bill_ui=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, growth=bill_growth_types, cpi=finance_data['Inflation'] )
@app.route('/newbilltype', methods=['POST'])
def InsertBillType():
@@ -166,13 +203,17 @@ def UpdateBillType():
def InsertBill():
data = request.get_json()
# last param is estimated - e.g. anything via GUI is not an estimate, but is a real bill
new_bill( data['name'], data['amount'], data['bill_date'], 0 )
if 'bill_date' in data:
new_bill( data['bill_type'], data['amount'], data['bill_date'], 0 )
else:
new_bill( data['bill_type'], data['amount'], 'future', 0 )
set_bill_type_growth( data['bill_type'], 0, 0, 0, data['growth'] )
return "200"
@app.route('/updatebill', methods=['POST'])
def UpdateBill():
data = request.get_json()
update_bill_data( data['id'], data['name'], data['amount'], data['bill_date'] )
update_bill_data( data['id'], data['bill_type'], data['amount'], data['bill_date'] )
return "200"
@app.route('/delbilltype', methods=['POST'])
@@ -199,6 +240,42 @@ def SaveUI():
save_ui( data )
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
if __name__ == '__main__':

View File

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

View File

@@ -18,12 +18,31 @@
</style>
</head>
<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>
<div class="mt-4 col-6">
<div class="row align-items-center">
<button id="new-bill-type-button" class="mb-3 px-0 offset-4 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillType()"><span class="bi bi-plus-lg"> New Bill Type</span></button>
{# DEBUG totals if needed
<table>
{% for bt in total %}
<tr><td></td><td>&nbsp;{{bt}}:</td>
{% for yr in range( 2025, 2032 ) %}
{% if yr in total[bt] %}
<td>
{{total[bt][yr]}}
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
#}
<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>
</div>
<div class="row align-items-center mb-3">
<button id="new-bill-type-button" class="mt-4 px-0 offset-4 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillType()"><span class="bi bi-plus-lg"> New Bill Type</span></button>
<div class="new-bill-type-class px-0 col-2 d-none"> <input type="text" class="form-control text-end float-end border border-primary" id="new-bill-type-name"></div>
<div class="new-bill-type-class px-0 col-2 d-none"><select id="new-bill-type-freq" class="form-select text-center">
{% for bf in bill_freqs %}
@@ -33,71 +52,145 @@
</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="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 class="row">
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"><br>Name</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"><br>Frequency</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Annual Growth Est (min/avg/max)</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"><br>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-2">
<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" for="min-{{bt.id}}">
{% if 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" for="avg-{{bt.id}}">
{% 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" for="max-{{bt.id}}">
{% if bt.ann_growth_max < 10 %} &nbsp; {% endif %}
{{'%.2f'|format(bt.ann_growth_max)}}
</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>
<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 %}
<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" %}
{% endif %}
<div class="row">
<div class="offset-4 col text-end {{markup}}">
Total bills in {{yr}}
</div>
<div class="col {{markup}} text-primary">
${{'%.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="pt-4 col-6">
<div class="row align-items-center">
<button id="new-bill-data-button" class="mb-3 px-0 offset-6 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillData()"><span class="bi bi-plus-lg"> New Bill</span></button>
<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>
<div id="new-bill-data-growth-label" class="col-4 form-control-inline d-none">Est. Annual Growth</div>
<div class="col-2 form-control-inline d-none new-bill-data-class">Amount</div>
</div>
<div class="row align-items-center mb-3">
<button id="new-bill-data-button" class="mt-4 px-0 offset-8 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillData()"><span class="bi bi-plus-lg"> New Bill</span></button>
<div class="new-bill-data-class px-0 col-2 d-none"> <select id="new-bill-data-type" class="form-select text-end float-end border border-primary">
{% for bt in bill_types %}
<option value={{bt.id}}>{{bt.name}}</option>
{% endfor %}
</select>
</div>
<div class="new-bill-data-class px-0 col-2 d-none"> <input type="date" class="form-control text-end float-end border border-primary" id="new-bill-data-date"> </div>
<div class="new-bill-data-class px-0 col-2 d-none"> <input type="number" class="form-control text-end float-end border border-primary" id="new-bill-data-amount"> </div>
<div class="new-bill-data-class px-0 col-4 d-none">
<div class="input-group" style="max-width: 300px;">
<input type="date" class="form-control" id="new-bill-data-date">
<input type="text" class="form-control d-none" id="new-bill-data-growth">
<button class="btn btn-outline-danger" type="button" id="toggleDateBtn">When quit</button>
</div>
</div>
<div class="new-bill-data-class px-0 col-2 d-none">
<input type="number" class="form-control text-end float-end border border-primary" id="new-bill-data-amount">
</div>
<button id="save-bill" class="new-bill-data-class px-0 col-1 btn btn-success bg-success-subtle text-success d-none" onClick="NewBill()">
<span class="bi bi-floppy"></span> Save </button>
<button class="new-bill-data-class px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelNewBill()" ><span class="bi bi-x"> Cancel</span> </button>
<button class="new-bill-data-class px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelNewBill()" >
<span class="bi bi-x"> Cancel</span> </button>
</div>
<!-- create tabbed view for each bill type -->
@@ -121,13 +214,14 @@
{% for bd in bill_data %}
{% if loop.first %}
<div class="row pt-2">
<div class="p-0 col-2"> <label class="form-control text-center border-0 fw-bold bg-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-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></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"><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_id == bt.id %}
{% if bd.bill_type == bt.id %}
{% if bd.estimated == 1 %}
<div class="row est d-none fst-italic">
{% set classes="fst-italic form-control text-center" %}
@@ -136,15 +230,28 @@
{% set classes="form-control text-center" %}
{% endif %}
<div class="px-0 col-2"> <input type="text" class="{{classes}}" id="bill-data-type-{{bd.id}}" value="{{ bd.name }}" disabled> </div>
{% if bd.bill_date == 'future' %}
<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>
<div class="px-0 col-2"> <input type="number" class="{{classes}}" id="bill-data-amount-{{bd.id}}" value="{{ bd.amount }}" 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"> <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-1 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-1 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-1 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-1 btn btn-danger bg-danger-subtle text-danger d-none"
<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 %}
@@ -178,10 +285,29 @@
function NewBill()
{
if( $('#new-bill-data-growth').hasClass('d-none') )
{
// if growth is hidden, then we have normal bill
$.ajax( { type: 'POST', url: '/newbill',
contentType: 'application/json', data: JSON.stringify( { 'name': $('#new-bill-data-type').val(), 'amount': $('#new-bill-data-amount').val(), 'bill_date': $('#new-bill-data-date').val() } ),
contentType: 'application/json',
data: JSON.stringify( {
'bill_type': $('#new-bill-data-type').val(),
'amount': $('#new-bill-data-amount').val(),
'bill_date': $('#new-bill-data-date').val() } ),
success: function() { window.location='bills' } } )
}
else
{
// if growth is visible, then we have future bill/growth & no date
$.ajax( { type: 'POST', url: '/newbill',
contentType: 'application/json',
data: JSON.stringify( {
'bill_type': $('#new-bill-data-type').val(),
'amount': $('#new-bill-data-amount').val(),
'growth': $('#new-bill-data-growth').val() } ),
success: function() { window.location='bills' } } )
}
}
function CancelNewBill()
{
@@ -220,7 +346,7 @@
$.ajax( { type: 'POST', url: '/updatebill',
contentType: 'application/json', data: JSON.stringify( {
'id': id,
'name': $('#bill-data-type-'+id).val(),
'bill_type': $('#bill-data-type-'+id).val(),
'bill_date' : $('#bill-data-date-'+id).val(),
'amount': $('#bill-data-amount-'+id).val() } ),
success: function() { window.location='bills' } } )
@@ -317,14 +443,17 @@
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 )
{
// set the drop-down for new bill to be this tab now...
$("#new-bill-data-type").val( $('.nav-tabs .nav-link.active').prop('id').replace("tab-but-", "") )
$.ajax( { type: 'POST', url: '/saveui', contentType: 'application/json', data: JSON.stringify( { 'last_tab': last_tab } ), success: function() { } } )
}
@@ -347,8 +476,50 @@
{% else %}
$('#tab-but-1').tab('show');
{% endif %}
// make the new bill drop-down default to the same as the current tab
$("#new-bill-data-type").val( {{bill_ui.last_tab}} )
} )
$(function () {
let disabled = false;
$('#toggleDateBtn').on('click', function () {
disabled = !disabled;
if (disabled) {
$('#new-bill-data-date').addClass('d-none')
$('#new-bill-data-growth').removeClass('d-none')
$(this)
.removeClass('btn-outline-danger')
.addClass('btn-outline-success')
.html('Normal date');
$('#new-bill-data-date-label').addClass('d-none')
$('#new-bill-data-growth-label').removeClass('d-none')
} else {
$('#new-bill-data-date').removeClass('d-none')
$('#new-bill-data-growth').addClass('d-none')
$(this)
.removeClass('btn-outline-success')
.addClass('btn-outline-danger')
.html('When quit');
$('#new-bill-data-date-label').removeClass('d-none')
$('#new-bill-data-growth-label').addClass('d-none')
}
});
});
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>
</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>
@@ -18,8 +20,31 @@
</style>
</head>
<body>
<div class="containerfluid">
<h3 align="center">Finance Tracker (go to <a href="bills">Bills</a>)</h3>
<div class="container-fluid">
<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 %}
@@ -27,7 +52,7 @@
{% for el in r %}
{% if COMP and ( COMP['vars'][el.varname] != finance[el.varname] or
(COMP['vars'][el.datevarname] is defined and COMP['vars'][el.datevarname] != finance[el.datevarname]) ) %}
{% set extra=" text-primary" %}
{% set extra=" text-info" %}
{% else %}
{% set extra="" %}
{% endif %}
@@ -40,7 +65,7 @@
{% endif %}
class="col-form-label me-2 text-end float-end {{extra}}">{{el.label}}
</label>
<select class="form-select border border-primary text-primary text-end" id="{{el.varname}}" name="{{el.varname}}" style="width: 120px;"
<select class="form-select border border-info text-info text-end" id="{{el.varname}}" name="{{el.varname}}" style="width: 120px;"
onchange="this.form.submit()">
{% for o in el.opts %}
<option value="{{o.val}}">{{o.label}}</option>
@@ -52,9 +77,9 @@
{% endif %}
class="col-form-label me-2 text-end float-end {{extra}}">{{el.label}}
</label>
<input type="number" step="any" class="form-control text-end float-end border border-primary" onchange="this.form.submit()" style="max-width: 120px;"
<input type="number" step="any" class="form-control text-end float-end border border-info" onchange="this.form.submit()" style="max-width: 120px;"
id="{{el.varname}}" name="{{el.varname}}" value="{{ finance[el.varname] }}" {{el.display}}>
<input type="date" class="form-control text-end float-end border border-primary" id="{{el.datevarname}}" style="max-width: 150px;"
<input type="date" class="form-control text-end float-end border border-info" id="{{el.datevarname}}" style="max-width: 150px;"
name="{{el.datevarname}}" value="{{ finance[el.datevarname] }}" onchange="this.form.submit()">
{% else %}
{% if COMP and COMP['vars'][el.varname] != finance[el.varname] %}
@@ -67,7 +92,7 @@
{% set bd="" %}
{% else %}
{% set bg="" %}
{% set bd="border-1 border-primary" %}
{% set bd="border-1 border-info" %}
{% endif %}
<input type="number" step="any" class="{{bg}} form-control text-end float-end {{bd}}" onchange="this.form.submit()" style="max-width: 120px;"
id="{{el.varname}}" name="{{el.varname}}" value="{{ finance[el.varname] }}" {{el.display}}>
@@ -78,6 +103,19 @@
</div>
{% endfor %}
<!-- create tabbed view for each bill type -->
<nav id="ft-nav" class="nav nav-tabs pt-3">
<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>
<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>
{% if COMP %}
<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>
{% else %}
<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 %}
</nav>
<div class="tab-content">
<div id="tab-findata" class="tab-pane">
<h5 align="center" class="mt-4">Fortnighthly Savings data:
{% if COMP %}
{# get comparison date so we can use it below in loop to know when to print it out #}
@@ -92,26 +130,45 @@
</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 ) %}
<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-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>
</font>
</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;
@@ -137,9 +194,9 @@
</div>
<div id="comp_col" class="col-auto">
<div class="input-group">
<button type="button" class="btn btn-primary me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save</button>
<button type="submit" class="disabled btn btn-primary rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button>
<select class="form-select border border-primary text-primary" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
<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 %}
@@ -151,13 +208,22 @@
</div>
</div>
</div>
</div>
</form>
<div class="row mt-4 highcharts-dark" id="container" style="width:100%; height:400px;"></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">
// make these global so we can also use them in the /save route (via modal)
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']}} )
@@ -190,6 +256,7 @@
};
document.addEventListener('DOMContentLoaded', function () {
$('#tab-but-findata').click()
// Parse the savings_data from Flask
const chartData = savingsData.map(entry => [new Date(entry[0]).getTime(), parseFloat(entry[1])]);
{% if COMP %}
@@ -231,10 +298,9 @@
});
const annotations = [];
// the al, x, offset are used to make the altenrate annotations 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
// offset is used to make the next annotation be on slightly different vertical offsets (size is based on $'s)
// HACK: start at 13, later we adjust in steps of 50s allowing 4 steps, then we go back to top
var offset=13
{% if not COMP %}
// Add annotations for changes greater than 5000
{% for a in finance['annotations'] %}
annotations.push({
@@ -247,18 +313,26 @@
yAxis: 0
},
x: -70,
{% if '-$' in a['label'] %}
y: offset,
{% else %}
y: -20,
{% endif %}
text: '{{a['label']}}'
}], labelOptions: { allowOverlap: true }
});
{% if a['y'] > 200000 %}
offset = ({{loop.index}} * 50 % 200) +50
{% else %}
offset = -100
{% endif %}
{% endfor %}
document.keep = annotations
{% endif %}
// Highcharts configuration
Highcharts.chart('container', {
Highcharts.chart('graph', {
chart: { type: 'line' },
colors: [ 'orange' ],
title: { text: 'Savings Over Time' },
xAxis: {
type: 'datetime',
@@ -274,13 +348,37 @@
}, 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' },
colors: [
'orange', // Custom color 1
'cyan', // Custom color 2
],
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
},
series: [
{ name: "Savings", data: chartData, marker: { radius: 2 } }
{% if COMP %}
,{ name: "{{COMP['vars']['name']}}", data: compChartData, marker: { radius: 2 } }
{% endif %}
]
});
{% endif %}
});
</script>
<div id="save_modal" class="modal modal-lg" tabindex="-1">
@@ -301,7 +399,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary"
<button type="button" class="btn btn-info"
onClick="
vars['name']=$('#save_name').val();
$.ajax( {
@@ -317,4 +415,3 @@
</div>
</body>
</html>