diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 6c59d242cb0..5180cc41656 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -933,7 +933,7 @@ var BasicModel = AbstractModel.extend({
group.isOpen = true;
var def;
if (group.count > 0) {
- def = this._fetchUngroupedList(group);
+ def = this._load(group);
}
return $.when(def).then(function () {
return groupId;
@@ -2048,6 +2048,7 @@ var BasicModel = AbstractModel.extend({
var fields = _.uniq(list.fieldNames.concat(list.groupedBy));
return this._rpc(list.model, 'read_group')
.withFields(fields)
+ .withDomain(list.domain)
.withContext(list.context)
.groupBy(list.groupedBy)
.lazy(true)
@@ -2071,7 +2072,7 @@ var BasicModel = AbstractModel.extend({
var newGroup = self._makeDataPoint({
modelName: list.model,
count: group[rawGroupBy + '_count'],
- domain: group.__domain,
+ domain: list.domain.concat(group.__domain),
context: list.context,
fields: list.fields,
fieldNames: list.fieldNames,
@@ -2082,7 +2083,7 @@ var BasicModel = AbstractModel.extend({
orderedBy: list.orderedBy,
limit: list.limit,
openGroupByDefault: list.openGroupByDefault,
- type: 'list', // nested groupedBys not handled yet
+ type: 'list',
});
list.data.push(newGroup.id);
var old_group = _.find(previousGroups, function (g) {
@@ -2090,13 +2091,15 @@ var BasicModel = AbstractModel.extend({
});
if (old_group) {
newGroup.isOpen = old_group.isOpen;
+ // copy the oldGroup data to keep 'isOpen' state of sub-groups
+ newGroup.data = newGroup.isOpen ? old_group.data : [];
} else if (!newGroup.openGroupByDefault) {
newGroup.isOpen = false;
} else {
newGroup.isOpen = '__fold' in group ? !group.__fold : true;
}
if (newGroup.isOpen && newGroup.count > 0) {
- defs.push(self._fetchUngroupedList(newGroup));
+ defs.push(self._load(newGroup));
}
});
return $.when.apply($, defs).then(function () {
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index d82e31fc781..5618a0e36e2 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -354,9 +354,10 @@ var ListRenderer = BasicRenderer.extend({
*
* @private
* @param {Object} group
+ * @param {integer} groupLevel the nesting level (0 for root groups)
* @returns {jQueryElement} a
element
*/
- _renderGroupRow: function (group) {
+ _renderGroupRow: function (group, groupLevel) {
var aggregateValues = _.mapObject(group.aggregateValues, function (value) {
return { value: value };
});
@@ -370,7 +371,9 @@ var ListRenderer = BasicRenderer.extend({
.addClass('o_group_name')
.text(name + ' (' + group.count + ')');
if (group.count > 0) {
- var $arrow = $('')
+ var $arrow = $('')
+ .css('padding-left', (groupLevel * 20) + 'px')
+ .css('padding-right', '5px')
.addClass('fa')
.toggleClass('fa-caret-right', !group.isOpen)
.toggleClass('fa-caret-down', group.isOpen);
@@ -390,23 +393,33 @@ var ListRenderer = BasicRenderer.extend({
* row, then possibly a bunch of rows for each record.
*
* @private
+ * @param {Object} data the dataPoint containing the groups
+ * @param {integer} [groupLevel=0] the nesting level. 0 is for the root group
* @returns {jQueryElement[]} a list of
*/
- _renderGroups: function () {
+ _renderGroups: function (data, groupLevel) {
var self = this;
+ groupLevel = groupLevel || 0;
var result = [];
var $tbody = $('');
- _.each(this.state.data, function (group) {
+ _.each(data, function (group) {
if (!$tbody) {
$tbody = $('');
}
- $tbody.append(self._renderGroupRow(group));
+ $tbody.append(self._renderGroupRow(group, groupLevel));
if (group.data.length) {
result.push($tbody);
- var $records = _.map(group.data, function (record) {
- return self._renderRow(record).prepend($('| '));
- });
- result.push($(' | ').append($records));
+ // render an opened group
+ if (group.groupedBy.length) {
+ // the opened group contains subgroups
+ result = result.concat(self._renderGroups(group.data, groupLevel + 1));
+ } else {
+ // the opened group contains records
+ var $records = _.map(group.data, function (record) {
+ return self._renderRow(record).prepend($(''));
+ });
+ result.push($(' | ').append($records));
+ }
$tbody = null;
}
});
@@ -548,7 +561,7 @@ var ListRenderer = BasicRenderer.extend({
if (is_grouped) {
$table
.append(this._renderHeader(true))
- .append(this._renderGroups())
+ .append(this._renderGroups(this.state.data))
.append(this._renderFooter(true));
} else {
$table
diff --git a/addons/web/static/test/views/list_tests.js b/addons/web/static/test/views/list_tests.js
index 069de6f8b31..32cdb296ed9 100644
--- a/addons/web/static/test/views/list_tests.js
+++ b/addons/web/static/test/views/list_tests.js
@@ -1077,6 +1077,126 @@ QUnit.module('Views', {
list.destroy();
});
+
+ QUnit.test('list view with nested groups', function (assert) {
+ assert.expect(36);
+
+ this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1});
+
+ var nbRPCs = {readGroup: 0, searchRead: 0};
+ var groupID;
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '',
+ groupBy: ['m2o', 'foo'],
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ if (args.kwargs.groupby[0] === 'foo') { // nested read_group
+ // called twice (once when opening the group, once when sorting)
+ assert.deepEqual(args.kwargs.domain, [['m2o', '=', 1]],
+ "nested read_group should be called with correct domain");
+ }
+ nbRPCs.readGroup++;
+ } else if (route === '/web/dataset/search_read') {
+ // called twice (once when opening the group, once when sorting)
+ assert.deepEqual(args.domain, [['m2o', '=', 1], ['foo', '=', 'blip']],
+ "nested search_read should be called with correct domain");
+ nbRPCs.searchRead++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ intercepts: {
+ open_record: function (event) {
+ event.stopPropagation();
+ assert.strictEqual(event.data.id, groupID,
+ "'open_record' event should have been triggered");
+ },
+ },
+ });
+
+ assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group");
+ assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
+
+ // basic rendering tests
+ assert.strictEqual(list.$('tbody').length, 1, "there should be 1 tbody");
+ assert.strictEqual(list.$('.o_group_header').length, 2,
+ "should contain 2 groups at first level");
+ assert.strictEqual(list.$('.o_group_name:first').text(), 'Value 1 (4)',
+ "group should have correct name and count");
+ assert.strictEqual(list.$('.o_group_name .fa-caret-right').length, 2,
+ "the carret of closed groups should be right");
+ assert.strictEqual(list.$('.o_group_name:first span').css('padding-left'),
+ '0px', "groups of level 1 should have a 0px padding-left");
+ assert.strictEqual(list.$('.o_group_header:first td:last').text(), '16',
+ "group aggregates are correctly displayed");
+
+ // open the first group
+ nbRPCs = {readGroup: 0, searchRead: 0};
+ list.$('.o_group_header:first').click();
+ assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group");
+ assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
+
+ var $openGroup = list.$('tbody:nth(1)');
+ assert.strictEqual(list.$('tbody').length, 3, "there should be 3 tbodys");
+ assert.strictEqual(list.$('.o_group_name:first .fa-caret-down').length, 1,
+ "the carret of open groups should be down");
+ assert.strictEqual($openGroup.find('.o_group_header').length, 3,
+ "open group should contain 3 groups");
+ assert.strictEqual($openGroup.find('.o_group_name:nth(2)').text(), 'blip (2)',
+ "group should have correct name and count");
+ assert.strictEqual($openGroup.find('.o_group_name:nth(2) span').css('padding-left'),
+ '20px', "groups of level 2 should have a 20px padding-left");
+ assert.strictEqual($openGroup.find('.o_group_header:nth(2) td:last').text(), '-11',
+ "inner group aggregates are correctly displayed");
+
+ // open subgroup
+ nbRPCs = {readGroup: 0, searchRead: 0};
+ $openGroup.find('.o_group_header:nth(2)').click();
+ assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
+ assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
+
+ var $openSubGroup = list.$('tbody:nth(2)');
+ assert.strictEqual(list.$('tbody').length, 4, "there should be 4 tbodys");
+ assert.strictEqual($openSubGroup.find('.o_data_row').length, 2,
+ "open subgroup should contain 2 data rows");
+ assert.strictEqual($openSubGroup.find('.o_data_row:first td:last').text(), '-4',
+ "first record in open subgroup should be res_id 4 (with int_field -4)");
+
+ // open a record (should trigger event 'open_record')
+ var $row = $openSubGroup.find('.o_data_row:first');
+ groupID = $row.data('id');
+ $openSubGroup.find('.o_data_row:first').click();
+
+ // sort by int_field (ASC) and check that open groups are still open
+ nbRPCs = {readGroup: 0, searchRead: 0};
+ list.$('thead th:last').click();
+ assert.strictEqual(nbRPCs.readGroup, 2, "should have done two read_groups");
+ assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
+
+ $openSubGroup = list.$('tbody:nth(2)');
+ assert.strictEqual(list.$('tbody').length, 4, "there should be 4 tbodys");
+ assert.strictEqual($openSubGroup.find('.o_data_row').length, 2,
+ "open subgroup should contain 2 data rows");
+ assert.strictEqual($openSubGroup.find('.o_data_row:first td:last').text(), '-7',
+ "first record in open subgroup should be res_id 5 (with int_field -7)");
+
+ // close first level group
+ nbRPCs = {readGroup: 0, searchRead: 0};
+ list.$('.o_group_header:first').click();
+ assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
+ assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
+
+ assert.strictEqual(list.$('tbody').length, 1, "there should be 1 tbody");
+ assert.strictEqual(list.$('.o_group_header').length, 2,
+ "should contain 2 groups at first level");
+ assert.strictEqual(list.$('.o_group_name .fa-caret-right').length, 2,
+ "the carret of closed groups should be right");
+
+ list.destroy();
+ });
});
});