diff --git a/web_widget_one2many_product_picker/README.rst b/web_widget_one2many_product_picker/README.rst index 3a6070abb..df183ae87 100644 --- a/web_widget_one2many_product_picker/README.rst +++ b/web_widget_one2many_product_picker/README.rst @@ -180,6 +180,11 @@ Other example for 'purchase.order.line' fields: Usage ===== +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..5f3fbba75 100644 --- a/web_widget_one2many_product_picker/readme/USAGE.rst +++ b/web_widget_one2many_product_picker/readme/USAGE.rst @@ -1,3 +1,8 @@ +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..621f91237 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,28 @@ options="{'search': [{'name': _('Starts With'), 'domain': [('name', '=like'

    Usage

    +
    -

    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 +594,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 +616,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_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 @@

    -
    +
    - + +
    +
    + +
    + 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 @@
    - +