diff --git a/blocnote/apps/__init__.py b/blocnote/apps/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/blocnote/apps/financialInputs/migrations/0004_growthrate.py b/blocnote/apps/financialInputs/migrations/0004_growthrate.py new file mode 100644 index 0000000000000000000000000000000000000000..4caeb6d5a53caf29429b4b743264759ada157119 --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0004_growthrate.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-05-04 22:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0003_auto_20170503_2200'), + ] + + operations = [ + migrations.CreateModel( + name='GrowthRate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('building_id', models.IntegerField()), + ('growth_rate', models.DecimalField(decimal_places=2, max_digits=6)), + ], + ), + ] diff --git a/blocnote/apps/financialInputs/models.py b/blocnote/apps/financialInputs/models.py index 36dbbdbcdc21db561ba4ce0b6c8fafa72c48259e..78a76d8ec1ef2429e355952186c05bbb3aebdc91 100644 --- a/blocnote/apps/financialInputs/models.py +++ b/blocnote/apps/financialInputs/models.py @@ -140,3 +140,10 @@ class LoanOptions(models.Model): interest_rate = models.DecimalField(max_digits=5, decimal_places=3) duration = models.DecimalField(max_digits=3, decimal_places=0) max_loan_amount = models.DecimalField(max_digits=10, decimal_places=2) + + +class GrowthRate(models.Model): + """Store growth rate for a building.""" + + building_id = models.IntegerField() + growth_rate = models.DecimalField(max_digits=6, decimal_places=2) diff --git a/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js b/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js index 964c36ca97349be716c6401c88382d2af9011d19..2b2d615b3870f68ad10ad915e9b3170329c73a01 100644 --- a/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js +++ b/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js @@ -20,6 +20,7 @@ getLoanOptionsTable(); * This message is displayed when the mouse is over the fund select box. */ var fund = document.querySelector('#id_fund'); +var didFundChange = false; fund.onmouseenter = function() { var errorDiv = document.querySelector('#show-error'); errorDiv.innerHTML = `Changing fund will affect loan options`; @@ -30,6 +31,10 @@ fund.onmouseleave = function() { errorDiv.innerHTML = ""; } +fund.onchange = function() { + didFundChange = true; +} + /** * Handle submition of the header form. Validate commissioning date is greater * than construction start date. Create result dictionary containing the form @@ -78,17 +83,20 @@ function billProjectionDatesForm(form) { * This ensures that the loan options table is up to date with the latest fund. Clear the loan options table * and reload with new fund's default loan options. */ - request(`loan-options/?loans=delete-loan-options`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json' - }, - }).then(res => { - var tableBody = document.querySelector('#loan-options-table tbody'); - tableBody.innerHTML = ``; - getLoanOptionsTable(); - }); + if (didFundChange) { + didFundChange = false; + request(`loan-options/?loans=delete-loan-options`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json' + }, + }).then(res => { + var tableBody = document.querySelector('#loan-options-table tbody'); + tableBody.innerHTML = ``; + getLoanOptionsTable(); + }); + } }); } return false; @@ -1164,7 +1172,7 @@ function addLoanOptionsRow(lenderList, lender, interestRate, duration, maxLoanAm cell = row.insertCell(1); cell.innerHTML = `${lenderOptions(lenderList, lender)}`; cell = row.insertCell(2); - cell.innerHTML = `%`; + cell.innerHTML = `%`; cell = row.insertCell(3); cell.innerHTML = `months`; cell = row.insertCell(4); diff --git a/blocnote/apps/financialInputs/views.py b/blocnote/apps/financialInputs/views.py index 905c7b25475ece6d45f4f31deebb47c602928b1d..e7102f5059edf4fabca70189cce7377ca51fc9e9 100644 --- a/blocnote/apps/financialInputs/views.py +++ b/blocnote/apps/financialInputs/views.py @@ -9,7 +9,7 @@ from bpfin.utilbills.bill_backend_call import bill_prior_proj_rough_annual from bpfin.financials.financial_lib import organize_bill_overview from bpfin.financials.financial_lib import Income_Statement_Table -from .models import Fund, FinancingOverview, Bills, BillsOverview, CustomerPreference, EstimationAlgorithm, Liabilities, CashBalance, IncomeStatement, LoanOptions, DefaultLoan, Lender +from .models import Fund, FinancingOverview, Bills, BillsOverview, CustomerPreference, EstimationAlgorithm, Liabilities, CashBalance, IncomeStatement, LoanOptions, DefaultLoan, Lender, GrowthRate from .forms import BlocNoteHeaderForm @@ -855,6 +855,11 @@ class IncomeStatementTable(View): # Convert the response body to the format bpfin accepts. raw_income_statement_input, growth_rate = self.convert_response_format(put) + GrowthRate.objects.filter(building_id=building_id).delete() + GrowthRate.objects.create( + building_id=building_id, + growth_rate=float("{0:.2f}".format(growth_rate)), + ) # Get bills overview data from the database. bill_overview = self.get_bills_overview_data(building_id) @@ -865,7 +870,6 @@ class IncomeStatementTable(View): '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) diff --git a/blocnote/apps/preliminaryFinance/__init__.py b/blocnote/apps/preliminaryFinance/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/blocnote/apps/preliminaryFinance/admin.py b/blocnote/apps/preliminaryFinance/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/blocnote/apps/preliminaryFinance/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/blocnote/apps/preliminaryFinance/apps.py b/blocnote/apps/preliminaryFinance/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..bfb73be3b22931ee8044eefaac58393bedc69bf6 --- /dev/null +++ b/blocnote/apps/preliminaryFinance/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PreliminaryfinanceConfig(AppConfig): + name = 'preliminaryFinance' diff --git a/blocnote/apps/preliminaryFinance/migrations/0001_initial.py b/blocnote/apps/preliminaryFinance/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..54013bc670fd4d942f217de72a2734e81def7f10 --- /dev/null +++ b/blocnote/apps/preliminaryFinance/migrations/0001_initial.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-05-04 21:53 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CostEstimation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('item', models.CharField(max_length=150)), + ('cost', models.DecimalField(decimal_places=2, max_digits=10)), + ], + ), + migrations.CreateModel( + name='SavingsEstimation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('utility_type', models.CharField(max_length=11)), + ('estimated_savings', models.DecimalField(decimal_places=4, max_digits=4)), + ('used_before_retrofit', models.BooleanField()), + ('used_after_retrofit', models.BooleanField()), + ], + ), + migrations.CreateModel( + name='Scenario', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('building_id', models.IntegerField()), + ('name', models.CharField(max_length=150)), + ('analysis_level', models.CharField(max_length=11)), + ('estimated_cost', models.DecimalField(decimal_places=2, max_digits=10)), + ('overall_savings', models.DecimalField(decimal_places=2, max_digits=10)), + ('first_year_savings', models.DecimalField(decimal_places=2, max_digits=10)), + ('simple_payback', models.DecimalField(decimal_places=2, max_digits=4)), + ('self_finance_amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('min_savings_dscr', models.DecimalField(decimal_places=2, max_digits=5)), + ('min_net_operating_income_dscr', models.DecimalField(decimal_places=2, max_digits=5)), + ('min_cash_dscr', models.DecimalField(decimal_places=2, max_digits=5)), + ], + ), + migrations.AddField( + model_name='savingsestimation', + name='scenario', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='preliminaryFinance.Scenario'), + ), + migrations.AddField( + model_name='costestimation', + name='scenario', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='preliminaryFinance.Scenario'), + ), + ] diff --git a/blocnote/apps/preliminaryFinance/migrations/__init__.py b/blocnote/apps/preliminaryFinance/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/blocnote/apps/preliminaryFinance/models.py b/blocnote/apps/preliminaryFinance/models.py new file mode 100644 index 0000000000000000000000000000000000000000..96b63361c662ea3cf1a5c952cbd9cb3bfb0f8223 --- /dev/null +++ b/blocnote/apps/preliminaryFinance/models.py @@ -0,0 +1,42 @@ +from django.db import models + + +class Scenario(models.Model): + """Store scenario for preliminary finance analysis. + + Model to store scenario information for a particular building. All financial analysis will be performed based + on what is obtained from financial-inputs and following data. + """ + + building_id = models.IntegerField() + name = models.CharField(max_length=150) + analysis_level = models.CharField(max_length=11) + estimated_cost = models.DecimalField(max_digits=10, decimal_places=2) + overall_savings = models.DecimalField(max_digits=10, decimal_places=2) + first_year_savings = models.DecimalField(max_digits=10, decimal_places=2) + simple_payback = models.DecimalField(max_digits=4, decimal_places=2) + self_finance_amount = models.DecimalField(max_digits=10, decimal_places=2) + min_savings_dscr = models.DecimalField(max_digits=5, decimal_places=2) + min_net_operating_income_dscr = models.DecimalField(max_digits=5, decimal_places=2) + min_cash_dscr = models.DecimalField(max_digits=5, decimal_places=2) + + +class CostEstimation(models.Model): + """Store an item and it's cost. + + Model to store the items and their costs. This data will be used to estimate total cost in a particular scenario. + """ + + scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE, blank=True, null=True) + item = models.CharField(max_length=150) + cost = models.DecimalField(max_digits=10, decimal_places=2) + + +class SavingsEstimation(models.Model): + """Store the utility savings estimation.""" + + scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE, blank=True, null=True) + utility_type = models.CharField(max_length=11) + estimated_savings = models.DecimalField(max_digits=4, decimal_places=4) + used_before_retrofit = models.BooleanField() + used_after_retrofit = models.BooleanField() diff --git a/blocnote/apps/preliminaryFinance/static/preliminaryFinance/scripts/app.js b/blocnote/apps/preliminaryFinance/static/preliminaryFinance/scripts/app.js new file mode 100644 index 0000000000000000000000000000000000000000..3028a456d95ea2414a775720889756fabc1143ce --- /dev/null +++ b/blocnote/apps/preliminaryFinance/static/preliminaryFinance/scripts/app.js @@ -0,0 +1,305 @@ +var tabCounter = 1; +costEstimationTableHead(); +costEstimationTableBody(); +savingEstimationTableHead(); +savingEstimationTableBody(); + +/** + * Return the Scenario name. + */ +function getScenarioName() { + var docs = document.querySelector("a.nav-link.active input"); + return docs.value; +} + +/** + * Add column headings to the Cost Estimation table. + */ +function costEstimationTableHead() { + head = document.querySelector('#cost-estimation-table thead'); + head.innerHTML = ` + + Item + Estimated Cost (in $) + Option + + `; +} + +/** + * Load the Cost Estimation table body. + */ +function costEstimationTableBody() { + body = document.querySelector('#cost-estimation-table tbody'); + var rowCount = body.rows.length; + var row = body.insertRow(rowCount); + var cell = row.insertCell(0); + cell.innerHTML = ``; + cell = row.insertCell(1); + cell.innerHTML = `$`; + cell = row.insertCell(2); + cell.innerHTML = ``; + return false; +} + +/** + * Delete a row in Cost Estimation table. + */ +function deleteCostEstimationRow(rowIndex) { + table = document.querySelector('#cost-estimation-table tbody'); + body.deleteRow(rowIndex-1); + return false; +} + +/** + * Display column headings in Savings Estimation Table. + */ +function savingEstimationTableHead() { + head = document.querySelector('#saving-estimation-table thead'); + head.innerHTML = ` + + Utility + Estimated Savings (in %) + Used Before Retrofit + Used After Retrofit + + `; +} + +/** + * Load Savings Estimation Table body. + */ +function savingEstimationTableBody() { + body = document.querySelector('#saving-estimation-table tbody'); + var rowCount = body.rows.length; + var row = body.insertRow(rowCount); + var cell = row.insertCell(0); + cell.innerHTML = `Electricity`; + cell = row.insertCell(1); + cell.innerHTML = ` + % + `; + cell = row.insertCell(2); + cell.innerHTML = ` + + `; + cell = row.insertCell(3); + cell.innerHTML = ` + + `; + + rowCount += 1; + row = body.insertRow(rowCount); + cell = row.insertCell(0); + cell.innerHTML = `Gas`; + cell = row.insertCell(1); + cell.innerHTML = ` + % + `; + cell = row.insertCell(2); + cell.innerHTML = ` + + `; + cell = row.insertCell(3); + cell.innerHTML = ` + + `; + + rowCount += 1; + row = body.insertRow(rowCount); + cell = row.insertCell(0); + cell.innerHTML = `Oil`; + cell = row.insertCell(1); + cell.innerHTML = ` + % + `; + cell = row.insertCell(2); + cell.innerHTML = ` + + `; + cell = row.insertCell(3); + cell.innerHTML = ` + + `; + + rowCount += 1; + row = body.insertRow(rowCount); + cell = row.insertCell(0); + cell.innerHTML = `Water`; + cell = row.insertCell(1); + cell.innerHTML = ` + % + `; + cell = row.insertCell(2); + cell.innerHTML = ` + + `; + cell = row.insertCell(3); + cell.innerHTML = ` + + `; + return false; +} + +/** + * Submit the Cost and Saving Estimation. Send information to the backend and this triggers the preliminary analysis. + * Upon getting response, display Project Economics table and Energy Expense Savings Projection graph. + */ +function submitScenario() { + var scenarioName = getScenarioName(); + var savingEstimationBody = document.querySelector('#saving-estimation-table tbody'); + var rowCount = savingEstimationBody.rows.length; + + // Package savings estimate into a list of dictionaries. + savings = []; + for (var rowIndex=0; rowIndex < rowCount; rowIndex++){ + beforeRetrofit = false; + afterRetrofit = false; + record = {}; + checkBoxes = savingEstimationBody.rows[rowIndex].querySelectorAll('input[type="checkbox"]:checked'); + for (index = 0; index < checkBoxes.length; index++) { + checkbox = checkBoxes[index]; + if (checkbox.name === 'Used before retrofit') { + beforeRetrofit = true; + } + if (checkbox.name === 'Used after retrofit') { + afterRetrofit = true; + } + } + if (Number(savingEstimationBody.rows[rowIndex].cells[1].children[0].value) > 100) { + alert(`Please enter a valid savings percentage for ${savingEstimationBody.rows[rowIndex].cells[0].innerHTML}`); + return false; + } + + record = { + 'utility_type': savingEstimationBody.rows[rowIndex].cells[0].innerHTML, + 'savings_estimate': savingEstimationBody.rows[rowIndex].cells[1].children[0].value, + 'used_before_retrofit': beforeRetrofit, + 'used_after_retrofit': afterRetrofit, + } + savings.push(record); + } + + var costEstimationBody = document.querySelector('#cost-estimation-table tbody'); + rowCount = costEstimationBody.rows.length; + const ITEM_INDEX = 0; + const COST_INDEX = 1; + + // Package cost estimation into a list of dictionaries. + var cost = []; + for (rowIndex = 0; rowIndex < rowCount; rowIndex++) { + record = { + 'item': costEstimationBody.rows[rowIndex].cells[ITEM_INDEX].children[0].value, + 'cost': costEstimationBody.rows[rowIndex].cells[COST_INDEX].children[0].value, + }; + cost.push(record); + } + result = { + 'scenario': scenarioName, + 'savings': savings, + 'cost': cost, + } + request(`scenario/`, { + method: 'PUT', + credentials: 'same-origin', + body: JSON.stringify(result), + headers: new Headers({ + 'Content-Type': 'application/json', + 'X-CSRFToken': Cookies.get('csrftoken') + }) + }).then(res => { + loadProjectEconomicsTable(res) + loadGraph(res); + }); + return false; +} + +/** + * Take the response as argument and display the project economics table. This is the summary of the priliminary + * analysis that gives vital information about the financial health of the building. + */ +function loadProjectEconomicsTable(res) { + var doc = document.querySelector('#project-economics'); + var title = document.querySelector('#project-economics-title'); + title.innerHTML = `Project Economics` + var economicsOverview = res.payload.economics_overview; + var body = document.querySelector('#project-economics-table tbody'); + addPERow(body, economicsOverview); +} + +/** + * Add rows to the project economics table. + */ +function addPERow(body, economicsOverview) { + body.innerHTML = ``; + for (var key in economicsOverview){ + rowCount = body.rows.length; + row = body.insertRow(rowCount); + cell = row.insertCell(0); + cell.innerHTML = `${key}`; + cell = row.insertCell(1); + cell.innerHTML = `${economicsOverview[key]}`; + } +} + +/** + * Load the Energy Expense Savings Projection stacked bar graph. + */ +function loadGraph(res) { + var graph = document.querySelector('#myChart'); + graph.innerHTML = ``; + var config = { + type: 'bar', + data: { + labels: res.payload.year_list, + datasets: [{ + type: 'bar', + label: 'Energy Expense', + backgroundColor: "#66B2FF", + data: res.payload.energy_expense_list, + }, { + type: 'bar', + label: 'Total Loan', + backgroundColor: "#E0E0E0", + data: res.payload.total_loan_list, + }, { + type: 'bar', + label: 'Net Savings', + backgroundColor: "#99FF99", + data: res.payload.net_savings_list, + }] + }, + options: { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } + } + }; + var doc = document.querySelector('#savings-schedule'); + var title = document.querySelector('#savings-schedule-title'); + title.innerHTML = 'Energy Expense Savings Projection'; + var ctx = document.getElementById("myChart").getContext("2d"); + new Chart(ctx, config); +} + diff --git a/blocnote/apps/preliminaryFinance/templates/preliminaryFinance/index.html b/blocnote/apps/preliminaryFinance/templates/preliminaryFinance/index.html new file mode 100644 index 0000000000000000000000000000000000000000..787a01a7909d6254c7958776c872a1e9801472f4 --- /dev/null +++ b/blocnote/apps/preliminaryFinance/templates/preliminaryFinance/index.html @@ -0,0 +1,25 @@ +{% extends 'base.html' %} +{% load staticfiles %} +{% block content %} +
+
+

+ Preliminary Finance +

+
+ {{ building.address }} +
+
+
+
+ {% include "preliminaryFinance/scenario.html" %} +
+{% endblock %} +{% block scripts %} + + + + + +{% endblock %} + diff --git a/blocnote/apps/preliminaryFinance/templates/preliminaryFinance/scenario.html b/blocnote/apps/preliminaryFinance/templates/preliminaryFinance/scenario.html new file mode 100644 index 0000000000000000000000000000000000000000..06e429c5c87b2f5e6af42dc1e3474bc48e00b714 --- /dev/null +++ b/blocnote/apps/preliminaryFinance/templates/preliminaryFinance/scenario.html @@ -0,0 +1,61 @@ +
+
+
+ +
+
+ +
+
+
+
+
+

+ Cost Estimation +

+ + + + + +
+ +
+
+

+ Saving Estimation +

+ + + + + +
+
+
+ +
+
+
+
+
+

+

+ + + + + +
+
+
+

+

+ +
diff --git a/blocnote/apps/preliminaryFinance/tests.py b/blocnote/apps/preliminaryFinance/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/blocnote/apps/preliminaryFinance/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/blocnote/apps/preliminaryFinance/urls.py b/blocnote/apps/preliminaryFinance/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..6bd796e289919f27e4211cbe8b568708c2bdfa9f --- /dev/null +++ b/blocnote/apps/preliminaryFinance/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^$', views.Index.as_view(), name='index'), + url(r'^scenario/$', views.Scenarios.as_view(), name='scenario'), +] diff --git a/blocnote/apps/preliminaryFinance/views.py b/blocnote/apps/preliminaryFinance/views.py new file mode 100644 index 0000000000000000000000000000000000000000..78560ec24186b2d442d73e684e73291042649632 --- /dev/null +++ b/blocnote/apps/preliminaryFinance/views.py @@ -0,0 +1,436 @@ +import json + +from django.shortcuts import render +from django.views import View +from django.http import JsonResponse +from django.db import connections +import datetime + +from bpfin.financials.financial_lib import organize_bill_overview +from bpfin.financials.borrower_schedule import packaging_data +from bpfin.financials.financial_lib import Income_Statement_Table +from bpfin.financials.cash_balance import cash_balance +from bpfin.financials.liability import final_liability_dict +from bpfin.financials.financial_lib import Balance_Sheet_Table +from bpfin.financials.scenario import Scenario as ScenarioClass + +from .models import Scenario, CostEstimation, SavingsEstimation +from blocnote.apps.financialInputs.models import GrowthRate, IncomeStatement, BillsOverview, FinancingOverview, CashBalance, Liabilities, LoanOptions, CustomerPreference, Bills +from bpfin.utilbills.bill_backend_call import prior_proj_rough_month + + +class Index(View): + """Render the view when you hit preliminary-finance endpoint.""" + + def get_building_data(self, building_id): + """Return building data given it's id. + + Connect to the building database and retrieves the building data. + + Args: + building_id: id of the building_id. + + Returns: + building: A dictionary containing all the relevant building data. + """ + cursor = connections['building_db'].cursor() + command = 'SELECT * FROM public.get_building(in_building_id := {})' + cursor.execute(command.format(building_id)) + columns = [col[0] for col in cursor.description] + row = cursor.fetchone() + building = dict(zip(columns, row)) + return building + + def get(self, request, building_id): + """Implement HTTP GET request. + + Fetch building data from the building database. Pass this information to the frontend. + + Args: + request: HTTP GET request. + building_id: id of the building. + + Returns: + render: Renders index.html page with building data. + """ + building = self.get_building_data(building_id) + context = {'building': building} + return render(request, 'preliminaryFinance/index.html', context=context) + + +class Scenarios(View): + """Perform preliminary analysis for a scenario. + + Get Cost and Savings estimates from the frontend. Fetch all relevant data from the database. Make function calls + to bpfin to calculate all the outputs to be displayed and pass the information to the frontend. + """ + + def get_liability(self, building_id, analysis_date): + """Obtain liability information. + + Fetch the records containing the liability information from the database. + + Args: + building_id: id of the building. + analysis_date: Dictionary containing: + proforma_start with value Start of projection. + proforma_duration with value Total duration of projection. + + Returns: + liability_dictionary: Dictionary containing: + keys: + debt_id- id of the debt. + value- dictionary containing: + keys: + 1. lender with value as lender for the liability/mortgage. + 2. date with value when the mortgage/liabiity statment was + requested. + 3. remaining_term with value the number of months remaining + in the mortgage/liability. + 4. liability with value as monthly service for the mortgage. + """ + liability_objs = Liabilities.objects.filter(building_id=building_id) + liabilities_input = {} + index = 1 + for obj in liability_objs: + key = 'debt'+str(index) + index += 1 + liability = float(obj.monthly_service) + lender = obj.lender + remaining_term = int(obj.remaining_term) + input_date = obj.input_date + liabilities_input[key] = (liability, lender, remaining_term, input_date) + liability_dictionary = final_liability_dict( + analysis_date['proforma_start'], + liabilities_input, + analysis_date['proforma_duration'] + ) + return liability_dictionary + + def get_cash_balance(self, building_id): + """Fetch Cash Balance data from Database. + + Args: + building_id: id of the building. + + Returns: + cash_balance: Dictionary that contains: + keys: + statement_date- date of the statement(Cash balance or bank statement). + value- Tuple containing balance amount and a boolean saying if it is + from balance sheet or not. + """ + cash_balance_objs = CashBalance.objects.filter(building_id=building_id) + cash_balance_input = {} + for obj in cash_balance_objs: + cash_balance_input[obj.statement_date] = (float(obj.balance_amount), obj.is_from_balance_sheet) + return cash_balance_input + + def get_analysis_date(self, building_id): + """Fetch analysis date for the building. + + Args: + building_id: id of the building. + + Returns: + analysis_date: Dictionary: + keys: + 1. proforma_start- Start date for projection. + 2. proforma_duration- Duration of the projection. + """ + financing_overview_obj = FinancingOverview.objects.get(building_id=building_id) + analysis_date = { + 'proforma_start': financing_overview_obj.pro_forma_start_date, + 'proforma_duration': int(financing_overview_obj.pro_forma_duration), + } + return analysis_date + + def get_bill_overview(self, building_id): + """Fetch bill overview data from database. + + Args: + building_id: id of the building_id. + + Returns: + bill_overview: Dictionary containing utility as key and value as a dictionary with key as year and value + as annual charge for that utility. + """ + 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 get_raw_income_statement(self, building_id): + """Fetch income statement data from database. + + Args: + building_id: id of the building. + + Returns: + raw_income_statement: Dictionary with key as year and value a dictionary with revenue, utility expense and + non-utility expense data. + """ + income_statement_objs = IncomeStatement.objects.filter(building_id=building_id) + raw_income_statment = {} + for obj in income_statement_objs: + record = {} + record['revenue'] = float(obj.revenue) + record['utility_expense'] = float(obj.utility_expense) + record['non_utility_expense'] = float(obj.non_utility_operating_expense) + raw_income_statment[int(obj.year)] = record + return raw_income_statment + + def get_growth_rate(self, building_id): + """Fetch growth rate from the database. + + Args: + building_id: id of the building. + + Returns: + growth_rate: Growth rate stored for this building. + """ + growth_rate_obj = GrowthRate.objects.filter(building_id=building_id) + return float(growth_rate_obj[0].growth_rate) + + def get_loan_input(self, building_id): + """Fetch loan options from database. + + Args: + building_id: id of the building. + + Returns: + loan_options: List of records. Each record is a dicitonary containing instititue, max loan amount, rate of + interest and duration for loan. + """ + loan_options_objs = LoanOptions.objects.filter(building_id=building_id) + loan_options = [] + for obj in loan_options_objs: + record = { + 'institute': obj.lender.name, + 'max_amount': float(obj.max_loan_amount), + 'interest': float(obj.interest_rate)/100, + 'duration': int(obj.duration), + } + loan_options.append(record) + return loan_options + + def get_commission_date(self, building_id): + """Fetch the commissioning date. + + Args: + building_id: id of the building. + + Returns: + Anticipated commissioning date. + """ + return FinancingOverview.objects.filter( + building_id=building_id + )[0].anticipated_commissioning_date + + def get_savings_percent(self, savings): + """Format savings to be in decimal value for each utility. + + Args: + savings: List of records. Each record has a utility type and savings as keys. + + Returns: + savings_percent: Dictionary with key as utility_type and value as savings as a decimal. + """ + savings_percent = {} + for record in savings: + savings_percent[str.lower(record['utility_type'])] = float(record['savings_estimate'])/100 + return savings_percent + + def get_customer_preference(self, building_id): + """Fetch customer preference data from database. + + Args: + building_id: id of the building. + + Returns: + customer_preference: Dictionary with downpayment, expected payback and customer savings DCSR. + """ + customer_preference_obj = CustomerPreference.objects.filter(building_id=building_id)[0] + customer_preference = { + 'downpayment_max': float(customer_preference_obj.downpayment), + 'expected_payback': int(customer_preference_obj.expected_payback), + 'cust_saving_dscr': float(customer_preference_obj.expected_net_noi_dscr) + } + return customer_preference + + def get_prior_month_bill(self, building_id, analysis_date): + """Get the prior bills on a monthly basis than annual. + + Args: + building_id: id of the building. + analysis_date: Dictionary with proforma_start and proforma_duration. + + Returns: + prior_month_bill: Dictionary with utility type as key value as monthly charge. + """ + utilities = [ + 'electricity', + 'gas', + 'oil', + 'water', + ] + prior_month_bill = {} + for utility_type in utilities: + bills_object = Bills.objects.filter( + building_id=building_id, + utility_type=utility_type, + ) + + if bills_object: + raw_bill = {} + raw_bill['utility_type'] = utility_type + raw_bill['date_from'] = [] + raw_bill['date_to'] = [] + raw_bill['charge'] = [] + raw_bill['usage'] = [] + + for bill in bills_object: + raw_bill['date_from'].append(bill.date_from) + raw_bill['date_to'].append(bill.date_to) + raw_bill['usage'].append(float(bill.usage)) + raw_bill['charge'].append(float(bill.charge)) + month_rough = prior_proj_rough_month(raw_bill, analysis_date) + prior_month_bill[utility_type] = month_rough + return prior_month_bill + + def put(self, request, building_id): + """Handle HTTP PUT request. + + Args: + request: HTTP PUT request. + building_id: id of the building. + + Returns: Dictionary with data to display the graph. + """ + put = json.loads(request.body.decode()) + + # Fetch all relevant data from database. + growth_rate = self.get_growth_rate(building_id) + raw_income_statement = self.get_raw_income_statement(building_id) + analysis_date = self.get_analysis_date(building_id) + bill_overview = self.get_bill_overview(building_id) + bill_overview_organized = organize_bill_overview(bill_overview, analysis_date) + cash_balance_input_dict = self.get_cash_balance(building_id) + cash_balance_input = cash_balance(analysis_date, cash_balance_input_dict) + + prior_income_statement_table = Income_Statement_Table( + raw_income_statement, + bill_overview_organized + ) + prior_income_statement_table.project( + growth_rate, + analysis_date, + bill_overview_organized + ) + + liability_dictionary = self.get_liability(building_id, analysis_date) + noi_dictionary = prior_income_statement_table.get_noi_dict() + + raw_balance_sheet = { + 'cash': cash_balance_input, + 'other_debt_service': liability_dictionary, + 'net_income': noi_dictionary, + } + prior_balance_sheet_table = Balance_Sheet_Table(raw_balance_sheet) + prior_balance_sheet_table.project_balance_sheet( + analysis_date, + liability_dictionary, + noi_dictionary, + ) + costs = [] + cost_list = put['cost'] + for record in cost_list: + costs.append(float(record['cost'])) + total_cost = sum(costs) + loan_options = self.get_loan_input(building_id) + commission_date = self.get_commission_date(building_id) + savings_percent = self.get_savings_percent(put['savings']) + customer_preference = self.get_customer_preference(building_id) + prior_month_bill = self.get_prior_month_bill(building_id, analysis_date) + full_saving_dict = { + 'electricity': None, + 'gas': None, + 'oil': None, + 'water': None, + } + req_dscr = { + 'req_noi_dscr': 1.15, + 'req_cash_dscr': 1.15, + 'req_saving_dscr': 1.10 + } + # The scenario class would store relevant data and perform preliminary analysis. + scenario = ScenarioClass( + analysis_date, + commission_date, + total_cost, + bill_overview, + bill_overview_organized, + liability_dictionary, + prior_income_statement_table, + prior_balance_sheet_table, + loan_options, + ) + + # Perform preliminary analysis + scenario.prelim_anlaysis( + prior_month_bill, + savings_percent, + full_saving_dict, + growth_rate, + req_dscr, + customer_preference + ) + + # graph_dict contains the data for the stacked bar graph. + graph_dict = scenario.get_graph_dict() + energy_expense = graph_dict['energy_expenses'] + total_loan = graph_dict['total_loan'] + net_savings = graph_dict['net_savings'] + energy_expense_list = [] + total_loan_list = [] + net_savings_list = [] + year_list = [] + for year in sorted(energy_expense): + year_list.append(year) + energy_expense_list.append(energy_expense[year]) + total_loan_list.append(total_loan[year]) + net_savings_list.append(net_savings[year]) + + # economics_overview contains the information for the summary table on frontend. + project_economics_overview = scenario.get_economics() + economics_overview = { + 'Estimated Cost': float("{0:.2f}".format(project_economics_overview['estimated_cost'])), + 'Overall Saving': float("{0:.2f}".format(project_economics_overview['overall_saving'])), + 'First Year Saving': float("{0:.2f}".format(project_economics_overview['first_year_saving'])), + 'Simple Payback': float("{0:.2f}".format(project_economics_overview['simple_payback'])), + 'Minimum Saving DSCR': float("{0:.2f}".format(project_economics_overview['min_saving_dscr'])), + 'Minimum NOI DSCR': float("{0:.2f}".format(project_economics_overview['min_noi_dscr'])), + 'Minimum Cash DSCR': float("{0:.2f}".format(project_economics_overview['min_cash_dscr'])), + } + return JsonResponse({ + 'year_list': year_list, + 'energy_expense_list': energy_expense_list, + 'total_loan_list': total_loan_list, + 'net_savings_list': net_savings_list, + 'economics_overview': economics_overview, + }) diff --git a/blocnote/settings.py b/blocnote/settings.py index cce5531dba956a3ebabe10f17bcec30d3f5725c5..ad0f38ba6eb035d735a16a396cb342291af75f20 100644 --- a/blocnote/settings.py +++ b/blocnote/settings.py @@ -42,6 +42,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'blocnote.apps.financialInputs', + 'blocnote.apps.preliminaryFinance', ] MIDDLEWARE = [ diff --git a/blocnote/urls.py b/blocnote/urls.py index 2cefee10ee421cd9f7c9696bed9cda720ec0d2ce..d8c2845780794a6ded7b8564f44e2e24b6bba7d2 100644 --- a/blocnote/urls.py +++ b/blocnote/urls.py @@ -18,6 +18,7 @@ from django.contrib import admin urlpatterns = [ url('', admin.site.urls), + url(r'^buildings/(?P[0-9]+)/preliminary-finance/', include('blocnote.apps.preliminaryFinance.urls')), url(r'^buildings/(?P[0-9]+)/financial-inputs/', include('blocnote.apps.financialInputs.urls')), url(r'^admin/', admin.site.urls), ]