diff --git a/web_widget_one2many_product_picker/README.rst b/web_widget_one2many_product_picker/README.rst index 3a6070abb..418951d79 100644 --- a/web_widget_one2many_product_picker/README.rst +++ b/web_widget_one2many_product_picker/README.rst @@ -180,6 +180,15 @@ Other example for 'purchase.order.line' fields: Usage ===== +When you change the value of a field and switch to edit another record, the +changes will be applied to the previous record without having to click on +accept changes. + +Parts of the widget: +~~~~~~~~~~~~~~~~~~~~ + + .. image:: https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker_anat.png + Preview: ~~~~~~~~ diff --git a/web_widget_one2many_product_picker/readme/USAGE.rst b/web_widget_one2many_product_picker/readme/USAGE.rst index a3b95d69b..dced5bc77 100644 --- a/web_widget_one2many_product_picker/readme/USAGE.rst +++ b/web_widget_one2many_product_picker/readme/USAGE.rst @@ -1,3 +1,12 @@ +When you change the value of a field and switch to edit another record, the +changes will be applied to the previous record without having to click on +accept changes. + +Parts of the widget: +~~~~~~~~~~~~~~~~~~~~ + + .. image:: ../static/img/product_picker_anat.png + Preview: ~~~~~~~~ diff --git a/web_widget_one2many_product_picker/static/description/index.html b/web_widget_one2many_product_picker/static/description/index.html index 639e6d921..941f4dc72 100644 --- a/web_widget_one2many_product_picker/static/description/index.html +++ b/web_widget_one2many_product_picker/static/description/index.html @@ -380,15 +380,16 @@ ul.auto-toc {
  • Usage
  • -
  • Known issues / Roadmap
  • -
  • Bug Tracker
  • -
  • Credits @@ -564,22 +565,31 @@ options="{'search': [{'name': _('Starts With'), 'domain': [('name', '=like'

    Usage

    +

    When you change the value of a field and switch to edit another record, the +changes will be applied to the previous record without having to click on +accept changes.

    +
    -

    Known issues / Roadmap

    +

    Known issues / Roadmap

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed @@ -587,15 +597,15 @@ If you spotted it first, help us smashing it by providing a detailed and welcome

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Tecnativa
    -

    Contributors

    +

    Contributors

    • Tecnativa:

      @@ -609,7 +619,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association

    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 @@
    -
    +
    - + +
    +
    + +
    + Add 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 8ce6c28bd..be1b9d2b1 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 @@ -3,7 +3,7 @@
    - + diff --git a/web_widget_one2many_product_picker/static/tests/widget_tests.js b/web_widget_one2many_product_picker/static/tests/widget_tests.js index 4f9e8ff18..9c52b193b 100644 --- a/web_widget_one2many_product_picker/static/tests/widget_tests.js +++ b/web_widget_one2many_product_picker/static/tests/widget_tests.js @@ -26,8 +26,6 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require ''; }; - console.log(getArch()); - QUnit.module('Web Widget One2Many Product Picker', { beforeEach: function () { this.data = {