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:
parent
e5f8a4f86f
commit
89e1443b48
brewman
@ -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')
|
||||
|
769
brewman/static/css/nv.d3.css
Normal file
769
brewman/static/css/nv.d3.css
Normal 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
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
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
6
brewman/static/js/nv.d3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
|
141
brewman/static/scripts/angular_directive.js
vendored
141
brewman/static/scripts/angular_directive.js
vendored
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
} ]
|
||||
};
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
96
brewman/views/dashboard.py
Normal file
96
brewman/views/dashboard.py
Normal 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
|
Loading…
Reference in New Issue
Block a user