diff --git a/.gitignore b/.gitignore index 8310350f1b71b65fdaaf5a8b6d2d92a7ed48bf28..32b1e5a16a36f3d59c0566fb584e5e9f6c6a9f23 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ node_modules # misc .idea +.vscode/ diff --git a/blocnote/apps/financialInputs/forms.py b/blocnote/apps/financialInputs/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..584eda407a4749a7625223279e4fce023af0f1fb --- /dev/null +++ b/blocnote/apps/financialInputs/forms.py @@ -0,0 +1,52 @@ +import datetime + +from django.forms import ModelForm +from django import forms +from django.forms.extras import SelectDateWidget + +from .models import FinancingOverview + + +class BlocNoteHeaderForm(ModelForm): + """Header Form. + + Form that appears at the top of the page. This class specifies the fields + and sets the form field types for dates, sets the range of dates to display + and adds attributes to some of the fields. + """ + + years_to_display = range(datetime.datetime.now().year - 15, + datetime.datetime.now().year + 50) + anticipated_construction_start_date = forms.DateField( + widget=SelectDateWidget(years=years_to_display) + ) + anticipated_commissioning_date = forms.DateField( + widget=SelectDateWidget(years=years_to_display) + ) + pro_forma_start_date = forms.DateField( + widget=SelectDateWidget(years=years_to_display) + ) + analysis_date = forms.DateField( + widget=SelectDateWidget(years=years_to_display) + ) + + class Meta: + model = FinancingOverview + fields = ('anticipated_construction_start_date', + 'anticipated_commissioning_date', + 'anticipated_construction_period', + 'pro_forma_start_date', + 'pro_forma_duration', + 'analysis_date', + 'fund') + + def __init__(self, *args, **kwargs): + super(BlocNoteHeaderForm, self).__init__(*args, **kwargs) + for field in iter(self.fields): + self.fields[field].widget.attrs.update({ + 'class': 'form-control' + }) + self.fields['fund'].widget.attrs.update({ + 'class': 'custom-select' + }) + self.initial['fund'] = 1 diff --git a/blocnote/apps/financialInputs/migrations/0001_initial.py b/blocnote/apps/financialInputs/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..934afed6c8470922d73f813e67723908b2d77ff9 --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-12 15:19 +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='FinancingOverview', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('building_id', models.IntegerField()), + ('required_noi_dscr', models.DecimalField(decimal_places=2, max_digits=5)), + ('requrired_cash_dscr', models.DecimalField(decimal_places=2, max_digits=5)), + ('anticipated_construction_start_date', models.DateField()), + ('anticipated_commissioning_date', models.DateField()), + ('anticipated_construction_period', models.DecimalField(decimal_places=0, max_digits=2)), + ], + ), + migrations.CreateModel( + name='Fund', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('Name', models.CharField(max_length=200)), + ], + ), + migrations.AddField( + model_name='financingoverview', + name='fund', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='financialInputs.Fund'), + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0002_auto_20170412_2043.py b/blocnote/apps/financialInputs/migrations/0002_auto_20170412_2043.py new file mode 100644 index 0000000000000000000000000000000000000000..bb6bee6ae8aae9983dd72ac6ec54d9b55b0d6c5a --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0002_auto_20170412_2043.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-12 20:43 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='financingoverview', + name='fund', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='financialInputs.Fund'), + ), + migrations.AlterField( + model_name='financingoverview', + name='required_noi_dscr', + field=models.DecimalField(decimal_places=2, default=0, max_digits=5), + ), + migrations.AlterField( + model_name='financingoverview', + name='requrired_cash_dscr', + field=models.DecimalField(decimal_places=2, default=0, max_digits=5), + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0003_auto_20170412_2143.py b/blocnote/apps/financialInputs/migrations/0003_auto_20170412_2143.py new file mode 100644 index 0000000000000000000000000000000000000000..29295423a4aae5474ae5b5b171f05fc9cf7ebc29 --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0003_auto_20170412_2143.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-12 21:43 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0002_auto_20170412_2043'), + ] + + operations = [ + migrations.AlterField( + model_name='financingoverview', + name='fund', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='financialInputs.Fund'), + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0004_auto_20170412_2153.py b/blocnote/apps/financialInputs/migrations/0004_auto_20170412_2153.py new file mode 100644 index 0000000000000000000000000000000000000000..ca3454003527e023f27ee4850f5fc0fc35c44237 --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0004_auto_20170412_2153.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-12 21:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0003_auto_20170412_2143'), + ] + + operations = [ + migrations.AlterField( + model_name='financingoverview', + name='anticipated_construction_period', + field=models.DecimalField(decimal_places=0, max_digits=4), + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0005_auto_20170413_1530.py b/blocnote/apps/financialInputs/migrations/0005_auto_20170413_1530.py new file mode 100644 index 0000000000000000000000000000000000000000..3086180e64810a89ca2dddfcf4620f742331c8b9 --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0005_auto_20170413_1530.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-13 15:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0004_auto_20170412_2153'), + ] + + operations = [ + migrations.AddField( + model_name='financingoverview', + name='analysis_date', + field=models.DateField(default='2017-01-01'), + preserve_default=False, + ), + migrations.AddField( + model_name='financingoverview', + name='pro_forma_duration', + field=models.DecimalField(decimal_places=0, default=2, max_digits=2), + preserve_default=False, + ), + migrations.AddField( + model_name='financingoverview', + name='pro_forma_start_date', + field=models.DateField(default='2017-01-01'), + preserve_default=False, + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0006_auto_20170413_1533.py b/blocnote/apps/financialInputs/migrations/0006_auto_20170413_1533.py new file mode 100644 index 0000000000000000000000000000000000000000..a10168597e492fc49dafc85978c7e816be95b0e4 --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0006_auto_20170413_1533.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-13 15:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0005_auto_20170413_1530'), + ] + + operations = [ + migrations.AlterField( + model_name='financingoverview', + name='anticipated_construction_period', + field=models.DecimalField(decimal_places=0, max_digits=3), + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0007_bills.py b/blocnote/apps/financialInputs/migrations/0007_bills.py new file mode 100644 index 0000000000000000000000000000000000000000..cf6002808452df06658696372ffd4dbe8fee51af --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0007_bills.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-17 15:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0006_auto_20170413_1533'), + ] + + operations = [ + migrations.CreateModel( + name='Bills', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('building_id', models.IntegerField()), + ('date_from', models.DateField()), + ('date_to', models.DateField()), + ('utility_type', models.CharField(max_length=11)), + ('usage', models.DecimalField(decimal_places=2, max_digits=6)), + ('charge', models.DecimalField(decimal_places=2, max_digits=10)), + ], + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0008_auto_20170417_2026.py b/blocnote/apps/financialInputs/migrations/0008_auto_20170417_2026.py new file mode 100644 index 0000000000000000000000000000000000000000..3ecb611b8ef94452420c6e61c5a5611fda2608dc --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0008_auto_20170417_2026.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-17 20:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0007_bills'), + ] + + operations = [ + migrations.AlterField( + model_name='bills', + name='usage', + field=models.DecimalField(decimal_places=2, max_digits=10), + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0009_billsoverview.py b/blocnote/apps/financialInputs/migrations/0009_billsoverview.py new file mode 100644 index 0000000000000000000000000000000000000000..6f0b789ce5f777dfe621e5a07a64cabe604531fe --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0009_billsoverview.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-17 23:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0008_auto_20170417_2026'), + ] + + operations = [ + migrations.CreateModel( + name='BillsOverview', + 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)), + ('estimation_algorithm', models.CharField(choices=[('FR', 'Freshman'), ('SO', 'Sophomore'), ('JR', 'Junior'), ('SR', 'Senior')], max_length=100)), + ], + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0010_auto_20170417_2338.py b/blocnote/apps/financialInputs/migrations/0010_auto_20170417_2338.py new file mode 100644 index 0000000000000000000000000000000000000000..5a4c4fd2850f30f2dcd8f59bd9f8ec9a01278fd5 --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0010_auto_20170417_2338.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-17 23:38 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0009_billsoverview'), + ] + + operations = [ + migrations.AlterField( + model_name='billsoverview', + name='estimation_algorithm', + field=models.CharField(max_length=100), + ), + ] diff --git a/blocnote/apps/financialInputs/migrations/0011_auto_20170418_0232.py b/blocnote/apps/financialInputs/migrations/0011_auto_20170418_0232.py new file mode 100644 index 0000000000000000000000000000000000000000..8198ef55d1219d378f4dabb3d10a0da9892d922f --- /dev/null +++ b/blocnote/apps/financialInputs/migrations/0011_auto_20170418_0232.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-18 02:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('financialInputs', '0010_auto_20170417_2338'), + ] + + operations = [ + migrations.AddField( + model_name='billsoverview', + name='electricity', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10), + preserve_default=False, + ), + migrations.AddField( + model_name='billsoverview', + name='electricity_is_user_input', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='billsoverview', + name='gas', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10), + preserve_default=False, + ), + migrations.AddField( + model_name='billsoverview', + name='gas_is_user_input', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='billsoverview', + name='oil', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10), + preserve_default=False, + ), + migrations.AddField( + model_name='billsoverview', + name='oil_is_user_input', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='billsoverview', + name='water', + field=models.DecimalField(decimal_places=2, default=0, max_digits=10), + preserve_default=False, + ), + migrations.AddField( + model_name='billsoverview', + name='water_is_user_input', + field=models.BooleanField(default=False), + ), + ] diff --git a/blocnote/apps/financialInputs/models.py b/blocnote/apps/financialInputs/models.py index 71a836239075aa6e6e4ecb700e9c42c95c022d91..b10647c3055533d0d0f66255ec4a9bd5b48c8e83 100644 --- a/blocnote/apps/financialInputs/models.py +++ b/blocnote/apps/financialInputs/models.py @@ -1,3 +1,58 @@ from django.db import models -# Create your models here. + +class Fund(models.Model): + """Model that represents which fund is the money from.""" + + Name = models.CharField(max_length=200) + + def __str__(self): + """Return fund name in string format.""" + return self.Name + + +class FinancingOverview(models.Model): + """Basic dates and fund. + + Store necessary dates, fund name and relevant time periods. This is used in + Bill Projection in bpfin. + """ + + building_id = models.IntegerField() + fund = models.ForeignKey(Fund, on_delete=models.CASCADE, blank=True, null=True) + required_noi_dscr = models.DecimalField(max_digits=5, decimal_places=2, default=0) + requrired_cash_dscr = models.DecimalField(max_digits=5, decimal_places=2, default=0) + pro_forma_start_date = models.DateField() + pro_forma_duration = models.DecimalField(max_digits=2, decimal_places=0) + analysis_date = models.DateField() + anticipated_construction_start_date = models.DateField() + anticipated_commissioning_date = models.DateField() + anticipated_construction_period = models.DecimalField(max_digits=3, + decimal_places=0) + + +class Bills(models.Model): + """Utility bill information obtained from an external file.""" + + building_id = models.IntegerField() + date_from = models.DateField() + date_to = models.DateField() + utility_type = models.CharField(max_length=11) + usage = models.DecimalField(max_digits=10, decimal_places=2) + charge = models.DecimalField(max_digits=10, decimal_places=2) + + +class BillsOverview(models.Model): + """Store annual charge of each utility used for bill projection.""" + + building_id = models.IntegerField() + year = models.DecimalField(max_digits=4, decimal_places=0) + estimation_algorithm = models.CharField(max_length=100) + electricity = models.DecimalField(max_digits=10, decimal_places=2) + electricity_is_user_input = models.BooleanField(default=False) + water = models.DecimalField(max_digits=10, decimal_places=2) + water_is_user_input = models.BooleanField(default=False) + oil = models.DecimalField(max_digits=10, decimal_places=2) + oil_is_user_input = models.BooleanField(default=False) + gas = models.DecimalField(max_digits=10, decimal_places=2) + gas_is_user_input = models.BooleanField(default=False) diff --git a/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js b/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js new file mode 100644 index 0000000000000000000000000000000000000000..9f076c4b68a695e03d3bd55c485ab0ec93ead38e --- /dev/null +++ b/blocnote/apps/financialInputs/static/financialInputs/scripts/app.js @@ -0,0 +1,274 @@ +utilities = ['electricity', 'gas', 'oil', 'water']; +for (var utility_index in utilities) { + loadInitialBillsTable(utilities[utility_index]); +} +loadBillsOverview(); + +function billProjectionDatesForm(form) { + /* + * Handle submition of the header form. Validate commissioning date is greater + * than construction start date. Create result dictionary containing the form + * data and convert into JSON to send to the backend. Upon success, load the + * Bills overview table as the Pro Forma year changes the start year of the + * Bills overview table. + */ + const formData = new FormData(form); + var validDate = validateDate(formData); + if (!validDate) { + alert("Anticipated Commissioning date has to be after Anticipated Construction start date"); + } + else { + const result = {}; + for (const [key, value] of formData.entries()) { + result[key] = value; + } + request('finance-overview/', { + method: 'put', + credentials: 'same-origin', + body: JSON.stringify(result), + headers: new Headers({ + 'Content-Type': 'application/json', + 'X-CSRFToken': Cookies.get('csrftoken') + }) + }).then(res => { + loadBillsOverview(); + }); + } + return false; +} + +function validateDate(data) { + /* Validate that commissioning date is after the construction start date. */ + var startDateYear = data.get('anticipated_construction_start_date_year'); + var startDateMonth = data.get('anticipated_construction_start_date_month'); + var startDateDay = data.get('anticipated_construction_start_date_day'); + var endDateYear = data.get('anticipated_commissioning_date_year'); + var endDateMonth = data.get('anticipated_commissioning_date_month'); + var endDateDay = data.get('anticipated_commissioning_date_day'); + if (endDateYear < startDateYear) { + return false; + } + else if (endDateYear === startDateYear) { + if (endDateMonth < startDateMonth) { + return false; + } + else if (endDateMonth === startDateMonth) { + if (endDateDay <= startDateDay) { + return false; + } + } + } + return true; +} + +function loadInitialBillsTable(utility) { + /* + * Load initial bills table given a utility. Send GET request to obtain + * bill information and call updateTable function to update the bills table + * with the data obtained from backend. + */ + const table = document.querySelector('#'+utility); + + request(`bills/?utility_type=${utility}`, { + method: 'get', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json' + }, + }).then(res => { + if (res.payload.present) { + var text = getText(res.payload.result, utility); + updateTable(utility, text); + } + }) +} + + +function updateTable(utility, text) { + /* Update the Bills table with the data. Function queries to get the table + * component and then updates the component with the text. + */ + table = document.querySelector('#'+utility); + table.innerHTML = text; +} + +function getText(result, utility) { + /* Generate html text for a given utility and bills data from backend. */ + var text = ` + + + + + + + + + `; + for(var i =0; i`; + for(var j=0; j` + result[i][j] + ``; + } + text += ``; + } + text += ``; + return text; +} + +function uploadFile(data) { + /* Upload an energy bill. Get the id of the component to identify the + * utility type. Use reader to read as text and call the sendFile function + * to send the contents to the backend. + */ + const id = document.getElementsByClassName("show")[0].getAttribute("id"); + csvFile = data.files[0]; + var reader = new FileReader(); + reader.onload = function(e) { + var content = {} + content.text = reader.result; + content.utility_type = id; + sendFile(id, content); + } + reader.readAsText(csvFile); + return false; +} + +function sendFile(id, content) { + /* HTTP PUT request to upload the contents of the utility bills file */ + request(`bills/`, { + method: 'put', + credentials: 'same-origin', + body: JSON.stringify(content), + headers: new Headers({ + 'Content-Type': 'application/json', + 'X-CSRFToken': Cookies.get('csrftoken') + }) + }).then(res => { + var text = getText(res.payload.result, id); + updateTable(id, text); + loadBillsOverview(); + }); +} + +function startOverviewForm() { + /* Create form tag Energy Bills Overview */ + var text = ``; + return text; +} + +function estimationDropDownBox() { + /* Create dropdown box to select estimation algorithm */ + var text = ``; + return text; +} + +function createOverviewColumnHeaders(year) { + /* Create table and column headings */ + var text = `
Date FromDate ToUsage Bill Charge ($)
+ + + + + + + `; + return text; +} + +function createFlag(state) { + /* Display if user input is needed or not */ + if(state) { + text = `Need User Input`; + } + else { + text = `Bill Data Present`; + } + return text; +} + +function createInput(id, state, value) { + /* Create input field. Add attribute readonly if value is grabbed from + * database. If value is not from database, make it a user input. Name + * the input along with utility type to uniquely identify it. + */ + var text = ``; + var is_readonly = ``; + if (!state) { + is_readonly = `readonly`; + } + text = ``; + return text; +} + +function createOverviewRows(data) { + /* Add rows to the energy bills table. Currently adds only one entry in + * each column. Will be modified to display entries for 20 years. + */ + var user_input = ''; + var text = ``; + for (var utility in utilities) { + user_input = utilities[utility] + '_is_user_input'; + text += ` + + + + `; + } + text += ` +
User InputUtility/Year${year}
+ ${createFlag(data[user_input])} + ${utilities[utility]} + ${createInput(utilities[utility], data[user_input], data[utilities[utility]])} +
+ + `; + return text; +} + +function loadBillsOverview() { + /* Make HTTP GET request to get the bills overview information. Call all + * the relevant functions to create the energy overview table. + */ + request(`bills-overview/`, { + method: 'get', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json' + }, + }).then(res => { + if(!res.err) { + const table = document.querySelector('#Energy-Bills-Overview'); + var text = ""; + text += startOverviewForm(); + text += estimationDropDownBox(); + text += createOverviewColumnHeaders(res.payload.instance.year); + text += createOverviewRows(res.payload.instance); + table.innerHTML = text; + } + }) +} + +function billsOverviewFormSubmit(data) { + /* Handle Submition of energy bills overview form. Get form data and create + * a dictionary result with the formdata. Send result to backend. This will + * be modified to receive all projected rows for ~20 years. + */ + const formData = new FormData(data); + const result = {}; + for (const [key, value] of formData.entries()) { + result[key] = value; + } + request(`bills-overview/`, { + method: 'put', + credentials: 'same-origin', + body: JSON.stringify(result), + headers: new Headers({ + 'Content-Type': 'application/json', + 'X-CSRFToken': Cookies.get('csrftoken') + }) + }); + return false; +} diff --git a/blocnote/apps/financialInputs/templates/financialInputs/bills.html b/blocnote/apps/financialInputs/templates/financialInputs/bills.html new file mode 100644 index 0000000000000000000000000000000000000000..b57366d474d7b0149e8f15da4fc23a46cc20d1d4 --- /dev/null +++ b/blocnote/apps/financialInputs/templates/financialInputs/bills.html @@ -0,0 +1,41 @@ +
+

Energy Bills

+
+
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
diff --git a/blocnote/apps/financialInputs/templates/financialInputs/billsOverview.html b/blocnote/apps/financialInputs/templates/financialInputs/billsOverview.html new file mode 100644 index 0000000000000000000000000000000000000000..c0154420e77c83c51d854b9fb38fbe33d62ea1a7 --- /dev/null +++ b/blocnote/apps/financialInputs/templates/financialInputs/billsOverview.html @@ -0,0 +1,5 @@ +
+

Energy Bills Overview

+
+
+
diff --git a/blocnote/apps/financialInputs/templates/financialInputs/headerForm.html b/blocnote/apps/financialInputs/templates/financialInputs/headerForm.html new file mode 100644 index 0000000000000000000000000000000000000000..c5b3ccdbbe7d4f1084c037bbe21f54e2a30164d3 --- /dev/null +++ b/blocnote/apps/financialInputs/templates/financialInputs/headerForm.html @@ -0,0 +1,49 @@ +
+ {% csrf_token %} +
+
+
+ {{ form.pro_forma_start_date.label_tag }} +
+
+ {{ form.pro_forma_start_date }} +
+
+ {{ form.pro_forma_duration.label_tag }} +
+
+ {{ form.pro_forma_duration }} (years) +
+
+
+
+
+
+ {{ form.analysis_date.label_tag }} +
+
+ {{ form.analysis_date }} +
+
+ Select Fund: {{ form.fund }} +
+
+
+
+
+ {{ form.anticipated_construction_start_date.label_tag }} + {{ form.anticipated_construction_start_date }} + + {{ form.anticipated_commissioning_date.label_tag }} + {{ form.anticipated_commissioning_date }} +
+
+
+
+ {{ form.anticipated_construction_period.label_tag }} + {{ form.anticipated_construction_period }} weeks +
+ +
+
+
diff --git a/blocnote/apps/financialInputs/templates/financialInputs/index.html b/blocnote/apps/financialInputs/templates/financialInputs/index.html index 580a3fd1eb3198bf92d90f9d2701d3357c0f837e..f615ef0a6142def9c67e3e7de991e991339bc2a0 100644 --- a/blocnote/apps/financialInputs/templates/financialInputs/index.html +++ b/blocnote/apps/financialInputs/templates/financialInputs/index.html @@ -1,18 +1,31 @@ {% extends 'base.html' %} {% load staticfiles %} - {% block content %} -

- This is finance inputs -

-
- Building id: {{ id }}
- Address: {{ address }} +
+
+

+ Financial Inputs +

+
+ {{ building.address }} +
+
+
+
+ {% include "financialInputs/headerForm.html" with form=header_form %} +
+
+
+ {% include "financialInputs/bills.html" %} +
+
+ {% include "financialInputs/billsOverview.html" %} +
{% endblock %} - {% block scripts %} + {% endblock %} diff --git a/blocnote/apps/financialInputs/urls.py b/blocnote/apps/financialInputs/urls.py index a3780aa2190c93ed23dd96691de9cef83decf8aa..7f66beda4aa2c7435872a1ebc970b32be905b464 100644 --- a/blocnote/apps/financialInputs/urls.py +++ b/blocnote/apps/financialInputs/urls.py @@ -3,5 +3,8 @@ from django.conf.urls import url from . import views urlpatterns = [ - url(r'^$', views.index, name='index'), + url(r'^$', views.Index.as_view(), name='index'), + url(r'^finance-overview/$', views.BlocNoteHeader.as_view(), name='header'), + url(r'^bills/$', views.BillsTable.as_view(), name='bills'), + url(r'^bills-overview/$', views.BillsOverviewView.as_view(), name='bills_overview'), ] diff --git a/blocnote/apps/financialInputs/views.py b/blocnote/apps/financialInputs/views.py index 89bc0b7b4181c292eaee8557bf0a9311e5c39057..0488f731b9637cc2a280bd0f83ef9d637aefa8ce 100644 --- a/blocnote/apps/financialInputs/views.py +++ b/blocnote/apps/financialInputs/views.py @@ -1,14 +1,453 @@ +import json from django.shortcuts import render -from django.http import HttpResponse +from django.http import JsonResponse from django.db import connections +from django.views import View +from .models import FinancingOverview, Bills, BillsOverview +from .forms import BlocNoteHeaderForm -def index(request, building_id): - cursor = connections['building_db'].cursor() - cursor.execute('SELECT * FROM public.get_building(in_building_id := {})'.format(building_id)) - columns = [col[0] for col in cursor.description] - row = cursor.fetchone() - building = dict(zip(columns, row)) - context = {'address': building['address'], 'id': building['building_id']} - return render(request, 'financialInputs/index.html', context=context) +def get_model_object(model, building_id): + """Function that returns a model object. + + Return the model object for a given model for a building. + + Args: + model: Model class whose object needs to be obtained. + building_id: The id of the building to which the model belongs. + + Returns: + obj: The object for the given model and building_id. + """ + try: + obj = model.objects.get(building_id=building_id) + return obj + except: + return 0 + + +def change_date_format(date): + """Change date format. + + The CSV file contains date in MM/DD/YYYY format. The database needs it + to be in YYYY-MM-DD. This function performs this task. + + Args: + date: The date in MM/DD/YYYY format. + + Returns: + new_date: The date in YYYY-MM-DD format. + """ + date_split = date.split('/') + new_date = date_split[2] + '-' + date_split[0] + '-' + date_split[1] + return new_date + + +class Index(View): + """Class that renders the index page.""" + + model = FinancingOverview + + 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_context_for_index(self, building_id, building): + """Return context for index page. + + Create the context for the index page. The index page should get the + building data(which is a dictionary) and the header form. The function + checks if a header form for the given building. If it does not exist, + it creates a new form else it takes the existing form. + + Args: + building_id: id of the building. + building: A dictionary containing the building data. + + Returns: + context: A dictionary with the building data and header form. + """ + financing_overview_model_obj = get_model_object( + self.model, building_id + ) + if financing_overview_model_obj: + header_form = BlocNoteHeaderForm( + instance=financing_overview_model_obj + ) + else: + header_form = BlocNoteHeaderForm() + context = { + 'building': building, + 'header_form': header_form, + } + return context + + def get(self, request, building_id): + """GET route for Index page. + + Get the building data and context. Renders result to the frontend. + + Args: + request: HTTP GET request. + building_id: id of the building. + + Returns: + render: The index.html page with the context. + """ + building = self.get_building_data(building_id) + context = self.get_context_for_index(building_id, building) + return render(request, 'financialInputs/index.html', context=context) + + +class BlocNoteHeader(View): + """This class handles header creation and update.""" + + model = FinancingOverview + form_class = BlocNoteHeaderForm + + def handle_form(self, put, form, building_id): + """Handle form submit. + + Take the header form passed by the frontend and validates it. If it is + a valid form, it makes an entry in the database and returns the JSON + response to be sent to the frontend. + + Args: + put: This is the data sent in the PUT request by frontend. + form: Header form with data passed by frontend. + building_id: id of the building. + + Returns: + JsonResponse: A JSON response sent to the frontend. If success, it + sends the form data else it returns error. + """ + financing_overview_obj = get_model_object(self.model, building_id) + if financing_overview_obj: + form = self.form_class(put, instance=financing_overview_obj) + else: + form.instance.building_id = building_id + if form.is_valid(): + header = form.save() + return JsonResponse({**form.data, 'id': header.id}) + return JsonResponse(form.errors, status=400) + + def put(self, request, building_id): + """PUT route for header. + + Handle the PUT request for the header form. It loads the JSON data + onto a form and handles the submition of the form. It sends a JSON + response to the frontend with form data or error status. + + Args: + request: HTTP PUT request. + building_id: id of the building. + + Returns: + JsonResponse: If success, returns form data and if not, returns + error status as JSON. + """ + put = json.loads(request.body.decode()) + form = self.form_class(put) + return self.handle_form(put, form, building_id) + + +class BillsTable(View): + """Create the Energy Bills tables. + + Class to upload energy bills for each utility, store bills and send + the data to the frontend. + """ + + model = Bills + + def put(self, request, building_id): + """Handle HTTP PUT request. + + Obtain bill information from frontend as JSON. The data is in CSV. + The relevant data is stored in the database. Upon successfull upload, + the data is sent to the frontend to create the table. + + Args: + request: HTTP PUT request. + building_id: id of the building. + + Returns: + JsonResponse: JSON of the result which is a list containing + rows to be displayed. + """ + put = json.loads(request.body.decode()) + result = self.handle_file(put, building_id) + return JsonResponse({'result': result}) + + def handle_file(self, data, building_id): + """Handle file input. + + Take file data from frontend and obtain the indices of the relevant + columns. Go through the file and get the relevant data and store in the + database. + + Args: + data: The file data obtained from frontend. + building_id: id of the building. + + Returns: + result: List of rows to be sent to the frontend to display table. + """ + utility_type = data['utility_type'] + + self.model.objects.filter( + building_id=building_id, + utility_type=utility_type + ).delete() + + text = data['text'].split('\n') + + column_headings = text[0].split(',') + for index, heading in enumerate(column_headings): + if heading == "Bill From Date": + date_from_index = index + elif heading == "Bill To Date": + date_to_index = index + elif heading == "Usage": + usage_index = index + elif heading == "Total Charges": + charge_index = index + + result = [] + for line in range(1, len(text)-1): + row_list = [] + line_array = text[line].split(',') + date_from = change_date_format(line_array[date_from_index]) + row_list.append(date_from) + date_to = change_date_format(line_array[date_to_index]) + row_list.append(date_to) + usage = line_array[usage_index] + row_list.append(usage) + charge = line_array[charge_index] + row_list.append(charge) + self.model.objects.create(building_id=building_id, + date_from=date_from, + date_to=date_to, + utility_type=data['utility_type'], + usage=usage, + charge=charge) + result.append(row_list) + + return result + + def get(self, request, building_id): + """Handle HTTP GET request. + + Fetch stored utility bills data and send to the frontend to display as + table. + + Args: + request: HTTP GET request. + building_id: id of the building. + + Return: + JsonResponse: JSON of the result which is a list of rows to populate + the table. + """ + result = [] + present = False + utility_type = request.GET.get('utility_type') + + try: + model_obj = self.model.objects.filter( + building_id=building_id, + utility_type=utility_type + ) + if model_obj: + present = True + for row in model_obj: + row_list = [] + row_list.append(row.date_from) + row_list.append(row.date_to) + row_list.append(row.usage) + row_list.append(row.charge) + result.append(row_list) + + return JsonResponse({'result': result, 'present': present}) + except: + return JsonResponse({'result': result, 'present': present}) + + +def get_total_charge(obj): + """Get total utility charge. + + Obtain the total charge for a utility. This function will be replaced by + finance team logic. + + Args: + object: Model object which contains the charge value. + + Returns: + total: The sum of all charges for the utility. + """ + total = 0 + for row in obj: + total += row.charge + return total + + +class BillsOverviewView(View): + """Generate Energy Bills Overview table. + + Generate energy bills overview table from the energy bills data. If bill not + available, take user input for calculation purposes. + """ + + model_bills_overview = BillsOverview + model_bills = Bills + utility = ['electricity', 'gas', 'oil', 'water'] + + def get(self, request, building_id): + """Handle HTTP GET request. + + Generate energy bills overview table with bills present in the + database. If a certain utility bill is absent, allow user to input + data. + + Args: + request: HTTP GET request. + building_id: id of the building. + + Returns: + JsonResponse: Returns instance, which is a dictionary containing + all the data to be filled in the table. + """ + instance = self.get_instance(building_id) + return JsonResponse({'instance': instance}) + + def get_instance(self, building_id): + """Create dictionary instance. + + This dictionary is an instance of the data present in the database. It + contains the annual utility charge(for now, this will be replaced by + value obtained from finance logic), Pro Forma start year and flags + indicating if a utility is present in the database. This is to tell the + frontend if an input has to be taken or not. + + Args: + building_id: id of the building. + + Returns: + instance: A dictionary containing the annual utility charge and a + flag indicating if utility bill present or not. + """ + instance = {} + + try: + obj = FinancingOverview.objects.get(building_id=building_id) + date = obj.pro_forma_start_date + year = date.year + except: + year = 'TBD' + + utility_objects = {} + for util in self.utility: + utility_objects[util] = self.model_bills.objects.filter( + building_id=building_id, + utility_type=util + ) + + for util in self.utility: + instance[util] = 0 + instance[util+'_is_user_input'] = True + + for util in self.utility: + if utility_objects[util]: + instance[util] = get_total_charge( + utility_objects[util] + ) + instance[util+'_is_user_input'] = False + else: + try: + bills_overview_obj = self.model_bills_overview.objects.get( + building_id=building_id + ) + if util == 'electricity': + instance[util] = bills_overview_obj.electricity + elif util == 'gas': + instance[util] = bills_overview_obj.gas + elif util == 'oil': + instance[util] = bills_overview_obj.oil + elif util == 'water': + instance[util] = bills_overview_obj.water + except: + instance[util] = 0 + instance['year'] = year + + return instance + + def put(self, request, building_id): + """Handle HTTP PUT request. + + Take annual utility charge and projection option from frontend and + store in the database. This will be modified to call the finance + projection fuction. + + Args: + request: HTTP PUT request. + building_id: id of the building. + + Returns: + JsonResponse: Returns new instance with the updated values. + """ + put = json.loads(request.body.decode()) + + entry = self.model_bills_overview.objects.filter( + building_id=building_id + ) + + instance = self.get_instance(building_id) + + if entry: + entry.update( + building_id=building_id, + year=instance['year'], + electricity=put['electricity-value'], + electricity_is_user_input=instance['electricity_is_user_input'], + gas=put['gas-value'], + gas_is_user_input=instance['gas_is_user_input'], + oil=put['oil-value'], + oil_is_user_input=instance['oil_is_user_input'], + water=put['water-value'], + water_is_user_input=instance['water_is_user_input'], + estimation_algorithm=put['Estimation Model'] + ) + else: + entry = self.model_bills_overview.objects.create( + building_id=building_id, + year=instance['year'], + electricity=put['electricity-value'], + electricity_is_user_input=instance['electricity_is_user_input'], + gas=put['gas-value'], + gas_is_user_input=instance['gas_is_user_input'], + oil=put['oil-value'], + oil_is_user_input=instance['oil_is_user_input'], + water=put['water-value'], + water_is_user_input=instance['water_is_user_input'], + estimation_algorithm=put['Estimation Model'] + ) + + new_instance = self.get_instance(building_id) + + return JsonResponse({'status': new_instance}) diff --git a/blocnote/settings.py b/blocnote/settings.py index eef190ff4ac4b02e7a27679855501695620cc5d2..cce5531dba956a3ebabe10f17bcec30d3f5725c5 100644 --- a/blocnote/settings.py +++ b/blocnote/settings.py @@ -135,7 +135,7 @@ USE_TZ = True # https://docs.djangoproject.com/en/1.11/howto/static-files/ STATIC_URL = config('STATIC_URL') -# STATICFILES_DIRS = [ -# os.path.join(BASE_DIR, 'blocnote/static'), -# ] +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'blocnote/static'), +] STATIC_ROOT = os.path.join(BASE_DIR, 'static') diff --git a/blocnote/templates/base.html b/blocnote/templates/base.html index 3e4727eef5394dac81384e3337815d310bf5c934..ae04b1bd901f7289810b22c646607e39c9b35e57 100644 --- a/blocnote/templates/base.html +++ b/blocnote/templates/base.html @@ -10,11 +10,37 @@ {% block title %}BlocNote{% endblock %} + + {% block head %}{% endblock %} {% block content %}{% endblock %} + + + + {% block scripts %}{% endblock %} diff --git a/blocnote/urls.py b/blocnote/urls.py index b55bc834d9683a21f7deb1d2ecd11b97418b8ab6..69e442f03e135dccb48451ab4c825baff226461f 100644 --- a/blocnote/urls.py +++ b/blocnote/urls.py @@ -17,6 +17,7 @@ from django.conf.urls import include, url from django.contrib import admin urlpatterns = [ - url(r'^building/(?P[0-9]+)/financial-inputs', include('blocnote.apps.financialInputs.urls')), + url('', admin.site.urls), + url(r'^building/(?P[0-9]+)/financial-inputs/', include('blocnote.apps.financialInputs.urls')), url(r'^admin/', admin.site.urls), ]