diff --git a/web_widget_one2many_product_picker/README.rst b/web_widget_one2many_product_picker/README.rst index 43be865e3..9db4b125e 100644 --- a/web_widget_one2many_product_picker/README.rst +++ b/web_widget_one2many_product_picker/README.rst @@ -175,87 +175,6 @@ Other example for 'purchase.order.line' fields: Usage ===== -You need to define the view fields. The view must be of ``form`` type. -This is an example that uses the 'sale.order.line' fields: - -.. code:: xml - - -
- - - - - - - - - - - - - - -Other example for 'purchase.order.line' fields: - -.. code:: xml - - -
- - - - - - - - - - - -** In this example we don't use 'field_map' option because the default match with the sale.order.line field names. - - -Default context: -~~~~~~~~~~~~~~~~ - -The widget sends a defaults context with the 'search_read' request: - - * active_search_group_name > Contains the name of the active search group - - * 'all' > Is the hard-coded name for the 'All' group - * 'main_lines' > Is the hard-coded name for the 'Lines' group - - * active_search_involved_fields > Contains an array of dictionaries with the fields used with the searchbox content - - * 'type' > Can be 'text' or 'number' - * 'field' > The field name - * 'oper' > The operator used - - Preview: ~~~~~~~~ diff --git a/web_widget_one2many_product_picker/readme/USAGE.rst b/web_widget_one2many_product_picker/readme/USAGE.rst index df29c4af0..a3b95d69b 100644 --- a/web_widget_one2many_product_picker/readme/USAGE.rst +++ b/web_widget_one2many_product_picker/readme/USAGE.rst @@ -1,84 +1,3 @@ -You need to define the view fields. The view must be of ``form`` type. -This is an example that uses the 'sale.order.line' fields: - -.. code:: xml - - -
- - - - - - - - - - - - - - -Other example for 'purchase.order.line' fields: - -.. code:: xml - - -
- - - - - - - - - - - -** In this example we don't use 'field_map' option because the default match with the sale.order.line field names. - - -Default context: -~~~~~~~~~~~~~~~~ - -The widget sends a defaults context with the 'search_read' request: - - * active_search_group_name > Contains the name of the active search group - - * 'all' > Is the hard-coded name for the 'All' group - * 'main_lines' > Is the hard-coded name for the 'Lines' group - - * active_search_involved_fields > Contains an array of dictionaries with the fields used with the searchbox content - - * 'type' > Can be 'text' or 'number' - * 'field' > The field name - * 'oper' > The operator used - - Preview: ~~~~~~~~ diff --git a/web_widget_one2many_product_picker/static/description/index.html b/web_widget_one2many_product_picker/static/description/index.html index 69e68cf65..67e169734 100644 --- a/web_widget_one2many_product_picker/static/description/index.html +++ b/web_widget_one2many_product_picker/static/description/index.html @@ -372,38 +372,37 @@ ul.auto-toc {

Table of contents

-

Installation

+

Installation

It’s advisable to install ‘web_widget_numeric_step’ to have a better usability on touch screens.

-

Configuration

+

Configuration

Create or edit a new view and use the new widget called ‘one2many_product_picker’. You need to define the view fields. The view must be of form type.

-

Widget options:

+

Widget options:

  • product_per_page > Integer -> Used to control the load more behaviour (16 by default)

  • @@ -474,7 +473,7 @@ options="{'search': [{'name': _('Starts With'), 'domain': [('name', '=like'
-

Default context:

+

Default context:

The widget sends a defaults context with the ‘search_read’ request:

    @@ -499,7 +498,7 @@ options="{'search': [{'name': _('Starts With'), 'domain': [('name', '=like'
-

Examples:

+

Examples:

This is an example that uses the ‘sale.order.line’ fields:

 <field
@@ -559,105 +558,23 @@ options="{'search': [{'name': _('Starts With'), 'domain': [('name', '=like'
 
-

Usage

-

You need to define the view fields. The view must be of form type. -This is an example that uses the ‘sale.order.line’ fields:

-
-<field
-    name="order_line"
-    attrs="{'readonly': [('state', 'in', ('done','cancel'))]}"
-    nolabel="1"
-    mode="form"
-    widget="one2many_product_picker"
-    options="{'search': [{'name': 'Test', 'domain': [['name', 'ilike', '$search']]}] ,'edit_discount': True, 'show_discount': True, 'groups': [{'name': 'desk', 'string': _('Desks'), 'domain': [('name', 'ilike', '%desk%')], 'order': [{'name': 'id', 'asc': true}]}, {'name': 'chair', 'string': _('Chairs'), 'domain': [('name', 'ilike', '%chair%')]}]}"
->
-    <form>
-        <field name="state" invisible="1" />
-        <field name="display_type" invisible="1" />
-        <field name="currency_id" invisible="1" />
-        <field name="discount" widget="numeric_step" options="{'max': 100}" invisible="1"/>
-        <field name="price_unit" widget="numeric_step" invisible="1"/>
-        <field name="name" invisible="1" />
-        <field name="product_id" invisible="1" />
-        <field name="order_id" invisible="1"/>
-        <field name="product_uom_qty" class="mb-1" widget="numeric_step" context="{
-            'partner_id': parent.partner_id,
-            'quantity': product_uom_qty,
-            'pricelist': parent.pricelist_id,
-            'uom': product_uom,
-            'company_id': parent.company_id
-        }" />
-        <field name="product_uom" force_save="1" attrs="{
-            'readonly': [('state', 'in', ('sale','done', 'cancel'))],
-            'required': [('display_type', '=', False)],
-        }" context="{'company_id': parent.company_id}" class="mb-2" options="{'no_open': True, 'no_create': True, 'no_edit': True}" />
-    </form>
-</field>
-
-

Other example for ‘purchase.order.line’ fields:

-
-<field
-    name="order_line"
-    attrs="{'readonly': [('state', 'in', ('done','cancel'))]}"
-    nolabel="1"
-    widget="one2many_product_picker"
-    mode="form"
-    options="{'search': [{'name': _('Name'), 'domain': [['name', 'ilike', '$search']]}, {'name': _('Price'), 'domain': [['list_price', '=', $number_search]]}], 'field_map': {'product_uom_qty': 'product_qty'}, 'groups': [{'name': _('Desk'), 'domain': [['name', 'ilike', 'desk']], 'order': {'name': 'id', 'asc': true}}, {'name': _('Chairs'), 'domain': [['name', 'ilike', 'chair']]}]}"
->
-    <form>
-        <field name="name" invisible="1" />
-        <field name="product_id" invisible="1" />
-        <field name="price_unit" invisible="1"  />
-        <field name="currency_id" invisible="1" />
-        <field name="order_id" invisible="1" />
-        <field name="date_planned" class="mb-1" />
-        <field name="product_qty" class="mb-1" widget="numeric_step" required="1" />
-        <field name="product_uom" class="mb-2" options="{'no_open': True, 'no_create': True, 'no_edit': True}" />
-    </form>
-</field>
-
-

** In this example we don’t use ‘field_map’ option because the default match with the sale.order.line field names.

-
-

Default context:

-

The widget sends a defaults context with the ‘search_read’ request:

-
-
    -
  • active_search_group_name > Contains the name of the active search group

    -
    -
      -
    • ‘all’ > Is the hard-coded name for the ‘All’ group
    • -
    • ‘main_lines’ > Is the hard-coded name for the ‘Lines’ group
    • -
    -
    -
  • -
  • active_search_involved_fields > Contains an array of dictionaries with the fields used with the searchbox content

    -
    -
      -
    • ‘type’ > Can be ‘text’ or ‘number’
    • -
    • ‘field’ > The field name
    • -
    • ‘oper’ > The operator used
    • -
    -
    -
  • -
-
-
+

Usage

-

Known issues / Roadmap

+

Known issues / Roadmap

  • Translations in the xml ‘options’ attribute of the field that use the widget can’t be exported automatically to be translated
  • The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects.
-

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 @@ -665,15 +582,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:

    @@ -687,7 +604,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/src/js/tools.js b/web_widget_one2many_product_picker/static/src/js/tools.js index 3c9ccafc8..34a9c018a 100644 --- a/web_widget_one2many_product_picker/static/src/js/tools.js +++ b/web_widget_one2many_product_picker/static/src/js/tools.js @@ -3,6 +3,8 @@ odoo.define("web_widget_one2many_product_picker.tools", function ( require ) { + "use strict"; + var field_utils = require("web.field_utils"); /** @@ -10,20 +12,23 @@ odoo.define("web_widget_one2many_product_picker.tools", function ( * * @param {Number} price * @param {Number} discount + * @returns {Number} */ - function priceReduce(price, discount) { + function priceReduce (price, discount) { return price * (1.0 - discount / 100.0); - }; + } /** * Print formatted price using the 'currency_field' * info in 'data'. * * @param {Number} value + * @param {Object} field_info, * @param {String} currency_field * @param {Object} data + * @returns {String} */ - function monetary(value, field_info, currency_field, data) { + function monetary (value, field_info, currency_field, data) { return field_utils.format.monetary( value, field_info, @@ -32,7 +37,7 @@ odoo.define("web_widget_one2many_product_picker.tools", function ( currency_field: currency_field, field_digits: true, }); - }; + } return { monetary: monetary, 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 a199d9762..338698e96 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 @@ -1,12 +1,16 @@ // Copyright 2020 Tecnativa - Alexandre Díaz // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", function (require) { +odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", function ( + require +) { "use strict"; var core = require("web.core"); var Widget = require("web.Widget"); var widgetRegistry = require("web.widget_registry"); - var ProductPickerQuickCreateFormView = require("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView").ProductPickerQuickCreateFormView; + var ProductPickerQuickCreateFormView = require( + "web_widget_one2many_product_picker.ProductPickerQuickCreateFormView" + ).ProductPickerQuickCreateFormView; var qweb = core.qweb; @@ -42,6 +46,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f this.id = this.state && this.state.id; this.editContext = {}; }, + /** * @override */ @@ -65,8 +70,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f var refinedContext = _.extend( {}, this.main_state.getContext(), - this.nodeContext, - ); + this.nodeContext); _.extend(refinedContext, this.editContext); this.formView = new ProductPickerQuickCreateFormView(fieldsView, { context: refinedContext, @@ -136,10 +140,10 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f this.start(); } else { var self = this; - this.getParent()._generateVirtualState({}, this.editContext).then(function(state) { + this.getParent()._generateVirtualState({}, this.editContext).then(function (state) { var data = {}; data[self.compareKey] = {operation: 'ADD', id: evt.data.compareValue}; - self.basicFieldParams.model._applyChange(state.id, data).then(function(){ + self.basicFieldParams.model._applyChange(state.id, data).then(function () { self.res_id = state.res_id; self.id = state.id; self.start(); 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 7f6eba729..a3c3b827d 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 @@ -1,6 +1,8 @@ // Copyright 2020 Tecnativa - Alexandre Díaz // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView", function (require) { +odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView", function ( + require +) { "use strict"; /** @@ -17,256 +19,282 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView BasicModel.include({ _applyOnChange: function (values, record, viewType) { if ('ignore_onchanges' in record.context) { - for (var field_name of record.context['ignore_onchanges']) { + var ignore_changes = record.context.ignore_onchanges; + for (var index in ignore_changes) { + var field_name = ignore_changes[index]; delete values[field_name]; } - delete record.context['ignore_onchanges']; + delete record.context.ignore_onchanges; } return this._super(values, record, viewType); }, }); - var ProductPickerQuickCreateFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend( - { - /** - * @override - */ - start: function () { - this.$el.addClass("oe_one2many_product_picker_form_view o_xxs_form_view"); - return this._super.apply(this, arguments); - }, - } - ); + var ProductPickerQuickCreateFormRenderer = + QuickCreateFormView.prototype.config.Renderer.extend( + { - var ProductPickerQuickCreateFormController = QuickCreateFormView.prototype.config.Controller.extend( - { - events: _.extend({}, QuickCreateFormView.prototype.events, { - "click .oe_record_add": "_onClickAdd", - "click .oe_record_remove": "_onClickRemove", - "click .oe_record_change": "_onClickChange", - "click .oe_record_discard": "_onClickDiscard", - }), + /** + * @override + */ + start: function () { + this.$el.addClass( + "oe_one2many_product_picker_form_view o_xxs_form_view"); + return this._super.apply(this, arguments); + }, + } + ); - init: function (parent, model, renderer, params) { - this.compareKey = params.compareKey; - this.fieldMap = params.fieldMap; - this.context = params.context; - this.mainRecordData = params.mainRecordData; - this._super.apply(this, arguments); - }, + var ProductPickerQuickCreateFormController = + QuickCreateFormView.prototype.config.Controller.extend( + { + events: _.extend({}, QuickCreateFormView.prototype.events, { + "click .oe_record_add": "_onClickAdd", + "click .oe_record_remove": "_onClickRemove", + "click .oe_record_change": "_onClickChange", + "click .oe_record_discard": "_onClickDiscard", + }), - /** - * Updates buttons depending on record status - * - * @private - */ - _updateButtons: function () { - var record = this.model.get(this.handle); - var state = "record"; - if (this.model.isNew(record.id)) { - state = "new"; - } else if (record.isDirty()) { - state = "dirty"; - } - if (state === "new") { - for (var index in this.mainRecordData.data) { - var recordData = this.mainRecordData.data[index]; - if (recordData.ref === record.ref) { - if (record.isDirty()) { - state = "dirty"; - } else { - state = "record"; - } - break; - } - } - } - this.$el.find(".oe_one2many_product_picker_form_buttons").remove(); - this.$el.find(".o_form_view").append( - qweb.render("One2ManyProductPicker.QuickCreate.FormButtons", { - state: state, - }) - ); - }, - - /** - * @private - */ - _disableQuickCreate: function () { - this._disabled = true; // ensures that the record won't be created twice - this.$el.addClass("o_disabled"); - this.$("input:not(:disabled)") - .addClass("o_temporarily_disabled") - .attr("disabled", "disabled"); - }, - - /** - * @private - */ - _enableQuickCreate: function () { - this._disabled = false; // allows to create again - this.$el.removeClass("o_disabled"); - this.$("input.o_temporarily_disabled") - .removeClass("o_temporarily_disabled") - .attr("disabled", false); - }, - - /** - * @private - * @param {Array[String]} fields_changed - */ - _needReloadCard: function (fields_changed) { - for (var index in fields_changed) { - var field = fields_changed[index]; - if (field === this.fieldMap[this.compareKey]) { - return true; - } - } - return false; - }, - - /** - * Handle "compare field" changes. This field is used - * as master to know if we are editing or creating a - * new record. - * - * @private - * @param {ChangeEvent} ev - */ - _onFieldChanged: function (ev) { - var fields_changed = Object.keys(ev.data.changes); - if (this._needReloadCard(fields_changed)) { - var field = ev.data.changes[fields_changed[0]]; - var new_value = false; - if (typeof field === "object") { - new_value = field.id; - } else { - new_value = field; - } - var reload_values = { - compareValue: new_value, - } - var record = this.model.get(this.handle); - if (!('base_record_id' in record.context)) { - var old_value = record.data[this.compareKey]; - if (typeof old_value === 'object') { - old_value = old_value.data.id; - } - reload_values['baseRecordID'] = record.id; - reload_values['baseRecordResID'] = record.ref; - reload_values['baseRecordCompareValue'] = old_value; - } else { - reload_values['baseRecordID'] = record.context.base_record_id; - reload_values['baseRecordResID'] = record.context.base_record_res_id; - reload_values['baseRecordCompareValue'] = record.context.base_record_compare_value; - } - this.trigger_up("reload_view", reload_values); - - // Discard current change - ev.data.changes = {}; - } else { + init: function (parent, model, renderer, params) { + this.compareKey = params.compareKey; + this.fieldMap = params.fieldMap; + this.context = params.context; + this.mainRecordData = params.mainRecordData; this._super.apply(this, arguments); - if (!_.isEmpty(ev.data.changes)) { - if (this.model.isPureVirtual(this.handle)) { - this.model.unsetDirty(this.handle); - } - this.model.updateRecordContext(this.handle, {has_changes_confirmed: false}); - this.trigger_up("quick_record_updated", { - changes: ev.data.changes, - }); - this._updateButtons(); + }, + + /** + * Updates buttons depending on record status + * + * @private + */ + _updateButtons: function () { + var record = this.model.get(this.handle); + var state = "record"; + if (this.model.isNew(record.id)) { + state = "new"; + } else if (record.isDirty()) { + state = "dirty"; } - } - }, + if (state === "new") { + for (var index in this.mainRecordData.data) { + var recordData = this.mainRecordData.data[index]; + if (recordData.ref === record.ref) { + if (record.isDirty()) { + state = "dirty"; + } else { + state = "record"; + } + break; + } + } + } + this.$el.find( + ".oe_one2many_product_picker_form_buttons").remove(); + this.$el.find(".o_form_view").append( + qweb.render( + "One2ManyProductPicker.QuickCreate.FormButtons", { + state: state, + }) + ); + }, - /** - * @returns {Deferred} - */ - _add: function () { - if (this._disabled) { - // don't do anything if we are already creating a record - return $.Deferred(); - } - var self = this; - this._disableQuickCreate(); - return this.saveRecord(this.handle, { - stayInEdit: true, - reload: true, - 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.model.unsetDirty(self.handle); - self._updateButtons(); - }); - }, + /** + * @private + */ + _disableQuickCreate: function () { - /** - * @private - * @param {MouseEvent} ev - */ - _onClickAdd: function (ev) { - ev.stopPropagation(); - this.model.updateRecordContext(this.handle, {has_changes_confirmed: true}); - this._add(); - }, + // Ensures that the record won't be created twice + this._disabled = true; + this.$el.addClass("o_disabled"); + this.$("input:not(:disabled)") + .addClass("o_temporarily_disabled") + .attr("disabled", "disabled"); + }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickRemove: function (ev) { - ev.stopPropagation(); - this.trigger_up("list_record_remove", {id: this.renderer.state.id}); - }, + /** + * @private + */ + _enableQuickCreate: function () { - /** - * @private - * @param {MouseEvent} ev - */ - _onClickChange: function (ev) { - ev.stopPropagation(); - 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"); - this.model.unsetDirty(this.handle); - this._updateButtons(); - }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickDiscard: function (ev) { - var self = this; - ev.stopPropagation(); - var record = this.model.get(this.handle); - this.model.discardChanges(this.handle, { - rollback: true, - }); - this.trigger_up("quick_record_updated", { - changes: record.data, - }); - if (this.model.isNew(record.id)) { - this.update({}, {reload: false}); - this.trigger_up("restore_flip_card"); - this._updateButtons(); - } else { - this.update({}, {reload: false}).then(function(){ + // Allows to create again + this._disabled = false; + this.$el.removeClass("o_disabled"); + this.$("input.o_temporarily_disabled") + .removeClass("o_temporarily_disabled") + .attr("disabled", false); + }, + + /** + * @private + * @param {Array[String]} fields_changed + * @returns {Boolean} + */ + _needReloadCard: function (fields_changed) { + for (var index in fields_changed) { + var field = fields_changed[index]; + if (field === this.fieldMap[this.compareKey]) { + return true; + } + } + return false; + }, + + /** + * Handle "compare field" changes. This field is used + * as master to know if we are editing or creating a + * new record. + * + * @private + * @param {ChangeEvent} ev + */ + _onFieldChanged: function (ev) { + var fields_changed = Object.keys(ev.data.changes); + if (this._needReloadCard(fields_changed)) { + var field = ev.data.changes[fields_changed[0]]; + var new_value = false; + if (typeof field === "object") { + new_value = field.id; + } else { + new_value = field; + } + var reload_values = { + compareValue: new_value, + }; + var record = this.model.get(this.handle); + if (!('base_record_id' in record.context)) { + var old_value = record.data[this.compareKey]; + if (typeof old_value === 'object') { + old_value = old_value.data.id; + } + reload_values.baseRecordID = record.id; + reload_values.baseRecordResID = record.ref; + reload_values.baseRecordCompareValue = old_value; + } else { + reload_values.baseRecordID = + record.context.base_record_id; + reload_values.baseRecordResID = + record.context.base_record_res_id; + reload_values.baseRecordCompareValue = + record.context.base_record_compare_value; + } + this.trigger_up("reload_view", reload_values); + + // Discard current change + ev.data.changes = {}; + } else { + this._super.apply(this, arguments); + if (!_.isEmpty(ev.data.changes)) { + if (this.model.isPureVirtual(this.handle)) { + this.model.unsetDirty(this.handle); + } + this.model.updateRecordContext(this.handle, { + has_changes_confirmed: false, + }); + this.trigger_up("quick_record_updated", { + changes: ev.data.changes, + }); + this._updateButtons(); + } + } + }, + + /** + * @returns {Deferred} + */ + _add: function () { + if (this._disabled) { + + // Don't do anything if we are already creating a record + return $.Deferred(); + } + var self = this; + this._disableQuickCreate(); + return this.saveRecord(this.handle, { + stayInEdit: true, + reload: true, + 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.model.unsetDirty(self.handle); - self.trigger_up("restore_flip_card"); self._updateButtons(); }); - } - }, - } - ); + }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickAdd: function (ev) { + ev.stopPropagation(); + this.model.updateRecordContext(this.handle, { + has_changes_confirmed: true, + }); + this._add(); + }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickRemove: function (ev) { + ev.stopPropagation(); + this.trigger_up("list_record_remove", { + id: this.renderer.state.id, + }); + }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickChange: function (ev) { + ev.stopPropagation(); + 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"); + this.model.unsetDirty(this.handle); + this._updateButtons(); + }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickDiscard: function (ev) { + var self = this; + ev.stopPropagation(); + var record = this.model.get(this.handle); + this.model.discardChanges(this.handle, { + rollback: true, + }); + this.trigger_up("quick_record_updated", { + changes: record.data, + }); + if (this.model.isNew(record.id)) { + this.update({}, {reload: false}); + this.trigger_up("restore_flip_card"); + this._updateButtons(); + } else { + this.update({}, {reload: false}).then(function () { + self.model.unsetDirty(self.handle); + self.trigger_up("restore_flip_card"); + self._updateButtons(); + }); + } + }, + } + ); var ProductPickerQuickCreateFormView = QuickCreateFormView.extend({ config: _.extend({}, QuickCreateFormView.prototype.config, { diff --git a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form.js b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form.js index e8a0550df..e014707d5 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form.js +++ b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form.js @@ -1,11 +1,15 @@ // Copyright 2020 Tecnativa - Alexandre Díaz // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm", function (require) { +odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm", function ( + require +) { "use strict"; var core = require("web.core"); var Widget = require("web.Widget"); - var ProductPickerQuickModifPriceFormView = require("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView").ProductPickerQuickModifPriceFormView; + var ProductPickerQuickModifPriceFormView = require( + "web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView" + ).ProductPickerQuickModifPriceFormView; var qweb = core.qweb; @@ -101,7 +105,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm */ _generateFormArch: function () { var wanted_field_states = this._getWantedFieldState(); - var template = ""; + var template = + ""; template += this.basicFieldParams.field.views.form.arch; template += ""; qweb.add_template(template); @@ -115,7 +120,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm for (var index in field_names) { var field_name = field_names[index]; var $field = $arch.find("field[name='"+field_name+"']"); - var modifiers = $field.attr("modifiers") ? JSON.parse($field.attr("modifiers")) : {}; + var modifiers = + $field.attr("modifiers") ? JSON.parse($field.attr("modifiers")) : {}; modifiers.invisible = false; modifiers.readonly = wanted_field_states[field_name]; $field.attr("modifiers", JSON.stringify(modifiers)); @@ -134,7 +140,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm * @private * @returns {Object} */ - _getWantedFieldState: function() { + _getWantedFieldState: function () { var wantedFieldState = {}; wantedFieldState[this.fieldMap.discount] = !this.canEditDiscount; wantedFieldState[this.fieldMap.price_unit] = !this.canEditPrice; diff --git a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form_view.js b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form_view.js index be3bef345..7b6a8f7a2 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form_view.js +++ b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form_view.js @@ -1,6 +1,8 @@ // Copyright 2020 Tecnativa - Alexandre Díaz // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView", function (require) { +odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView", function ( + require +) { "use strict"; /** @@ -14,175 +16,190 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm var qweb = core.qweb; - var ProductPickerQuickModifPriceFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend( - { - /** - * @override - */ - start: function () { - var self = this; - this.$el.addClass("oe_one2many_product_picker_form_view o_xxs_form_view"); - return this._super.apply(this, arguments).then(function(){ - self._appendPrice(); - self._appendButtons(); - }); - }, + var ProductPickerQuickModifPriceFormRenderer = + QuickCreateFormView.prototype.config.Renderer.extend( + { + /** + * @override + */ + start: function () { + var self = this; + this.$el.addClass( + "oe_one2many_product_picker_form_view o_xxs_form_view"); + return this._super.apply(this, arguments).then(function () { + self._appendPrice(); + self._appendButtons(); + }); + }, - /** - * @private - */ - _appendButtons: function () { - this.$el.find(".oe_one2many_product_picker_form_buttons").remove(); - this.$el.append( - qweb.render("One2ManyProductPicker.QuickModifPrice.FormButtons", { - mode: this.mode, - }) - ); - }, + /** + * @private + */ + _appendButtons: function () { + this.$el.find( + ".oe_one2many_product_picker_form_buttons").remove(); + this.$el.append( + qweb.render( + "One2ManyProductPicker.QuickModifPrice.FormButtons", { + mode: this.mode, + }) + ); + }, - /** - * @private - */ - _appendPrice: function () { - this.$el.find(".oe_price").remove(); - this.$el.append( - qweb.render("One2ManyProductPicker.QuickModifPrice.Price") - ); + /** + * @private + */ + _appendPrice: function () { + this.$el.find(".oe_price").remove(); + this.$el.append( + qweb.render("One2ManyProductPicker.QuickModifPrice.Price") + ); + }, } + ); - } - ); + var ProductPickerQuickModifPriceFormController = + QuickCreateFormView.prototype.config.Controller.extend( + { + events: _.extend({}, QuickCreateFormView.prototype.events, { + "click .oe_record_change": "_onClickChange", + "click .oe_record_discard": "_onClickDiscard", + }), - var ProductPickerQuickModifPriceFormController = QuickCreateFormView.prototype.config.Controller.extend( - { - events: _.extend({}, QuickCreateFormView.prototype.events, { - "click .oe_record_change": "_onClickChange", - "click .oe_record_discard": "_onClickDiscard", - }), + /** + * @override + */ + init: function (parent, model, renderer, params) { + this.fieldMap = params.fieldMap; + this.context = params.context; + this._super.apply(this, arguments); + this.currencyField = params.currencyField; + this.parentRecordData = params.parentRecordData; + }, - /** - * @override - */ - init: function (parent, model, renderer, params) { - this.fieldMap = params.fieldMap; - this.context = params.context; - this._super.apply(this, arguments); - this.currencyField = params.currencyField; - this.parentRecordData = params.parentRecordData; - }, + /** + * @override + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self._updatePrice(); + }); + }, - /** - * @override - */ - start: function () { - var self = this; - return this._super.apply(this, arguments).then(function () { - self._updatePrice(); - }); - }, + /** + * @override + */ + _onFieldChanged: function () { + this._super.apply(this, arguments); + this._updatePrice(); + }, - /** - * @override - */ - _onFieldChanged: function (ev) { - this._super.apply(this, arguments); - this._updatePrice(); - }, + /** + * @private + */ + _updatePrice: function () { + var record = this.model.get(this.handle); + var price_reduce = tools.priceReduce( + record.data[this.fieldMap.price_unit], + record.data[this.fieldMap.discount]); + this.renderer.$el.find(".oe_price").html( + tools.monetary( + price_reduce, + this.getParent().state.fields[this.fieldMap.price_unit], + this.currencyField, + record + ) + ); + }, - /** - * @private - */ - _updatePrice: function () { - var record = this.model.get(this.handle); - var price_reduce = tools.priceReduce(record.data[this.fieldMap.price_unit], record.data[this.fieldMap.discount]); - this.renderer.$el.find(".oe_price").html( - tools.monetary( - price_reduce, - this.getParent().state.fields[this.fieldMap.price_unit], - this.currencyField, - record - ) - ); - }, + /** + * @private + */ + _disableQuickCreate: function () { - /** - * @private - */ - _disableQuickCreate: function () { - this._disabled = true; // ensures that the record won't be created twice - this.$el.addClass("o_disabled"); - this.$("input:not(:disabled)") - .addClass("o_temporarily_disabled") - .attr("disabled", "disabled"); - }, + // Ensures that the record won't be created twice + this._disabled = true; + this.$el.addClass("o_disabled"); + this.$("input:not(:disabled)") + .addClass("o_temporarily_disabled") + .attr("disabled", "disabled"); + }, - /** - * @private - */ - _enableQuickCreate: function () { - this._disabled = false; // allows to create again - this.$el.removeClass("o_disabled"); - this.$("input.o_temporarily_disabled") - .removeClass("o_temporarily_disabled") - .attr("disabled", false); - }, + /** + * @private + */ + _enableQuickCreate: function () { - /** - * @private - * @param {MouseEvent} ev - */ - _onClickChange: function (ev) { - var self = this; - ev.stopPropagation(); - this.model.updateRecordContext(this.handle, {has_changes_confirmed: true}); - var is_virtual = this.model.isPureVirtual(this.handle); - // If is a 'pure virtual' record, save it in the selected list - if (is_virtual) { - if (this.model.isDirty(this.handle)) { - this._disableQuickCreate(); - this.saveRecord(this.handle, { - stayInEdit: true, - reload: true, - savePoint: true, - viewType: "form", - }).then(function() { - self._enableQuickCreate(); - var record = self.model.get(self.handle); - self.trigger_up("create_quick_record", { - id: record.id, + // Allows to create again + this._disabled = false; + this.$el.removeClass("o_disabled"); + this.$("input.o_temporarily_disabled") + .removeClass("o_temporarily_disabled") + .attr("disabled", false); + }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickChange: function (ev) { + var self = this; + ev.stopPropagation(); + this.model.updateRecordContext(this.handle, { + has_changes_confirmed: true, + }); + var is_virtual = this.model.isPureVirtual(this.handle); + + // If is a 'pure virtual' record, save it in the selected list + if (is_virtual) { + if (this.model.isDirty(this.handle)) { + this._disableQuickCreate(); + this.saveRecord(this.handle, { + stayInEdit: true, + reload: true, + savePoint: true, + viewType: "form", + }).then(function () { + self._enableQuickCreate(); + var record = self.model.get(self.handle); + self.model.unsetDirty(self.handle); + self.trigger_up("create_quick_record", { + id: record.id, + }); + self.getParent().destroy(); }); - self.getParent().destroy(); - }); + } else { + this.getParent().destroy(); + } } else { + + // If is a "normal" record, update it + var record = this.model.get(this.handle); + this.trigger_up("update_quick_record", { + id: record.id, + }); this.getParent().destroy(); } - } else { - // If is a "normal" record, update it + }, + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickDiscard: function (ev) { + ev.stopPropagation(); + this.model.discardChanges(this.handle, { + rollback: true, + }); var record = this.model.get(this.handle); this.trigger_up("update_quick_record", { id: record.id, }); this.getParent().destroy(); - } - }, - - /** - * @private - * @param {MouseEvent} ev - */ - _onClickDiscard: function (ev) { - ev.stopPropagation(); - this.model.discardChanges(this.handle, { - rollback: true, - }); - var record = this.model.get(this.handle); - this.trigger_up("update_quick_record", { - id: record.id, - }); - this.getParent().destroy(); - }, - } - ); + }, + } + ); var ProductPickerQuickModifPriceFormView = QuickCreateFormView.extend({ config: _.extend({}, QuickCreateFormView.prototype.config, { 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 c07d59531..a8b583cf8 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 @@ -1,3 +1,4 @@ +/* global py */ // Copyright 2020 Tecnativa - Alexandre Díaz // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", function ( @@ -9,12 +10,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var Widget = require("web.Widget"); var Domain = require("web.Domain"); var widgetRegistry = require("web.widget_registry"); - var core = require("web.core"); var tools = require("web_widget_one2many_product_picker.tools"); var ProductPickerQuickModifPriceForm = require( "web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm"); var qweb = core.qweb; + var _t = core._t; /* This represent a record (a card) */ var One2ManyProductPickerRecord = Widget.extend({ @@ -74,7 +75,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @override */ update: function (record) { - // detach the widgets because the record will empty its $el, which + + // Detach the widgets because the record will empty its $el, which // will remove all event handlers on its descendants, and we want // to keep those handlers alive as we will re-use these widgets _.invoke(_.pluck(this.subWidgets, "$el"), "detach"); @@ -100,8 +102,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * Generates the URL for the given product using the selected field * * @private - * @param {string} field - * @returns {string} + * @param {Number} product_id + * @param {String} field_name + * @returns {String} */ _getImageUrl: function (product_id, field_name) { return _.str.sprintf( @@ -128,8 +131,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu /** * @private - * @param {string} d a stringified domain - * @returns {boolean} the domain evaluted with the current values + * @param {String} d a stringified domain + * @returns {Boolean} the domain evaluted with the current values */ _computeDomain: function (d) { return new Domain(d).compute( @@ -160,6 +163,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @returns {Object} */ _getQWebContext: function () { + // 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. @@ -186,7 +190,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu */ _getInternalVirtualRecordContext: function () { var context = {}; - context["default_" + this.options.basicFieldParams.relation_field] = this.options.basicFieldParams.state.id || null; + context["default_" + this.options.basicFieldParams.relation_field] = + this.options.basicFieldParams.state.id || null; return context; }, @@ -199,7 +204,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu */ _getInternalVirtualRecordData: function () { var data = {}; - data[this.options.fieldMap.product] = {operation: 'ADD', id: this.recordSearch.id}; + data[this.options.fieldMap.product] = { + operation: 'ADD', + id: this.recordSearch.id, + }; return data; }, @@ -211,12 +219,14 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu */ _generateVirtualState: function (data, context) { var model = this.options.basicFieldParams.model; - var scontext = _.extend({}, this._getInternalVirtualRecordContext(), context); + var scontext = _.extend( + {}, this._getInternalVirtualRecordContext(), context); var sdata = _.extend({}, this._getInternalVirtualRecordData(), data); - return model.createVirtualRecord(this.options.basicFieldParams.value.id, { - data: sdata, - context: scontext, - }); + return model.createVirtualRecord( + this.options.basicFieldParams.value.id, { + data: sdata, + context: scontext, + }); }, /** @@ -225,7 +235,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu _render: function () { this.defs = []; this._replaceElement( - qweb.render("One2ManyProductPicker.FlipCard", this._getQWebContext()) + qweb.render( + "One2ManyProductPicker.FlipCard", + this._getQWebContext() + ) ); this.$card = this.$(".oe_flip_card"); this.$front = this.$(".oe_flip_card_front"); @@ -241,6 +254,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * any, or directly by the formatted value * * @private + * @param {jQueryElement} $container */ _processWidgetFields: function ($container) { var self = this; @@ -257,13 +271,21 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu // even if it is not specified in the view. if (field_widget || self.fields[field_name].type === "many2many") { var widget = self.subWidgets[field_name]; - if (!widget) { + if (widget) { + + // a widget already exists for that field, so reset it + // with the new state + widget.reset(self.state); + $field.replaceWith(widget.$el); + } else { + // the widget doesn't exist yet, so instanciate it var Widget = self.fieldsInfo[field_name].Widget; if (Widget) { widget = self._processWidget($field, field_name, Widget); self.subWidgets[field_name] = widget; } else if (config.debug) { + // the widget is not implemented $field.replaceWith( $("", { @@ -274,11 +296,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu }) ); } - } else { - // a widget already exists for that field, so reset it - // with the new state - widget.reset(self.state); - $field.replaceWith(widget.$el); } } }); @@ -294,6 +311,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @returns {Widget} the widget instance */ _processWidget: function ($field, field_name, Widget) { + // some field's attrs might be record dependent (they start with // 't-att-') and should thus be evaluated, which is done by qweb // we here replace those attrs in the dict of attrs of the state @@ -301,7 +319,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu // field's widgets point of view // that dict being shared between records, we don't modify it // in place - var self = this; var attrs = Object.create(null); _.each(this.fieldsInfo[field_name], function (value, key) { if (_.str.startsWith(key, "t-att-")) { @@ -310,8 +327,14 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu } attrs[key] = value; }); - var options = _.extend({}, this.options, {attrs: attrs, data: this.state.data}); - var widget = new Widget(this, field_name, this.getParent().state, options); + var options = _.extend({}, this.options, { + attrs: attrs, + data: this.state.data, + }); + var widget = new Widget( + this, field_name, + this.getParent().state, + options); var def = widget.replace($field); if (def.state() === "pending") { this.defs.push(def); @@ -329,8 +352,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var self = this; $container.find("widget").each(function () { var $field = $(this); - var Widget = widgetRegistry.get($field.attr("name")); - var widget = new Widget(self, { + var FieldWidget = widgetRegistry.get($field.attr("name")); + var widget = new FieldWidget(self, { fieldsInfo: self.fieldsInfo, fields: self.fields, main_state: self.getParent().state, @@ -346,8 +369,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu self.widgets.push(widget); var def = widget - ._widgetRenderAndInsert(function () {}) - .then(function () { + ._widgetRenderAndInsert(function () { + // Do nothing + }).then(function () { widget.$el.addClass("o_widget"); $field.replaceWith(widget.$el); }); @@ -384,7 +408,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var to_find = []; if (!_.isEmpty(fields)) { - to_find = _.map(fields, function(field){ return _.str.sprintf("[data-field=%s]", [field]); }); + to_find = _.map(fields, function (field) { + return _.str.sprintf("[data-field=%s]", [field]); + }); } else { to_find = ["[data-field]"]; } @@ -401,11 +427,13 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var field_map = this.options.fieldMap; if (state_data) { var has_discount = state_data[field_map.discount] > 0.0; - this.$el.find(".original_price,.discount_price").toggleClass("d-none", !has_discount); + this.$el.find(".original_price,.discount_price") + .toggleClass("d-none", !has_discount); if (has_discount) { this.$el.find(".price_unit").html(this._calcPriceReduced()); } else { - this.$el.find(".price_unit").html(this._getMonetaryFieldValue("price_unit")); + this.$el.find(".price_unit").html( + this._getMonetaryFieldValue("price_unit")); } } } @@ -420,7 +448,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var field_map = this.options.fieldMap; var state_data = this.state.data; if (state_data && state_data[field_map.discount]) { - price_reduce = tools.priceReduce(state_data[field_map.price_unit], state_data[field_map.discount]); + price_reduce = tools.priceReduce( + state_data[field_map.price_unit], + state_data[field_map.discount]); } return price_reduce && tools.monetary( price_reduce, @@ -451,7 +481,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu canEditDiscount: this.options.editDiscount, currencyField: this.options.currencyField, }); - this.$modifPricePopup = $(qweb.render("One2ManyProductPicker.QuickModifPricePopup")); + this.$modifPricePopup = $( + qweb.render("One2ManyProductPicker.QuickModifPricePopup")); this.$modifPricePopup.appendTo($(".o_main_content")); modif_price_form.attachTo(this.$modifPricePopup); }, @@ -463,12 +494,15 @@ 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) { return; } if (!this._clickFlipCardDelayed) { - this._clickFlipCardDelayed = setTimeout(this._onClickDelayedFlipCard.bind(this, evt), this._click_card_delayed_time); + this._clickFlipCardDelayed = setTimeout( + this._onClickDelayedFlipCard.bind(this, evt), + this._click_card_delayed_time); } ++this._clickFlipCardCount; if (this._clickFlipCardCount >= 2) { @@ -481,9 +515,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu /** * @private - * @param {MouseEvent} evt */ - _onClickDelayedFlipCard: function (evt) { + _onClickDelayedFlipCard: function () { this._clickFlipCardDelayed = false; this._clickFlipCardCount = 0; @@ -499,12 +532,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu this._processWidgetFields(this.$back); this._processWidgets(this.$back); this._processDynamicFields(); - $.when(this.defs).then(function(){ + $.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(() => { + setTimeout(function () { self.$('.oe_flip_card_front').addClass("d-none"); }, 200); }); @@ -517,7 +550,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu */ _onDblClickDelayedFlipCard: function (evt) { var $target = $(evt.target); - if ($target.hasClass('badge_price') || $target.parents('.badge_price').length) { + if ( + $target.hasClass('badge_price') || + $target.parents('.badge_price').length + ) { this._openPriceModifier(); } else { var $currentTarget = $(evt.currentTarget); @@ -525,7 +561,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var cur_img_src = $img.attr("src"); if ($currentTarget.hasClass('oe_flip_card_maximized')) { $currentTarget.removeClass('oe_flip_card_maximized'); - $currentTarget.on('transitionend', function() { + $currentTarget.on('transitionend', function () { $currentTarget.css({ position: "", top: "", @@ -540,7 +576,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var $actived_card = this.$el.parent().find(".active"); if ($actived_card[0] !== $currentTarget[0]) { $actived_card.removeClass("active"); - $actived_card.find('.oe_flip_card_front').removeClass("d-none"); + $actived_card.find('.oe_flip_card_front') + .removeClass("d-none"); } var offset = $currentTarget.offset(); $currentTarget.css({ @@ -551,7 +588,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu height: $currentTarget.height(), zIndex: 50, }); - _.defer(function(){ + _.defer(function () { $currentTarget.addClass('oe_flip_card_maximized'); }); } @@ -562,9 +599,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu /** * @private - * @param {CustomEvent} evt */ - _onRestoreFlipCard: function (evt) { + _onRestoreFlipCard: function () { this.$(".oe_flip_card").removeClass("active"); this.$('.oe_flip_card_front').removeClass("d-none"); }, @@ -584,8 +620,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @private * @param {CustomEvent} evt */ - _onQuickRecordUpdated: function (ev) { - this._processDynamicFields(Object.keys(ev.data.changes)); + _onQuickRecordUpdated: function (evt) { + this._processDynamicFields(Object.keys(evt.data.changes)); this.trigger_up("update_subtotal"); }, }); diff --git a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/renderer.js b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/renderer.js index 54c1fc4f2..8fe8626e0 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 @@ -1,11 +1,14 @@ // Copyright 2020 Tecnativa - Alexandre Díaz // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", function (require) { +odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", function ( + require +) { "use strict"; var core = require("web.core"); var BasicRenderer = require("web.BasicRenderer"); - var One2ManyProductPickerRecord = require("web_widget_one2many_product_picker.One2ManyProductPickerRecord"); + var One2ManyProductPickerRecord = require( + "web_widget_one2many_product_picker.One2ManyProductPickerRecord"); var qweb = core.qweb; @@ -14,7 +17,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", className: 'oe_one2many_product_picker_view', events: { - //'scroll': '_lazyOnScrollView', 'click #productPickerLoadMore': '_onClickLoadMore', }, @@ -30,13 +32,16 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", this.recordOptions = _.extend({}, params.record_options, { viewType: 'One2ManyProductPicker', }); - // Workaraound: Odoo initilize this class so we need do this to + + // Workaround: Odoo initilize this class so we need do this to // 'receive' more arguments. this.options = parent.options; this.mode = parent.mode; this.search_data = parent._searchRecords; this.last_search_data_count = parent._lastSearchRecordsCount; - this._lazyOnScrollView = _.debounce(this._onScrollView.bind(this), this.DELAY_GET_RECORDS); + this._lazyOnScrollView = _.debounce( + this._onScrollView.bind(this), + this.DELAY_GET_RECORDS); }, /** @@ -67,12 +72,11 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", * @override */ start: function () { - //this.$el.addClass("row"); return this._super.apply(this, arguments); }, /** - * @param {Object} searchState + * @param {Object} search_data */ updateSearchData: function (search_data, count) { this.search_data = search_data; @@ -99,7 +103,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", return this._super.apply(this, arguments); } var old_state = _.clone(this.state.data); - return this._super(state, _.extend({}, params, {noRender: true})).then(function() { + return this._super( + state, + _.extend({}, params, {noRender: true}) + ).then(function () { self._updateStateRecords(old_state); }); }, @@ -112,8 +119,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", _removeRecords: function (states) { var defs = []; var to_destroy = []; - for (var index in states) { - var state = states[index]; + for (var index_state in states) { + var state = states[index_state]; for (var e = this.widgets.length-1; e>=0; --e) { var widget = this.widgets[e]; if (widget && widget.state.id === state.id) { @@ -126,22 +133,32 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", // If doesn't exists other records with the same product, we need // create a 'pure virtual' record again. - for (var index in to_destroy) { - var widget_destroyed = to_destroy[index]; - var widget_product_id = widget_destroyed.state.data[this.options.field_map.product].data.id; + for (var index_destroy in to_destroy) { + var widget_destroyed = to_destroy[index_destroy]; + var widget_product_id = widget_destroyed.state + .data[this.options.field_map.product].data.id; var found = false; - for (var e = this.widgets.length-1; e>=0; --e) { - var widget = this.widgets[e]; - if (widget.state.data[this.options.field_map.product].data.id === widget_product_id) { + for (var eb = this.widgets.length-1; eb>=0; --eb) { + var widget = this.widgets[eb]; + if ( + widget.state.data[this.options.field_map.product].data.id === widget_product_id + ) { found = true; break; } } if (!found) { - var search_record = _.find(this.search_data, {id: widget_product_id}) + var search_record = _.find(this.search_data, {id: widget_product_id}); var new_search_record = _.extend({}, search_record, {__id: state.id}); var search_record_index = widget_destroyed.$el.index(); - defs.push(this.appendSearchRecords([new_search_record], false, true, search_record_index)); + defs.push( + this.appendSearchRecords( + [new_search_record], + false, + true, + search_record_index + ) + ); } } @@ -155,9 +172,11 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", * Thanks to this we don't need re-render 'pure virtual' records. * * @private + * @param {Object} old_states * @returns {Deferred} */ _updateStateRecords: function (old_states) { + // States to remove var states_to_destroy = []; for (var index in old_states) { @@ -187,6 +206,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", for (var e = this.widgets.length-1; e>=0; --e) { var widget = this.widgets[e]; if (!widget) { + // Already processed widget (deleted) continue; } @@ -194,17 +214,25 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", widget.recreate(state); exists = true; break; - } else if (widget.recordSearch.id === state.data[this.options.field_map.product].data.id) { + } else if ( + widget.recordSearch.id === state.data[this.options.field_map.product].data.id + ) { + // Is a new record search_record_index = widget.$el.index(); search_record = widget.recordSearch; } + // 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) { + if ( + widget.is_virtual && + 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]; } } + // Need add a new one? if (!exists && search_record_index !== -1) { var new_search_record = _.extend({}, search_record, {__id: state.id}); @@ -228,8 +256,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", }); this.$extraButtonsContainer = $(qweb.render("One2ManyProductPicker.ExtraButtons")); this.$btnLoadMore = this.$extraButtonsContainer.find("#productPickerLoadMore"); - return $.Deferred(function(d){ - self.appendSearchRecords(self.search_data, true).then(function(){ + return $.Deferred(function (d) { + self.appendSearchRecords(self.search_data, true).then(function () { _.invoke(oldWidgets, "destroy"); self.$el.empty(); self.$el.append(self.$recordsContainer); @@ -248,6 +276,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", * * @private * @param {Array[Object]} results + * @returns {Array[Object]} */ _processSearchRecords: function (results) { var field_name = this.options.field_map.product; @@ -256,7 +285,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", var record_search = results[index]; var state_data_found = false; - for (var state_record of this.state.data) { + for (var index_data in this.state.data) { + var state_record = this.state.data[index_data]; var field = state_record.data[field_name]; if ( (typeof field === "object" && field.data.id === record_search.id) || @@ -321,7 +351,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", */ _appendSearchRecords: function (search_records, no_process_records, position) { var self = this; - var processed_records = no_process_records?search_records:this._processSearchRecords(search_records); + var processed_records = + no_process_records?search_records:this._processSearchRecords(search_records); _.each(processed_records, function (search_record) { var state_data = self._getRecordDataById(search_record.__id); var ProductPickerRecord = new One2ManyProductPickerRecord( @@ -330,6 +361,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", self._getRecordOptions(search_record) ); self.widgets.push(ProductPickerRecord); + // Simulate new lines to dispatch get_default & onchange's to get the // relevant data to print. This case increase the TTI time. if (!state_data) { @@ -338,15 +370,17 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", self.defsVirtualState.push(defVirtualState); } } + // At this point the widget will use the existing state (line) or // 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); - } - }); + 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); + } + }); if (def.state() === "pending") { self.defs.push(def); } @@ -380,7 +414,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", delete this.defs; var defsVirtualState = this.defsVirtualState; delete this.defsVirtualState; - $.when.apply($, defsVirtualState).then(function(){ + $.when.apply($, defsVirtualState).then(function () { self.trigger_up("loading_records", {finished:true}); }); return $.when.apply($, defs).then(function () { @@ -415,11 +449,11 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", /** * @private */ - _onClickLoadMore: function (evt) { + _onClickLoadMore: function () { this.$btnLoadMore.attr("disabled", true); this.trigger_up("load_more"); this._loadMoreWorking = true; - } + }, }); diff --git a/web_widget_one2many_product_picker/static/src/js/views/basic_model.js b/web_widget_one2many_product_picker/static/src/js/views/basic_model.js index d2da98ef8..9a69b6ccd 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/basic_model.js +++ b/web_widget_one2many_product_picker/static/src/js/views/basic_model.js @@ -6,6 +6,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require) var BasicModel = require("web.BasicModel"); BasicModel.include({ + /** * @param {Number/String} handle * @param {Object} context @@ -62,7 +63,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require) var list = this.localData[listID]; var context = _.extend({}, this._getContext(list), options.context); - var position = (options && options.position) || 'top'; + var position = options?options.position:'top'; var params = { context: context, fields: list.fields, @@ -74,17 +75,22 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require) doNotSetDirty: true, }; - return $.Deferred(function(d){ - self._makeDefaultRecord(list.model, params).then(function (recordID) { - self.setPureVirtual(recordID, true); - if (options.data) { - self._applyChangeNoWarnings(recordID, options.data, params).then(function(){ + return $.Deferred(function (d) { + self._makeDefaultRecord(list.model, params) + .then(function (recordID) { + self.setPureVirtual(recordID, true); + if (options.data) { + self._applyChangeNoWarnings( + recordID, + options.data, + params + ).then(function () { + d.resolve(self.get(recordID)); + }); + } else { d.resolve(self.get(recordID)); - }); - } else { - d.resolve(self.get(recordID)); - } - }); + } + }); }); }, @@ -92,9 +98,9 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require) * Cloned '_applyChange' but without warning messages * * @private - * @param {Object} record - * @param {Object} fields - * @param {String} viewType + * @param {Number} recordID + * @param {Object} changes + * @param {Object} options * @returns {Deferred} */ _applyChangeNoWarnings: function (recordID, changes, options) { @@ -112,11 +118,16 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require) initialData[elem.id] = $.extend(true, {}, _.pick(elem, 'data', '_changes')); }); - // apply changes to local data + // Apply changes to local data for (var fieldName in changes) { field = record.fields[fieldName]; if (field && (field.type === 'one2many' || field.type === 'many2many')) { - defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options.viewType, options.allowWarning)); + defs.push(this._applyX2ManyChange( + record, + fieldName, + changes[fieldName], + options.viewType, + options.allowWarning)); } else if (field && (field.type === 'many2one' || field.type === 'reference')) { defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName])); } else { @@ -129,12 +140,25 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require) } return $.when.apply($, defs).then(function () { - var onChangeFields = []; // the fields that have changed and that have an on_change + + // The fields that have changed and that have an on_change + var onChangeFields = []; for (var fieldName in changes) { field = record.fields[fieldName]; if (field && field.onChange) { - var isX2Many = field.type === 'one2many' || field.type === 'many2many'; - if (!isX2Many || (self._isX2ManyValid(record._changes[fieldName] || record.data[fieldName]))) { + var isX2Many = ( + field.type === 'one2many' || + field.type === 'many2many' + ); + if ( + !isX2Many || + ( + self._isX2ManyValid( + record._changes[fieldName] || + record.data[fieldName] + ) + ) + ) { onChangeFields.push(fieldName); } } @@ -144,12 +168,15 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require) self._performOnChangeNoWarnings(record, onChangeFields, options.viewType) .then(function (result) { delete record._warning; - onchangeDef.resolve(_.keys(changes).concat(Object.keys(result && result.value || {}))); + onchangeDef.resolve( + _.keys(changes).concat( + Object.keys((result && result.value) || {}))); }).fail(function () { self._visitChildren(record, function (elem) { _.extend(elem, initialData[elem.id]); }); - // safe fix for stable version, for opw-2267444 + + // Safe fix for stable version, for opw-2267444 if (!options.force_fail) { onchangeDef.resolve({}); } else { @@ -161,12 +188,16 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require) } return onchangeDef.then(function (fieldNames) { _.each(fieldNames, function (name) { - if (record._changes && record._changes[name] === record.data[name]) { + if ( + record._changes && + record._changes[name] === record.data[name] + ) { delete record._changes[name]; record._isDirty = !_.isEmpty(record._changes); } }); return self._fetchSpecialData(record).then(function (fieldNames2) { + // Return the names of the fields that changed (onchange or // associated special data change) return _.union(fieldNames, fieldNames2); @@ -196,32 +227,33 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require) }; if (fields.length === 1) { fields = fields[0]; - // if only one field changed, add its context to the RPC context + + // If only one field changed, add its context to the RPC context options.fieldName = fields; } var context = this._getContext(record, options); var currentData = this._generateOnChangeData(record, {changesOnly: false}); return self._rpc({ - model: record.model, - method: 'onchange', - args: [idList, currentData, fields, onchangeSpec, context], - }) - .then(function (result) { - if (!record._changes) { - // if the _changes key does not exist anymore, it means that - // it was removed by discarding the changes after the rpc - // to onchange. So, in that case, the proper response is to - // ignore the onchange. - return; - } - if (result.domain) { - record._domains = _.extend(record._domains, result.domain); - } - return self._applyOnChange(result.value, record).then(function () { - return result; - }); + model: record.model, + method: 'onchange', + args: [idList, currentData, fields, onchangeSpec, context], + }).then(function (result) { + if (!record._changes) { + + // If the _changes key does not exist anymore, it means that + // it was removed by discarding the changes after the rpc + // to onchange. So, in that case, the proper response is to + // ignore the onchange. + return; + } + if (result.domain) { + record._domains = _.extend(record._domains, result.domain); + } + return self._applyOnChange(result.value, record).then(function () { + return result; }); + }); }, }); diff --git a/web_widget_one2many_product_picker/static/src/js/views/basic_view.js b/web_widget_one2many_product_picker/static/src/js/views/basic_view.js index eb69bc417..f10d35071 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/basic_view.js +++ b/web_widget_one2many_product_picker/static/src/js/views/basic_view.js @@ -1,3 +1,4 @@ +/* global py */ // Copyright 2020 Tecnativa - Alexandre Díaz // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). odoo.define("web_widget_one2many_product_picker.BasicView", function (require) { @@ -9,21 +10,31 @@ odoo.define("web_widget_one2many_product_picker.BasicView", function (require) { var _t = core._t; - // py.js _ -> _t() call - var PY_t = new py.PY_def.fromJSON(function() { + // Add ref to _() -> _t() call + var PY_t = new py.PY_def.fromJSON(function () { var args = py.PY_parseArgs(arguments, ['str']); return py.str.fromJSON(_t(args.str.toJSON())); }); BasicView.include({ + /** * @override */ _processField: function (viewType, field, attrs) { - /* We need process 'options' attribute to handle translations and special replacements */ - if (attrs.widget === "one2many_product_picker" && !_.isObject(attrs.options)) { + + /** + * We need process 'options' attribute to handle translations and + * special replacements + */ + if ( + attrs.widget === "one2many_product_picker" && + !_.isObject(attrs.options) + ) { attrs.options = attrs.options ? pyUtils.py_eval(attrs.options, { _: PY_t, + + // Hack: This allow use $number_search out of an string number_search: '$number_search', }) : {}; } 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 aac76e59a..ecbb5085d 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 @@ -8,7 +8,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun var core = require("web.core"); var field_registry = require("web.field_registry"); var FieldOne2Many = require("web.relational_fields").FieldOne2Many; - var One2ManyProductPickerRenderer = require("web_widget_one2many_product_picker.One2ManyProductPickerRenderer"); + var One2ManyProductPickerRenderer = require( + "web_widget_one2many_product_picker.One2ManyProductPickerRenderer"); var tools = require("web_widget_one2many_product_picker.tools"); var _t = core._t; @@ -36,7 +37,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun _auto_search_delay: 450, - // product.product fields + // Model product.product fields search_read_fields: [ "id", "display_name", @@ -45,9 +46,12 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun /** * @override */ - init: function (parent, name, record, options) { + init: function (parent, name, record) { this._super.apply(this, arguments); - this.state = record; // This is the parent state + + // This is the parent state + this.state = record; + // Use jquery 'extend' to have a 'deep' merge. this.options = $.extend(true, this._getDefaultOptions(), this.attrs.options); if (!this.options.search) { @@ -58,6 +62,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun if (!(this.options.search[0] instanceof Array)) { this._searchCategoryNames = _.map(this.options.search, "name"); } + // FIXME: Choose a better way to get the active controller or model objects this.parent_controller = parent.getParent(); if (this.view) { @@ -72,6 +77,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun if (!this.view) { return $.when(); } + // Uses to work with searchs, so we can mix properties with the user values. this._searchContext = { domain: this.mode === "readonly" ? this._getLinesDomain() : false, @@ -96,9 +102,13 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun var prices = []; var field_map = this.options.field_map; var records = this.parent_controller.model.get(this.state.id).data[this.name].data; - if (this.options.show_discounts) { + if (this.options.show_discount) { prices = _.map(records, function (line) { - return line.data[field_map.product_uom_qty] * tools.priceReduce(line.data[field_map.price_unit], line.data[field_map.discount]); + return line.data[field_map.product_uom_qty] * + tools.priceReduce( + line.data[field_map.price_unit], + line.data[field_map.discount] + ); }); } else { prices = _.map(records, function (line) { @@ -198,26 +208,25 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * @override */ _renderButtons: function () { - if (!this.isReadonly) { - this.$buttons = $( - qweb.render("One2ManyProductPicker.ControlPanelButtons", { - search_category_names: this._searchCategoryNames, - search_mode: this._searchMode, - } - )); - this.$searchInput = this.$buttons.find(".oe_search_input"); - this.$groups = $( - qweb.render("One2ManyProductPicker.ControlPanelGroupButtons", { - groups: this.searchGroups, - }) - ); - this.$btnLines = this.$groups.find(".oe_btn_lines"); - this.$badgeLines = this.$btnLines.find(".badge"); - this.updateBadgeLines(); - this.$groups.appendTo(this.$buttons); - } else { + if (this.isReadonly) { return this._super.apply(this, arguments); } + this.$buttons = $( + qweb.render("One2ManyProductPicker.ControlPanelButtons", { + search_category_names: this._searchCategoryNames, + search_mode: this._searchMode, + } + )); + this.$searchInput = this.$buttons.find(".oe_search_input"); + this.$groups = $( + qweb.render("One2ManyProductPicker.ControlPanelGroupButtons", { + groups: this.searchGroups, + }) + ); + this.$btnLines = this.$groups.find(".oe_btn_lines"); + this.$badgeLines = this.$btnLines.find(".badge"); + this.updateBadgeLines(); + this.$groups.appendTo(this.$buttons); }, /** @@ -226,6 +235,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun _render: function () { var self = this; var def = this._super.apply(this, arguments); + // Parent implementation can return 'undefined' :( return ( def && @@ -246,7 +256,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun */ doRenderSearchRecords: function () { var self = this; - return $.Deferred(function(d){ + return $.Deferred(function (d) { self._getSearchRecords().then(function () { self.renderer.$el.scrollTop(0); self.renderer._renderView().then(d.resolve); @@ -303,7 +313,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun var context = _.extend({ 'active_search_group_name': this._activeSearchGroup.name, 'active_search_involved_fields': this._searchContext.involvedFields, - },this.state.data[this.name].getContext()); + }, this.state.data[this.name].getContext()); return $.Deferred(function (d) { var limit = soptions.limit || self.options.records_per_page; @@ -329,7 +339,10 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun self._lastSearchRecordsCount = results.length; self._searchOffset = offset + limit; if (self.renderer) { - self.renderer.updateSearchData(self._searchRecords, self._lastSearchRecordsCount); + self.renderer.updateSearchData( + self._searchRecords, + self._lastSearchRecordsCount + ); } d.resolve(results); }); @@ -341,7 +354,6 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * @param {MouseEvent} evt */ _onClickSearchGroup: function (evt) { - var self = this; var $btn = $(evt.target); var groupIndex = Number($btn.data("group")) || 0; this._activeSearchGroup = this.searchGroups[groupIndex]; @@ -383,7 +395,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun this._searchMode = $target.index(); $target.parent().children().removeClass('active'); $target.addClass('active'); - this.doRenderSearchRecords().then(function(){ + this.doRenderSearchRecords().then(function () { self.$searchInput.focus(); }); }, @@ -430,29 +442,36 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun search_domain = search_domain[this._searchMode].domain; } var involved_fields = []; + // Iterate domain triplets and logic operators for (var index in search_domain) { - var domain = _.clone(search_domain[index]); + var domain_cloned = _.clone(search_domain[index]); + // Is a triplet - if (domain instanceof Array) { + if (domain_cloned instanceof Array) { + // Replace right leaf with the current value of the search input - if (domain[2] === "$number_search") { - domain[2] = Number(this._searchContext.text); + if (domain_cloned[2] === "$number_search") { + domain_cloned[2] = Number(this._searchContext.text); involved_fields.push({ type: 'number', - field: domain[0], - oper: domain[1], + field: domain_cloned[0], + oper: domain_cloned[1], }); - } else if (typeof(domain[2]) === "string" && domain[2].includes("$search")) { - domain[2] = domain[2].replace(/\$search/, this._searchContext.text); + } else if ( + typeof domain_cloned[2] === "string" && + domain_cloned[2].includes("$search") + ) { + domain_cloned[2] = domain_cloned[2] + .replace(/\$search/, this._searchContext.text); involved_fields.push({ type: 'text', - field: domain[0], - oper: domain[1], + field: domain_cloned[0], + oper: domain_cloned[1], }); } } - sdomain.push(domain); + sdomain.push(domain_cloned); } this._searchContext.involvedFields = involved_fields; } @@ -471,7 +490,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun return []; } var field_name = this.options.field_map.product; - var lines = this.parent_controller.model.get(this.state.id).data[this.name].data; + var lines = this.parent_controller.model.get(this.state.id) + .data[this.name].data; var ids = _.map(lines, function (line) { return line.data[field_name].data.id; }); @@ -483,13 +503,12 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * that the search results. Use directy in-memory values. */ showLines: function () { - var self = this; this._clearSearchInput(); this.$btnLines.parent().find(".active").removeClass("active"); this.$btnLines.addClass("active"); this._activeSearchGroup = { 'name': 'main_lines', - } + }; this._searchContext.domain = this._getLinesDomain(); this._searchContext.order = false; this.doRenderSearchRecords(); @@ -507,7 +526,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun if (evt.keyCode === $.ui.keyCode.ENTER) { var self = this; this._searchContext.text = evt.target.value; - this.doRenderSearchRecords().then(function(){ + this.doRenderSearchRecords().then(function () { self.$searchInput.focus(); }); } @@ -526,9 +545,13 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * @param {DropdownEvent} evt */ _onShowSearchDropdown: function (evt) { + // Workaround: This "ensures" a correct dropdown position var offset = $(evt.currentTarget).find(".dropdown-toggle").parent().height(); - _.defer(function() { $(evt.currentTarget).find(".dropdown-menu").css("transform", "translate3d(0px, " + offset + "px, 0px)"); }); + _.defer(function () { + $(evt.currentTarget).find(".dropdown-menu") + .css("transform", "translate3d(0px, " + offset + "px, 0px)"); + }); }, /** 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 e3cfe8d19..4f9e8ff18 100644 --- a/web_widget_one2many_product_picker/static/tests/widget_tests.js +++ b/web_widget_one2many_product_picker/static/tests/widget_tests.js @@ -1,3 +1,4 @@ +/* global QUnit */ // Copyright 2020 Tecnativa - Alexandre Díaz // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). odoo.define('web_widget_one2many_product_picker.widget_tests', function (require) { @@ -28,7 +29,7 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require console.log(getArch()); QUnit.module('Web Widget One2Many Product Picker', { - beforeEach: function() { + beforeEach: function () { this.data = { foo: { fields: { @@ -37,8 +38,8 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require display_name: {string: "Display Name", type: "char"}, }, records: [ - {id: 1, line_ids: [1,2], currency_id: 1, display_name: "FT01"}, - ] + {id: 1, line_ids: [1, 2], currency_id: 1, display_name: "FT01"}, + ], }, line: { fields: { @@ -53,7 +54,7 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require records: [ {id: 1, name: "Large Cabinet", product_id: 1, product_uom: 1, product_uom_qty: 3, price_unit: 9.99, price_reduce: 9.00, foo_id: 1}, {id: 2, name: "Cabinet with Doors", product_id: 2, product_uom: 1, product_uom_qty: 8, price_unit: 42.99, price_reduce: 40.00, foo_id: 1}, - ] + ], }, product: { fields: { @@ -94,9 +95,9 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require ], }, }; - } + }, }, function () { - QUnit.test('Load widget', function(assert) { + QUnit.test('Load widget', function (assert) { assert.expect(4); var form = createView({