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_anat.png b/web_widget_one2many_product_picker/static/img/product_picker_anat.png new file mode 100644 index 000000000..6a477ac89 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 80218e41e..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 @@ -126,6 +126,10 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView state: this._getRecordState(), }) ); + + if (this._disabled) { + this._disableQuickCreate(); + } }, /** @@ -138,7 +142,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView // 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"); }, @@ -151,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); }, @@ -234,15 +238,14 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView * @returns {Deferred} */ _add: function () { - this.model.updateRecordContext(this.handle, { - has_changes_confirmed: true, - }); - if (this._disabled) { // 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, { @@ -251,39 +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(); - self.trigger_up("restore_flip_card"); }); }, _remove: function () { - this.trigger_up("restore_flip_card"); + if (this._disabled) { + + // 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, }); }, _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(); }, _discard: function () { var self = this; + 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, @@ -295,11 +329,13 @@ 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(); }); } }, 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 263cb0c48..b7bafa559 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; @@ -42,6 +43,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu front: [], back: [], }; + + this._lazyUpdateRecord = _.debounce(this._updateRecord.bind(this), 450); }, /** @@ -105,6 +108,7 @@ 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(); @@ -177,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; } @@ -193,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 || {}, @@ -204,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: '', }; }, @@ -497,6 +505,93 @@ 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); + }); + }, + + /** + * @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); + }, + + /** + * @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'); + $target.on('animationend', function () { + $target.removeClass('o_catch_attention'); + $target.off('animationend'); + }); + $img.addClass('oe_product_picker_catch_attention'); + $img.on('animationend', function () { + $img.removeClass('oe_product_picker_catch_attention'); + $img.off('animationend'); + }); + }, + /** * @private */ @@ -531,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), @@ -644,7 +759,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu /** * @private */ - _onRestoreFlipCard: function () { + _onRestoreFlipCard: function (evt) { var self = this; this.$card.removeClass("active"); this.$front.removeClass("d-none"); @@ -660,7 +775,16 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu 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 b5a181115..a78808fe6 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 @@ -104,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); }); }, @@ -151,6 +149,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", var widget = this.widgets[eb]; if ( widget && + widget.state && widget.state.data[this.options.field_map.product].data.id === widget_product_id ) { found = true; @@ -194,7 +193,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; } @@ -203,6 +202,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", states_to_destroy.push(old_state); } } + this._removeRecords(states_to_destroy); // Records to Update or Create @@ -215,12 +215,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; @@ -236,8 +236,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", // Remove "pure virtual" records that have the same product that the new record if ( widget.is_virtual && - widget.state.data[this.options.field_map.product].data.id === state.data[this.options.field_map.product].data.id && - widget.state.data[this.options.compa].data.id === state.data[this.options.field_map.product].data.id + widget.state.data[this.options.field_map.product].data.id === state.data[this.options.field_map.product].data.id ) { to_destroy.push(widget); delete this.widgets[e]; @@ -284,7 +283,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 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/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 @@