Compare commits
70 Commits
89d58e4cd3
...
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 | |||
| 392daa1deb | |||
| 2937866617 | |||
| 0ab0a112e4 | |||
| 4c96a9b576 | |||
| 7ad767759f | |||
| 24c581a35a | |||
| 3749c01e93 | |||
| c41048ab82 | |||
| 7422321227 | |||
| 338b63aa06 | |||
| de32bdc7ff | |||
| 4a2dd4d2da | |||
| aee8916471 | |||
| 8f69023ffd | |||
| d2bf472845 | |||
| e84faffd79 | |||
| 2bdd1348b8 | |||
| 91ebc227b6 | |||
| 89fe874c5c | |||
| dda3a3e3fe | |||
| 706aee6947 | |||
| 742911ec1b |
4
BUGS
4
BUGS
@@ -0,0 +1,4 @@
|
||||
* kayo bills are wrong in between normal bills
|
||||
|
||||
* added an electricity bill by accident for 2018, that kills lots :(
|
||||
- something to do with missing year of data in quarterly bills - still an issue
|
||||
|
||||
12
README
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:
|
||||
|
||||
|
||||
69
TODO
69
TODO
@@ -1,51 +1,24 @@
|
||||
bills html, and growth types are very lame... could I do something more like:
|
||||
{% for gt in growth %}
|
||||
{% if gt.name == 'Min' %}
|
||||
<option value='min'
|
||||
{% if bt.which_growth == gt.name %}selected{% endif %}
|
||||
>{{'%.2f'|format(bt.ann_growth_min)}}% {{gt.name}}</option>
|
||||
|
||||
|
||||
CALC:
|
||||
* if I quit at different times of the financial year, technically the amount I earn will be taxed
|
||||
differently, but hard to calc - slides around with tax brackets in future
|
||||
|
||||
UI:
|
||||
* to allow <yr> total on bill summary, need to re-work annual growth to a drop-down
|
||||
- [DONE] mock-up
|
||||
- need to change 'which growth' to accommodate new growth models (cpi, override*)
|
||||
- [DONE] do this in DB with new table - then don't do crazy pattern matching/making up overrides in the UI code
|
||||
- get UI to use bill_growth_types table
|
||||
- get UseGrowth() to use bill_growth_types table
|
||||
|
||||
For bills:
|
||||
* gas bills are a mess and more than 4 per year... *SIGH* try this:
|
||||
- if len(bills) > num_ann_bills (effectively too many bills for what we expect)
|
||||
- normalise by looping over *EVERY* year of bills
|
||||
- for each bill in yr:
|
||||
- pb=find_prev_bill( bill )...
|
||||
- calc days between pb and bill to get daily cost
|
||||
- work out qtr() and take # days from bill in Qtr X and add it to Q[X]
|
||||
- if # days i X+1 add those to Q[X+1]
|
||||
- then need to be careful when working out totals/growth to use Q[...] not bill['year']
|
||||
* growth for internet/monthly is a bit skewed.... Really think min/avg/max might need to be smarter
|
||||
- at least max, its 114-134 BUT, not in 1 year, really that changed from 2022 to 2025, so 3 years... (or max = 18% over 3 years, or 6%)
|
||||
-- so ann_growth_avg or max needs to factor in years of same prices
|
||||
[DONE] -- Pragmatical growth before I bonkers:
|
||||
[DONE] - monthly, prob. just flat by default (simple 1 or 12 bills for the year each year and I can get a growth rate from that)
|
||||
[DONE] - and apply monthly growth - annually 12 + months from last bill each year
|
||||
[DONE] - quarterly - should be able to take last qtr-1 ... qtr-4 and then grow them all by growth
|
||||
[DONE]- annual easy
|
||||
* once auto-filled bills exist:
|
||||
[DONE]- calc growth
|
||||
[DONE] - project out to I am 60 (A/Q/M) - A/Q done, M to go
|
||||
[DONE] - probably need to allow a toggle to: allow show manual, show auto-filled past, show auto-filled future, show all
|
||||
- remove bills from Living_Expenses (carefully - but by hand)
|
||||
- fold future bills into calc so they are taken out in a more time and growth appropriate way
|
||||
- inflation can then be put to a more realistic quarterly figure
|
||||
|
||||
LONGER/HARDER:
|
||||
* need to work out 'first bill' and 'last bill' to auto-fill missing bills based on
|
||||
-- all missing bills follow varying growth models & its by choice -- therefore I need this in DB
|
||||
- ANN: flat, min, avg, max, manual
|
||||
- QTR: flat, qtrly seasonal: min/avg/max/manual, qtrly simple: min/avg/max/manual, annual: min/avg/max/manual
|
||||
- MON: flat, monthly: min/avg/max/manual, annual: min/avg/max/manual
|
||||
-- use this logic to add missing bills (date):
|
||||
-- ANN: missing annual bill, find date based on MM-DD and add new year - given we start with first_bill anyway, will only be used for future bill predictions
|
||||
-- QTR: missing quarterly bill, find date based on MM-DD and ??? - can have missing bilsl in first year
|
||||
-- MON: missing monthly bills, find date based on DD and put in each missing month
|
||||
-- use this logic to add missing bills (amount):
|
||||
-- ANN: future only, so add ann_growth (based on drop-down) for each future year
|
||||
-- QTR: add growth (based on drop-down) for each future year
|
||||
-- MON: add growth (based on drop-down) for each future year
|
||||
|
||||
MUCH LONGER/HARDER:
|
||||
potentially for each bill_type, there are unique extras - e.g. THIS feels too hard:
|
||||
water has 2 fixed charges (water & sewerage) and then a consumption charge (per ML)
|
||||
elec has 1 fixe charge (daily) and then consumption (per kwh) BUT, also daily solar rate
|
||||
gas has fixed charge and consumption
|
||||
internet, kayo is monthly fixed (but can go up sometimes)
|
||||
eweka is annual fixed
|
||||
phone is messier again.
|
||||
* might need to be able to mark a specific bill as an outlier:
|
||||
- so we ignore the data somehow (think Gas is messing with my bills)
|
||||
- and even electricity, water, etc. for when we were away in Europe but mostly gas/elec
|
||||
|
||||
329
bills.py
329
bills.py
@@ -1,6 +1,8 @@
|
||||
from db import get_bill_data, get_bill_types, get_bill_freqs, set_bill_type_growth, new_bill
|
||||
from db import set_bill_type_growth, new_bill, deleteFutureEstimates, get_finance_data, get_bill_data, get_bill_types, get_bill_freqs
|
||||
from calc import calc_key_dates
|
||||
from defines import END_YEAR
|
||||
import datetime
|
||||
import re
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
@@ -51,6 +53,8 @@ def allocate_by_quarter( bill_info, bill_type, yr, prev_bill, bill):
|
||||
bill_info[bill_type]['qtr'][q_start.year] = {}
|
||||
for i in range(1,5):
|
||||
bill_info[bill_type]['qtr'][q_start.year][i]=0
|
||||
if q not in bill_info[bill_type]['qtr'][q_start.year]:
|
||||
bill_info[bill_type]['qtr'][q_start.year][q]=0
|
||||
bill_info[bill_type]['qtr'][q_start.year][q] += days*cost_per_day
|
||||
# next quarter
|
||||
cur = q_end + timedelta(days=1)
|
||||
@@ -86,6 +90,18 @@ def find_next_bill( bill_type, bill_info, bill_date ):
|
||||
return None
|
||||
|
||||
|
||||
# see if this bill exists (used to prevent adding more than once in future
|
||||
# estimated bills)
|
||||
def find_this_bill( bill_type, bill_info, bill_date ):
|
||||
yr = int(bill_date[:4])
|
||||
if not bill_type in bill_info or not 'year' in bill_info[bill_type] or not yr in bill_info[bill_type]['year']:
|
||||
return None
|
||||
for b in bill_info[bill_type]['year'][yr]:
|
||||
if bill_type == b['bill_type'] and bill_date == b['bill_date']:
|
||||
return b
|
||||
return None
|
||||
|
||||
|
||||
# find the bill just before the date given
|
||||
def find_previous_bill( bill_type, bill_info, bill_date ):
|
||||
wanted_year = int(bill_date[:4])
|
||||
@@ -108,6 +124,10 @@ def find_previous_bill( bill_type, bill_info, bill_date ):
|
||||
# okay, we have the previous billing year, and we wanted one for a year in the future,
|
||||
# just return the last one in this year as its the most recent
|
||||
if wanted_year > yr:
|
||||
# small chance of future bills having estimates and reals (kayo did this)
|
||||
for tmp in bill_info[bill_type]['year'][yr]:
|
||||
if tmp['estimated'] == 0:
|
||||
return tmp
|
||||
return bill_info[bill_type]['year'][yr][0]
|
||||
else:
|
||||
# lets go through the newest to oldest of these bills
|
||||
@@ -133,20 +153,24 @@ def new_estimated_bill( bill_info, yr, bill_type, amt, new_date ):
|
||||
bill={}
|
||||
bill['bill_date']=new_date
|
||||
bill['amount']=amt
|
||||
bill['bill_type']=bill_type
|
||||
bill['estimated']=1
|
||||
# need this for find_previous_bill to work but only need the above 3 fields
|
||||
# need to insert(0,) to add this "newest" bill to start of the data for {yr} so that find_previous_bill can work - only need the above 3 fields
|
||||
bill_info[bill_type]['year'][yr].insert(0,bill)
|
||||
|
||||
if bill_info[bill_type]['num_ann_bills'] == 4:
|
||||
q = qtr( new_date )
|
||||
# new bill in this qtr of this year, so set arrays up
|
||||
if yr not in bill_info[bill_type]['qtr']:
|
||||
bill_info[bill_type]['qtr'][yr]={}
|
||||
pb = find_previous_bill( bill_type, bill_info, new_date )
|
||||
if pb['estimated'] == 0:
|
||||
print( f" FIXFIXFIX - have a prev real bill={pb['bill_date']} & this is first est - likely need to better apportion this bill into the quarters" )
|
||||
allocate_by_quarter( bill_info, bill_type, yr, pb, bill )
|
||||
|
||||
bill_info[bill_type]['qtr'][yr][q]=amt
|
||||
else:
|
||||
if not q in bill_info[bill_type]['qtr'][yr]:
|
||||
# first in this year, just init it...
|
||||
bill_info[bill_type]['qtr'][yr][q]=0
|
||||
bill_info[bill_type]['qtr'][yr][q]+=amt
|
||||
return
|
||||
|
||||
|
||||
@@ -155,12 +179,16 @@ def new_estimated_bill( bill_info, yr, bill_type, amt, new_date ):
|
||||
# NOTE: only ever called when there is a need to add a new bill
|
||||
def add_missing_annual_bill_in_yr( bill_type, bill_info, yr ):
|
||||
mm_dd = bill_info[bill_type]['last_bill']['bill_date'][5:]
|
||||
new_date= f'{yr}-{mm_dd}'
|
||||
pb=find_previous_bill( bill_type, bill_info, new_date )
|
||||
if pb:
|
||||
amt = pb['amount']
|
||||
else:
|
||||
amt = bill_info[bill_type]['last_bill']['amount']
|
||||
# okay the missing bill is before the first bill...
|
||||
for i in range( bill_info[bill_type]['last_bill_year'], yr ):
|
||||
amt += amt * bill_info[bill_type]['growth']/100
|
||||
|
||||
new_estimated_bill( bill_info, yr, bill_type, amt, f'{yr}-{mm_dd}' )
|
||||
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
|
||||
return
|
||||
|
||||
# missing quarterly bill, find date based on MM-DD and ??? - can have missing bilsl in first year
|
||||
@@ -177,15 +205,32 @@ def add_missing_quarter_bills_in_yr( bill_type, bill_info, yr ):
|
||||
else:
|
||||
r=range(1,5)
|
||||
for q in r:
|
||||
# amt is total of last year's qtr bill proportion
|
||||
amt = bill_info[bill_type]['qtr'][yr-1][q]*(1+bill_info[bill_type]['growth']/100)
|
||||
# just make new bills first of last month of a qtr (good as any date for GAS, they move anyway)
|
||||
new_date = f'{yr}-{q*3:02d}-01'
|
||||
# SANITY CHECK: we might be adding a bill estimate we already have (due to stupid gas bills /qtrly code)
|
||||
if yr in bill_info[bill_type]['year']:
|
||||
for b in bill_info[bill_type]['year'][yr]:
|
||||
if b['bill_date'] == new_date:
|
||||
if 'forced' in bill_info[bill_type]['freq']:
|
||||
actually_add_estimated_new_quarter_bill_forced(bill_type, bill_info, yr, q)
|
||||
else:
|
||||
actually_add_estimated_new_quarter_bill(bill_type, bill_info, yr, q)
|
||||
return
|
||||
|
||||
################################################################################
|
||||
# func take a qtr in a year, finds equiv from previous year, calcs new based on
|
||||
# it (same 'day' with amt * growth)
|
||||
################################################################################
|
||||
def actually_add_estimated_new_quarter_bill( bill_type, bill_info, yr, q ):
|
||||
|
||||
# amt is total of last year's qtr bill (NOTE: use 4-q, bills are in desc order)
|
||||
last_yrs_bill_in_this_q = bill_info[bill_type]['year'][yr-1][4-q]
|
||||
amt = last_yrs_bill_in_this_q['amount']*(1+bill_info[bill_type]['growth']/100)
|
||||
|
||||
# make new qtr bill same 'day' (mm-dd) as last year, just chg (yr)
|
||||
mmdd=last_yrs_bill_in_this_q['bill_date'][5:]
|
||||
new_date = f'{yr}-{mmdd}'
|
||||
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
|
||||
return
|
||||
|
||||
def actually_add_estimated_new_quarter_bill_forced( bill_type, bill_info, yr, q ):
|
||||
last_yrs_qtr_amount = bill_info[bill_type]['qtr'][yr-1][q]
|
||||
amt=last_yrs_qtr_amount*(1+bill_info[bill_type]['growth']/100)
|
||||
new_date = f'{yr}-{q*3:02d}-01'
|
||||
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
|
||||
return
|
||||
|
||||
@@ -194,6 +239,8 @@ def add_missing_quarter_bills_in_yr( bill_type, bill_info, yr ):
|
||||
# NOTE: ALWAYS called for first year - don't always add bills/see below
|
||||
def add_missing_monthly_bills_in_yr( bill_type, bill_info, yr ):
|
||||
|
||||
print( f"add_missing_monthly_bills_in_yr for ( bt={bill_type} -- yr={yr} )" )
|
||||
|
||||
# start date arithmetic from first bill (this is possibly an issue if monthly is not
|
||||
# really perfectly the same each month, but its only for an estimate so should be ok
|
||||
dd = bill_info[bill_type]['first_bill']['bill_date'][8:]
|
||||
@@ -210,12 +257,13 @@ def add_missing_monthly_bills_in_yr( bill_type, bill_info, yr ):
|
||||
for i in range( start_m+1, 13 ):
|
||||
bill_found=False
|
||||
new_date = f'{yr}-{i:02d}-{dd}'
|
||||
new_date_yymm=f'{yr}-{i:02d}'
|
||||
if yr in bill_info[bill_type]['year']:
|
||||
for b in bill_info[bill_type]['year'][yr]:
|
||||
# this bill exists, skip adding it (this occurs when called to
|
||||
# add bills as there are < 12 bills in first_year, BUT, we
|
||||
# don't fill before first_bill so the < 12 ALWAYS triggers
|
||||
if str(b['bill_date']) == new_date:
|
||||
if new_date_yymm in str(b['bill_date']):
|
||||
bill_found=True
|
||||
break
|
||||
if not bill_found:
|
||||
@@ -248,8 +296,20 @@ def get_growth_value( bt, bill_type ):
|
||||
return el['ann_growth_avg']
|
||||
elif which == 'min':
|
||||
return el['ann_growth_min']
|
||||
else:
|
||||
elif which == 'simple':
|
||||
return el['ann_growth_simple']
|
||||
elif which == 'max':
|
||||
return el['ann_growth_max']
|
||||
elif which == 'cpi':
|
||||
finance_data = get_finance_data()
|
||||
return finance_data['Inflation']
|
||||
else:
|
||||
match = re.match("flat-(\d+)", which )
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
else:
|
||||
print( f"FAILED TO GET_GROWTH_VALUE --> which={which}" )
|
||||
return 0
|
||||
|
||||
|
||||
################################################################################
|
||||
@@ -258,24 +318,44 @@ def get_growth_value( bt, bill_type ):
|
||||
# and I didn't want to input 12 of them at the same price), and it always
|
||||
# occurs for future bills
|
||||
################################################################################
|
||||
def process_bill_data(bd, bt, bf):
|
||||
def process_bill_data(bd, bt, bf, key_dates):
|
||||
# this maps a bill id to a freq id (e.g. bill #34 - has a frequency of #2 (which might be quarterly)
|
||||
bt_id_freq = {row["id"]: row["freq"] for row in bt}
|
||||
bt_id_ann_growth_avg = {row["id"]: row["ann_growth_avg"] for row in bt}
|
||||
bt_id_name = {row["id"]: row["name"] for row in bt}
|
||||
|
||||
# this maps freq to bills per annum (e.g. id=2 to 4 bills per annum)
|
||||
bf_id_num = {row["id"]: row["num_bills_per_annum"] for row in bf}
|
||||
# and allows me a way to see if the bill is quarterly but also fixed or seasonal
|
||||
bf_id_name = {row["id"]: row["name"] for row in bf}
|
||||
|
||||
# want to proces all bill data into easier to maniuplate structure, so make
|
||||
# a bill_info[bill_id] with first_bill, last_bill, [yr] with matching bills to process
|
||||
bill_info={}
|
||||
future_car_bills=[]
|
||||
future_D_quit_bills=[]
|
||||
|
||||
for bill in bd:
|
||||
bill_type = bill['bill_type_id']
|
||||
bill_type = bill['bill_type']
|
||||
if bill['bill_date'] == 'future':
|
||||
# Future bills, deal with them at the end - they have dynamic start dates
|
||||
if 'Hyundai' in bt_id_name[bill_type]:
|
||||
future_car_bills.insert( 0, bill )
|
||||
else:
|
||||
future_D_quit_bills.insert( 0, bill )
|
||||
bill_info[bill_type]={}
|
||||
bill_info[bill_type]['future'] = 1
|
||||
bill_info[bill_type]['freq'] = bf_id_name[bt_id_freq[bill_type]]
|
||||
bill_info[bill_type]['growth'] = get_growth_value( bt, bill_type )
|
||||
bill_info[bill_type]['num_ann_bills'] = bf_id_num[bt_id_freq[bill_type]]
|
||||
bill_info[bill_type]['year']={}
|
||||
continue
|
||||
|
||||
yr= int(bill['bill_date'][:4])
|
||||
# new bill type
|
||||
if not bill_type in bill_info:
|
||||
bill_info[bill_type]={}
|
||||
bill_info[bill_type]['freq'] = bf_id_name[bt_id_freq[bill_type]]
|
||||
bill_info[bill_type]['growth'] = get_growth_value( bt, bill_type )
|
||||
bill_info[bill_type]['num_ann_bills'] = bf_id_num[bt_id_freq[bill_type]]
|
||||
bill_info[bill_type]['first_bill']={}
|
||||
@@ -294,11 +374,13 @@ def process_bill_data(bd, bt, bf):
|
||||
bill_info[bill_type]['first_bill_year']=int(bill['bill_date'][:4])
|
||||
if not 'last_real_bill_year' in bill_info[bill_type] and not bill['estimated']:
|
||||
bill_info[bill_type]['last_real_bill_year']=int(bill['bill_date'][:4])
|
||||
# add this bill to list for this year
|
||||
# append this bill to list for this year
|
||||
bill_info[bill_type]['year'][yr].append(bill)
|
||||
|
||||
# now process the bill_info from yr of first bill to yr of last bill
|
||||
for bill_type in bill_info:
|
||||
if 'future' in bill_info[bill_type]:
|
||||
continue
|
||||
# find freq id based on bill_type id, then use that to find num bills by freq id
|
||||
num = bf_id_num[bt_id_freq[bill_type]]
|
||||
|
||||
@@ -313,10 +395,70 @@ def process_bill_data(bd, bt, bf):
|
||||
# go from first_bill year until reach end year
|
||||
for yr in range( yr_min, END_YEAR+1 ):
|
||||
# we have all the bills needed for yr - but dont be cute with qtrly, gas bills suck can have missing with 4 bills
|
||||
if yr in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr]) == bill_info[bill_type]['num_ann_bills'] and bill_info[bill_type]['num_ann_bills'] !=4:
|
||||
# > can occur when we add a real bill "on top of" an estimate.
|
||||
if yr in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr]) >= bill_info[bill_type]['num_ann_bills'] and bill_info[bill_type]['num_ann_bills'] !=4:
|
||||
continue
|
||||
add_missing_bills_for_yr( bill_type, bill_info, yr )
|
||||
derive_ann_growth( bill_type, bill_info )
|
||||
derive_ann_growth( bill_type, bill_info, key_dates )
|
||||
|
||||
deal_with_future_car_bills( key_dates, future_car_bills, bill_info )
|
||||
deal_with_future_D_quit_bills( key_dates, future_D_quit_bills, bill_info )
|
||||
|
||||
return bill_info
|
||||
|
||||
################################################################################
|
||||
# deal_with_future_car_bills - just add these estimate bills based on when we
|
||||
# own the car (data can change if I buy it out)
|
||||
################################################################################
|
||||
def deal_with_future_car_bills( key_dates, future_car_bills, bill_info ):
|
||||
car_yr=key_dates['D_hyundai_owned'][0:4]
|
||||
car_mmdd=key_dates['D_hyundai_owned'][5:]
|
||||
for fb in future_car_bills:
|
||||
# deal with future bills due to their starting dates being dynamic
|
||||
amt=fb['amount']
|
||||
bt=fb['bill_type']
|
||||
# factor in growth for next bill
|
||||
for yr in range( int(car_yr), END_YEAR+1 ):
|
||||
new_date=f"{yr}-{car_mmdd}"
|
||||
# if we dont already have an annual bill for this year (all car bills are annual)
|
||||
if yr not in bill_info[bt]['year']:
|
||||
new_estimated_bill( bill_info, yr, fb['bill_type'], amt, new_date )
|
||||
amt += amt * bill_info[bt]['growth']/100
|
||||
|
||||
|
||||
################################################################################
|
||||
# deal_with_future_D_quit_bills - just add these estimate bills based on when I
|
||||
# quit
|
||||
################################################################################
|
||||
def deal_with_future_D_quit_bills( key_dates, future_D_quit_bills, bill_info ):
|
||||
D_quit_yr = key_dates['D_quit_date'][0:4]
|
||||
dq_mm=key_dates['D_quit_date'][5:7]
|
||||
dq_dd=key_dates['D_quit_date'][8:]
|
||||
if int(dq_dd) > 28: dq_dd=28
|
||||
for fb in future_D_quit_bills:
|
||||
# deal with future bills due to their starting dates being dynamic
|
||||
amt=fb['amount']
|
||||
bt=fb['bill_type']
|
||||
if bill_info[bt]['num_ann_bills'] == 1:
|
||||
# factor in growth for next bill
|
||||
for yr in range( int(D_quit_yr), END_YEAR+1 ):
|
||||
new_date=f"{yr}-{dq_mm}-{dq_dd}"
|
||||
# if we dont already have an annual bill for this year
|
||||
if not find_this_bill( bt, bill_info, new_date ):
|
||||
new_estimated_bill( bill_info, yr, bt, amt, new_date )
|
||||
amt += amt * bill_info[bt]['growth']/100
|
||||
elif bill_info[bt]['num_ann_bills'] == 12:
|
||||
# do rest of this year, then next years
|
||||
for m in range( int(dq_mm), 13):
|
||||
new_date=f"{D_quit_yr}-{m:02d}-{dq_dd}"
|
||||
if not find_this_bill( bt, bill_info, new_date ):
|
||||
new_estimated_bill( bill_info, yr, bt, amt, new_date )
|
||||
for yr in range( int(D_quit_yr)+1, END_YEAR+1 ):
|
||||
amt += amt * bill_info[bt]['growth']/100
|
||||
for m in range( 1, 13):
|
||||
new_date=f"{yr}-{m:02d}-{dq_dd}"
|
||||
if not find_this_bill( bt, bill_info, new_date ):
|
||||
new_estimated_bill( bill_info, yr, bt, amt, new_date )
|
||||
|
||||
################################################################################
|
||||
# add_missing_bills_for_yr -- wrapper to call right func based on bill freq
|
||||
@@ -333,7 +475,7 @@ def add_missing_bills_for_yr( bill_type, bill_info, yr ):
|
||||
|
||||
################################################################################
|
||||
# Takes qtrly bills and start from 2nd year of bills (so we can estimate growth)
|
||||
# and go through each bill allocating hte proportion of each bill to each
|
||||
# and go through each bill allocating the proportion of each bill to each
|
||||
# relevant quarter - to build more accurate totals. Would be mostly marginal
|
||||
# accept when Gas qtrly bills have 6 per year, and we need to guess say qtr4 in
|
||||
# the future, we can't easily find corresponding bill form previous year, so
|
||||
@@ -344,7 +486,8 @@ def ProportionQtrlyData( bill_type, bill_info ):
|
||||
now_yr = datetime.date.today().year
|
||||
# FIX UP CRAPPY QUARTERLY BILLING PROPORTIONS (only useful as some gas bills are 6 / year!)
|
||||
if bill_info[bill_type]['num_ann_bills']==4:
|
||||
for yr in range( bill_info[bill_type]['first_bill_year'], now_yr+1):
|
||||
for yr in range( bill_info[bill_type]['first_bill_year'], END_YEAR+1):
|
||||
if yr in bill_info[bill_type]['year']:
|
||||
for b in bill_info[bill_type]['year'][yr]:
|
||||
pb = find_previous_bill( bill_type, bill_info, b['bill_date'] )
|
||||
if not pb:
|
||||
@@ -357,7 +500,7 @@ def ProportionQtrlyData( bill_type, bill_info ):
|
||||
# terms of min/avg/max - uses qtr data for qtrly bills, or just normal totals
|
||||
# for other bill types
|
||||
################################################################################
|
||||
def derive_ann_growth( bill_type, bill_info ):
|
||||
def derive_ann_growth( bill_type, bill_info, key_dates ):
|
||||
# just do up to now so we stop earlier than looking at other estimated (just an optimisation)
|
||||
now_yr = datetime.date.today().year
|
||||
|
||||
@@ -368,7 +511,7 @@ def derive_ann_growth( bill_type, bill_info ):
|
||||
continue;
|
||||
|
||||
# just going to make sure we dont use estimated data in the last year of real data - can skew growths
|
||||
if yr == bill_info[bill_type]['last_real_bill_year']:
|
||||
if yr == bill_info[bill_type]['last_real_bill_year'] or bill_info[bill_type]['num_ann_bills'] ==1:
|
||||
skip_yr=False
|
||||
for b in bill_info[bill_type]['year'][yr]:
|
||||
if b['estimated']:
|
||||
@@ -390,31 +533,151 @@ def derive_ann_growth( bill_type, bill_info ):
|
||||
# use new derived qtr, slightly more accurate
|
||||
total[yr]=tot
|
||||
|
||||
|
||||
# once we have all yr totals:
|
||||
growth = {}
|
||||
min_growth = 999
|
||||
avg_growth = 0
|
||||
max_growth = 0
|
||||
count = 0
|
||||
simple_first_yr=0
|
||||
simple_last_yr=0
|
||||
# start from year after first bill, so we can see annual growth from the following year onwards
|
||||
for yr in range( bill_info[bill_type]['first_bill_year']+1, now_yr+1):
|
||||
# if full data sets for consecutive years, work out annual growth stats
|
||||
if yr-1 in total and yr in total:
|
||||
if simple_first_yr==0:
|
||||
simple_first_yr=yr
|
||||
growth = (total[yr] - total[yr-1]) / total[yr-1] * 100
|
||||
avg_growth += growth
|
||||
count += 1
|
||||
simple_last_yr=yr
|
||||
if growth < min_growth:
|
||||
min_growth = growth
|
||||
if growth > max_growth:
|
||||
max_growth = growth
|
||||
# data to work with
|
||||
if count:
|
||||
## print( f"Before sanity check, min={min_growth}, avg={avg_growth/count}, max_growth={max_growth}" )
|
||||
# HACK FOR SANITY SAKE NOW - bills wont decrease normally, and 10% is unlikely for sustained growth
|
||||
if min_growth< 0: min_growth=0
|
||||
if avg_growth< 0 or avg_growth > 10: avg_growth = 3*count
|
||||
if max_growth>10 : max_growth = 9.99
|
||||
set_bill_type_growth( bill_type, min_growth, avg_growth/count, max_growth )
|
||||
# strt with 0, set it if we can below
|
||||
simple_growth=0
|
||||
if simple_first_yr != simple_last_yr:
|
||||
# calculate a simple growth with full year consecutive totals -> last - first / years
|
||||
simple_growth=( ((total[simple_last_yr]-total[simple_first_yr])/(simple_last_yr-simple_first_yr)) / total[simple_first_yr] )*100.0
|
||||
else:
|
||||
# calculate a simple growth based on last - first / years - only 1 consecutive year I guess, so can't use it, use real first/last
|
||||
if bill_info[bill_type]['first_bill_year'] != bill_info[bill_type]['last_real_bill_year'] and bill_info[bill_type]['first_bill_year'] in total and bill_info[bill_type]['last_real_bill_year'] in total:
|
||||
simple_growth=( ((total[bill_info[bill_type]['last_real_bill_year']]-total[bill_info[bill_type]['first_bill_year']])/(bill_info[bill_type]['last_real_bill_year']-bill_info[bill_type]['first_bill_year'])) / total[bill_info[bill_type]['first_bill_year']] )*100.0
|
||||
set_bill_type_growth( bill_type, min_growth, avg_growth/count, max_growth, simple_growth )
|
||||
else:
|
||||
# okay use last - first / years to get a simple_growth, just need bills from different years
|
||||
# if there are totals for them (may not be set with monthly and < 12 bills in 1st year)
|
||||
if 'last_real_bill_year' in bill_info[bill_type] and bill_info[bill_type]['first_bill_year'] != bill_info[bill_type]['last_real_bill_year'] and bill_info[bill_type]['first_bill_year'] in total and bill_info[bill_type]['last_real_bill_year'] in total:
|
||||
simple_growth=( ((total[bill_info[bill_type]['last_real_bill_year']]-total[bill_info[bill_type]['first_bill_year']])/(bill_info[bill_type]['last_real_bill_year']-bill_info[bill_type]['first_bill_year'])) / total[bill_info[bill_type]['first_bill_year']] )*100.0
|
||||
set_bill_type_growth( bill_type, 0, 0, 0, simple_growth )
|
||||
else:
|
||||
# failsafe (just in case fill bills failed to add enough bills to average out)
|
||||
print( f"{bill_type}: Unable to calculate growth!" )
|
||||
|
||||
################################################################################
|
||||
# just go through this year to END_YEAR, total any bills for each year up
|
||||
# so we can display the annual estimated bills onwards...
|
||||
################################################################################
|
||||
def calc_future_totals(bill_info, bill_types):
|
||||
total={}
|
||||
now_yr = datetime.date.today().year
|
||||
for bt in bill_types:
|
||||
total[bt['id']]={}
|
||||
for yr in range( now_yr, END_YEAR+1):
|
||||
total[bt['id']][yr]=0.0
|
||||
if bt['id'] in bill_info and yr in bill_info[bt['id']]['year']:
|
||||
for b in bill_info[bt['id']]['year'][yr]:
|
||||
total[bt['id']][yr] += b['amount']
|
||||
# had to round to 2 decimal here to get sensible totals
|
||||
total[bt['id']][yr] = round( total[bt['id']][yr], 2 )
|
||||
return total
|
||||
|
||||
|
||||
################################################################################
|
||||
# When we change the day D_quits, or we buyout the car, then future bills need
|
||||
# to change/rebuild estimates, convenience routine used to find future bills -
|
||||
# rather than go through them as we render /bills
|
||||
################################################################################
|
||||
def getFutureBills(bd,bt,future_car_bills, future_D_quit_bills):
|
||||
# this maps a bill id to a name
|
||||
bt_id_name = {row["id"]: row["name"] for row in bt}
|
||||
|
||||
for bill in bd:
|
||||
bill_type = bill['bill_type']
|
||||
if bill['bill_date'] == 'future':
|
||||
# Future bills, deal with them at the end - they have dynamic start dates
|
||||
if 'Hyundai' in bt_id_name[bill_type]:
|
||||
future_car_bills.insert( 0, bill )
|
||||
else:
|
||||
future_D_quit_bills.insert( 0, bill )
|
||||
return
|
||||
|
||||
|
||||
################################################################################
|
||||
# When we change the day D_quits, or we buyout the car, then future bills need
|
||||
# to change/rebuild estimates, convenience routine used to handle this
|
||||
################################################################################
|
||||
def recalcFutureBills():
|
||||
future_car_bills=[]
|
||||
future_D_quit_bills=[]
|
||||
|
||||
print("Recalculating future bills as we changed a key date" )
|
||||
finance_data = get_finance_data()
|
||||
key_dates = calc_key_dates( finance_data )
|
||||
bill_data = get_bill_data("order_by_date_only")
|
||||
bill_types = get_bill_types()
|
||||
bill_freqs = get_bill_freqs()
|
||||
|
||||
bt_id_freq = {row["id"]: row["freq"] for row in bill_types}
|
||||
# this maps freq to bills per annum (e.g. id=2 to 4 bills per annum)
|
||||
bf_id_num = {row["id"]: row["num_bills_per_annum"] for row in bill_freqs}
|
||||
|
||||
getFutureBills(bill_data, bill_types, future_car_bills, future_D_quit_bills)
|
||||
deleteFutureEstimates()
|
||||
# deal with future car bills
|
||||
car_yr=key_dates['D_hyundai_owned'][0:4]
|
||||
car_mmdd=key_dates['D_hyundai_owned'][5:]
|
||||
for fb in future_car_bills:
|
||||
amt=fb['amount']
|
||||
bt=fb['bill_type']
|
||||
# only can use simple growth as its a future bill
|
||||
growth=bill_types[bt]['ann_growth_simple']
|
||||
# factor in growth for next bills
|
||||
for yr in range( int(car_yr), END_YEAR+1 ):
|
||||
new_date=f"{yr}-{car_mmdd}"
|
||||
new_bill( fb['bill_type'], amt, new_date, 1 )
|
||||
amt += amt * growth/100
|
||||
|
||||
# deal with future D_Quit bills
|
||||
D_quit_yr = key_dates['D_quit_date'][0:4]
|
||||
dq_mm=key_dates['D_quit_date'][5:7]
|
||||
dq_dd=key_dates['D_quit_date'][8:]
|
||||
# avoid feb 29+ :)
|
||||
if int(dq_dd) > 28: dq_dd=28
|
||||
for fb in future_D_quit_bills:
|
||||
# deal with future bills due to their starting dates being dynamic
|
||||
amt=fb['amount']
|
||||
bt=fb['bill_type']
|
||||
growth=bill_types[bt]['ann_growth_simple']
|
||||
num_ann_bills= bf_id_num[bt_id_freq[bt]]
|
||||
if num_ann_bills == 1:
|
||||
# factor in growth for next bill
|
||||
for yr in range( int(D_quit_yr), END_YEAR+1 ):
|
||||
new_date=f"{yr}-{dq_mm}-{dq_dd}"
|
||||
# if we dont already have an annual bill for this year
|
||||
new_bill( fb['bill_type'], amt, new_date, 1 )
|
||||
amt += amt * growth/100
|
||||
elif num_ann_bills == 12:
|
||||
# do rest of this year, then next years
|
||||
for m in range( int(dq_mm), 13):
|
||||
new_date=f"{D_quit_yr}-{m:02d}-{dq_dd}"
|
||||
new_bill( fb['bill_type'], amt, new_date, 1 )
|
||||
for yr in range( int(D_quit_yr)+1, END_YEAR+1 ):
|
||||
amt += amt * growth/100
|
||||
for m in range( 1, 13):
|
||||
new_date=f"{yr}-{m:02d}-{dq_dd}"
|
||||
new_bill( fb['bill_type'], amt, new_date, 1 )
|
||||
return
|
||||
|
||||
142
calc.py
142
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:
|
||||
D_quit_date = current_date
|
||||
# going to pay tax on payout, so claim it back next year
|
||||
claim_tax_on_leave = True
|
||||
print(f"{current_date}: D has resigned in new year- get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax_new_fin_year}" )
|
||||
current_savings += D_leave_after_tax_new_fin_year
|
||||
add_annotation(finance, current_date, current_savings, D_leave_after_tax_new_fin_year, "D quit" )
|
||||
else:
|
||||
claim_tax_on_leave = False
|
||||
print(f"{current_date}: D has resigned - get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax}" )
|
||||
print(f"{current_date}: D has resigned in new year- get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax}" )
|
||||
current_savings += D_leave_after_tax
|
||||
add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
|
||||
D_leave_after_tax = 0
|
||||
|
||||
# its end of 'next' fin year, if tax_diff > 0, then ddp quit after new tax year and gets back the overpaid tax
|
||||
if current_date > new_fin_year_26 and claim_tax_on_leave:
|
||||
current_savings += tax_diff_D_leave
|
||||
add_annotation(finance, current_date, current_savings, tax_diff_D_leave, "D quit - tax back" )
|
||||
# can only claim the tax back once :)
|
||||
claim_tax_on_leave=False
|
||||
|
||||
if fortnight_income:
|
||||
print(f"{current_date}: salary paid by Deakin - adding: {fortnight_income}" )
|
||||
current_savings += fortnight_income
|
||||
@@ -178,6 +194,14 @@ def calculate_savings_depletion(finance):
|
||||
|
||||
savings_per_fortnight.append((current_date.strftime("%Y-%m-%d"), round(current_savings, 2)))
|
||||
|
||||
# its end of fin year, if claim_tax_on_leave > 0 then get tax back
|
||||
if current_date.month == 7 and current_date.day == 1 and claim_tax_on_leave:
|
||||
current_savings += tax_diff_D_leave
|
||||
print( f"I quit last fin year, so now its 1st July {current_date.year}, get tax back of {tax_diff_D_leave}" )
|
||||
add_annotation(finance, current_date, current_savings, tax_diff_D_leave, "D quit - tax back" )
|
||||
# can only claim the tax back once :)
|
||||
claim_tax_on_leave=False
|
||||
|
||||
# if I have quit, then car lease payments are made on the 15th of the month for full Car_loan
|
||||
if D_has_quit and current_date.day == 15:
|
||||
if Ioniq6_future == LEASE and current_date <= car_balloon_date:
|
||||
@@ -191,6 +215,7 @@ 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
|
||||
@@ -210,24 +235,6 @@ def calculate_savings_depletion(finance):
|
||||
add_annotation(finance, current_date, current_savings, -Car_buyout, "car buyout")
|
||||
print(f"{current_date}: car buyout - ${Car_buyout}" )
|
||||
|
||||
# Anniversary of Car purchase/balloon so potentially insurance/rego
|
||||
# when I quit, the if we haven't paid the car outright, then need to add rego, but not insurance
|
||||
# if we pay-out the car, then add insurace and rego
|
||||
if current_date.month == car_balloon_date.month and current_date.day == car_balloon_date.day:
|
||||
# staying with the lease (0), if I have quit, then pay monthly rego only up to lease date, but full cost after car balloon date
|
||||
if Ioniq6_future == LEASE:
|
||||
if current_date.year >= car_balloon_date.year:
|
||||
current_savings -= (ioniq6_ins + ioniq6_rego)
|
||||
add_annotation(finance, current_date, current_savings, -(ioniq6_ins+ioniq6_rego), "IONIQ 6 ins/rego" )
|
||||
# if we buy car outright, then as long as this anniversary is after buyout date, pay ins and rego
|
||||
elif current_date.year >= car_buyout_date.year:
|
||||
current_savings -= (ioniq6_ins + ioniq6_rego)
|
||||
add_annotation(finance, current_date, current_savings, -(ioniq6_ins+ioniq6_rego), "IONIQ 6 ins/rego" )
|
||||
|
||||
if current_date.date() == overseas_trip_date.date():
|
||||
current_savings -= Overseas_trip
|
||||
add_annotation(finance, current_date, current_savings, -Overseas_trip, "O/S trip")
|
||||
|
||||
if current_date.date() == mich_present_date.date():
|
||||
current_savings -= Mich_present
|
||||
add_annotation(finance, current_date, current_savings, -Mich_present, "Mich's present" )
|
||||
@@ -251,7 +258,7 @@ def calculate_savings_depletion(finance):
|
||||
# if selling shares, and its 1st of July...
|
||||
# BUT not if D quits before end of financial year - as I won't be able to sell CBA shares for no cap gains
|
||||
# so wait until the following year
|
||||
if current_date.month == 7 and current_date.day == 1 and D_has_quit and Sell_shares>0 and (current_date.year > D_quit_year or current_date.year == D_quit_year and claim_tax_on_leave == False):
|
||||
if current_date.month == 7 and current_date.day == 1 and D_has_quit and Sell_shares>0 and (D_quit_date.month<7 or D_quit_date.year < current_date.year ):
|
||||
# 2024 Govt. value
|
||||
tax_threshold = 18200
|
||||
# cap-gains is 50% of profit (lazy profit calc here, just assume its all profit)
|
||||
@@ -281,6 +288,27 @@ def calculate_savings_depletion(finance):
|
||||
finance['CBA']=D_CBA_shares
|
||||
finance['TLS']=D_TLS_shares+M_TLS_shares
|
||||
|
||||
|
||||
return depletion_date, savings_per_fortnight, current_savings
|
||||
|
||||
################################################################################
|
||||
# work out the date D quits and when we own the car, so we can then use it to
|
||||
# handle future bills
|
||||
################################################################################
|
||||
def calc_key_dates( finance ):
|
||||
key_dates={}
|
||||
now = datetime.today()
|
||||
# this will be 0 to 13 days - how far into this fortnights pay cycle are we now
|
||||
days_in_pay_fortnight= ( now - first_pay_date ).days % 14
|
||||
|
||||
# add 1 less fortnight than we continue to work, then add rest of pay cycle (14-days_in_pay_fortnight)
|
||||
key_dates['D_quit_date'] = (now+timedelta(weeks=2*(finance['D_Num_fortnights_pay']-1))+timedelta(days=(14-days_in_pay_fortnight))).strftime('%Y-%m-%d')
|
||||
|
||||
# use lease date
|
||||
if finance['Ioniq6_future'] == LEASE:
|
||||
key_dates['D_hyundai_owned'] = car_balloon_date.strftime('%Y-%m-%d')
|
||||
# use buyout date
|
||||
else:
|
||||
key_dates['D_hyundai_owned'] = finance['Car_buyout_date']
|
||||
|
||||
return key_dates
|
||||
|
||||
|
||||
131
db.py
131
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,
|
||||
@@ -91,15 +97,10 @@ def init_db():
|
||||
ann_growth_min REAL,
|
||||
ann_growth_avg REAL,
|
||||
ann_growth_max REAL,
|
||||
ann_growth_simple REAL,
|
||||
FOREIGN KEY(freq) REFERENCES bill_freq(id)
|
||||
)''')
|
||||
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS bill_freq (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name STRING,
|
||||
num_bills_per_annum INTEGER
|
||||
)''')
|
||||
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS bill_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
bill_type INTEGER,
|
||||
@@ -115,27 +116,56 @@ def init_db():
|
||||
show_estimated INTEGER
|
||||
)''')
|
||||
|
||||
# Check if table is empty, if so insert default values
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS bill_growth_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name string,
|
||||
value INTEGER
|
||||
)''')
|
||||
|
||||
# Check if finance table is empty, if so insert default values
|
||||
cur.execute('SELECT COUNT(*) FROM finance')
|
||||
if cur.fetchone()[0] == 0:
|
||||
###
|
||||
# For now manually update below on the fortnight of the original pay shcedule to compare saved version vs. our reality. Update:
|
||||
# Savings (Macq+me bank) -- noting ME bank is: $1876.19, nab is 2727.95
|
||||
# TLS/CBA prices
|
||||
# Interest rate
|
||||
# D_leave_owed_in_days
|
||||
# maybe quarterly update Inflation? (this is harder to appreciate, seems much lower officialy than Savings, but which inflation:
|
||||
# I've decided to use RBA Trimmed Mean CPI YoY -- https://tradingeconomics.com/australia/inflation-cpi
|
||||
###
|
||||
cur.execute('''INSERT INTO finance (D_Salary, D_Num_fortnights_pay, School_Fees, Car_loan_via_pay, Car_loan, Car_balloon, Car_buyout, Living_Expenses, Savings, Interest_Rate,
|
||||
Inflation, Mich_present, Overseas_trip, Mark_reno, D_leave_owed_in_days, D_TLS_shares, M_TLS_shares, D_CBA_shares, TLS_price, CBA_price, Overseas_trip_date, Mark_reno_date, Car_buyout_date, Sell_shares, compare_to, Ioniq6_future)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||
(4762.29, 10, 24000, 620, 2412, 45824.68, 83738.74, 80000, 424875.26, 4.75, 2.4, 10000, 50000, 10000, 76.85, 1000, 750, 1111, 4.52, 163.32, '2025-06-01', '2025-09-01', '2025-02-20', 4, 0, 0))
|
||||
|
||||
# Check if bill_freq table is empty, if so insert default values
|
||||
cur.execute('SELECT COUNT(*) FROM bill_freq')
|
||||
if cur.fetchone()[0] == 0:
|
||||
cur.execute( "INSERT INTO bill_freq values ( 1, 'Annual', 1 )" )
|
||||
cur.execute( "INSERT INTO bill_freq values ( 2, 'Quarterly', 4 )" )
|
||||
cur.execute( "INSERT INTO bill_freq values ( 3, 'Monthly', 12 )" )
|
||||
cur.execute( "INSERT INTO bill_freq values ( 3, 'Quarterly (forced)', 4 )" )
|
||||
cur.execute( "INSERT INTO bill_freq values ( 4, 'Monthly', 12 )" )
|
||||
# start with no specific Tab/bill_type to show, and dont show_estimated
|
||||
|
||||
# Check if bill_ui table is empty, if so insert default values
|
||||
cur.execute('SELECT COUNT(*) FROM bill_ui')
|
||||
if cur.fetchone()[0] == 0:
|
||||
cur.execute( "INSERT INTO bill_ui values ( 1, null, 0 )" )
|
||||
|
||||
# Check if bill_growth_types table is empty, if so insert default values
|
||||
cur.execute('SELECT COUNT(*) FROM bill_growth_types')
|
||||
if cur.fetchone()[0] == 0:
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Min', 0 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Avg', 0 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Max', 0 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Simple', 0 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'CPI', 0 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 0', 0 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 1', 1 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 2', 2 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 3', 3 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 4', 4 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 5', 5 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 6', 6 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 7', 7 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 8', 8 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 9', 9 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 10', 10 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 12', 12 )" )
|
||||
cur.execute( "INSERT INTO bill_growth_types ( name, value ) values ( 'Flat 15', 15 )" )
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -148,8 +178,9 @@ def get_finance_data():
|
||||
return dict(finance)
|
||||
|
||||
def get_budget_data(finance_data):
|
||||
# annual bills - health ins (5k), rates (2.4), electricity (1.5), gas (2), internet (1.6), car insurance (.7), rego (.8), house insurance (2.4), GFC (2.2), phones (.5), melb. pollen (.03), nabu casa (.1), eweka (.1) --- noting phone is elevated presuming I also go onto Aldi plan, but that there is no family discount
|
||||
bills = 19330
|
||||
# annual bills - health ins (5k), rates (2.6), electricity (1.2), gas (2.1) - but 1.4 in 2025 due to EU trip, internet (1.6), car insurance (.7), rego (.8), house insurance (2.4), GFC (2.6), water (1.1), eweka (.1), phones (.5), melb. pollen (.03), nabu casa (.1) --- noting phone is elevated presuming I also go onto Aldi plan, but that there is no family discount, and health will be extra after stop working
|
||||
# fudging below - its more like 15.2 + health, and really gas will be more than 2.1 than 1.4, so about 16+5
|
||||
bills = 21000
|
||||
BUDGET=[]
|
||||
BUDGET.append( ('Bills', f"${bills:,.2f}") )
|
||||
BUDGET.append( ('Buffer', f"${finance_data['CBA']*finance_data['CBA_price']+finance_data['TLS']*finance_data['TLS_price']:,.2f}") )
|
||||
@@ -268,10 +299,15 @@ def get_comp_set_options(finance):
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def get_bill_data():
|
||||
def get_bill_data(order_by):
|
||||
conn = connect_db(True)
|
||||
cur = conn.cursor()
|
||||
cur.execute('''SELECT bd.id, bt.id as bill_type_id, bt.name, bd.amount, bd.bill_date, bd.estimated
|
||||
if order_by == "order_by_date_only":
|
||||
cur.execute('''SELECT bd.id, bt.id as bill_type, bt.name, bd.amount, bd.bill_date, bd.estimated
|
||||
FROM bill_type bt, bill_data bd
|
||||
where bt.id = bd.bill_type order by bd.bill_date desc''')
|
||||
else:
|
||||
cur.execute('''SELECT bd.id, bt.id as bill_type, bt.name, bd.amount, bd.bill_date, bd.estimated
|
||||
FROM bill_type bt, bill_data bd
|
||||
where bt.id = bd.bill_type order by bt.name, bd.bill_date desc''')
|
||||
bd = cur.fetchall()
|
||||
@@ -308,6 +344,12 @@ def get_bill_freqs():
|
||||
def new_bill( bill_type, amount, bill_date, estimated ):
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
# if we are a real bill added by UI
|
||||
if not estimated:
|
||||
# delete old estimates as new bill will potentially change them/growth, etc.
|
||||
cur.execute( f"delete from bill_data where estimated=1" )
|
||||
# force the next /bills load to show the tab for the bill we are adding
|
||||
cur.execute( f"update bill_ui set last_tab='{bill_type}'" )
|
||||
cur.execute( f"insert into bill_data ( 'bill_type', 'amount', 'bill_date', 'estimated' ) values ( '{bill_type}', '{float(amount):.2f}', '{bill_date}', {estimated} )" )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -325,7 +367,7 @@ def insert_bill_type( bt, fq ):
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
print( f"fq={fq}" )
|
||||
cur.execute( f"insert into bill_type ( 'name', 'freq', 'ann_growth_min', 'ann_growth_avg', 'ann_growth_max' ) values ( '{bt}', {fq}, 0, 0, 0 )" )
|
||||
cur.execute( f"insert into bill_type ( 'name', 'freq', 'ann_growth_min', 'ann_growth_avg', 'ann_growth_max', 'ann_growth_simple' ) values ( '{bt}', {fq}, 0, 0, 0, 0 )" )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
@@ -354,14 +396,22 @@ def delete_bill_type( id ):
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def set_bill_type_growth( id, min_g, avg_g, max_g ):
|
||||
def set_bill_type_growth( id, min_g, avg_g, max_g, simple_g ):
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
cur.execute( f"update bill_type set ann_growth_min='{min_g}', ann_growth_avg ='{avg_g}', ann_growth_max='{max_g}' where id = {id}" )
|
||||
cur.execute( f"update bill_type set ann_growth_min={min_g}, ann_growth_avg ={avg_g}, ann_growth_max={max_g}, ann_growth_simple= {simple_g} where id = {id}" )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def get_bill_growth_types():
|
||||
conn = connect_db(True)
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT * FROM bill_growth_types')
|
||||
growth = cur.fetchall()
|
||||
conn.close()
|
||||
return growth
|
||||
|
||||
def get_bill_ui():
|
||||
conn = connect_db(True)
|
||||
cur = conn.cursor()
|
||||
@@ -381,3 +431,36 @@ def save_ui(data):
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
|
||||
def deleteFutureEstimates():
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
cur.execute( "delete from bill_data where bill_date != 'future' and bill_type in ( select bill_type from bill_data where bill_date='future')" )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def delete_estimated_bills():
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
cur.execute( "delete from bill_data where estimated=1" )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def delete_estimated_bills_for(bt_id):
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
cur.execute( f"delete from bill_data where estimated=1 and bill_type = {bt_id}" )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def delete_cset(id):
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
cur.execute( f"delete from comparison_set where id = '{id}'" )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
103
main.py
103
main.py
@@ -1,15 +1,19 @@
|
||||
# main.py
|
||||
from flask import Flask, render_template, request, redirect, url_for, Response, jsonify
|
||||
from calc import calculate_savings_depletion
|
||||
from db import init_db, get_finance_data, update_finance, get_budget_data, insert_cset, get_comp_set_data, get_comp_set_options, get_bill_freqs
|
||||
from db import get_bill_data, new_bill, update_bill_data, delete_bill
|
||||
from calc import calculate_savings_depletion, calc_key_dates
|
||||
from db import init_db, get_finance_data, update_finance, get_budget_data
|
||||
from db import insert_cset, get_comp_set_data, get_comp_set_options, delete_cset
|
||||
from db import get_bill_freqs, get_bill_growth_types
|
||||
from db import get_bill_data, new_bill, update_bill_data, delete_bill, delete_estimated_bills, delete_estimated_bills_for
|
||||
from db import get_bill_ui, save_ui
|
||||
from db import get_bill_types, insert_bill_type, update_bill_type, delete_bill_type, use_growth
|
||||
from bills import process_bill_data
|
||||
from bills import process_bill_data, calc_future_totals, set_bill_type_growth, recalcFutureBills
|
||||
from defines import END_YEAR
|
||||
from collections import defaultdict, Counter
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
import csv
|
||||
import io
|
||||
import requests
|
||||
from disp import FP_VAR
|
||||
|
||||
app = Flask(__name__)
|
||||
@@ -24,7 +28,9 @@ init_db()
|
||||
def index():
|
||||
finance_data = get_finance_data()
|
||||
get_comp_set_options(finance_data)
|
||||
depletion_date, savings_per_fortnight, final_savings = calculate_savings_depletion(finance_data)
|
||||
bill_data = get_bill_data("order_by_date_only")
|
||||
bill_types = get_bill_types()
|
||||
depletion_date, savings_per_fortnight, final_savings = calculate_savings_depletion(finance_data, bill_data, bill_types)
|
||||
BUDGET=get_budget_data(finance_data)
|
||||
|
||||
if depletion_date:
|
||||
@@ -99,7 +105,8 @@ def index():
|
||||
|
||||
# now work out how much padding we need in the first year to align the last dates for all years
|
||||
padding=second_count - first_count
|
||||
return render_template('index.html', now=now, first_yr=first_yr, padding=padding, finance=finance_data, depletion_date=depletion_date, savings=savings_per_fortnight, BUDGET=BUDGET, COMP=COMP, DISP=DISP)
|
||||
key_dates = calc_key_dates( finance_data )
|
||||
return render_template('index.html', now=now, first_yr=first_yr, padding=padding, finance=finance_data, depletion_date=depletion_date, savings=savings_per_fortnight, BUDGET=BUDGET, COMP=COMP, DISP=DISP, key_dates=key_dates)
|
||||
|
||||
@app.route('/save', methods=['POST'])
|
||||
def save():
|
||||
@@ -109,6 +116,8 @@ def save():
|
||||
@app.route('/update', methods=['POST'])
|
||||
def update():
|
||||
|
||||
old_finance_data = get_finance_data()
|
||||
|
||||
finance_data = (
|
||||
request.form['D_Salary'],
|
||||
request.form['D_Num_fortnights_pay'],
|
||||
@@ -138,17 +147,45 @@ def update():
|
||||
request.form['Ioniq6_future']
|
||||
)
|
||||
update_finance(finance_data)
|
||||
new_finance_data = get_finance_data()
|
||||
# changed Ioniq6_future, Car_buyout_date or D_Num_fortnights_pay, so lets force recalc key_dates, and therefore estimated bills
|
||||
if old_finance_data['D_Num_fortnights_pay'] != new_finance_data['D_Num_fortnights_pay'] or old_finance_data['Ioniq6_future'] != new_finance_data['Ioniq6_future'] or old_finance_data['Car_buyout_date'] != new_finance_data['Car_buyout_date']:
|
||||
recalcFutureBills()
|
||||
if old_finance_data['Inflation'] != new_finance_data['Inflation']:
|
||||
# need to check if any bill type is using CPI, if so, force those future bills to be recalculated
|
||||
bill_types = get_bill_types()
|
||||
for bt in bill_types:
|
||||
if bt['which_growth'] == 'cpi':
|
||||
print( f"OK, changed inflation and need to redo bills for bt_id={bt['id']}" )
|
||||
delete_estimated_bills_for( bt['id'] )
|
||||
#recalc_estimated_bills_for( bt['id'] )
|
||||
# okay, now go through code to recalc bills...
|
||||
base=request.url_root
|
||||
response = requests.get(f"{base}/bills")
|
||||
if response.status_code == 200:
|
||||
print("ALL GOOD")
|
||||
else:
|
||||
print("FFS")
|
||||
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/bills')
|
||||
def DisplayBillData():
|
||||
bill_data = get_bill_data()
|
||||
finance_data = get_finance_data()
|
||||
# work out when D quits, when car is owned
|
||||
key_dates = calc_key_dates( finance_data )
|
||||
bill_data = get_bill_data("order_by_bill_type_then_date")
|
||||
bill_types = get_bill_types()
|
||||
bill_freqs = get_bill_freqs()
|
||||
bill_ui = get_bill_ui()
|
||||
process_bill_data(bill_data, bill_types, bill_freqs)
|
||||
bill_data = get_bill_data()
|
||||
return render_template('bills.html', bill_data=bill_data, bill_types=bill_types, bill_freqs=bill_freqs, bill_ui=bill_ui )
|
||||
bill_growth_types = get_bill_growth_types()
|
||||
# take bill data, AND work out estimated future bills - process this into the bill_info array,
|
||||
bill_info=process_bill_data(bill_data, bill_types, bill_freqs, key_dates)
|
||||
# get an array of the total costs of bills each year - purely cosmetic (using bill_info)
|
||||
total=calc_future_totals(bill_info, bill_types)
|
||||
# update/re-get bill_data now that new estimated bills have been added
|
||||
bill_data = get_bill_data("order_by_bill_type_then_date")
|
||||
return render_template('bills.html', bill_data=bill_data, bill_types=bill_types, bill_freqs=bill_freqs, bill_ui=bill_ui, this_year=datetime.today().year, END_YEAR=END_YEAR, total=total, key_dates=key_dates, growth=bill_growth_types, cpi=finance_data['Inflation'] )
|
||||
|
||||
@app.route('/newbilltype', methods=['POST'])
|
||||
def InsertBillType():
|
||||
@@ -166,13 +203,17 @@ def UpdateBillType():
|
||||
def InsertBill():
|
||||
data = request.get_json()
|
||||
# last param is estimated - e.g. anything via GUI is not an estimate, but is a real bill
|
||||
new_bill( data['name'], data['amount'], data['bill_date'], 0 )
|
||||
if 'bill_date' in data:
|
||||
new_bill( data['bill_type'], data['amount'], data['bill_date'], 0 )
|
||||
else:
|
||||
new_bill( data['bill_type'], data['amount'], 'future', 0 )
|
||||
set_bill_type_growth( data['bill_type'], 0, 0, 0, data['growth'] )
|
||||
return "200"
|
||||
|
||||
@app.route('/updatebill', methods=['POST'])
|
||||
def UpdateBill():
|
||||
data = request.get_json()
|
||||
update_bill_data( data['id'], data['name'], data['amount'], data['bill_date'] )
|
||||
update_bill_data( data['id'], data['bill_type'], data['amount'], data['bill_date'] )
|
||||
return "200"
|
||||
|
||||
@app.route('/delbilltype', methods=['POST'])
|
||||
@@ -199,6 +240,42 @@ def SaveUI():
|
||||
save_ui( data )
|
||||
return "200"
|
||||
|
||||
@app.route('/force_recalc_bills', methods=['POST'])
|
||||
def force_recalc_bills():
|
||||
delete_estimated_bills()
|
||||
recalcFutureBills()
|
||||
return "200"
|
||||
|
||||
@app.route('/cset')
|
||||
def cset():
|
||||
finance_data = get_finance_data()
|
||||
get_comp_set_options(finance_data)
|
||||
comp_data={}
|
||||
for el in finance_data['COMP_SETS']:
|
||||
comp_data[el[0]] = get_comp_set_data( el[0] )
|
||||
# delete items not that helpful (same for all, not that interesting)
|
||||
if el[0]:
|
||||
del comp_data[el[0]]['vars']['Car_loan_via_pay']
|
||||
del comp_data[el[0]]['vars']['Mark_reno']
|
||||
del comp_data[el[0]]['vars']['Mark_reno_date']
|
||||
del comp_data[el[0]]['vars']['Overseas_trip_date']
|
||||
del comp_data[el[0]]['vars']['Car_balloon']
|
||||
del comp_data[el[0]]['vars']['Mich_present']
|
||||
del comp_data[el[0]]['vars']['D_TLS_shares']
|
||||
del comp_data[el[0]]['vars']['M_TLS_shares']
|
||||
|
||||
return render_template('cset.html', finance=finance_data, comp_data=comp_data )
|
||||
|
||||
@app.route('/delcset', methods=['POST'])
|
||||
def DeleteCSet():
|
||||
data = request.get_json()
|
||||
delete_cset( data['id'] )
|
||||
return "200"
|
||||
|
||||
# quick health route so traefik knows we are up
|
||||
@app.route('/health')
|
||||
def health():
|
||||
return {"status": "ok"}, 200
|
||||
|
||||
# Main program
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -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-6">
|
||||
<div class="row align-items-center">
|
||||
<button id="new-bill-type-button" class="mb-3 px-0 offset-4 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillType()"><span class="bi bi-plus-lg"> New Bill Type</span></button>
|
||||
{# DEBUG totals if needed
|
||||
<table>
|
||||
{% for bt in total %}
|
||||
<tr><td></td><td> {{bt}}:</td>
|
||||
{% for yr in range( 2025, 2032 ) %}
|
||||
{% if yr in total[bt] %}
|
||||
<td>
|
||||
{{total[bt][yr]}}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
#}
|
||||
<div class="col-8">
|
||||
<div class="row">
|
||||
<div class="col-2 form-control-inline d-none new-bill-type-class">Bill Type</div>
|
||||
<div class="col-2 form-control-inline d-none new-bill-type-class">Frequency</div>
|
||||
</div>
|
||||
<div class="row align-items-center mb-3">
|
||||
<button id="new-bill-type-button" class="mt-4 px-0 offset-4 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillType()"><span class="bi bi-plus-lg"> New Bill Type</span></button>
|
||||
<div class="new-bill-type-class px-0 col-2 d-none"> <input type="text" class="form-control text-end float-end border border-primary" id="new-bill-type-name"></div>
|
||||
<div class="new-bill-type-class px-0 col-2 d-none"><select id="new-bill-type-freq" class="form-select text-center">
|
||||
{% for bf in bill_freqs %}
|
||||
@@ -33,71 +52,145 @@
|
||||
</div>
|
||||
<button id="save-bill-type" class="new-bill-type-class px-0 col-1 btn btn-success bg-success-subtle text-success d-none" onClick="NewBillType()"><span class="bi bi-floppy"></span> Save</button>
|
||||
<button id="canc-bill-type" class="new-bill-type-class px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelNewBillType()"><span class="bi bi-x"> Cancel</span></button>
|
||||
<button id="recalc-bills" class="mt-4 col-2 offset-3 btn btn-warning bg-warning-subtle text-warning" onClick="ForceRecalcBills()"><span class="bi bi-repeat"> Recalculate</span></button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"><br>Name</ ></div>
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"><br>Frequency</ ></div>
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Annual Growth Est (min/avg/max)</ ></div>
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"><br>Actions</ ></div>
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></div>
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Frequency</ ></div>
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold
|
||||
bg-body-tertiary rounded-0">Annual Growth</ ></div>
|
||||
{% for yr in range( 2025, 2032 ) %}
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0
|
||||
fw-bold bg-body-tertiary rounded-0">{{yr}} Total</ ></div>
|
||||
{% endfor %}
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
|
||||
{# spacer to get header line right now we don't use forced col widths #}
|
||||
<div class="px-0 col"><label class="form-control d-flex
|
||||
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0"> </ ></div>
|
||||
</div>
|
||||
{% for bt in bill_types %}
|
||||
<div class="row">
|
||||
<div class="px-0 col-2"><input type="text" class="bill-type-{{bt.id}} form-control text-center" id="bill-type-name-{{bt.id}}" value="{{ bt.name }}" disabled> </div>
|
||||
<div class="px-0 col-1"><input type="text" class="bill-type-{{bt.id}} form-control text-center" id="bill-type-name-{{bt.id}}" value="{{ bt.name }}" disabled> </div>
|
||||
<!-- bind Enter to save this bill-type -->
|
||||
<script>$("#bill-type-name-{{bt.id}}").keyup(function(event){ if(event.which == 13){ $('#bill-type-save-{{bt.id}}').click(); } event.preventDefault(); });</script>
|
||||
<div class="px-0 col-2"><select id="bill-type-freq-{{bt.id}}" class="bill-type-{{bt.id}} form-select text-center" disabled>
|
||||
<div class="px-0 col-1"><select id="bill-type-freq-{{bt.id}}" class="bill-type-{{bt.id}} form-select text-center" disabled>
|
||||
{% for bf in bill_freqs %}
|
||||
<option value={{bf.id}}>{{bf.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<script>$('#bill-type-freq-{{bt.id}}').val( {{bt.freq}} );</script>
|
||||
<div class="px-0 col-2">
|
||||
<div class="px-0 col">
|
||||
<div class="btn-group w-100" role="group">
|
||||
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="min-{{bt.id}}" autocomplete="off"
|
||||
onChange="UseGrowth({{bt.id}}, 'min')" {% if bt.which_growth == 'min' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary" for="min-{{bt.id}}">
|
||||
{% if bt.ann_growth_min < 10 %} {% endif %}
|
||||
{{'%.2f'|format(bt.ann_growth_min)}}
|
||||
</label>
|
||||
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="avg-{{bt.id}}" autocomplete="off"
|
||||
onChange="UseGrowth({{bt.id}}, 'avg')" {% if bt.which_growth == 'avg' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary" for="avg-{{bt.id}}">
|
||||
{% if bt.ann_growth_avg < 10 %} {% endif %}
|
||||
{{'%.2f'|format(bt.ann_growth_avg)}}
|
||||
</label>
|
||||
<input type="radio" class="btn-check" name="growth-{{bt.id}}" id="max-{{bt.id}}" autocomplete="off"
|
||||
onChange="UseGrowth({{bt.id}}, 'max')" {% if bt.which_growth == 'max' %}checked{% endif %}>
|
||||
<label class="btn btn-outline-secondary" for="max-{{bt.id}}">
|
||||
{% if bt.ann_growth_max < 10 %} {% endif %}
|
||||
{{'%.2f'|format(bt.ann_growth_max)}}
|
||||
</label>
|
||||
<select id="{{bt.id}}_growth" class="form-select col" onChange="UseGrowth({{bt.id}})">
|
||||
{% for gt in growth %}
|
||||
{% if gt.name == 'Min' %}
|
||||
<option value='min'
|
||||
{% if bt.which_growth == 'min' %}selected{% endif %}
|
||||
>{{'%.2f'|format(bt.ann_growth_min)}}% {{gt.name}}</option>
|
||||
{% elif gt.name == 'Avg' %}
|
||||
<option value='avg'
|
||||
{% if bt.which_growth == 'avg' %}selected{% endif %}
|
||||
>{{'%.2f'|format(bt.ann_growth_avg)}}% {{gt.name}}</option>
|
||||
{% elif gt.name == 'Max' %}
|
||||
<option value='max'
|
||||
{% if bt.which_growth == 'max' %}selected{% endif %}
|
||||
>{{'%.2f'|format(bt.ann_growth_max)}}% {{gt.name}}</option>
|
||||
{% elif gt.name == 'Simple' %}
|
||||
<option value='simple'
|
||||
{% if bt.which_growth == 'simple' %}selected{% endif %}
|
||||
>{{'%.2f'|format(bt.ann_growth_simple)}}% {{gt.name}}</option>
|
||||
{% elif gt.name == 'CPI' %}
|
||||
<option value='cpi'
|
||||
{% if bt.which_growth == 'cpi' %}selected{% endif %}
|
||||
>{{'%.2f'|format(cpi)}}% {{gt.name}}</option>
|
||||
{% else %}
|
||||
<option value='flat-{{gt.value}}'
|
||||
{% if bt.which_growth == 'flat-'+gt.value|string %}selected{% endif %}
|
||||
>{{'%.2f'|format(gt.value)}}% {{gt.name}}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="bill-type-chg-{{bt.id}}" class="px-0 col-1 btn btn-success bg-success-subtle text-success" onClick="StartUpdateBillType( {{bt.id}} )"><span class="bi bi-pencil-square"> Change</button>
|
||||
<button id="bill-type-del-{{bt.id}}" class="px-0 col-1 btn btn-danger bg-danger-subtle text-danger" onClick="DelBillType({{bt.id}})"><span class="bi bi-trash3"> Delete</button>
|
||||
<button id="bill-type-save-{{bt.id}}" class="px-0 col-1 btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBillType( {{bt.id}} )"><spam class="bi bi-floppy"> Save</button>
|
||||
<button id="bill-type-canc-{{bt.id}}" class="px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelUpdateBillType({{bt.id}}, '{{bt.name}}')"><span class="bi bi-x"> Cancel</button>
|
||||
{% for yr in range( 2025, 2032 ) %}
|
||||
<div class="px-0 col"><input type="text" class="bill-type-total-{{bt.id}} form-control text-center" id="bill-type-total-{{bt.id}}" value="${{'%.2f'|format(total[bt.id][yr])}}" disabled> </div>
|
||||
{% endfor %}
|
||||
<button id="bill-type-chg-{{bt.id}}" class="px-0 col btn btn-success bg-success-subtle text-success" onClick="StartUpdateBillType( {{bt.id}} )"><span class="bi bi-pencil-square"> Change</button>
|
||||
<button id="bill-type-del-{{bt.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger" onClick="DelBillType({{bt.id}})"><span class="bi bi-trash3"> Delete</button>
|
||||
<button id="bill-type-save-{{bt.id}}" class="px-0 col btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBillType( {{bt.id}} )"><spam class="bi bi-floppy"> Save</button>
|
||||
<button id="bill-type-canc-{{bt.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelUpdateBillType({{bt.id}}, '{{bt.name}}')"><span class="bi bi-x"> Cancel</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="px-0 col"></div>
|
||||
<div class="px-0 col"></div>
|
||||
<div class="px-0 col"><input type="text" class="form-control text-end text-primary fs-5" value="TOTAL:"></div>
|
||||
{% for yr in range( this_year, END_YEAR+1) %}
|
||||
{% set tot=namespace( sum=0 ) %}
|
||||
{% for bt in bill_types %}
|
||||
{% if bt.id in total %}
|
||||
{% set tot.sum = tot.sum + total[bt.id][yr] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="px-0 col"><input type="text" class="form-control text-center text-primary bg-dark fs-5" value="${{'%.2f'|format(tot.sum)}}" disabled></div>
|
||||
{#
|
||||
{% set markup="h5" %}
|
||||
{% if yr == this_year %}
|
||||
{% set markup="h4 pt-4" %}
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="offset-4 col text-end {{markup}}">
|
||||
Total bills in {{yr}}
|
||||
</div>
|
||||
<div class="col {{markup}} text-primary">
|
||||
${{'%.2f'|format(tot.sum)}}
|
||||
</div>
|
||||
</div>
|
||||
#}
|
||||
{% endfor %}
|
||||
<div class="px-0 col"></div>
|
||||
<div class="px-0 col"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- right-hand-side, bill types (e.g. gas, phone, etc.) -->
|
||||
|
||||
<div class="pt-4 col-6">
|
||||
<div class="row align-items-center">
|
||||
<button id="new-bill-data-button" class="mb-3 px-0 offset-6 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillData()"><span class="bi bi-plus-lg"> New Bill</span></button>
|
||||
<div class="col-4">
|
||||
<div class="row">
|
||||
<div class="col-2 form-control-inline d-none new-bill-data-class">Bill Type</div>
|
||||
<div id="new-bill-data-date-label" class="col-4 form-control-inline d-none new-bill-data-class">Date</div>
|
||||
<div id="new-bill-data-growth-label" class="col-4 form-control-inline d-none">Est. Annual Growth</div>
|
||||
<div class="col-2 form-control-inline d-none new-bill-data-class">Amount</div>
|
||||
</div>
|
||||
<div class="row align-items-center mb-3">
|
||||
<button id="new-bill-data-button" class="mt-4 px-0 offset-8 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillData()"><span class="bi bi-plus-lg"> New Bill</span></button>
|
||||
<div class="new-bill-data-class px-0 col-2 d-none"> <select id="new-bill-data-type" class="form-select text-end float-end border border-primary">
|
||||
{% for bt in bill_types %}
|
||||
<option value={{bt.id}}>{{bt.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="new-bill-data-class px-0 col-2 d-none"> <input type="date" class="form-control text-end float-end border border-primary" id="new-bill-data-date"> </div>
|
||||
<div class="new-bill-data-class px-0 col-2 d-none"> <input type="number" class="form-control text-end float-end border border-primary" id="new-bill-data-amount"> </div>
|
||||
<div class="new-bill-data-class px-0 col-4 d-none">
|
||||
<div class="input-group" style="max-width: 300px;">
|
||||
<input type="date" class="form-control" id="new-bill-data-date">
|
||||
<input type="text" class="form-control d-none" id="new-bill-data-growth">
|
||||
<button class="btn btn-outline-danger" type="button" id="toggleDateBtn">When quit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="new-bill-data-class px-0 col-2 d-none">
|
||||
<input type="number" class="form-control text-end float-end border border-primary" id="new-bill-data-amount">
|
||||
</div>
|
||||
<button id="save-bill" class="new-bill-data-class px-0 col-1 btn btn-success bg-success-subtle text-success d-none" onClick="NewBill()">
|
||||
<span class="bi bi-floppy"></span> Save </button>
|
||||
<button class="new-bill-data-class px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelNewBill()" ><span class="bi bi-x"> Cancel</span> </button>
|
||||
<button class="new-bill-data-class px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelNewBill()" >
|
||||
<span class="bi bi-x"> Cancel</span> </button>
|
||||
</div>
|
||||
|
||||
<!-- create tabbed view for each bill type -->
|
||||
@@ -123,11 +216,12 @@
|
||||
<div class="row pt-2">
|
||||
<div class="p-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></div>
|
||||
<div class="p-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Date</ ></div>
|
||||
<div class="p-0 col-2"> <label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Amount</ > </div>
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
|
||||
<div class="p-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Amount</ ></div>
|
||||
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
|
||||
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"></ ></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if bd.bill_type_id == bt.id %}
|
||||
{% if bd.bill_type == bt.id %}
|
||||
{% if bd.estimated == 1 %}
|
||||
<div class="row est d-none fst-italic">
|
||||
{% set classes="fst-italic form-control text-center" %}
|
||||
@@ -136,15 +230,28 @@
|
||||
{% set classes="form-control text-center" %}
|
||||
{% endif %}
|
||||
<div class="px-0 col-2"> <input type="text" class="{{classes}}" id="bill-data-type-{{bd.id}}" value="{{ bd.name }}" disabled> </div>
|
||||
{% if bd.bill_date == 'future' %}
|
||||
<div class="px-0 col-2"> <input type="text" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
|
||||
{% else %}
|
||||
<div class="px-0 col-2"> <input type="date" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
|
||||
<div class="px-0 col-2"> <input type="number" class="{{classes}}" id="bill-data-amount-{{bd.id}}" value="{{ bd.amount }}" disabled> </div>
|
||||
<script>
|
||||
if( typeof future_id !== 'undefined' && future_id>0) {
|
||||
first_col_id={{bd.id}}
|
||||
future_id=0
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
<div class="px-0 col"> <input type="number" class="{{classes}}" id="bill-data-amount-{{bd.id}}" value="{{ bd.amount }}" disabled> </div>
|
||||
{% if bd.estimated == 0 %}
|
||||
<button id="bill-data-chg-{{bd.id}}" class="px-0 col-1 btn btn-success bg-success-subtle text-success" onClick="StartUpdateBill( {{bd.id}} )"><span class="bi bi-pencil-square"> Change</button>
|
||||
<button id="bill-data-del-{{bd.id}}" class="px-0 col-1 btn btn-danger bg-danger-subtle text-danger" onClick="DeleteBill( {{bd.id }} )"><span class="bi bi-trash3"> Delete
|
||||
<button id="bill-data-save-{{bd.id}}" class="px-0 col-1 btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBill( {{bd.id}} )"><span class="bi bi-floppy"> Save</button>
|
||||
<button id="bill-data-canc-{{bd.id}}" class="px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none"
|
||||
<button id="bill-data-chg-{{bd.id}}" class="px-0 col btn btn-success bg-success-subtle text-success" onClick="StartUpdateBill( {{bd.id}} )"><span class="bi bi-pencil-square"> Change</button>
|
||||
<button id="bill-data-del-{{bd.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger" onClick="DeleteBill( {{bd.id }} )"><span class="bi bi-trash3"> Delete
|
||||
<button id="bill-data-save-{{bd.id}}" class="px-0 col btn btn-success bg-success-subtle text-success d-none" onClick="UpdateBill( {{bd.id}} )"><span class="bi bi-floppy"> Save</button>
|
||||
<button id="bill-data-canc-{{bd.id}}" class="px-0 col btn btn-danger bg-danger-subtle text-danger d-none"
|
||||
onClick="CancelUpdateBill({{bd.id}}, '{{bd.name}}', '{{bd.bill_date}}', '{{bd.amount}}')"> <span class="bi bi-x"> Cancel</button>
|
||||
</button>
|
||||
{% else %}
|
||||
<div class="px-0 col"></div>
|
||||
<div class="px-0 col"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -178,10 +285,29 @@
|
||||
|
||||
function NewBill()
|
||||
{
|
||||
if( $('#new-bill-data-growth').hasClass('d-none') )
|
||||
{
|
||||
// if growth is hidden, then we have normal bill
|
||||
$.ajax( { type: 'POST', url: '/newbill',
|
||||
contentType: 'application/json', data: JSON.stringify( { 'name': $('#new-bill-data-type').val(), 'amount': $('#new-bill-data-amount').val(), 'bill_date': $('#new-bill-data-date').val() } ),
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify( {
|
||||
'bill_type': $('#new-bill-data-type').val(),
|
||||
'amount': $('#new-bill-data-amount').val(),
|
||||
'bill_date': $('#new-bill-data-date').val() } ),
|
||||
success: function() { window.location='bills' } } )
|
||||
}
|
||||
else
|
||||
{
|
||||
// if growth is visible, then we have future bill/growth & no date
|
||||
$.ajax( { type: 'POST', url: '/newbill',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify( {
|
||||
'bill_type': $('#new-bill-data-type').val(),
|
||||
'amount': $('#new-bill-data-amount').val(),
|
||||
'growth': $('#new-bill-data-growth').val() } ),
|
||||
success: function() { window.location='bills' } } )
|
||||
}
|
||||
}
|
||||
|
||||
function CancelNewBill()
|
||||
{
|
||||
@@ -220,7 +346,7 @@
|
||||
$.ajax( { type: 'POST', url: '/updatebill',
|
||||
contentType: 'application/json', data: JSON.stringify( {
|
||||
'id': id,
|
||||
'name': $('#bill-data-type-'+id).val(),
|
||||
'bill_type': $('#bill-data-type-'+id).val(),
|
||||
'bill_date' : $('#bill-data-date-'+id).val(),
|
||||
'amount': $('#bill-data-amount-'+id).val() } ),
|
||||
success: function() { window.location='bills' } } )
|
||||
@@ -317,14 +443,17 @@
|
||||
data: JSON.stringify( { 'id': id } ), success: function() { window.location='bills' } } )
|
||||
}
|
||||
|
||||
function UseGrowth( bt, which )
|
||||
function UseGrowth( bt_id )
|
||||
{
|
||||
which = $('#'+bt_id+ '_growth option:selected').val()
|
||||
$.ajax( { type: 'POST', url: '/usegrowth', contentType: 'application/json',
|
||||
data: JSON.stringify( { 'bill_type': bt, 'which_growth': which } ), success: function() { window.location='bills' } } )
|
||||
data: JSON.stringify( { 'bill_type': bt_id, 'which_growth': which } ), success: function() { window.location='bills' } } )
|
||||
}
|
||||
|
||||
function SaveTab( last_tab )
|
||||
{
|
||||
// set the drop-down for new bill to be this tab now...
|
||||
$("#new-bill-data-type").val( $('.nav-tabs .nav-link.active').prop('id').replace("tab-but-", "") )
|
||||
$.ajax( { type: 'POST', url: '/saveui', contentType: 'application/json', data: JSON.stringify( { 'last_tab': last_tab } ), success: function() { } } )
|
||||
}
|
||||
|
||||
@@ -347,8 +476,50 @@
|
||||
{% else %}
|
||||
$('#tab-but-1').tab('show');
|
||||
{% endif %}
|
||||
// make the new bill drop-down default to the same as the current tab
|
||||
$("#new-bill-data-type").val( {{bill_ui.last_tab}} )
|
||||
} )
|
||||
|
||||
$(function () {
|
||||
let disabled = false;
|
||||
|
||||
$('#toggleDateBtn').on('click', function () {
|
||||
disabled = !disabled;
|
||||
|
||||
if (disabled) {
|
||||
$('#new-bill-data-date').addClass('d-none')
|
||||
$('#new-bill-data-growth').removeClass('d-none')
|
||||
$(this)
|
||||
.removeClass('btn-outline-danger')
|
||||
.addClass('btn-outline-success')
|
||||
.html('Normal date');
|
||||
$('#new-bill-data-date-label').addClass('d-none')
|
||||
$('#new-bill-data-growth-label').removeClass('d-none')
|
||||
} else {
|
||||
$('#new-bill-data-date').removeClass('d-none')
|
||||
$('#new-bill-data-growth').addClass('d-none')
|
||||
$(this)
|
||||
.removeClass('btn-outline-success')
|
||||
.addClass('btn-outline-danger')
|
||||
.html('When quit');
|
||||
$('#new-bill-data-date-label').removeClass('d-none')
|
||||
$('#new-bill-data-growth-label').addClass('d-none')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function ForceRecalcBills()
|
||||
{
|
||||
$.ajax( { type: 'POST', url: '/force_recalc_bills', contentType: 'application/json', success: function() { window.location='bills' } } )
|
||||
}
|
||||
|
||||
/*
|
||||
$(document.ready() {
|
||||
for( bt in future_ids ) {
|
||||
$('#bill-data-date-'+future_ids[bt]).width( $('#bill-data-date-'+first_col_id[bt]).width() )
|
||||
}
|
||||
}
|
||||
*/
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
66
templates/cset.html
Normal file
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,6 +103,19 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- create tabbed view for each bill type -->
|
||||
<nav id="ft-nav" class="nav nav-tabs pt-3">
|
||||
<button class="nav-link" id="tab-but-findata" data-bs-toggle="tab" data-bs-target="#tab-findata" type="button" role="tab" aria-controls="tab1" aria-selected="true" >Fortnighthly Savings data</button>
|
||||
<button class="nav-link" id="tab-but-graph" data-bs-toggle="tab" data-bs-target="#tab-graph" type="button" role="tab" aria-controls="tab2" aria-selected="true" >Graph of savings over time</button>
|
||||
{% if COMP %}
|
||||
<button class="nav-link" id="tab-but-compgraph" data-bs-toggle="tab" data-bs-target="#tab-compgraph" type="button" role="tab" aria-controls="tab3" aria-selected="true" >Comparison graph</button>
|
||||
{% else %}
|
||||
<button class="nav-link" id="tab-but-compgraph" data-bs-toggle="tab" data-bs-target="#tab-compgraph" type="button" role="tab" aria-controls="tab3" aria-selected="true" disabled>Comparison graph</button>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="tab-findata" class="tab-pane">
|
||||
<h5 align="center" class="mt-4">Fortnighthly Savings data:
|
||||
{% if COMP %}
|
||||
{# get comparison date so we can use it below in loop to know when to print it out #}
|
||||
@@ -92,26 +130,45 @@
|
||||
</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">2025</div>
|
||||
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">{{savings[0][0][:4]}}</div>
|
||||
|
||||
{# inside started if below, we add blank lines to the start of the year so the dates line up #}
|
||||
{% for _ in range( 0, padding ) %}
|
||||
<br>
|
||||
{% endfor %}
|
||||
|
||||
{% set car_done=namespace( val=0 ) %}
|
||||
|
||||
{% for date, dollars in savings %}
|
||||
{% set yr=date[:4] %}
|
||||
{% set mon=date[5:7] %}
|
||||
{% set day=date[8:10 ] %}
|
||||
|
||||
{% set car_yr=key_dates['D_hyundai_owned'][:4] %}
|
||||
{% set car_mon=key_dates['D_hyundai_owned'][5:7] %}
|
||||
{% set car_day=key_dates['D_hyundai_owned'][8:10 ] %}
|
||||
|
||||
{% if yr|int > first_yr|int and mon == '01' and day|int <= 14 %}
|
||||
</div><div class="col-auto">
|
||||
<div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">{{yr}}</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if date == key_dates['D_quit_date'] %}
|
||||
<font class="text-warning">
|
||||
<label data-bs-toggle="tooltip" title="D quits">
|
||||
{% elif (yr == car_yr and mon == car_mon and day >= car_day and car_done.val == 0) %}
|
||||
{%set car_done.val=1 %}
|
||||
<font class="text-warning">
|
||||
<label data-bs-toggle="tooltip" title="We own car">
|
||||
{% else %}
|
||||
<font class="text-secondary">
|
||||
<label>
|
||||
{% endif %}
|
||||
{{ date }}:
|
||||
{{ '$%0.2f' % dollars|float }}<br>
|
||||
</font>
|
||||
</label>
|
||||
</font><br>
|
||||
{% if comp_done.val == 0 and yr == comp_yr and mon == comp_mon and day|int >= comp_day|int %}
|
||||
<font class="text-info">
|
||||
{{ COMP['date'] }}:
|
||||
@@ -137,9 +194,9 @@
|
||||
</div>
|
||||
<div id="comp_col" class="col-auto">
|
||||
<div class="input-group">
|
||||
<button type="button" class="btn btn-primary me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save</button>
|
||||
<button type="submit" class="disabled btn btn-primary rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button>
|
||||
<select class="form-select border border-primary text-primary" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
|
||||
<button type="button" class="btn btn-info me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save Comparison set</button>
|
||||
<button type="submit" class="disabled btn btn-info rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button>
|
||||
<select class="form-select border border-info text-info" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
|
||||
{% for el in finance['COMP_SETS'] %}
|
||||
<option value="{{el[0]}}">{{el[1]}}</option>
|
||||
{% endfor %}
|
||||
@@ -151,13 +208,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row mt-4 highcharts-dark" id="container" style="width:100%; height:400px;"></div>
|
||||
|
||||
<div id="tab-graph" class="tab-pane">
|
||||
<div class="row mt-4 highcharts-dark" id="graph" style="width:100%; height:800px;"></div>
|
||||
</div>
|
||||
<div id="tab-compgraph" class="tab-pane">
|
||||
<div class="row mt-4 highcharts-dark" id="graph-comp" style="width:100%; height:800px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
// make these global so we can also use them in the /save route (via modal)
|
||||
const savingsData = JSON.parse('{{ savings | tojson }}');
|
||||
const vars = JSON.parse('{{ finance | tojson }}');
|
||||
|
||||
$(function() { $('[data-bs-toggle="popover"]').popover(); });
|
||||
window.onload = function() {
|
||||
$('#Sell_shares').val( {{finance['Sell_shares']}} )
|
||||
$('#compare_to').val( {{finance['compare_to']}} )
|
||||
@@ -190,6 +256,7 @@
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
$('#tab-but-findata').click()
|
||||
// Parse the savings_data from Flask
|
||||
const chartData = savingsData.map(entry => [new Date(entry[0]).getTime(), parseFloat(entry[1])]);
|
||||
{% if COMP %}
|
||||
@@ -231,10 +298,9 @@
|
||||
});
|
||||
|
||||
const annotations = [];
|
||||
// the al, x, offset are used to make the altenrate annotations be on slightly different vertical offsets (size is based on $'s)
|
||||
// al alternates every 2 annotations left / right (so 2 left, then 2 right), x is just used to also move the label more left/right to get the connecting line
|
||||
// offset is used to make the next annotation be on slightly different vertical offsets (size is based on $'s)
|
||||
// HACK: start at 13, later we adjust in steps of 50s allowing 4 steps, then we go back to top
|
||||
var offset=13
|
||||
{% if not COMP %}
|
||||
// Add annotations for changes greater than 5000
|
||||
{% for a in finance['annotations'] %}
|
||||
annotations.push({
|
||||
@@ -247,18 +313,26 @@
|
||||
yAxis: 0
|
||||
},
|
||||
x: -70,
|
||||
{% if '-$' in a['label'] %}
|
||||
y: offset,
|
||||
{% else %}
|
||||
y: -20,
|
||||
{% endif %}
|
||||
text: '{{a['label']}}'
|
||||
}], labelOptions: { allowOverlap: true }
|
||||
});
|
||||
{% if a['y'] > 200000 %}
|
||||
offset = ({{loop.index}} * 50 % 200) +50
|
||||
{% else %}
|
||||
offset = -100
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
document.keep = annotations
|
||||
{% endif %}
|
||||
|
||||
// Highcharts configuration
|
||||
Highcharts.chart('container', {
|
||||
Highcharts.chart('graph', {
|
||||
chart: { type: 'line' },
|
||||
colors: [ 'orange' ],
|
||||
title: { text: 'Savings Over Time' },
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
@@ -274,13 +348,37 @@
|
||||
}, shared:true
|
||||
},
|
||||
annotations: annotations, // Add annotations
|
||||
series: [ { name: "Savings", data: chartData, marker: { radius: 2 } } ]
|
||||
});
|
||||
|
||||
{% if COMP %}
|
||||
// Highcharts configuration
|
||||
Highcharts.chart('graph-comp', {
|
||||
chart: { type: 'line' },
|
||||
colors: [
|
||||
'orange', // Custom color 1
|
||||
'cyan', // Custom color 2
|
||||
],
|
||||
title: { text: 'Savings Over Time' },
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
title: { text: 'Date' },
|
||||
plotBands: plotBands // Alternating background for years
|
||||
},
|
||||
yAxis: { title: { text: 'Amount ($)' } },
|
||||
tooltip: {
|
||||
pointFormatter: function ()
|
||||
{
|
||||
if( this.series.symbol == 'circle' ) { s='\u25CF' } else { s='\u2B25' }
|
||||
return '<span style="color:' + this.point.color + '">' + s + '</span> <b>' + this.point.y + ':</b> ' + this.series.name + '<br>'
|
||||
}, shared:true
|
||||
},
|
||||
series: [
|
||||
{ name: "Savings", data: chartData, marker: { radius: 2 } }
|
||||
{% if COMP %}
|
||||
,{ name: "{{COMP['vars']['name']}}", data: compChartData, marker: { radius: 2 } }
|
||||
{% endif %}
|
||||
]
|
||||
});
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
<div id="save_modal" class="modal modal-lg" tabindex="-1">
|
||||
@@ -301,7 +399,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary"
|
||||
<button type="button" class="btn btn-info"
|
||||
onClick="
|
||||
vars['name']=$('#save_name').val();
|
||||
$.ajax( {
|
||||
@@ -317,4 +415,3 @@
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user