Compare commits
21 Commits
3b3c42caab
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 71606035fd | |||
| b8c1edc084 | |||
| f0fee15548 | |||
| 20239d72c8 | |||
| e3d2d8ea08 | |||
| 8446f59740 | |||
| 3a04b8321c | |||
| 037a9c2d87 | |||
| e7a3cb3d7d | |||
| 2ba2ece1d0 | |||
| 0a0a7b321b | |||
| 85a53c8c5f | |||
| 8e2f0ae340 | |||
| 1cf835b7e7 | |||
| e104dd8270 | |||
| a46b8f895a | |||
| 09de41e093 | |||
| 9c02bc94ff | |||
| 94f4108a3e | |||
| 8d2809fdd9 | |||
| 2d1dcfffd9 |
5
BUGS
5
BUGS
@@ -1,4 +1,9 @@
|
|||||||
|
* can put in dumb dates - DO SOME INPUT VALIDATION, *sigh*
|
||||||
|
- e.g. 0026-02-19 for a Gas bill
|
||||||
|
|
||||||
* kayo bills are wrong in between normal bills
|
* kayo bills are wrong in between normal bills
|
||||||
|
|
||||||
* added an electricity bill by accident for 2018, that kills lots :(
|
* added an electricity bill by accident for 2018, that kills lots :(
|
||||||
- something to do with missing year of data in quarterly bills - still an issue
|
- something to do with missing year of data in quarterly bills - still an issue
|
||||||
|
|
||||||
|
* if change a bill (like Amazon), then its future estimates don't update
|
||||||
|
|||||||
8
TODO
8
TODO
@@ -1,4 +1,6 @@
|
|||||||
bills:
|
bills:
|
||||||
|
bill for the year (when removed from LE, should be dynamic)
|
||||||
|
|
||||||
bills html, and growth types are poor code repitition / lame... could I do something more like:
|
bills html, and growth types are poor code repitition / lame... could I do something more like:
|
||||||
{% for gt in growth %}
|
{% for gt in growth %}
|
||||||
{% if gt.name == 'Min' %}
|
{% if gt.name == 'Min' %}
|
||||||
@@ -13,9 +15,3 @@ CALC:
|
|||||||
* still get double health insurance bills sometimes (just viewing a new date might trigger this??? or at least when I changed years)
|
* still get double health insurance bills sometimes (just viewing a new date might trigger this??? or at least when I changed years)
|
||||||
|
|
||||||
UI:
|
UI:
|
||||||
* add FIXED Annotations:
|
|
||||||
* historical annotations (at least M_quit_date, Trip_date, school_fees in 25)
|
|
||||||
* should try AI with how to distribute annotations better
|
|
||||||
* make bills tabs a vertical navbar instead of horizontal
|
|
||||||
|
|
||||||
* make FIRST_YEAR dynamic, and maybe just WARN if next pay is > FIRST_YEAR (let me sort if by hand - probably non-issue as unlikely to be working in late Dec 26)
|
|
||||||
|
|||||||
7
bills.py
7
bills.py
@@ -304,7 +304,7 @@ def get_growth_value( bt, bill_type ):
|
|||||||
finance_data = get_finance_data()
|
finance_data = get_finance_data()
|
||||||
return finance_data['Inflation']
|
return finance_data['Inflation']
|
||||||
else:
|
else:
|
||||||
match = re.match("flat-(\d+)", which )
|
match = re.match(r"flat-(\d+)", which )
|
||||||
if match:
|
if match:
|
||||||
return int(match.group(1))
|
return int(match.group(1))
|
||||||
else:
|
else:
|
||||||
@@ -643,8 +643,7 @@ def recalcFutureBills():
|
|||||||
for fb in future_car_bills:
|
for fb in future_car_bills:
|
||||||
amt=fb['amount']
|
amt=fb['amount']
|
||||||
bt=fb['bill_type']
|
bt=fb['bill_type']
|
||||||
# only can use simple growth as its a future bill
|
growth = get_growth_value( bill_types, bt )
|
||||||
growth=bill_types[bt]['ann_growth_simple']
|
|
||||||
# factor in growth for next bills
|
# factor in growth for next bills
|
||||||
for yr in range( int(car_yr), END_YEAR+1 ):
|
for yr in range( int(car_yr), END_YEAR+1 ):
|
||||||
new_date=f"{yr}-{car_mmdd}"
|
new_date=f"{yr}-{car_mmdd}"
|
||||||
@@ -661,7 +660,7 @@ def recalcFutureBills():
|
|||||||
# deal with future bills due to their starting dates being dynamic
|
# deal with future bills due to their starting dates being dynamic
|
||||||
amt=fb['amount']
|
amt=fb['amount']
|
||||||
bt=fb['bill_type']
|
bt=fb['bill_type']
|
||||||
growth=bill_types[bt]['ann_growth_simple']
|
growth = get_growth_value( bill_types, bt )
|
||||||
num_ann_bills= bf_id_num[bt_id_freq[bt]]
|
num_ann_bills= bf_id_num[bt_id_freq[bt]]
|
||||||
if num_ann_bills == 1:
|
if num_ann_bills == 1:
|
||||||
# factor in growth for next bill
|
# factor in growth for next bill
|
||||||
|
|||||||
71
calc.py
71
calc.py
@@ -1,9 +1,11 @@
|
|||||||
# calc.py
|
# calc.py
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from defines import END_YEAR
|
from defines import END_YEAR
|
||||||
|
from db import get_historical_data
|
||||||
|
|
||||||
# GLOBAL CONSTANTS
|
# GLOBAL CONSTANTS
|
||||||
LEASE = 0
|
LEASE = 0
|
||||||
|
ANNOT_LIMIT = 1000
|
||||||
|
|
||||||
# Dates that don't change
|
# Dates that don't change
|
||||||
first_pay_date = datetime(2026,1,8)
|
first_pay_date = datetime(2026,1,8)
|
||||||
@@ -20,7 +22,7 @@ def bill_amount_today(finance, day, bill_data, bt_id_name, total ):
|
|||||||
# there may be more than one bill on this day, keep add amount and keep going in loop
|
# there may be more than one bill on this day, keep add amount and keep going in loop
|
||||||
if b['bill_date'] == day_str:
|
if b['bill_date'] == day_str:
|
||||||
amt += b['amount']
|
amt += b['amount']
|
||||||
if b['amount'] > 1000:
|
if b['amount'] > ANNOT_LIMIT:
|
||||||
n=bt_id_name[ b['bill_type'] ]
|
n=bt_id_name[ b['bill_type'] ]
|
||||||
print( f"bill_amt_today {n} for {day_str} has amt={b['amount']}" )
|
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}" )
|
add_annotation(finance, day, total-b['amount'], -b['amount'], f"Pay {n}" )
|
||||||
@@ -31,9 +33,8 @@ def bill_amount_today(finance, day, bill_data, bt_id_name, total ):
|
|||||||
return amt
|
return amt
|
||||||
|
|
||||||
def add_annotation(finance, dt, total, delta, text):
|
def add_annotation(finance, dt, total, delta, text):
|
||||||
# dont add an annotation for small changes (jic)
|
|
||||||
tm = dt.timestamp() * 1000
|
tm = dt.timestamp() * 1000
|
||||||
if delta > 0:
|
if delta >= 0:
|
||||||
text += f": ${int(abs(delta))}"
|
text += f": ${int(abs(delta))}"
|
||||||
else:
|
else:
|
||||||
text += f": -${int(abs(delta))}"
|
text += f": -${int(abs(delta))}"
|
||||||
@@ -65,7 +66,7 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
|
|||||||
CBA_price = finance['CBA_price']
|
CBA_price = finance['CBA_price']
|
||||||
Ioniq6_future = finance['Ioniq6_future']
|
Ioniq6_future = finance['Ioniq6_future']
|
||||||
|
|
||||||
### COMPLEX tax implications with my leave I have not taken. It will be taxed in the year I 'quit' ###
|
### COMPLEX tax implications with my leave I have not taken. It will be taxed in the financial year I 'quit' ###
|
||||||
|
|
||||||
# leave in days, 10 business days to a fortnight,
|
# leave in days, 10 business days to a fortnight,
|
||||||
# paid before tax I earn $7830.42 / fortnight. Tax on that will be at 37% or $4933.16 after tax
|
# paid before tax I earn $7830.42 / fortnight. Tax on that will be at 37% or $4933.16 after tax
|
||||||
@@ -75,27 +76,30 @@ def calculate_savings_depletion(finance, bill_data, bill_type):
|
|||||||
# this is what I now earn before-tax (and I *THINK* vehicle allowance won't be paid X 12 weeks)
|
# this is what I now earn before-tax (and I *THINK* vehicle allowance won't be paid X 12 weeks)
|
||||||
pre_tax_D_earning = 8143.65
|
pre_tax_D_earning = 8143.65
|
||||||
|
|
||||||
# whenever I leave, I get 12 weeks (or 60 business days) + whatever leave they owe me
|
# whenever I quit, I get my leave paid out and I get 12 weeks (or 6 pays) -- notice
|
||||||
payout = ((60+D_leave_owed_in_days)/bus_days_in_fortnight) * pre_tax_D_earning
|
payout = ((D_leave_owed_in_days/bus_days_in_fortnight) + 6) * pre_tax_D_earning
|
||||||
|
|
||||||
# just use redundancy calc...
|
# just use redundancy calc...
|
||||||
payout = 83115.84
|
# payout = 83115.84
|
||||||
print( f"leave payout gross={payout}" )
|
print( f"leave payout gross={payout}" )
|
||||||
|
|
||||||
# However, if I quit in the next fin year - tax will be: $4,288 plus 30c for each $1 over $45,000
|
# However, if I quit in the next fin year - tax will be: $4,288 plus 30c for each $1 over $45,000
|
||||||
# (assuming the 7830.42 * ~90/bus_days_in_fortnight = ~ $64k - > 45k and < $135k bracket is 30%)
|
# (assuming the 7830.42 * ~90/bus_days_in_fortnight = ~ $64k - > 45k and < $135k bracket is 30% OR .37c for $135->$190k)
|
||||||
|
# - IF I am close to $190k+, just wait a month and quit in new financial year
|
||||||
# Given, I probably can't stop Deakin doing PAYG deductions, I won't get
|
# Given, I probably can't stop Deakin doing PAYG deductions, I won't get
|
||||||
# the tax back until the end of the financial year, so work out the
|
# the tax back until the end of the financial year, so work out the
|
||||||
# amount of tax I will get back info: tax_diff_D_leave
|
# amount of tax I will get back into: tax_diff_D_leave
|
||||||
tax_on_leave = (payout - 45000)*.37 + 4288
|
tax_on_leave = (payout - 45000)*.30 + 4288
|
||||||
D_leave_after_tax = payout - tax_on_leave
|
D_leave_after_tax = payout - tax_on_leave
|
||||||
|
|
||||||
# just use redunancy calc...
|
print(f"my calc would say D_leave_after_tax = {D_leave_after_tax}" )
|
||||||
D_leave_after_tax = 56518.77
|
|
||||||
|
# just use quick google/calc - it claims tax rules apply and are capped at 32% - its possible I get more like 75k then pay tax in July?
|
||||||
|
D_leave_after_tax = 59000
|
||||||
|
|
||||||
tax_diff_D_leave = payout - D_leave_after_tax
|
tax_diff_D_leave = payout - D_leave_after_tax
|
||||||
|
|
||||||
### FIXME: for now, assume no tax back after leave - think this may be needed if I quit anytime nowish until end of Jun
|
### FIXME: for now, assume no tax back after leave - see above comment
|
||||||
tax_diff_D_leave = 0
|
tax_diff_D_leave = 0
|
||||||
|
|
||||||
print( f"tax_diff_D_leave: {tax_diff_D_leave}")
|
print( f"tax_diff_D_leave: {tax_diff_D_leave}")
|
||||||
@@ -315,3 +319,44 @@ def calc_key_dates( finance ):
|
|||||||
|
|
||||||
return key_dates
|
return key_dates
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# go through finance_history table, find big items to make annoations from
|
||||||
|
################################################################################
|
||||||
|
def add_historical_annotations( finance ):
|
||||||
|
|
||||||
|
added={}
|
||||||
|
hist=get_historical_data()
|
||||||
|
|
||||||
|
day=hist[0]['M_payout_date'];
|
||||||
|
amt=hist[0]['M_payout'];
|
||||||
|
dt = datetime.strptime(day, "%Y-%m-%d")
|
||||||
|
|
||||||
|
# cover Mandys resignation
|
||||||
|
add_annotation(finance, dt, hist[0]['Savings']+amt, amt, "M Resigned" )
|
||||||
|
|
||||||
|
# cover O/S trip
|
||||||
|
for h in hist:
|
||||||
|
if 'Overseas_trip' in h and h['Overseas_trip']:
|
||||||
|
last_mpd=h['Overseas_trip_date']
|
||||||
|
last_mpd_savings=h['Savings']
|
||||||
|
snap_dt=datetime.strptime(h['snapshot_date'], "%Y-%m-%d")
|
||||||
|
# keep every savings value until we find the last one before the school fees are paid
|
||||||
|
if snap_dt < school_fees_date:
|
||||||
|
savings_b4_sfd = h['Savings']
|
||||||
|
|
||||||
|
dt = datetime.strptime(last_mpd, "%Y-%m-%d")
|
||||||
|
# HARDCODING this, based on post-analysis of CC charges (AND this dt is post most of the spend, so dont adjust)
|
||||||
|
amt = 33405.01
|
||||||
|
print( f"last_mpd_savings={last_mpd_savings}" )
|
||||||
|
add_annotation(finance, dt, last_mpd_savings, -amt, "O/S trip" )
|
||||||
|
print( f"should have added o/s trip (-{amt}) - lpd {last_mpd} == {dt}, savings = {last_mpd_savings}" )
|
||||||
|
|
||||||
|
# cover School Fees for 2025
|
||||||
|
amt = hist[0]['School_Fees']
|
||||||
|
|
||||||
|
print( f"savings_b4_sfd ={savings_b4_sfd}")
|
||||||
|
add_annotation(finance, school_fees_date, savings_b4_sfd-amt, -amt, "Pay school fees" )
|
||||||
|
|
||||||
|
# go through bills from oldest historical date until 'today' and add any > AMT_LIMIT
|
||||||
|
|
||||||
|
return
|
||||||
|
|||||||
2
crontab
2
crontab
@@ -1,2 +1,2 @@
|
|||||||
# run once every 5 days or so
|
# run once every 5 days or so
|
||||||
0 23 2-27/5 * * finplan /code/snapshot.sh
|
0 23 2-27/5 * * finplan cd /tmp && ENV="production" /code/snapshot.sh
|
||||||
|
|||||||
4
db.py
4
db.py
@@ -8,6 +8,8 @@ def connect_db(as_object):
|
|||||||
conn = sqlite3.connect('/data/finance.db')
|
conn = sqlite3.connect('/data/finance.db')
|
||||||
else:
|
else:
|
||||||
conn = sqlite3.connect('./finance.db')
|
conn = sqlite3.connect('./finance.db')
|
||||||
|
# allow deleting cset to clear our compare_to properly in finance table
|
||||||
|
conn.execute("PRAGMA foreign_keys = ON;")
|
||||||
if as_object:
|
if as_object:
|
||||||
conn.row_factory = sqlite3.Row # This allows us to access columns by name
|
conn.row_factory = sqlite3.Row # This allows us to access columns by name
|
||||||
return conn
|
return conn
|
||||||
@@ -190,7 +192,7 @@ def get_historical_data():
|
|||||||
def get_budget_data(finance_data):
|
def get_budget_data(finance_data):
|
||||||
# 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
|
# 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
|
# 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 = 25357.07
|
bills = 25321.03
|
||||||
BUDGET=[]
|
BUDGET=[]
|
||||||
BUDGET.append( ('Bills', f"${bills:,.2f}") )
|
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}") )
|
BUDGET.append( ('Buffer', f"${finance_data['CBA']*finance_data['CBA_price']+finance_data['TLS']*finance_data['TLS_price']:,.2f}") )
|
||||||
|
|||||||
27
main.py
27
main.py
@@ -1,6 +1,6 @@
|
|||||||
# main.py
|
# main.py
|
||||||
from flask import Flask, render_template, request, redirect, url_for, Response, jsonify
|
from flask import Flask, render_template, request, redirect, url_for, Response, jsonify
|
||||||
from calc import calculate_savings_depletion, calc_key_dates
|
from calc import calculate_savings_depletion, calc_key_dates, add_historical_annotations
|
||||||
from db import init_db, get_historical_data, get_finance_data, update_finance, get_budget_data
|
from db import init_db, get_historical_data, 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 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_freqs, get_bill_growth_types
|
||||||
@@ -32,13 +32,19 @@ def index():
|
|||||||
bill_data = get_bill_data("order_by_date_only")
|
bill_data = get_bill_data("order_by_date_only")
|
||||||
bill_types = get_bill_types()
|
bill_types = get_bill_types()
|
||||||
depletion_date, savings_per_fortnight, final_savings = calculate_savings_depletion(finance_data, bill_data, bill_types)
|
depletion_date, savings_per_fortnight, final_savings = calculate_savings_depletion(finance_data, bill_data, bill_types)
|
||||||
|
add_historical_annotations( finance_data )
|
||||||
|
# now sort this by X, then Y, to get the historical ones in the right order
|
||||||
|
finance_data['annotations'].sort(key=lambda item: (item['x'], item['y']))
|
||||||
BUDGET=get_budget_data(finance_data)
|
BUDGET=get_budget_data(finance_data)
|
||||||
|
|
||||||
if depletion_date:
|
if depletion_date:
|
||||||
depletion_date=depletion_date.date(); # just show date
|
depletion_date=depletion_date.date(); # just show date
|
||||||
|
|
||||||
# if we are comparing...(compare_to will be 0 / None to start with, and then COMP will be None
|
# if we are comparing...(compare_to will be 0 / None to start with, and then COMP will be None
|
||||||
COMP=get_comp_set_data(finance_data['compare_to'])
|
if finance_data['compare_to']:
|
||||||
|
COMP=get_comp_set_data(finance_data['compare_to'])
|
||||||
|
else:
|
||||||
|
COMP={}
|
||||||
|
|
||||||
DISP=[]
|
DISP=[]
|
||||||
# Row 1
|
# Row 1
|
||||||
@@ -47,7 +53,7 @@ def index():
|
|||||||
r.append( FP_VAR( 'Savings', 'Savings' ) )
|
r.append( FP_VAR( 'Savings', 'Savings' ) )
|
||||||
r.append( FP_VAR( 'Car Loan via Pay', 'Car_loan_via_pay', 'readonly' ) )
|
r.append( FP_VAR( 'Car Loan via Pay', 'Car_loan_via_pay', 'readonly' ) )
|
||||||
r.append( FP_VAR( 'Living Expenses', 'Living_Expenses' ) )
|
r.append( FP_VAR( 'Living Expenses', 'Living_Expenses' ) )
|
||||||
r.append( FP_VAR( 'Overseas Trip', 'Overseas_trip', 'date', 'col-auto', 'Overseas_trip_date' ) )
|
r.append( FP_VAR( 'Overseas Trip', 'Overseas_trip', 'date', 'col', 'Overseas_trip_date' ) )
|
||||||
DISP.append(r)
|
DISP.append(r)
|
||||||
|
|
||||||
# Row 2
|
# Row 2
|
||||||
@@ -56,7 +62,7 @@ def index():
|
|||||||
r.append( FP_VAR( 'Interest Rate', 'Interest_Rate' ) )
|
r.append( FP_VAR( 'Interest Rate', 'Interest_Rate' ) )
|
||||||
r.append( FP_VAR( 'Car Loan (monthly)', 'Car_loan', 'readonly' ) )
|
r.append( FP_VAR( 'Car Loan (monthly)', 'Car_loan', 'readonly' ) )
|
||||||
r.append( FP_VAR( 'Inflation', 'Inflation' ) )
|
r.append( FP_VAR( 'Inflation', 'Inflation' ) )
|
||||||
r.append( FP_VAR( 'Reno Costs', 'Mark_reno', 'date', 'col-auto', 'Mark_reno_date' ) )
|
r.append( FP_VAR( 'Reno Costs', 'Mark_reno', 'date', 'col', 'Mark_reno_date' ) )
|
||||||
DISP.append(r)
|
DISP.append(r)
|
||||||
|
|
||||||
# Row 2
|
# Row 2
|
||||||
@@ -69,7 +75,7 @@ def index():
|
|||||||
ss_opt.append( { 'val': el, 'label': f'{el} years' } )
|
ss_opt.append( { 'val': el, 'label': f'{el} years' } )
|
||||||
|
|
||||||
r.append( FP_VAR( 'Sell Shares for:', 'Sell_shares', 'select', 'col-auto', '', ss_opt ) )
|
r.append( FP_VAR( 'Sell Shares for:', 'Sell_shares', 'select', 'col-auto', '', ss_opt ) )
|
||||||
r.append( FP_VAR( 'Car Buyout', 'Car_buyout', 'date', 'col-auto', 'Car_buyout_date' ) )
|
r.append( FP_VAR( 'Car Buyout', 'Car_buyout', 'date', 'col', 'Car_buyout_date' ) )
|
||||||
DISP.append(r)
|
DISP.append(r)
|
||||||
|
|
||||||
# Row 3
|
# Row 3
|
||||||
@@ -111,14 +117,18 @@ def index():
|
|||||||
|
|
||||||
@app.route('/save', methods=['POST'])
|
@app.route('/save', methods=['POST'])
|
||||||
def save():
|
def save():
|
||||||
insert_cset( request.get_json() )
|
cset_id=insert_cset( request.get_json() )
|
||||||
return "200"
|
name = request.get_json()['vars']['name']
|
||||||
|
return jsonify( cset_id=cset_id, name=name )
|
||||||
|
|
||||||
@app.route('/update', methods=['POST'])
|
@app.route('/update', methods=['POST'])
|
||||||
def update():
|
def update():
|
||||||
|
|
||||||
old_finance_data = get_finance_data()
|
old_finance_data = get_finance_data()
|
||||||
|
|
||||||
|
raw_compare_to = request.form.get('compare_to')
|
||||||
|
compare_to_value = None if raw_compare_to == "0" else int(raw_compare_to)
|
||||||
|
|
||||||
finance_data = (
|
finance_data = (
|
||||||
request.form['D_Salary'],
|
request.form['D_Salary'],
|
||||||
request.form['D_Num_fortnights_pay'],
|
request.form['D_Num_fortnights_pay'],
|
||||||
@@ -144,9 +154,10 @@ def update():
|
|||||||
request.form['Mark_reno_date'],
|
request.form['Mark_reno_date'],
|
||||||
request.form['Car_buyout_date'],
|
request.form['Car_buyout_date'],
|
||||||
request.form['Sell_shares'],
|
request.form['Sell_shares'],
|
||||||
request.form['compare_to'],
|
compare_to_value,
|
||||||
request.form['Ioniq6_future']
|
request.form['Ioniq6_future']
|
||||||
)
|
)
|
||||||
|
|
||||||
update_finance(finance_data)
|
update_finance(finance_data)
|
||||||
new_finance_data = get_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
|
# changed Ioniq6_future, Car_buyout_date or D_Num_fortnights_pay, so lets force recalc key_dates, and therefore estimated bills
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
DB_FILE="finance.db"
|
if [ "$ENV" == "production" ]; then
|
||||||
|
DB_FILE="/data/finance.db"
|
||||||
|
else
|
||||||
|
DB_FILE="./finance.db"
|
||||||
|
fi
|
||||||
HISTORY_TABLE="finance_history"
|
HISTORY_TABLE="finance_history"
|
||||||
# Current date in the format you've been using
|
# Current date in the format you've been using
|
||||||
DATE_STR=$(date +%Y-%m-%d)
|
DATE_STR=$(date +%Y-%m-%d)
|
||||||
|
|||||||
11
static/favicon.svg
Normal file
11
static/favicon.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#1B2E3C"/>
|
||||||
|
|
||||||
|
<rect x="9" y="17" width="4" height="7" rx="1" fill="white"/>
|
||||||
|
<rect x="15" y="14" width="4" height="10" rx="1" fill="white"/>
|
||||||
|
<rect x="21" y="10" width="4" height="14" rx="1" fill="white"/>
|
||||||
|
|
||||||
|
<path d="M8 14L14 9L18 11L25 4" stroke="lightblue" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M21 4H25V8" stroke="lightblue" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 585 B |
@@ -14,7 +14,8 @@
|
|||||||
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
|
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
|
||||||
<style>
|
<style>
|
||||||
.col-form-label { width:140px; }
|
.col-form-label { width:140px; }
|
||||||
html { font-size: 80%; }
|
html { font-size: 75% !important; }
|
||||||
|
@media (max-width: 2000px) { html { font-size: 70% !important; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -193,15 +194,17 @@
|
|||||||
<span class="bi bi-x"> Cancel</span> </button>
|
<span class="bi bi-x"> Cancel</span> </button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
<!-- create tabbed view for each bill type -->
|
<!-- create tabbed view for each bill type -->
|
||||||
<nav id="bills-nav" class="nav nav-tabs">
|
<nav id="bills-nav" class="nav flex-column nav-pills me-3">
|
||||||
{% for bt in bill_types %}
|
{% 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>
|
<button class="nav-link w-100 text-end col px-1 py-2" 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 %}
|
{% endfor %}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="col-2 form-check form-switch form-check-inline">
|
<div class="col form-check form-switch form-check-inline">
|
||||||
<input class="form-check-input" type="checkbox" value="" id="showEstimated" onChange="ToggleEstimated()">
|
<input class="form-check-input" type="checkbox" value="" id="showEstimated" onChange="ToggleEstimated()">
|
||||||
<label class="form-check-label" for="flexCheckDefault">Show Estimates</label>
|
<label class="form-check-label" for="flexCheckDefault">Show Estimates</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,8 +217,7 @@
|
|||||||
{% for bd in bill_data %}
|
{% for bd in bill_data %}
|
||||||
{% if loop.first %}
|
{% if loop.first %}
|
||||||
<div class="row pt-2">
|
<div class="row pt-2">
|
||||||
<div class="p-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></div>
|
<div class="p-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Date</ ></div>
|
||||||
<div class="p-0 col-2"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Date</ ></div>
|
|
||||||
<div class="p-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Amount</ ></div>
|
<div class="p-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Amount</ ></div>
|
||||||
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
|
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
|
||||||
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"></ ></div>
|
<div class="px-0 col"><label class="form-control text-center border-0 fw-bold bg-body-tertiary rounded-0 h-100"></ ></div>
|
||||||
@@ -229,11 +231,11 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
{% set classes="form-control text-center" %}
|
{% set classes="form-control text-center" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="px-0 col-2"> <input type="text" class="{{classes}}" id="bill-data-type-{{bd.id}}" value="{{ bd.name }}" disabled> </div>
|
<input type="hidden" id="bill-data-type-{{bd.id}}" value="{{ bd.name }}"> </input>
|
||||||
{% if bd.bill_date == 'future' %}
|
{% 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>
|
<div class="px-0 col"> <input type="text" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="px-0 col-2"> <input type="date" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
|
<div class="px-0 col"> <input type="date" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
|
||||||
<script>
|
<script>
|
||||||
if( typeof future_id !== 'undefined' && future_id>0) {
|
if( typeof future_id !== 'undefined' && future_id>0) {
|
||||||
first_col_id={{bd.id}}
|
first_col_id={{bd.id}}
|
||||||
@@ -259,6 +261,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function ToggleEstimated()
|
function ToggleEstimated()
|
||||||
@@ -453,7 +456,7 @@
|
|||||||
function SaveTab( last_tab )
|
function SaveTab( last_tab )
|
||||||
{
|
{
|
||||||
// set the drop-down for new bill to be this tab now...
|
// 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-", "") )
|
$("#new-bill-data-type").val( $('.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() { } } )
|
$.ajax( { type: 'POST', url: '/saveui', contentType: 'application/json', data: JSON.stringify( { 'last_tab': last_tab } ), success: function() { } } )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
|
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
|
||||||
<style>
|
<style>
|
||||||
.col-form-label { width:140px; }
|
.col-form-label { width:140px; }
|
||||||
html { font-size: 80%; }
|
html { font-size: 75% !important; }
|
||||||
|
@media (max-width: 2000px) { html { font-size: 70% !important; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -5,18 +5,20 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||||
|
|
||||||
<title>Finance Form</title>
|
<title>Finance Form</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<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.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/highcharts.js"></script>
|
||||||
|
<script src="https://code.highcharts.com/modules/mouse-wheel-zoom.js"></script>
|
||||||
<script src="https://code.highcharts.com/modules/annotations.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/modules/accessibility.js"></script>
|
||||||
<script src="https://code.highcharts.com/themes/adaptive.js"></script>
|
<script src="https://code.highcharts.com/themes/adaptive.js"></script>
|
||||||
<style>
|
<style>
|
||||||
.col-form-label { width:140px; }
|
.col-form-label { width:140px; }
|
||||||
html { font-size: 75% !important; }
|
html { font-size: 75% !important; }
|
||||||
|
@media (max-width: 2000px) { html { font-size: 69% !important; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -33,7 +35,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Savings (<a href='https://online.macquarie.com.au/personal/#/login'>Macquarie</a>
|
<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.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>
|
+<a href='https://ib.nab.com.au/login'>NAB</a>) -- noting ME bank is: $934.07</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><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>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><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>
|
||||||
@@ -228,7 +230,12 @@
|
|||||||
$(function() { $('[data-bs-toggle="popover"]').popover(); });
|
$(function() { $('[data-bs-toggle="popover"]').popover(); });
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
$('#Sell_shares').val( {{finance['Sell_shares']}} )
|
$('#Sell_shares').val( {{finance['Sell_shares']}} )
|
||||||
$('#compare_to').val( {{finance['compare_to']}} )
|
{% if finance['compare_to'] %}
|
||||||
|
$('#compare_to').val( {{finance['compare_to']}} )
|
||||||
|
{% else %}
|
||||||
|
// set this to Nothing by default
|
||||||
|
$('#compare_to').val( 0 )
|
||||||
|
{% endif %}
|
||||||
$('#Ioniq6_future').val( {{finance['Ioniq6_future']}} )
|
$('#Ioniq6_future').val( {{finance['Ioniq6_future']}} )
|
||||||
|
|
||||||
if( $("#Ioniq6_future option:selected"). text() == 'lease' )
|
if( $("#Ioniq6_future option:selected"). text() == 'lease' )
|
||||||
@@ -301,11 +308,43 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const annotations = [];
|
const annotations = [];
|
||||||
// offset is used to make the next annotation be on slightly different vertical offsets (size is based on $'s)
|
// this annotation stuff is painful, its in y-coords for the point
|
||||||
// HACK: start at 13, later we adjust in steps of 50s allowing 4 steps, then we go back to top
|
// (based on graphs y-axis, but then needs an offset in absolute pixels) so this two coord systems makes it hard to keep the labels going, there
|
||||||
var offset=13
|
// are a few hacks below that probably will break if the data changes too much
|
||||||
|
var last_y = {{finance['annotations'][0]['y']}}
|
||||||
|
var how_many_up = 0
|
||||||
|
var how_many_down = 0
|
||||||
|
var offset_y = 0
|
||||||
|
var offset_x = 0
|
||||||
|
|
||||||
// Add annotations for changes greater than 5000
|
// Add annotations for changes greater than 5000
|
||||||
{% for a in finance['annotations'] %}
|
{% for a in finance['annotations'] %}
|
||||||
|
{% if '-$' in a['label'] %}
|
||||||
|
how_many_down += 1
|
||||||
|
offset_y = ({{a['y']}} - last_y)/650 + 25*how_many_down
|
||||||
|
offset_x = -60-10*how_many_down
|
||||||
|
if( how_many_down > 12 )
|
||||||
|
how_many_down =0
|
||||||
|
// console.log( "{{a['label']}} U:" + how_many_up + ", D: " + how_many_down + ", last_y=" + last_y + ", y={{a['y']}}, offset_y=" + offset_y + ", offset_x=" + offset_x )
|
||||||
|
last_y = {{a['y']}}
|
||||||
|
{% else %}
|
||||||
|
how_many_up += 1
|
||||||
|
offset_y = -25
|
||||||
|
{% if a['y'] > 400000 %}
|
||||||
|
offset_x = -80 -30*(6-how_many_up)
|
||||||
|
{% else %}
|
||||||
|
offset_x = -50 -10*how_many_up
|
||||||
|
{% endif %}
|
||||||
|
if( how_many_up > 3 )
|
||||||
|
how_many_up=0
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if a['y'] < 250000 %}
|
||||||
|
offset_y -= 350
|
||||||
|
{% endif %}
|
||||||
|
{% if a['y'] < 250000 and a['y'] > 200000 %}
|
||||||
|
offset_x += 100
|
||||||
|
{% endif %}
|
||||||
annotations.push({
|
annotations.push({
|
||||||
labels: [{
|
labels: [{
|
||||||
point: {
|
point: {
|
||||||
@@ -315,31 +354,23 @@
|
|||||||
xAxis: 0,
|
xAxis: 0,
|
||||||
yAxis: 0
|
yAxis: 0
|
||||||
},
|
},
|
||||||
x: -70,
|
x: offset_x,
|
||||||
{% if '-$' in a['label'] %}
|
y: offset_y,
|
||||||
y: offset,
|
|
||||||
{% else %}
|
|
||||||
y: -20,
|
|
||||||
{% endif %}
|
|
||||||
text: '{{a['label']}}'
|
text: '{{a['label']}}'
|
||||||
}], labelOptions: { allowOverlap: true }
|
}], labelOptions: { allowOverlap: true }
|
||||||
});
|
});
|
||||||
{% if a['y'] > 200000 %}
|
|
||||||
offset = ({{loop.index}} * 50 % 200) +50
|
|
||||||
{% else %}
|
|
||||||
offset = -100
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
document.keep = annotations
|
document.keep = annotations
|
||||||
|
|
||||||
// Highcharts configuration
|
// Highcharts configuration
|
||||||
Highcharts.chart('graph', {
|
Highcharts.chart('graph', {
|
||||||
chart: { type: 'line' },
|
chart: { type: 'line', zooming: { type: 'x' } },
|
||||||
colors: [ 'orange' ],
|
colors: [ 'orange' ],
|
||||||
title: { text: 'Savings Over Time' },
|
title: { text: 'Savings Over Time' },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
title: { text: 'Date' },
|
title: { text: 'Date' },
|
||||||
|
min: Date.UTC(2024, 6, 1),
|
||||||
plotBands: plotBands // Alternating background for years
|
plotBands: plotBands // Alternating background for years
|
||||||
},
|
},
|
||||||
yAxis: { title: { text: 'Amount ($)' } },
|
yAxis: { title: { text: 'Amount ($)' } },
|
||||||
@@ -360,7 +391,7 @@
|
|||||||
{% if COMP %}
|
{% if COMP %}
|
||||||
// Highcharts configuration
|
// Highcharts configuration
|
||||||
Highcharts.chart('graph-comp', {
|
Highcharts.chart('graph-comp', {
|
||||||
chart: { type: 'line' },
|
chart: { type: 'line', zooming: { type: 'x' } },
|
||||||
colors: [
|
colors: [
|
||||||
'orange', // Custom color 1
|
'orange', // Custom color 1
|
||||||
'cyan', // Custom color 2
|
'cyan', // Custom color 2
|
||||||
@@ -408,13 +439,17 @@
|
|||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="button" class="btn btn-info"
|
<button type="button" class="btn btn-info"
|
||||||
onClick="
|
onClick="
|
||||||
vars['name']=$('#save_name').val();
|
vars['name'] = $('#save_name').val();
|
||||||
$.ajax( {
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: '/save',
|
url: '/save',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
data: JSON.stringify( { 'vars': vars, 'savings_data' :savingsData } ),
|
data: JSON.stringify({ 'vars': vars, 'savings_data': savingsData }),
|
||||||
success: function() { $('#save_modal').modal('hide'); } } )"
|
success: function(resp) {
|
||||||
|
$('#save_modal').modal('hide');
|
||||||
|
$('#compare_to').append($('<option>', { value: resp.cset_id, text: resp.name }));
|
||||||
|
}
|
||||||
|
});"
|
||||||
>Save</button>
|
>Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user