Feature: Charts in the dashboard for Cash Flow and Creditors using nvd3.js and d3.js

It is still a work in progress with not all charts supporting dynamic data.  But working enough to be put in production use.
This commit is contained in:
Amritanshu 2013-12-17 15:56:36 +05:30
parent e5f8a4f86f
commit 89e1443b48
12 changed files with 15534 additions and 5 deletions

View File

@ -49,6 +49,9 @@ def main(global_config, **settings):
config.add_static_view('partial', 'brewman:static/partial', cache_max_age=get_age(10))
config.add_static_view('template', 'brewman:static/template', cache_max_age=get_age(10))
config.add_route('api_dashboard', '/api/Dashboard')
config.add_route('dashboard', '/Dashboard')
config.add_route('api_login', '/api/login')
config.add_route('login', '/login')
config.add_route('logout', '/logout')

View File

@ -0,0 +1,769 @@
/********************
* HTML CSS
*/
.chartWrap {
margin: 0;
padding: 0;
overflow: hidden;
}
/********************
Box shadow and border radius styling
*/
.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip {
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
}
/********************
* TOOLTIP CSS
*/
.nvtooltip {
position: absolute;
background-color: rgba(255,255,255,1.0);
padding: 1px;
border: 1px solid rgba(0,0,0,.2);
z-index: 10000;
font-family: Arial;
font-size: 13px;
text-align: left;
pointer-events: none;
white-space: nowrap;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/*Give tooltips that old fade in transition by
putting a "with-transitions" class on the container div.
*/
.nvtooltip.with-transitions, .with-transitions .nvtooltip {
transition: opacity 250ms linear;
-moz-transition: opacity 250ms linear;
-webkit-transition: opacity 250ms linear;
transition-delay: 250ms;
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
}
.nvtooltip.x-nvtooltip,
.nvtooltip.y-nvtooltip {
padding: 8px;
}
.nvtooltip h3 {
margin: 0;
padding: 4px 14px;
line-height: 18px;
font-weight: normal;
background-color: rgba(247,247,247,0.75);
text-align: center;
border-bottom: 1px solid #ebebeb;
-webkit-border-radius: 5px 5px 0 0;
-moz-border-radius: 5px 5px 0 0;
border-radius: 5px 5px 0 0;
}
.nvtooltip p {
margin: 0;
padding: 5px 14px;
text-align: center;
}
.nvtooltip span {
display: inline-block;
margin: 2px 0;
}
.nvtooltip table {
margin: 6px;
border-spacing:0;
}
.nvtooltip table td {
padding: 2px 9px 2px 0;
vertical-align: middle;
}
.nvtooltip table td.key {
font-weight:normal;
}
.nvtooltip table td.value {
text-align: right;
font-weight: bold;
}
.nvtooltip table tr.highlight td {
padding: 1px 9px 1px 0;
border-bottom-style: solid;
border-bottom-width: 1px;
border-top-style: solid;
border-top-width: 1px;
}
.nvtooltip table td.legend-color-guide div {
width: 8px;
height: 8px;
vertical-align: middle;
}
.nvtooltip .footer {
padding: 3px;
text-align: center;
}
.nvtooltip-pending-removal {
position: absolute;
pointer-events: none;
}
/********************
* SVG CSS
*/
svg {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Trying to get SVG to act like a greedy block in all browsers */
display: block;
width:100%;
height:100%;
}
svg text {
font: normal 12px Arial;
}
svg .title {
font: bold 14px Arial;
}
.nvd3 .nv-background {
fill: white;
fill-opacity: 0;
/*
pointer-events: none;
*/
}
.nvd3.nv-noData {
font-size: 18px;
font-weight: bold;
}
/**********
* Brush
*/
.nv-brush .extent {
fill-opacity: .125;
shape-rendering: crispEdges;
}
/**********
* Legend
*/
.nvd3 .nv-legend .nv-series {
cursor: pointer;
}
.nvd3 .nv-legend .disabled circle {
fill-opacity: 0;
}
/**********
* Axes
*/
.nvd3 .nv-axis {
pointer-events:none;
}
.nvd3 .nv-axis path {
fill: none;
stroke: #000;
stroke-opacity: .75;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis path.domain {
stroke-opacity: .75;
}
.nvd3 .nv-axis.nv-x path.domain {
stroke-opacity: 0;
}
.nvd3 .nv-axis line {
fill: none;
stroke: #e5e5e5;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis .zero line,
/*this selector may not be necessary*/ .nvd3 .nv-axis line.zero {
stroke-opacity: .75;
}
.nvd3 .nv-axis .nv-axisMaxMin text {
font-weight: bold;
}
.nvd3 .x .nv-axis .nv-axisMaxMin text,
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
text-anchor: middle
}
/**********
* Brush
*/
.nv-brush .resize path {
fill: #eee;
stroke: #666;
}
/**********
* Bars
*/
.nvd3 .nv-bars .negative rect {
zfill: brown;
}
.nvd3 .nv-bars rect {
zfill: steelblue;
fill-opacity: .75;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-bars rect.hover {
fill-opacity: 1;
}
.nvd3 .nv-bars .hover rect {
fill: lightblue;
}
.nvd3 .nv-bars text {
fill: rgba(0,0,0,0);
}
.nvd3 .nv-bars .hover text {
fill: rgba(0,0,0,1);
}
/**********
* Bars
*/
.nvd3 .nv-multibar .nv-groups rect,
.nvd3 .nv-multibarHorizontal .nv-groups rect,
.nvd3 .nv-discretebar .nv-groups rect {
stroke-opacity: 0;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-multibar .nv-groups rect:hover,
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
.nvd3 .nv-discretebar .nv-groups rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-discretebar .nv-groups text,
.nvd3 .nv-multibarHorizontal .nv-groups text {
font-weight: bold;
fill: rgba(0,0,0,1);
stroke: rgba(0,0,0,0);
}
/***********
* Pie Chart
*/
.nvd3.nv-pie path {
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-pie .nv-slice text {
stroke: #000;
stroke-width: 0;
}
.nvd3.nv-pie path {
stroke: #fff;
stroke-width: 1px;
stroke-opacity: 1;
}
.nvd3.nv-pie .hover path {
fill-opacity: .7;
}
.nvd3.nv-pie .nv-label {
pointer-events: none;
}
.nvd3.nv-pie .nv-label rect {
fill-opacity: 0;
stroke-opacity: 0;
}
/**********
* Lines
*/
.nvd3 .nv-groups path.nv-line {
fill: none;
stroke-width: 1.5px;
/*
stroke-linecap: round;
shape-rendering: geometricPrecision;
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
*/
}
.nvd3 .nv-groups path.nv-line.nv-thin-line {
stroke-width: 1px;
}
.nvd3 .nv-groups path.nv-area {
stroke: none;
/*
stroke-linecap: round;
shape-rendering: geometricPrecision;
stroke-width: 2.5px;
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
*/
}
.nvd3 .nv-line.hover path {
stroke-width: 6px;
}
/*
.nvd3.scatter .groups .point {
fill-opacity: 0.1;
stroke-opacity: 0.1;
}
*/
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point {
fill-opacity: .5 !important;
stroke-opacity: .5 !important;
}
.with-transitions .nvd3 .nv-groups .nv-point {
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-scatter .nv-groups .nv-point.hover,
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 7px;
fill-opacity: .95 !important;
stroke-opacity: .95 !important;
}
.nvd3 .nv-point-paths path {
stroke: #aaa;
stroke-opacity: 0;
fill: #eee;
fill-opacity: 0;
}
.nvd3 .nv-indexLine {
cursor: ew-resize;
}
/**********
* Distribution
*/
.nvd3 .nv-distribution {
pointer-events: none;
}
/**********
* Scatter
*/
/* **Attempting to remove this for useVoronoi(false), need to see if it's required anywhere
.nvd3 .nv-groups .nv-point {
pointer-events: none;
}
*/
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .5;
}
.nvd3 .nv-scatter .nv-point.hover {
fill-opacity: 1;
}
/*
.nv-group.hover .nv-point {
fill-opacity: 1;
}
*/
/**********
* Stacked Area
*/
.nvd3.nv-stackedarea path.nv-area {
fill-opacity: .7;
/*
stroke-opacity: .65;
fill-opacity: 1;
*/
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
/*
transition-delay: 500ms;
-moz-transition-delay: 500ms;
-webkit-transition-delay: 500ms;
*/
}
.nvd3.nv-stackedarea path.nv-area.hover {
fill-opacity: .9;
/*
stroke-opacity: .85;
*/
}
/*
.d3stackedarea .groups path {
stroke-opacity: 0;
}
*/
.nvd3.nv-stackedarea .nv-groups .nv-point {
stroke-opacity: 0;
fill-opacity: 0;
}
/*
.nvd3.nv-stackedarea .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .75;
fill-opacity: 1;
}*/
/**********
* Line Plus Bar
*/
.nvd3.nv-linePlusBar .nv-bar rect {
fill-opacity: .75;
}
.nvd3.nv-linePlusBar .nv-bar rect:hover {
fill-opacity: 1;
}
/**********
* Bullet
*/
.nvd3.nv-bullet { font: 10px sans-serif; }
.nvd3.nv-bullet .nv-measure { fill-opacity: .8; }
.nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; }
.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; }
.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; }
.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; }
.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; }
.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; }
.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; }
.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; }
.nvd3.nv-bullet .nv-subtitle { fill: #999; }
.nvd3.nv-bullet .nv-range {
fill: #bababa;
fill-opacity: .4;
}
.nvd3.nv-bullet .nv-range:hover {
fill-opacity: .7;
}
/**********
* Sparkline
*/
.nvd3.nv-sparkline path {
fill: none;
}
.nvd3.nv-sparklineplus g.nv-hoverValue {
pointer-events: none;
}
.nvd3.nv-sparklineplus .nv-hoverValue line {
stroke: #333;
stroke-width: 1.5px;
}
.nvd3.nv-sparklineplus,
.nvd3.nv-sparklineplus g {
pointer-events: all;
}
.nvd3 .nv-hoverArea {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-sparklineplus .nv-xValue,
.nvd3.nv-sparklineplus .nv-yValue {
/*
stroke: #666;
*/
stroke-width: 0;
font-size: .9em;
font-weight: normal;
}
.nvd3.nv-sparklineplus .nv-yValue {
stroke: #f66;
}
.nvd3.nv-sparklineplus .nv-maxValue {
stroke: #2ca02c;
fill: #2ca02c;
}
.nvd3.nv-sparklineplus .nv-minValue {
stroke: #d62728;
fill: #d62728;
}
.nvd3.nv-sparklineplus .nv-currentValue {
/*
stroke: #444;
fill: #000;
*/
font-weight: bold;
font-size: 1.1em;
}
/**********
* historical stock
*/
.nvd3.nv-ohlcBar .nv-ticks .nv-tick {
stroke-width: 2px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover {
stroke-width: 4px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive {
stroke: #2ca02c;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative {
stroke: #d62728;
}
.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel {
font-weight: bold;
}
.nvd3.nv-historicalStockChart .nv-dragTarget {
fill-opacity: 0;
stroke: none;
cursor: move;
}
.nvd3 .nv-brush .extent {
/*
cursor: ew-resize !important;
*/
fill-opacity: 0 !important;
}
.nvd3 .nv-brushBackground rect {
stroke: #000;
stroke-width: .4;
fill: #fff;
fill-opacity: .7;
}
/**********
* Indented Tree
*/
/**
* TODO: the following 3 selectors are based on classes used in the example. I should either make them standard and leave them here, or move to a CSS file not included in the library
*/
.nvd3.nv-indentedtree .name {
margin-left: 5px;
}
.nvd3.nv-indentedtree .clickable {
color: #08C;
cursor: pointer;
}
.nvd3.nv-indentedtree span.clickable:hover {
color: #005580;
text-decoration: underline;
}
.nvd3.nv-indentedtree .nv-childrenCount {
display: inline-block;
margin-left: 5px;
}
.nvd3.nv-indentedtree .nv-treeicon {
cursor: pointer;
/*
cursor: n-resize;
*/
}
.nvd3.nv-indentedtree .nv-treeicon.nv-folded {
cursor: pointer;
/*
cursor: s-resize;
*/
}
/**********
* Parallel Coordinates
*/
.nvd3 .background path {
fill: none;
stroke: #ccc;
stroke-opacity: .4;
shape-rendering: crispEdges;
}
.nvd3 .foreground path {
fill: none;
stroke: steelblue;
stroke-opacity: .7;
}
.nvd3 .brush .extent {
fill-opacity: .3;
stroke: #fff;
shape-rendering: crispEdges;
}
.nvd3 .axis line, .axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.nvd3 .axis text {
text-shadow: 0 1px 0 #fff;
}
/****
Interactive Layer
*/
.nvd3 .nv-interactiveGuideLine {
pointer-events:none;
}
.nvd3 line.nv-guideline {
stroke: #ccc;
}

5
brewman/static/js/d3.v3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

14346
brewman/static/js/nv.d3.js Normal file

File diff suppressed because it is too large Load Diff

6
brewman/static/js/nv.d3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,70 @@
<div class="panel panel-default" ng-if="dashboard.Enabled">
<div class="panel-heading">
<div class="btn-group btn-group-xs">
<div class="col-md-4">
<div class="input-group">
<input class="form-control" id="txtStartDate" type="text" datepicker-popup="dd-MMM-yyyy"
ng-model="dashboard.StartDate"/>
<span class="input-group-btn">
<button class="btn btn-default"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
</div>
<div class="col-md-4">
<button class="btn btn-block btn-default" tan-click="getDashboard()">Show Dashboard</button>
</div>
<div class="col-md-4">
<div class="input-group">
<input type="text" id="txtFinishDate" class="form-control" datepicker-popup="dd-MMM-yyyy"
ng-model="dashboard.FinishDate"/>
<span class="input-group-btn">
<button class="btn btn-default"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default" ng-if="dashboard.Enabled && Data.HasCash">
<div class="panel-heading" ng-click="cashCollapsed = !cashCollapsed">
<div class="btn-group btn-group-xs">
<h3 class="panel-title">Cash/Bank in Hand</h3>
</div>
<span class="badge pull-right">{{Cash.Total | currency}}</span>
</div>
<div class="panel-body panel-collapse collapse" ng-class="{'in':!cashCollapsed}">
<div class="col-md-8">
<div chart-line-plus-bar="Data.Cash.LinePlusBar" id="cashLinePlusBarId">
<svg style="height: 400px;"></svg>
</div>
</div>
<div class="col-md-4">
<div chart-pie="Data.Cash.Pie" id="cashPieId">
<svg style="height: 400px;"></svg>
</div>
</div>
</div>
</div>
<div class="panel panel-default" ng-if="dashboard.Enabled && Data.HasCreditors">
<div class="panel-heading" ng-click="creditorsCollapsed = !creditorsCollapsed">
<div class="btn-group btn-group-xs">
<h3 class="panel-title">Creditors</h3>
</div>
<span class="badge pull-right">{{Creditors.Total | currency}}</span>
</div>
<div class="panel-body panel-collapse collapse" ng-class="{'in':!creditorsCollapsed}">
<div class="col-md-8">
<div chart-line-plus-bar="Data.Creditors.LinePlusBar" id="creditorsLinePlusBarId">
<svg style="height: 400px;"></svg>
</div>
</div>
<div class="col-md-4">
<div chart-pie="Data.Creditors.Pie" id="creditorsPieId">
<svg style="height: 400px;"></svg>
</div>
</div>
</div>
</div>
<h2>Messages <a href="/Message" class="btn btn-success pull-right">Add <i class="glyphicon glyphicon-plus"></i></a>
</h2>
<div class="widget-box">
@ -11,7 +78,8 @@
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li ng-repeat="(name, count) in tags"><a href="#"><span class="badge pull-right">{{count}}</span>{{name}}</a></li>
<li ng-repeat="(name, count) in tags"><a href="#"><span class="badge pull-right">{{count}}</span>{{name}}</a>
</li>
</ul>
</li>
</ul>

View File

@ -200,3 +200,144 @@ overlord_directive.directive('tanClick', ['$parse', '$timeout', function ($parse
}
};
}]);
overlord_directive.directive('chartDiscreteBar', ['$parse', function ($parse) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
nv.addGraph(function () {
var chart = nv.models.discreteBarChart()
.x(function (d) {return d.label;})
.y(function (d) {return d.value;})
.staggerLabels(true)
.tooltips(false)
.showValues(true);
var data = $parse(attrs.chartDiscreteBar)(scope);
d3.select('#' + attrs.id + ' svg')
.datum(data)
.transition().duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
}
};
}]);
overlord_directive.directive('chartMultiBar', ['$parse', function ($parse) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
nv.addGraph(function () {
var chart = nv.models.multiBarChart();
var data = $parse(attrs.chartMultiBar)(scope);
d3.select('#' + attrs.id + ' svg')
.datum(data)
.transition().duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
}
};
}]);
overlord_directive.directive('chartLine', ['$parse', function ($parse) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
nv.addGraph(function () {
var chart = nv.models.lineChart();
chart.xAxis.axisLabel('Date').tickFormat(function (d) {
return d3.time.format('%b %d')(new Date(d));
});
var data = $parse(attrs.chartLine)(scope);
d3.select('#' + attrs.id + ' svg')
.datum(data)
.transition().duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
}
};
}]);
overlord_directive.directive('chartPie', ['$parse', function ($parse) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var chart;
scope.$watch(attrs.chartPie, function (data) {
if (!angular.isUndefined(chart)) {
d3.select('#' + attrs.id + ' svg')
.datum(data)
.transition().duration(1200)
.call(chart);
return chart;
}
nv.addGraph(function () {
chart = nv.models.pieChart()
.x(function (d) {return d.label;})
.y(function (d) {return d.value;})
.showLabels(true);
// var data = $parse(attrs.chartPie)(scope);
d3.select('#' + attrs.id + ' svg')
.datum(data)
.transition().duration(1200)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
});
}
};
}]);
overlord_directive.directive('chartLinePlusBar', ['$parse', function ($parse) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var chart;
scope.$watch(attrs.chartLinePlusBar, function (data) {
if (!angular.isUndefined(chart)) {
d3.select('#' + attrs.id + ' svg')
.datum(data)
.transition().duration(500)
.call(chart);
return chart;
}
nv.addGraph(function () {
chart = nv.models.linePlusBarChart()
.x(function (d) {return d.label;})
.y(function (d) {return d.value;});
chart.xAxis.axisLabel('Date').tickFormat(function (d) {
return d3.time.format('%b %d')(new Date(d));
});
chart.bars.forceY([0]);
// var data = $parse(attrs.chartLinePlusBar)(scope);
d3.select('#' + attrs.id + ' svg')
.datum(data)
.transition().duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
});
}
};
}]);

View File

@ -1,7 +1,8 @@
'use strict';
var HomeCtrl = ['$scope', '$location', 'messages', 'Message', function ($scope, $location, messages, Message) {
var HomeCtrl = ['$scope', '$location', '$http', 'dateFilter', 'messages', 'dashboard', 'Message', function ($scope, $location, $http, dateFilter, messages, dashboard, Message) {
$scope.chosen = messages.Type;
$scope.dashboard = dashboard;
$scope.info = messages.Threads;
$scope.tags = messages.Tags;
$scope.getMessages = function (type) {
@ -18,9 +19,98 @@ var HomeCtrl = ['$scope', '$location', 'messages', 'Message', function ($scope,
$scope.chosen = result.Type;
});
};
$scope.getDashboard = function () {
var start_date = angular.isDate($scope.dashboard.StartDate) ? dateFilter($scope.dashboard.StartDate, 'dd-MMM-yyyy') : $scope.dashboard.StartDate;
var finish_date = angular.isDate($scope.dashboard.FinishDate) ? dateFilter($scope.dashboard.FinishDate, 'dd-MMM-yyyy') : $scope.dashboard.FinishDate;
return $http({ method: 'GET', url: '/api/Dashboard', params: {s: start_date, f: finish_date}}).then(function (data, status, headers, config) {
$scope.dashboard = data.data;
formatData();
});
};
function formatData() {
$scope.Data = {};
if ('Cash' in $scope.dashboard) {
$scope.Data.HasCash = true;
$scope.Data.Cash = {
Pie: _.chain($scope.dashboard.Cash.Details)
.reduce(function (accumulator, value, index, collection) {
if (accumulator.length < 4) {
accumulator.push(value);
} else if (accumulator.length === 4) {
accumulator.push({Name: 'Others', Amount: value.Amount});
} else {
accumulator[4].Amount += value.Amount;
}
return accumulator;
}, [])
.map(function (item) {
return {label: item.Name.substr(0, 5), value: item.Amount};
})
.value(),
LinePlusBar: [
{
key: "Total Cash",
values: _.map($scope.dashboard.Cash.Daily, function (item) {
return {label: new Date(item.Date).getTime(), value: item.Amount};
})
},
{
key: "Cash Flow",
bar: true,
values: _.map($scope.dashboard.Cash.Changes, function (item) {
return {label: new Date(item.Date).getTime(), value: item.Amount};
})
}
],
Total: _.last($scope.dashboard.Cash.Daily).Amount
};
}
if ('Creditors' in $scope.dashboard) {
$scope.Data.HasCreditors = true;
$scope.Data.Creditors = {
Pie: _.chain($scope.dashboard.Creditors.Details)
.reduceRight(function (accumulator, value, index, collection) {
if (accumulator.length < 4) {
accumulator.push(value);
} else if (accumulator.length === 4) {
accumulator.push({Name: 'Others', Amount: value.Amount});
} else {
accumulator[4].Amount += value.Amount;
}
return accumulator;
}, [])
.map(function (item) {
return {label: item.Name.substr(0, 5), value: item.Amount * -1};
})
.value(),
LinePlusBar: [
{
key: "Total Creditors",
values: _.map($scope.dashboard.Creditors.Daily, function (item) {
return {label: new Date(item.Date).getTime(), value: item.Amount * -1};
})
},
{
key: "Change in Creditors",
bar: true,
values: _.map($scope.dashboard.Creditors.Changes, function (item) {
return {label: new Date(item.Date).getTime(), value: item.Amount * -1};
})
}
],
Total: _.last($scope.dashboard.Creditors.Daily).Amount * -1
};
}
}
formatData();
}];
HomeCtrl.resolve = {
messages: ['Message', function (Message) {
return Message.query({type: 'open'}).$promise;
}]
}],
dashboard: ['$http', function ($http) {
return $http.get('/api/Dashboard', {}).then(function (data, status, headers, config) {
return data.data;
});
} ]
};

View File

@ -17,6 +17,7 @@
<link rel="stylesheet" type="text/css" media="screen" href="/css/table.css"/>
<link rel="stylesheet" type="text/css" media="screen" href="/css/chosen.css"/>
<link rel="stylesheet" type="text/css" media="screen" href="/css/loading-bar.min.css"/>
<link rel="stylesheet" type="text/css" media="screen" href="/css/nv.d3.css"/>
<style type="text/css">
body {
padding-top: 60px;
@ -32,6 +33,8 @@
<script src="/js/ace/ace.js"></script>
<script src="/js/showdown.min.js"></script>
<script src="/js/chosen.min.js"></script>
<script src="/js/d3.v3.min.js"></script>
<script src="/js/nv.d3.js"></script>
<script src="/js/angular.min-1.2.3.js"></script>
<script src="/js/angular-locale_en-in-1.2.3.js"></script>

View File

@ -7,7 +7,7 @@ from sqlalchemy import func, and_
from sqlalchemy.orm import aliased
import transaction
from brewman.models import DBSession
from brewman.models.master import LedgerBase, CostCenter, Product
from brewman.models.master import LedgerBase, CostCenter, Product, LedgerType
from brewman.models.validation_exception import TryCatchFunction
from brewman.models.voucher import Journal, Voucher, VoucherType, Batch, Inventory, SalaryDeduction, Fingerprint, Attendance
@ -50,6 +50,8 @@ def opening_ledgers(date, user_id):
type=VoucherType.by_name('Opening Ledgers'))
for ledger, amount in query:
amount = round(Decimal(amount), 2)
if not ledger.type_object().balance_sheet:
pass
if amount != 0:
running_total += amount
journal = Journal(amount=abs(amount), debit=-1 if amount < 0 else 1, ledger_id=ledger.id,

View File

@ -42,7 +42,7 @@ def set_lock_info(request):
lock_date = DbSetting(name='Lock Info', data=data)
DBSession.add(lock_date)
transaction.commit()
return {}
return get_lock_info(request)
@view_config(request_method='DELETE', route_name='api_lock_info', renderer='json', permission='Lock Date')

View File

@ -0,0 +1,96 @@
from pyramid.security import authenticated_userid
from pyramid.view import view_config
from sqlalchemy import func
from brewman import groupfinder
from brewman.models import DBSession
from brewman.models.master import LedgerBase
from brewman.models.validation_exception import TryCatchFunction
from brewman.models.voucher import Voucher, Journal, VoucherType
from brewman.views.services.session import session_period_finish, session_period_start, session_current_date
@view_config(route_name='dashboard', renderer='brewman:templates/angular_base.mako', permission='Authenticated')
def html(request):
return {}
@view_config(route_name='api_dashboard', renderer='json')
@TryCatchFunction
def dashboard(request):
user = authenticated_userid(request)
if user is None or 'Dashboard' not in groupfinder(user, request):
return {'Enabled': False}
start_date = request.GET.get('s', session_period_start(request))
finish_date = request.GET.get('f', session_period_finish(request))
dash = {'Enabled': True, 'StartDate': start_date, 'FinishDate': finish_date}
if 'Cash Flow' in groupfinder(user, request):
cash_details, cash_changes, cash_daily = get_ledger_type_info(1, start_date, finish_date)
dash['Cash'] = {'Details': cash_details, 'Daily': cash_daily, 'Changes': cash_changes}
if 'Balance Sheet' in groupfinder(user, request):
creditors_details, creditors_changes, creditors_daily = get_ledger_type_info(9, start_date, finish_date)
dash['Creditors'] = {'Details': creditors_details, 'Daily': creditors_daily, 'Changes': creditors_changes}
if 'Profit & Loss' in groupfinder(user, request):
sale_details, sale_changes, sale_daily = get_ledger_type_info(3, start_date, finish_date)
dash['Sale'] = {'Details': sale_details, 'Daily': sale_daily, 'Changes': sale_changes}
return dash
def get_sale(start_date, finish_date):
SALE_LEDGER_TYPE = 3
amount_sum = func.sum(Journal.amount * Journal.debit).label('sum')
daily_sale = DBSession.query(Voucher.date, amount_sum) \
.join(Journal.voucher).join(Journal.ledger) \
.filter(Voucher.date >= start_date) \
.filter(Voucher.date <= finish_date) \
.filter(Voucher.type != VoucherType.by_name('Issue').id) \
.filter(LedgerBase.type == SALE_LEDGER_TYPE) \
.group_by(Voucher.date).order_by(Voucher.date).all()
total_sale = 0
days = 0
for date, sale in daily_sale:
days += 1
total_sale += sale
return round(total_sale * -1, 2), round(total_sale / days * -1, 2), days
def get_ledger_type_info(ledger_type, start_date, finish_date):
amount_sum = func.sum(Journal.amount * Journal.debit).label('sum')
query = DBSession.query(LedgerBase, amount_sum) \
.join(Journal.voucher).join(Journal.ledger) \
.filter(Voucher.date <= finish_date) \
.filter(Voucher.type != VoucherType.by_name('Issue').id) \
.filter(LedgerBase.type == ledger_type) \
.group_by(LedgerBase).order_by(amount_sum.desc()).all()
details = []
for ledger, amount in query:
if round(amount, 2) != 0:
details.append({'Name': ledger.name, 'Amount': round(amount, 2)})
running = DBSession.query(amount_sum) \
.join(Journal.voucher).join(Journal.ledger) \
.filter(Voucher.date < start_date) \
.filter(Voucher.type != VoucherType.by_name('Issue').id) \
.filter(LedgerBase.type == ledger_type) \
.scalar()
running = 0 if running is None else round(running, 2)
query = DBSession.query(Voucher.date, amount_sum) \
.join(Journal.voucher).join(Journal.ledger) \
.filter(Voucher.date >= start_date) \
.filter(Voucher.date <= finish_date) \
.filter(Voucher.type != VoucherType.by_name('Issue').id) \
.filter(LedgerBase.type == ledger_type) \
.group_by(Voucher.date).order_by(Voucher.date).all()
daily = []
changes = []
for date, amount in query:
changes.append({'Date': date.strftime('%d-%b-%Y'), 'Amount': round(amount, 2)})
running += round(amount, 2)
daily.append({'Date': date.strftime('%d-%b-%Y'), 'Amount': round(running, 2)})
return details, changes, daily