diff --git a/blocnote/apps/financialInputs/migrations/0002_incomestatement.py b/blocnote/apps/financialInputs/migrations/0002_incomestatement.py new file mode 100644 index 0000000000000000000000000000000000000000..0f9ce6adc55a65412696af020621529d7db27f9f --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0002_incomestatement.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-27 20:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='IncomeStatement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('building_id', models.IntegerField()), + ('year', models.DecimalField(decimal_places=0, max_digits=4)), + ('revenue', models.DecimalField(decimal_places=2, max_digits=10)), + ('utility_expense', models.DecimalField(decimal_places=2, max_digits=10)), + ('other_utility_expense', models.DecimalField(decimal_places=2, max_digits=10)), + ('non_utility_operating_expense', models.DecimalField(decimal_places=2, max_digits=10)), + ], + ), + ] diff --git a/blocnote/apps/financialInputs/models.py b/blocnote/apps/financialInputs/models.py index 16d26d4ee2250e637770260e315026548ee10067..030d735693c38f2d256f93b362629ac929368886 100644 --- a/blocnote/apps/financialInputs/models.py +++ b/blocnote/apps/financialInputs/models.py @@ -93,3 +93,18 @@ class CashBalance(models.Model): statement_date = models.DateField() is_from_balance_sheet = models.BooleanField(default=False) balance_amount = models.DecimalField(max_digits=10, decimal_places=2) + + +class IncomeStatement(models.Model): + """Store income statement data. + + Store revenue, utility expense, utility expenses other than electricity, gas, oil and water and non-utility + expenses for a particular building for a particular year. + """ + + building_id = models.IntegerField() + year = models.DecimalField(max_digits=4, decimal_places=0) + revenue = models.DecimalField(max_digits=10, decimal_places=2) + utility_expense = models.DecimalField(max_digits=10, decimal_places=2) + other_utility_expense = models.DecimalField(max_digits=10, decimal_places=2) + non_utility_operating_expense = models.DecimalField(max_digits=10, decimal_places=2) diff --git a/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js b/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js index 7b871edaed4b178d398a50641e1af31cff3ed838..83a9488399e1b7f15a5e93c3e1af4590e2e51349 100644 --- a/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js +++ b/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js @@ -3,6 +3,7 @@ for (var utility_index in utilities) { loadInitialBillsTable(utilities[utility_index]); } loadBillsOverview(); +getIncomeStatementTable(); getCustomerPreferenceTable(); getLiabilitiesTable(); getCashBalanceForm(); @@ -748,3 +749,304 @@ function createDateComponent(mon, day, yr, num) { return text; } + +/** + * Load income statement table. The column heading are displayed first along with the table body. The growth rate + * drop down box is displayed as well. + */ +function loadIncomeStatementTable() { + loadIncomeStatemenHeading(); + loadIncomeStatemenBody(); + loadIncomeStatementDropdownBox(''); +} + +/** + * Display column heading of the income statement table. + */ +function loadIncomeStatemenHeading() { + var heading = document.querySelector('#income-statement-table thead'); + heading.innerHTML = ` + + Year + + + + Next Year + Average + + `; +} + +/** + * Display body of the income statement table. + */ +function loadIncomeStatemenBody() { + var body = document.querySelector('#income-statement-table tbody'); + body.innerHTML = ` + + Revenue + + + + + + + + Utility Expense + + + + + + + + Energy Expense + + + + + + + + Electricity Bill + + + + + + + + Gas Bill + + + + + + + + Oil Bill + + + + + + + + Water Bill + + + + + + + + Other Utility Expense + + + + + + + + Non-Utility Operating Expense + + + + + + + + Net Non-Energy Expense + + + + + + + + Total Expense + + + + + + + + Net Operating Expense + + + + + + + `; +} + +/** + * Display the growth rate drop down box. This takes in a CAGR value if present. The CAGR value will be passed only + * when the calculate button is hit. + */ +function loadIncomeStatementDropdownBox(cagr) { + dropdownBox = document.querySelector('#income-statement-dropdown-box'); + var text = ``; + text += ` + Please select Growth Rate + + `; + dropdownBox.innerHTML = text; +} + +/** + * HTTP GET request to backend. If income statement records already existing, display relevant fields in the table. + * Display empty table otherwise. + */ +function getIncomeStatementTable() { + request(`income-statement/`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json' + }, + }).then(res => { + loadIncomeStatementTable(); + if(res.payload.instance.present) { + recordList = res.payload.instance.result; + heading = document.querySelector('#income-statement-table-head'); + body = document.querySelector('#income-statement-table-body'); + const REVENUE_ROW = 0; + const UTILITY_EXPENSE_ROW = 1; + const NON_UTILITY_OPERATING_EXPENSE_ROW = 8; + for (var index in recordList) { + cellIndex = Number(index)+1; + heading.rows[0].cells[cellIndex].innerHTML = ` + + `; + var revenue = recordList[index].revenue; + var utilityExpense = recordList[index].utility_expense; + var nonUtilityExpense = recordList[index].non_utility_operating_expense; + var revenueInput = ` + + `; + var utilityExpenseInput = ` + + `; + var nonUtilityExpenseInput = ` + + `; + updateIncomeStatementTable(REVENUE_ROW, cellIndex, revenueInput); + updateIncomeStatementTable(UTILITY_EXPENSE_ROW, cellIndex, utilityExpenseInput); + updateIncomeStatementTable(NON_UTILITY_OPERATING_EXPENSE_ROW, cellIndex, nonUtilityExpenseInput); + } + } + }); +} + +/** + * Fire up when the calculate button is hit. Get all the input data from the table and the growth rate value + * and make HTTP PUT request to the backend. Update the table with the calculated data received from the backend. + */ +function onCalculateIncomeStatement(data) { + var head = document.querySelector('#income-statement-table-head'); + var body = document.querySelector('#income-statement-table-body'); + var form = document.querySelector('#income-statement-form'); + var formData = new FormData(form); + var growthRate = formData.get('Growth Rate'); + var result = []; + for (var cellIndex = 1; cellIndex <= 3; cellIndex++) { + var record = {}; + record['year'] = head.rows[0].cells[cellIndex].children[0].value; + record['revenue'] = body.rows[0].cells[cellIndex].children[0].value; + record['utility-expense'] = body.rows[1].cells[cellIndex].children[0].value; + record['non-utility-operating-expense'] = body.rows[8].cells[cellIndex].children[0].value; + result.push(record); + } + result.push({'growth-rate': growthRate}); + request('income-statement/', { + method: 'PUT', + credentials: 'same-origin', + body: JSON.stringify(result), + headers: new Headers({ + 'Content-Type': 'application/json', + 'X-CSRFToken': Cookies.get('csrftoken') + }) + }).then(res => { + const REVENUE_ROW = 0; + const UTILITY_EXPENSE_ROW = 1; + const ENERGY_EXPENSE_ROW = 2; + const ELECTRICITY_ROW = 3; + const GAS_ROW = 4; + const OIL_ROW = 5; + const WATER_ROW = 6; + const OTHER_UTILITY_EXPENSE_ROW = 7; + const NON_UTILITY_OPERATING_EXPENSE_ROW = 8; + const NET_NON_ENERGY_EXPENSE_ROW = 9; + const TOTAL_EXPENSE_ROW = 10; + const NET_OPERATING_EXPENSE = 11; + var incomeStatementFull = res.payload.instance; + for (key in incomeStatementFull) { + cellIndex = Number(key)+1; + updateIncomeStatementTable(ENERGY_EXPENSE_ROW, cellIndex, incomeStatementFull[key].energy_opex); + updateIncomeStatementTable(ELECTRICITY_ROW, cellIndex, incomeStatementFull[key].electricity_opex); + updateIncomeStatementTable(GAS_ROW, cellIndex, incomeStatementFull[key].gas_opex); + updateIncomeStatementTable(OIL_ROW, cellIndex, incomeStatementFull[key].oil_opex); + updateIncomeStatementTable(WATER_ROW, cellIndex, incomeStatementFull[key].water_opex); + updateIncomeStatementTable(OTHER_UTILITY_EXPENSE_ROW, cellIndex, incomeStatementFull[key].other_utility); + updateIncomeStatementTable(NET_NON_ENERGY_EXPENSE_ROW, cellIndex, incomeStatementFull[key].net_non_energy_opex); + updateIncomeStatementTable(TOTAL_EXPENSE_ROW, cellIndex, incomeStatementFull[key].total_opex); + updateIncomeStatementTable(NET_OPERATING_EXPENSE, cellIndex, incomeStatementFull[key].noi); + } + var future = res.payload.future; + const FUTURE_INDEX = 4; + heading = document.querySelector('#income-statement-table thead'); + heading.rows[0].cells[FUTURE_INDEX].innerHTML = future.year; + updateIncomeStatementTable(REVENUE_ROW,FUTURE_INDEX,future.revenue); + updateIncomeStatementTable(UTILITY_EXPENSE_ROW,FUTURE_INDEX,future.utility_expense); + updateIncomeStatementTable(ENERGY_EXPENSE_ROW,FUTURE_INDEX,future.energy_opex); + updateIncomeStatementTable(ELECTRICITY_ROW,FUTURE_INDEX,future.electricity_opex); + updateIncomeStatementTable(GAS_ROW,FUTURE_INDEX,future.gas_opex); + updateIncomeStatementTable(OIL_ROW,FUTURE_INDEX,future.oil_opex); + updateIncomeStatementTable(WATER_ROW,FUTURE_INDEX,future.water_opex); + updateIncomeStatementTable(OTHER_UTILITY_EXPENSE_ROW,FUTURE_INDEX,future.other_utility); + updateIncomeStatementTable(NON_UTILITY_OPERATING_EXPENSE_ROW,FUTURE_INDEX,future.non_utility_expense); + updateIncomeStatementTable(NET_NON_ENERGY_EXPENSE_ROW,FUTURE_INDEX,future.net_non_energy_opex); + updateIncomeStatementTable(TOTAL_EXPENSE_ROW,FUTURE_INDEX,future.total_opex); + updateIncomeStatementTable(NET_OPERATING_EXPENSE,FUTURE_INDEX,future.noi); + + var averageDict = res.payload.average; + const AVERAGE_DICT_INDEX = 5; + updateIncomeStatementTable(REVENUE_ROW,AVERAGE_DICT_INDEX,averageDict.revenue); + updateIncomeStatementTable(UTILITY_EXPENSE_ROW,AVERAGE_DICT_INDEX,averageDict.utility_expense); + updateIncomeStatementTable(ENERGY_EXPENSE_ROW,AVERAGE_DICT_INDEX,averageDict.energy_opex); + updateIncomeStatementTable(ELECTRICITY_ROW,AVERAGE_DICT_INDEX,averageDict.electricity_opex); + updateIncomeStatementTable(GAS_ROW,AVERAGE_DICT_INDEX,averageDict.gas_opex); + updateIncomeStatementTable(OIL_ROW,AVERAGE_DICT_INDEX,averageDict.oil_opex); + updateIncomeStatementTable(WATER_ROW,AVERAGE_DICT_INDEX,averageDict.water_opex); + updateIncomeStatementTable(OTHER_UTILITY_EXPENSE_ROW,AVERAGE_DICT_INDEX,averageDict.other_utility); + updateIncomeStatementTable(NON_UTILITY_OPERATING_EXPENSE_ROW,AVERAGE_DICT_INDEX,averageDict.non_utility_expense); + updateIncomeStatementTable(NET_NON_ENERGY_EXPENSE_ROW,AVERAGE_DICT_INDEX,averageDict.net_non_energy_opex); + updateIncomeStatementTable(TOTAL_EXPENSE_ROW,AVERAGE_DICT_INDEX,averageDict.total_opex); + updateIncomeStatementTable(NET_OPERATING_EXPENSE,AVERAGE_DICT_INDEX,averageDict.noi); + + var box = document.querySelector('#growth-rate option'); + box.innerHTML = `CAGR ${res.payload.CAGR}`; + }) + return false; +} + +/** + * Update the income statement table. This takes in the rowIndex, cellIndex and value to be inserted at that cell. + */ +function updateIncomeStatementTable(rowIndex, cellIndex, value) { + var body = document.querySelector('#income-statement-table tbody'); + body.rows[rowIndex].cells[cellIndex].innerHTML = value; +} diff --git a/blocnote/apps/financialInputs/templates/financialInputs/incomeStatement.html b/blocnote/apps/financialInputs/templates/financialInputs/incomeStatement.html new file mode 100644 index 0000000000000000000000000000000000000000..f1ce56c4d34af8a8def9b171f847819e387f9bdf --- /dev/null +++ b/blocnote/apps/financialInputs/templates/financialInputs/incomeStatement.html @@ -0,0 +1,22 @@ +
+

Income Statements

+
+
+
+
+
+
+ +
+
+
+
+ + + + + +
+
+
+
diff --git a/blocnote/apps/financialInputs/templates/financialInputs/index.html b/blocnote/apps/financialInputs/templates/financialInputs/index.html index 25174a020214e199257dd30b9e56911c5bade034..1cd3039f5a1f30b7552c28f584dce1dd8877ee8f 100644 --- a/blocnote/apps/financialInputs/templates/financialInputs/index.html +++ b/blocnote/apps/financialInputs/templates/financialInputs/index.html @@ -26,6 +26,9 @@ {% include "financialInputs/billsOverview.html" %} +
+ {% include "financialInputs/incomeStatement.html" %} +
{% include "financialInputs/cashBalance.html" %} diff --git a/blocnote/apps/financialInputs/urls.py b/blocnote/apps/financialInputs/urls.py index 0a0786b2d955a1ac33643c16746046b69f58b7ba..8856aed8d065ed1e90ccf7034c71e7d213bb24e7 100644 --- a/blocnote/apps/financialInputs/urls.py +++ b/blocnote/apps/financialInputs/urls.py @@ -10,4 +10,5 @@ urlpatterns = [ url(r'^customer-preference/$', views.CustomerPreferenceView.as_view(), name='customer_preference'), url(r'^liabilities/$', views.LiabilitiesTable.as_view(), name='liabilities'), url(r'^cash-balance/$', views.CashBalanceView.as_view(), name='cash_balance'), + url(r'^income-statement/$', views.IncomeStatementTable.as_view(), name='income_statement'), ] diff --git a/blocnote/apps/financialInputs/views.py b/blocnote/apps/financialInputs/views.py index ea50b691b20d80b64c65d24955c75edb6fec2956..67fc7bf1fba3dc67012e7d05e35dc77b566abf10 100644 --- a/blocnote/apps/financialInputs/views.py +++ b/blocnote/apps/financialInputs/views.py @@ -6,9 +6,10 @@ from django.views import View from datetime import date from bpfin.utilbills.bill_backend_call import bill_prior_proj_rough_annual -from bpfin.financials.cash_balance import cash_balance +from bpfin.financials.financial_lib import organize_bill_overview +from bpfin.financials.financial_lib import Income_Statement_Table -from .models import FinancingOverview, Bills, BillsOverview, CustomerPreference, EstimationAlgorithm, Liabilities, CashBalance +from .models import FinancingOverview, Bills, BillsOverview, CustomerPreference, EstimationAlgorithm, Liabilities, CashBalance, IncomeStatement from .forms import BlocNoteHeaderForm @@ -745,3 +746,204 @@ class CashBalanceView(View): ) return JsonResponse({'instance': 'OK'}) + +class IncomeStatementTable(View): + """Income Statement table. + + Store income statement data. Calculate historical income statement information and one year in the future. + Calculate the average of the historical year and the one future year and display in a new column. + """ + + model = IncomeStatement + + def convert_response_format(self, put): + """Convert PUT body format. + + Convert the data in HTTP PUT request into format accepted by bpfin. Obtain the growth rate as well. + + Args: + put: HTTP PUT request from frontend. + + Returns: + result: Dictionary with year as key and income statement information dictionary as value. + growth rate: The selected growth rate to project the future income statement. + """ + result = {} + for record in put: + if 'growth-rate' not in record: + value = {} + value['revenue'] = float(record['revenue']) + value['utility_expense'] = float(record['utility-expense']) + value['non_utility_expense'] = float(record['non-utility-operating-expense']) + result[int(record['year'])] = value + else: + growth_rate = float(record['growth-rate']) + return result, growth_rate + + def get_bills_overview_data(self, building_id): + """Get Bills overview table data. + + Query the database to get bills overview data. + + Args: + building_id: id of the building. + + Returns: + bill_overview: A dictionary with utility as key and value a list. This list contains the first element + a dictionary with key the year and value the charge for that particular year(actual or + projected). The second element is the flag which is true if the utility bill does exist. + """ + objs = BillsOverview.objects.filter(building_id=building_id) + bill_overview = {} + electricity = {} + gas = {} + oil = {} + water = {} + if objs: + for obj in objs: + electricity[obj.year] = float(obj.electricity) + gas[obj.year] = float(obj.gas) + oil[obj.year] = float(obj.oil) + water[obj.year] = float(obj.water) + bill_overview = { + 'electricity': [electricity, not obj.electricity_is_user_input], + 'gas': [gas, not obj.gas_is_user_input], + 'oil': [oil, not obj.oil_is_user_input], + 'water': [water, not obj.water_is_user_input], + } + return bill_overview + + def save(self, result, building_id): + """Save income statement data in database. + + Args: + result: List of historical income statement data to be stored in the database. + building_id: id of the building. + """ + self.model.objects.filter(building_id=building_id).delete() + for record in result: + self.model.objects.create( + building_id=building_id, + year=record['year'], + revenue=record['revenue'], + utility_expense=record['utility_expense'], + other_utility_expense=record['other_utility'], + non_utility_operating_expense=record['non_utility_expense'] + ) + + def put(self, request, building_id): + """Handle HTTP PUT request. + + Receive income statement data from frontend. Convert the format to use in bpfin function. Store result from + bpfin logic to the database. Send the historical remaining calculated income statement data, future one year + data and average back to the frontend. + + Args: + request: HTTP PUT request with the income statement inputs in the body. + building_id: id of the building. + + Returns: + JsonResponse: Dictionary with key-value as instance with value the historical results, future with future + results and average with average results. + """ + put = json.loads(request.body.decode()) + + # Convert the response body to the format bpfin accepts. + raw_income_statement_input, growth_rate = self.convert_response_format(put) + + # Get bills overview data from the database. + bill_overview = self.get_bills_overview_data(building_id) + + # Get the analysis date from the financing overview model. + financing_overview_obj = FinancingOverview.objects.get(building_id=building_id) + analysis_date = { + 'proforma_start': financing_overview_obj.pro_forma_start_date, + 'proforma_duration': financing_overview_obj.pro_forma_duration, + } + + # organize_bill_overview takes in the bills overview and analysis date and fills in average values in places + # where there are no values. + bill_overview_organized = organize_bill_overview(bill_overview, analysis_date) + + # income statement table is an object that takes in the income statement input and bill_overview_organized. + income_statement_table = Income_Statement_Table(raw_income_statement_input, bill_overview_organized) + + # income_statement_full is the list of records with historical income statement data. + income_statement_full = income_statement_table.get_hist_table() + + # cagr is compound annual growth rate. + cagr = income_statement_table.get_cagr() + + # project function calculates the income statement data for the next few years based on the growth rate. + income_statement_table.project(growth_rate, analysis_date, bill_overview_organized) + + # result stores the historical income statement data as a list. + result = [income_statement_full[year] for year in income_statement_full] + + # result_dict has the income statement data for the next year. + result_dict = income_statement_table.get_single_year(income_statement_table.hist_end_year + 1) + + # average_dict has the income statement data for the 3 historical years and the next year projected. + average_dict = income_statement_table.get_average() + + # save function stores the data in the database. + self.save(result, building_id) + + # Change the float formatting to have only 2 decimal places. + for record in result: + record['energy_opex'] = float("{0:.2f}".format(record['energy_opex'])) + record['other_utility'] = float("{0:.2f}".format(record['other_utility'])) + record['net_non_energy_opex'] = float("{0:.2f}".format(record['net_non_energy_opex'])) + result_dict['energy_opex'] = float("{0:.2f}".format(result_dict['energy_opex'])) + result_dict['other_utility'] = float("{0:.2f}".format(result_dict['other_utility'])) + result_dict['net_non_energy_opex'] = float("{0:.2f}".format(result_dict['net_non_energy_opex'])) + result_dict['revenue'] = float("{0:.2f}".format(result_dict['revenue'])) + result_dict['utility_expense'] = float("{0:.2f}".format(result_dict['utility_expense'])) + result_dict['non_utility_expense'] = float("{0:.2f}".format(result_dict['non_utility_expense'])) + result_dict['total_opex'] = float("{0:.2f}".format(result_dict['total_opex'])) + result_dict['noi'] = float("{0:.2f}".format(result_dict['noi'])) + + average_dict['electricity_opex'] = float("{0:.2f}".format(average_dict['electricity_opex'])) + average_dict['gas_opex'] = float("{0:.2f}".format(average_dict['gas_opex'])) + average_dict['oil_opex'] = float("{0:.2f}".format(average_dict['oil_opex'])) + average_dict['water_opex'] = float("{0:.2f}".format(average_dict['water_opex'])) + average_dict['energy_opex'] = float("{0:.2f}".format(average_dict['energy_opex'])) + average_dict['other_utility'] = float("{0:.2f}".format(average_dict['other_utility'])) + average_dict['net_non_energy_opex'] = float("{0:.2f}".format(average_dict['net_non_energy_opex'])) + average_dict['revenue'] = float("{0:.2f}".format(average_dict['revenue'])) + average_dict['utility_expense'] = float("{0:.2f}".format(average_dict['utility_expense'])) + average_dict['non_utility_expense'] = float("{0:.2f}".format(average_dict['non_utility_expense'])) + average_dict['total_opex'] = float("{0:.2f}".format(average_dict['total_opex'])) + average_dict['noi'] = float("{0:.2f}".format(average_dict['noi'])) + + cagr = float("{0:.2f}".format(cagr)) + return JsonResponse({'instance': result, 'future': result_dict, 'average': average_dict, 'CAGR': cagr}) + + def get(self, request, building_id): + """Handle HTTP GET request. + + Fetch stored income statement data from the database and send to frontend to display. + + Args: + request: HTTP GET request. + building_id: id of the building. + + Returns: + JsonResponse: Instance which contains a list of records. Each record is a dictionary with income + statement information from the database. + """ + objs = self.model.objects.filter(building_id=building_id) + instance = {} + instance['present'] = False + if objs: + instance['present'] = True + result = [] + for obj in objs: + record = {} + record['year'] = obj.year + record['revenue'] = obj.revenue + record['utility_expense'] = obj.utility_expense + record['non_utility_operating_expense'] = obj.non_utility_operating_expense + result.append(record) + instance['result'] = result + return JsonResponse({'instance': instance})