added Tenant Batch Analysis Report

pull/43/MERGE
13621160019@163.com 2021-05-13 08:19:05 +08:00
parent 900470e016
commit 13249842e6
7 changed files with 577 additions and 4 deletions

View File

@ -49,8 +49,6 @@ from reports import shopfloorenergyitem
from reports import shopfloorload
from reports import shopfloorsaving
from reports import shopfloorstatistics
from reports import virtualmeterenergy
from reports import virtualmetercost
from reports import spaceefficiency
from reports import spacecost
from reports import spaceenergycategory
@ -71,8 +69,11 @@ from reports import tenantcost
from reports import tenantenergycategory
from reports import tenantenergyitem
from reports import tenantload
from reports import tenantbatch
from reports import tenantsaving
from reports import tenantstatistics
from reports import virtualmeterenergy
from reports import virtualmetercost
########################################################################################################################
# BEGIN imports for Enterprise Version
@ -589,6 +590,8 @@ api.add_route('/reports/storesaving',
storesaving.Reporting())
api.add_route('/reports/storestatistics',
storestatistics.Reporting())
api.add_route('/reports/tenantbatch',
tenantbatch.Reporting())
api.add_route('/reports/tenantbill',
tenantbill.Reporting())
api.add_route('/reports/tenantcost',

View File

@ -0,0 +1,235 @@
import falcon
import simplejson as json
import mysql.connector
import config
from anytree import Node, AnyNode, LevelOrderIter
from datetime import datetime, timedelta, timezone
from decimal import Decimal
class Reporting:
@staticmethod
def __init__():
pass
@staticmethod
def on_options(req, resp):
resp.status = falcon.HTTP_200
####################################################################################################################
# PROCEDURES
# Step 1: valid parameters
# Step 2: build a space tree
# Step 3: query all tenants in the space tree
# Step 4: query energy categories
# Step 5: query reporting period energy input
# Step 6: construct the report
####################################################################################################################
@staticmethod
def on_get(req, resp):
print(req.params)
space_id = req.params.get('spaceid')
reporting_period_start_datetime_local = req.params.get('reportingperiodstartdatetime')
reporting_period_end_datetime_local = req.params.get('reportingperiodenddatetime')
################################################################################################################
# Step 1: valid parameters
################################################################################################################
if space_id is None:
raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', description='API.INVALID_SPACE_ID')
else:
space_id = str.strip(space_id)
if not space_id.isdigit() or int(space_id) <= 0:
raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST', description='API.INVALID_SPACE_ID')
else:
space_id = int(space_id)
timezone_offset = int(config.utc_offset[1:3]) * 60 + int(config.utc_offset[4:6])
if config.utc_offset[0] == '-':
timezone_offset = -timezone_offset
if reporting_period_start_datetime_local is None:
raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST',
description="API.INVALID_REPORTING_PERIOD_START_DATETIME")
else:
reporting_period_start_datetime_local = str.strip(reporting_period_start_datetime_local)
try:
reporting_start_datetime_utc = datetime.strptime(reporting_period_start_datetime_local,
'%Y-%m-%dT%H:%M:%S')
except ValueError:
raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST',
description="API.INVALID_REPORTING_PERIOD_START_DATETIME")
reporting_start_datetime_utc = reporting_start_datetime_utc.replace(tzinfo=timezone.utc) - \
timedelta(minutes=timezone_offset)
if reporting_period_end_datetime_local is None:
raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST',
description="API.INVALID_REPORTING_PERIOD_END_DATETIME")
else:
reporting_period_end_datetime_local = str.strip(reporting_period_end_datetime_local)
try:
reporting_end_datetime_utc = datetime.strptime(reporting_period_end_datetime_local,
'%Y-%m-%dT%H:%M:%S')
except ValueError:
raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST',
description="API.INVALID_REPORTING_PERIOD_END_DATETIME")
reporting_end_datetime_utc = reporting_end_datetime_utc.replace(tzinfo=timezone.utc) - \
timedelta(minutes=timezone_offset)
if reporting_start_datetime_utc >= reporting_end_datetime_utc:
raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST',
description='API.INVALID_REPORTING_PERIOD_END_DATETIME')
if reporting_start_datetime_utc + timedelta(minutes=15) >= reporting_end_datetime_utc:
raise falcon.HTTPError(falcon.HTTP_400, title='API.BAD_REQUEST',
description='API.THE_REPORTING_PERIOD_MUST_BE_LONGER_THAN_15_MINUTES')
cnx_system_db = mysql.connector.connect(**config.myems_system_db)
cursor_system_db = cnx_system_db.cursor(dictionary=True)
cursor_system_db.execute(" SELECT name "
" FROM tbl_spaces "
" WHERE id = %s ", (space_id,))
row = cursor_system_db.fetchone()
if row is None:
if cursor_system_db:
cursor_system_db.close()
if cnx_system_db:
cnx_system_db.disconnect()
raise falcon.HTTPError(falcon.HTTP_404, title='API.NOT_FOUND',
description='API.SPACE_NOT_FOUND')
################################################################################################################
# Step 2: build a space tree
################################################################################################################
query = (" SELECT id, name, parent_space_id "
" FROM tbl_spaces "
" ORDER BY id ")
cursor_system_db.execute(query)
rows_spaces = cursor_system_db.fetchall()
node_dict = dict()
if rows_spaces is not None and len(rows_spaces) > 0:
for row in rows_spaces:
parent_node = node_dict[row['parent_space_id']] if row['parent_space_id'] is not None else None
node_dict[row['id']] = AnyNode(id=row['id'], parent=parent_node, name=row['name'])
################################################################################################################
# Step 3: query all tenants in the space tree
################################################################################################################
tenant_dict = dict()
space_dict = dict()
for node in LevelOrderIter(node_dict[space_id]):
space_dict[node.id] = node.name
cursor_system_db.execute(" SELECT t.id, t.name AS tenant_name, s.name AS space_name, "
" cc.name AS cost_center_name, t.description "
" FROM tbl_spaces s, tbl_spaces_tenants st, tbl_tenants t, tbl_cost_centers cc "
" WHERE s.id IN ( " + ', '.join(map(str, space_dict.keys())) + ") "
" AND st.space_id = s.id AND st.tenant_id = t.id "
" AND t.cost_center_id = cc.id ", )
rows_tenants = cursor_system_db.fetchall()
if rows_tenants is not None and len(rows_tenants) > 0:
for row in rows_tenants:
tenant_dict[row['id']] = {"tenant_name": row['tenant_name'],
"space_name": row['space_name'],
"cost_center_name": row['cost_center_name'],
"description": row['description'],
"values": list()}
################################################################################################################
# Step 4: query energy categories
################################################################################################################
cnx_energy_db = mysql.connector.connect(**config.myems_energy_db)
cursor_energy_db = cnx_energy_db.cursor()
# query energy categories in reporting period
energy_category_set = set()
cursor_energy_db.execute(" SELECT DISTINCT(energy_category_id) "
" FROM tbl_tenant_input_category_hourly "
" WHERE start_datetime_utc >= %s AND start_datetime_utc < %s ",
(reporting_start_datetime_utc, reporting_end_datetime_utc))
rows_energy_categories = cursor_energy_db.fetchall()
if rows_energy_categories is not None or len(rows_energy_categories) > 0:
for row_energy_category in rows_energy_categories:
energy_category_set.add(row_energy_category[0])
# query all energy categories
cursor_system_db.execute(" SELECT id, name, unit_of_measure "
" FROM tbl_energy_categories "
" ORDER BY id ", )
rows_energy_categories = cursor_system_db.fetchall()
if rows_energy_categories is None or len(rows_energy_categories) == 0:
if cursor_system_db:
cursor_system_db.close()
if cnx_system_db:
cnx_system_db.disconnect()
if cursor_energy_db:
cursor_energy_db.close()
if cnx_energy_db:
cnx_energy_db.disconnect()
raise falcon.HTTPError(falcon.HTTP_404,
title='API.NOT_FOUND',
description='API.ENERGY_CATEGORY_NOT_FOUND')
energy_category_list = list()
for row_energy_category in rows_energy_categories:
if row_energy_category['id'] in energy_category_set:
energy_category_list.append({"id": row_energy_category['id'],
"name": row_energy_category['name'],
"unit_of_measure": row_energy_category['unit_of_measure']})
################################################################################################################
# Step 5: query reporting period energy input
################################################################################################################
for tenant_id in tenant_dict:
cursor_energy_db.execute(" SELECT energy_category_id, SUM(actual_value) "
" FROM tbl_tenant_input_category_hourly "
" WHERE tenant_id = %s "
" AND start_datetime_utc >= %s "
" AND start_datetime_utc < %s "
" GROUP BY energy_category_id ",
(tenant_id,
reporting_start_datetime_utc,
reporting_end_datetime_utc))
rows_tenant_energy = cursor_energy_db.fetchall()
for energy_category in energy_category_list:
subtotal = Decimal(0.0)
for row_tenant_energy in rows_tenant_energy:
if energy_category['id'] == row_tenant_energy[0]:
subtotal = row_tenant_energy[1]
break
tenant_dict[tenant_id]['values'].append(subtotal)
if cursor_system_db:
cursor_system_db.close()
if cnx_system_db:
cnx_system_db.disconnect()
if cursor_energy_db:
cursor_energy_db.close()
if cnx_energy_db:
cnx_energy_db.disconnect()
################################################################################################################
# Step 6: construct the report
################################################################################################################
tenant_list = list()
for tenant_id, tenant in tenant_dict.items():
tenant_list.append({
"id": tenant_id,
"tenant_name": tenant['tenant_name'],
"space_name": tenant['space_name'],
"cost_center_name": tenant['cost_center_name'],
"description": tenant['description'],
"values": tenant['values'],
})
result = {'tenants': tenant_list,
'energycategories': energy_category_list}
resp.body = json.dumps(result)

View File

@ -262,9 +262,8 @@ const MeterTracking = ({ setRedirect, setRedirectUrl, t }) => {
return response.json();
}).then(json => {
if (isResponseOK) {
let json_meters = JSON.parse(JSON.stringify([json['meters']]).split('"id":').join('"value":').split('"name":').join('"label":'));
let meters = [];
json_meters[0].forEach((currentValue, index) => {
json['meters'].forEach((currentValue, index) => {
meters.push({
'id': currentValue['id'],
'name': currentValue['meter_name'],

View File

@ -0,0 +1,330 @@
import React, { Fragment, useEffect, useState } from 'react';
import {
Breadcrumb,
BreadcrumbItem,
Row,
Col,
Card,
CardBody,
Button,
ButtonGroup,
Form,
FormGroup,
Input,
Label,
CustomInput,
Spinner,
} from 'reactstrap';
import Datetime from 'react-datetime';
import moment from 'moment';
import loadable from '@loadable/component';
import Cascader from 'rc-cascader';
import { getCookieValue, createCookie } from '../../../helpers/utils';
import withRedirect from '../../../hoc/withRedirect';
import { withTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import ButtonIcon from '../../common/ButtonIcon';
import { APIBaseURL } from '../../../config';
const DetailedDataTable = loadable(() => import('../common/DetailedDataTable'));
const TenantBatch = ({ setRedirect, setRedirectUrl, t }) => {
let current_moment = moment();
useEffect(() => {
let is_logged_in = getCookieValue('is_logged_in');
let user_name = getCookieValue('user_name');
let user_display_name = getCookieValue('user_display_name');
let user_uuid = getCookieValue('user_uuid');
let token = getCookieValue('token');
if (is_logged_in === null || !is_logged_in) {
setRedirectUrl(`/authentication/basic/login`);
setRedirect(true);
} else {
//update expires time of cookies
createCookie('is_logged_in', true, 1000 * 60 * 60 * 8);
createCookie('user_name', user_name, 1000 * 60 * 60 * 8);
createCookie('user_display_name', user_display_name, 1000 * 60 * 60 * 8);
createCookie('user_uuid', user_uuid, 1000 * 60 * 60 * 8);
createCookie('token', token, 1000 * 60 * 60 * 8);
}
});
// State
// Query Parameters
const [selectedSpaceName, setSelectedSpaceName] = useState(undefined);
const [selectedSpaceID, setSelectedSpaceID] = useState(undefined);
const [tenantList, setTenantList] = useState([]);
const [reportingPeriodBeginsDatetime, setReportingPeriodBeginsDatetime] = useState(current_moment.clone().startOf('month'));
const [reportingPeriodEndsDatetime, setReportingPeriodEndsDatetime] = useState(current_moment);
const [cascaderOptions, setCascaderOptions] = useState(undefined);
// buttons
const [submitButtonDisabled, setSubmitButtonDisabled] = useState(false);
const [spinnerHidden, setSpinnerHidden] = useState(true);
const [exportButtonHidden, setExportButtonHidden] = useState(true);
//Results
const [detailedDataTableColumns, setDetailedDataTableColumns] = useState(
[{dataField: 'name', text: t('Name'), sort: true}, {dataField: 'space', text: t('Space'), sort: true}]);
const [excelBytesBase64, setExcelBytesBase64] = useState(undefined);
useEffect(() => {
let isResponseOK = false;
fetch(APIBaseURL + '/spaces/tree', {
method: 'GET',
headers: {
"Content-type": "application/json",
"User-UUID": getCookieValue('user_uuid'),
"Token": getCookieValue('token')
},
body: null,
}).then(response => {
console.log(response);
if (response.ok) {
isResponseOK = true;
}
return response.json();
}).then(json => {
console.log(json);
if (isResponseOK) {
// rename keys
json = JSON.parse(JSON.stringify([json]).split('"id":').join('"value":').split('"name":').join('"label":'));
setCascaderOptions(json);
// set the default selected space
setSelectedSpaceName([json[0]].map(o => o.label));
setSelectedSpaceID([json[0]].map(o => o.value));
setSubmitButtonDisabled(false);
setSpinnerHidden(true);
} else {
toast.error(json.description);
}
}).catch(err => {
console.log(err);
});
}, []);
const labelClasses = 'ls text-uppercase text-600 font-weight-semi-bold mb-0';
let onSpaceCascaderChange = (value, selectedOptions) => {
setSelectedSpaceName(selectedOptions.map(o => o.label).join('/'));
setSelectedSpaceID(value[value.length - 1]);
setTenantList([]);
setExportButtonHidden(true);
setSubmitButtonDisabled(false);
}
let onReportingPeriodBeginsDatetimeChange = (newDateTime) => {
setReportingPeriodBeginsDatetime(newDateTime);
}
let onReportingPeriodEndsDatetimeChange = (newDateTime) => {
setReportingPeriodEndsDatetime(newDateTime);
}
var getValidReportingPeriodBeginsDatetimes = function (currentDate) {
return currentDate.isBefore(moment(reportingPeriodEndsDatetime, 'MM/DD/YYYY, hh:mm:ss a'));
}
var getValidReportingPeriodEndsDatetimes = function (currentDate) {
return currentDate.isAfter(moment(reportingPeriodBeginsDatetime, 'MM/DD/YYYY, hh:mm:ss a'));
}
// Handler
const handleSubmit = e => {
e.preventDefault();
console.log('handleSubmit');
console.log(selectedSpaceID);
console.log(reportingPeriodBeginsDatetime.format('YYYY-MM-DDTHH:mm:ss'));
console.log(reportingPeriodEndsDatetime.format('YYYY-MM-DDTHH:mm:ss'));
// disable submit button
setSubmitButtonDisabled(true);
// show spinner
setSpinnerHidden(false);
// hide export buttion
setExportButtonHidden(true)
// Reinitialize tables
setTenantList([]);
let isResponseOK = false;
fetch(APIBaseURL + '/reports/tenantbatch?' +
'spaceid=' + selectedSpaceID +
'&reportingperiodstartdatetime=' + reportingPeriodBeginsDatetime.format('YYYY-MM-DDTHH:mm:ss') +
'&reportingperiodenddatetime=' + reportingPeriodEndsDatetime.format('YYYY-MM-DDTHH:mm:ss'), {
method: 'GET',
headers: {
"Content-type": "application/json",
"User-UUID": getCookieValue('user_uuid'),
"Token": getCookieValue('token')
},
body: null,
}).then(response => {
if (response.ok) {
isResponseOK = true;
};
return response.json();
}).then(json => {
if (isResponseOK) {
console.log(json)
let tenants = [];
if (json['tenants'].length > 0) {
json['tenants'].forEach((currentTenant, index) => {
let detailed_value = {};
detailed_value['id'] = currentTenant['id'];
detailed_value['name'] = currentTenant['tenant_name'];
detailed_value['space'] = currentTenant['space_name'];
detailed_value['costcenter'] = currentTenant['cost_center_name'];
currentTenant['values'].forEach((currentValue, energyCategoryIndex) => {
detailed_value['a' + energyCategoryIndex] = currentValue.toFixed(2);
});
tenants.push(detailed_value);
});
};
setTenantList(tenants);
let detailed_column_list = [];
detailed_column_list.push({
dataField: 'name',
text: t('Name'),
sort: true
});
detailed_column_list.push({
dataField: 'space',
text: t('Space'),
sort: true
});
json['energycategories'].forEach((currentValue, index) => {
detailed_column_list.push({
dataField: 'a' + index,
text: currentValue['name'] + ' (' + currentValue['unit_of_measure'] + ')',
sort: true
})
});
setDetailedDataTableColumns(detailed_column_list);
setExcelBytesBase64(json['excel_bytes_base64']);
// enable submit button
setSubmitButtonDisabled(false);
// hide spinner
setSpinnerHidden(true);
// show export buttion
setExportButtonHidden(false);
} else {
toast.error(json.description)
}
}).catch(err => {
console.log(err);
});
};
const handleExport = e => {
e.preventDefault();
const mimeType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
const fileName = 'tenantbatch.xlsx'
var fileUrl = "data:" + mimeType + ";base64," + excelBytesBase64;
fetch(fileUrl)
.then(response => response.blob())
.then(blob => {
var link = window.document.createElement("a");
link.href = window.URL.createObjectURL(blob, { type: mimeType });
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
};
return (
<Fragment>
<div>
<Breadcrumb>
<BreadcrumbItem>{t('Tenant Data')}</BreadcrumbItem><BreadcrumbItem active>{t('Batch Analysis')}</BreadcrumbItem>
</Breadcrumb>
</div>
<Card className="bg-light mb-3">
<CardBody className="p-3">
<Form onSubmit={handleSubmit}>
<Row form>
<Col xs={6} sm={3}>
<FormGroup className="form-group">
<Label className={labelClasses} for="space">
{t('Space')}
</Label>
<br />
<Cascader options={cascaderOptions}
onChange={onSpaceCascaderChange}
changeOnSelect
expandTrigger="hover">
<Input value={selectedSpaceName || ''} readOnly />
</Cascader>
</FormGroup>
</Col>
<Col xs={6} sm={3}>
<FormGroup className="form-group">
<Label className={labelClasses} for="reportingPeriodBeginsDatetime">
{t('Reporting Period Begins')}
</Label>
<Datetime id='reportingPeriodBeginsDatetime'
value={reportingPeriodBeginsDatetime}
onChange={onReportingPeriodBeginsDatetimeChange}
isValidDate={getValidReportingPeriodBeginsDatetimes}
closeOnSelect={true} />
</FormGroup>
</Col>
<Col xs={6} sm={3}>
<FormGroup className="form-group">
<Label className={labelClasses} for="reportingPeriodEndsDatetime">
{t('Reporting Period Ends')}
</Label>
<Datetime id='reportingPeriodEndsDatetime'
value={reportingPeriodEndsDatetime}
onChange={onReportingPeriodEndsDatetimeChange}
isValidDate={getValidReportingPeriodEndsDatetimes}
closeOnSelect={true} />
</FormGroup>
</Col>
<Col xs="auto">
<FormGroup>
<br></br>
<ButtonGroup id="submit">
<Button color="success" disabled={submitButtonDisabled} >{t('Submit')}</Button>
</ButtonGroup>
</FormGroup>
</Col>
<Col xs="auto">
<FormGroup>
<br></br>
<Spinner color="primary" hidden={spinnerHidden} />
</FormGroup>
</Col>
<Col xs="auto">
<br></br>
<ButtonIcon icon="external-link-alt" transform="shrink-3 down-2" color="falcon-default"
hidden={exportButtonHidden}
onClick={handleExport} >
{t('Export')}
</ButtonIcon>
</Col>
</Row>
</Form>
</CardBody>
</Card>
<DetailedDataTable data={tenantList} title={t('Detailed Data')} columns={detailedDataTableColumns} pagesize={50} >
</DetailedDataTable>
</Fragment>
);
};
export default withTranslation()(withRedirect(TenantBatch));

View File

@ -28,6 +28,7 @@ const resources = {
"Efficiency": "Efficiency",
"Load": "Load",
"Statistics": "Statistics",
"Batch Analysis": "Batch Analysis",
"Saving": "Saving",
"Equipment Tracking": "Equipment Tracking",
"Meter Energy": "Meter Energy",
@ -369,6 +370,7 @@ const resources = {
"Efficiency": "Effizienz",
"Load": "Stromlast",
"Statistics": "Statistiken",
"Batch Analysis": "Charge Analyse",
"Saving": "Sparen",
"Equipment Tracking": "Ausrüstung Datei",
"Meter Energy": "Meter Energie ",
@ -713,6 +715,7 @@ const resources = {
"Efficiency": "效率分析",
"Load": "负荷分析",
"Statistics": "统计分析",
"Batch Analysis": "批量分析",
"Saving": "节能分析",
"Equipment Tracking": "设备台账",
"Meter Energy": "计量表能耗分析",

View File

@ -142,6 +142,7 @@ import TenantLoad from '../components/MyEMS/Tenant/TenantLoad';
import TenantStatistics from '../components/MyEMS/Tenant/TenantStatistics';
import TenantSaving from '../components/MyEMS/Tenant/TenantSaving';
import TenantBill from '../components/MyEMS/Tenant/TenantBill';
import TenantBatch from '../components/MyEMS/Tenant/TenantBatch';
// Store
import StoreEnergyCategory from '../components/MyEMS/Store/StoreEnergyCategory';
import StoreEnergyItem from '../components/MyEMS/Store/StoreEnergyItem';
@ -375,6 +376,7 @@ const MyEMSRoutes = () => (
<Route path="/tenant/statistics" exact component={TenantStatistics} />
<Route path="/tenant/saving" exact component={TenantSaving} />
<Route path="/tenant/bill" exact component={TenantBill} />
<Route path="/tenant/batch" exact component={TenantBatch} />
{/*Sotore*/}
<Route path="/store/energycategory" exact component={StoreEnergyCategory} />

View File

@ -375,6 +375,7 @@ export const tenantRoutes = {
{ to: '/tenant/statistics', name: 'Statistics' },
{ to: '/tenant/saving', name: 'Saving' },
{ to: '/tenant/bill', name: 'Tenant Bill' },
{ to: '/tenant/batch', name: 'Batch Analysis' },
]
};