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 @@
+
+
+
+
+
+
+
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),
]