diff --git a/CHANGELOG b/CHANGELOG
index 564005a73f..71b8b16ab5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add sum of records on list view
* Add shortcuts to Date and DateTime widgets
* Use limit to expand tree
* Use spellcheck and lang attributes for textbox widget
diff --git a/src/common.js b/src/common.js
index 15c8407f5e..e72948550e 100644
--- a/src/common.js
+++ b/src/common.js
@@ -3353,4 +3353,14 @@
var ellipsis = Sao.i18n.gettext('...');
return string.slice(0, length - ellipsis.length) + ellipsis;
};
+
+ Sao.common.debounce = function(func, wait) {
+ return function() {
+ var args = [].slice(arguments);
+ clearTimeout(func._debounceTimeout);
+ func._debounceTimeout = setTimeout(function() {
+ func.apply(this, args);
+ }.bind(this), wait);
+ }.bind(this);
+ };
}());
diff --git a/src/sao.js b/src/sao.js
index 15e3f60b52..4e3844b1e9 100644
--- a/src/sao.js
+++ b/src/sao.js
@@ -464,6 +464,7 @@ var Sao = {};
init: function(favorite) {
this.field = favorite;
this.header = jQuery('
| ');
+ this.footers = [];
this.attributes = jQuery.extend({}, this.field.description);
this.attributes.name = this.field.name;
diff --git a/src/sao.less b/src/sao.less
index fcf2dc77a4..44dc6e300e 100644
--- a/src/sao.less
+++ b/src/sao.less
@@ -120,6 +120,10 @@ img.icon {
width: 1em;
height: 1em;
}
+
+ & tr.more-row {
+ text-align: center;
+ }
}
& .editabletree-char, .editabletree-date, .editabletree-datetime,
.editabletree-integer, .editabletree-float, .editabletree-selection,
@@ -137,9 +141,6 @@ img.icon {
& .expander {
cursor: pointer;
}
- & .treefooter {
- text-align: center;
- }
& label {
font-weight: normal;
display: inline;
@@ -324,7 +325,10 @@ input.column-boolean {
table.responsive > thead > tr > th:not(.invisible),
table.responsive > tbody,
table.responsive > tbody > tr,
- table.responsive > tbody > tr > td:not(.invisible) {
+ table.responsive > tbody > tr > td:not(.invisible),
+ table.responsive > tfoot,
+ table.responsive > tfoot > tr,
+ table.responsive > tfoot > tr > td:not(.invisible) {
display: block !important;
}
table.responsive {
@@ -343,6 +347,13 @@ input.column-boolean {
display: block;
}
}
+
+ & tfoot {
+ > tr > td:empty,
+ > tr > td > label {
+ display: none !important;
+ }
+ }
}
table.responsive-noheader {
& > tbody > tr > td {
@@ -353,13 +364,15 @@ input.column-boolean {
> tbody > tr {
border: 1px solid #ccc;
}
- & > tbody > tr > td {
+ & > tbody > tr > td,
+ & > tfoot > tr > td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid #eee;
position: relative;
}
- & > tbody > tr {
+ & > tbody > tr,
+ & > tfoot > tr {
& > td:before {
/* Now like a table header */
position: absolute;
diff --git a/src/view/tree.js b/src/view/tree.js
index cc764eba1b..fa58a476f3 100644
--- a/src/view/tree.js
+++ b/src/view/tree.js
@@ -53,6 +53,7 @@
init: function(screen, xml, children_field) {
Sao.View.Tree._super.init.call(this, screen, xml);
this.view_type = 'tree';
+ this.sum_widgets = {};
this.selection_mode = (screen.attributes.selection_mode ||
Sao.common.SELECTION_MULTIPLE);
this.el = jQuery('', {
@@ -88,6 +89,17 @@
th.append(this.selection);
tr.append(th);
this.thead.append(tr);
+
+ this.tfoot = null;
+ var sum_row;
+ if (!jQuery.isEmptyObject(this.sum_widgets)) {
+ sum_row = jQuery('
');
+ sum_row.append(jQuery(' | '));
+ this.tfoot = jQuery('');
+ this.tfoot.append(sum_row);
+ this.table.append(this.tfoot);
+ }
+
this.columns.forEach(function(column) {
th = jQuery(' | ', {
'class': column.attributes.widget,
@@ -116,26 +128,28 @@
}
tr.append(th.append(label));
column.header = th;
+
+ column.footers = [];
+ if (!jQuery.isEmptyObject(this.sum_widgets)) {
+ var field_name = column.attributes.name;
+ var total_cell = jQuery(' | ', {
+ 'class': column.class_,
+ });
+ if (field_name in this.sum_widgets) {
+ var sum_label = this.sum_widgets[field_name][0];
+ var sum_div = this.sum_widgets[field_name][1];
+ total_cell.append(label);
+ total_cell.append(sum_div);
+ total_cell.attr('data-title', label.text());
+ }
+ sum_row.append(total_cell);
+ column.footers.push(total_cell);
+ }
}, this);
this.tbody = jQuery('');
this.table.append(this.tbody);
- // Footer for more
- var footer = jQuery('', {
- 'class': 'treefooter'
- });
- this.more = jQuery('', {
- 'class': 'btn btn-default',
- 'type': 'button'
- }).append(Sao.i18n.gettext('More')
- ).click(function() {
- this.display_size += Sao.config.display_size;
- this.display();
- }.bind(this));
- footer.append(this.more);
- this.more.hide();
this.display_size = Sao.config.display_size;
- this.el.append(footer);
},
create_columns: function(model, xml) {
xml.find('tree').children().each(function(pos, child) {
@@ -211,7 +225,7 @@
column.sortable = true;
}
this.fields[name] = true;
- // TODO sum
+ this.add_sum(attributes);
} else if (child.tagName == 'button') {
column = new Sao.View.Tree.ButtonColumn(this.screen,
attributes);
@@ -219,6 +233,19 @@
this.columns.push(column);
}.bind(this));
},
+ add_sum: function(attributes){
+ if (!attributes.sum) {
+ return;
+ }
+ var sum = jQuery('', {
+ text: attributes.sum,
+ 'data-title': attributes.sum + Sao.i18n.gettext(': ')
+ });
+ var aggregate = jQuery('', {
+ 'class': 'value',
+ });
+ this.sum_widgets[attributes.name] = [sum, aggregate];
+ },
sort_model: function(e){
var column = e.data;
var arrow = column.arrow;
@@ -314,6 +341,7 @@
}
// Set column visibility depending on attributes and domain
+ var visible_columns = 1; // start at 1 because of the checkbox
var domain = [];
if (!jQuery.isEmptyObject(this.screen.domain)) {
domain.push(this.screen.domain);
@@ -326,14 +354,20 @@
domain = inversion.simplify(domain);
var decoder = new Sao.PYSON.Decoder(this.screen.context());
this.columns.forEach(function(column) {
+ visible_columns += 1;
var name = column.attributes.name;
if (!name) {
return;
}
+ var related_cells = column.footers.slice();
+ related_cells.push(column.header);
if ((decoder.decode(column.attributes.tree_invisible || '0')) ||
(name === this.screen.exclude_field)) {
- column.header.hide();
- column.header.addClass('invisible');
+ visible_columns -= 1;
+ related_cells.forEach(function(cell) {
+ cell.hide();
+ cell.addClass('invisible');
+ });
} else {
var inv_domain = inversion.domain_inversion(domain, name);
if (typeof inv_domain != 'boolean') {
@@ -341,14 +375,21 @@
}
var unique = inversion.unique_value(inv_domain)[0];
if (unique && jQuery.isEmptyObject(this.children_field)) {
- column.header.hide();
- column.header.addClass('invisible');
+ visible_columns -= 1;
+ related_cells.forEach(function(cell) {
+ cell.hide();
+ cell.addClass('invisible');
+ });
} else {
- column.header.show();
- column.header.removeClass('invisible');
+ related_cells.forEach(function(cell) {
+ cell.show();
+ cell.removeClass('invisible');
+ });
}
}
}.bind(this));
+ this.tbody.find('tr.more-row > td').attr(
+ 'colspan', visible_columns);
if (this.columns.filter(function(c) {
return c.header.is(':visible');
@@ -360,13 +401,16 @@
this.table.removeClass('responsive-header');
}
- return this.redraw(selected, expanded);
+ return this.redraw(selected, expanded).done(
+ Sao.common.debounce(this.update_sum.bind(this), 250));
},
construct: function(selected, expanded, extend) {
var tbody = this.tbody;
if (!extend) {
this.rows = [];
this.tbody = jQuery('');
+ } else {
+ this.tbody.find('tr.more-row').remove();
}
var start = this.rows.length;
var add_row = function(record, pos, group) {
@@ -382,14 +426,27 @@
};
this.screen.group.slice(start, this.display_size).forEach(
add_row.bind(this));
- if (this.display_size >= this.screen.group.length) {
- this.more.hide();
- } else {
- this.more.show();
- }
if (!extend) {
tbody.replaceWith(this.tbody);
}
+
+ if (this.display_size < this.screen.group.length) {
+ var more_row = jQuery('
', {
+ 'class': 'more-row',
+ });
+ var more_cell = jQuery(' | ');
+ var more_button = jQuery('', {
+ 'class': 'btn btn-default',
+ 'type': 'button'
+ }).append(Sao.i18n.gettext('More')
+ ).click(function() {
+ this.display_size += Sao.config.display_size;
+ this.display();
+ }.bind(this));
+ more_cell.append(more_button);
+ more_row.append(more_cell);
+ this.tbody.append(more_row);
+ }
},
redraw: function(selected, expanded) {
return redraw_async(this.rows, selected, expanded);
@@ -425,6 +482,79 @@
}
// TODO update_children
},
+ update_sum: function() {
+ for (var name in this.sum_widgets) {
+ if (!this.sum_widgets.hasOwnProperty(name)) {
+ continue;
+ }
+
+ var selected_records = this.selected_records();
+ var aggregate = '-';
+ var aggregate_el = this.sum_widgets[name][1];
+ var sum_ = null;
+ var selected_sum = null;
+ var loaded = true;
+ var digit = 0;
+ var field = this.screen.model.fields[name];
+ var i, record;
+ var records_ids = selected_records.map(function(record){
+ return record.id;
+ });
+ for (i=0; i < this.screen.group.length; i++){
+ record = this.screen.group[i];
+ if (!record.get_loaded([name]) && record.id >=0){
+ loaded = false;
+ break;
+ }
+ var value = field.get(record);
+ if (value && value.isTimeDelta) {
+ value = value.asSeconds();
+ }
+ if (value !== null){
+ if (sum_ === null){
+ sum_ = value;
+ }else {
+ sum_ += value;
+ }
+ if (~records_ids.indexOf(record.id) ||
+ !selected_records){
+ if (selected_sum === null){
+ selected_sum = value;
+ }else {
+ selected_sum += value;
+ }
+ }
+ if (field.digits) {
+ var fdigits = field.digits(record);
+ if (fdigits && digit !== null){
+ digit = Math.max(fdigits[1], digit);
+ } else {
+ digit = null;
+ }
+ }
+ }
+ }
+ if (loaded) {
+ if (field.description.type == 'timedelta'){
+ var converter = field.converter(record.group);
+ selected_sum = Sao.common.timedelta.format(
+ Sao.TimeDelta(null, selected_sum), converter);
+ sum_ = Sao.common.timedelta.format(
+ Sao.TimeDelta(null, sum_), converter);
+ } else if (digit !== null){
+ var options = {};
+ options.minimumFractionDigits = digit;
+ options.maximumFractionDigits = digit;
+ selected_sum = (selected_sum || 0).toLocaleString(
+ Sao.i18n.getlang(), options);
+ sum_ = (sum_ || 0).toLocaleString(
+ Sao.i18n.getlang(), options);
+ }
+ aggregate = selected_sum + ' / ' + sum_;
+ }
+ aggregate_el.text(aggregate);
+ }
+ },
selected_records: function() {
if (this.selection_mode == Sao.common.SELECTION_NONE) {
return [];
@@ -458,8 +588,10 @@
} else {
this.select_changed(null);
}
+ this.update_sum();
},
update_selection: function() {
+ this.update_sum();
if (this.selection.prop('checked')) {
return;
}
@@ -1393,6 +1525,7 @@
this.prefixes = [];
this.suffixes = [];
this.header = null;
+ this.footers = [];
},
get_cell: function() {
var cell = jQuery('', {