diff --git a/sao/CHANGELOG b/sao/CHANGELOG index ed7da86998..e7117cac11 100644 --- a/sao/CHANGELOG +++ b/sao/CHANGELOG @@ -1,3 +1,4 @@ +* Support create record from autocomplete * Use grid layout for form container * Support scanning code * Reset search offset when domain has changed diff --git a/sao/src/common.js b/sao/src/common.js index 5b58816405..085e914d9a 100644 --- a/sao/src/common.js +++ b/sao/src/common.js @@ -3740,15 +3740,15 @@ Sao.common.set_overflow(this.input, 'show'); }); }, - set_actions: function(action_activated, search, create) { + set_actions: function(action_activated, search=true, create=true) { if (action_activated !== undefined) { this.action_activated = action_activated; } var actions = []; - if (search || search == undefined) { + if (search) { actions.push(['search', Sao.i18n.gettext('Search...')]); } - if (create || create == undefined) { + if (create) { actions.push(['create', Sao.i18n.gettext('Create...')]); } this.menu.find('li.action').remove(); @@ -3840,7 +3840,7 @@ }); Sao.common.get_completion = function(el, source, - match_selected, action_activated, search, create) { + match_selected, action_activated, search=true, create=true) { var format = function(content) { return content.name; }; @@ -3849,6 +3849,7 @@ if (action_activated) { completion.set_actions(action_activated, search, create); } + completion._allow_create = create; return completion; }; @@ -3867,7 +3868,20 @@ var sao_model = new Sao.Model(model); return sao_model.execute( 'autocomplete', - [search_text, domain, Sao.config.limit, order], context).fail(function() { + [search_text, domain, Sao.config.limit, order], context) + .then( + function(result) { + return result.filter((value) => { + return value.id || entry.completion._allow_create; + }).map((value) => { + if (value.id === null) { + value.name = Sao.i18n.gettext( + 'Create "%1"...', value.name); + } + return value; + }); + }, + function() { Sao.Logger.warning( "Unable to search for completion of %s", model); }); diff --git a/sao/src/view/form.js b/sao/src/view/form.js index 8e5c5e64d4..420fc00982 100644 --- a/sao/src/view/form.js +++ b/sao/src/view/form.js @@ -2515,6 +2515,7 @@ function eval_pyson(value){ group, this._update_completion.bind(this), this._completion_match_selected.bind(this)); + this.entry.completion = this.wid_completion; } this.el.change(this.focus_out.bind(this)); this._readonly = false; @@ -2908,9 +2909,13 @@ function eval_pyson(value){ this.entry, record, field, model); }, _completion_match_selected: function(value) { - this.record.field_set_client(this.field_name, + if (value.id !== null) { + this.record.field_set_client(this.field_name, this.value_from_id( value.id, value.rec_name), true); + } else { + this.new_(value.defaults); + } }, _completion_action_activated: function(action) { if (action == 'search') { @@ -3188,6 +3193,7 @@ function eval_pyson(value){ this._completion_match_selected.bind(this), this._completion_action_activated.bind(this), this.read_access, this.create_access); + this.wid_text.completion = this.wid_completion; } buttons = jQuery('
', { @@ -3749,8 +3755,12 @@ function eval_pyson(value){ this.wid_text, this.record, this.field, model, domain); }, _completion_match_selected: function(value) { - this.screen.group.load([value.id], true); - this.wid_text.val(''); + if (value.id !== null) { + this.screen.group.load([value.id], true); + this.wid_text.val(''); + } else { + this.new_(value.defaults); + } }, _completion_action_activated: function(action) { if (action == 'search') { @@ -3813,6 +3823,7 @@ function eval_pyson(value){ this._completion_match_selected.bind(this), this._completion_action_activated.bind(this), this.read_access, this.create_access); + this.entry.completion = this.wid_completion; } var buttons = jQuery('', { @@ -4106,8 +4117,12 @@ function eval_pyson(value){ this.entry, this.record, this.field, model, domain); }, _completion_match_selected: function(value) { - this.screen.group.load([value.id], true); - this.entry.val(''); + if (value.id !== null) { + this.screen.group.load([value.id], true); + this.entry.val(''); + } else { + this.new_(value.defaults); + } }, _completion_action_activated: function(action) { if (action == 'search') { @@ -4754,6 +4769,7 @@ function eval_pyson(value){ group, this._update_completion.bind(this), this._completion_match_selected.bind(this)); + this.wid_text.completion = this.wid_completion; } this.but_add = jQuery('', { diff --git a/tryton/CHANGELOG b/tryton/CHANGELOG index 0f3e1dd343..8e0b8b2a80 100644 --- a/tryton/CHANGELOG +++ b/tryton/CHANGELOG @@ -1,3 +1,4 @@ +* Support create record from autocomplete * Support scanning code * Reset search offset when domain has changed * Add a canonicalize function for domains diff --git a/tryton/tryton/common/completion.py b/tryton/tryton/common/completion.py index 18a0102a29..e8bb0d7b6e 100644 --- a/tryton/tryton/common/completion.py +++ b/tryton/tryton/common/completion.py @@ -17,13 +17,14 @@ def get_completion(search=True, create=True): "Return a EntryCompletion" completion = Gtk.EntryCompletion() completion.set_match_func(lambda *a: True) - completion.set_model(Gtk.ListStore(str, int)) + completion.set_model(Gtk.ListStore(str, object, object)) completion.set_text_column(0) completion.props.popup_set_width = False if search: completion.insert_action_markup(0, _('Search...')) if create: completion.insert_action_markup(1, _('Create...')) + completion._allow_create = create return completion @@ -34,7 +35,8 @@ def update_completion(entry, record, field, model, domain=None): return False if search_text != entry.get_text(): return False - completion_model = entry.get_completion().get_model() + completion = entry.get_completion() + completion_model = completion.get_model() if not search_text or not model: completion_model.clear() completion_model.search_text = search_text @@ -55,7 +57,13 @@ def update_completion(entry, record, field, model, domain=None): return False completion_model.clear() for result in results: - completion_model.append([result['name'], result['id']]) + if result['id'] is not None or completion._allow_create: + if result['id'] is None: + name = _('Create "%s"...') % result['name'] + else: + name = result['name'] + completion_model.append(( + name, result['id'], result['defaults'])) completion_model.search_text = search_text # Force display of popup entry.emit('changed') diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py b/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py index b40a706568..c452f2b994 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py @@ -307,14 +307,17 @@ class Many2Many(Widget): return True def _completion_match_selected(self, completion, model, iter_): - record_id, = model.get(iter_, 1) - self.screen.load([record_id], modified=True) - self.wid_text.set_text('') - self.wid_text.grab_focus() + record_id, defaults = model.get(iter_, 1, 2) + if record_id is not None: + self.screen.load([record_id], modified=True) + self.wid_text.set_text('') + self.wid_text.grab_focus() - completion_model = self.wid_completion.get_model() - completion_model.clear() - completion_model.search_text = self.wid_text.get_text() + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = self.wid_text.get_text() + else: + self._sig_new(defaults) return True def _update_completion(self, widget): diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py b/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py index 4560d7d9e3..6d5c91086e 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py @@ -347,15 +347,18 @@ class Many2One(Widget): self.wid_text.set_completion(self.wid_completion) def _completion_match_selected(self, completion, model, iter_): - rec_name, record_id = model.get(iter_, 0, 1) - # GTK on win32 doesn't like synchronous call to set_client - # because it triggers a display which reset the completion - GLib.idle_add(self.field.set_client, self.record, - self.value_from_id(record_id, rec_name), True) + rec_name, record_id, defaults = model.get(iter_, 0, 1, 2) + if record_id is not None: + # GTK on win32 doesn't like synchronous call to set_client + # because it triggers a display which reset the completion + GLib.idle_add(self.field.set_client, self.record, + self.value_from_id(record_id, rec_name), True) - completion_model = self.wid_completion.get_model() - completion_model.clear() - completion_model.search_text = rec_name + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = rec_name + else: + self.sig_new(defaults) return True def _update_completion(self, widget): diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py b/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py index 00ce93d6bb..7652058114 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py @@ -564,14 +564,17 @@ class One2Many(Widget): return True def _completion_match_selected(self, completion, model, iter_): - record_id, = model.get(iter_, 1) - self.screen.load([record_id], modified=True) - self.wid_text.set_text('') - self.wid_text.grab_focus() + record_id, defaults = model.get(iter_, 1, 2) + if record_id is not None: + self.screen.load([record_id], modified=True) + self.wid_text.set_text('') + self.wid_text.grab_focus() - completion_model = self.wid_completion.get_model() - completion_model.clear() - completion_model.search_text = self.wid_text.get_text() + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = self.wid_text.get_text() + else: + self._sig_new(defaults) return True def _update_completion(self, widget): diff --git a/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py b/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py index 8ee1f6a9e6..4178c33000 100644 --- a/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py +++ b/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py @@ -979,14 +979,25 @@ class M2O(GenericText): def _completion_match_selected( self, completion, model, iter_, record, field, model_name): - rec_name, record_id = model.get(iter_, 0, 1) - field.set_client( - record, self.value_from_id(model_name, record_id, rec_name)) + rec_name, record_id, defaults = model.get(iter_, 0, 1, 2) + if record_id is not None: + field.set_client( + record, self.value_from_id(model_name, record_id, rec_name)) - completion.get_entry().set_text(rec_name) - completion_model = completion.get_model() - completion_model.clear() - completion_model.search_text = rec_name + completion.get_entry().set_text(rec_name) + completion_model = completion.get_model() + completion_model.clear() + completion_model.search_text = rec_name + else: + entry = completion.get_entry() + entry.handler_block(entry.editing_done_id) + + def callback(): + entry.handler_unblock(entry.editing_done_id) + entry.set_text(field.get_client(record)) + self.open_remote( + record, create=True, text=entry.get_text(), defaults=defaults, + callback=callback) return True def _update_completion(self, entry, record, field): diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 7d01f255c6..8738718316 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Support default values for autocomplete * Support default value from context * Add autocomplete method on ModelView * Add likify tools diff --git a/trytond/doc/ref/models.rst b/trytond/doc/ref/models.rst index 209a81c954..488d051507 100644 --- a/trytond/doc/ref/models.rst +++ b/trytond/doc/ref/models.rst @@ -245,10 +245,14 @@ Instance methods: .. method:: ModelView.autocomplete(text[, domain[, limit[, order]]]) Return a list of dictionary for records completing ``text``. - The dictionary is composed of ``id`` and ``name``. + The dictionary is composed of ``id``, ``name`` and ``defaults``. The client call this method to provide completion. + .. note:: + The ``defaults`` is used when ``id`` is empty to get the default values + for the record to create. + ModelStorage ============ diff --git a/trytond/trytond/model/modelview.py b/trytond/trytond/model/modelview.py index cf37eb45ae..31366143d3 100644 --- a/trytond/trytond/model/modelview.py +++ b/trytond/trytond/model/modelview.py @@ -813,7 +813,9 @@ class ModelView(Model): if issubclass(cls, ModelStorage): result = cls.search_read( domain, limit=limit, order=order, fields_names=['rec_name']) - result = [{'id': r['id'], 'name': r['rec_name']} for r in result] + result = [ + {'id': r['id'], 'name': r['rec_name'], 'defaults': None} + for r in result] return result @property diff --git a/trytond/trytond/tests/test_modelview.py b/trytond/trytond/tests/test_modelview.py index 2e32f1bd2a..4a0d5dad48 100644 --- a/trytond/trytond/tests/test_modelview.py +++ b/trytond/trytond/tests/test_modelview.py @@ -734,15 +734,16 @@ class ModelView(unittest.TestCase): self.assertEqual(Model.autocomplete('test'), []) self.assertEqual( Model.autocomplete('foo'), - [{'name': 'foo', 'id': foo.id}]) + [{'name': 'foo', 'id': foo.id, 'defaults': None}]) self.assertEqual( Model.autocomplete(''), [ - {'name': 'foo', 'id': foo.id}, - {'name': 'bar', 'id': bar.id}]) + {'name': 'foo', 'id': foo.id, 'defaults': None}, + {'name': 'bar', 'id': bar.id, 'defaults': None}]) self.assertEqual(Model.autocomplete('foo', [('id', '!=', foo.id)]), []) self.assertEqual( - Model.autocomplete('', limit=1), [{'name': 'foo', 'id': foo.id}]) + Model.autocomplete('', limit=1), + [{'name': 'foo', 'id': foo.id, 'defaults': None}]) self.assertEqual( Model.autocomplete('', order=[('name', 'ASC')]), [ - {'name': 'bar', 'id': bar.id}, - {'name': 'foo', 'id': foo.id}]) + {'name': 'bar', 'id': bar.id, 'defaults': None}, + {'name': 'foo', 'id': foo.id, 'defaults': None}])