Compare commits
48 Commits
392daa1deb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b33390c3e | |||
| f309dfa947 | |||
| ce20c57d11 | |||
| d4662b9051 | |||
| 0a9a50f9a1 | |||
| 9cc907fb62 | |||
| 4bb336645a | |||
| 227e95cab7 | |||
| bf66e9fa7c | |||
| 5ce614ed28 | |||
| b6b396342f | |||
| fb2fffea7b | |||
| 252dc23364 | |||
| a75db565ee | |||
| b1614760a6 | |||
| 670a63cfd7 | |||
| 1c112e6f6b | |||
| 8274da0ce0 | |||
| 6618dd16b4 | |||
| 4594630b9e | |||
| 45d173e236 | |||
| 5ca99ca1f4 | |||
| 4389045ed5 | |||
| b69ec82510 | |||
| 2bd39ab24c | |||
| c49520af7a | |||
| 1a56f80cca | |||
| 6ae1023f6e | |||
| fc1746d749 | |||
| 489fb3ee2b | |||
| c5cfc00793 | |||
| ebac4aaf66 | |||
| 4b63b8bd44 | |||
| a0d9ac45cd | |||
| d80cffa0dd | |||
| 2459dc6ea1 | |||
| 95d792e72f | |||
| 5914f3fdd4 | |||
| c21bda8da0 | |||
| f4490e937a | |||
| e373dd0009 | |||
| 3a5b77f12d | |||
| c74383f89e | |||
| 4a7080787b | |||
| 07f2a321ec | |||
| f67ca61cc7 | |||
| 9ad5089ac5 | |||
| 17f2534056 |
4
BUGS
4
BUGS
@@ -1,4 +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
|
||||
|
||||
* UI: add bills, but growth figures dont update?
|
||||
|
||||
12
README
12
README
@@ -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:
|
||||
|
||||
|
||||
49
TODO
49
TODO
@@ -1,31 +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:
|
||||
[DONE] - calculate pragmatic min/avg/max/simple
|
||||
- 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
|
||||
|
||||
254
bills.py
254
bills.py
@@ -1,6 +1,8 @@
|
||||
from db import set_bill_type_growth, new_bill
|
||||
from db import set_bill_type_growth, new_bill, deleteFutureEstimates, get_finance_data, get_bill_data, get_bill_types, get_bill_freqs
|
||||
from calc import calc_key_dates
|
||||
from defines import END_YEAR
|
||||
import datetime
|
||||
import re
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
@@ -88,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])
|
||||
@@ -139,8 +153,9 @@ 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:
|
||||
@@ -190,16 +205,33 @@ 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:
|
||||
return
|
||||
new_estimated_bill( bill_info, yr, bill_type, amt, 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
|
||||
|
||||
# missing monthly bills, find date based on DD and put in each missing month
|
||||
@@ -207,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:]
|
||||
@@ -264,8 +298,18 @@ def get_growth_value( bt, bill_type ):
|
||||
return el['ann_growth_min']
|
||||
elif which == 'simple':
|
||||
return el['ann_growth_simple']
|
||||
else:
|
||||
elif which == 'max':
|
||||
return el['ann_growth_max']
|
||||
elif which == 'cpi':
|
||||
finance_data = get_finance_data()
|
||||
return finance_data['Inflation']
|
||||
else:
|
||||
match = re.match("flat-(\d+)", which )
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
else:
|
||||
print( f"FAILED TO GET_GROWTH_VALUE --> which={which}" )
|
||||
return 0
|
||||
|
||||
|
||||
################################################################################
|
||||
@@ -274,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']={}
|
||||
@@ -310,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]]
|
||||
|
||||
@@ -333,9 +399,67 @@ def process_bill_data(bd, bt, bf):
|
||||
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
|
||||
################################################################################
|
||||
@@ -376,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
|
||||
|
||||
@@ -409,7 +533,6 @@ 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
|
||||
@@ -447,7 +570,7 @@ def derive_ann_growth( bill_type, bill_info ):
|
||||
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 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:
|
||||
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:
|
||||
@@ -464,8 +587,97 @@ def calc_future_totals(bill_info, bill_types):
|
||||
for bt in bill_types:
|
||||
total[bt['id']]={}
|
||||
for yr in range( now_yr, END_YEAR+1):
|
||||
total[bt['id']][yr]=0
|
||||
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
|
||||
|
||||
150
calc.py
150
calc.py
@@ -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:
|
||||
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}" )
|
||||
current_savings += D_leave_after_tax
|
||||
add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
|
||||
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}" )
|
||||
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
|
||||
|
||||
|
||||
117
db.py
117
db.py
@@ -84,6 +84,12 @@ def init_db():
|
||||
FOREIGN KEY(comparison_set_id) REFERENCES comparison_set(id)
|
||||
)''')
|
||||
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS bill_freq (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name STRING,
|
||||
num_bills_per_annum INTEGER
|
||||
)''')
|
||||
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS bill_type (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
freq INTEGER,
|
||||
@@ -95,12 +101,6 @@ def init_db():
|
||||
FOREIGN KEY(freq) REFERENCES bill_freq(id)
|
||||
)''')
|
||||
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS bill_freq (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name STRING,
|
||||
num_bills_per_annum INTEGER
|
||||
)''')
|
||||
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS bill_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
bill_type INTEGER,
|
||||
@@ -116,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()
|
||||
|
||||
@@ -270,12 +299,17 @@ 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
|
||||
FROM bill_type bt, bill_data bd
|
||||
where bt.id = bd.bill_type order by bt.name, bd.bill_date desc''')
|
||||
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()
|
||||
conn.close()
|
||||
return bd
|
||||
@@ -370,6 +404,14 @@ def set_bill_type_growth( id, min_g, avg_g, max_g, simple_g ):
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def get_bill_growth_types():
|
||||
conn = connect_db(True)
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT * FROM bill_growth_types')
|
||||
growth = cur.fetchall()
|
||||
conn.close()
|
||||
return growth
|
||||
|
||||
def get_bill_ui():
|
||||
conn = connect_db(True)
|
||||
cur = conn.cursor()
|
||||
@@ -389,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
|
||||
|
||||
99
main.py
99
main.py
@@ -1,16 +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, calc_future_totals
|
||||
from bills import process_bill_data, calc_future_totals, set_bill_type_growth, recalcFutureBills
|
||||
from defines import END_YEAR
|
||||
from collections import defaultdict, Counter
|
||||
from datetime import datetime, date
|
||||
import csv
|
||||
import io
|
||||
import requests
|
||||
from disp import FP_VAR
|
||||
|
||||
app = Flask(__name__)
|
||||
@@ -25,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:
|
||||
@@ -100,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():
|
||||
@@ -110,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'],
|
||||
@@ -139,18 +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()
|
||||
bill_info=process_bill_data(bill_data, bill_types, bill_freqs)
|
||||
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)
|
||||
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, this_year=datetime.today().year, END_YEAR=END_YEAR, total=total )
|
||||
# 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():
|
||||
@@ -168,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'])
|
||||
@@ -201,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__':
|
||||
|
||||
@@ -8,4 +8,4 @@ pysqlite3
|
||||
Werkzeug
|
||||
flask-compress
|
||||
gunicorn
|
||||
|
||||
requests
|
||||
|
||||
@@ -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-7">
|
||||
<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> {{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,68 +52,95 @@
|
||||
</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">Name</ ></div>
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Frequency</ ></div>
|
||||
<div class="px-0 col-4"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Annual Growth Est (min/avg/max/simple)</ ></div>
|
||||
<div class="px-0 col-1"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">{{this_year}} Total</ ></div>
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></div>
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Frequency</ ></div>
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold
|
||||
bg-body-tertiary rounded-0">Annual Growth</ ></div>
|
||||
{% for yr in range( 2025, 2032 ) %}
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0
|
||||
fw-bold bg-body-tertiary rounded-0">{{yr}} Total</ ></div>
|
||||
{% endfor %}
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
|
||||
{# spacer to get header line right now we don't use forced col widths #}
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0"> </ ></div>
|
||||
</div>
|
||||
{% for bt in bill_types %}
|
||||
<div class="row">
|
||||
<div class="px-0 col-2"><input type="text" class="bill-type-{{bt.id}} form-control text-center" id="bill-type-name-{{bt.id}}" value="{{ bt.name }}" disabled> </div>
|
||||
<div class="px-0 col-1"><input type="text" class="bill-type-{{bt.id}} form-control text-center" id="bill-type-name-{{bt.id}}" value="{{ bt.name }}" disabled> </div>
|
||||
<!-- bind Enter to save this bill-type -->
|
||||
<script>$("#bill-type-name-{{bt.id}}").keyup(function(event){ if(event.which == 13){ $('#bill-type-save-{{bt.id}}').click(); } event.preventDefault(); });</script>
|
||||
<div class="px-0 col-2"><select id="bill-type-freq-{{bt.id}}" class="bill-type-{{bt.id}} form-select text-center" disabled>
|
||||
<div class="px-0 col-1"><select id="bill-type-freq-{{bt.id}}" class="bill-type-{{bt.id}} form-select text-center" disabled>
|
||||
{% for bf in bill_freqs %}
|
||||
<option value={{bf.id}}>{{bf.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<script>$('#bill-type-freq-{{bt.id}}').val( {{bt.freq}} );</script>
|
||||
<div class="px-0 col-4">
|
||||
<div class="px-0 col">
|
||||
<div class="btn-group w-100" role="group">
|
||||
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="min-{{bt.id}}" autocomplete="off"
|
||||
onChange="UseGrowth({{bt.id}}, 'min')" {% if bt.which_growth == 'min' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="min-{{bt.id}}" style="width: 6ch;">
|
||||
{% if bt.ann_growth_min> 0 and bt.ann_growth_min < 10 %} {% endif %}
|
||||
{{'%5.2f'|format(bt.ann_growth_min)}}
|
||||
</label>
|
||||
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="avg-{{bt.id}}" autocomplete="off"
|
||||
onChange="UseGrowth({{bt.id}}, 'avg')" {% if bt.which_growth == 'avg' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="avg-{{bt.id}}" style="width: 6ch;">
|
||||
{% if bt.ann_growth_avg < 10 %} {% endif %}
|
||||
{{'%5.2f'|format(bt.ann_growth_avg)}}
|
||||
</label>
|
||||
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="max-{{bt.id}}" autocomplete="off"
|
||||
onChange="UseGrowth({{bt.id}}, 'max')" {% if bt.which_growth == 'max' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="max-{{bt.id}}" style="width: 6ch;">
|
||||
{% if bt.ann_growth_max < 10 %} {% endif %}
|
||||
{{'%5.2f'|format(bt.ann_growth_max)}}
|
||||
</label>
|
||||
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="simple-{{bt.id}}" autocomplete="off"
|
||||
onChange="UseGrowth({{bt.id}}, 'simple')" {% if bt.which_growth == 'simple' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary font-monospace d-inline-block text-end" for="simple-{{bt.id}}" style="width: 6ch;">
|
||||
{% if bt.ann_growth_simple < 10 %} {% endif %}
|
||||
{{'%5.2f'|format(bt.ann_growth_simple)}}
|
||||
</label>
|
||||
<select id="{{bt.id}}_growth" class="form-select col" onChange="UseGrowth({{bt.id}})">
|
||||
{% for gt in growth %}
|
||||
{% if gt.name == 'Min' %}
|
||||
<option value='min'
|
||||
{% if bt.which_growth == 'min' %}selected{% endif %}
|
||||
>{{'%.2f'|format(bt.ann_growth_min)}}% {{gt.name}}</option>
|
||||
{% elif gt.name == 'Avg' %}
|
||||
<option value='avg'
|
||||
{% if bt.which_growth == 'avg' %}selected{% endif %}
|
||||
>{{'%.2f'|format(bt.ann_growth_avg)}}% {{gt.name}}</option>
|
||||
{% elif gt.name == 'Max' %}
|
||||
<option value='max'
|
||||
{% if bt.which_growth == 'max' %}selected{% endif %}
|
||||
>{{'%.2f'|format(bt.ann_growth_max)}}% {{gt.name}}</option>
|
||||
{% elif gt.name == 'Simple' %}
|
||||
<option value='simple'
|
||||
{% if bt.which_growth == 'simple' %}selected{% endif %}
|
||||
>{{'%.2f'|format(bt.ann_growth_simple)}}% {{gt.name}}</option>
|
||||
{% elif gt.name == 'CPI' %}
|
||||
<option value='cpi'
|
||||
{% if bt.which_growth == 'cpi' %}selected{% endif %}
|
||||
>{{'%.2f'|format(cpi)}}% {{gt.name}}</option>
|
||||
{% else %}
|
||||
<option value='flat-{{gt.value}}'
|
||||
{% if bt.which_growth == 'flat-'+gt.value|string %}selected{% endif %}
|
||||
>{{'%.2f'|format(gt.value)}}% {{gt.name}}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-0 col-1"><input type="text" class="bill-type-total-{{bt.id}} form-control text-center" id="bill-type-total-{{bt.id}}" value="${{'%.2f'|format(total[bt.id][this_year])}}" disabled> </div>
|
||||
<button id="bill-type-chg-{{bt.id}}" class="px-0 col-1 btn btn-success bg-success-subtle text-success" onClick="StartUpdateBillType( {{bt.id}} )"><span class="bi bi-pencil-square"> Change</button>
|
||||
<button id="bill-type-del-{{bt.id}}" class="px-0 col-1 btn btn-danger bg-danger-subtle text-danger" onClick="DelBillType({{bt.id}})"><span class="bi bi-trash3"> Delete</button>
|
||||
<button id="bill-type-save-{{bt.id}}" class="px-0 col-1 btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBillType( {{bt.id}} )"><spam class="bi bi-floppy"> Save</button>
|
||||
<button id="bill-type-canc-{{bt.id}}" class="px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelUpdateBillType({{bt.id}}, '{{bt.name}}')"><span class="bi bi-x"> Cancel</button>
|
||||
{% for yr in range( 2025, 2032 ) %}
|
||||
<div class="px-0 col"><input type="text" class="bill-type-total-{{bt.id}} form-control text-center" id="bill-type-total-{{bt.id}}" value="${{'%.2f'|format(total[bt.id][yr])}}" disabled> </div>
|
||||
{% endfor %}
|
||||
<button id="bill-type-chg-{{bt.id}}" class="px-0 col btn btn-success bg-success-subtle text-success" onClick="StartUpdateBillType( {{bt.id}} )"><span class="bi bi-pencil-square"> Change</button>
|
||||
<button id="bill-type-del-{{bt.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger" onClick="DelBillType({{bt.id}})"><span class="bi bi-trash3"> Delete</button>
|
||||
<button id="bill-type-save-{{bt.id}}" class="px-0 col btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBillType( {{bt.id}} )"><spam class="bi bi-floppy"> Save</button>
|
||||
<button id="bill-type-canc-{{bt.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelUpdateBillType({{bt.id}}, '{{bt.name}}')"><span class="bi bi-x"> Cancel</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for yr in range( this_year, END_YEAR) %}
|
||||
<div class="row">
|
||||
<div class="px-0 col"></div>
|
||||
<div class="px-0 col"></div>
|
||||
<div class="px-0 col"><input type="text" class="form-control text-end text-primary fs-5" value="TOTAL:"></div>
|
||||
{% for yr in range( this_year, END_YEAR+1) %}
|
||||
{% set tot=namespace( sum=0 ) %}
|
||||
{% for bt in bill_types %}
|
||||
{% if bt.id in total %}
|
||||
{% set tot.sum = tot.sum + total[bt.id][yr] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="px-0 col"><input type="text" class="form-control text-center text-primary bg-dark fs-5" value="${{'%.2f'|format(tot.sum)}}" disabled></div>
|
||||
{#
|
||||
{% set markup="h5" %}
|
||||
{% if yr == this_year %}
|
||||
{% set markup="h4 pt-4" %}
|
||||
@@ -107,25 +153,44 @@
|
||||
${{'%.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-5">
|
||||
<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 -->
|
||||
@@ -149,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-4"><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" %}
|
||||
@@ -163,16 +229,29 @@
|
||||
<div class="row">
|
||||
{% 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>
|
||||
<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>
|
||||
<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>
|
||||
<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-2 btn btn-success bg-success-subtle text-success" onClick="StartUpdateBill( {{bd.id}} )"><span class="bi bi-pencil-square"> Change</button>
|
||||
<button id="bill-data-del-{{bd.id}}" class="px-0 col-2 btn btn-danger bg-danger-subtle text-danger" onClick="DeleteBill( {{bd.id }} )"><span class="bi bi-trash3"> Delete
|
||||
<button id="bill-data-save-{{bd.id}}" class="px-0 col-2 btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBill( {{bd.id}} )"><span class="bi bi-floppy"> Save</button>
|
||||
<button id="bill-data-canc-{{bd.id}}" class="px-0 col-2 btn btn-danger bg-danger-subtle text-danger d-none"
|
||||
onClick="CancelUpdateBill({{bd.id}}, '{{bd.name}}', '{{bd.bill_date}}', '{{bd.amount}}')"> <span class="bi bi-x"> Cancel</button>
|
||||
</button>
|
||||
<button id="bill-data-chg-{{bd.id}}" class="px-0 col btn btn-success bg-success-subtle text-success" onClick="StartUpdateBill( {{bd.id}} )"><span class="bi bi-pencil-square"> Change</button>
|
||||
<button id="bill-data-del-{{bd.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger" onClick="DeleteBill( {{bd.id }} )"><span class="bi bi-trash3"> Delete
|
||||
<button id="bill-data-save-{{bd.id}}" class="px-0 col btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBill( {{bd.id}} )"><span class="bi bi-floppy"> Save</button>
|
||||
<button id="bill-data-canc-{{bd.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger d-none"
|
||||
onClick="CancelUpdateBill({{bd.id}}, '{{bd.name}}', '{{bd.bill_date}}', '{{bd.amount}}')"> <span class="bi bi-x"> Cancel</button>
|
||||
</button>
|
||||
{% else %}
|
||||
<div class="px-0 col"></div>
|
||||
<div class="px-0 col"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -206,9 +285,28 @@
|
||||
|
||||
function NewBill()
|
||||
{
|
||||
$.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() } ),
|
||||
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( {
|
||||
'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()
|
||||
@@ -248,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' } } )
|
||||
@@ -345,10 +443,11 @@
|
||||
data: JSON.stringify( { 'id': id } ), success: function() { window.location='bills' } } )
|
||||
}
|
||||
|
||||
function UseGrowth( bt, which )
|
||||
function UseGrowth( bt_id )
|
||||
{
|
||||
which = $('#'+bt_id+ '_growth option:selected').val()
|
||||
$.ajax( { type: 'POST', url: '/usegrowth', contentType: 'application/json',
|
||||
data: JSON.stringify( { 'bill_type': bt, 'which_growth': which } ), success: function() { window.location='bills' } } )
|
||||
data: JSON.stringify( { 'bill_type': bt_id, 'which_growth': which } ), success: function() { window.location='bills' } } )
|
||||
}
|
||||
|
||||
function SaveTab( last_tab )
|
||||
@@ -380,7 +479,47 @@
|
||||
// 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
66
templates/cset.html
Normal 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>
|
||||
|
||||
@@ -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,86 +103,127 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<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 #}
|
||||
{% set comp_yr=COMP['date'][:4] %}
|
||||
{% set comp_mon=COMP['date'][5:7] %}
|
||||
{% set comp_day=COMP['date'][8:10 ] %}
|
||||
{% set comp_done=namespace( val=0 ) %}
|
||||
{% else %}
|
||||
{# we dont need to do a comparison, so consider it done before we begin #}
|
||||
{% set comp_done=namespace( val=1 ) %}
|
||||
{% endif %}
|
||||
</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">2025</div>
|
||||
|
||||
{# inside started if below, we add blank lines to the start of the year so the dates line up #}
|
||||
{% for _ in range( 0, padding ) %}
|
||||
<br>
|
||||
{% endfor %}
|
||||
|
||||
{% for date, dollars in savings %}
|
||||
{% set yr=date[:4] %}
|
||||
{% set mon=date[5:7] %}
|
||||
{% set day=date[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 %}
|
||||
<font class="text-secondary">
|
||||
{{ date }}:
|
||||
{{ '$%0.2f' % dollars|float }}<br>
|
||||
</font>
|
||||
{% 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'] }}:
|
||||
{{ '$%0.2f' % COMP['amount']|float }}<br>
|
||||
</font>
|
||||
{% set comp_done.val=1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if depletion_date %}
|
||||
<div class="alert alert-danger">Run out of $'s:<br>{{depletion_date}}</div>
|
||||
<!-- 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 %}
|
||||
<div class="alert alert-success">Super kicks in!!!</div>
|
||||
<button class="nav-link" id="tab-but-compgraph" data-bs-toggle="tab" data-bs-target="#tab-compgraph" type="button" role="tab" aria-controls="tab3" aria-selected="true" disabled>Comparison graph</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="alert alert-warning">
|
||||
<h6 class="alert-heading">SUMMARY/BUDGET</h6>
|
||||
{% for label, value in BUDGET %}
|
||||
<div>
|
||||
{{label}} {{value}}
|
||||
</div>
|
||||
</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 #}
|
||||
{% set comp_yr=COMP['date'][:4] %}
|
||||
{% set comp_mon=COMP['date'][5:7] %}
|
||||
{% set comp_day=COMP['date'][8:10 ] %}
|
||||
{% set comp_done=namespace( val=0 ) %}
|
||||
{% else %}
|
||||
{# we dont need to do a comparison, so consider it done before we begin #}
|
||||
{% set comp_done=namespace( val=1 ) %}
|
||||
{% endif %}
|
||||
</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">{{savings[0][0][:4]}}</div>
|
||||
|
||||
{# inside started if below, we add blank lines to the start of the year so the dates line up #}
|
||||
{% for _ in range( 0, padding ) %}
|
||||
<br>
|
||||
{% endfor %}
|
||||
</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()">
|
||||
{% for el in finance['COMP_SETS'] %}
|
||||
<option value="{{el[0]}}">{{el[1]}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if COMP %}
|
||||
<div style="display:none" id="comp_alert" class="alert alert-info mt-2">Note: {{ '$%0.2f' % COMP['amount']|float }} is the final value of the compared to data (with a buffer of: {{ '$%0.2f' % COMP['buffer']|float }}). Hover over blue labels above to see what compared to values differed</div>
|
||||
|
||||
{% 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 }}:
|
||||
{{ '$%0.2f' % dollars|float }}<br>
|
||||
</label>
|
||||
</font><br>
|
||||
{% if comp_done.val == 0 and yr == comp_yr and mon == comp_mon and day|int >= comp_day|int %}
|
||||
<font class="text-info">
|
||||
{{ COMP['date'] }}:
|
||||
{{ '$%0.2f' % COMP['amount']|float }}<br>
|
||||
</font>
|
||||
{% set comp_done.val=1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if depletion_date %}
|
||||
<div class="alert alert-danger">Run out of $'s:<br>{{depletion_date}}</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">Super kicks in!!!</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="alert alert-warning">
|
||||
<h6 class="alert-heading">SUMMARY/BUDGET</h6>
|
||||
{% for label, value in BUDGET %}
|
||||
<div>
|
||||
{{label}} {{value}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="comp_col" class="col-auto">
|
||||
<div class="input-group">
|
||||
<button type="button" class="btn btn-info me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save Comparison set</button>
|
||||
<button type="submit" class="disabled btn btn-info rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button>
|
||||
<select class="form-select border border-info text-info" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
|
||||
{% for el in finance['COMP_SETS'] %}
|
||||
<option value="{{el[0]}}">{{el[1]}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if COMP %}
|
||||
<div style="display:none" id="comp_alert" class="alert alert-info mt-2">Note: {{ '$%0.2f' % COMP['amount']|float }} is the final value of the compared to data (with a buffer of: {{ '$%0.2f' % COMP['buffer']|float }}). Hover over blue labels above to see what compared to values differed</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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,33 +298,62 @@
|
||||
});
|
||||
|
||||
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({
|
||||
labels: [{
|
||||
point: {
|
||||
x: {{a['x']}},
|
||||
y: {{a['y']}},
|
||||
crop: true,
|
||||
xAxis: 0,
|
||||
yAxis: 0
|
||||
},
|
||||
x: -70,
|
||||
// Add annotations for changes greater than 5000
|
||||
{% for a in finance['annotations'] %}
|
||||
annotations.push({
|
||||
labels: [{
|
||||
point: {
|
||||
x: {{a['x']}},
|
||||
y: {{a['y']}},
|
||||
crop: true,
|
||||
xAxis: 0,
|
||||
yAxis: 0
|
||||
},
|
||||
x: -70,
|
||||
{% if '-$' in a['label'] %}
|
||||
y: offset,
|
||||
text: '{{a['label']}}'
|
||||
}], labelOptions: { allowOverlap: true }
|
||||
});
|
||||
{% else %}
|
||||
y: -20,
|
||||
{% endif %}
|
||||
text: '{{a['label']}}'
|
||||
}], labelOptions: { allowOverlap: true }
|
||||
});
|
||||
{% if a['y'] > 200000 %}
|
||||
offset = ({{loop.index}} * 50 % 200) +50
|
||||
{% endfor %}
|
||||
document.keep = annotations
|
||||
{% endif %}
|
||||
{% else %}
|
||||
offset = -100
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
document.keep = annotations
|
||||
|
||||
// Highcharts configuration
|
||||
Highcharts.chart('container', {
|
||||
Highcharts.chart('graph', {
|
||||
chart: { type: 'line' },
|
||||
colors: [ 'orange' ],
|
||||
title: { text: 'Savings Over Time' },
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
title: { text: 'Date' },
|
||||
plotBands: plotBands // Alternating background for years
|
||||
},
|
||||
yAxis: { title: { text: 'Amount ($)' } },
|
||||
tooltip: {
|
||||
pointFormatter: function ()
|
||||
{
|
||||
if( this.series.symbol == 'circle' ) { s='\u25CF' } else { s='\u2B25' }
|
||||
return '<span style="color:' + this.point.color + '">' + s + '</span> <b>' + this.point.y + ':</b> ' + this.series.name + '<br>'
|
||||
}, shared:true
|
||||
},
|
||||
annotations: annotations, // Add annotations
|
||||
series: [ { name: "Savings", data: chartData, marker: { radius: 2 } } ]
|
||||
});
|
||||
|
||||
{% if COMP %}
|
||||
// Highcharts configuration
|
||||
Highcharts.chart('graph-comp', {
|
||||
chart: { type: 'line' },
|
||||
colors: [
|
||||
'orange', // Custom color 1
|
||||
@@ -271,20 +367,18 @@
|
||||
},
|
||||
yAxis: { title: { text: 'Amount ($)' } },
|
||||
tooltip: {
|
||||
pointFormatter: function ()
|
||||
{
|
||||
if( this.series.symbol == 'circle' ) { s='\u25CF' } else { s='\u2B25' }
|
||||
return '<span style="color:' + this.point.color + '">' + s + '</span> <b>' + this.point.y + ':</b> ' + this.series.name + '<br>'
|
||||
}, shared:true
|
||||
},
|
||||
annotations: annotations, // Add annotations
|
||||
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">
|
||||
@@ -305,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( {
|
||||
@@ -321,4 +415,3 @@
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user