# main.py from flask import Flask, render_template, request, redirect, url_for, Response, jsonify from calc import calculate_savings_depletion, calc_key_dates from db import init_db, get_finance_data, update_finance, get_budget_data, insert_cset, get_comp_set_data, get_comp_set_options, get_bill_freqs from db import get_bill_data, new_bill, update_bill_data, delete_bill, delete_estimated_bills from db import get_bill_ui, save_ui, delete_cset 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, date import csv import io from disp import FP_VAR app = Flask(__name__) # core vars that we will save for comparisons VARS= ( 'D_Salary', 'D_Num_fortnights_pay', 'School_Fees', 'Car_loan_via_pay', 'Car_loan', 'Car_balloon', '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', 'Sell_shares', 'CBA', 'TLS' ) # Initialize the database init_db() @app.route('/') def index(): finance_data = get_finance_data() get_comp_set_options(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: depletion_date=depletion_date.date(); # just show date # if we are comparing...(compare_to will be 0 / None to start with, and then COMP will be None COMP=get_comp_set_data(finance_data['compare_to']) DISP=[] # Row 1 r=[] r.append( FP_VAR( 'D Salary', 'D_Salary' ) ) r.append( FP_VAR( 'Savings', 'Savings' ) ) r.append( FP_VAR( 'Car Loan via Pay', 'Car_loan_via_pay', 'readonly' ) ) r.append( FP_VAR( 'Living Expenses', 'Living_Expenses' ) ) r.append( FP_VAR( 'Overseas Trip', 'Overseas_trip', 'date', 'col-auto', 'Overseas_trip_date' ) ) DISP.append(r) # Row 2 r=[] r.append( FP_VAR( 'D # Pays to quit', 'D_Num_fortnights_pay' ) ) r.append( FP_VAR( 'Interest Rate', 'Interest_Rate' ) ) r.append( FP_VAR( 'Car Loan (monthly)', 'Car_loan', 'readonly' ) ) r.append( FP_VAR( 'Inflation', 'Inflation' ) ) r.append( FP_VAR( 'Reno Costs', 'Mark_reno', 'date', 'col-auto', 'Mark_reno_date' ) ) DISP.append(r) # Row 2 r=[] r.append( FP_VAR( 'D # days leave', 'D_leave_owed_in_days' ) ) r.append( FP_VAR( 'M TLS amount', 'M_TLS_shares' ) ) r.append( FP_VAR( 'Car Balloon', 'Car_balloon', 'readonly' ) ) ss_opt = [ { 'val': 0, 'label': 'Never' } ] for el in range(1,7): ss_opt.append( { 'val': el, 'label': f'{el} years' } ) r.append( FP_VAR( 'Sell Shares for:', 'Sell_shares', 'select', 'col-auto', '', ss_opt ) ) r.append( FP_VAR( 'Car Buyout', 'Car_buyout', 'date', 'col-auto', 'Car_buyout_date' ) ) DISP.append(r) # Row 3 r=[] r.append( FP_VAR( 'D CBA amount', 'D_CBA_shares' ) ) r.append( FP_VAR( 'D TLS amount', 'D_TLS_shares' ) ) r.append( FP_VAR( 'Mich Present', 'Mich_present', 'readonly' ) ) if_opt = [ { 'val': 0, 'label': 'lease' } , {'val': 1, 'label': 'buyout' } ] r.append( FP_VAR( 'Ioniq6 future', 'Ioniq6_future', 'select', 'col-auto', '', if_opt ) ) DISP.append(r) # Row 4 r=[] r.append( FP_VAR( 'CBA price', 'CBA_price' ) ) r.append( FP_VAR( 'TLS price', 'TLS_price' ) ) r.append( FP_VAR( 'School Fees', 'School_Fees', 'readonly' ) ) r.append( FP_VAR( 'FINAL # of CBA', 'CBA', 'readonly' ) ) r.append( FP_VAR( 'FINAL # of TLS', 'TLS', 'readonly' ) ) DISP.append(r) now=datetime.today().strftime('%Y-%m-%d') # Extract years from the date strings years = [datetime.strptime(date, "%Y-%m-%d").year for date, _ in savings_per_fortnight] # Count how many times each year appears year_counts = Counter(years) # Sort the items by year sorted_years = sorted(year_counts.items()) # List of (year, count) tuples # Access the first and second years first_yr, first_count = sorted_years[0] second_yr, second_count = sorted_years[1] # 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 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(): insert_cset( request.get_json() ) return "200" @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'], request.form['School_Fees'], request.form['Car_loan_via_pay'], request.form['Car_loan'], request.form['Car_balloon'], request.form['Car_buyout'], request.form['Living_Expenses'], request.form['Savings'], request.form['Interest_Rate'], request.form['Inflation'], request.form['Mich_present'], request.form['Overseas_trip'], request.form['Mark_reno'], request.form['D_leave_owed_in_days'], request.form['D_TLS_shares'], request.form['M_TLS_shares'], request.form['D_CBA_shares'], request.form['TLS_price'], request.form['CBA_price'], request.form['Overseas_trip_date'], request.form['Mark_reno_date'], request.form['Car_buyout_date'], request.form['Sell_shares'], 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() 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() # 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 ) @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() 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)