Compare commits
108 Commits
7bab6eabdd
...
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 | |||
| 89d58e4cd3 | |||
| 65fc68e0bf | |||
| f3b828b051 | |||
| 4b5b713c20 | |||
| 54c4c38403 | |||
| 1719032ebf | |||
| 6bccfade2b | |||
| b05f7b05e8 | |||
| 5bd94fc2c5 | |||
| 444a01ea42 | |||
| 1729c93bcd | |||
| 78141d097f | |||
| 0cf3d9897f | |||
| 3bfeb30640 | |||
| aa0512087f | |||
| 3521d4c126 | |||
| ada6dfa3f5 | |||
| 19cba866de | |||
| cd7eca0c6e | |||
| c469f6d281 | |||
| 3d95cd1d2e | |||
| d7320e8aa8 | |||
| 5556b0ef15 | |||
| 9cd14505bf | |||
| b43b472e4b | |||
| 5ebd623d88 | |||
| 676e9ab95f | |||
| 7ac7acf44c | |||
| 232f16deba | |||
| a1ed4e364c | |||
| c05fa1cc61 | |||
| 0df1d4d2d2 | |||
| 28b07c0842 | |||
| cf104b5a56 | |||
| 98fa17acd7 | |||
| 6403ca7775 | |||
| adac3eceeb | |||
| 27048a450f |
6
BUGS
6
BUGS
@@ -1,2 +1,4 @@
|
||||
when I cancel items, the changes need to be reverted for:
|
||||
new bill, new bill type, change bill
|
||||
* 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:
|
||||
|
||||
|
||||
24
TODO
Normal file
24
TODO
Normal file
@@ -0,0 +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:
|
||||
* 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
|
||||
683
bills.py
Normal file
683
bills.py
Normal file
@@ -0,0 +1,683 @@
|
||||
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
|
||||
|
||||
|
||||
################################################################################
|
||||
# this finds start and end dates of a quarter for a given date
|
||||
################################################################################
|
||||
def quarter_bounds(d):
|
||||
q = (d.month-1)//3
|
||||
start = date(d.year, 3*q+1, 1)
|
||||
# last day of quarter = first day of next quarter minus 1 day
|
||||
if q == 3:
|
||||
next_start = date(d.year+1, 1, 1)
|
||||
else:
|
||||
next_start = date(d.year, 3*q+4, 1)
|
||||
end = next_start - timedelta(days=1)
|
||||
return start, end
|
||||
|
||||
################################################################################
|
||||
# takes a bill and its previous bill, works out days between and adds cost / day
|
||||
# to each quarter the bill covers from prev. to now. Usually means it splits
|
||||
# one bill in a previous and this qtr (or just puts it all into the current qtr)
|
||||
################################################################################
|
||||
def allocate_by_quarter( bill_info, bill_type, yr, prev_bill, bill):
|
||||
start = date( int(prev_bill['bill_date'][:4]), int(prev_bill['bill_date'][5:7]), int(prev_bill['bill_date'][8:]))
|
||||
end = date( int(bill['bill_date'][:4]), int(bill['bill_date'][5:7]), int(bill['bill_date'][8:]))
|
||||
|
||||
time_difference = end - start
|
||||
days = time_difference.days
|
||||
cost_per_day = bill['amount']/days
|
||||
if end < start:
|
||||
return {}
|
||||
if not 'qtr' in bill_info[bill_type]:
|
||||
bill_info[bill_type]['qtr'] = {}
|
||||
|
||||
q_start, q_end = quarter_bounds(start)
|
||||
cur = q_start
|
||||
# walk quarters that might overlap - start from the quarter of `start`, iterate until past `end`
|
||||
while cur <= end:
|
||||
q_start, q_end = quarter_bounds(cur)
|
||||
overlap_start = max(start, q_start)
|
||||
overlap_end = min(end, q_end)
|
||||
# only add qtr total for yr being calc'd
|
||||
if overlap_end >= overlap_start:
|
||||
days = (overlap_end - overlap_start).days + 1
|
||||
q = (q_start.month-1)//3 + 1
|
||||
# initialise arrays if needed
|
||||
if q_start.year not in bill_info[bill_type]['qtr']:
|
||||
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)
|
||||
return
|
||||
|
||||
################################################################################
|
||||
# given a bill date in format YYYY-MM-DD, return quarter (1-4)
|
||||
################################################################################
|
||||
def qtr(d):
|
||||
m = int(d[5:7])
|
||||
return ( (m-1)//3 + 1 )
|
||||
|
||||
################################################################################
|
||||
# find the bill just after the date given
|
||||
################################################################################
|
||||
def find_next_bill( bill_type, bill_info, bill_date ):
|
||||
wanted_year = int(bill_date[:4])
|
||||
wanted_mm = int(bill_date[5:7])
|
||||
# if we want a bill after our last year, just return None
|
||||
if int(wanted_year) > int(bill_info[bill_type]['last_bill_year']):
|
||||
return None
|
||||
|
||||
for yr in range( wanted_year, bill_info[bill_type]['last_bill_year']+1 ):
|
||||
# start with bills in the year wanted (if any)
|
||||
if yr in bill_info[bill_type]['year']:
|
||||
# reverse this list so we can q1 bills before q4
|
||||
for bill in bill_info[bill_type]['year'][yr][::-1]:
|
||||
bill_mm = int(bill['bill_date'][5:7])
|
||||
# if bill is in this year but later OR its a later year, return this bill
|
||||
if (wanted_year == yr and bill_mm > wanted_mm) or wanted_year < yr:
|
||||
return bill
|
||||
# failsafe
|
||||
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])
|
||||
wanted_mm = int(bill_date[5:7])
|
||||
# if we don't have a bill before this date, no way to set price
|
||||
if int(wanted_year) < int(bill_info[bill_type]['first_bill_year']):
|
||||
return None
|
||||
|
||||
# start loop from bill_date, go backwards and find which one it is (same year, should be month-based)
|
||||
# earlier year, then just last one from the year.
|
||||
yr_range=range( wanted_year, bill_info[bill_type]['first_bill_year']-1, -1 )
|
||||
if wanted_year == int(bill_info[bill_type]['first_bill_year']):
|
||||
# range of this year with -1, does not return anything, so force this year.
|
||||
yr_range=[ wanted_year ]
|
||||
|
||||
for yr in yr_range:
|
||||
# start with bills in the year wanted (if any)
|
||||
# must include 'estimated' bills to deal with growth of future years
|
||||
if yr in bill_info[bill_type]['year']:
|
||||
# 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
|
||||
for bill in bill_info[bill_type]['year'][yr]:
|
||||
bill_mm = int(bill['bill_date'][5:7])
|
||||
# reversing the bills, means we start with the 'most recent' in this year to the oldest
|
||||
# if the month we want is after the bill, we are done
|
||||
if wanted_mm > bill_mm:
|
||||
return bill
|
||||
return None
|
||||
|
||||
|
||||
# quick wrapper to add a new estimated bill - new estimates have the flag in
|
||||
# the DB set, but also we update bill_info to reflect the new bill so future
|
||||
# growth can build of this esimate too - e.g 2030 can use 2029, etc
|
||||
def new_estimated_bill( bill_info, yr, bill_type, amt, new_date ):
|
||||
# add to DB
|
||||
new_bill( bill_type, amt, new_date, 1 )
|
||||
|
||||
# patch this data back into bill_info so growth works in future
|
||||
if not yr in bill_info[bill_type]['year']:
|
||||
bill_info[bill_type]['year'][yr]=[]
|
||||
bill={}
|
||||
bill['bill_date']=new_date
|
||||
bill['amount']=amt
|
||||
bill['bill_type']=bill_type
|
||||
bill['estimated']=1
|
||||
# need to insert(0,) to add this "newest" bill to start of the data for {yr} so that find_previous_bill can work - only need the above 3 fields
|
||||
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:
|
||||
allocate_by_quarter( bill_info, bill_type, yr, pb, bill )
|
||||
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
|
||||
|
||||
|
||||
# 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
|
||||
# future only, so add ann_growth (based on drop-down) for each future year
|
||||
# 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...
|
||||
amt += amt * bill_info[bill_type]['growth']/100
|
||||
|
||||
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
|
||||
# add growth (based on drop-down) for each future year
|
||||
def add_missing_quarter_bills_in_yr( bill_type, bill_info, yr ):
|
||||
# okay we have data for last year but some missing (in this year), lets fill in gaps
|
||||
# could be called if only have data for q2 - q4 in first year and we dont have a previous years q1 data so don't try
|
||||
if 'qtr' in bill_info[bill_type] and yr-1 in bill_info[bill_type]['qtr']:
|
||||
# if we do have data in this year, we have q1-q3 only, and want missing qtrs set range appropriately...
|
||||
if yr in bill_info[bill_type]['qtr']:
|
||||
# per if above, ONLY get here if we have first few bills of {yr}, cannot be last few
|
||||
have_q = qtr( bill_info[bill_type]['year'][yr][0]['bill_date'] )
|
||||
r=range(have_q+1,5)
|
||||
else:
|
||||
r=range(1,5)
|
||||
for q in r:
|
||||
if 'forced' in bill_info[bill_type]['freq']:
|
||||
actually_add_estimated_new_quarter_bill_forced(bill_type, bill_info, yr, q)
|
||||
else:
|
||||
actually_add_estimated_new_quarter_bill(bill_type, bill_info, yr, q)
|
||||
return
|
||||
|
||||
################################################################################
|
||||
# func take a qtr in a year, finds equiv from previous year, calcs new based on
|
||||
# it (same 'day' with amt * growth)
|
||||
################################################################################
|
||||
def actually_add_estimated_new_quarter_bill( bill_type, bill_info, yr, q ):
|
||||
|
||||
# amt is total of last year's qtr bill (NOTE: use 4-q, bills are in desc order)
|
||||
last_yrs_bill_in_this_q = bill_info[bill_type]['year'][yr-1][4-q]
|
||||
amt = last_yrs_bill_in_this_q['amount']*(1+bill_info[bill_type]['growth']/100)
|
||||
|
||||
# make new qtr bill same 'day' (mm-dd) as last year, just chg (yr)
|
||||
mmdd=last_yrs_bill_in_this_q['bill_date'][5:]
|
||||
new_date = f'{yr}-{mmdd}'
|
||||
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
|
||||
return
|
||||
|
||||
def actually_add_estimated_new_quarter_bill_forced( bill_type, bill_info, yr, q ):
|
||||
last_yrs_qtr_amount = bill_info[bill_type]['qtr'][yr-1][q]
|
||||
amt=last_yrs_qtr_amount*(1+bill_info[bill_type]['growth']/100)
|
||||
new_date = f'{yr}-{q*3:02d}-01'
|
||||
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
|
||||
return
|
||||
|
||||
# missing monthly bills, find date based on DD and put in each missing month
|
||||
# add growth (based on drop-down) for each future year
|
||||
# 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:]
|
||||
mm = bill_info[bill_type]['first_bill']['bill_date'][5:7]
|
||||
lb_mm = bill_info[bill_type]['last_bill']['bill_date'][5:7]
|
||||
|
||||
#okay add monthly bills for the rest of this year if its the first year
|
||||
if bill_info[bill_type]['first_bill_year'] == yr:
|
||||
start_m=int(mm)
|
||||
else:
|
||||
start_m=0
|
||||
|
||||
# fill in rest of this year
|
||||
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 new_date_yymm in str(b['bill_date']):
|
||||
bill_found=True
|
||||
break
|
||||
if not bill_found:
|
||||
pb=find_previous_bill( bill_type, bill_info, new_date )
|
||||
nb=find_next_bill( bill_type, bill_info, new_date )
|
||||
if not pb:
|
||||
print("Failed to find previous_bill, can't calculate missing bill - returning" )
|
||||
return
|
||||
|
||||
amt = pb['amount']
|
||||
# if there is no next bill then use growth, otherwise, I am only putting in real bills
|
||||
# where changes occur, so keep the pb amount 'unchanged'
|
||||
if not nb:
|
||||
# if this month is the same as the last bill month and as per above
|
||||
# we don't have a bill for this date, then add annual grotwh
|
||||
if i == int(lb_mm):
|
||||
amt += amt * bill_info[bill_type]['growth']/100
|
||||
bill_info[bill_type]['last_bill_amount']=amt
|
||||
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
|
||||
return
|
||||
|
||||
# given the bill_type has a which_growth contain min/avg/max, return the corresponding growth number
|
||||
def get_growth_value( bt, bill_type ):
|
||||
for el in bt:
|
||||
if el['id'] == bill_type:
|
||||
which = el['which_growth']
|
||||
break
|
||||
|
||||
if which == 'avg':
|
||||
return el['ann_growth_avg']
|
||||
elif which == 'min':
|
||||
return el['ann_growth_min']
|
||||
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
|
||||
|
||||
|
||||
################################################################################
|
||||
# go through the bill data from the DB, put it into more friendly formats, then
|
||||
# work out and then add missing bill data (might be b/c we have monthly bills,
|
||||
# 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, 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']
|
||||
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']={}
|
||||
bill_info[bill_type]['last_bill']={}
|
||||
# due to sql sorting, this first instance is the last bill
|
||||
bill_info[bill_type]['last_bill']=bill
|
||||
bill_info[bill_type]['last_bill_year']=int(bill['bill_date'][:4])
|
||||
if not bill['estimated']:
|
||||
bill_info[bill_type]['last_real_bill_year']=int(bill['bill_date'][:4])
|
||||
bill_info[bill_type]['year']={}
|
||||
if not yr in bill_info[bill_type]['year']:
|
||||
bill_info[bill_type]['year'][yr]=[]
|
||||
|
||||
# keep updating last to this matching bill
|
||||
bill_info[bill_type]['first_bill']=bill
|
||||
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])
|
||||
# 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]]
|
||||
|
||||
if 'last_bill' not in bill_info[bill_type]:
|
||||
print("Cannot process bill_type={bill_type} - no bill info for it at all" )
|
||||
# range of years to process (yr_min to yr_max)
|
||||
yr_min=int(bill_info[bill_type]['first_bill']['bill_date'][:4])
|
||||
yr_max=int(bill_info[bill_type]['last_bill']['bill_date'][:4])
|
||||
|
||||
ProportionQtrlyData( bill_type, bill_info )
|
||||
|
||||
# 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
|
||||
# > 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, 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
|
||||
################################################################################
|
||||
def add_missing_bills_for_yr( bill_type, bill_info, yr ):
|
||||
num = bill_info[bill_type]['num_ann_bills']
|
||||
if num == 1:
|
||||
add_missing_annual_bill_in_yr( bill_type, bill_info, yr )
|
||||
elif num == 4:
|
||||
add_missing_quarter_bills_in_yr( bill_type, bill_info, yr )
|
||||
elif num == 12:
|
||||
add_missing_monthly_bills_in_yr( bill_type, bill_info, yr )
|
||||
return
|
||||
|
||||
################################################################################
|
||||
# Takes qtrly bills and start from 2nd year of bills (so we can estimate growth)
|
||||
# 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
|
||||
# this allows us to aggregate per quarter and use matching quarter
|
||||
################################################################################
|
||||
def ProportionQtrlyData( bill_type, bill_info ):
|
||||
# just do up to now for the moment so that add_missing_bills later will have qtr data to use
|
||||
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'], 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:
|
||||
continue
|
||||
allocate_by_quarter( bill_info, bill_type, yr, pb, b )
|
||||
return
|
||||
|
||||
################################################################################
|
||||
# function to work out totals per year, and then calcuates annual growth in
|
||||
# terms of min/avg/max - uses qtr data for qtrly bills, or just normal totals
|
||||
# for other bill types
|
||||
################################################################################
|
||||
def derive_ann_growth( bill_type, bill_info, key_dates ):
|
||||
# just do up to now so we stop earlier than looking at other estimated (just an optimisation)
|
||||
now_yr = datetime.date.today().year
|
||||
|
||||
total={}
|
||||
for yr in range( bill_info[bill_type]['first_bill_year'], now_yr+1):
|
||||
# if not enough bills in this year (or none), then try next year (first year might have not enough bills)
|
||||
if yr not in bill_info[bill_type]['year'] or len(bill_info[bill_type]['year'][yr]) < bill_info[bill_type]['num_ann_bills']:
|
||||
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'] or bill_info[bill_type]['num_ann_bills'] ==1:
|
||||
skip_yr=False
|
||||
for b in bill_info[bill_type]['year'][yr]:
|
||||
if b['estimated']:
|
||||
skip_yr=True
|
||||
if skip_yr:
|
||||
continue
|
||||
|
||||
total[yr] = 0
|
||||
for b in bill_info[bill_type]['year'][yr]:
|
||||
total[yr] += b['amount']
|
||||
|
||||
# crazily we can have more bills in this year than expected, so work out qtrly costs, and patch that back into total array
|
||||
for yr in range( bill_info[bill_type]['first_bill_year'], now_yr+1):
|
||||
if 'qtr' in bill_info[bill_type] and yr in bill_info[bill_type]['qtr']:
|
||||
tot=0
|
||||
for q in range( 1,5 ):
|
||||
tot += bill_info[bill_type]['qtr'][yr][q]
|
||||
if yr in total:
|
||||
# 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:
|
||||
# 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
|
||||
151
calc.py
151
calc.py
@@ -1,9 +1,35 @@
|
||||
# calc.py
|
||||
from datetime import datetime, timedelta
|
||||
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
|
||||
@@ -14,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']
|
||||
@@ -56,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}")
|
||||
|
||||
@@ -79,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
|
||||
@@ -90,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(2031, 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")
|
||||
@@ -125,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)
|
||||
@@ -133,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
|
||||
|
||||
@@ -147,29 +176,17 @@ def calculate_savings_depletion(finance):
|
||||
current_savings -= Car_loan_via_pay
|
||||
print( f"{current_date}: making car loan pay as pre-tax lease: ${Car_loan_via_pay}" )
|
||||
|
||||
# no more pay and if leave after tax > 0 this is the day I quit
|
||||
if D_Num_fortnights_pay == 0 and D_leave_after_tax > 0:
|
||||
D_has_quit = True
|
||||
D_quit_year = current_date.year
|
||||
# okay, if we leave before Jun 30th 2024, then I pay full tax, otherwise I get 'extra', but have to await end of next fin year
|
||||
if current_date > new_fin_year_25:
|
||||
claim_tax_on_leave = True
|
||||
print(f"{current_date}: D has resigned in new year- get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax_new_fin_year}" )
|
||||
current_savings += D_leave_after_tax_new_fin_year
|
||||
add_annotation(finance, current_date, current_savings, D_leave_after_tax_new_fin_year, "D quit" )
|
||||
else:
|
||||
claim_tax_on_leave = False
|
||||
print(f"{current_date}: D has resigned - get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax}" )
|
||||
current_savings += D_leave_after_tax
|
||||
add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
|
||||
D_quit_date = current_date
|
||||
# going to pay tax on payout, so claim it back next year
|
||||
claim_tax_on_leave = True
|
||||
print(f"{current_date}: D has resigned in new year- get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax}" )
|
||||
current_savings += D_leave_after_tax
|
||||
add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
|
||||
D_leave_after_tax = 0
|
||||
|
||||
# its end of 'next' fin year, if tax_diff > 0, then ddp quit after new tax year and gets back the overpaid tax
|
||||
if current_date > new_fin_year_26 and claim_tax_on_leave:
|
||||
current_savings += tax_diff_D_leave
|
||||
add_annotation(finance, current_date, current_savings, tax_diff_D_leave, "D quit - tax back" )
|
||||
# can only claim the tax back once :)
|
||||
claim_tax_on_leave=False
|
||||
|
||||
if fortnight_income:
|
||||
print(f"{current_date}: salary paid by Deakin - adding: {fortnight_income}" )
|
||||
current_savings += fortnight_income
|
||||
@@ -177,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:
|
||||
@@ -190,10 +215,11 @@ def calculate_savings_depletion(finance):
|
||||
current_savings += monthly_interest
|
||||
#print(f"{current_date}: interest paid - ${monthly_interest}")
|
||||
monthly_interest = 0
|
||||
|
||||
# monthly increase living expenses by a monthly inflation multiplier
|
||||
Living_Expenses += (Inflation/100.0)/12 * Living_Expenses
|
||||
daily_living_expenses = Living_Expenses / 365
|
||||
#print(f"{current_date}: Living Exp inceased - ${Living_Expenses}")
|
||||
# print(f"{current_date}: Living Exp inceased - ${Living_Expenses}")
|
||||
|
||||
if current_date.date() == school_fees_date.date():
|
||||
current_savings -= School_Fees
|
||||
@@ -209,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" )
|
||||
@@ -250,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)
|
||||
@@ -280,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
|
||||
|
||||
|
||||
200
db.py
200
db.py
@@ -84,9 +84,21 @@ 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,
|
||||
name STRING
|
||||
freq INTEGER,
|
||||
name STRING,
|
||||
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_data (
|
||||
@@ -94,25 +106,66 @@ def init_db():
|
||||
bill_type INTEGER,
|
||||
amount INTEGER,
|
||||
bill_date DATE,
|
||||
estimated INTEGER,
|
||||
FOREIGN KEY(bill_type) REFERENCES bill_type(id)
|
||||
)''')
|
||||
|
||||
# Check if table is empty, if so insert default values
|
||||
cur.execute('''CREATE TABLE IF NOT EXISTS bill_ui (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
last_tab INTEGER,
|
||||
show_estimated INTEGER
|
||||
)''')
|
||||
|
||||
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, '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()
|
||||
|
||||
@@ -125,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}") )
|
||||
@@ -245,10 +299,17 @@ def get_comp_set_options(finance):
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def get_bill_data():
|
||||
def get_bill_data(order_by):
|
||||
conn = connect_db(True)
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT bd.id, bt.id as bill_type_id, bt.name, bd.amount, bd.bill_date FROM bill_type bt, bill_data bd where bt.id = bd.bill_type order by bt.name, bd.bill_date')
|
||||
if order_by == "order_by_date_only":
|
||||
cur.execute('''SELECT bd.id, bt.id as bill_type, bt.name, bd.amount, bd.bill_date, bd.estimated
|
||||
FROM bill_type bt, bill_data bd
|
||||
where bt.id = bd.bill_type order by bd.bill_date desc''')
|
||||
else:
|
||||
cur.execute('''SELECT bd.id, bt.id as bill_type, bt.name, bd.amount, bd.bill_date, bd.estimated
|
||||
FROM bill_type bt, bill_data bd
|
||||
where bt.id = bd.bill_type order by bt.name, bd.bill_date desc''')
|
||||
bd = cur.fetchall()
|
||||
conn.close()
|
||||
return bd
|
||||
@@ -256,15 +317,40 @@ def get_bill_data():
|
||||
def get_bill_types():
|
||||
conn = connect_db(True)
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT id, name FROM bill_type order by name')
|
||||
cur.execute('SELECT * FROM bill_type order by name')
|
||||
bt = cur.fetchall()
|
||||
conn.close()
|
||||
return bt
|
||||
|
||||
def new_bill( name, amount, bill_date ):
|
||||
def use_growth( bill_type, which_growth ):
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
cur.execute( f"insert into bill_data ( 'bill_type', 'amount', 'bill_date' ) values ( '{name}', '{amount}', '{bill_date}' )" )
|
||||
cur.execute( f"update bill_type set which_growth = '{which_growth}' where id = {bill_type}" )
|
||||
# okay, new growth type being used, delete old estimated bills are recreate them
|
||||
cur.execute( f"delete from bill_data where estimated=1 and bill_type = {bill_type}" )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def get_bill_freqs():
|
||||
conn = connect_db(True)
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT * FROM bill_freq order by name')
|
||||
bf = cur.fetchall()
|
||||
conn.close()
|
||||
return bf
|
||||
|
||||
|
||||
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()
|
||||
return
|
||||
@@ -277,18 +363,19 @@ def update_bill_data( id, name, amount, bill_date ):
|
||||
conn.close()
|
||||
return
|
||||
|
||||
def insert_bill_type( bt ):
|
||||
def insert_bill_type( bt, fq ):
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
cur.execute( f"insert into bill_type ( 'name' ) values ( '{bt}' )" )
|
||||
print( f"fq={fq}" )
|
||||
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
|
||||
|
||||
def update_bill_type(id, bill_type):
|
||||
def update_bill_type(id, bill_type, freq):
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
cur.execute( f"update bill_type set name ='{bill_type}' where id = {id}" )
|
||||
cur.execute( f"update bill_type set name ='{bill_type}', freq={freq} where id = {id}" )
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
@@ -308,3 +395,72 @@ def delete_bill_type( id ):
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
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}, 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()
|
||||
# only ever be 1
|
||||
cur.execute('SELECT * FROM bill_ui')
|
||||
ui = cur.fetchone()
|
||||
conn.close()
|
||||
return ui
|
||||
|
||||
def save_ui(data):
|
||||
conn = connect_db(False)
|
||||
cur = conn.cursor()
|
||||
if 'last_tab' in data:
|
||||
cur.execute( f"update bill_ui set last_tab='{data['last_tab']}'" )
|
||||
if 'show_estimated' in data:
|
||||
cur.execute( f"update bill_ui set show_estimated='{data['show_estimated']}'" )
|
||||
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
|
||||
|
||||
1
defines.py
Normal file
1
defines.py
Normal file
@@ -0,0 +1 @@
|
||||
END_YEAR=2031
|
||||
124
main.py
124
main.py
@@ -1,13 +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
|
||||
from db import get_bill_data, new_bill, update_bill_data, delete_bill
|
||||
from db import get_bill_types, insert_bill_type, update_bill_type, delete_bill_type
|
||||
from calc import calculate_savings_depletion, calc_key_dates
|
||||
from db import init_db, get_finance_data, update_finance, get_budget_data
|
||||
from db import insert_cset, get_comp_set_data, get_comp_set_options, delete_cset
|
||||
from db import get_bill_freqs, get_bill_growth_types
|
||||
from db import get_bill_data, new_bill, update_bill_data, delete_bill, delete_estimated_bills, delete_estimated_bills_for
|
||||
from db import get_bill_ui, save_ui
|
||||
from db import get_bill_types, insert_bill_type, update_bill_type, delete_bill_type, use_growth
|
||||
from bills import process_bill_data, calc_future_totals, 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__)
|
||||
@@ -22,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:
|
||||
@@ -97,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():
|
||||
@@ -107,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'],
|
||||
@@ -136,37 +147,73 @@ 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()
|
||||
now=datetime.today().strftime('%Y-%m-%d')
|
||||
return render_template('bills.html', now=now, bill_data=bill_data, bill_types=bill_types )
|
||||
bill_freqs = get_bill_freqs()
|
||||
bill_ui = get_bill_ui()
|
||||
bill_growth_types = get_bill_growth_types()
|
||||
# take bill data, AND work out estimated future bills - process this into the bill_info array,
|
||||
bill_info=process_bill_data(bill_data, bill_types, bill_freqs, key_dates)
|
||||
# get an array of the total costs of bills each year - purely cosmetic (using bill_info)
|
||||
total=calc_future_totals(bill_info, bill_types)
|
||||
# update/re-get bill_data now that new estimated bills have been added
|
||||
bill_data = get_bill_data("order_by_bill_type_then_date")
|
||||
return render_template('bills.html', bill_data=bill_data, bill_types=bill_types, bill_freqs=bill_freqs, bill_ui=bill_ui, this_year=datetime.today().year, END_YEAR=END_YEAR, total=total, key_dates=key_dates, growth=bill_growth_types, cpi=finance_data['Inflation'] )
|
||||
|
||||
@app.route('/newbilltype', methods=['POST'])
|
||||
def InsertBillType():
|
||||
data = request.get_json()
|
||||
insert_bill_type( data['bill_type'] )
|
||||
insert_bill_type( data['bill_type'], data['freq'] )
|
||||
return "200"
|
||||
|
||||
@app.route('/updatebilltype', methods=['POST'])
|
||||
def UpdateBillType():
|
||||
data = request.get_json()
|
||||
update_bill_type( data['id'], data['bill_type'] )
|
||||
update_bill_type( data['id'], data['bill_type'], data['freq'] )
|
||||
return "200"
|
||||
|
||||
@app.route('/newbill', methods=['POST'])
|
||||
def InsertBill():
|
||||
data = request.get_json()
|
||||
new_bill( data['name'], data['amount'], data['bill_date'] )
|
||||
# last param is estimated - e.g. anything via GUI is not an estimate, but is a real bill
|
||||
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'])
|
||||
@@ -181,6 +228,55 @@ def DeleteBill():
|
||||
delete_bill( data['id'] )
|
||||
return "200"
|
||||
|
||||
@app.route('/usegrowth', methods=['POST'])
|
||||
def UseGrowth():
|
||||
data = request.get_json()
|
||||
use_growth( data['bill_type'], data['which_growth'] )
|
||||
return "200"
|
||||
|
||||
@app.route('/saveui', methods=['POST'])
|
||||
def SaveUI():
|
||||
data = request.get_json()
|
||||
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__':
|
||||
app.run(debug=True)
|
||||
|
||||
@@ -8,4 +8,4 @@ pysqlite3
|
||||
Werkzeug
|
||||
flask-compress
|
||||
gunicorn
|
||||
|
||||
requests
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -18,73 +18,264 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="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="col-6">
|
||||
<div class="row align-items-center">
|
||||
<button id="new-bill-data-button" class="px-0 offset-6 col-2 btn btn-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 el in bill_types %}
|
||||
<option value={{el.id}}>{{el.name}}</option>
|
||||
{# 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 %}
|
||||
<option value={{bf.id}}>{{bf.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>
|
||||
<button id="save-bill" class="new-bill-data-class px-0 col-1 btn btn-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 d-none" onClick="CancelNewBill()" ><span class="bi bi-trash3"> Cancel</span> </button>
|
||||
<button id="save-bill-type" class="new-bill-type-class px-0 col-1 btn btn-success bg-success-subtle text-success d-none" onClick="NewBillType()"><span class="bi bi-floppy"></span> Save</button>
|
||||
<button id="canc-bill-type" class="new-bill-type-class px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelNewBillType()"><span class="bi bi-x"> Cancel</span></button>
|
||||
<button id="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">Name</ > </div>
|
||||
<div class="px-0 col-2"> <label class="form-control text-center border-0">Date</ > </div>
|
||||
<div class="px-0 col-2"> <label class="form-control text-center border-0">Amount</ > </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 el in bill_data %}
|
||||
{% for bt in bill_types %}
|
||||
<div class="row">
|
||||
<div class="px-0 col-2"> <input type="text" class="form-control text-center bg-white" id="bill-data-type-{{el.id}}" value="{{ el.name }}" disabled> </div>
|
||||
<div class="px-0 col-2"> <input type="date" class="form-control text-center bg-white" id="bill-data-date-{{el.id}}" value="{{ el.bill_date }}" disabled> </div>
|
||||
<div class="px-0 col-2"> <input type="number" class="form-control text-center bg-white" id="bill-data-amount-{{el.id}}" value="{{ el.amount }}" disabled> </div>
|
||||
<button id="bill-data-chg-{{el.id}}" class="px-0 col-1 btn btn-success" onClick="StartUpdateBill( {{el.id}} )">Change</button>
|
||||
<button id="bill-data-del-{{el.id}}" class="px-0 col-1 btn btn-danger" onClick="DeleteBill( {{el.id }} )"><span class="bi bi-trash3"> Delete
|
||||
<button id="bill-data-save-{{el.id}}" class="px-0 col-1 btn btn-success d-none" onClick="UpdateBill( {{el.id}} )">Save</button>
|
||||
<button id="bill-data-canc-{{el.id}}" class="px-0 col-1 btn btn-danger d-none"
|
||||
onClick="CancelUpdateBill({{el.id}}, '{{el.name}}', '{{el.bill_date}}', '{{el.amount}}')"> <span class="bi bi-trash3"> Cancel</button>
|
||||
</button>
|
||||
<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-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">
|
||||
<div class="btn-group w-100" role="group">
|
||||
<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>
|
||||
{% 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="col-6">
|
||||
<div class="row align-items-center">
|
||||
<button id="new-bill-type-button" class="px-0 offset-2 col-2 btn btn-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>
|
||||
<button id="save-bill-type" class="new-bill-type-class px-0 col-1 btn btn-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 d-none" onClick="CancelNewBillType()"><span class="bi bi-trash3"> Cancel</span></button>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="row">
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0">Name</ ></div>
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0">Frequency</ ></div>
|
||||
<div class="px-0 col-2"><label class="form-control text-center border-0">Annual Growth Est</ ></div>
|
||||
<div class="col-2 form-control-inline d-none new-bill-data-class">Bill Type</div>
|
||||
<div id="new-bill-data-date-label" class="col-4 form-control-inline d-none new-bill-data-class">Date</div>
|
||||
<div id="new-bill-data-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>
|
||||
{% for el in bill_types %}
|
||||
<div class="row">
|
||||
<div class="px-0 col-2"><input type="text" class="bill-type-name form-control text-center bg-white" id="bill-type-name-{{el.id}}" value="{{ el.name }}" disabled> </div>
|
||||
<!-- bind Enter to save this bill-type -->
|
||||
<script>$("#bill-type-name-{{el.id}}").keyup(function(event){ if(event.which == 13){ $('#bill-type-save-{{el.id}}').click(); } event.preventDefault(); });</script>
|
||||
<div class="px-0 col-2"><input type="text" class="bill-type-{{el.id}} form-control text-center bg-white" id="bill-type-freq-{{el.id}}" value="not yet" disabled> </div>
|
||||
<div class="px-0 col-2"><input type="text" class="bill-type-{{el.id}} form-control text-center bg-white" id="bill-type-grow-{{el.id}}" value="not yet" disabled> </div>
|
||||
<button id="bill-type-chg-{{el.id}}" class="px-0 col-1 btn btn-success" onClick="StartUpdateBillType( {{el.id}} )">Change</button>
|
||||
<button id="bill-type-del-{{el.id}}" class="px-0 col-1 btn btn-danger" onClick="DelBillType({{el.id}})"><span class="bi bi-trash3"> Delete</button>
|
||||
<button id="bill-type-save-{{el.id}}" class="px-0 col-1 btn btn-success d-none" onClick="UpdateBillType( {{el.id}} )">Save</button>
|
||||
<button id="bill-type-canc-{{el.id}}" class="px-0 col-1 btn btn-danger d-none" onClick="CancelUpdateBillType({{el.id}}, '{{el.name}}')"><span class="bi bi-trash3"> Cancel</button>
|
||||
<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-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>
|
||||
</div>
|
||||
|
||||
<!-- create tabbed view for each bill type -->
|
||||
<nav id="bills-nav" class="nav nav-tabs">
|
||||
{% for bt in bill_types %}
|
||||
<button class="nav-link" id="tab-but-{{bt.id}}" data-bs-toggle="tab" data-bs-target="#tab-{{bt.id}}" type="button" role="tab" aria-controls="tab1" aria-selected="true" onClick="SaveTab('{{bt.id}}')">{{bt.name}}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="col-2 form-check form-switch form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" value="" id="showEstimated" onChange="ToggleEstimated()">
|
||||
<label class="form-check-label" for="flexCheckDefault">Show Estimates</label>
|
||||
</div>
|
||||
{% for bt in bill_types %}
|
||||
{% if loop.first %}
|
||||
<div id="tab-{{bt.id}}" class="tab-pane active">
|
||||
{% else %}
|
||||
<div id="tab-{{bt.id}}" class="tab-pane">
|
||||
{% endif %}
|
||||
{% for bd in bill_data %}
|
||||
{% if loop.first %}
|
||||
<div class="row pt-2">
|
||||
<div class="p-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></div>
|
||||
<div class="p-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Date</ ></div>
|
||||
<div class="p-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Amount</ ></div>
|
||||
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
|
||||
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"></ ></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if bd.bill_type == bt.id %}
|
||||
{% if bd.estimated == 1 %}
|
||||
<div class="row est d-none fst-italic">
|
||||
{% set classes="fst-italic form-control text-center" %}
|
||||
{% else %}
|
||||
<div class="row">
|
||||
{% set classes="form-control text-center" %}
|
||||
{% endif %}
|
||||
<div class="px-0 col-2"> <input type="text" class="{{classes}}" id="bill-data-type-{{bd.id}}" value="{{ bd.name }}" disabled> </div>
|
||||
{% if bd.bill_date == 'future' %}
|
||||
<div class="px-0 col-2"> <input type="text" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
|
||||
{% else %}
|
||||
<div class="px-0 col-2"> <input type="date" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
|
||||
<script>
|
||||
if( typeof future_id !== 'undefined' && future_id>0) {
|
||||
first_col_id={{bd.id}}
|
||||
future_id=0
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
<div class="px-0 col"> <input type="number" class="{{classes}}" id="bill-data-amount-{{bd.id}}" value="{{ bd.amount }}" disabled> </div>
|
||||
{% if bd.estimated == 0 %}
|
||||
<button id="bill-data-chg-{{bd.id}}" class="px-0 col 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 %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function ToggleEstimated()
|
||||
{
|
||||
if( $("#showEstimated").is(":checked") )
|
||||
{
|
||||
val=1
|
||||
$('.est').removeClass('d-none')
|
||||
}
|
||||
else
|
||||
{
|
||||
val=0
|
||||
$('.est').addClass('d-none')
|
||||
}
|
||||
$.ajax( { type: 'POST', url: '/saveui', contentType: 'application/json', data: JSON.stringify( { 'show_estimated': val } ), success: function() { } } )
|
||||
}
|
||||
|
||||
function StartNewBillData()
|
||||
{
|
||||
$('.new-bill-data-class').removeClass('d-none')
|
||||
@@ -94,9 +285,28 @@
|
||||
|
||||
function NewBill()
|
||||
{
|
||||
$.ajax( { type: 'POST', url: '/newbill',
|
||||
contentType: 'application/json', data: JSON.stringify( { 'name': $('#new-bill-data-type').val(), 'amount': $('#new-bill-data-amount').val(), 'bill_date': $('#new-bill-data-date').val() } ),
|
||||
if( $('#new-bill-data-growth').hasClass('d-none') )
|
||||
{
|
||||
// if growth is hidden, then we have normal bill
|
||||
$.ajax( { type: 'POST', url: '/newbill',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify( {
|
||||
'bill_type': $('#new-bill-data-type').val(),
|
||||
'amount': $('#new-bill-data-amount').val(),
|
||||
'bill_date': $('#new-bill-data-date').val() } ),
|
||||
success: function() { window.location='bills' } } )
|
||||
}
|
||||
else
|
||||
{
|
||||
// if growth is visible, then we have future bill/growth & no date
|
||||
$.ajax( { type: 'POST', url: '/newbill',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify( {
|
||||
'bill_type': $('#new-bill-data-type').val(),
|
||||
'amount': $('#new-bill-data-amount').val(),
|
||||
'growth': $('#new-bill-data-growth').val() } ),
|
||||
success: function() { window.location='bills' } } )
|
||||
}
|
||||
}
|
||||
|
||||
function CancelNewBill()
|
||||
@@ -136,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' } } )
|
||||
@@ -145,7 +355,6 @@
|
||||
function CancelUpdateBill( id, bill_type, bill_date, amount )
|
||||
{
|
||||
// fix-up type, date and amount fields
|
||||
$('#bill-data-type-'+id).addClass('bg-white')
|
||||
$('#bill-data-date-'+id).prop('disabled', true )
|
||||
$('#bill-data-amount-'+id).prop('disabled', true )
|
||||
// alter change/delete buttons to be save/cancel
|
||||
@@ -177,12 +386,14 @@
|
||||
$('.new-bill-type-class').addClass('d-none')
|
||||
$('#new-bill-type-button').removeClass('d-none')
|
||||
$('#new-bill-type-name').val('')
|
||||
// reset select to first option
|
||||
$('#new-bill-type-freq').val( $('#new-bill-type-freq option:first').attr('value') )
|
||||
}
|
||||
|
||||
function NewBillType()
|
||||
{
|
||||
$.ajax( { type: 'POST', url: '/newbilltype',
|
||||
contentType: 'application/json', data: JSON.stringify( { 'bill_type': $('#new-bill-type-name').val() } ),
|
||||
contentType: 'application/json', data: JSON.stringify( { 'bill_type': $('#new-bill-type-name').val(), 'freq': $('#new-bill-type-freq').val() } ),
|
||||
success: function() { window.location='bills' } } )
|
||||
}
|
||||
|
||||
@@ -190,11 +401,11 @@
|
||||
{
|
||||
val=$('#bill-type-name-'+id).val()
|
||||
|
||||
// "disable" the freq & growth
|
||||
$('.bill-type-'+id).addClass('bg-light text-secondary').removeClass('bg-white')
|
||||
// "enable" fields for edits
|
||||
$('.bill-type-'+id).prop('disabled', false)
|
||||
|
||||
// "enable" name for edits
|
||||
$('#bill-type-name-'+id).prop('disabled', false).focus()
|
||||
// put focus into name field
|
||||
$('#bill-type-name-'+id).focus()
|
||||
|
||||
// move cursor to the end after 'focus()' above
|
||||
$('#bill-type-name-'+id).val('').val( val )
|
||||
@@ -209,16 +420,14 @@
|
||||
function UpdateBillType(id)
|
||||
{
|
||||
$.ajax( { type: 'POST', url: '/updatebilltype',
|
||||
contentType: 'application/json', data: JSON.stringify( { 'id': id, 'bill_type': $('#bill-type-name-'+id).val() } ),
|
||||
contentType: 'application/json', data: JSON.stringify( { 'id': id, 'bill_type': $('#bill-type-name-'+id).val(), 'freq': $('#bill-type-freq-'+id).val() } ),
|
||||
success: function() { window.location='bills' } } )
|
||||
}
|
||||
|
||||
function CancelUpdateBillType(id,orig_name)
|
||||
{
|
||||
// "re-enable" the freq & growth
|
||||
$('.bill-type-'+id).removeClass('bg-light text-secondary').addClass('bg-white')
|
||||
// "disable" name for edits
|
||||
$('#bill-type-name-'+id).prop('disabled', true)
|
||||
$('.bill-type-'+id).prop('disabled', true)
|
||||
// alter change/delete buttons to be save/cancel
|
||||
$('#bill-type-chg-'+id).removeClass('d-none')
|
||||
$('#bill-type-del-'+id).removeClass('d-none')
|
||||
@@ -234,6 +443,20 @@
|
||||
data: JSON.stringify( { 'id': id } ), success: function() { window.location='bills' } } )
|
||||
}
|
||||
|
||||
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_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() { } } )
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// if amount has enter key in it then save, but dont do this for other fields in new bill
|
||||
$("#new-bill-data-amount").keyup(function(event){ if(event.which == 13){ $("#save-bill").click(); } event.preventDefault(); });
|
||||
@@ -241,8 +464,62 @@
|
||||
$("#new-bill-type-name").keyup(function(event){ if(event.which == 13){ $("#save-bill-type").click(); } event.preventDefault(); });
|
||||
|
||||
// note we also dynamically bound each bill-type-name to save on Enter when we create them in a loop
|
||||
|
||||
// force something to be active
|
||||
$('#bills-nav .nav-link').first().addClass('active');
|
||||
{% if bill_ui %}
|
||||
// if we have data on it - go back to last tab
|
||||
$('#tab-but-{{bill_ui.last_tab}}').tab('show');
|
||||
{% if bill_ui.show_estimated %}
|
||||
$('#showEstimated').click()
|
||||
{% endif %}
|
||||
{% 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>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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.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>
|
||||
@@ -11,14 +13,38 @@
|
||||
<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>
|
||||
<script src="https://code.highcharts.com/themes/adaptive.js"></script>
|
||||
<style>
|
||||
.col-form-label { width:140px; }
|
||||
html { font-size: 80%; }
|
||||
</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 %}
|
||||
@@ -26,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 %}
|
||||
@@ -39,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>
|
||||
@@ -51,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] %}
|
||||
@@ -62,11 +88,11 @@
|
||||
class="col-form-label me-2 text-end float-end {{extra}}">{{el.label}}
|
||||
</label>
|
||||
{% if el.display== "readonly" %}
|
||||
{% set bg="bg-light" %}
|
||||
{% set bg="bg-body-tertiary" %}
|
||||
{% 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}}>
|
||||
@@ -77,86 +103,127 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<h5 align="center" class="mt-4">Fortnighthly Savings data:
|
||||
{% if COMP %}
|
||||
{# get comparison date so we can use it below in loop to know when to print it out #}
|
||||
{% set comp_yr=COMP['date'][:4] %}
|
||||
{% set comp_mon=COMP['date'][5:7] %}
|
||||
{% set comp_day=COMP['date'][8:10 ] %}
|
||||
{% set comp_done=namespace( val=0 ) %}
|
||||
{% else %}
|
||||
{# we dont need to do a comparison, so consider it done before we begin #}
|
||||
{% set comp_done=namespace( val=1 ) %}
|
||||
{% endif %}
|
||||
</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center" style="background:lemonchiffon">2025</div>
|
||||
|
||||
{# inside started if below, we add blank lines to the start of the year so the dates line up #}
|
||||
{% for _ in range( 0, padding ) %}
|
||||
<br>
|
||||
{% endfor %}
|
||||
|
||||
{% for date, dollars in savings %}
|
||||
{% set yr=date[:4] %}
|
||||
{% set mon=date[5:7] %}
|
||||
{% set day=date[8:10 ] %}
|
||||
|
||||
{% if yr|int > first_yr|int and mon == '01' and day|int <= 14 %}
|
||||
</div><div class="col-auto">
|
||||
<div class="pt-1 pb-1 mb-0 alert text-center" style="background:lemonchiffon">{{yr}}</div>
|
||||
{% endif %}
|
||||
<font color="black">
|
||||
{{ date }}:
|
||||
{{ '$%0.2f' % dollars|float }}<br>
|
||||
</font>
|
||||
{% if comp_done.val == 0 and yr == comp_yr and mon == comp_mon and day|int >= comp_day|int %}
|
||||
<font color="blue">
|
||||
{{ COMP['date'] }}:
|
||||
{{ '$%0.2f' % COMP['amount']|float }}<br>
|
||||
</font>
|
||||
{% set comp_done.val=1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if depletion_date %}
|
||||
<div class="alert alert-danger">Run out of $'s:<br>{{depletion_date}}</div>
|
||||
<!-- create tabbed view for each bill type -->
|
||||
<nav id="ft-nav" class="nav nav-tabs pt-3">
|
||||
<button class="nav-link" id="tab-but-findata" data-bs-toggle="tab" data-bs-target="#tab-findata" type="button" role="tab" aria-controls="tab1" aria-selected="true" >Fortnighthly Savings data</button>
|
||||
<button class="nav-link" id="tab-but-graph" data-bs-toggle="tab" data-bs-target="#tab-graph" type="button" role="tab" aria-controls="tab2" aria-selected="true" >Graph of savings over time</button>
|
||||
{% if COMP %}
|
||||
<button class="nav-link" id="tab-but-compgraph" data-bs-toggle="tab" data-bs-target="#tab-compgraph" type="button" role="tab" aria-controls="tab3" aria-selected="true" >Comparison graph</button>
|
||||
{% else %}
|
||||
<div class="alert alert-success">Super kicks in!!!</div>
|
||||
<button class="nav-link" id="tab-but-compgraph" data-bs-toggle="tab" data-bs-target="#tab-compgraph" type="button" role="tab" aria-controls="tab3" aria-selected="true" disabled>Comparison graph</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="alert alert-warning">
|
||||
<h6 class="alert-heading">SUMMARY/BUDGET</h6>
|
||||
{% for label, value in BUDGET %}
|
||||
<div>
|
||||
{{label}} {{value}}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="tab-findata" class="tab-pane">
|
||||
<h5 align="center" class="mt-4">Fortnighthly Savings data:
|
||||
{% if COMP %}
|
||||
{# get comparison date so we can use it below in loop to know when to print it out #}
|
||||
{% set comp_yr=COMP['date'][:4] %}
|
||||
{% set comp_mon=COMP['date'][5:7] %}
|
||||
{% set comp_day=COMP['date'][8:10 ] %}
|
||||
{% set comp_done=namespace( val=0 ) %}
|
||||
{% else %}
|
||||
{# we dont need to do a comparison, so consider it done before we begin #}
|
||||
{% set comp_done=namespace( val=1 ) %}
|
||||
{% endif %}
|
||||
</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">{{savings[0][0][:4]}}</div>
|
||||
|
||||
{# inside started if below, we add blank lines to the start of the year so the dates line up #}
|
||||
{% for _ in range( 0, padding ) %}
|
||||
<br>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="comp_col" class="col-auto">
|
||||
<div class="input-group">
|
||||
<button type="button" class="btn btn-primary me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save</button>
|
||||
<button type="submit" class="disabled btn btn-primary rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button>
|
||||
<select class="form-select border border-primary text-primary" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
|
||||
{% for el in finance['COMP_SETS'] %}
|
||||
<option value="{{el[0]}}">{{el[1]}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if COMP %}
|
||||
<div style="display:none" id="comp_alert" class="alert alert-info mt-2">Note: {{ '$%0.2f' % COMP['amount']|float }} is the final value of the compared to data (with a buffer of: {{ '$%0.2f' % COMP['buffer']|float }}). Hover over blue labels above to see what compared to values differed</div>
|
||||
|
||||
{% set car_done=namespace( val=0 ) %}
|
||||
|
||||
{% for date, dollars in savings %}
|
||||
{% set yr=date[:4] %}
|
||||
{% set mon=date[5:7] %}
|
||||
{% set day=date[8:10 ] %}
|
||||
|
||||
{% set car_yr=key_dates['D_hyundai_owned'][:4] %}
|
||||
{% set car_mon=key_dates['D_hyundai_owned'][5:7] %}
|
||||
{% set car_day=key_dates['D_hyundai_owned'][8:10 ] %}
|
||||
|
||||
{% if yr|int > first_yr|int and mon == '01' and day|int <= 14 %}
|
||||
</div><div class="col-auto">
|
||||
<div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">{{yr}}</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if date == key_dates['D_quit_date'] %}
|
||||
<font class="text-warning">
|
||||
<label data-bs-toggle="tooltip" title="D quits">
|
||||
{% elif (yr == car_yr and mon == car_mon and day >= car_day and car_done.val == 0) %}
|
||||
{%set car_done.val=1 %}
|
||||
<font class="text-warning">
|
||||
<label data-bs-toggle="tooltip" title="We own car">
|
||||
{% else %}
|
||||
<font class="text-secondary">
|
||||
<label>
|
||||
{% endif %}
|
||||
{{ date }}:
|
||||
{{ '$%0.2f' % dollars|float }}<br>
|
||||
</label>
|
||||
</font><br>
|
||||
{% if comp_done.val == 0 and yr == comp_yr and mon == comp_mon and day|int >= comp_day|int %}
|
||||
<font class="text-info">
|
||||
{{ COMP['date'] }}:
|
||||
{{ '$%0.2f' % COMP['amount']|float }}<br>
|
||||
</font>
|
||||
{% set comp_done.val=1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if depletion_date %}
|
||||
<div class="alert alert-danger">Run out of $'s:<br>{{depletion_date}}</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">Super kicks in!!!</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="alert alert-warning">
|
||||
<h6 class="alert-heading">SUMMARY/BUDGET</h6>
|
||||
{% for label, value in BUDGET %}
|
||||
<div>
|
||||
{{label}} {{value}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="comp_col" class="col-auto">
|
||||
<div class="input-group">
|
||||
<button type="button" class="btn btn-info me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save Comparison set</button>
|
||||
<button type="submit" class="disabled btn btn-info rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button>
|
||||
<select class="form-select border border-info text-info" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
|
||||
{% for el in finance['COMP_SETS'] %}
|
||||
<option value="{{el[0]}}">{{el[1]}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if COMP %}
|
||||
<div style="display:none" id="comp_alert" class="alert alert-info mt-2">Note: {{ '$%0.2f' % COMP['amount']|float }} is the final value of the compared to data (with a buffer of: {{ '$%0.2f' % COMP['buffer']|float }}). Hover over blue labels above to see what compared to values differed</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row mt-4" 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']}} )
|
||||
@@ -165,17 +232,17 @@
|
||||
if( $("#Ioniq6_future option:selected"). text() == 'lease' )
|
||||
{
|
||||
// disable buyout
|
||||
$('#lbl-Car_buyout').addClass('bg-light text-secondary border-secondary')
|
||||
$('#Car_buyout').addClass('bg-light text-secondary border-secondary').attr('readonly', 'readonly' )
|
||||
$('#Car_buyout_date').addClass('bg-light text-secondary border-secondary').attr('readonly', 'readonly' )
|
||||
$('#lbl-Car_buyout').addClass('bg-body-tertiary border-secondary')
|
||||
$('#Car_buyout').addClass('bg-body-tertiary border-secondary').attr('readonly', 'readonly' )
|
||||
$('#Car_buyout_date').addClass('bg-body-tertiary border-secondary').attr('readonly', 'readonly' )
|
||||
}
|
||||
else
|
||||
{
|
||||
// disable lease
|
||||
$('#lbl-Car_loan').addClass('bg-light text-secondary')
|
||||
$('#Car_loan').addClass('bg-light text-secondary')
|
||||
$('#lbl-Car_balloon').addClass('bg-light text-secondary')
|
||||
$('#Car_balloon').addClass('bg-light text-secondary')
|
||||
$('#lbl-Car_loan').addClass('bg-body-tertiary')
|
||||
$('#Car_loan').addClass('bg-body-tertiary')
|
||||
$('#lbl-Car_balloon').addClass('bg-body-tertiary')
|
||||
$('#Car_balloon').addClass('bg-body-tertiary')
|
||||
}
|
||||
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll("[data-bs-toggle='tooltip']"))
|
||||
@@ -189,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 %}
|
||||
@@ -215,7 +283,7 @@
|
||||
plotBands.push({
|
||||
from: start,
|
||||
to: end,
|
||||
color: year % 2 === 0 ? 'white' : 'lemonchiffon' // Alternate colors
|
||||
color: year % 2 === 0 ? '#101010' : 'black' // Alternate colors
|
||||
});
|
||||
year = currentYear;
|
||||
start = new Date(`${year}-01-01`).getTime();
|
||||
@@ -226,38 +294,45 @@
|
||||
plotBands.push({
|
||||
from: start,
|
||||
to: end,
|
||||
color: year % 2 === 0 ? 'white' : 'lemonchiffon'
|
||||
color: year % 2 === 0 ? 'charcoal' : 'black'
|
||||
});
|
||||
|
||||
const annotations = [];
|
||||
// the al, x, offset are used to make the altenrate annotations be on slightly different vertical offsets (size is based on $'s)
|
||||
// al alternates every 2 annotations left / right (so 2 left, then 2 right), x is just used to also move the label more left/right to get the connecting line
|
||||
// offset is used to make the next annotation be on slightly different vertical offsets (size is based on $'s)
|
||||
// HACK: start at 13, later we adjust in steps of 50s allowing 4 steps, then we go back to top
|
||||
var offset=13
|
||||
{% if not COMP %}
|
||||
// Add annotations for changes greater than 5000
|
||||
{% for a in finance['annotations'] %}
|
||||
annotations.push({
|
||||
labels: [{
|
||||
point: {
|
||||
x: {{a['x']}},
|
||||
y: {{a['y']}},
|
||||
crop: true,
|
||||
xAxis: 0,
|
||||
yAxis: 0
|
||||
},
|
||||
x: -70,
|
||||
// Add annotations for changes greater than 5000
|
||||
{% for a in finance['annotations'] %}
|
||||
annotations.push({
|
||||
labels: [{
|
||||
point: {
|
||||
x: {{a['x']}},
|
||||
y: {{a['y']}},
|
||||
crop: true,
|
||||
xAxis: 0,
|
||||
yAxis: 0
|
||||
},
|
||||
x: -70,
|
||||
{% if '-$' in a['label'] %}
|
||||
y: offset,
|
||||
text: '{{a['label']}}'
|
||||
}], labelOptions: { allowOverlap: true }
|
||||
});
|
||||
{% else %}
|
||||
y: -20,
|
||||
{% endif %}
|
||||
text: '{{a['label']}}'
|
||||
}], labelOptions: { allowOverlap: true }
|
||||
});
|
||||
{% if a['y'] > 200000 %}
|
||||
offset = ({{loop.index}} * 50 % 200) +50
|
||||
{% endfor %}
|
||||
document.keep = annotations
|
||||
{% endif %}
|
||||
{% else %}
|
||||
offset = -100
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
document.keep = annotations
|
||||
|
||||
// Highcharts configuration
|
||||
Highcharts.chart('container', {
|
||||
Highcharts.chart('graph', {
|
||||
chart: { type: 'line' },
|
||||
colors: [ 'orange' ],
|
||||
title: { text: 'Savings Over Time' },
|
||||
xAxis: {
|
||||
type: 'datetime',
|
||||
@@ -265,21 +340,45 @@
|
||||
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
|
||||
},
|
||||
tooltip: {
|
||||
pointFormatter: function ()
|
||||
{
|
||||
if( this.series.symbol == 'circle' ) { s='\u25CF' } else { s='\u2B25' }
|
||||
return '<span style="color:' + this.point.color + '">' + s + '</span> <b>' + this.point.y + ':</b> ' + this.series.name + '<br>'
|
||||
}, shared:true
|
||||
},
|
||||
annotations: annotations, // Add annotations
|
||||
series: [ { name: "Savings", data: chartData, marker: { radius: 2 } } ]
|
||||
});
|
||||
|
||||
{% if COMP %}
|
||||
// Highcharts configuration
|
||||
Highcharts.chart('graph-comp', {
|
||||
chart: { type: 'line' },
|
||||
colors: [
|
||||
'orange', // Custom color 1
|
||||
'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">
|
||||
@@ -300,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( {
|
||||
@@ -316,4 +415,3 @@
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user