Compare commits

...

6 Commits

4 changed files with 127 additions and 59 deletions

119
bills.py
View File

@@ -1,7 +1,6 @@
from db import get_bill_data, get_bill_types, get_bill_freqs, set_bill_type_growth
from db import get_bill_data, get_bill_types, get_bill_freqs, set_bill_type_growth, new_bill
from defines import END_YEAR
# give a bill dat in format YYYY-MM-DD, return quarter (1-4)
def qtr(d):
m = int(d[5:7])
@@ -9,27 +8,60 @@ def qtr(d):
# 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
def add_missing_annual_bill_in_yr( bill_type, bill_info, num, yr ):
# print( f"{bill_type}: Seems we are missing an annual bill in {yr}, use first_bill={bill_info[bill_type]['first_bill']['bill_date']} to add one" )
# 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:]
l_amt = bill_info[bill_type]['last_bill']['amount']
# print( f"{bill_type}: Should fake a bill into date={yr}-{mm_dd} of adjusted amount from base of {l_amt}" )
amt = bill_info[bill_type]['last_bill']['amount']
# okay the missing bill is before the first bill...
for i in range( bill_info[bill_type]['last_bill_year'], yr ):
l_amt += l_amt * 5.26/100
print( f"{bill_type}: So should insert bill as: ${l_amt:.02f} on '{yr}-{mm_dd}'")
amt += amt * bill_info[bill_type]['growth']/100
# last param is estimated (and this is an estimate for a future bill / not real)
new_bill( bill_type, amt, f'{yr}-{mm_dd}', 1 )
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, num, yr ):
# print( f"{bill_type}: Seems we are missing a quarterly bill in {yr}, use first_bill={bill_info[bill_type]['first_bill']['bill_date']} to add one" )
def add_missing_quarter_bills_in_yr( bill_type, bill_info, yr ):
print( f"*** add_missing_quarter_bills_in_yr( {bill_type}, bill_info, {yr} ): NOT YET" )
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
def add_missing_monthly_bills_in_yr( bill_type, bill_info, num, yr ):
# print( f"{bill_type}: Seems we are missing a monthly bill in {yr}, use first_bill={bill_info[bill_type]['first_bill']['bill_date']} to add one" )
# 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 ):
# 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]
# choose last bill from last amount to grow from as its most relevant
amt = bill_info[bill_type]['last_bill']['amount']
growth=0
#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}'
if yr in bill_info[bill_type]['year']:
for b in bill_info[bill_type]['year'][yr]:
# this bill exists, skip adding it (this occurs when called to
# add bills as there are < 12 bills in first_year, BUT, we
# don't fill before first_bill so the < 12 ALWAYS triggers
if str(b['bill_date']) == new_date:
bill_found=True
break
if not bill_found:
amt += amt * growth/100
# last param is estimated (and this is an estimate for a future bill / not real)
new_bill( bill_type, amt, new_date, 1 )
return
@@ -40,6 +72,7 @@ def add_missing_monthly_bills_in_yr( bill_type, bill_info, num, yr ):
def process_bill_data(bd, bt, bf):
# 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}
# 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}
@@ -54,20 +87,26 @@ def process_bill_data(bd, bt, bf):
# new bill type
if not bill_type in bill_info:
bill_info[bill_type]={}
bill_info[bill_type]['growth'] = bt_id_ann_growth_avg[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])
bill_info[bill_type]['year']={}
bill_info[bill_type]['year_real']={}
if not yr in bill_info[bill_type]['year']:
bill_info[bill_type]['year'][yr]=[]
bill_info[bill_type]['year_real'][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])
# add this bill to list for this year
bill_info[bill_type]['year'][yr].append(bill)
if not bill['estimated']:
bill_info[bill_type]['year_real'][yr].append(bill)
# now process the bill_info from yr of first bill to yr of last bill
for bill_type in bill_info:
@@ -82,40 +121,39 @@ def process_bill_data(bd, bt, bf):
# go from first_bill year until reach end year
for yr in range( yr_min, END_YEAR+1 ):
if yr in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr]) == num:
# print(f"{bill_type}: need {num} annual bills and found then for {yr}" )
# we have all the bills needed for yr
if yr in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr]) == bill_info[bill_type]['num_ann_bills']:
continue
# if yr not in bill_info[bill_type]['year']:
# print(f"{bill_type}: need {num} annual bills and 0 found for {yr}" )
# else:
# print(f"{bill_type}: need {num} annual bills and only {len(bill_info[bill_type]['year'][yr])} found for {yr}" )
add_missing_bills_for_yr( bill_type, bill_info, num, yr )
# now should have missing bills, calculate ann growth properly
derive_ann_growth( bill_type, bill_info, num )
add_missing_bills_for_yr( bill_type, bill_info, yr )
derive_ann_growth( bill_type, bill_info )
################################################################################
# add_missing_bills_for_yr -- wrapper to call right func based on bill freq
################################################################################
def add_missing_bills_for_yr( bill_type, bill_info, num, yr ):
def add_missing_bills_for_yr( bill_type, bill_info, yr ):
print(f"{bill_type}: 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, num, yr )
add_missing_annual_bill_in_yr( bill_type, bill_info, yr )
elif num == 4:
add_missing_quarter_bills_in_yr( bill_type, bill_info, num, yr )
add_missing_quarter_bills_in_yr( bill_type, bill_info, yr )
elif num == 12:
add_missing_monthly_bills_in_yr( bill_type, bill_info, num, yr )
add_missing_monthly_bills_in_yr( bill_type, bill_info, yr )
return
def derive_ann_growth( bill_type, bill_info, num ):
print(f"Derive annual growth on bill_type: {bill_type} " )
# DDP: rewrite loop below to use bill_info more cleverly, start with type, then year, then use the data in there rather than in bd
def derive_ann_growth( bill_type, bill_info ):
print(f"{bill_type}: Derive annual growth on bill_type: {bill_type} " )
total={}
for yr in range( bill_info[bill_type]['first_bill_year'], bill_info[bill_type]['last_bill_year']+1):
if len(bill_info[bill_type]['year_real'][yr]) != bill_info[bill_type]['num_ann_bills']:
continue
total[yr] = 0
for b in bill_info[bill_type]['year'][yr]:
# ignore estimated bills, only use real bills to calc growth stats
if b['estimated']:
continue
total[yr] += b['amount']
# print( f"{yr} => {b['bill_date']} -- {b['amount']}" )
# print( f"total for {bill_type} in {yr} is {total[yr]}" )
# once we have all yr totals:
growth = {}
@@ -124,12 +162,14 @@ def derive_ann_growth( bill_type, bill_info, num ):
max_growth = 0
count = 0
for yr in range( bill_info[bill_type]['first_bill_year'], bill_info[bill_type]['last_bill_year']+1):
if yr-1 in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr-1]) != num:
# print(f"less than {num} bills in yr: {yr-1}, so can't use data" )
# less than {num_ann_bills} bills in yr: {yr-1}, so can't use data
if yr-1 in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr-1]) != bill_info[bill_type]['num_ann_bills']:
continue
if yr in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr]) != num:
# print(f"less than {num} bills in yr: {yr-1}, so can't use data" )
# less than {num_ann_bills} bills in yr: {yr-1}, so can't use data
if yr in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr]) != bill_info[bill_type]['num_ann_bills']:
continue
# we full data sets for consecutive years, work out annual growth stats
if yr-1 in total and yr in total:
growth = (total[yr] - total[yr-1]) / total[yr-1] * 100
avg_growth += growth
@@ -138,12 +178,11 @@ def derive_ann_growth( bill_type, bill_info, num ):
min_growth = growth
if growth > max_growth:
max_growth = growth
# print( f"growth from {yr} to {yr-1} = {growth}%")
if count:
print( f"Min growth was: {min_growth}" )
print( f"Avg growth is: {avg_growth/count}" )
print( f"Max growth was: {max_growth}" )
set_bill_type_growth( bill_type, avg_growth/count )
print( f"{bill_type}: Min growth was: {min_growth}" )
print( f"{bill_type}: Avg growth is: {avg_growth/count}" )
print( f"{bill_type}: Max growth was: {max_growth}" )
set_bill_type_growth( bill_type, min_growth, avg_growth/count, max_growth )
else:
# failsafe (just in case fill bills failed to add enough bills to average out)
print( f"Unable to calculate growth!" )
print( f"{bill_type}: Unable to calculate growth!" )

19
db.py
View File

@@ -88,7 +88,9 @@ def init_db():
id INTEGER PRIMARY KEY AUTOINCREMENT,
freq INTEGER,
name STRING,
ann_growth REAL,
ann_growth_min REAL,
ann_growth_avg REAL,
ann_growth_max REAL,
FOREIGN KEY(freq) REFERENCES bill_freq(id)
)''')
@@ -103,6 +105,7 @@ def init_db():
bill_type INTEGER,
amount INTEGER,
bill_date DATE,
estimated INTEGER,
FOREIGN KEY(bill_type) REFERENCES bill_type(id)
)''')
@@ -260,7 +263,9 @@ def get_comp_set_options(finance):
def get_bill_data():
conn = connect_db(True)
cur = conn.cursor()
cur.execute('SELECT bd.id, bt.id as bill_type_id, bt.name, bd.amount, bd.bill_date FROM bill_type bt, bill_data bd where bt.id = bd.bill_type order by bt.name, bd.bill_date desc')
cur.execute('''SELECT bd.id, bt.id as bill_type_id, bt.name, bd.amount, bd.bill_date, bd.estimated
FROM bill_type bt, bill_data bd
where bt.id = bd.bill_type order by bt.name, bd.bill_date desc''')
bd = cur.fetchall()
conn.close()
return bd
@@ -282,10 +287,10 @@ def get_bill_freqs():
return bf
def new_bill( name, amount, bill_date ):
def new_bill( bill_type, amount, bill_date, estimated ):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"insert into bill_data ( 'bill_type', 'amount', 'bill_date' ) values ( '{name}', '{amount}', '{bill_date}' )" )
cur.execute( f"insert into bill_data ( 'bill_type', 'amount', 'bill_date', 'estimated' ) values ( '{bill_type}', '{amount}', '{bill_date}', {estimated} )" )
conn.commit()
conn.close()
return
@@ -302,7 +307,7 @@ def insert_bill_type( bt, fq ):
conn = connect_db(False)
cur = conn.cursor()
print( f"fq={fq}" )
cur.execute( f"insert into bill_type ( 'name', 'freq', 'ann_growth' ) values ( '{bt}', {fq}, 0 )" )
cur.execute( f"insert into bill_type ( 'name', 'freq', 'ann_growth_min', 'ann_growth_avg', 'ann_growth_max' ) values ( '{bt}', {fq}, 0, 0, 0 )" )
conn.commit()
conn.close()
return
@@ -331,10 +336,10 @@ def delete_bill_type( id ):
conn.close()
return
def set_bill_type_growth( id, g ):
def set_bill_type_growth( id, min_g, avg_g, max_g ):
conn = connect_db(False)
cur = conn.cursor()
cur.execute( f"update bill_type set ann_growth ='{g}' where id = {id}" )
cur.execute( f"update bill_type set ann_growth_min='{min_g}', ann_growth_avg ='{avg_g}', ann_growth_max='{max_g}' where id = {id}" )
conn.commit()
conn.close()
return

View File

@@ -145,6 +145,7 @@ def DisplayBillData():
bill_types = get_bill_types()
bill_freqs = get_bill_freqs()
process_bill_data(bill_data, bill_types, bill_freqs)
bill_data = get_bill_data()
return render_template('bills.html', bill_data=bill_data, bill_types=bill_types, bill_freqs=bill_freqs )
@app.route('/newbilltype', methods=['POST'])
@@ -162,7 +163,8 @@ def UpdateBillType():
@app.route('/newbill', methods=['POST'])
def InsertBill():
data = request.get_json()
new_bill( data['name'], data['amount'], data['bill_date'] )
# last param is estimated - e.g. anything via GUI is not an estimate, but is a real bill
new_bill( data['name'], data['amount'], data['bill_date'], 0 )
return "200"
@app.route('/updatebill', methods=['POST'])

View File

@@ -23,7 +23,7 @@
<div class="mt-4 col-6">
<div class="row align-items-center">
<button id="new-bill-type-button" class="mb-3 px-0 offset-2 col-2 btn btn-success" onCLick="StartNewBillType()"><span class="bi bi-plus-lg"> New Bill Type</span></button>
<button id="new-bill-type-button" class="mb-3 px-0 offset-4 col-2 btn btn-success" onCLick="StartNewBillType()"><span class="bi bi-plus-lg"> New Bill Type</span></button>
<div class="new-bill-type-class px-0 col-2 d-none"> <input type="text" class="form-control text-end float-end border border-primary" id="new-bill-type-name"></div>
<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 %}
@@ -35,10 +35,10 @@
<button id="canc-bill-type" class="new-bill-type-class px-0 col-1 btn btn-danger d-none" onClick="CancelNewBillType()"><span class="bi bi-trash3"> Cancel</span></button>
</div>
<div class="row">
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Name</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Frequency</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Annual Growth Est</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Actions</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0 h-100"><br>Name</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0 h-100"><br>Frequency</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0">Annual Growth Est (min/avg/max)</ ></div>
<div class="px-0 col-2"><label class="form-control text-center border-0 fw-bold bg-light rounded-0 h-100"><br>Actions</ ></div>
</div>
{% for bt in bill_types %}
<div class="row">
@@ -52,7 +52,9 @@
</select>
</div>
<script>console.log( 'set freq to {{bt.freq}}' ); $('#bill-type-freq-{{bt.id}}').val( {{bt.freq}} );</script>
<div class="px-0 col-2"><input type="text" class="form-control text-center" id="bill-type-grow-{{bt.id}}" value="{{'%.2f'|format(bt.ann_growth)}}%" disabled> </div>
<div class="px-0 col-2"><input type="text" class="form-control text-center" id="bill-type-grow-{{bt.id}}"
value="{{'%.2f'|format(bt.ann_growth_min)}} / {{'%.2f'|format(bt.ann_growth_avg)}} / {{'%.2f'|format(bt.ann_growth_max)}}%"
disabled> </div>
<button id="bill-type-chg-{{bt.id}}" class="px-0 col-1 btn btn-success" onClick="StartUpdateBillType( {{bt.id}} )">Change</button>
<button id="bill-type-del-{{bt.id}}" class="px-0 col-1 btn btn-danger" onClick="DelBillType({{bt.id}})"><span class="bi bi-trash3"> Delete</button>
<button id="bill-type-save-{{bt.id}}" class="px-0 col-1 btn btn-success d-none" onClick="UpdateBillType( {{bt.id}} )">Save</button>
@@ -87,6 +89,10 @@
</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">
@@ -103,16 +109,24 @@
</div>
{% endif %}
{% if bd.bill_type_id == bt.id %}
{% if bd.estimated == 1 %}
<div class="row est d-none fst-italic">
{% set classes="fst-italic form-control text-center bg-white" %}
{% else %}
<div class="row">
<div class="px-0 col-2"> <input type="text" class="form-control text-center bg-white" id="bill-data-type-{{bd.id}}" value="{{ bd.name }}" disabled> </div>
<div class="px-0 col-2"> <input type="date" class="form-control text-center bg-white" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
<div class="px-0 col-2"> <input type="number" class="form-control text-center bg-white" id="bill-data-amount-{{bd.id}}" value="{{ bd.amount }}" disabled> </div>
{% set classes="form-control text-center bg-white" %}
{% endif %}
<div class="px-0 col-2"> <input type="text" class="{{classes}}" id="bill-data-type-{{bd.id}}" value="{{ bd.name }}" disabled> </div>
<div class="px-0 col-2"> <input type="date" class="{{classes}}" id="bill-data-date-{{bd.id}}" value="{{ bd.bill_date }}" disabled> </div>
<div class="px-0 col-2"> <input type="number" class="{{classes}}" id="bill-data-amount-{{bd.id}}" value="{{ bd.amount }}" disabled> </div>
{% if bd.estimated == 0 %}
<button id="bill-data-chg-{{bd.id}}" class="px-0 col-1 btn btn-success" onClick="StartUpdateBill( {{bd.id}} )">Change</button>
<button id="bill-data-del-{{bd.id}}" class="px-0 col-1 btn btn-danger" onClick="DeleteBill( {{bd.id }} )"><span class="bi bi-trash3"> Delete
<button id="bill-data-save-{{bd.id}}" class="px-0 col-1 btn btn-success d-none" onClick="UpdateBill( {{bd.id}} )">Save</button>
<button id="bill-data-canc-{{bd.id}}" class="px-0 col-1 btn btn-danger d-none"
onClick="CancelUpdateBill({{bd.id}}, '{{bd.name}}', '{{bd.bill_date}}', '{{bd.amount}}')"> <span class="bi bi-trash3"> Cancel</button>
</button>
{% endif %}
</div>
{% endif %}
{% endfor %}
@@ -121,6 +135,14 @@
</div>
<script>
function ToggleEstimated()
{
if( $("#showEstimated").is(":checked") )
$('.est').removeClass('d-none')
else
$('.est').addClass('d-none')
}
function StartNewBillData()
{
$('.new-bill-data-class').removeClass('d-none')