From 6c2440070946580a38ddddf8f07ac01b843cba27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20D=2E=20D=C3=ADaz?= Date: Wed, 9 Dec 2020 15:08:20 +0100 Subject: [PATCH 1/6] [IMP] web_widget_one2many_product_picker: Modify rpc request context --- web_widget_one2many_product_picker/README.rst | 145 +++++++++++- .../readme/CONFIG.rst | 47 ---- .../readme/CONFIGURATION.rst | 1 - .../readme/CONFIGURE.rst | 131 +++++++++++ .../readme/ROADMAP.rst | 2 +- .../readme/USAGE.rst | 8 +- .../static/description/index.html | 221 ++++++++++++++++-- .../quick_create_form_view.js | 1 - .../js/views/One2ManyProductPicker/record.js | 2 - .../widgets/field_one2many_product_picker.js | 16 +- 10 files changed, 495 insertions(+), 79 deletions(-) delete mode 100644 web_widget_one2many_product_picker/readme/CONFIG.rst delete mode 100644 web_widget_one2many_product_picker/readme/CONFIGURATION.rst create mode 100644 web_widget_one2many_product_picker/readme/CONFIGURE.rst diff --git a/web_widget_one2many_product_picker/README.rst b/web_widget_one2many_product_picker/README.rst index f44960cec..43be865e3 100644 --- a/web_widget_one2many_product_picker/README.rst +++ b/web_widget_one2many_product_picker/README.rst @@ -37,6 +37,141 @@ Installation It's advisable to install 'web_widget_numeric_step' to have a better usability on touch screens. +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 + + +
+ + + + + + + + + + + + + +** 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 + + +
+ + + + + + + + + + + Usage ===== @@ -87,7 +222,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']]}]}" >
@@ -114,6 +249,12 @@ The widget sends a defaults context with the 'search_read' request: * 'all' > Is the hard-coded name for the 'All' group * 'main_lines' > Is the hard-coded name for the 'Lines' group + * active_search_involved_fields > Contains an array of dictionaries with the fields used with the searchbox content + + * 'type' > Can be 'text' or 'number' + * 'field' > The field name + * 'oper' > The operator used + Preview: ~~~~~~~~ @@ -124,7 +265,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..df29c4af0 100644 --- a/web_widget_one2many_product_picker/readme/USAGE.rst +++ b/web_widget_one2many_product_picker/readme/USAGE.rst @@ -45,7 +45,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']]}]}" >
@@ -72,6 +72,12 @@ The widget sends a defaults context with the 'search_read' request: * 'all' > Is the hard-coded name for the 'All' group * 'main_lines' > Is the hard-coded name for the 'Lines' group + * active_search_involved_fields > Contains an array of dictionaries with the fields used with the searchbox content + + * 'type' > Can be 'text' or 'number' + * 'field' > The field name + * 'oper' > The operator used + Preview: ~~~~~~~~ diff --git a/web_widget_one2many_product_picker/static/description/index.html b/web_widget_one2many_product_picker/static/description/index.html index 19e778bbe..69e68cf65 100644 --- a/web_widget_one2many_product_picker/static/description/index.html +++ b/web_widget_one2many_product_picker/static/description/index.html @@ -372,28 +372,194 @@ ul.auto-toc {

Table of contents

-

Installation

+

Installation

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

+
+

Configuration

+

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

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

Usage

+

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:

@@ -436,7 +602,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" /> @@ -451,8 +617,8 @@ This is an example that uses the ‘sale.order.line’ fields:

</field>

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

-
-

Default context:

+
+

Default context:

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

    @@ -464,25 +630,34 @@ This is an example that uses the ‘sale.order.line’ fields:

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

    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 +665,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 +687,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/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..7f6eba729 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 @@ -259,7 +259,6 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView this._updateButtons(); } else { this.update({}, {reload: false}).then(function(){ - debugger; self.model.unsetDirty(self.handle); self.trigger_up("restore_flip_card"); self._updateButtons(); 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..c07d59531 100644 --- a/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js +++ b/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js @@ -13,8 +13,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu var tools = require("web_widget_one2many_product_picker.tools"); var ProductPickerQuickModifPriceForm = require( "web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm"); - var ProductPickerQuickCreateForm = require( - "web_widget_one2many_product_picker.ProductPickerQuickCreateForm"); var qweb = core.qweb; 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..aac76e59a 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 @@ -302,6 +302,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun var soptions = options || {}; var context = _.extend({ 'active_search_group_name': this._activeSearchGroup.name, + 'active_search_involved_fields': this._searchContext.involvedFields, },this.state.data[this.name].getContext()); return $.Deferred(function (d) { @@ -420,6 +421,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun */ _getFullSearchDomain: function (domain) { var sdomain = _.clone(domain); + this._searchContext.involvedFields = []; if (!sdomain) { sdomain = _.clone(this._searchContext.domain) || []; if (this._searchContext.text) { @@ -427,6 +429,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun 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 = _.clone(search_domain[index]); @@ -435,12 +438,23 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun // Replace right leaf with the current value of the search input if (domain[2] === "$number_search") { domain[2] = Number(this._searchContext.text); + involved_fields.push({ + type: 'number', + field: domain[0], + oper: domain[1], + }); } else if (typeof(domain[2]) === "string" && domain[2].includes("$search")) { - domain[2] = domain[2].replace(/\$search/, this._searchContext.text) + domain[2] = domain[2].replace(/\$search/, this._searchContext.text); + involved_fields.push({ + type: 'text', + field: domain[0], + oper: domain[1], + }); } } sdomain.push(domain); } + this._searchContext.involvedFields = involved_fields; } } return sdomain || []; From 890595e4462e622f56a4866d29966620e9151a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20D=2E=20D=C3=ADaz?= Date: Wed, 9 Dec 2020 15:57:54 +0100 Subject: [PATCH 2/6] [FIX] web_widget_one2many_product_picker: Subtotal with discounts --- .../static/src/js/widgets/field_one2many_product_picker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js b/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js index aac76e59a..f3ff2d34c 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 @@ -96,7 +96,7 @@ 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]); }); From 5abae48794d259335a10523d9db2fdd992a89695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20D=2E=20D=C3=ADaz?= Date: Thu, 10 Dec 2020 00:53:43 +0100 Subject: [PATCH 3/6] [IMP] web_widget_one2many_product_picker: linter changes --- web_widget_one2many_product_picker/README.rst | 81 --- .../readme/USAGE.rst | 81 --- .../static/description/index.html | 135 +---- .../static/src/js/tools.js | 13 +- .../quick_create_form.js | 16 +- .../quick_create_form_view.js | 492 +++++++++--------- .../quick_modif_price_form.js | 16 +- .../quick_modif_price_form_view.js | 315 +++++------ .../js/views/One2ManyProductPicker/record.js | 128 +++-- .../views/One2ManyProductPicker/renderer.js | 100 ++-- .../static/src/js/views/basic_model.js | 114 ++-- .../static/src/js/views/basic_view.js | 19 +- .../widgets/field_one2many_product_picker.js | 111 ++-- .../static/tests/widget_tests.js | 13 +- 14 files changed, 793 insertions(+), 841 deletions(-) diff --git a/web_widget_one2many_product_picker/README.rst b/web_widget_one2many_product_picker/README.rst index 43be865e3..9db4b125e 100644 --- a/web_widget_one2many_product_picker/README.rst +++ b/web_widget_one2many_product_picker/README.rst @@ -175,87 +175,6 @@ Other example for 'purchase.order.line' fields: Usage ===== -You need to define the view fields. The view must be of ``form`` type. -This is an example that uses the 'sale.order.line' fields: - -.. code:: xml - - - - - - - - - - - - - - - - - -Other example for 'purchase.order.line' fields: - -.. code:: xml - - -

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

    Table of contents

    -

    Installation

    +

    Installation

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

    -

    Configuration

    +

    Configuration

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

    -

    Widget options:

    +

    Widget options:

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

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

    Default context:

    +

    Default context:

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

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

    Examples:

    +

    Examples:

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

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

    Usage

    -

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

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

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

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

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

    -
    -

    Default context:

    -

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

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

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

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

    Usage

    -

    Known issues / Roadmap

    +

    Known issues / Roadmap

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

    Bug Tracker

    +

    Bug Tracker

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

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Tecnativa
    -

    Contributors

    +

    Contributors

    • Tecnativa:

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

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association

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