Ver código fonte

Graphs, and target notifications (#3641)

* [sales goal] in company; graph, notifs

* cleanup notifications.js, summary and specific val in graph

* Add line graph, add goal data methods in goal.py

* [tests] targets in notification config

* [minor] type of graph as argument in parent

* Update graph docs

* remove company dependent test for notification

* [fix] test

* look for monthly history in cache

* check for cached graph data in field
version-14
Prateeksha Singh 8 anos atrás
committed by Makarand Bauskar
pai
commit
b602a07c7a
17 arquivos alterados com 985 adições e 144 exclusões
  1. +8
    -5
      frappe/desk/doctype/event/test_records.json
  2. +47
    -1
      frappe/desk/notifications.py
  3. BIN
      frappe/docs/assets/img/desk/bar_graph.png
  4. BIN
      frappe/docs/assets/img/desk/line_graph.png
  5. +61
    -0
      frappe/docs/user/en/guides/desk/making_graphs.md
  6. +1
    -0
      frappe/public/build.json
  7. +11
    -0
      frappe/public/css/desk.css
  8. +86
    -0
      frappe/public/css/form.css
  9. +48
    -5
      frappe/public/js/frappe/form/dashboard.js
  10. +1
    -1
      frappe/public/js/frappe/form/templates/form_dashboard.html
  11. +308
    -0
      frappe/public/js/frappe/ui/graph.js
  12. +103
    -116
      frappe/public/js/frappe/ui/toolbar/notifications.js
  13. +1
    -1
      frappe/public/js/legacy/form.js
  14. +31
    -15
      frappe/public/less/desk.less
  15. +117
    -0
      frappe/public/less/form.less
  16. +34
    -0
      frappe/tests/test_goal.py
  17. +128
    -0
      frappe/utils/goal.py

+ 8
- 5
frappe/desk/doctype/event/test_records.json Ver arquivo

@@ -3,18 +3,21 @@
"doctype": "Event",
"subject":"_Test Event 1",
"starts_on": "2014-01-01",
"event_type": "Public"
"event_type": "Public",
"creation": "2014-01-01"
},
{
"doctype": "Event",
"starts_on": "2014-01-01",
"subject":"_Test Event 2",
"event_type": "Private"
"starts_on": "2014-01-01",
"event_type": "Private",
"creation": "2014-01-01"
},
{
"doctype": "Event",
"starts_on": "2014-01-01",
"subject": "_Test Event 3",
"event_type": "Private"
"starts_on": "2014-02-01",
"event_type": "Private",
"creation": "2014-02-01"
}
]

+ 47
- 1
frappe/desk/notifications.py Ver arquivo

@@ -13,10 +13,12 @@ def get_notifications():
return

config = get_notification_config()

groups = config.get("for_doctype").keys() + config.get("for_module").keys()
cache = frappe.cache()

notification_count = {}
notification_percent = {}

for name in groups:
count = cache.hget("notification_count:" + name, frappe.session.user)
@@ -27,6 +29,7 @@ def get_notifications():
"open_count_doctype": get_notifications_for_doctypes(config, notification_count),
"open_count_module": get_notifications_for_modules(config, notification_count),
"open_count_other": get_notifications_for_other(config, notification_count),
"targets": get_notifications_for_targets(config, notification_percent),
"new_messages": get_new_messages()
}

@@ -111,6 +114,49 @@ def get_notifications_for_doctypes(config, notification_count):

return open_count_doctype

def get_notifications_for_targets(config, notification_percent):
"""Notifications for doc targets"""
can_read = frappe.get_user().get_can_read()
doc_target_percents = {}

# doc_target_percents = {
# "Company": {
# "Acme": 87,
# "RobotsRUs": 50,
# }, {}...
# }

for doctype in config.targets:
if doctype in can_read:
if doctype in notification_percent:
doc_target_percents[doctype] = notification_percent[doctype]
else:
doc_target_percents[doctype] = {}
d = config.targets[doctype]
condition = d["filters"]
target_field = d["target_field"]
value_field = d["value_field"]
try:
if isinstance(condition, dict):
doc_list = frappe.get_list(doctype, fields=["name", target_field, value_field],
filters=condition, limit_page_length = 100, ignore_ifnull=True)

except frappe.PermissionError:
frappe.clear_messages()
pass
except Exception as e:
if e.args[0]!=1412:
raise

else:
for doc in doc_list:
value = doc[value_field]
target = doc[target_field]
doc_target_percents[doctype][doc.name] = (value/target * 100) if value < target else 100

return doc_target_percents


def clear_notifications(user=None):
if frappe.flags.in_install:
return
@@ -163,7 +209,7 @@ def get_notification_config():
config = frappe._dict()
for notification_config in frappe.get_hooks().notification_config:
nc = frappe.get_attr(notification_config)()
for key in ("for_doctype", "for_module", "for_other"):
for key in ("for_doctype", "for_module", "for_other", "targets"):
config.setdefault(key, {})
config[key].update(nc.get(key, {}))
return config


BIN
frappe/docs/assets/img/desk/bar_graph.png Ver arquivo

Antes Depois
Largura: 776  |  Altura: 296  |  Tamanho: 29 KiB

BIN
frappe/docs/assets/img/desk/line_graph.png Ver arquivo

Antes Depois
Largura: 776  |  Altura: 295  |  Tamanho: 39 KiB

+ 61
- 0
frappe/docs/user/en/guides/desk/making_graphs.md Ver arquivo

@@ -0,0 +1,61 @@
# Making Graphs

The Frappe UI **Graph** object enables you to render simple line and bar graphs for a discreet set of data points. You can also set special checkpoint values and summary stats.

### Example: Line graph
Here's is an example of a simple sales graph:

render_graph: function() {
$('.form-graph').empty();

var months = ['Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
var values = [2410, 3100, 1700, 1200, 2700, 1600, 2740, 1000, 850, 1500, 400, 2013];

var goal = 2500;
var current_val = 2013;

new frappe.ui.Graph({
parent: $('.form-graph'),
width: 700,
height: 140,
mode: 'line-graph',

title: 'Sales',
subtitle: 'Monthly',
y_values: values,
x_points: months,

specific_values: [
{
name: "Goal",
line_type: "dashed", // "dashed" or "solid"
value: goal
},
],
summary_values: [
{
name: "This month",
color: 'green', // Indicator colors: 'grey', 'blue', 'red',
// 'green', 'orange', 'purple', 'darkgrey',
// 'black', 'yellow', 'lightblue'
value: '₹ ' + current_val
},
{
name: "Goal",
color: 'blue',
value: '₹ ' + goal
},
{
name: "Completed",
color: 'green',
value: (current_val/goal*100).toFixed(1) + "%"
}
]
});
},

<img src="{{docs_base_url}}/assets/img/desk/line_graph.png" class="screenshot">

Setting the mode to 'bar-graph':

<img src="{{docs_base_url}}/assets/img/desk/bar_graph.png" class="screenshot">

+ 1
- 0
frappe/public/build.json Ver arquivo

@@ -161,6 +161,7 @@
"public/js/frappe/query_string.js",

"public/js/frappe/ui/charts.js",
"public/js/frappe/ui/graph.js",
"public/js/frappe/misc/rating_icons.html",

"public/js/frappe/feedback.js"


+ 11
- 0
frappe/public/css/desk.css Ver arquivo

@@ -508,6 +508,17 @@ fieldset[disabled] .form-control {
cursor: pointer;
margin-right: 10px;
}
a.progress-small .progress-chart {
width: 60px;
margin-top: 4px;
float: right;
}
a.progress-small .progress {
margin-bottom: 0;
}
a.progress-small .progress-bar {
background-color: #98d85b;
}
/* on small screens, show only icons on top */
@media (max-width: 767px) {
.module-view-layout .nav-stacked > li {


+ 86
- 0
frappe/public/css/form.css Ver arquivo

@@ -642,6 +642,92 @@ select.form-control {
box-shadow: none;
}
}
/* goals */
.goals-page-container {
background-color: #fafbfc;
padding-top: 1px;
}
.goals-page-container .goal-container {
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
border-radius: 2px;
padding: 10px;
margin: 10px;
}
.graph-container .graphics {
margin-top: 10px;
padding: 10px 0px;
}
.graph-container .stats-group {
display: flex;
justify-content: space-around;
flex: 1;
}
.graph-container .stats-container {
display: flex;
justify-content: space-around;
}
.graph-container .stats-container .stats {
padding-bottom: 15px;
}
.graph-container .stats-container .stats-title {
color: #8D99A6;
}
.graph-container .stats-container .stats-value {
font-size: 20px;
font-weight: 300;
}
.graph-container .stats-container .stats-description {
font-size: 12px;
color: #8D99A6;
}
.graph-container .stats-container .graph-data .stats-value {
color: #98d85b;
}
.bar-graph .axis,
.line-graph .axis {
font-size: 10px;
fill: #6a737d;
}
.bar-graph .axis line,
.line-graph .axis line {
stroke: rgba(27, 31, 35, 0.1);
}
.data-points circle {
fill: #28a745;
stroke: #fff;
stroke-width: 2;
}
.data-points g.mini {
fill: #98d85b;
}
.data-points path {
fill: none;
stroke: #28a745;
stroke-opacity: 1;
stroke-width: 2px;
}
.line-graph .path {
fill: none;
stroke: #28a745;
stroke-opacity: 1;
stroke-width: 2px;
}
line.dashed {
stroke-dasharray: 5,3;
}
.tick.x-axis-label {
display: block;
}
.tick .specific-value {
text-anchor: start;
}
.tick .y-value-text {
text-anchor: end;
}
.tick .x-value-text {
text-anchor: middle;
}
body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] {
height: 80px !important;
}

+ 48
- 5
frappe/public/js/frappe/form/dashboard.js Ver arquivo

@@ -11,7 +11,7 @@ frappe.ui.form.Dashboard = Class.extend({

this.progress_area = this.wrapper.find(".progress-area");
this.heatmap_area = this.wrapper.find('.form-heatmap');
this.chart_area = this.wrapper.find('.form-chart');
this.graph_area = this.wrapper.find('.form-graph');
this.stats_area = this.wrapper.find('.form-stats');
this.stats_area_row = this.stats_area.find('.row');
this.links_area = this.wrapper.find('.form-links');
@@ -43,9 +43,9 @@ frappe.ui.form.Dashboard = Class.extend({
this.frm.layout.show_message();
},

add_comment: function(text, permanent) {
add_comment: function(text, alert_class, permanent) {
var me = this;
this.set_headline_alert(text);
this.set_headline_alert(text, alert_class);
if(!permanent) {
setTimeout(function() {
me.clear_headline();
@@ -91,6 +91,7 @@ frappe.ui.form.Dashboard = Class.extend({

this.show();
},

format_percent: function(title, percent) {
var width = cint(percent) < 1 ? 1 : cint(percent);
var progress_class = "";
@@ -138,6 +139,11 @@ frappe.ui.form.Dashboard = Class.extend({
show = true;
}

if(this.data.graph) {
this.setup_graph();
show = true;
}

if(show) {
this.show();
}
@@ -383,13 +389,50 @@ frappe.ui.form.Dashboard = Class.extend({
},

//graphs
setup_graph: function() {
var me = this;

var method = this.data.graph_method;
var args = {
doctype: this.frm.doctype,
docname: this.frm.doc.name,
};

$.extend(args, this.data.graph_method_args);

frappe.call({
type: "GET",
method: method,
args: args,

callback: function(r) {
if(r.message) {
me.render_graph(r.message);
}
}
});
},

render_graph: function(args) {
var me = this;
this.graph_area.empty().removeClass('hidden');
$.extend(args, {
parent: me.graph_area,
width: 700,
height: 140,
mode: 'line-graph'
});

new frappe.ui.Graph(args);
},

setup_chart: function(opts) {
var me = this;

this.chart_area.removeClass('hidden');
this.graph_area.removeClass('hidden');

$.extend(opts, {
wrapper: me.wrapper.find('.form-chart'),
wrapper: me.graph_area,
padding: {
right: 30,
bottom: 30


+ 1
- 1
frappe/public/js/frappe/form/templates/form_dashboard.html Ver arquivo

@@ -5,7 +5,7 @@
<div id="heatmap-{{ frappe.model.scrub(frm.doctype) }}"></div>
<div class="text-muted small heatmap-message hidden"></div>
</div>
<div class="form-chart form-dashboard-section hidden"></div>
<div class="form-graph form-dashboard-section hidden"></div>
<div class="form-stats form-dashboard-section hidden">
<div class="row"></div>
</div>


+ 308
- 0
frappe/public/js/frappe/ui/graph.js Ver arquivo

@@ -0,0 +1,308 @@
// specific_values = [
// {
// name: "Average",
// line_type: "dashed", // "dashed" or "solid"
// value: 10
// },

// summary_values = [
// {
// name: "Total",
// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange',
// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue'
// value: 80
// }
// ]

frappe.ui.Graph = class Graph {
constructor({
parent = null,

width = 0, height = 0,
title = '', subtitle = '',

y_values = [],
x_points = [],

specific_values = [],
summary_values = [],

color = '',
mode = '',
} = {}) {

if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) {
if(mode === 'line-graph') {
return new frappe.ui.LineGraph(arguments[0]);
} else if(mode === 'bar-graph') {
return new frappe.ui.BarGraph(arguments[0]);
}
}

this.parent = parent;

this.width = width;
this.height = height;

this.title = title;
this.subtitle = subtitle;

this.y_values = y_values;
this.x_points = x_points;

this.specific_values = specific_values;
this.summary_values = summary_values;

this.color = color;
this.mode = mode;

this.$graph = null;

frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this));
}

setup() {
this.setup_container();
this.refresh();
}

refresh() {
this.setup_values();
this.setup_components();
this.make_y_axis();
this.make_x_axis();
this.make_units();
if(this.specific_values.length > 0) {
this.show_specific_values();
}
this.setup_group();

if(this.summary_values.length > 0) {
this.show_summary();
}
}

setup_container() {
this.container = $('<div>')
.addClass('graph-container')
.append($(`<h6 class="title" style="margin-top: 15px;">${this.title}</h6>`))
.append($(`<h6 class="sub-title uppercase">${this.subtitle}</h6>`))
.append($(`<div class="graphics"></div>`))
.append($(`<div class="stats-container"></div>`))
.appendTo(this.parent);

let $graphics = this.container.find('.graphics');
this.$stats_container = this.container.find('.stats-container');

this.$graph = $('<div>')
.addClass(this.mode)
.appendTo($graphics);

this.$svg = $(`<svg class="svg" width="${this.width}" height="${this.height}"></svg>`);
this.$graph.append(this.$svg);

this.snap = new Snap(this.$svg[0]);
}

setup_values() {
this.upper_graph_bound = this.get_upper_limit_and_parts(this.y_values)[0];
this.y_axis = this.get_y_axis(this.y_values);
this.avg_unit_width = (this.width-50)/(this.x_points.length - 1);
}

setup_components() {
this.y_axis_group = this.snap.g().attr({
class: "y axis"
});

this.x_axis_group = this.snap.g().attr({
class: "x axis"
});

this.graph_list = this.snap.g().attr({
class: "data-points",
});

this.specific_y_lines = this.snap.g().attr({
class: "specific axis",
});
}

setup_group() {
this.snap.g(
this.y_axis_group,
this.x_axis_group,
this.graph_list,
this.specific_y_lines
).attr({
transform: "translate(40, 10)" // default
});
}

show_specific_values() {
this.specific_values.map(d => {
this.specific_y_lines.add(this.snap.g(
this.snap.line(0, 0, this.width - 50, 0).attr({
class: d.line_type === "dashed" ? "dashed": ""
}),
this.snap.text(this.width - 100, 0, d.name.toUpperCase()).attr({
dy: ".32em",
class: "specific-value",
})
).attr({
class: "tick",
transform: `translate(0, ${100 - 100/(this.upper_graph_bound/d.value) })`
}));
});
}

show_summary() {
this.summary_values.map(d => {
this.$stats_container.append($(`<div class="stats">
<span class="indicator ${d.color}">${d.name}: ${d.value}</span>
</div>`));
});
}

// Helpers
get_upper_limit_and_parts(array) {
let specific_values = this.specific_values.map(d => d.value);
let max_val = Math.max(...array, ...specific_values);
if((max_val+"").length <= 1) {
return [10, 5];
} else {
let multiplier = Math.pow(10, ((max_val+"").length - 1));
let significant = Math.ceil(max_val/multiplier);
if(significant % 2 !== 0) significant++;
let parts = (significant < 5) ? significant : significant/2;
return [significant * multiplier, parts];
}
}

get_y_axis(array) {
let upper_limit, parts;
[upper_limit, parts] = this.get_upper_limit_and_parts(array);
let y_axis = [];
for(var i = 0; i <= parts; i++){
y_axis.push(upper_limit / parts * i);
}
return y_axis;
}
};

frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
}

setup_values() {
super.setup_values();
this.avg_unit_width = (this.width-50)/(this.x_points.length + 2);
}

make_y_axis() {
this.y_axis.map((point) => {
this.y_axis_group.add(this.snap.g(
this.snap.line(0, 0, this.width, 0),
this.snap.text(-3, 0, point+"").attr({
dy: ".32em",
class: "y-value-text"
})
).attr({
class: "tick",
transform: `translate(0, ${100 - (100/(this.y_axis.length-1) * this.y_axis.indexOf(point)) })`
}));
});
}

make_x_axis() {
this.x_axis_group.attr({
transform: "translate(0,100)"
});
this.x_points.map((point, i) => {
this.x_axis_group.add(this.snap.g(
this.snap.line(0, 0, 0, 6),
this.snap.text(0, 9, point).attr({
dy: ".71em",
class: "x-value-text"
})
).attr({
class: "tick x-axis-label",
transform: `translate(${ ((this.avg_unit_width - 5)*3/2) + i * (this.avg_unit_width + 5) }, 0)`
}));
});
}

make_units() {
this.y_values.map((value, i) => {
this.graph_list.add(this.snap.g(
this.snap.rect(
0,
(100 - 100/(this.upper_graph_bound/value)),
this.avg_unit_width - 5,
100/(this.upper_graph_bound/value)
)
).attr({
class: "bar mini",
transform: `translate(${ (this.avg_unit_width - 5) + i * (this.avg_unit_width + 5) }, 0)`,
}));
});
}
};

frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
}

make_y_axis() {
this.y_axis.map((point) => {
this.y_axis_group.add(this.snap.g(
this.snap.line(0, 0, -6, 0),
this.snap.text(-9, 0, point+"").attr({
dy: ".32em",
class: "y-value-text"
})
).attr({
class: "tick",
transform: `translate(0, ${100 - (100/(this.y_axis.length-1)
* this.y_axis.indexOf(point)) })`
}));
});
}

make_x_axis() {
this.x_axis_group.attr({
transform: "translate(0,-7)"
});
this.x_points.map((point, i) => {
this.x_axis_group.add(this.snap.g(
this.snap.line(0, 0, 0, this.height - 25),
this.snap.text(0, this.height - 15, point).attr({
dy: ".71em",
class: "x-value-text"
})
).attr({
class: "tick",
transform: `translate(${ i * this.avg_unit_width }, 0)`
}));
});
}

make_units() {
let points_list = [];
this.y_values.map((value, i) => {
let x = i * this.avg_unit_width;
let y = (100 - 100/(this.upper_graph_bound/value));
this.graph_list.add(this.snap.circle( x, y, 4));
points_list.push(x+","+y);
});

this.make_path("M"+points_list.join("L"));
}

make_path(path_str) {
this.graph_list.prepend(this.snap.path(path_str));
}

};

+ 103
- 116
frappe/public/js/frappe/ui/toolbar/notifications.js Ver arquivo

@@ -1,125 +1,112 @@
frappe.provide("frappe.ui.notifications")

frappe.ui.notifications.update_notifications = function() {
frappe.ui.notifications.total = 0;
var doctypes = Object.keys(frappe.boot.notification_info.open_count_doctype).sort();
var modules = Object.keys(frappe.boot.notification_info.open_count_module).sort();
var other = Object.keys(frappe.boot.notification_info.open_count_other).sort();

// clear toolbar / sidebar notifications
frappe.ui.notifications.dropdown_notification = $("#dropdown-notification").empty();

// add these first.
frappe.ui.notifications.add_notification("Comment");
frappe.ui.notifications.add_notification("ToDo");
frappe.ui.notifications.add_notification("Event");

// add other
$.each(other, function(i, name) {
frappe.ui.notifications.add_notification(name, frappe.boot.notification_info.open_count_other);
});


// add a divider
if(frappe.ui.notifications.total) {
var divider = '<li class="divider"></li>';
frappe.ui.notifications.dropdown_notification.append($(divider));
}

// add to toolbar and sidebar
$.each(doctypes, function(i, doctype) {
if(!in_list(["ToDo", "Comment", "Event"], doctype)) {
frappe.ui.notifications.add_notification(doctype);
}
});

// set click events
$("#dropdown-notification a").on("click", function() {
var doctype = $(this).attr("data-doctype");
var config = frappe.ui.notifications.config[doctype] || {};
if (config.route) {
frappe.set_route(config.route);
} else if (config.click) {
config.click();
} else {
frappe.views.show_open_count_list(this);
}
});

// switch colour on the navbar and disable if no notifications
$(".navbar-new-comments")
.html(frappe.ui.notifications.total > 20 ? '20+' : frappe.ui.notifications.total)
.toggleClass("navbar-new-comments-true", frappe.ui.notifications.total ? true : false)
.parent().toggleClass("disabled", frappe.ui.notifications.total ? false : true);

}

frappe.ui.notifications.add_notification = function(doctype, notifications_map) {
if(!notifications_map) {
notifications_map = frappe.boot.notification_info.open_count_doctype;
}
frappe.provide("frappe.ui.notifications");

frappe.ui.notifications = {
config: {
"ToDo": { label: __("To Do") },
"Chat": { label: __("Chat"), route: "chat"},
"Event": { label: __("Calendar"), route: "List/Event/Calendar" },
"Email": { label: __("Email"), route: "List/Communication/Inbox" },
"Likes": { label: __("Likes"),
click: function() {
frappe.route_options = { show_likes: true };
if (frappe.get_route()[0]=="activity") {
frappe.pages['activity'].page.list.refresh();
} else {
frappe.set_route("activity");
}
}
},
},

var count = notifications_map[doctype];
if(count) {
var config = frappe.ui.notifications.config[doctype] || {};
var label = config.label || doctype;
var notification_row = repl('<li><a class="badge-hover" data-doctype="%(data_doctype)s">\
<span class="badge pull-right">\
%(count)s</span> \
%(label)s </a></li>', {
label: __(label),
count: count > 20 ? '20+' : count,
data_doctype: doctype
update_notifications: function() {
this.total = 0;
this.dropdown = $("#dropdown-notification").empty();
this.boot_info = frappe.boot.notification_info;
let defaults = ["Comment", "ToDo", "Event"];

this.get_counts(this.boot_info.open_count_doctype, 0, defaults);
this.get_counts(this.boot_info.open_count_other, 1);

// Target counts are stored for docs per doctype
let targets = { doctypes : {} }, map = this.boot_info.targets;
Object.keys(map).map(doctype => {
Object.keys(map[doctype]).map(doc => {
targets[doc] = map[doctype][doc];
targets.doctypes[doc] = doctype;
});
});
this.get_counts(targets, 1, null, ["doctypes"], true);
this.get_counts(this.boot_info.open_count_doctype,
0, null, defaults);

this.bind_list();

// switch colour on the navbar and disable if no notifications
$(".navbar-new-comments")
.html(this.total > 20 ? '20+' : this.total)
.toggleClass("navbar-new-comments-true", this.total ? true : false)
.parent().toggleClass("disabled", this.total ? false : true);
},

frappe.ui.notifications.dropdown_notification.append($(notification_row));

frappe.ui.notifications.total += count;
}
}
get_counts: function(map, divide, keys, excluded = [], target = false) {
keys = keys ? keys
: Object.keys(map).sort().filter(e => !excluded.includes(e));
keys.map(key => {
let doc_dt = (map.doctypes) ? map.doctypes[key] : undefined;
if(map[key] > 0) {
this.add_notification(key, map[key], doc_dt, target);
}
});
if(divide)
this.dropdown.append($('<li class="divider"></li>'));
},

// default notification config
frappe.ui.notifications.config = {
"ToDo": { label: __("To Do") },
"Chat": { label: __("Chat"), route: "chat"},
"Event": { label: __("Calendar"), route: "List/Event/Calendar" },
"Email": { label: __("Email"), route: "List/Communication/Inbox" },
"Likes": {
label: __("Likes"),
click: function() {
frappe.route_options = {
show_likes: true
};
add_notification: function(name, value, doc_dt, target = false) {
let label = this.config[name] ? this.config[name].label : name;
let $list_item = !target
? $(`<li><a class="badge-hover" data-doctype="${name}">${label}
<span class="badge pull-right">${value}</span>
</a></li>`)
: $(`<li><a class="progress-small" data-doctype="${doc_dt}"
data-doc="${name}">${label}
<div class="progress-chart"><div class="progress">
<div class="progress-bar" style="width: ${value}%"></div>
</div></div>
</a></li>`);
this.dropdown.append($list_item);
if(!target) this.total += value;
},

if (frappe.get_route()[0]=="activity") {
frappe.pages['activity'].page.list.refresh();
bind_list: function() {
var me = this;
$("#dropdown-notification a").on("click", function() {
var doctype = $(this).attr("data-doctype");
var doc = $(this).attr("data-doc");
if(!doc) {
var config = me.config[doctype] || {};
if (config.route) {
frappe.set_route(config.route);
} else if (config.click) {
config.click();
} else {
frappe.ui.notifications.show_open_count_list(doctype);
}
} else {
frappe.set_route("activity");
frappe.set_route("Form", doctype, doc);
}
}
});
},
};

frappe.views.show_open_count_list = function(element) {
var doctype = $(element).attr("data-doctype");
var filters = frappe.ui.notifications.get_filters(doctype);

if(filters) {
frappe.route_options = filters;
}

var route = frappe.get_route();
if(route[0]==="List" && route[1]===doctype) {
frappe.pages["List/" + doctype].list_view.refresh();
} else {
frappe.set_route("List", doctype);
}
}

frappe.ui.notifications.get_filters = function(doctype) {
var conditions = frappe.boot.notification_info.conditions[doctype];

if(conditions && $.isPlainObject(conditions)) {
return conditions;
}
}
show_open_count_list: function(doctype) {
let filters = this.boot_info.conditions[doctype];
if(filters && $.isPlainObject(filters)) {
frappe.route_options = filters;
}
let route = frappe.get_route();
if(route[0]==="List" && route[1]===doctype) {
frappe.pages["List/" + doctype].list_view.refresh();
} else {
frappe.set_route("List", doctype);
}
},
}

+ 1
- 1
frappe/public/js/legacy/form.js Ver arquivo

@@ -335,7 +335,7 @@ _f.Frm.prototype.refresh_header = function(is_a_different_doc) {
! this.is_dirty() &&
! this.is_new() &&
this.doc.docstatus===0) {
this.dashboard.add_comment(__('Submit this document to confirm'), true);
this.dashboard.add_comment(__('Submit this document to confirm'), 'alert-warning', true);
}

this.clear_custom_buttons();


+ 31
- 15
frappe/public/less/desk.less Ver arquivo

@@ -66,8 +66,8 @@ a[disabled="disabled"] {

#alert-container .desk-alert {
-webkit-box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1);
box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1);
box-shadow: 0 0px 5px rgba(0, 0, 0, 0.1);

padding: 10px 40px 10px 20px;
max-width: 400px;
@@ -318,19 +318,35 @@ textarea.form-control {
}

.open-notification {
position:relative;
position:relative;
left: 2px;
display:inline-block;
background:#ff5858;
font-size: @text-medium;
line-height:20px;
padding:0 8px;
color:#fff;
border-radius:10px;
display:inline-block;
background:#ff5858;
font-size: @text-medium;
line-height:20px;
padding:0 8px;
color:#fff;
border-radius:10px;
cursor: pointer;
margin-right: 10px;
}

a.progress-small {
.progress-chart {
width: 60px;
margin-top: 4px;
float: right;
}

.progress {
margin-bottom: 0;
}

.progress-bar {
background-color: #98d85b;
}
}

/* on small screens, show only icons on top */
@media (max-width: 767px) {
.module-view-layout .nav-stacked > li {
@@ -825,7 +841,7 @@ textarea.form-control {
}

.c3-line {
stroke-width: 3px;
stroke-width: 3px;
}

.c3-tooltip {
@@ -897,10 +913,10 @@ input[type="checkbox"] {
// Will not be required after commonifying lists with empty state
.multiselect-empty-state{
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

// mozilla doesn't support


+ 117
- 0
frappe/public/less/form.less Ver arquivo

@@ -827,6 +827,123 @@ select.form-control {
}
}

/* goals */

.goals-page-container {
background-color: #fafbfc;
padding-top: 1px;

.goal-container {
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
border-radius: 2px;
padding: 10px;
margin: 10px;
}
}

.graph-container {
.graphics {
margin-top: 10px;
padding: 10px 0px;
}

.stats-group {
display: flex;
justify-content: space-around;
flex: 1;
}

.stats-container {
display: flex;
justify-content: space-around;

.stats {
padding-bottom: 15px;
}

.stats-title {
color: #8D99A6;
}
.stats-value {
font-size: 20px;
font-weight: 300;
}
.stats-description {
font-size: 12px;
color: #8D99A6;
}
.graph-data .stats-value {
color: #98d85b;
}
}
}

.bar-graph, .line-graph {

.axis {
font-size: 10px;
fill: #6a737d;

line {
stroke: rgba(27,31,35,0.1);
}
}
}

.data-points {
circle {
fill: #28a745;
stroke: #fff;
stroke-width: 2;
}

g.mini {
fill: #98d85b;
}

path {
fill: none;
stroke: #28a745;
stroke-opacity: 1;
stroke-width: 2px;
}
}

.line-graph {
.path {
fill: none;
stroke: #28a745;
stroke-opacity: 1;
stroke-width: 2px;
}
}

line.dashed {
stroke-dasharray: 5,3;
}

.tick {
&.x-axis-label {
display: block;
}

.specific-value {
text-anchor: start;
}

.y-value-text {
text-anchor: end;
}

.x-value-text {
text-anchor: middle;
}
}


body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] {
height: 80px !important;
}



+ 34
- 0
frappe/tests/test_goal.py Ver arquivo

@@ -0,0 +1,34 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
import unittest
import frappe

from frappe.utils.goal import get_monthly_results, get_monthly_goal_graph_data
from frappe.test_runner import make_test_objects
import frappe.utils

class TestGoal(unittest.TestCase):
def setUp(self):
make_test_objects('Event', reset=True)

def tearDown(self):
frappe.db.sql('delete from `tabEvent`')
# make_test_objects('Event', reset=True)
frappe.db.commit()

def test_get_monthly_results(self):
'''Test monthly aggregation values of a field'''
result_dict = get_monthly_results('Event', 'subject', 'creation', 'event_type="Private"', 'count')

from frappe.utils import today, formatdate
self.assertEquals(result_dict[formatdate(today(), "MM-yyyy")], 2)

def test_get_monthly_goal_graph_data(self):
'''Test for accurate values in graph data (based on test_get_monthly_results)'''
docname = frappe.get_list('Event', filters = {"subject": ["=", "_Test Event 1"]})[0]["name"]
frappe.db.set_value('Event', docname, 'description', 1)
data = get_monthly_goal_graph_data('Test', 'Event', docname, 'description', 'description', 'description',
'Event', '', 'description', 'creation', 'starts_on = "2014-01-01"', 'count')
self.assertEquals(float(data['y_values'][-1]), 1)

+ 128
- 0
frappe/utils/goal.py Ver arquivo

@@ -0,0 +1,128 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt

from __future__ import unicode_literals
import frappe

def get_monthly_results(goal_doctype, goal_field, date_col, filter_str, aggregation = 'sum'):
'''Get monthly aggregation values for given field of doctype'''

where_clause = ('where ' + filter_str) if filter_str else ''
results = frappe.db.sql('''
select
{0}({1}) as {1}, date_format({2}, '%m-%Y') as month_year
from
`{3}`
{4}
group by
month_year'''.format(aggregation, goal_field, date_col, "tab" +
goal_doctype, where_clause), as_dict=True)

month_to_value_dict = {}
for d in results:
month_to_value_dict[d['month_year']] = d[goal_field]

return month_to_value_dict

@frappe.whitelist()
def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_total_field, goal_history_field,
goal_doctype, goal_doctype_link, goal_field, date_field, filter_str, aggregation="sum"):
'''
Get month-wise graph data for a doctype based on aggregation values of a field in the goal doctype

:param title: Graph title
:param doctype: doctype of graph doc
:param docname: of the doc to set the graph in
:param goal_value_field: goal field of doctype
:param goal_total_field: current month value field of doctype
:param goal_history_field: cached history field
:param goal_doctype: doctype the goal is based on
:param goal_doctype_link: doctype link field in goal_doctype
:param goal_field: field from which the goal is calculated
:param filter_str: where clause condition
:param aggregation: a value like 'count', 'sum', 'avg'

:return: dict of graph data
'''

from frappe.utils.formatters import format_value
import json

meta = frappe.get_meta(doctype)
doc = frappe.get_doc(doctype, docname)

goal = doc.get(goal_value_field)
formatted_goal = format_value(goal, meta.get_field(goal_value_field), doc)

current_month_value = doc.get(goal_total_field)
formatted_value = format_value(current_month_value, meta.get_field(goal_total_field), doc)

from frappe.utils import today, getdate, formatdate, add_months
current_month_year = formatdate(today(), "MM-yyyy")

history = doc.get(goal_history_field)
try:
month_to_value_dict = json.loads(history) if history and '{' in history else None
except ValueError:
month_to_value_dict = None

if month_to_value_dict is None:
doc_filter = (goal_doctype_link + ' = "' + docname + '"') if doctype != goal_doctype else ''
if filter_str:
doc_filter += ' and ' + filter_str if doc_filter else filter_str
month_to_value_dict = get_monthly_results(goal_doctype, goal_field, date_field, doc_filter, aggregation)
frappe.db.set_value(doctype, docname, goal_history_field, json.dumps(month_to_value_dict))

month_to_value_dict[current_month_year] = current_month_value

months = []
values = []
for i in xrange(0, 12):
month_value = formatdate(add_months(today(), -i), "MM-yyyy")
month_word = getdate(month_value).strftime('%b')
months.insert(0, month_word)
if month_value in month_to_value_dict:
values.insert(0, month_to_value_dict[month_value])
else:
values.insert(0, 0)

specific_values = []
summary_values = [
{
'name': "This month",
'color': 'green',
'value': formatted_value
}
]

if float(goal) > 0:
specific_values = [
{
'name': "Goal",
'line_type': "dashed",
'value': goal
},
]
summary_values += [
{
'name': "Goal",
'color': 'blue',
'value': formatted_goal
},
{
'name': "Completed",
'color': 'green',
'value': str(int(round(float(current_month_value)/float(goal)*100))) + "%"
}
]

data = {
'title': title,
# 'subtitle':
'y_values': values,
'x_points': months,
'specific_values': specific_values,
'summary_values': summary_values
}

return data

Carregando…
Cancelar
Salvar