diff --git a/web_widget_one2many_product_picker/README.rst b/web_widget_one2many_product_picker/README.rst index f44960cec..9db4b125e 100644 --- a/web_widget_one2many_product_picker/README.rst +++ b/web_widget_one2many_product_picker/README.rst @@ -37,10 +37,80 @@ Installation It's advisable to install 'web_widget_numeric_step' to have a better usability on touch screens. -Usage -===== +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: +~~~~~~~~~~~~~~~ + +* product_per_page > Integer -> Used to control the load more behaviour (16 by default) +* groups > Array of dictionaries -> Declare the groups + + * name -> The group name + * string -> The text displayed + * domain -> Forced domain to use + * order -> The order + + * name -> The field name to order + * asc -> Flag to use 'asc' order + +* currency_field > Model field used to format monetary values ('currency_id' by default) +* field_map > Dictionary: + + * product -> The field that represent the product (`product_id` by default) + * name -> The field that represent a name ('name' by default) + * product_uom -> The field that represent a product_uom ('product_uom' by default) + * product_uom_qty -> The field that represent a product_uom_qty ('product_uom_qty' by default) + * price_unit -> The field that represent a price_unit ('price_unit' by default) + * discount -> The field that represent a discount ('discount' by default) + +* search > Array of dictionaries or Array of 'triplets' ([[field_map.name, 'ilike', '$search']] by default) + + * name -> The name to display + * domain -> The domain to use + + * $search -> Replaces it with the current value of the searchbox + * $number_search -> Replaces all the leaf with the current value of the searchbox as a number + +* edit_discount > Enable/Disable discount edits (False by default) +* edit_price > Enable/Disable price edits (True by default) +* show_discount > Enable/Disable display discount (False by default) +* show_subtotal > Enable/Disable show subtotal (True by default) + +All widget options are optional. +Notice that you can call '_' method to use translations. This only can be used with this widget. + +Example: + +.. code:: + + options="{'search': [{'name': _('Starts With'), 'domain': [('name', '=like', '$search%')]}], 'groups': [{'name': 'cheap', 'string': _('Cheap'), 'domain': [('list_price', '<', 10.0)], 'field_map': { 'product': 'my_product_id' }}]}" + + +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 + + +Examples: +~~~~~~~~~ + This is an example that uses the 'sale.order.line' fields: .. code:: xml @@ -76,6 +146,7 @@ This is an example that uses the 'sale.order.line' fields: +** In this example we don't use 'field_map' option because the default match with the sale.order.line field names. Other example for 'purchase.order.line' fields: @@ -87,7 +158,7 @@ Other example for 'purchase.order.line' fields: 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': {'name': 'name', 'product': 'product_id', 'product_uom': 'product_uom', 'price': 'price_unit', 'parent_id': 'order_id', 'product_uom_qty': 'product_qty'}, 'groups': [{'name': _('Desk'), 'domain': [['name', 'ilike', 'desk']], 'order': {'name': 'id', 'asc': true}}, {'name': _('Chairs'), 'domain': [['name', 'ilike', 'chair']]}]}" + 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']]}]}" >
@@ -101,19 +172,8 @@ Other example for 'purchase.order.line' fields:
-** 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 - +Usage +===== Preview: ~~~~~~~~ @@ -124,7 +184,7 @@ 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 states to apply correct effects. +* The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects. Bug Tracker =========== diff --git a/web_widget_one2many_product_picker/readme/CONFIG.rst b/web_widget_one2many_product_picker/readme/CONFIG.rst deleted file mode 100644 index cb36faf47..000000000 --- a/web_widget_one2many_product_picker/readme/CONFIG.rst +++ /dev/null @@ -1,47 +0,0 @@ -Create or edit a new view and use the new widget called 'one2many_product_picker'. - -Widget options: -~~~~~~~~~~~~~~~ - -* product_per_page > Integer -> Used to control the load more behaviour (16 by default) -* groups > Array of dictionaries -> Declare the groups - - * name -> The group name - * string -> The text displayed - * domain -> Forced domain to use - * order -> The order - - * name -> The field name to order - * asc -> Flag to use 'asc' order - -* currency_field > Model field used to format monetary values ('currency_id' by default) -* field_map > Dictionary: - - * product -> The field that represent the product (`product_id` by default) - * name -> The field that represent a name ('name' by default) - * product_uom -> The field that represent a product_uom ('product_uom' by default) - * product_uom_qty -> The field that represent a product_uom_qty ('product_uom_qty' by default) - * price_unit -> The field that represent a price_unit ('price_unit' by default) - * discount -> The field that represent a discount ('discount' by default) - -* search > Array of dictionaries or Array of 'triplets' ([[field_map.name, 'ilike', '$search']] by default) - - * name -> The name to display - * domain -> The domain to use - - * $search -> Replaces it with the current value of the searchbox - * $number_search -> Replaces all the leaf with the current value of the searchbox as a number - -* edit_discount > Enable/Disable discount edits (False by default) -* edit_price > Enable/Disable price edits (True by default) -* show_discount > Enable/Disable display discount (False by default) -* show_subtotal > Enable/Disable show subtotal (True by default) - -All widget options are optional. -Notice that you can call '_' method to use translations. This only can be used with this widget. - -Example: - -.. code:: - - options="{'search': [{'name': _('Starts With'), 'domain': [('name', '=like', '$search%')]}], 'groups': [{'name': 'cheap', 'string': _('Cheap'), 'domain': [('list_price', '<', 10.0)], 'field_map': { 'product': 'my_product_id' }}]}" diff --git a/web_widget_one2many_product_picker/readme/CONFIGURATION.rst b/web_widget_one2many_product_picker/readme/CONFIGURATION.rst deleted file mode 100644 index e6592be0a..000000000 --- a/web_widget_one2many_product_picker/readme/CONFIGURATION.rst +++ /dev/null @@ -1 +0,0 @@ -It's recommendable install 'web_widget_numeric_step' to have a better usability on touch screens. diff --git a/web_widget_one2many_product_picker/readme/CONFIGURE.rst b/web_widget_one2many_product_picker/readme/CONFIGURE.rst new file mode 100644 index 000000000..2d1112c87 --- /dev/null +++ b/web_widget_one2many_product_picker/readme/CONFIGURE.rst @@ -0,0 +1,131 @@ +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: +~~~~~~~~~~~~~~~ + +* product_per_page > Integer -> Used to control the load more behaviour (16 by default) +* groups > Array of dictionaries -> Declare the groups + + * name -> The group name + * string -> The text displayed + * domain -> Forced domain to use + * order -> The order + + * name -> The field name to order + * asc -> Flag to use 'asc' order + +* currency_field > Model field used to format monetary values ('currency_id' by default) +* field_map > Dictionary: + + * product -> The field that represent the product (`product_id` by default) + * name -> The field that represent a name ('name' by default) + * product_uom -> The field that represent a product_uom ('product_uom' by default) + * product_uom_qty -> The field that represent a product_uom_qty ('product_uom_qty' by default) + * price_unit -> The field that represent a price_unit ('price_unit' by default) + * discount -> The field that represent a discount ('discount' by default) + +* search > Array of dictionaries or Array of 'triplets' ([[field_map.name, 'ilike', '$search']] by default) + + * name -> The name to display + * domain -> The domain to use + + * $search -> Replaces it with the current value of the searchbox + * $number_search -> Replaces all the leaf with the current value of the searchbox as a number + +* edit_discount > Enable/Disable discount edits (False by default) +* edit_price > Enable/Disable price edits (True by default) +* show_discount > Enable/Disable display discount (False by default) +* show_subtotal > Enable/Disable show subtotal (True by default) + +All widget options are optional. +Notice that you can call '_' method to use translations. This only can be used with this widget. + +Example: + +.. code:: + + options="{'search': [{'name': _('Starts With'), 'domain': [('name', '=like', '$search%')]}], 'groups': [{'name': 'cheap', 'string': _('Cheap'), 'domain': [('list_price', '<', 10.0)], 'field_map': { 'product': 'my_product_id' }}]}" + + +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 + + +Examples: +~~~~~~~~~ + +This is an example that uses the 'sale.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. + +Other example for 'purchase.order.line' fields: + +.. code:: xml + + +
+ + + + + + + + + + diff --git a/web_widget_one2many_product_picker/readme/ROADMAP.rst b/web_widget_one2many_product_picker/readme/ROADMAP.rst index 43b92528c..0e3fe6e62 100644 --- a/web_widget_one2many_product_picker/readme/ROADMAP.rst +++ b/web_widget_one2many_product_picker/readme/ROADMAP.rst @@ -1,2 +1,2 @@ * 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 states to apply correct effects. +* The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects. diff --git a/web_widget_one2many_product_picker/readme/USAGE.rst b/web_widget_one2many_product_picker/readme/USAGE.rst index 4f227a595..a3b95d69b 100644 --- a/web_widget_one2many_product_picker/readme/USAGE.rst +++ b/web_widget_one2many_product_picker/readme/USAGE.rst @@ -1,78 +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 - - Preview: ~~~~~~~~ diff --git a/web_widget_one2many_product_picker/static/description/index.html b/web_widget_one2many_product_picker/static/description/index.html index 19e778bbe..67e169734 100644 --- a/web_widget_one2many_product_picker/static/description/index.html +++ b/web_widget_one2many_product_picker/static/description/index.html @@ -373,17 +373,22 @@ ul.auto-toc {
-
-

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:

+
+

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:

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

    +
  • +
  • groups > Array of dictionaries -> Declare the groups

    +
    +
      +
    • name -> The group name

      +
    • +
    • string -> The text displayed

      +
    • +
    • domain -> Forced domain to use

      +
    • +
    • order -> The order

      +
      +
        +
      • name -> The field name to order
      • +
      • asc -> Flag to use ‘asc’ order
      • +
      +
      +
    • +
    +
    +
  • +
  • currency_field > Model field used to format monetary values (‘currency_id’ by default)

    +
  • +
  • field_map > Dictionary:

    +
    +
      +
    • product -> The field that represent the product (product_id by default)
    • +
    • name -> The field that represent a name (‘name’ by default)
    • +
    • product_uom -> The field that represent a product_uom (‘product_uom’ by default)
    • +
    • product_uom_qty -> The field that represent a product_uom_qty (‘product_uom_qty’ by default)
    • +
    • price_unit -> The field that represent a price_unit (‘price_unit’ by default)
    • +
    • discount -> The field that represent a discount (‘discount’ by default)
    • +
    +
    +
  • +
  • search > Array of dictionaries or Array of ‘triplets’ ([[field_map.name, ‘ilike’, ‘$search’]] by default)

    +
    +
      +
    • name -> The name to display

      +
    • +
    • domain -> The domain to use

      +
      +
        +
      • $search -> Replaces it with the current value of the searchbox
      • +
      • $number_search -> Replaces all the leaf with the current value of the searchbox as a number
      • +
      +
      +
    • +
    +
    +
  • +
  • edit_discount > Enable/Disable discount edits (False by default)

    +
  • +
  • edit_price > Enable/Disable price edits (True by default)

    +
  • +
  • show_discount > Enable/Disable display discount (False by default)

    +
  • +
  • show_subtotal > Enable/Disable show subtotal (True by default)

    +
  • +
+

All widget options are optional. +Notice that you can call ‘_’ method to use translations. This only can be used with this widget.

+

Example:

+
+options="{'search': [{'name': _('Starts With'), 'domain': [('name', '=like', '$search%')]}], 'groups': [{'name': 'cheap', 'string': _('Cheap'), 'domain': [('list_price', '<', 10.0)], 'field_map': { 'product': 'my_product_id' }}]}"
+
+
+
+

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

Examples:

+

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

 <field
     name="order_line"
@@ -428,6 +532,7 @@ This is an example that uses the ‘sale.order.line’ fields:

</form> </field>
+

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

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

 <field
@@ -436,7 +541,7 @@ This is an example that uses the ‘sale.order.line’ fields:

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': {'name': 'name', 'product': 'product_id', 'product_uom': 'product_uom', 'price': 'price_unit', 'parent_id': 'order_id', 'product_uom_qty': 'product_qty'}, 'groups': [{'name': _('Desk'), 'domain': [['name', 'ilike', 'desk']], 'order': {'name': 'id', 'asc': true}}, {'name': _('Chairs'), 'domain': [['name', 'ilike', 'chair']]}]}" + 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" /> @@ -450,39 +555,26 @@ This is an example that uses the ‘sale.order.line’ fields:

</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
    • -
    -
    -
  • -
-
+
+
-

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 states to apply correct effects.
  • +
  • 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 @@ -490,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:

    @@ -512,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 ccb9dc9cb..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,257 +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(){ - debugger; + // 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 e36351a25..34d581562 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,14 +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 ProductPickerQuickCreateForm = require( - "web_widget_one2many_product_picker.ProductPickerQuickCreateForm"); var qweb = core.qweb; + var _t = core._t; /* This represent a record (a card) */ var One2ManyProductPickerRecord = Widget.extend({ @@ -76,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"); @@ -102,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( @@ -130,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( @@ -147,6 +148,15 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu * @param {Object} recordSearch */ _setState: function (viewState, recordSearch) { + + // No parent = product_pricker widget destroyed + // So this is a 'zombie' record. Destroy it! + if (!this.getParent()) { + this.on_detach_callback(); + this.destroy(); + return; + } + this.fields = this.getParent().state.fields; this.fieldsInfo = this.getParent().state.fieldsInfo.form; this.state = viewState; @@ -162,6 +172,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. @@ -188,7 +199,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; }, @@ -201,7 +213,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; }, @@ -213,12 +228,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, + }); }, /** @@ -227,7 +244,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"); @@ -243,6 +263,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; @@ -259,13 +280,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( $("", { @@ -276,11 +305,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); } } }); @@ -296,6 +320,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 @@ -303,7 +328,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-")) { @@ -312,8 +336,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); @@ -331,8 +361,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, @@ -348,8 +378,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); }); @@ -386,7 +417,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]"]; } @@ -403,11 +436,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")); } } } @@ -422,7 +457,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, @@ -453,7 +490,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); }, @@ -465,12 +503,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) { @@ -483,9 +524,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; @@ -501,12 +541,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); }); @@ -519,7 +559,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); @@ -527,7 +570,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: "", @@ -542,7 +585,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({ @@ -553,7 +597,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'); }); } @@ -564,9 +608,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"); }, @@ -586,8 +629,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..bc42ad57e 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,13 @@ 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); }, /** @@ -67,12 +69,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 +100,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 +116,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 +130,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 +169,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 +203,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 +211,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 +253,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 +273,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 +282,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 +348,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 +358,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 +367,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 +411,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 () { @@ -391,35 +422,14 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", }); }, - /** - * Auto-load more records (scroll pagination). - * - * @private - * @param {ScrollEvent} evt - */ - _onScrollView: function (evt) { - var cur_pos = evt.target.scrollTop; - var max_pos = evt.target.scrollHeight - evt.target.clientHeight; - var perc_pos = cur_pos / max_pos; - if (perc_pos > this.MIN_PERC_GET_RECORDS) { - if (!this._loadMoreWorking) { - this.trigger_up("load_more"); - this._loadMoreWorking = true; - this.$btnLoadMore.attr("disabled", true); - } - } else { - this._loadMoreWorking = false; - } - }, - /** * @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 ce4db1600..bcfb772a6 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; @@ -18,6 +19,10 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun var FieldOne2ManyProductPicker = FieldOne2Many.extend({ className: "oe_field_one2many_product_picker", + // Workaround: We need know all records, + // the widget pagination works with product.product. + limit: 9999999, + events: _.extend({}, FieldOne2Many.prototype.events, { "click .dropdown-item": "_onClickSearchMode", "click .oe_search_erase": "_onClickSearchEraser", @@ -36,7 +41,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 +50,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 +66,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,12 +81,20 @@ 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, text: false, order: false, + activeTest: true, }; + if (this.mode === "readonly") { + this._activeSearchGroup = { + 'name': 'main_lines', + }; + this._searchContext.activeTest = false; + } return $.when(this._super.apply(this, arguments), this._getSearchRecords()); }, @@ -96,9 +113,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) { @@ -114,7 +135,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun }) || 0; total = tools.monetary( total, - this.state.data[this.name].fields[this.options.field_map.price_unit], + this.value.fields[this.options.field_map.price_unit], this.options.currency_field, this.state.data ); @@ -198,26 +219,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 +246,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 +267,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); @@ -284,12 +305,11 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * If merge is true the current records aren't removed. * * @private - * @param {Array} domain * @param {Dictionary} options * @param {Boolean} merge * @returns {Deferred} */ - _getSearchRecords: function (domain, options, merge) { + _getSearchRecords: function (options, merge) { var self = this; var arch = this.view.arch; var field_name = this.options.field_map.product; @@ -298,11 +318,13 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun // Launch the rpc request and ensures that we wait for the reply // to continue - var sdomain = this._getFullSearchDomain(domain); + var domain = this._getFullSearchDomain(); var soptions = options || {}; var context = _.extend({ 'active_search_group_name': this._activeSearchGroup.name, - },this.state.data[this.name].getContext()); + 'active_search_involved_fields': this._searchContext.involvedFields, + 'active_test': this._searchContext.activeTest, + }, this.value.getContext()); return $.Deferred(function (d) { var limit = soptions.limit || self.options.records_per_page; @@ -311,7 +333,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun model: model, method: "search_read", fields: self.search_read_fields, - domain: sdomain, + domain: domain, limit: limit, offset: offset, orderBy: self._searchContext.order, @@ -328,7 +350,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); }); @@ -340,12 +365,12 @@ 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]; this._searchContext.domain = this._activeSearchGroup.domain; this._searchContext.order = this._activeSearchGroup.order; + this._searchContext.activeTest = true; this.doRenderSearchRecords(); this.$btnLines.removeClass("active"); $btn.parent().find(".active").removeClass("active"); @@ -382,7 +407,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(); }); }, @@ -415,35 +440,52 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun * This domain is used to get the records to display. * * @private - * @param {Array} domain * @returns {Array} */ - _getFullSearchDomain: function (domain) { - var sdomain = _.clone(domain); - if (!sdomain) { - sdomain = _.clone(this._searchContext.domain) || []; - if (this._searchContext.text) { - var search_domain = this.options.search; - if (!(search_domain[0] instanceof Array)) { - search_domain = search_domain[this._searchMode].domain; - } - // Iterate domain triplets and logic operators - for (var index in search_domain) { - var domain = _.clone(search_domain[index]); - // Is a triplet - if (domain instanceof Array) { - // Replace right leaf with the current value of the search input - if (domain[2] === "$number_search") { - domain[2] = Number(this._searchContext.text); - } else if (typeof(domain[2]) === "string" && domain[2].includes("$search")) { - domain[2] = domain[2].replace(/\$search/, this._searchContext.text) - } - } - sdomain.push(domain); - } + _getFullSearchDomain: function () { + this._searchContext.involvedFields = []; + var domain = _.clone(this._searchContext.domain) || []; + if (this._searchContext.text) { + var search_domain = this.options.search; + if (!(search_domain[0] instanceof Array)) { + search_domain = search_domain[this._searchMode].domain; } + var involved_fields = []; + + // Iterate domain triplets and logic operators + for (var index in search_domain) { + var domain_cloned = _.clone(search_domain[index]); + + // Is a triplet + if (domain_cloned instanceof Array) { + + // Replace right leaf with the current value of the search input + if (domain_cloned[2] === "$number_search") { + domain_cloned[2] = Number(this._searchContext.text); + involved_fields.push({ + type: 'number', + field: domain_cloned[0], + oper: domain_cloned[1], + }); + } 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_cloned[0], + oper: domain_cloned[1], + }); + } + } + domain.push(domain_cloned); + } + this._searchContext.involvedFields = involved_fields; } - return sdomain || []; + + return domain || []; }, /** @@ -457,7 +499,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; }); @@ -469,15 +512,15 @@ 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._searchContext.activeTest = false; this.doRenderSearchRecords(); }, @@ -493,7 +536,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(); }); } @@ -512,9 +555,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)"); + }); }, /** @@ -557,7 +604,6 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun } var self = this; this._getSearchRecords( - false, { offset: this._searchOffset, }, 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({