From 699cc2e5bc2924a8459879cbf77872f40b54c6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20D=2E=20D=C3=ADaz?= Date: Wed, 28 Apr 2021 17:47:50 +0200 Subject: [PATCH] [IMP] web_widget_one2many_product_picker: Instant search mode --- web_widget_one2many_product_picker/README.rst | 2 + .../readme/CONFIGURE.rst | 2 + .../static/description/index.html | 4 + .../quick_create_form_view.js | 62 ++- .../js/views/One2ManyProductPicker/record.js | 284 +++++++++++-- .../views/One2ManyProductPicker/renderer.js | 389 ++++++++++-------- .../static/src/js/views/basic_controller.js | 35 ++ .../static/src/js/views/basic_model.js | 272 +++++++++++- .../widgets/field_one2many_product_picker.js | 200 ++++----- .../src/scss/one2many_product_picker.scss | 49 ++- .../src/xml/one2many_product_picker.xml | 192 ++++----- .../one2many_product_picker_quick_create.xml | 7 +- .../templates/assets.xml | 4 + 13 files changed, 1064 insertions(+), 438 deletions(-) create mode 100644 web_widget_one2many_product_picker/static/src/js/views/basic_controller.js diff --git a/web_widget_one2many_product_picker/README.rst b/web_widget_one2many_product_picker/README.rst index 99b02c73e..7f7e54dfa 100644 --- a/web_widget_one2many_product_picker/README.rst +++ b/web_widget_one2many_product_picker/README.rst @@ -91,6 +91,8 @@ Widget options: modify/create a record with the widget. * ignore_warning > Enable/Disable display onchange warnings (False by default) +* instant_search > Enable/Disable instant search mode (False by default) +* trigger_refresh_fields > Fields in the main record that dispatch a widget refresh (["partner_id", "currency_id"] by default) All widget options are optional. Notice that you can call '_' method to use translations. This only can be used with this widget. diff --git a/web_widget_one2many_product_picker/readme/CONFIGURE.rst b/web_widget_one2many_product_picker/readme/CONFIGURE.rst index 581d56152..984006746 100644 --- a/web_widget_one2many_product_picker/readme/CONFIGURE.rst +++ b/web_widget_one2many_product_picker/readme/CONFIGURE.rst @@ -49,6 +49,8 @@ Widget options: modify/create a record with the widget. * ignore_warning > Enable/Disable display onchange warnings (False by default) +* instant_search > Enable/Disable instant search mode (False by default) +* trigger_refresh_fields > Fields in the main record that dispatch a widget refresh (["partner_id", "currency_id"] by default) All widget options are optional. Notice that you can call '_' method to use translations. This only can be used with this widget. diff --git a/web_widget_one2many_product_picker/static/description/index.html b/web_widget_one2many_product_picker/static/description/index.html index 39353f7b4..84b25088e 100644 --- a/web_widget_one2many_product_picker/static/description/index.html +++ b/web_widget_one2many_product_picker/static/description/index.html @@ -479,6 +479,10 @@ modify/create a record with the widget.

  • ignore_warning > Enable/Disable display onchange warnings (False by default)

  • +
  • instant_search > Enable/Disable instant search mode (False by default)

    +
  • +
  • trigger_refresh_fields > Fields in the main record that dispatch a widget refresh ([“partner_id”, “currency_id”] by default)

    +
  • All widget options are optional. Notice that you can call ‘_’ method to use translations. This only can be used with this widget.

    diff --git a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form_view.js b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form_view.js index 774526a14..0c5b178bb 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form_view.js +++ b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form_view.js @@ -62,6 +62,15 @@ odoo.define( this._super.apply(this, arguments); }, + /** + * @override + */ + _applyChanges: function() { + return this._super.apply(this, arguments).then(() => { + this._updateButtons(); + }); + }, + /** * Create or accept changes */ @@ -227,7 +236,6 @@ odoo.define( this.trigger_up("quick_record_updated", { changes: ev.data.changes, }); - this._updateButtons(); } } }, @@ -261,7 +269,6 @@ odoo.define( saving: false, }); this.model.unsetDirty(this.handle); - // Self._updateButtons(); this._enableQuickCreate(); }, }); @@ -298,13 +305,25 @@ odoo.define( this.trigger_up("restore_flip_card", { success_callback: function() { - self.trigger_up("update_quick_record", { - id: record.id, - callback: function() { - self.model.unsetDirty(self.handle); - // Self._updateButtons(); - self._enableQuickCreate(); - }, + // Qty are handled in a special way because can be modified without + // wait for server response + self.model.localData[record.id].data[ + self.fieldMap.product_uom_qty + ] = record.data[self.fieldMap.product_uom_qty]; + // SaveRecord used to make a save point. + self.saveRecord(self.handle, { + stayInEdit: true, + reload: true, + savePoint: true, + viewType: "form", + }).then(() => { + self.trigger_up("update_quick_record", { + id: record.id, + callback: function() { + self.model.unsetDirty(self.handle); + self._enableQuickCreate(); + }, + }); }); }, block: true, @@ -314,29 +333,30 @@ odoo.define( _discard: function() { if (this._disabled) { // Don't do anything if we are already creating a record - return Promise.resolve(); + return; } + this._disableQuickCreate(); - const record = this.model.get(this.handle); + this.model.updateRecordContext(this.handle, { + has_changes_confirmed: true, + }); + // Rollback to restore the save point this.model.discardChanges(this.handle, { rollback: true, }); + const record = this.model.get(this.handle); this.trigger_up("quick_record_updated", { changes: record.data, }); - if (this.model.isNew(record.id)) { - this.update({}, {reload: false}); + + this.update({}, {reload: false}).then(() => { + if (!this.model.isNew(record.id)) { + this.model.unsetDirty(this.handle); + } this.trigger_up("restore_flip_card"); this._updateButtons(); this._enableQuickCreate(); - } else { - this.update({}, {reload: false}).then(() => { - this.model.unsetDirty(this.handle); - this.trigger_up("restore_flip_card"); - this._updateButtons(); - this._enableQuickCreate(); - }); - } + }); }, /** diff --git a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js index 8eea2653b..652864ec7 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js +++ b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js @@ -42,7 +42,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu front: [], back: [], }; - this._lazyUpdateRecord = _.debounce(this._updateRecord.bind(this), 450); }, @@ -51,8 +50,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * * @returns {Object} */ - generateVirtualState: function() { - return this._generateVirtualState().then(this.recreate.bind(this)); + generateVirtualState: function(simple_mode) { + return this._generateVirtualState(undefined, undefined, simple_mode).then( + this.recreate.bind(this) + ); }, /** @@ -80,11 +81,26 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @override */ destroy: function() { + this.abortTimeouts(); + if (this.state) { + this.options.basicFieldParams.model.removeVirtualRecord(this.state.id); + } this.$el.remove(); this.$card.off(""); this._super.apply(this, arguments); }, + abortTimeouts: function() { + if (this._timerOnChange) { + clearTimeout(this._timerOnChange); + this._timerOnChange = false; + } + if (this.state) { + const model = this.options.basicFieldParams.model; + model.updateRecordContext(this.state.id, {aborted: true}); + } + }, + /** * @override */ @@ -105,6 +121,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @returns {Promise} */ recreate: function(state) { + if (!this.getParent()) { + // It's a zombie record! ensure kill it! + this.destroy(); + return; + } + if (state) { this._setState(state); } @@ -121,6 +143,15 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu return this._render(); }, + markToDestroy: function() { + this.toDestroy = true; + this.$el.hide(); + }, + + isMarkedToDestroy: function() { + return this.toDestroy === true; + }, + /** * Generates the URL for the given product using the selected field * @@ -151,7 +182,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu price, this.state.fields[field_name], this.options.currencyField, - this.state.data + this.options.basicFieldParams.record.data ); }, @@ -184,12 +215,15 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu this.fields = this.getParent().state.fields; this.fieldsInfo = this.getParent().state.fieldsInfo.form; + const model = this.options.basicFieldParams.model; + if (this.state && (!viewState || this.state.id !== viewState.id)) { + model.removeVirtualRecord(this.state.id); + } this.state = viewState; if (recordSearch) { this.recordSearch = recordSearch; } - const model = this.options.basicFieldParams.model; this.is_virtual = (this.state && model.isPureVirtual(this.state.id)) || false; @@ -203,6 +237,16 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu this._incProductQty(lazy_qty - 1); } } + + this._setMasterUomMap(); + }, + + _setMasterUomMap: function() { + this.master_uom_map = { + field_uom: "product_uom", + field_uom_qty: "product_uom_qty", + search_field_uom: "uom_id", + }; }, /** @@ -232,6 +276,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu auto_save: this.options.autoSave, is_saving: record && record.context.saving, lazy_qty: record && record.context.lazy_qty, + has_onchange: record && !record.context.not_onchange, + field_uom: this.master_uom_map.field_uom, + field_uom_qty: this.master_uom_map.field_uom_qty, }; }, @@ -243,25 +290,151 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu */ _getInternalVirtualRecordContext: function() { const context = {}; - context["default_" + this.options.basicFieldParams.relation_field] = - this.options.basicFieldParams.state.id || null; + context[`default_${this.options.basicFieldParams.relation_field}`] = + this.options.basicFieldParams.record.id || null; + context[`default_${this.options.fieldMap.product}`] = + this.recordSearch.id || null; + context[`default_${this.master_uom_map.field_uom_qty}`] = 1.0; return context; }, /** * Forced data used in virtual states. - * Be careful with the onchanges sequence. Think as user interaction, not as CRUD operation. + * Be careful with the onchanges sequence. Think as user interaction ("ADD", "DELETE", ... commands), not as CRUD operation. * * @private * @returns {Object} */ _getInternalVirtualRecordData: function() { - const data = {}; - data[this.options.fieldMap.product] = { - operation: "ADD", - id: this.recordSearch.id, - }; - return data; + // To be overwritten + return {}; + }, + + _generateVirtualStateSimple: function(context, def_values) { + const model = this.options.basicFieldParams.model; + return new Promise(resolve => { + const record_def = model.createVirtualDatapoint( + this.options.basicFieldParams.value.id, + { + context: context, + } + ); + model.applyDefaultValues(record_def.record.id, def_values).then(() => { + const new_state = model.get(record_def.record.id); + const product_uom_id = + new_state.data[ + this.options.fieldMap[this.master_uom_map.field_uom] + ].id; + // Apply default values + model + .applyDefaultValues( + product_uom_id, + { + display_name: this.recordSearch[ + this.master_uom_map.search_field_uom + ][1], + }, + { + fieldNames: ["display_name"], + } + ) + .then(() => { + return model._fetchRelationalData(record_def.record); + }) + .then(() => { + return model._postprocess(record_def.record); + }) + .then(() => { + this._timerOnChange = setTimeout( + (current_batch_id, record_def) => { + this._timerOnChange = false; + if ( + current_batch_id != + this.options.basicFieldParams + .current_batch_id || + record_def.record.context.aborted + ) { + return; + } + model + ._makeDefaultRecordNoDatapoint( + record_def.record, + record_def.params + ) + .then(() => { + if (record_def.record.context.aborted) { + return; + } + model.updateRecordContext( + record_def.record.id, + { + not_onchange: false, + } + ); + this.recreate( + model.get(record_def.record.id) + ); + }); + }, + 750, + this.options.basicFieldParams.current_batch_id, + record_def + ); + + resolve(model.get(record_def.record.id)); + }); + }); + }); + }, + + _generateVirtualStateFull: function(data, context, def_values) { + const model = this.options.basicFieldParams.model; + return new Promise(resolve => { + model + .createVirtualRecord(this.options.basicFieldParams.value.id, { + context: context, + }) + .then(result => { + // Apply default values + model + .applyDefaultValues(result.record.id, def_values) + .then(() => { + const new_state = model.get(result.record.id); + const product_uom_id = + new_state.data[ + this.options.fieldMap[ + this.master_uom_map.field_uom + ] + ].id; + model + .applyDefaultValues( + product_uom_id, + { + display_name: this.recordSearch[ + this.master_uom_map.search_field_uom + ][1], + }, + { + fieldNames: ["display_name"], + } + ) + .then(() => { + const sdata = _.extend( + {}, + this._getInternalVirtualRecordData(), + data + ); + this._applyChanges( + result.record.id, + sdata, + result.params + ).then(() => + resolve(model.get(result.record.id)) + ); + }); + }); + }); + }); }, /** @@ -270,19 +443,39 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @param {Object} context * @returns {Object} */ - _generateVirtualState: function(data, context) { - const model = this.options.basicFieldParams.model; + _generateVirtualState: function(data, context, simple_mode) { const scontext = _.extend( {}, this._getInternalVirtualRecordContext(), context ); - // Force qty to 1.0 to launch correct onchanges - scontext[`default_${this.options.fieldMap.product_uom_qty}`] = 1.0; - const sdata = _.extend({}, this._getInternalVirtualRecordData(), data); - return model.createVirtualRecord(this.options.basicFieldParams.value.id, { - data: sdata, - context: scontext, + // Apply default values + const def_values = { + [this.options.fieldMap.product]: this.recordSearch.id, + [this.options.fieldMap[this.master_uom_map.field_uom_qty]]: 1.0, + [this.options.fieldMap[this.master_uom_map.field_uom]]: this + .recordSearch[this.master_uom_map.search_field_uom][0], + }; + if (simple_mode) { + return this._generateVirtualStateSimple(scontext, def_values); + } + return this._generateVirtualStateFull(data, scontext, def_values); + }, + + /** + * Apply changes (with onchange) + * + * @param {Integer/String} record_id + * @param {Object} changes + * @param {Object} options + */ + _applyChanges: function(record_id, changes, options) { + const model = this.options.basicFieldParams.model; + return model._applyChange(record_id, changes, options).then(() => { + model.updateRecordContext(record_id, { + not_onchange: false, + }); + this.recreate(model.get(record_id)); }); }, @@ -482,15 +675,18 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu this.$el.find(to_find.join()).each((key, value) => { const $elm = $(value); const format_out = $elm.data("esc") || $elm.data("field"); - $elm.html( - py.eval(format_out, _.extend({}, state_data, this.recordSearch)) + const text_out = py.eval( + format_out, + _.extend({}, state_data, this.recordSearch) ); + $elm.html(text_out); + $elm.attr("title", text_out); }); if (this.options.showDiscount) { const field_map = this.options.fieldMap; if (state_data) { - const has_discount = state_data[field_map.discount] > 0.0; + const has_discount = state_data[field_map.discount] !== 0.0; this.$el .find(".original_price,.discount_price") .toggleClass("d-none", !has_discount); @@ -526,7 +722,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu price_reduce, this.state.fields[field_map.price_unit], this.options.currencyField, - this.state.data + this.options.basicFieldParams.record.data ) ); }, @@ -594,9 +790,14 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu if (record.context.saving) { return Promise.resolve(); } - const changes = _.pick(record.data, this.options.fieldMap.product_uom_qty); - if (changes[this.options.fieldMap.product_uom_qty] === 0) { - changes[this.options.fieldMap.product_uom_qty] = 1; + const changes = _.pick( + record.data, + this.options.fieldMap[this.master_uom_map.field_uom_qty] + ); + if ( + changes[this.options.fieldMap[this.master_uom_map.field_uom_qty]] === 0 + ) { + changes[this.options.fieldMap[this.master_uom_map.field_uom_qty]] = 1; } this.$card.addClass("blocked"); return model.notifyChanges(record.id, changes).then(() => { @@ -627,15 +828,25 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu // wait for the Odoo response. const model_record_data = model.localData[this.state.id].data; if ( - _.isNull(model_record_data[this.options.fieldMap.product_uom_qty]) + _.isNull( + model_record_data[ + this.options.fieldMap[this.master_uom_map.field_uom_qty] + ] + ) ) { - model_record_data[this.options.fieldMap.product_uom_qty] = 1; + model_record_data[ + this.options.fieldMap[this.master_uom_map.field_uom_qty] + ] = 1; } - model_record_data[this.options.fieldMap.product_uom_qty] += amount; + model_record_data[ + this.options.fieldMap[this.master_uom_map.field_uom_qty] + ] += amount; return model .notifyChanges(record.id, { - [this.options.fieldMap.product_uom_qty]: - model_record_data[this.options.fieldMap.product_uom_qty], + [this.options.fieldMap[this.master_uom_map.field_uom_qty]]: + model_record_data[ + this.options.fieldMap[this.master_uom_map.field_uom_qty] + ], }) .then(() => { this._processDynamicFields(); @@ -836,6 +1047,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu _onRestoreFlipCard: function(evt) { this.$card.removeClass("active"); this.$front.removeClass("d-none"); + const $img = this.$front.find("img"); + const cur_img_src = $img.attr("src"); if (this.$card.hasClass("oe_flip_card_maximized")) { this.$card.removeClass("oe_flip_card_maximized"); this.$card.on("transitionend", () => { @@ -859,6 +1072,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu if (evt.data.block) { this.$card.addClass("blocked"); } + $img.attr("src", $img.data("srcAlt")); + $img.data("srcAlt", cur_img_src); }, /** @@ -878,6 +1093,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu */ _onQuickRecordUpdated: function(evt) { this._processDynamicFields(Object.keys(evt.data.changes)); + // This.recreate(); this.trigger_up("update_subtotal"); }, }); diff --git a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/renderer.js b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/renderer.js index 48b6e2a17..994cd5252 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/renderer.js +++ b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/renderer.js @@ -40,9 +40,7 @@ odoo.define( // 'receive' more arguments. this.options = parent.options; this.mode = parent.mode; - this.search_data = parent._searchRecords; this.search_group = parent._activeSearchGroup; - this.last_search_data_count = parent._lastSearchRecordsCount; }, /** @@ -50,7 +48,7 @@ odoo.define( */ on_attach_callback: function() { this._isInDom = true; - _.invoke(this.widgets, "on_attach_callback"); + _.invoke(_.compact(this.widgets), "on_attach_callback"); }, /** @@ -58,7 +56,7 @@ odoo.define( */ on_detach_callback: function() { this._isInDom = false; - _.invoke(this.widgets, "on_detach_callback"); + _.invoke(_.compact(this.widgets), "on_detach_callback"); }, /** @@ -78,16 +76,10 @@ odoo.define( }, /** - * @param {Object} search_data - * @param {Number} count * @param {Object} search_group */ - updateSearchData: function(search_data, count, search_group) { - this.search_data = search_data; - this.last_search_data_count = count; + updateSearchGroup: function(search_group) { this.search_group = search_group; - this._loadMoreWorking = false; - this.$btnLoadMore.attr("disabled", false); }, /** @@ -115,20 +107,11 @@ odoo.define( }); }, - /** - * Recreate the given widget by the state id - * - * @param {String} state_id - * @param {Object} new_state - */ - updateRecord: function(state_id, new_state) { - for (let eb = this.widgets.length - 1; eb >= 0; --eb) { - const widget = this.widgets[eb]; - if (widget.state.id === state_id) { - widget.recreate(new_state); - break; - } - } + _isValidLineState: function(state) { + return ( + state.data[this.options.field_map.product] && + state.data[this.options.field_map.product].data.id + ); }, _isEqualState: function(state_a, state_b) { @@ -137,9 +120,31 @@ odoo.define( } const product_id_a = state_a.data[this.options.field_map.product].data.id; + const product_uom_id_a = + state_a.data[this.options.field_map.product_uom].data.id; const product_id_b = state_b.data[this.options.field_map.product].data.id; - return product_id_a === product_id_b; + const product_uom_id_b = + state_b.data[this.options.field_map.product_uom].data.id; + + return ( + product_id_a === product_id_b && + product_uom_id_a === product_uom_id_b + ); + }, + + _existsWidgetWithState: function(state) { + for (let eb = this.widgets.length - 1; eb >= 0; --eb) { + const widget = this.widgets[eb]; + if ( + widget && + widget.state && + this._isEqualState(widget.state, state) + ) { + return true; + } + } + return false; }, /** @@ -152,63 +157,52 @@ odoo.define( * @returns {Array} */ _processStatesToDestroy: function(states) { + // Get widgets to destroy + // Update states only affect to "non pure virtual" records const to_destroy = []; + const to_add = []; for (const state of states) { for (let e = this.widgets.length - 1; e >= 0; --e) { const widget = this.widgets[e]; if (widget && this._isEqualState(widget.state, state)) { - to_destroy.push(widget); - delete this.widgets[e]; - } - } - } - - // If doesn't exists other records with the same product, we need - // create a 'pure virtual' record again. - const to_add = []; - for (const index_destroy in to_destroy) { - const widget_destroyed = to_destroy[index_destroy]; - let found = false; - // If already exists a widget for the product don't try create a new one - for (let eb = this.widgets.length - 1; eb >= 0; --eb) { - const widget = this.widgets[eb]; - if ( - widget && - widget.state && - this._isEqualState(widget.state, widget_destroyed.state) - ) { - found = true; - break; - } - } - - if (!found) { - // Get the new state ID if exists to link it with the new record - let state_id = null; - for (let eb = this.state.data.length - 1; eb >= 0; --eb) { - const state = this.state.data[eb]; - if (this._isEqualState(state, widget_destroyed.state)) { - state_id = state.id; - break; + // If already exists a widget for the product don't try create a new one + let recreated = false; + if (!this._existsWidgetWithState(widget.state)) { + // Get the new state ID if exists to link it with the new record + // This happens when remove a record that have a new state info + for ( + let eb = this.state.data.length - 1; + eb >= 0; + --eb + ) { + const state = this.state.data[eb]; + if (!this._isValidLineState(state)) { + continue; + } + if (this._isEqualState(state, widget.state)) { + widget.recreate(state); + recreated = true; + break; + } + } + } + if (!recreated) { + widget.markToDestroy(); + to_destroy.push(widget); + const search_record = _.omit( + widget.recordSearch, + "__id" + ); + + to_add.push([ + [search_record], + { + no_attach_widgets: false, + no_process_records: false, + position: widget.state.id, + }, + ]); } - } - // "Lines" section doesn't show virtual records - if ( - (state_id && this.search_group.name === "main_lines") || - this.search_group.name !== "main_lines" - ) { - const widget_product_id = - widget_destroyed.state.data[ - this.options.field_map.product - ].data.id; - const search_record = _.find(this.search_data, { - id: widget_product_id, - }); - const new_search_record = _.extend({}, search_record, { - __id: state_id, - }); - const card_id = widget_destroyed.$el.data("cardId"); - to_add.push([[new_search_record], false, true, card_id]); } } } @@ -225,12 +219,16 @@ odoo.define( */ _processCurrentStates: function() { // Records to Update or Create + const model = this.getParent().getBasicFieldParams().model; const to_destroy = []; const to_add = []; for (const index in this.state.data) { const state = this.state.data[index]; + if (!this._isValidLineState(state)) { + continue; + } let exists = false; - let search_record_index = -1; + let search_record_index = false; let search_record = false; for (let e = this.widgets.length - 1; e >= 0; --e) { const widget = this.widgets[e]; @@ -238,24 +236,28 @@ odoo.define( // Already processed widget (deleted) continue; } - if (this._isEqualState(widget.state, state)) { - var model = this.getParent().getBasicFieldParams().model; - var record = model.get(widget.state.id); + + const is_equal_state = this._isEqualState(widget.state, state); + if (widget.isMarkedToDestroy()) { + exists = true; + } else if (is_equal_state) { + const record = model.get(widget.state.id); model.updateRecordContext(state.id, { lazy_qty: record.context.lazy_qty || 0, }); widget.recreate(state); exists = true; break; - } else if ( + } + if ( + !is_equal_state && widget.recordSearch.id === - state.data[this.options.field_map.product].data.id + state.data[this.options.field_map.product].data.id ) { // Is a new record (can be other record for the same 'search record' or a replacement for a pure virtual) - search_record_index = widget.$el.index(); + search_record_index = widget.state.id; search_record = widget.recordSearch; - var model = this.getParent().getBasicFieldParams().model; - var record = model.get(widget.state.id); + const record = model.get(widget.state.id); model.updateRecordContext(state.id, { lazy_qty: record.context.lazy_qty || 0, }); @@ -273,16 +275,18 @@ odoo.define( this.state.data = _.compact(this.state.data); - // Need add a new one? - if (!exists && search_record_index !== -1) { + // Add to create the new record + if (!exists && search_record_index) { const new_search_record = _.extend({}, search_record, { __id: state.id, }); to_add.push([ [new_search_record], - false, - true, - search_record_index, + { + no_attach_widgets: true, + no_process_records: true, + position: search_record_index, + }, ]); } } @@ -304,9 +308,15 @@ odoo.define( const states_to_destroy = []; for (const index in old_states) { const old_state = old_states[index]; + if (!this._isValidLineState(old_state)) { + continue; + } let found = false; for (const e in this.state.data) { const current_state = this.state.data[e]; + if (!this._isValidLineState(current_state)) { + continue; + } if (this._isEqualState(current_state, old_state)) { found = true; break; @@ -323,47 +333,40 @@ odoo.define( states_to_destroy ); - // Make widgets to destroy invisible to avoid render 'dance' - for (const widget of to_destroy_old) { - widget.$el.hide(); + const [ + destroyed_current, + to_add_current, + ] = this._processCurrentStates(); + + const currentTasks = []; + const to_add = [].concat(to_add_current, to_add_virtual); + for (const params of to_add) { + currentTasks.push(this.appendSearchRecords.apply(this, params)[0]); } - const oldTasks = []; - for (const params of to_add_virtual) { - oldTasks.push(this.appendSearchRecords.apply(this, params)[0]); - } - Promise.all(oldTasks).then(() => { - const [ - to_destroy_current, - to_add_current, - ] = this._processCurrentStates(); - - // Make widgets to destroy invisible to avoid render 'dance' - for (const widget of to_destroy_current) { - widget.$el.hide(); - } - - const currentTasks = []; - for (const params of to_add_current) { - currentTasks.push( - this.appendSearchRecords.apply(this, params)[0] - ); - } - Promise.all(currentTasks).then(() => { - _.invoke(to_destroy_old, "destroy"); - _.invoke(to_destroy_current, "destroy"); - def.resolve(); - }); + Promise.all(currentTasks).then(() => { + _.invoke(to_destroy_old, "destroy"); + _.invoke(destroyed_current, "destroy"); + this.widgets = _.difference(this.widgets, to_destroy_old); + def.resolve(); }); return def; }, + clearRecords: function() { + _.invoke(_.compact(this.widgets), "destroy"); + this.widgets = []; + if (this.$recordsContainer) { + this.$recordsContainer.empty(); + } + }, + /** * @override */ _renderView: function() { - const oldWidgets = _.compact(this.widgets); + _.invoke(_.compact(this.widgets), "destroy"); this.widgets = []; this.$recordsContainer = $("
    ", { class: "w-100 row", @@ -374,23 +377,14 @@ odoo.define( this.$btnLoadMore = this.$extraButtonsContainer.find( "#productPickerLoadMore" ); - this.search_data = this._sort_search_data(this.search_data); - return new Promise(resolve => { - const defs = this.appendSearchRecords(this.search_data, true); - Promise.all(defs).then(() => { - _.invoke(oldWidgets, "destroy"); - this.$el.empty(); - this.$el.append(this.$recordsContainer); - this.$el.append(this.$extraButtonsContainer); - this.showLoadMore( - this.last_search_data_count >= this.options.records_per_page - ); - if (this._isInDom) { - _.invoke(this.widgets, "on_attach_callback"); - } - return resolve(); - }); - }); + // This.search_data = this._sort_search_data(this.search_data); + this.$el.empty(); + this.$el.append(this.$recordsContainer); + this.$el.append(this.$extraButtonsContainer); + // This.showLoadMore( + // this.last_search_data_count >= this.options.records_per_page + // ); + return this._super.apply(this, arguments); }, /** @@ -405,7 +399,10 @@ odoo.define( for (const index_state in this.state.data) { const state_data = this.state.data[index_state]; - if (state_data.data[field_name].res_id === data.id) { + if ( + this._isValidLineState(state_data) && + state_data.data[field_name].res_id === data.id + ) { data._order_value = state_data.res_id; } } @@ -431,32 +428,76 @@ odoo.define( _processSearchRecords: function(results) { const field_name = this.options.field_map.product; const records = []; + const states = []; + + var test_values = function(field_value, record_search) { + return ( + (typeof field_value === "object" && + field_value.data.id === record_search.id) || + field_value === record_search.id + ); + }; + for (const index in results) { const record_search = results[index]; let state_data_found = false; + // Analyze 'pure virtual' records + // Pure virtual records aren't linked with field list + // so we need search them linked in the widgets. + for (const index_widget in this.widgets) { + const widget = this.widgets[index_widget]; + if (widget.isMarkedToDestroy()) { + continue; + } + if ( + record_search.__id === widget.state.id || + (!record_search.__id && + widget.recordSearch.id === record_search.id) + ) { + state_data_found = true; + if (widget.state) { + states.push(widget.state); + } + break; + } + } + + // If already exists a widget with the search result + // avoid create a new one + if (state_data_found) { + continue; + } + + // Analyze field records + // If not found any widget we need create a new one + // linked with the state record for (const index_data in this.state.data) { const state_record = this.state.data[index_data]; - const field = state_record.data[field_name]; - if ( - (typeof field === "object" && - field.data.id === record_search.id) || - field === record_search.id - ) { + if (!this._isValidLineState(state_record)) { + continue; + } + const field_value = state_record.data[field_name]; + if (test_values(field_value, record_search)) { records.push( _.extend({}, record_search, { __id: state_record.id, }) ); + states.push(state_record); state_data_found = true; } } + if (!state_data_found) { records.push(record_search); } } - return records; + return { + records: records, + states: states, + }; }, /** @@ -502,15 +543,12 @@ odoo.define( * @param {Boolean} no_process_records * @param {Number} position */ - _appendSearchRecords: function( - search_records, - no_process_records, - position - ) { - const processed_records = no_process_records + _appendSearchRecords: function(search_records, options) { + const processed_info = options.no_process_records ? search_records : this._processSearchRecords(search_records); - _.each(processed_records, search_record => { + const records_to_add = processed_info.records || search_records; + _.each(records_to_add, search_record => { const state_data = this._getRecordDataById(search_record.__id); const widget_options = this._getRecordOptions(search_record); widget_options.renderer_widget_index = this.widgets.length; @@ -524,7 +562,9 @@ odoo.define( // Simulate new lines to dispatch get_default & onchange's to get the // relevant data to print. This case increase the TTI time. if (!state_data) { - const defVirtualState = ProductPickerRecord.generateVirtualState(); + const defVirtualState = ProductPickerRecord.generateVirtualState( + this.options.instant_search + ); this.defsVirtualState.push(defVirtualState); } @@ -536,15 +576,39 @@ odoo.define( function(widget, widget_position) { if (typeof widget_position !== "undefined") { const $elm = this.$el.find( - `[data-card-id="${position}"]` + `[data-card-id="${widget_position}"]:first` ); widget.$el.insertBefore($elm); } def.resolve(); - }.bind(this, ProductPickerRecord, position) + }.bind(this, ProductPickerRecord, options.position) ); this.defs.push(def); }); + // Destroy unused + if (options.cleanup) { + const num_widgets = this.widgets.length; + for ( + let index_widget = num_widgets - 1; + index_widget >= 0; + --index_widget + ) { + const widget = this.widgets[index_widget]; + let found_state = false; + for (const state of processed_info.states) { + if (widget.state && widget.state.id === state.id) { + found_state = true; + break; + } + } + if (!found_state && widget.state) { + widget.destroy(); + delete this.widgets[index_widget]; + } + } + // Clean widget array + this.widgets = _.compact(this.widgets); + } }, /** @@ -563,24 +627,20 @@ odoo.define( * @param {Number} position * @returns {Array} */ - appendSearchRecords: function( - search_records, - no_attach_widgets, - no_process_records, - position - ) { + appendSearchRecords: function(search_records, options = {}) { this.trigger_up("loading_records"); this.defs = []; this.defsVirtualState = []; const cur_widget_index = this.widgets.length; - this._appendSearchRecords(search_records, no_process_records, position); + this._appendSearchRecords(search_records, options); + const defs = this.defs; delete this.defs; const defsVirtualState = this.defsVirtualState; delete this.defsVirtualState; return [ Promise.all(defs).then(() => { - if (!no_attach_widgets && this._isInDom) { + if (!options.no_attach_widgets && this._isInDom) { const new_widgets = this.widgets.slice(cur_widget_index); _.invoke(new_widgets, "on_attach_callback"); } @@ -597,7 +657,6 @@ odoo.define( _onClickLoadMore: function() { this.$btnLoadMore.attr("disabled", true); this.trigger_up("load_more"); - this._loadMoreWorking = true; }, /** diff --git a/web_widget_one2many_product_picker/static/src/js/views/basic_controller.js b/web_widget_one2many_product_picker/static/src/js/views/basic_controller.js new file mode 100644 index 000000000..68176cbff --- /dev/null +++ b/web_widget_one2many_product_picker/static/src/js/views/basic_controller.js @@ -0,0 +1,35 @@ +// Copyright 2021 Tecnativa - Alexandre Díaz +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +odoo.define("web_widget_one2many_product_picker.BasicController", function(require) { + "use strict"; + + const BasicController = require("web.BasicController"); + + BasicController.include({ + /** + * This is necessary to refresh 'one2many_product_picker' when some 'trigger_refresh_fields' fields changes. + * + * @override + */ + _confirmChange: function(id, fields, e) { + return this._super.apply(this, arguments).then(() => { + const product_picker_widgets = _.filter( + this.renderer.allFieldWidgets[this.handle], + item => item.attrs.widget === "one2many_product_picker" + ); + for (const widget of product_picker_widgets) { + const trigger_fields = widget.options.trigger_refresh_fields || []; + if ( + _.difference(trigger_fields, fields).length !== + trigger_fields.length + ) { + widget._reset(this.model.get(this.handle), e); + // Force re-launch onchanges on 'pure virtual' records + widget.renderer.clearRecords(); + widget._render(); + } + } + }); + }, + }); +}); diff --git a/web_widget_one2many_product_picker/static/src/js/views/basic_model.js b/web_widget_one2many_product_picker/static/src/js/views/basic_model.js index 0eaa7eba6..8b3bf4170 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/basic_model.js +++ b/web_widget_one2many_product_picker/static/src/js/views/basic_model.js @@ -7,19 +7,37 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) { BasicModel.include({ /** - * @param {Number/String} handle + * @override + */ + init: function() { + this._super.apply(this, arguments); + }, + + /** + * This is necessary because 'pure virtual' records + * can be destroyed at any time. + * + * @param {String} id + * @returns {Boolean} + */ + exists: function(id) { + return !_.isEmpty(this.localData[id]); + }, + + /** + * @param {String} id * @param {Object} context */ - updateRecordContext: function(handle, context) { - this.localData[handle].context = _.extend( + updateRecordContext: function(id, context) { + this.localData[id].context = _.extend( {}, - this.localData[handle].context, + this.localData[id].context, context ); }, /** - * @param {Number/String} id + * @param {String} id * @returns {Boolean} */ isPureVirtual: function(id) { @@ -28,7 +46,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) { }, /** - * @param {Number/String} id + * @param {String} id * @param {Boolean} status */ setPureVirtual: function(id, status) { @@ -41,7 +59,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) { }, /** - * @param {Number/String} id + * @param {String} id */ unsetDirty: function(id) { const data = this.localData[id]; @@ -52,7 +70,204 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) { }, /** - * Generates a virtual records without link it + * 'Pure virtual' records are not used by other + * elements so can be removed safesly + * + * @param {String} id + */ + removeVirtualRecord: function(id) { + if (!this.isPureVirtual(id)) { + return false; + } + + const data = this.localData[id]; + const to_remove = []; + this._visitChildren(data, item => { + to_remove.push(item.id); + }); + + to_remove.reverse(); + for (const remove_id of to_remove) { + this.removeLine(remove_id); + delete this.localData[remove_id]; + } + }, + + /** + * This is a cloned method from Odoo framework. + * Virtual records are processed in two parts, + * this is the second part and here we trigger onchange + * process. + * + * @param {Object} record + * @param {Object} params + * @returns {Promise} + */ + _makeDefaultRecordNoDatapoint: function(record, params) { + var self = this; + + var targetView = params.viewType; + var fieldsInfo = params.fieldsInfo; + var fieldNames = Object.keys(fieldsInfo[targetView]); + var fields_key = _.without(fieldNames, "__last_update"); + + // Fields that are present in the originating view, that need to be initialized + // Hence preventing their value to crash when getting back to the originating view + var parentRecord = + params.parentID && this.localData[params.parentID].type === "list" + ? this.localData[params.parentID] + : null; + + if (parentRecord && parentRecord.viewType in parentRecord.fieldsInfo) { + var originView = parentRecord.viewType; + fieldNames = _.union( + fieldNames, + Object.keys(parentRecord.fieldsInfo[originView]) + ); + } + + return this._rpc({ + model: record.model, + method: "default_get", + args: [fields_key], + context: params.context, + }).then(function(result) { + // Interrupt point (used in instant search) + if (!self.exists(record.id)) { + return Promise.reject(); + } + + // We want to overwrite the default value of the handle field (if any), + // in order for new lines to be added at the correct position. + // -> This is a rare case where the defaul_get from the server + // will be ignored by the view for a certain field (usually "sequence"). + + var overrideDefaultFields = self._computeOverrideDefaultFields( + params.parentID, + params.position + ); + + if (overrideDefaultFields) { + result[overrideDefaultFields.field] = overrideDefaultFields.value; + } + + return self + .applyDefaultValues(record.id, result, {fieldNames: fieldNames}) + .then(function() { + if (!self.exists(record.id)) { + return Promise.reject(); + } + var def = new Promise(function(resolve, reject) { + var always = function() { + if (record._warning) { + if (params.allowWarning) { + delete record._warning; + } else { + reject(); + } + } + resolve(); + }; + self._performOnChange(record, fields_key) + .then(always) + .guardedCatch(always); + }); + return def; + }) + .then(function() { + if (!self.exists(record.id)) { + return Promise.reject(); + } + return self._fetchRelationalData(record); + }) + .then(function() { + if (!self.exists(record.id)) { + return Promise.reject(); + } + return self._postprocess(record); + }) + .then(function() { + if (!self.exists(record.id)) { + return Promise.reject(); + } + // Save initial changes, so they can be restored later, + // if we need to discard. + self.save(record.id, {savePoint: true}); + + return record.id; + }); + }); + }, + + /** + * Virtual records are processed in two parts, + * this is the first part and here we create + * the state (without aditional process) + * + * @param {String} listID + * @param {Object} options + * @returns {Object} + */ + createVirtualDatapoint: function(listID, options) { + const list = this.localData[listID]; + const context = _.extend({}, this._getContext(list), options.context); + + const position = options ? options.position : "top"; + const params = { + context: context, + fields: list.fields, + fieldsInfo: list.fieldsInfo, + parentID: list.id, + position: position, + viewType: list.viewType, + allowWarning: true, + doNotSetDirty: true, + }; + + var targetView = params.viewType; + var fields = params.fields; + var fieldsInfo = params.fieldsInfo; + + // Fields that are present in the originating view, that need to be initialized + // Hence preventing their value to crash when getting back to the originating view + var parentRecord = + params.parentID && this.localData[params.parentID].type === "list" + ? this.localData[params.parentID] + : null; + + if (parentRecord && parentRecord.viewType in parentRecord.fieldsInfo) { + var originView = parentRecord.viewType; + fieldsInfo[targetView] = _.defaults( + {}, + fieldsInfo[targetView], + parentRecord.fieldsInfo[originView] + ); + fields = _.defaults({}, fields, parentRecord.fields); + } + + const record = this._makeDataPoint({ + modelName: list.model, + fields: fields, + fieldsInfo: fieldsInfo, + context: params.context, + parentID: params.parentID, + res_ids: params.res_ids, + viewType: targetView, + }); + this.setPureVirtual(record.id, true); + this.updateRecordContext(record.id, { + ignore_warning: true, + not_onchange: true, + }); + + return { + record: record, + params: params, + }; + }, + + /** + * Generates a virtual records without hard-link to any model. * * @param {Integer/String} listID * @param {Object} options @@ -77,14 +292,14 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) { return new Promise(resolve => { this._makeDefaultRecord(list.model, params).then(recordID => { this.setPureVirtual(recordID, true); - this.updateRecordContext(recordID, {ignore_warning: true}); - if (options.data) { - this._applyChange(recordID, options.data, params).then(() => { - resolve(this.get(recordID)); - }); - } else { - resolve(this.get(recordID)); - } + this.updateRecordContext(recordID, { + ignore_warning: true, + not_onchange: true, + }); + resolve({ + record: this.get(recordID), + params: params, + }); }); }); }, @@ -92,7 +307,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) { /** * Adds support to avoid show onchange warnings. * The implementation is a pure hack that clone - * the context and do a monkey path the + * the context and do a monkey patch to the * 'trigger_up' method. * * @override @@ -112,5 +327,28 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) { } return this._super.apply(this, arguments); }, + + /** + * This happens when the user discard main document changes (isn't a rollback) + * + * @override + */ + discardChanges: function(id, options) { + this._super.apply(this, arguments); + options = options || {}; + var isNew = this.isNew(id); + var rollback = "rollback" in options ? options.rollback : isNew; + if (rollback) { + return; + } + const element = this.localData[id]; + this._visitChildren(element, function(elem) { + if (_.isEmpty(elem._changes)) { + if (elem.context.product_picker_modified) { + elem.context.product_picker_modified = false; + } + } + }); + }, }); }); diff --git a/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js b/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js index 1e8a85f29..085cdb68d 100644 --- a/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js +++ b/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js @@ -27,6 +27,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun "click .oe_btn_lines": "_onClickLines", "click .oe_btn_search_group": "_onClickSearchGroup", "search .oe_search_input": "_onSearch", + "input .oe_search_input": "_onInputSearch", "focusin .oe_search_input": "_onFocusInSearch", "show.bs.dropdown .o_cp_buttons": "_onShowSearchDropdown", "click #product_picker_maximize": "_onClickMaximize", @@ -41,19 +42,17 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun }), _auto_search_delay: 450, + _input_instant_search_time: 150, // Model product.product fields - search_read_fields: ["id", "display_name"], + search_read_fields: ["id", "display_name", "uom_id"], /** * @override */ - init: function(parent, name, record) { + init: function(parent) { this._super.apply(this, arguments); - // This is the parent state - this.state = record; - // Use jquery 'extend' to have a 'deep' merge. this.options = $.extend( true, @@ -79,32 +78,31 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun if (this.view) { this._processGroups(); } + + this._currentSearchBatchID = 0; + + this._lazyRenderSearchRecords = _.debounce(() => { + this.doRenderSearchRecords(); + ++this._currentSearchBatchID; + }, this._input_instant_search_time); }, - /** - * @override - */ willStart: function() { - if (!this.view) { - return Promise.resolve(); - } - - if (this.mode === "readonly") { - this._updateSearchContext(-1); - } else { - this._updateSearchContext(0); - } - return Promise.all([ - this._super.apply(this, arguments), - this._getSearchRecords(), - ]); + return this._super.apply(this, arguments).then(() => { + if (this.isReadonly) { + // Show Lines + this._updateSearchContext(-1); + } else { + this._updateSearchContext(0); + } + }); }, /** * Updates the lines counter badge */ updateBadgeLines: function() { - const records = this.parent_controller.model.get(this.state.id).data[ + const records = this.parent_controller.model.get(this.record.id).data[ this.name ].data; this.$badgeLines.text(records.length); @@ -116,7 +114,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun } let prices = []; const field_map = this.options.field_map; - const records = this.parent_controller.model.get(this.state.id).data[ + const records = this.parent_controller.model.get(this.record.id).data[ this.name ].data; if (this.options.show_discount) { @@ -145,7 +143,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun total, this.value.fields[this.options.field_map.price_unit], this.options.currency_field, - this.state.data + this.record.data ); this.$totalZone.find(".total_price").html(total || 0.0); }, @@ -161,12 +159,13 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun domain: this.record.getDomain(this.recordParams), field: this.field, parentID: this.value.id, - state: this.state, + record: this.record, model: this.parent_controller.model, fieldName: this.name, recordData: this.recordData, value: this.value, - relation_field: this.state.fields[this.name].relation_field, + relation_field: this.record.fields[this.name].relation_field, + current_batch_id: this._currentSearchBatchID, }; }, @@ -252,21 +251,30 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun */ _render: function() { const def = this._super.apply(this, arguments); + if (def) { + this.renderer.updateSearchGroup(this._activeSearchGroup); - // Parent implementation can return 'undefined' :( - return ( - def && - def.then(() => { - if ( - !this.$el.hasClass("oe_field_one2many_product_picker_maximized") - ) { - this.$el.addClass("position-relative d-flex flex-column"); - } + // Check maximize state + if (!this.$el.hasClass("oe_field_one2many_product_picker_maximized")) { + this.$el.addClass("position-relative d-flex flex-column"); + } + + return new Promise(resolve => { + this._getSearchRecords() + .then(records => { + return this.renderer.appendSearchRecords(records, { + cleanup: false, + }); + }) + .then(() => resolve()); + }).then(() => { if (this.options.show_subtotal) { this._addTotalsZone(); } - }) - ); + }); + } + + return def; }, /** @@ -274,10 +282,14 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun */ doRenderSearchRecords: function() { return new Promise(resolve => { - this._getSearchRecords().then(() => { - this.renderer.$el.scrollTop(0); - this.renderer._renderView().then(() => resolve()); - }); + this._getSearchRecords() + .then(records => { + this.renderer.$el.scrollTop(0); + return this.renderer.appendSearchRecords(records, { + cleanup: true, + }); + }) + .then(() => resolve()); }); }, @@ -311,7 +323,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * @param {Boolean} merge * @returns {Deferred} */ - _getSearchRecords: function(options, merge) { + _getSearchRecords: function(options) { const arch = this.view.arch; const search_mode = this.options.search[this._searchMode]; const field_name = this.options.field_map.product; @@ -367,24 +379,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun } task.then(results => { - if (merge) { - this._searchRecords = _.union( - this._searchRecords || [], - results - ); - } else { - this._searchRecords = results; - } - this._lastSearchRecordsCount = results.length; this._searchOffset = offset + limit; - if (this.renderer) { - this.renderer.updateSearchData( - this._searchRecords, - this._lastSearchRecordsCount, - this._activeSearchGroup - ); - } - + this.renderer.showLoadMore(limit && results.length === limit); resolve(results); }); }); @@ -531,9 +527,11 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun price_unit: "price_unit", discount: "discount", }, + trigger_refresh_fields: ["partner_id", "currency_id"], auto_save: false, ignore_warning: false, all_domain: [], + instant_search: false, }; }, @@ -588,12 +586,17 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun return []; } const field_name = this.options.field_map.product; - const lines = this.parent_controller.model.get(this.state.id).data[ + const lines = this.parent_controller.model.get(this.record.id).data[ this.name ].data; - const ids = _.map(lines, line => { - return line.data[field_name].data.id; - }); + // Here only get lines with product_id assigned + // This happens beacuse sale.order has lines for sections/comments + const ids = _.chain(lines) + .filter(line => line.data[field_name] !== false) + .map(line => { + return line.data[field_name].data.id; + }) + .value(); return [["id", "in", ids]]; }, @@ -614,6 +617,9 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun this._searchContext.order = [{name: "sequence"}, {name: "id"}]; this._searchContext.activeTest = false; } + if (this.renderer) { + this.renderer.updateSearchGroup(this._activeSearchGroup); + } }, /** @@ -621,6 +627,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * that the search results. Use directy in-memory values. */ showLines: function() { + this.renderer.clearRecords(); this._updateSearchContext(-1); this._clearSearchInput(); this.$btnLines @@ -635,6 +642,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * @param {Number} group_id */ showGroup: function(group_id) { + this.renderer.clearRecords(); this._updateSearchContext(group_id); this.doRenderSearchRecords(); this.$btnLines.removeClass("active"); @@ -668,6 +676,15 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun this.doRenderSearchRecords(); }, + _onInputSearch: function(evt) { + if (!this.options.instant_search) { + return; + } + this._searchContext.text = evt.target.value; + this._lazyRenderSearchRecords(); + // This.doRenderSearchRecords() + }, + /** * Auto select all content when user enters into fields with this * widget. @@ -707,7 +724,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun */ _onCreateQuickRecord: function(evt) { evt.stopPropagation(); - this.parent_controller.model.setPureVirtual(evt.data.id, false); + var model = this.parent_controller.model; + model.setPureVirtual(evt.data.id, false); if (this.options.auto_save) { // Dont trigger state update @@ -718,16 +736,15 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun this.parent_controller .saveRecord(undefined, {stayInEdit: true}) .then(() => { - // Because 'create' generates a new state and we can't know these new id we - // need force update all the current states. - this._setValue( - {operation: "UPDATE", id: evt.data.id}, - {doNotSetDirty: true} - ).then(() => { - if (evt.data.callback) { - evt.data.callback(); - } - }); + self.renderer.updateState( + model.get(self.parent_controller.handle).data[ + self.name + ], + {force: true} + ); + if (evt.data.callback) { + evt.data.callback(); + } }); if (evt.data.callback) { evt.data.callback(); @@ -758,21 +775,15 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun self.parent_controller .saveRecord(undefined, {stayInEdit: true}) .then(function() { - // Workaround to get updated values - self.parent_controller.model - .reload(self.value.id) - .then(function(result) { - var new_data = self.parent_controller.model.get( - result - ); - self.value.data = new_data.data; - self.renderer.updateState(self.value, { - force: true, - }); - if (callback) { - callback(); - } - }); + self.renderer.updateState( + self.parent_controller.model.get( + self.parent_controller.handle + ).data[self.name], + {force: true} + ); + if (callback) { + callback(); + } }); if (callback) { callback(); @@ -844,12 +855,9 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun if (this._isLoading) { return; } - this._getSearchRecords( - { - offset: this._searchOffset, - }, - true - ).then(records => { + this._getSearchRecords({ + offset: this._searchOffset, + }).then(records => { this.renderer.appendSearchRecords(records); }); }, @@ -872,7 +880,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun */ _blockControlPanel: function(block) { if (this.$buttons) { - this.$buttons.find("input,button").attr("disabled", block); + this.$buttons.find("button").attr("disabled", block); } }, diff --git a/web_widget_one2many_product_picker/static/src/scss/one2many_product_picker.scss b/web_widget_one2many_product_picker/static/src/scss/one2many_product_picker.scss index 922ae16e6..48ec908be 100644 --- a/web_widget_one2many_product_picker/static/src/scss/one2many_product_picker.scss +++ b/web_widget_one2many_product_picker/static/src/scss/one2many_product_picker.scss @@ -83,11 +83,6 @@ .oe_flip_card_inner { height: 100% !important; box-shadow: 0px 0px 15px; - .img-fluid { - transform: translateY(-50%) !important; - top: 50%; - position: relative; - } .oe_one2many_product_picker_title { font-size: 1.95rem !important; } @@ -142,6 +137,14 @@ $one2many-product-picker-transition-3d-time/2; transform-style: preserve-3d; + .img-fluid { + transform: translate(-50%, -50%); + top: 50%; + left: 50%; + z-index: -1; + position: absolute; + } + .position-absolute { z-index: 1; } @@ -150,8 +153,19 @@ font-size: 1rem; } + .indicator_zones { + display: inline-flex; + flex-direction: column; + max-width: 50%; + align-items: flex-start; + + > span.badge { + font-size: 0.8rem; + } + } + .badge_price { - top: 50%; + top: 55%; right: -2px; transform: translateY(-50%); display: grid; @@ -190,8 +204,17 @@ .o_form_view.o_form_nosheet { padding: $one2many-product-picker-card-form-padding; - .o_field_widget .o_input_dropdown > input { - height: unset; + .o_field_widget { + &:not(.widget_numeric_step) { + max-width: 95%; + } + + .o_input_dropdown > input { + height: unset; + } + } + .btn.w-100 { + max-width: 95%; } } } @@ -211,6 +234,16 @@ top: 50%; left: 0; transform: translateY(-50%); + + .oe_one2many_product_picker_form_buttons { + display: flex; + padding: 0 3px; + justify-content: center; + + .oe_record_remove { + flex-grow: 1; + } + } } } diff --git a/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml b/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml index e20d8d4e6..c14b36ea0 100644 --- a/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml +++ b/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml @@ -86,6 +86,101 @@ >Load More
    + +
    + + x + + + + + + 1 + +
    +
    + +
    + + + + + + + + +
    +
    + +
    + +
    + + +
    + + +
    + + + + +
    +
    + +
    + +
    +
    -
    - - -
    - x -
    -
    - -
    - -
    -
    - -
    - 1 -
    -
    -
    - - - - - - - - -
    - - -
    - - - - -
    -
    - -
    + +
    diff --git a/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml b/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml index 43466160a..4fe591890 100644 --- a/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml +++ b/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml @@ -5,10 +5,10 @@
    - - @@ -16,6 +16,9 @@ + diff --git a/web_widget_one2many_product_picker/templates/assets.xml b/web_widget_one2many_product_picker/templates/assets.xml index cbe883e47..46664a0bc 100644 --- a/web_widget_one2many_product_picker/templates/assets.xml +++ b/web_widget_one2many_product_picker/templates/assets.xml @@ -61,6 +61,10 @@ type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/basic_model.js" /> +