diff --git a/CHANGELOG b/CHANGELOG index 292be0c9f8..374d393cb7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Support multiselection field * Support dot notation on PYSON Eval * Manage sequence * Add drag and drop on tree views diff --git a/src/common.js b/src/common.js index bb3eaf986c..a515698cab 100644 --- a/src/common.js +++ b/src/common.js @@ -1095,6 +1095,10 @@ } else { return test(value); } + } else if (field.type == 'multiselection') { + return (!value || + jQuery.isEmptyObject(value) || + (value instanceof Array)); } else { return true; } @@ -1454,6 +1458,7 @@ var completes = { 'boolean': complete_boolean, 'selection': complete_selection, + 'multiselection': complete_selection, 'reference': complete_reference, 'datetime': complete_datetime, 'date': complete_date, @@ -1705,12 +1710,17 @@ if (target) { field_name += '.rec_name'; } + } else if (field.type == 'multiselection') { + if ((value !== null) && !(value instanceof Array)) { + value = [value]; + } } if (!operator) { operator = this.default_operator(field); } - if (value instanceof Array) { + if ((value instanceof Array) && + (field.type != 'multiselection')) { if (operator == '!') { operator = 'not in'; } else { @@ -1721,6 +1731,13 @@ operator = this.negate_operator( this.default_operator(field)); } + if ((value === null) && operator.endsWith('in')) { + if (operator.startsWith('not')) { + operator = '!='; + } else { + operator = '='; + } + } if (~['integer', 'float', 'numeric', 'datetime', 'date', 'time'].indexOf(field.type)) { if (value && value.contains('..')) { @@ -1793,6 +1810,8 @@ if (~['char', 'text', 'many2one', 'many2many', 'one2many', 'reference'].indexOf(field.type)) { return 'ilike'; + } else if (field.type == 'multiselection') { + return 'in'; } else { return '='; } @@ -1886,6 +1905,7 @@ } }, 'selection': convert_selection, + 'multiselection': convert_selection, 'reference': convert_selection, 'datetime': function() { var result = Sao.common.parse_datetime( @@ -1990,6 +2010,7 @@ 'float': format_float, 'numeric': format_float, 'selection': format_selection, + 'multiselection': format_selection, 'reference': format_reference, 'datetime': function() { if (!value) { diff --git a/src/model.js b/src/model.js index abaeb52ca3..1701f8d74c 100644 --- a/src/model.js +++ b/src/model.js @@ -1403,6 +1403,8 @@ return Sao.field.Char; case 'selection': return Sao.field.Selection; + case 'multiselection': + return Sao.field.MultiSelection; case 'datetime': case 'timestamp': return Sao.field.DateTime; @@ -1677,6 +1679,25 @@ _default: null }); + Sao.field.MultiSelection = Sao.class_(Sao.field.Field, { + _default: null, + get: function(record) { + var value = Sao.field.MultiSelection._super.get.call(this, record); + if (jQuery.isEmptyObject(value)) { + value = this._default; + } + return value; + }, + get_eval: function(record) { + var value = Sao.field.MultiSelection._super.get_eval.call( + this, record); + if (value === null) { + value = []; + } + return value; + }, + }); + Sao.field.DateTime = Sao.class_(Sao.field.Field, { _default: null, time_format: function(record) { diff --git a/src/screen.js b/src/screen.js index 0a6aa4468a..8bcb189d1d 100644 --- a/src/screen.js +++ b/src/screen.js @@ -493,6 +493,7 @@ boolean_option(input)); break; case 'selection': + case 'multiselection': entry = new Sao.ScreenContainer.Selection( field.selection, prefix + field.name); input = entry.el; diff --git a/src/view/form.js b/src/view/form.js index df1e8605c0..de91b4df80 100644 --- a/src/view/form.js +++ b/src/view/form.js @@ -3757,10 +3757,7 @@ function eval_pyson(value){ }, get modified() { if (this.record && this.field) { - var group = new Set(this.field.get_client(this.record) - .map(function(r) { - return r.id; - })); + var group = new Set(this.field.get_eval(this.record)); var value = new Set(this.get_value()); return !Sao.common.compare(value, group); } @@ -3781,22 +3778,15 @@ function eval_pyson(value){ if (!field) { return; } - var value = []; - var group = record.field_get_client(this.field_name); - for (i = 0, len = group.length; i < len; i++) { - element = group[i]; - if (!~group.record_removed.indexOf(element) && - !~group.record_deleted.indexOf(element)) { - value.push(element.id); - } - } + var value = field.get_eval(record); + value = value.map(function(e) { return JSON.stringify(e); }); this.select.val(value); }.bind(this)); }, get_value: function() { var value = this.select.val(); if (value) { - return value.map(function(e) { return parseInt(e, 10); }); + return value.map(function(e) { return JSON.parse(e); }); } return []; }, diff --git a/src/view/tree.js b/src/view/tree.js index e5c12774dd..0e7115c74e 100644 --- a/src/view/tree.js +++ b/src/view/tree.js @@ -1499,8 +1499,11 @@ invisible = true; } if (editable) { + var EditableBuilder = Sao.View.EditableTree.WIDGETS[ + column.attributes.widget]; readonly = (column.attributes.readonly || - state_attrs.readonly); + state_attrs.readonly || + !EditableBuilder); } else { readonly = false; } @@ -2031,6 +2034,37 @@ } }); + Sao.View.Tree.MultiSelectionColumn = Sao.class_(Sao.View.Tree.CharColumn, { + class_: 'column-multiselection', + init: function(model, attributes) { + Sao.View.Tree.MultiSelectionColumn._super.init.call( + this, model, attributes); + Sao.common.selection_mixin.init.call(this); + this.init_selection(); + }, + init_selection: function(key) { + Sao.common.selection_mixin.init_selection.call(this, key); + }, + update_selection: function(record, callback) { + Sao.common.selection_mixin.update_selection.call(this, record, + this.field, callback); + }, + update_text: function(cell, record) { + this.update_selection(record, function() { + var values = this.field.get_eval(record).map(function(value) { + for (var i = 0; i < this.selection.length; i++) { + if (this.selection[i][0] === value) { + return this.selection[i][1]; + } + } + return ''; + }.bind(this)); + var text_value = values.join(';'); + cell.text(text_value).attr('title', text_value); + }.bind(this)); + }, + }); + Sao.View.Tree.ReferenceColumn = Sao.class_(Sao.View.Tree.CharColumn, { class_: 'column-reference', init: function(model, attributes) { @@ -2329,6 +2363,7 @@ 'progressbar': Sao.View.Tree.ProgressBar, 'reference': Sao.View.Tree.ReferenceColumn, 'selection': Sao.View.Tree.SelectionColumn, + 'multiselection': Sao.View.Tree.MultiSelectionColumn, 'sip': Sao.View.Tree.URLColumn, 'text': Sao.View.Tree.TextColum, 'time': Sao.View.Tree.TimeColumn, diff --git a/tests/sao.js b/tests/sao.js index 77c32fcd9f..a91ab150e4 100644 --- a/tests/sao.js +++ b/tests/sao.js @@ -1488,6 +1488,15 @@ ['female', 'Female'] ] }, + 'multiselection': { + 'string': "MultiSelection", + 'type': 'multiselection', + 'selection': [ + ['foo', "Foo"], + ['bar', "Bar"], + ['baz', "Baz"], + ], + }, 'reference': { 'string': 'Reference', 'name': 'reference', @@ -1535,6 +1544,24 @@ [[c(['Selection', null, ''])], [c(['selection', '=', ''])]], [[c(['Selection', null, ['Male', 'Female']])], [c(['selection', 'in', ['male', 'female']])]], + [[c(['MultiSelection', null, null])], + [c(['multiselection', '=', null])]], + [[c(['MultiSelection', null, ''])], + [c(['multiselection', 'in', ['']])]], + [[c(['MultiSelection', '=', ''])], + [c(['multiselection', '=', ['']])]], + [[c(['MultiSelection', '!', ''])], + [c(['multiselection', 'not in', ['']])]], + [[c(['MultiSelection', '!=', ''])], + [c(['multiselection', '!=', ['']])]], + [[c(['MultiSelection', null, ['Foo', 'Bar']])], + [c(['multiselection', 'in', ['foo', 'bar']])]], + [[c(['MultiSelection', '=', ['Foo', 'Bar']])], + [c(['multiselection', '=', ['foo', 'bar']])]], + [[c(['MultiSelection', '!', ['Foo', 'Bar']])], + [c(['multiselection', 'not in', ['foo', 'bar']])]], + [[c(['MultiSelection', '!=', ['Foo', 'Bar']])], + [c(['multiselection', '!=', ['foo', 'bar']])]], [[c(['Integer', null, null])], [c(['integer', '=', null])]], [[c(['Integer', null, '3..5'])], [[ c(['integer', '>=', 3]), @@ -1702,6 +1729,15 @@ 'string': 'Name', 'type': 'char', }, + 'multiselection': { + 'string': "MultiSelection", + 'type': 'multiselection', + 'selection': [ + ['foo', "Foo"], + ['bar', "Bar"], + ['baz', "Baz"], + ], + }, 'relation': { 'string': 'Relation', 'type': 'many2one', @@ -1725,6 +1761,10 @@ QUnit.ok(!parser.stringable(['AND', valid, invalid])); QUnit.ok(parser.stringable([[valid]])); QUnit.ok(!parser.stringable([[valid], [invalid]])); + QUnit.ok(parser.stringable([['multiselection', '=', null]])); + QUnit.ok(parser.stringable([['multiselection', '=', '']])); + QUnit.ok(!parser.stringable([['multiselection', '=', 'foo']])); + QUnit.ok(parser.stringable([['multiselection', '=', ['foo']]])); QUnit.ok(parser.stringable([['relation', '=', null]])); QUnit.ok(parser.stringable([['relation', '=', 'Foo']])); QUnit.ok(parser.stringable([['relation.rec_name', '=', 'Foo']])); @@ -1757,6 +1797,15 @@ ['femal', 'Femal'] ] }, + 'multiselection': { + 'string': "MultiSelection", + 'type': 'multiselection', + 'selection': [ + ['foo', "Foo"], + ['bar', "Bar"], + ['baz', "Baz"], + ], + }, 'reference': { 'string': 'Reference', 'type': 'reference', @@ -1818,6 +1867,17 @@ [[['selection', '!=', '']], 'Selection: !""'], [[['selection', '=', 'male']], 'Selection: Male'], [[['selection', '!=', 'male']], 'Selection: !Male'], + [[['multiselection', '=', null]], "MultiSelection: ="], + [[['multiselection', '=', '']], "MultiSelection: ="], + [[['multiselection', '!=', '']], "MultiSelection: !="], + [[['multiselection', '=', ['foo']]], "MultiSelection: =Foo"], + [[['multiselection', '!=', ['foo']]], "MultiSelection: !=Foo"], + [[['multiselection', '=', ['foo', 'bar']]], "MultiSelection: =Foo;Bar"], + [[['multiselection', '!=', ['foo', 'bar']]], "MultiSelection: !=Foo;Bar"], + [[['multiselection', 'in', ['foo']]], "MultiSelection: Foo"], + [[['multiselection', 'not in', ['foo']]], "MultiSelection: !Foo"], + [[['multiselection', 'in', ['foo', 'bar']]], "MultiSelection: Foo;Bar"], + [[['multiselection', 'not in', ['foo', 'bar']]], "MultiSelection: !Foo;Bar"], [[['reference', 'ilike', '%foo%']], 'Reference: foo'], [[['reference', 'ilike', '%bar%', 'spam']], 'Reference: Spam,bar'], [[['reference', 'in', ['foo', 'bar']]], 'Reference: foo;bar'],