Compare commits

...

113 Commits

Author SHA1 Message Date
3b33390c3e updated ME bank amount in help, but mainly fixed year start being hard-coded to 2025, now works for any year 2025-12-26 11:09:01 +11:00
f309dfa947 redid bills UI, you can now choose CPI or FLAT X% as well, via a drop-down not a set of buttons. The changing of inflation also tweaks any bills using CPI, all works 2025-12-23 18:35:52 +11:00
ce20c57d11 added health check, forgot to add template/cset <- allows deletion of comparison sets in earlier commit, and added a new TODO 2025-11-05 01:13:22 +11:00
d4662b9051 update TODO 2025-11-04 11:36:56 +11:00
0a9a50f9a1 added ability to delete comparison sets, also made future bills recalc for Hydunday/D_quit dependent future bills, this is not effectively functional -v1.0 :) 2025-11-04 11:36:36 +11:00
9cc907fb62 added a basic comparison set page, with a table of the data that changes only (just to fit it in) and allowing them to be deleted 2025-11-01 22:48:21 +11:00
4bb336645a have added quick re-estimate button 2025-10-29 22:57:50 +11:00
227e95cab7 add a button to recalc bills by removing all esimated bills and rebuild them 2025-10-17 21:22:07 +11:00
bf66e9fa7c redo D_quit logic (tax and when we can sell shares) 2025-10-17 21:20:22 +11:00
5ce614ed28 added note around tax and quitting 2025-10-17 21:19:50 +11:00
b6b396342f fix up annotation bug where we put daily amt, not bill amt in annot 2025-10-05 12:09:21 +11:00
fb2fffea7b noting need to tie bills/finance page items to recalc (change date of quit, need to redo bills future) 2025-10-05 12:08:33 +11:00
252dc23364 make payment annotations show as negatives, use this to make annotations of adding go above graph, and generally payment annotaions below the line. Then switch to have the last few annoations above the graph regardless as we run out of room at that end of the graph 2025-09-16 23:24:19 +10:00
a75db565ee Created tabbed interface for the front page, update the TODO to match 2025-09-16 22:51:23 +10:00
b1614760a6 more thoughts/how to make this more usable longer-term 2025-09-16 12:57:11 +10:00
670a63cfd7 updated TODO, removed old ones, added new around UI changes to have tabbed lower data/graphs 2025-09-16 12:54:16 +10:00
1c112e6f6b move future bills into bills.py, away from calc.py for file content consistency 2025-09-15 22:17:17 +10:00
8274da0ce0 fix up containerfluid to container-fluid, and add some margin for left/right on bills 2025-09-15 22:16:40 +10:00
6618dd16b4 use warning instead of info for consistency 2025-09-11 21:19:25 +10:00
4594630b9e simple visual treatment of key dates 2025-09-11 20:45:34 +10:00
45d173e236 actually nope, not a bug, I cant count :) 2025-09-11 17:55:44 +10:00
5ca99ca1f4 update to note fortnightly pay dates are wrong on year rollover and so I am paying myself a week early at those times 2025-09-11 17:53:56 +10:00
4389045ed5 moved some hard-coded dates to top of calc.py for ease of use in multiple functions, but also just for code readability, they are more like constants than variables. Code now works out key_dates for use in dealing with future bills / next steps 2025-09-11 17:52:28 +10:00
b69ec82510 just made graph taller for now, might one day make it smaller or larger depending on whether we are comparing or not? 2025-09-11 17:50:54 +10:00
2bd39ab24c updated TODO with progress and clarifying next steps 2025-09-11 17:50:19 +10:00
c49520af7a handle simple future bills and their growth all now done 2025-09-05 16:32:16 +10:00
1a56f80cca more clean ups 2025-09-05 16:20:09 +10:00
6ae1023f6e removed debugs, add automatic annotations if bill amount is > $1000, factor in growth for ioniq6*, added debug too 2025-09-05 16:19:53 +10:00
fc1746d749 also update to reflect bill_type_id change to bill_type 2025-09-05 16:19:01 +10:00
489fb3ee2b update comment 2025-09-05 16:13:51 +10:00
c5cfc00793 put back ioniq 6 future bills, but use bill data to set values - assumption at present is they are yearly bills, could do better, but good enough for now. This commit also changes selects to return bill_type not bill_type_id and removed some debugs for bill amt in calc loop 2025-09-05 12:20:08 +10:00
ebac4aaf66 incorporate bills for dates/amounts into calculations, still need to do future bills on triggers 2025-09-03 22:35:24 +10:00
4b63b8bd44 complete the future bill handling, added new UI to match a need for it, also tighten up other TODO items 2025-09-02 23:03:48 +10:00
a0d9ac45cd remove debug, and handle (by skipping) future dated bills 2025-09-02 23:03:03 +10:00
d80cffa0dd fix up mistaken col-4 for wrong Date header, move from name to bill_type for new/update bill and support future dates by showing future in the text, rather than show an actual input type=date 2025-09-02 23:01:00 +10:00
2459dc6ea1 allow handling creating future bills - for when I quit, and will help with when switch to owning Ioniq 6 2025-09-02 22:59:45 +10:00
95d792e72f added a set of titles when adding new bill / new bill types, allows to toggle date to be when quit or normal date, with normal date we use data, with when quit, we have growth we will use for simple growth and then date(s) can be factored in based on when I quit which is changable in the main financial data 2025-09-02 22:05:16 +10:00
5914f3fdd4 clarifying next TODO 2025-08-31 16:52:33 +10:00
c21bda8da0 the 5 is unnecessary, as the width of 6ch does the sizing anyway 2025-08-31 16:48:40 +10:00
f4490e937a with dark mode, using info instead of primary feels easier on the eye and allows for consistency with graph colours 2025-08-31 16:41:40 +10:00
e373dd0009 Change approach with Qtr bills, all are simple based on last qtr - when we have normal Qtr bills (freq 'Quarterly') we just use last qtrs data for growth much easier. For GAS, we have the 'Quaterly (forced)' freq. which uses the forced/calc. amount per qtr for growth, its good enough for Gas bills - which dont always have 4 bills a year 2025-08-31 16:32:18 +10:00
3a5b77f12d Change approach with Qtr bills, all are simple based on last qtr - when we have normal Qtr bills (freq 'Quarterly') we just use last qtrs data for growth much easier. For GAS, we have the 'Quaterly (forced)' freq. which uses the forced/calc. amount per qtr for growth, its good enough for Gas bills - which dont always have 4 bills a year 2025-08-31 16:32:00 +10:00
c74383f89e added a commented out simple debug of totals to help understand bills for now - prob will make this into real table somehow in future 2025-08-31 16:29:29 +10:00
4a7080787b new item 2025-08-31 16:28:36 +10:00
07f2a321ec added support for Quarterly (seasonal) and Quarterly (fixed), also updated live DB to match 2025-08-31 11:07:08 +10:00
f67ca61cc7 working through future bills, we need to do quarterly future estimates differently for seasonal/fixed bills 2025-08-31 11:06:29 +10:00
9ad5089ac5 updated README, todo & bugs are now in own files 2025-08-31 11:05:57 +10:00
17f2534056 bug fixed where we had a bill deleted it then growth was still trying to be calc when it no longer could 2025-08-31 10:39:58 +10:00
392daa1deb fixed total not showing in %.2f BUG 2025-08-31 10:33:08 +10:00
2937866617 improve how we find monthly bill in a month, dont use exact date just use yy-mm, also fix BUG where kayo used estimate in jan to project for the next 5years, rather than real bill in feb (all in the future) to estimate 2025-08-31 10:24:46 +10:00
0ab0a112e4 new BUGs 2025-08-31 10:08:22 +10:00
4c96a9b576 with monthly bills and < 12 in first year, wont have full yr total so cant do simple growth - fix for this 2025-08-31 10:08:14 +10:00
7ad767759f remove debug 2025-08-31 10:07:32 +10:00
24c581a35a use totals to calc bill totals - should be less work, also improved cosmetics of totals display 2025-08-30 14:29:31 +10:00
3749c01e93 hacky better formatting of Totals 2025-08-30 14:10:56 +10:00
c41048ab82 made lhs wider, shrank rhs, put this_year total per bill type in lhs, altered totals formatting at bottom 2025-08-30 14:00:14 +10:00
7422321227 call calc_future_totals to pass data onto html so we can show per bill type annual amount in this year 2025-08-30 13:59:29 +10:00
338b63aa06 now the UI shows annual costs, updated the comments and bills figure again to be more accurate 2025-08-30 13:58:56 +10:00
de32bdc7ff now have a calc_future_totals func that is used to allow html to show the bills as a simple annualised cost per year 2025-08-30 13:58:29 +10:00
4a2dd4d2da want to tweak formating and have current year totals included 2025-08-30 13:14:03 +10:00
aee8916471 updating comment / bills total 2025-08-30 13:13:43 +10:00
8f69023ffd format totals per year and based on dates/END_YEAR 2025-08-28 21:19:58 +10:00
d2bf472845 clean up TODO 2025-08-28 20:49:20 +10:00
e84faffd79 when choosing a Tab, make the new bill drop-down be of that type. When we save a new bill, change the last-tab to be the type of bill we just added 2025-08-28 20:47:12 +10:00
2bdd1348b8 fixed a few bugs, annual growth was just broken, dont add another estimate bill when we have one for that year or in that quarter, removed lots of debugs, fixed a few bugs where the first data point in a new year/qtr would not have arrays initialised properly first, apportion quarterly data in future real bills - it happens with Rates 2025-08-28 19:49:41 +10:00
91ebc227b6 add support for simple growth, also remove all estimated bills when we add a new real bill 2025-08-28 19:46:51 +10:00
89fe874c5c added simple growth, changed column widths and header formats to work better. Added a quick and dirty Total bills in 2025 section 2025-08-28 19:46:23 +10:00
dda3a3e3fe now we have dark mode, use different colors for lines on graphs 2025-08-28 19:45:21 +10:00
706aee6947 add a quick TODO to make UI slicker when adding bills 2025-08-28 19:45:02 +10:00
742911ec1b still this bug - adding a qtrly bill years before the rest 2025-08-28 19:44:40 +10:00
89d58e4cd3 fixed a few logic bugs with quarterly data, now accurately puts bill proportions into relevant quarters and estimates future bills based on quarterly data - all works so far 2025-08-25 18:46:24 +10:00
65fc68e0bf gone with forced dark mode 2025-08-24 22:00:45 +10:00
f3b828b051 well, calculated proportioned per quarter values - not making much difference on growth numbers, BUT, will now be able to project future estimates based on full quarter cost, not just a bill in that quarter that may only cover 2 months 2025-08-24 16:09:51 +10:00
4b5b713c20 hack to handle gass bills (for now), seems we have more than 4 for a quarterly cycle, and they are all over the shop/inconsistent. Added thoughts on how to tackle in TODO 2025-08-23 10:48:29 +10:00
54c4c38403 noting multi-year growth figures are off - e.g. internet stays same price for 3 years 2025-08-22 18:14:31 +10:00
1719032ebf wrapped new_bill in new_estimated_bill func, that adds to DB and to local bill_info, use this better to fill in quarterly future bills. Also exclude in growth calculations the final year of real bill if it includes estimates and real figures. For now, this is usable 2025-08-22 18:10:09 +10:00
6bccfade2b remove debug 2025-08-22 18:00:15 +10:00
b05f7b05e8 now remembers ui values for which tab we are on and whether we clicked show estimated or not 2025-08-22 16:51:04 +10:00
5bd94fc2c5 added basic UI improvements 2025-08-22 16:06:47 +10:00
444a01ea42 removed lots of debugs, added find_next_bill to allow better estimating of bills in the past. Growth now uses bills in the past (estimated included), which simplifies the code a bit. All working now except pragmatic missing quarterly bills not started 2025-08-22 16:01:45 +10:00
1729c93bcd cant have bills that are further than a year apart 2025-08-22 15:51:02 +10:00
78141d097f amount needs to be cast to a float to be a .2f in new bill 2025-08-22 14:07:49 +10:00
0cf3d9897f created find_previous_bill and use it to help work out gaps in bills - e.g. like with internet where I added only the new costs 2025-08-22 13:24:25 +10:00
3bfeb30640 fixed so estimating old bills works now 2025-08-22 13:23:49 +10:00
aa0512087f update BUG 2025-08-21 18:23:06 +10:00
3521d4c126 make radio button for min / avg / max growth value be pushed into which_growth in the DB for bill_type row, then also delete estimated bills for that bill_type, and then calling /bills, causes the estimated bills to be filled back in based on the new chosen growth model 2025-08-21 18:20:32 +10:00
ada6dfa3f5 allow growth toggle radio buttons, non-functional, just looks 2025-08-21 17:36:17 +10:00
19cba866de pragmatic growth patterns completed 2025-08-21 16:52:59 +10:00
cd7eca0c6e simplified when we calc totals and hence growth, applied monthly growth annually 2025-08-21 16:52:37 +10:00
c469f6d281 force new bill to restrict the amount to $/c (2 decimal points) 2025-08-21 16:51:57 +10:00
3d95cd1d2e estimates now show as italic and do not get any action buttons when shown 2025-08-20 18:30:02 +10:00
d7320e8aa8 remove num we now use - bill_info[bill_type][num_ann_bills], fix bug where we were re-adding first year bills 2025-08-20 18:21:52 +10:00
5556b0ef15 removed debugs, actually add new bills when needed for monthly and annual, support new growth fields, ensure growth only works on real bills not new estimated bills. 2025-08-20 18:11:00 +10:00
9cd14505bf moved new bill type button over 2 more to accom new freq field, added slider/support for show estimated and support new growth db fields 2025-08-20 18:08:44 +10:00
b43b472e4b support esimated for new_bill - any GUI new bill is not an estimate 2025-08-20 18:07:39 +10:00
5ebd623d88 replaced ann_growth with ann_growth_min, ann_growth_avg, ann_growth_max in DB 2025-08-20 18:07:09 +10:00
676e9ab95f really should consider quarterly bill additions as seasonal <- more likely for elec, gas, etc 2025-08-18 17:50:50 +10:00
7ac7acf44c major rewrite, took on-board thoughts in TODO, have completely re-written how we process bill_data, and then subsequent growth. Much simpler now (although still complex) - most is now done in one loop to take DB data nd reformat it into an in memory data structure, then process that a few different ways to see missing and future bills, and then calc growths. Still much to go, I do calc missing/future annual bills, but I am not actually adding them to the DB (want to distinguish them from real bills still in DB), not yet calculating additional bills for monthly or quarterly (so not adding them to DB either), then interface would need to show/hide real vs auto-filled bills. To note growth only takes into account real bills, BUT, it also only calcs growth on consecuttive full year data sets - e.g. years with quarterly bills for less than the full year are ignored for now 2025-08-18 17:49:36 +10:00
232f16deba made bill_freq have simple / hard-coded number of bills for a year, e.g. annual == 1, monthly == 12, etc) 2025-08-18 17:45:20 +10:00
a1ed4e364c put more energy into how to calculate future/missing bills 2025-08-18 17:44:41 +10:00
c05fa1cc61 clean up / rename derive_bill_data to be process_bill_data 2025-08-18 17:44:16 +10:00
0df1d4d2d2 use shared define of END_YEAR 2025-08-18 17:43:57 +10:00
28b07c0842 just shared defines, only 1 for now 2025-08-18 17:42:59 +10:00
cf104b5a56 added some better formatting (spacing, headers to tables, etc), flipped the left / right, so now bill type is on left with support for bill_freq being a <select> and on the right, we now have tabbed views of different bill_types 2025-08-18 11:16:32 +10:00
98fa17acd7 first pass of trying to work through deriving annual growth on bills, what info I need, etc. definitely not even close to finished 2025-08-18 11:14:21 +10:00
6403ca7775 add support for bill_freq 2025-08-18 11:13:55 +10:00
adac3eceeb added bill_freq table and referenced it, tweak growth field to ann_growth and added a set_bill_type_growth, for when we can derive a value 2025-08-18 11:13:31 +10:00
27048a450f time to have a more formal TODO 2025-08-18 11:12:21 +10:00
7bab6eabdd finished functional bills page, all naming conventions consistent for html entities, classes, and matching DB fields 2025-08-14 15:54:50 +10:00
b02e03339e Changing a bill (bill_data) now works, as does cancelling cleanly - this is now functional. I have renamed/improved the left-hand-side fields, right-hand-side next - to improve consistency between html and db and bill_data and bill_type 2025-08-14 15:32:09 +10:00
0c0745fe68 First pass of adding bills to finplan.
We now have a new page /bills that shows any bills on the left-hand side (type, date, amount)
and bill types and some derived values (frequency and annual growth rate) on the right-hand side

The new bill, and new bill type buttons/logic all work
The delete bill and bill type buttons/logic all work

The change bill type logic all works (and is a touch complex, it alters the GUI
to show/hide different buttons, and disable/re-enable content in the bill types
table

THe change bill is disabled for now and for later
2025-08-14 12:15:26 +10:00
e01af0b92b payrise increse included 2025-08-09 22:51:55 +10:00
1b0653a7fa use mara now we have DNS on modem 2025-08-09 22:51:34 +10:00
12 changed files with 2008 additions and 222 deletions

4
BUGS Normal file
View File

@@ -0,0 +1,4 @@
* kayo bills are wrong in between normal bills
* added an electricity bill by accident for 2018, that kills lots :(
- something to do with missing year of data in quarterly bills - still an issue

11
README
View File

@@ -1,18 +1,11 @@
TODO:
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:
cd ~/src/finplan
source ./.python/bin/activate
FLASK_APP=main ./.python/bin/flask --debug run --host=192.168.0.2
FLASK_APP=main ./.python/bin/flask --debug run --host=mara.ddp.net

24
TODO Normal file
View 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
View 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

149
calc.py
View File

@@ -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']
@@ -46,8 +72,8 @@ def calculate_savings_depletion(finance):
# if we could stretch this to July 2026, then would be more (due to less tax)
bus_days_in_fortnight=10
# this is what I earn before-tax (and I *THINK* vehicle allowance won't be paid X 12 weeks)
pre_tax_D_earning = 7830.42
# 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
# whenever I leave, I get 12 weeks (or 60 business days) + whatever leave they owe me
payout = ((60+D_leave_owed_in_days)/bus_days_in_fortnight) * pre_tax_D_earning
@@ -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:
D_quit_date = current_date
# going to pay tax on payout, so claim it back next year
claim_tax_on_leave = True
print(f"{current_date}: D has resigned in new year- get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax_new_fin_year}" )
current_savings += D_leave_after_tax_new_fin_year
add_annotation(finance, current_date, current_savings, D_leave_after_tax_new_fin_year, "D quit" )
else:
claim_tax_on_leave = False
print(f"{current_date}: D has resigned - get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax}" )
print(f"{current_date}: D has resigned in new year- get paid out my 12 weeks + remaining leave and lose some to tax - ${D_leave_after_tax}" )
current_savings += D_leave_after_tax
add_annotation(finance, current_date, current_savings, D_leave_after_tax, "D quit" )
D_leave_after_tax = 0
# its end of 'next' fin year, if tax_diff > 0, then ddp quit after new tax year and gets back the overpaid tax
if current_date > new_fin_year_26 and claim_tax_on_leave:
current_savings += tax_diff_D_leave
add_annotation(finance, current_date, current_savings, tax_diff_D_leave, "D quit - tax back" )
# can only claim the tax back once :)
claim_tax_on_leave=False
if fortnight_income:
print(f"{current_date}: salary paid by Deakin - adding: {fortnight_income}" )
current_savings += fortnight_income
@@ -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

258
db.py
View File

@@ -84,23 +84,88 @@ 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
)''')
# Check if table is empty, if so insert default values
cur.execute('''CREATE TABLE IF NOT EXISTS bill_type (
id INTEGER PRIMARY KEY AUTOINCREMENT,
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 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bill_type INTEGER,
amount INTEGER,
bill_date DATE,
estimated INTEGER,
FOREIGN KEY(bill_type) REFERENCES bill_type(id)
)''')
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: $1434.3
# 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()
@@ -113,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}") )
@@ -217,6 +283,7 @@ def get_comp_set_data(cset_id):
# do this for convenience in printing single last cset data point
COMP['date'], COMP['amount'] = COMP['savings_data'][-1]
conn.close()
return COMP
@@ -229,4 +296,171 @@ def get_comp_set_options(finance):
# get saved finance data for this comparison set
cur.execute( f"select id, name from comparison_set order by id" )
finance['COMP_SETS'].extend( cur.fetchall() )
conn.close()
return
def get_bill_data(order_by):
conn = connect_db(True)
cur = conn.cursor()
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
def get_bill_types():
conn = connect_db(True)
cur = conn.cursor()
cur.execute('SELECT * FROM bill_type order by name')
bt = cur.fetchall()
conn.close()
return bt
def use_growth( bill_type, which_growth ):
conn = connect_db(False)
cur = conn.cursor()
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
def update_bill_data( id, name, amount, bill_date ):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"update bill_data set bill_type =(select id from bill_type where name ='{name}'), amount='{amount}', bill_date='{bill_date}' where id = {id}" )
conn.commit()
conn.close()
return
def insert_bill_type( bt, fq ):
conn = connect_db(False)
cur = conn.cursor()
print( f"fq={fq}" )
cur.execute( f"insert into bill_type ( 'name', 'freq', 'ann_growth_min', 'ann_growth_avg', 'ann_growth_max', 'ann_growth_simple' ) values ( '{bt}', {fq}, 0, 0, 0, 0 )" )
conn.commit()
conn.close()
return
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}', freq={freq} where id = {id}" )
conn.commit()
conn.close()
return
def delete_bill(id):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"delete from bill_data where id = '{id}'" )
conn.commit()
conn.close()
return
def delete_bill_type( id ):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"delete from bill_type where id = '{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
View File

@@ -0,0 +1 @@
END_YEAR=2031

161
main.py
View File

@@ -1,11 +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 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__)
@@ -20,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:
@@ -95,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():
@@ -105,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'],
@@ -133,21 +146,137 @@ def update():
request.form['compare_to'],
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():
finance_data = get_finance_data()
# work out when D quits, when car is owned
key_dates = calc_key_dates( finance_data )
bill_data = get_bill_data("order_by_bill_type_then_date")
bill_types = get_bill_types()
bill_freqs = get_bill_freqs()
bill_ui = get_bill_ui()
bill_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'], data['freq'] )
return "200"
@app.route('/updatebilltype', methods=['POST'])
def UpdateBillType():
data = request.get_json()
update_bill_type( data['id'], data['bill_type'], data['freq'] )
return "200"
@app.route('/newbill', methods=['POST'])
def InsertBill():
data = request.get_json()
# 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['bill_type'], data['amount'], data['bill_date'] )
return "200"
@app.route('/delbilltype', methods=['POST'])
def DeleteBillType():
data = request.get_json()
delete_bill_type( data['id'] )
return "200"
@app.route('/delbill', methods=['POST'])
def DeleteBill():
data = request.get_json()
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)
##########
#
# How to cross-check, so we get paid: 4762.29 + 1962.56 per fortnight or: $174846.1 AFTER TAX
# take $20k for Cam, and $20k for Mich for schools last year, $10k for Cam pres, take $72k for living, take $8k in furniture.
# We went from 250 to 300k (more or less), so about right
# to note: transfers to Cam/Mich - $850
#
##########

View File

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

525
templates/bills.html Normal file
View File

@@ -0,0 +1,525 @@
<!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 (Bill Details)</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">Bill Details (go to <a href="/">Finance Tracker</a>)</h3>
{# DEBUG totals if needed
<table>
{% for bt in total %}
<tr><td></td><td>&nbsp;{{bt}}:</td>
{% for yr in range( 2025, 2032 ) %}
{% if yr in total[bt] %}
<td>
{{total[bt][yr]}}
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
#}
<div class="col-8">
<div class="row">
<div class="col-2 form-control-inline d-none new-bill-type-class">Bill Type</div>
<div class="col-2 form-control-inline d-none new-bill-type-class">Frequency</div>
</div>
<div class="row align-items-center mb-3">
<button id="new-bill-type-button" class="mt-4 px-0 offset-4 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillType()"><span class="bi bi-plus-lg"> New Bill Type</span></button>
<div class="new-bill-type-class px-0 col-2 d-none"> <input type="text" class="form-control text-end float-end border border-primary" id="new-bill-type-name"></div>
<div class="new-bill-type-class px-0 col-2 d-none"><select id="new-bill-type-freq" class="form-select text-center">
{% for bf in bill_freqs %}
<option value={{bf.id}}>{{bf.name}}</option>
{% endfor %}
</select>
</div>
<button id="save-bill-type" class="new-bill-type-class px-0 col-1 btn btn-success bg-success-subtle text-success d-none" onClick="NewBillType()"><span class="bi bi-floppy"></span> Save</button>
<button id="canc-bill-type" class="new-bill-type-class px-0 col-1 btn btn-danger bg-danger-subtle text-danger d-none" onClick="CancelNewBillType()"><span class="bi bi-x"> Cancel</span></button>
<button id="recalc-bills" class="mt-4 col-2 offset-3 btn btn-warning bg-warning-subtle text-warning" onClick="ForceRecalcBills()"><span class="bi bi-repeat"> Recalculate</span></button>
</div>
<div class="row">
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Name</ ></div>
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Frequency</ ></div>
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold
bg-body-tertiary rounded-0">Annual Growth</ ></div>
{% for yr in range( 2025, 2032 ) %}
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0
fw-bold bg-body-tertiary rounded-0">{{yr}} Total</ ></div>
{% endfor %}
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0">Actions</ ></div>
{# spacer to get header line right now we don't use forced col widths #}
<div class="px-0 col"><label class="form-control d-flex
align-items-end justify-content-center h-100 border-0 fw-bold bg-body-tertiary rounded-0"> </ ></div>
</div>
{% for bt in bill_types %}
<div class="row">
<div class="px-0 col-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-4">
<div class="row">
<div class="col-2 form-control-inline d-none new-bill-data-class">Bill Type</div>
<div id="new-bill-data-date-label" class="col-4 form-control-inline d-none new-bill-data-class">Date</div>
<div id="new-bill-data-growth-label" class="col-4 form-control-inline d-none">Est. Annual Growth</div>
<div class="col-2 form-control-inline d-none new-bill-data-class">Amount</div>
</div>
<div class="row align-items-center mb-3">
<button id="new-bill-data-button" class="mt-4 px-0 offset-8 col-2 btn btn-success bg-success-subtle text-success" onCLick="StartNewBillData()"><span class="bi bi-plus-lg"> New Bill</span></button>
<div class="new-bill-data-class px-0 col-2 d-none"> <select id="new-bill-data-type" class="form-select text-end float-end border border-primary">
{% for bt in bill_types %}
<option value={{bt.id}}>{{bt.name}}</option>
{% endfor %}
</select>
</div>
<div class="new-bill-data-class px-0 col-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 %}
</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')
$('#new-bill-data-button').addClass('d-none')
$('#new-bill-data-type').focus()
}
function NewBill()
{
if( $('#new-bill-data-growth').hasClass('d-none') )
{
// if growth is hidden, then we have normal bill
$.ajax( { type: 'POST', url: '/newbill',
contentType: 'application/json',
data: JSON.stringify( {
'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()
{
$('.new-bill-data-class').addClass('d-none')
$('#new-bill-data-button').removeClass('d-none')
// reset select to first option
$('#new-bill-data-type').val( $('#new-bill-data-type option:first').attr('value') )
// clear out date and amount
$('#new-bill-data-date').val('')
$('#new-bill-data-amount').val('')
}
function StartUpdateBill( id )
{
val=$('#bill-data-amount-'+id).val()
// enable date and amount fields
$('#bill-data-type-'+id).removeClass('bg-white')
$('#bill-data-date-'+id).prop('disabled', false )
$('#bill-data-amount-'+id).prop('disabled', false )
// "enable" name for edits
$('#bill-data-amount-'+id).prop('disabled', false).focus()
// move cursor to the end after 'focus()' above
$('#bill-data-amount-'+id).val('').val( val )
// alter change/delete buttons to be save/cancel
$('#bill-data-chg-'+id).addClass('d-none')
$('#bill-data-del-'+id).addClass('d-none')
$('#bill-data-save-'+id).removeClass('d-none')
$('#bill-data-canc-'+id).removeClass('d-none')
}
function UpdateBill(id)
{
$.ajax( { type: 'POST', url: '/updatebill',
contentType: 'application/json', data: JSON.stringify( {
'id': id,
'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' } } )
}
function CancelUpdateBill( id, bill_type, bill_date, amount )
{
// fix-up type, date and amount fields
$('#bill-data-date-'+id).prop('disabled', true )
$('#bill-data-amount-'+id).prop('disabled', true )
// alter change/delete buttons to be save/cancel
$('#bill-data-chg-'+id).removeClass('d-none')
$('#bill-data-del-'+id).removeClass('d-none')
$('#bill-data-save-'+id).addClass('d-none')
$('#bill-data-canc-'+id).addClass('d-none')
// finally we might have modified the string, and then clicked cancel, so rest bill values to orig
$('#bill-data-type-'+id).val(bill_type)
$('#bill-data-date-'+id).val(bill_date)
$('#bill-data-amount-'+id).val(amount)
}
function DeleteBill( id )
{
$.ajax( { type: 'POST', url: '/delbill', contentType: 'application/json',
data: JSON.stringify( { 'id': id } ), success: function() { window.location='bills' } } )
}
function StartNewBillType()
{
$('.new-bill-type-class').removeClass('d-none')
$('#new-bill-type-button').addClass('d-none')
$('#new-bill-type-name').focus()
}
function CancelNewBillType()
{
$('.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(), 'freq': $('#new-bill-type-freq').val() } ),
success: function() { window.location='bills' } } )
}
function StartUpdateBillType( id )
{
val=$('#bill-type-name-'+id).val()
// "enable" fields for edits
$('.bill-type-'+id).prop('disabled', false)
// 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 )
// alter change/delete buttons to be save/cancel
$('#bill-type-chg-'+id).addClass('d-none')
$('#bill-type-del-'+id).addClass('d-none')
$('#bill-type-save-'+id).removeClass('d-none')
$('#bill-type-canc-'+id).removeClass('d-none')
}
function UpdateBillType(id)
{
$.ajax( { type: 'POST', url: '/updatebilltype',
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)
{
// "disable" name for edits
$('.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')
$('#bill-type-save-'+id).addClass('d-none')
$('#bill-type-canc-'+id).addClass('d-none')
// finally we might have modified the string, and then clicked cancel, so rest bill type to its orig name
$('#bill-type-name-'+id).val(orig_name)
}
function DelBillType( id )
{
$.ajax( { type: 'POST', url: '/delbilltype', contentType: 'application/json',
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(); });
// if we hit enter in new bill type name field, save it
$("#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
View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<title>Finance Form (Comparison Sets)</title>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/annotations.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
<style>
.col-form-label { width:140px; }
html { font-size: 80%; }
</style>
</head>
<body>
<div class="pt-2 mx-2 container-fluid row">
<h3 align="center">Comparison Sets (go to <a href="/">Finance Tracker</a>)</h3>
<div class="row">
</div>
<table class="table table-sm table-dark table-striped">
<thead>
<tr>
<th class="text-center">Action</th>
{% for d in comp_data[finance['COMP_SETS'][1][0]]['vars'] %}
{% if d != 'id' %}
<th class="text-center">{{ d|replace('_', '<br>')|safe }}</th>
{% endif %}
{% endfor %}
</tr>
</thead>
{% for c in finance['COMP_SETS'] %}
{% if c[0] != 0 %}
<tr>
<td><button class="btn btn-danger bg-danger-subtle text-danger"
onClick="DelCSet({{c[0]}})"><span class="bi bi-trash3"> Delete</button></td>
{% for d in comp_data[c[0]]['vars'] %}
{% if d != 'id' %}
{% if d == 'name' %}
<td class="align-middle">{{comp_data[c[0]]['vars'][d]}}</td>
{% else %}
<td class="text-center align-middle">{{comp_data[c[0]]['vars'][d]}}</td>
{% endif %}
{% endif %}
{% endfor %}
</tr>
{% endif %}
{% endfor %}
</table>
<script>
function DelCSet( id )
{
// POST to a delete, success should just reload this page
$.ajax( { type: 'POST', url: '/delcset', contentType: 'application/json', data: JSON.stringify( { 'id': id } ), success: function() { window.location='cset' } } )
}
</script>
</body>
</html>

View File

@@ -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</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,6 +103,19 @@
</div>
{% endfor %}
<!-- create tabbed view for each bill type -->
<nav id="ft-nav" class="nav nav-tabs pt-3">
<button class="nav-link" id="tab-but-findata" data-bs-toggle="tab" data-bs-target="#tab-findata" type="button" role="tab" aria-controls="tab1" aria-selected="true" >Fortnighthly Savings data</button>
<button class="nav-link" id="tab-but-graph" data-bs-toggle="tab" data-bs-target="#tab-graph" type="button" role="tab" aria-controls="tab2" aria-selected="true" >Graph of savings over time</button>
{% if COMP %}
<button class="nav-link" id="tab-but-compgraph" data-bs-toggle="tab" data-bs-target="#tab-compgraph" type="button" role="tab" aria-controls="tab3" aria-selected="true" >Comparison graph</button>
{% else %}
<button class="nav-link" id="tab-but-compgraph" data-bs-toggle="tab" data-bs-target="#tab-compgraph" type="button" role="tab" aria-controls="tab3" aria-selected="true" disabled>Comparison graph</button>
{% endif %}
</nav>
<div class="tab-content">
<div id="tab-findata" class="tab-pane">
<h5 align="center" class="mt-4">Fortnighthly Savings data:
{% if COMP %}
{# get comparison date so we can use it below in loop to know when to print it out #}
@@ -91,28 +130,47 @@
</h5>
<div class="row">
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center" style="background:lemonchiffon">2025</div>
<div class="col-auto"> <div class="pt-1 pb-1 mb-0 alert text-center bg-secondary text-light">{{savings[0][0][:4]}}</div>
{# inside started if below, we add blank lines to the start of the year so the dates line up #}
{% for _ in range( 0, padding ) %}
<br>
{% endfor %}
{% set car_done=namespace( val=0 ) %}
{% for date, dollars in savings %}
{% set yr=date[:4] %}
{% set mon=date[5:7] %}
{% set day=date[8:10 ] %}
{% set car_yr=key_dates['D_hyundai_owned'][:4] %}
{% set car_mon=key_dates['D_hyundai_owned'][5:7] %}
{% set car_day=key_dates['D_hyundai_owned'][8:10 ] %}
{% if yr|int > first_yr|int and mon == '01' and day|int <= 14 %}
</div><div class="col-auto">
<div class="pt-1 pb-1 mb-0 alert text-center" style="background:lemonchiffon">{{yr}}</div>
<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 %}
<font color="black">
{{ date }}:&nbsp;
{{ '$%0.2f' % dollars|float }}<br>
</font>
</label>
</font><br>
{% if comp_done.val == 0 and yr == comp_yr and mon == comp_mon and day|int >= comp_day|int %}
<font color="blue">
<font class="text-info">
{{ COMP['date'] }}:&nbsp;
{{ '$%0.2f' % COMP['amount']|float }}<br>
</font>
@@ -136,9 +194,9 @@
</div>
<div id="comp_col" class="col-auto">
<div class="input-group">
<button type="button" class="btn btn-primary me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save</button>
<button type="submit" class="disabled btn btn-primary rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button>
<select class="form-select border border-primary text-primary" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
<button type="button" class="btn btn-info me-2 rounded" data-bs-toggle="modal" data-bs-target="#save_modal">Save Comparison set</button>
<button type="submit" class="disabled btn btn-info rounded-start" onClick="$('#vals_form').submit() disabled">Compare to:</button>
<select class="form-select border border-info text-info" id="compare_to" name="compare_to" onchange="$('#vals_form').submit()">
{% for el in finance['COMP_SETS'] %}
<option value="{{el[0]}}">{{el[1]}}</option>
{% endfor %}
@@ -150,13 +208,22 @@
</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,14 +294,13 @@
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({
@@ -246,18 +313,26 @@
yAxis: 0
},
x: -70,
{% if '-$' in a['label'] %}
y: offset,
{% else %}
y: -20,
{% endif %}
text: '{{a['label']}}'
}], labelOptions: { allowOverlap: true }
});
{% if a['y'] > 200000 %}
offset = ({{loop.index}} * 50 % 200) +50
{% else %}
offset = -100
{% endif %}
{% endfor %}
document.keep = annotations
{% endif %}
// Highcharts configuration
Highcharts.chart('container', {
Highcharts.chart('graph', {
chart: { type: 'line' },
colors: [ 'orange' ],
title: { text: 'Savings Over Time' },
xAxis: {
type: 'datetime',
@@ -273,13 +348,37 @@
}, shared:true
},
annotations: annotations, // Add annotations
series: [ { name: "Savings", data: chartData, marker: { radius: 2 } } ]
});
{% if COMP %}
// Highcharts configuration
Highcharts.chart('graph-comp', {
chart: { type: 'line' },
colors: [
'orange', // Custom color 1
'cyan', // Custom color 2
],
title: { text: 'Savings Over Time' },
xAxis: {
type: 'datetime',
title: { text: 'Date' },
plotBands: plotBands // Alternating background for years
},
yAxis: { title: { text: 'Amount ($)' } },
tooltip: {
pointFormatter: function ()
{
if( this.series.symbol == 'circle' ) { s='\u25CF' } else { s='\u2B25' }
return '<span style="color:' + this.point.color + '">' + s + '</span> <b>' + this.point.y + ':</b> ' + this.series.name + '<br>'
}, shared:true
},
series: [
{ name: "Savings", data: chartData, marker: { radius: 2 } }
{% if COMP %}
,{ name: "{{COMP['vars']['name']}}", data: compChartData, marker: { radius: 2 } }
{% endif %}
]
});
{% endif %}
});
</script>
<div id="save_modal" class="modal modal-lg" tabindex="-1">
@@ -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>