Maintainers
+Maintainers
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose diff --git a/web_widget_one2many_product_picker/static/img/product_picker.gif b/web_widget_one2many_product_picker/static/img/product_picker.gif index 12c6d63a7..dcc9f288b 100644 Binary files a/web_widget_one2many_product_picker/static/img/product_picker.gif and b/web_widget_one2many_product_picker/static/img/product_picker.gif differ diff --git a/web_widget_one2many_product_picker/static/img/product_picker_anat.png b/web_widget_one2many_product_picker/static/img/product_picker_anat.png new file mode 100644 index 000000000..e47ca65fa Binary files /dev/null and b/web_widget_one2many_product_picker/static/img/product_picker_anat.png differ diff --git a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form.js b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form.js index 338698e96..8cc47226e 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form.js +++ b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form.js @@ -89,9 +89,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f model: this.basicFieldParams.model, mainRecordData: this.getParent().getParent().state, }); - if (this.id) { - this.basicFieldParams.model.save(this.id, {savePoint: true}); - } + // if (this.id) { + // this.basicFieldParams.model.save(this.id, {savePoint: true}); + // } var def2 = this.formView.getController(this).then(function (controller) { self.controller = controller; self.$el.empty(); 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 a3c3b827d..f2bf19443 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 @@ -18,6 +18,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView BasicModel.include({ _applyOnChange: function (values, record, viewType) { + var vt = viewType || record.viewType; + // Ignore changes by record context 'ignore_onchanges' fields if ('ignore_onchanges' in record.context) { var ignore_changes = record.context.ignore_onchanges; for (var index in ignore_changes) { @@ -64,11 +66,28 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView }, /** - * Updates buttons depending on record status - * - * @private + * Create or accept changes */ - _updateButtons: function () { + auto: function () { + var record = this.model.get(this.handle); + if (record.context.has_changes_confirmed || typeof record.context.has_changes_confirmed === "undefined") { + return; + } + var state = this._getRecordState(); + if (state === "new") { + this._add(); + } else if (state === "dirty") { + this._change(); + } + }, + + /** + * Know the real state of the record + * - record: Normal + * - new: Is a new record + * - dirty: Has changes + */ + _getRecordState: function () { var record = this.model.get(this.handle); var state = "record"; if (this.model.isNew(record.id)) { @@ -89,25 +108,41 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView } } } + + return state; + }, + + /** + * Updates buttons depending on record status + * + * @private + */ + _updateButtons: function () { this.$el.find( ".oe_one2many_product_picker_form_buttons").remove(); this.$el.find(".o_form_view").append( qweb.render( "One2ManyProductPicker.QuickCreate.FormButtons", { - state: state, + state: this._getRecordState(), }) ); + + if (this._disabled) { + this._disableQuickCreate(); + } }, /** * @private */ _disableQuickCreate: function () { - + if (!this.$el) { + return; + } // Ensures that the record won't be created twice this._disabled = true; this.$el.addClass("o_disabled"); - this.$("input:not(:disabled)") + this.$("input:not(:disabled),button:not(:disabled)") .addClass("o_temporarily_disabled") .attr("disabled", "disabled"); }, @@ -120,7 +155,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView // Allows to create again this._disabled = false; this.$el.removeClass("o_disabled"); - this.$("input.o_temporarily_disabled") + this.$("input.o_temporarily_disabled,button.o_temporarily_disabled") .removeClass("o_temporarily_disabled") .attr("disabled", false); }, @@ -208,6 +243,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView // Don't do anything if we are already creating a record return $.Deferred(); } + this.model.updateRecordContext(this.handle, { + has_changes_confirmed: true, + }); var self = this; this._disableQuickCreate(); return this.saveRecord(this.handle, { @@ -216,64 +254,70 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView savePoint: true, viewType: "form", }).then(function () { - self._enableQuickCreate(); var record = self.model.get(self.handle); - self.trigger_up("create_quick_record", { - id: record.id, + self.trigger_up("restore_flip_card", { + success_callback: function () { + self.trigger_up("create_quick_record", { + id: record.id, + }); + self.model.unsetDirty(self.handle); + //self._updateButtons(); + self._enableQuickCreate(); + }, + block: true, }); - self.model.unsetDirty(self.handle); - self._updateButtons(); }); }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickAdd: function (ev) { - ev.stopPropagation(); - this.model.updateRecordContext(this.handle, { - has_changes_confirmed: true, - }); - this._add(); - }, + _remove: function () { + if (this._disabled) { - /** - * @private - * @param {MouseEvent} ev - */ - _onClickRemove: function (ev) { - ev.stopPropagation(); + // Don't do anything if we are already creating a record + return $.Deferred(); + } + + this._disableQuickCreate(); + this.trigger_up("restore_flip_card", {block: true}); + var record = this.model.get(this.handle); this.trigger_up("list_record_remove", { - id: this.renderer.state.id, + id: record.id, }); }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickChange: function (ev) { - ev.stopPropagation(); + _change: function () { + var self = this; + if (this._disabled) { + + // Don't do anything if we are already creating a record + return $.Deferred(); + } + this._disableQuickCreate(); this.model.updateRecordContext(this.handle, { has_changes_confirmed: true, }); var record = this.model.get(this.handle); - this.trigger_up("update_quick_record", { - id: record.id, + + this.trigger_up("restore_flip_card", { + success_callback: function () { + self.trigger_up("update_quick_record", { + id: record.id, + }); + self.model.unsetDirty(self.handle); + //self._updateButtons(); + self._enableQuickCreate(); + }, + block: true, }); - this.trigger_up("restore_flip_card"); - this.model.unsetDirty(this.handle); - this._updateButtons(); }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickDiscard: function (ev) { + _discard: function () { var self = this; - ev.stopPropagation(); + if (this._disabled) { + + // Don't do anything if we are already creating a record + return $.Deferred(); + } + this._disableQuickCreate(); var record = this.model.get(this.handle); this.model.discardChanges(this.handle, { rollback: true, @@ -285,14 +329,52 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView this.update({}, {reload: false}); this.trigger_up("restore_flip_card"); this._updateButtons(); + this._enableQuickCreate(); } else { this.update({}, {reload: false}).then(function () { self.model.unsetDirty(self.handle); self.trigger_up("restore_flip_card"); self._updateButtons(); + self._enableQuickCreate(); }); } }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickAdd: function (ev) { + ev.stopPropagation(); + this._add(); + }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickRemove: function (ev) { + ev.stopPropagation(); + this._remove(); + }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickChange: function (ev) { + ev.stopPropagation(); + this._change(); + }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickDiscard: function (ev) { + ev.stopPropagation(); + this._discard(); + }, } ); @@ -302,6 +384,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView Controller: ProductPickerQuickCreateFormController, }), + /** + * @override + */ init: function (viewInfo, params) { this._super.apply(this, arguments); this.controllerParams.compareKey = params.compareKey; 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 34d581562..66113689b 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 @@ -13,6 +13,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var tools = require("web_widget_one2many_product_picker.tools"); var ProductPickerQuickModifPriceForm = require( "web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm"); + var FieldManagerMixin = require('web.FieldManagerMixin'); var qweb = core.qweb; var _t = core._t; @@ -38,7 +39,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu this.subWidgets = {}; this._clickFlipCardCount = 0; this._setState(state, options.searchRecord); - this.widgets = []; + this.widgets = { + front: [], + back: [], + }; + + this._lazyUpdateRecord = _.debounce(this._updateRecord.bind(this), 450); }, /** @@ -71,6 +77,14 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu _.invoke(this.subWidgets, "on_detach_callback"); }, + /** + * @override + */ + destroy: function () { + this.$card.off("") + this._super.apply(this, arguments); + }, + /** * @override */ @@ -94,6 +108,13 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu if (state) { this._setState(state); } + this.$card.removeClass("blocked"); + // Avoid recreate active record + if (this.$card.hasClass("active")) { + this._processDynamicFields(); + return $.when(); + } + this.on_detach_callback(); return this._render(); }, @@ -160,6 +181,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu this.fields = this.getParent().state.fields; this.fieldsInfo = this.getParent().state.fieldsInfo.form; this.state = viewState; + if (recordSearch) { this.recordSearch = recordSearch; } @@ -176,6 +198,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu // Using directly the 'model record' instead of the state because // the state it's a parsed version of this record that doesn't // contains the '_virtual' attribute. + var model = this.options.basicFieldParams.model; + var record = model.get(this.state.id); return { record_search: this.recordSearch, user_context: this.getSession() && this.getSession().user_context || {}, @@ -187,6 +211,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu monetary: this._getMonetaryFieldValue.bind(this), show_discount: this.options.showDiscount, is_virtual: this.is_virtual, + modified: record && record.context.product_picker_modified, active_model: '', }; }, @@ -230,6 +255,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var model = this.options.basicFieldParams.model; var scontext = _.extend( {}, this._getInternalVirtualRecordContext(), context); + // Force qty to 1.0 to launch correct onchanges + scontext[`default_${this.options.fieldMap.product_uom_qty}`] = 1.0; var sdata = _.extend({}, this._getInternalVirtualRecordData(), data); return model.createVirtualRecord( this.options.basicFieldParams.value.id, { @@ -238,10 +265,20 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu }); }, + _detachAllWidgets: function () { + _.invoke(this.widgets.front, "on_detach_callback"); + _.invoke(this.widgets.back, "on_detach_callback"); + this.widgets = { + front: [], + back: [], + }; + }, + /** * @override */ _render: function () { + this._detachAllWidgets(); this.defs = []; this._replaceElement( qweb.render( @@ -249,11 +286,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu this._getQWebContext() ) ); + this.$el.data("renderer_widget_index", this.options.renderer_widget_index); this.$card = this.$(".oe_flip_card"); this.$front = this.$(".oe_flip_card_front"); this.$back = this.$(".oe_flip_card_back"); this._processWidgetFields(this.$front); - this._processWidgets(this.$front); + this._processWidgets(this.$front, 'front'); this._processDynamicFields(); return $.when.apply(this, this.defs); }, @@ -265,7 +303,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @private * @param {jQueryElement} $container */ - _processWidgetFields: function ($container) { + _processWidgetFields: function ($container, widget_list) { var self = this; $container.find("field").each(function () { var $field = $(this); @@ -357,7 +395,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @private * @param {jQueryElement} $container */ - _processWidgets: function ($container) { + _processWidgets: function ($container, widget_zone) { var self = this; $container.find("widget").each(function () { var $field = $(this); @@ -375,7 +413,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu data: self.state && self.state.data, }); - self.widgets.push(widget); + self.widgets[widget_zone].push(widget); var def = widget ._widgetRenderAndInsert(function () { @@ -469,6 +507,91 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu ); }, + /** + * @private + * @returns {Promise} + */ + _saveRecord: function () { + var self = this; + var model = this.options.basicFieldParams.model; + var record = model.get(this.state.id); + return model.save(record.id, { + savePoint: true, + }).then(function () { + var record = model.get(self.state.id); + self.trigger_up("create_quick_record", { + id: record.id, + }); + model.unsetDirty(self.state.id); + self.$card.find('.o_catch_attention').removeClass('o_catch_attention'); + }); + }, + + /** + * @private + */ + _updateRecord: function (changes) { + var model = this.options.basicFieldParams.model; + var record = model.get(this.state.id); + this.trigger_up("update_quick_record", { + id: record.id, + }); + model.unsetDirty(this.state.id); + this.$card.find('.o_catch_attention').removeClass('o_catch_attention'); + }, + + /** + * @private + * @returns {Promise} + */ + _addProduct: function () { + var self = this; + var changes = {}; + if (this.state.data[this.options.fieldMap.product_uom_qty] === 0) { + changes[this.options.fieldMap.product_uom_qty] = 1; + } + var model = this.options.basicFieldParams.model; + this.$card.addClass("blocked"); + return model.notifyChanges(this.state.id, changes).then(function () { + self._saveRecord(); + }); + }, + + /** + * @private + * @param {Number} amount + * @returns {Promise} + */ + _incProductQty: function (amount) { + var self = this; + this.state.data[this.options.fieldMap.product_uom_qty] += amount; + var model = this.options.basicFieldParams.model; + var record = model.get(this.state.id); + var state_data = record.data; + state_data[this.options.fieldMap.product_uom_qty] += amount; + var changes = _.pick(state_data, this.options.fieldMap.product_uom_qty); + + return model.notifyChanges(record.id, changes).then(function () { + self._processDynamicFields(); + self._lazyUpdateRecord(); + }); + }, + + /** + * @private + */ + _doInteractAnim: function (target, currentTarget) { + var $target = $(target); + var $currentTarget = $(currentTarget); + var $img = $currentTarget.find(".oe_flip_card_front img"); + $target.addClass('o_catch_attention'); + $img.addClass('oe_product_picker_catch_attention'); + $img.on('animationend', function () { + $img.removeClass('oe_product_picker_catch_attention'); + $img.off('animationend'); + }); + }, + /** * @private */ @@ -503,11 +626,31 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @param {ClickEvent} evt */ _onClickFlipCard: function (evt) { - // Avoid clicks on form elements - if (['INPUT', 'BUTTON', 'A'].indexOf(evt.target.tagName) !== -1) { + if (['INPUT', 'BUTTON', 'A'].indexOf(evt.target.tagName) !== -1 || this.$card.hasClass('blocked')) { return; } + var $target = $(evt.target); + if (!this.options.readOnlyMode) { + if ( + $target.hasClass('add_product') || + $target.parents('.add_product').length + ) { + if (!this.is_adding_product) { + this.is_adding_product = true; + this._addProduct(); + this._doInteractAnim(evt.target, evt.currentTarget); + } + return; + } else if ( + $target.hasClass('product_qty') || + $target.parents('.product_qty').length + ) { + this._incProductQty(1); + this._doInteractAnim(evt.target, evt.currentTarget); + return; + } + } if (!this._clickFlipCardDelayed) { this._clickFlipCardDelayed = setTimeout( this._onClickDelayedFlipCard.bind(this, evt), @@ -534,21 +677,28 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu } if (this.$card.hasClass("active")) { this.$card.removeClass("active"); - this.$card.find('.oe_flip_card_front').removeClass("d-none"); + this.$front.removeClass("d-none"); } else { var self = this; this.defs = []; - this._processWidgetFields(this.$back); - this._processWidgets(this.$back); + if (!this.widgets.back.length) { + this._processWidgetFields(this.$back); + this._processWidgets(this.$back, 'back'); + } this._processDynamicFields(); $.when(this.defs).then(function () { var $actived_card = self.$el.parent().find(".active"); $actived_card.removeClass("active"); $actived_card.find('.oe_flip_card_front').removeClass("d-none"); self.$card.addClass("active"); - setTimeout(function () { - self.$('.oe_flip_card_front').addClass("d-none"); - }, 200); + self.$card.on('transitionend', function () { + self.$front.addClass("d-none"); + self.$card.off('transitionend'); + }); + self.trigger_up("record_flip", { + widget_index: self.$el.data("renderer_widget_index"), + prev_widget_index: $actived_card.parent().data("renderer_widget_index"), + }); }); } }, @@ -609,9 +759,33 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu /** * @private */ - _onRestoreFlipCard: function () { - this.$(".oe_flip_card").removeClass("active"); - this.$('.oe_flip_card_front').removeClass("d-none"); + _onRestoreFlipCard: function (evt) { + var self = this; + this.$card.removeClass("active"); + this.$front.removeClass("d-none"); + if (this.$card.hasClass('oe_flip_card_maximized')) { + this.$card.removeClass('oe_flip_card_maximized'); + this.$card.on('transitionend', function () { + self.$card.css({ + position: "", + top: "", + left: "", + width: "", + height: "", + zIndex: "", + }); + self.$card.off('transitionend'); + if (evt.data.success_callback) { + evt.data.success_callback(); + } + }); + } else if (evt.data.success_callback) { + evt.data.success_callback(); + } + + if (evt.data.block) { + this.$card.addClass("blocked"); + } }, /** 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 2c71f771e..48820a031 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 @@ -9,6 +9,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", var BasicRenderer = require("web.BasicRenderer"); var One2ManyProductPickerRecord = require( "web_widget_one2many_product_picker.One2ManyProductPickerRecord"); + var ProductPickerQuickCreateForm = require( + "web_widget_one2many_product_picker.ProductPickerQuickCreateForm"); var qweb = core.qweb; @@ -19,6 +21,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", events: { 'click #productPickerLoadMore': '_onClickLoadMore', }, + custom_events: { + 'record_flip': '_onRecordFlip', + }, DELAY_GET_RECORDS: 150, MIN_PERC_GET_RECORDS: 0.9, @@ -62,8 +67,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", * @param {Object} widget */ removeWidget: function (widget) { - this.widgets.splice(this.widgets.indexOf(widget), 1); + var index = this.widgets.indexOf(widget); widget.destroy(); + delete this.widgets[index]; }, /** @@ -98,14 +104,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", */ updateState: function (state, params) { var self = this; + var sparams = _.extend({}, params, {noRender: true}); if (_.isEqual(this.state.data, state.data)) { - return this._super.apply(this, arguments); + return this._super(state, sparams); } var old_state = _.clone(this.state.data); - return this._super( - state, - _.extend({}, params, {noRender: true}) - ).then(function () { + return this._super(state, sparams).then(function () { self._updateStateRecords(old_state); }); }, @@ -115,7 +119,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", * @param {Array[Object]} states * @returns {Deferred} */ - _removeRecords: function (states) { + _removeRecords: function (states, new_states) { var defs = []; var to_destroy = []; for (var index_state in states) { @@ -128,7 +132,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", } } } - this.widgets = _.compact(this.widgets); if (this.search_group.name === "main_lines") { _.invoke(to_destroy, "destroy"); @@ -142,18 +145,33 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", var widget_product_id = widget_destroyed.state .data[this.options.field_map.product].data.id; var found = false; + // If already exists a widget for the product don't try create a new one for (var eb = this.widgets.length-1; eb>=0; --eb) { var widget = this.widgets[eb]; if ( + widget && + widget.state && widget.state.data[this.options.field_map.product].data.id === widget_product_id ) { found = true; break; } } + if (!found) { + // Get the new state ID if exists to link it with the new record + var new_state_id = undefined; + for (var eb = new_states.length-1; eb>=0; --eb) { + var state = new_states[eb]; + if ( + state.data[this.options.field_map.product].data.id === widget_product_id + ) { + new_state_id = state.id; + break; + } + } var search_record = _.find(this.search_data, {id: widget_product_id}); - var new_search_record = _.extend({}, search_record, {__id: state.id}); + var new_search_record = _.extend({}, search_record, {__id: new_state_id}); var search_record_index = widget_destroyed.$el.index(); defs.push( this.appendSearchRecords( @@ -188,7 +206,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", var found = false; for (var e in this.state.data) { var current_state = this.state.data[e]; - if (current_state.id === old_state.id) { + if (current_state.id === old_state.id || (typeof current_state.data.id !== 'undefined' && current_state.data.id === old_state.data.id)) { found = true; break; } @@ -197,7 +215,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", states_to_destroy.push(old_state); } } - this._removeRecords(states_to_destroy); + this._removeRecords(states_to_destroy, this.state.data); // Records to Update or Create var defs = []; @@ -209,12 +227,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", var search_record = false; for (var e = this.widgets.length-1; e>=0; --e) { var widget = this.widgets[e]; - if (!widget) { + if (!widget || !widget.state) { // Already processed widget (deleted) continue; } - if (widget.state.id === state.id) { + if (widget.state.id === state.id || (typeof state.data.id !== 'undefined' && widget.state.data.id === state.data.id)) { widget.recreate(state); exists = true; break; @@ -243,7 +261,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", defs.push(this.appendSearchRecords([new_search_record], false, true, search_record_index)[0]); } } - this.widgets = _.compact(this.widgets); + _.invoke(to_destroy, "destroy"); return $.when(defs); }, @@ -253,7 +271,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", */ _renderView: function () { var self = this; - var oldWidgets = this.widgets; + var oldWidgets = _.compact(this.widgets); this.widgets = []; this.$recordsContainer = $("
", { class: "w-100 row", @@ -277,7 +295,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", }, /** - * Compare search results with current lines + * Compare search results with current lines. + * Link a current state with the 'search record'. * * @private * @param {Array[Object]} results @@ -360,10 +379,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", no_process_records?search_records:this._processSearchRecords(search_records); _.each(processed_records, function (search_record) { var state_data = self._getRecordDataById(search_record.__id); + var widget_options = self._getRecordOptions(search_record); + widget_options.renderer_widget_index = self.widgets.length; var ProductPickerRecord = new One2ManyProductPickerRecord( self, state_data, - self._getRecordOptions(search_record) + widget_options ); self.widgets.push(ProductPickerRecord); @@ -380,12 +401,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", // the search data. Using search data instead of waiting for // simulated state gives a low FCP time. var def = ProductPickerRecord.appendTo(self.$recordsContainer) - .then(function () { - if (typeof position !== "undefined") { - var $elm = self.$el.find("> div > div:nth("+position+")"); - ProductPickerRecord.$el.insertAfter($elm); + .then(function (widget, widget_position) { + if (typeof widget_position !== "undefined") { + var $elm = this.$el.find("> div > div:nth("+widget_position+")"); + widget.$el.insertAfter($elm); } - }); + }.bind(self, ProductPickerRecord, position)); if (def.state() === "pending") { self.defs.push(def); } @@ -441,6 +462,53 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", this._loadMoreWorking = true; }, + /** + * Do card flip + * + * @param {Integer} index + */ + doWidgetFlip: function (index) { + var widget = this.widgets[index]; + var $actived_card = this.$el.find(".active"); + if (widget.$card.hasClass("active")) { + widget.$card.removeClass("active"); + widget.$card.find('.oe_flip_card_front').removeClass("d-none"); + } else { + var self = widget; + widget.defs = []; + widget._processWidgetFields(widget.$back); + widget._processWidgets(widget.$back); + widget._processDynamicFields(); + $.when(widget.defs).then(function () { + $actived_card.removeClass("active"); + $actived_card.find('.oe_flip_card_front').removeClass("d-none"); + self.$card.addClass("active"); + setTimeout(function () { + self.$('.oe_flip_card_front').addClass("d-none"); + }, 200); + }); + } + }, + + /** + * Handle card flip. + * Used to create/update the record + * + * @param {CustomEvent} evt + */ + _onRecordFlip: function (evt) { + var prev_widget_index = evt.data.prev_widget_index; + if (typeof prev_widget_index !== "undefined") { + // Only check 'back' widgets so there is where the form was created + for (var index in this.widgets[prev_widget_index].widgets.back) { + var widget = this.widgets[prev_widget_index].widgets.back[index]; + if (widget instanceof ProductPickerQuickCreateForm) { + widget.controller.auto(); + } + } + } + } + }); return One2ManyProductPickerRenderer; 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 91bd95d1f..f6d089d69 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 @@ -580,11 +580,20 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * @param {CustomEvent} evt */ _onCreateQuickRecord: function (evt) { + var self = this; this.parent_controller.model.setPureVirtual(evt.data.id, false); - this._setValue({operation: "ADD", id: evt.data.id}); - if (this.options.auto_save) { - this.parent_controller.saveRecord(undefined, {stayInEdit: true}); + if (!self.options.auto_save) { + self.parent_controller.model.updateRecordContext(evt.data.id, { + product_picker_modified: true, + }); } + this._setValue({operation: "ADD", id: evt.data.id}).then(function () { + if (self.options.auto_save) { + self.parent_controller.saveRecord(undefined, {stayInEdit: true}).then(function () { + self.renderer.updateState(self.value); + }); + } + }); }, /** @@ -594,10 +603,19 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * @param {CustomEevent} evt */ _onUpdateQuickRecord: function (evt) { - this._setValue({operation: "UPDATE", id: evt.data.id, data: evt.data.data}); - if (this.options.auto_save) { - this.parent_controller.saveRecord(undefined, {stayInEdit: true}); + var self = this; + if (!self.options.auto_save) { + self.parent_controller.model.updateRecordContext(evt.data.id, { + product_picker_modified: true, + }); } + this._setValue({operation: "UPDATE", id: evt.data.id, data: evt.data.data}).then(function () { + if (self.options.auto_save) { + self.parent_controller.saveRecord(undefined, {stayInEdit: true}).then(function () { + self.renderer.updateState(self.value); + }); + } + }); }, /** diff --git a/web_widget_one2many_product_picker/static/src/scss/_variables.scss b/web_widget_one2many_product_picker/static/src/scss/_variables.scss index 4a65150a3..6d2d3e00a 100644 --- a/web_widget_one2many_product_picker/static/src/scss/_variables.scss +++ b/web_widget_one2many_product_picker/static/src/scss/_variables.scss @@ -1,7 +1,6 @@ -$one2many-product-picker-max-height: 500px; $one2many-product-picker-card-min-height: 150px; $one2many-product-picker-card-max-height: 150px; -$one2many-product-picker-transition-3d-time: 0.3s; +$one2many-product-picker-transition-3d-time: 0.15s; $one2many-product-picker-card-form-padding: 0em; $one2many-product-picker-quick-modif-price-max-width: 400px; $one2many-product-picker-zoom-scale: 1.4; 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 3beb00318..1ea12b6f6 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 @@ -59,6 +59,10 @@ transition: top $one2many-product-picker-transition-3d-time, left $one2many-product-picker-transition-3d-time, width $one2many-product-picker-transition-3d-time, height $one2many-product-picker-transition-3d-time; height: $one2many-product-picker-card-min-height; + &.blocked { + filter: blur(2px); + } + &.disabled { filter: grayscale(100%); opacity: 0.5; @@ -211,6 +215,9 @@ font-size: 0.95rem; z-index: 0; } + .add_product, .product_qty, .price_unit { + cursor: pointer; + } } } } @@ -230,3 +237,20 @@ text-align: center; } } + +.oe_product_picker_catch_attention { + position: relative; + animation: productPickerCatchAttention 200ms normal forwards; +} + +@keyframes productPickerCatchAttention { + 0% { + transform: scale(1.0); + } + 50% { + transform: scale(1.5); + } + 100% { + transform: scale(1.0); + } +} \ No newline at end of file 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 700e7fe4f..d7eaf63b8 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 @@ -72,11 +72,16 @@
+