3
0
Fork 0

[IMP] web_widget_one2many_product_picker: Backport from 13.0

12.0
Alexandre D. Díaz 2021-11-08 13:08:32 +01:00
parent 2f392f564d
commit 655ea25784
32 changed files with 4343 additions and 2157 deletions

View File

@ -47,7 +47,6 @@ You need to define the view fields. The view must be of ``form`` type.
Widget options: Widget options:
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
* records_per_page > Integer -> Used to control the load more behaviour (16 by default)
* groups > Array of dictionaries -> Declare the groups * groups > Array of dictionaries -> Declare the groups
* name -> The group name * name -> The group name
@ -58,6 +57,9 @@ Widget options:
* name -> The field name to order * name -> The field name to order
* asc -> Flag to use 'asc' order * asc -> Flag to use 'asc' order
* records_per_page > Integer -> Used to control the load more behaviour (16 by default)
* active -> Boolean -> Select the default group to use ('false' by default = 'All' group)
* currency_field > Model field used to format monetary values ('currency_id' by default) * currency_field > Model field used to format monetary values ('currency_id' by default)
* field_map > Dictionary: * field_map > Dictionary:
@ -68,7 +70,7 @@ Widget options:
* price_unit -> The field that represent a price_unit ('price_unit' 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) * 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) * search > Array of dictionaries (defines to use name_search by default)
* name -> The name to display * name -> The name to display
* domain -> The domain to use * domain -> The domain to use
@ -76,11 +78,15 @@ Widget options:
* $search -> Replaces it with the current value of the searchbox * $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 * $number_search -> Replaces all the leaf with the current value of the searchbox as a number
* name_search_value -> Enables the use of 'name_search' instead of 'search_read' and defines the value to search ('$search' by default)
* operator -> Operator used in 'name_search' ('ilike' by default)
* edit_discount > Enable/Disable discount edits (False by default) * edit_discount > Enable/Disable discount edits (False by default)
* edit_price > Enable/Disable price edits (True by default) * edit_price > Enable/Disable price edits (True by default)
* show_discount > Enable/Disable display discount (False by default) * show_discount > Enable/Disable display discount (False by default)
* show_subtotal > Enable/Disable show subtotal (True by default) * show_subtotal > Enable/Disable show subtotal (True by default)
* auto_save > Enable/Disable auto save (False by default) * auto_save > Enable/Disable auto save (False by default)
* auto_save_delay > The time (in milliseconds) to wait after the last interaction before launching the autosave (1500 by default)
* all_domain > The domain used in 'All' section ([] by default) * all_domain > The domain used in 'All' section ([] by default)
If using auto save feature, you should keep in mind that the "Save" and "Discard" buttons If using auto save feature, you should keep in mind that the "Save" and "Discard" buttons
@ -88,7 +94,9 @@ Widget options:
modify/create a record with the widget. modify/create a record with the widget.
* ignore_warning > Enable/Disable display onchange warnings (False by default) * ignore_warning > Enable/Disable display onchange warnings (False by default)
* instant_search > Enable/Disable instant search mode (False by default)
* trigger_refresh_fields > Fields in the main record that dispatch a widget refresh (["partner_id", "currency_id"] by default) * trigger_refresh_fields > Fields in the main record that dispatch a widget refresh (["partner_id", "currency_id"] by default)
* auto_focus > Keep the focus on the search box after performing a search (True by default)
All widget options are optional. All widget options are optional.
Notice that you can call '_' method to use translations. This only can be used with this widget. Notice that you can call '_' method to use translations. This only can be used with this widget.
@ -209,7 +217,8 @@ Known issues / Roadmap
====================== ======================
* Translations in the xml 'options' attribute of the field that use the widget can't be exported automatically to be translated * 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. * The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects
* sale.order onchanges that affects to "order_line" subfields will be ommitted to increase the performance
Bug Tracker Bug Tracker
=========== ===========

View File

@ -2,23 +2,16 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
'name': 'Web Widget One2Many Product Picker', "name": "Web Widget One2Many Product Picker",
'summary': 'Widget to select products on one2many fields', "summary": "Widget to select products on one2many fields",
'version': '12.0.2.4.2', "version": "12.0.2.4.2",
'category': 'Website', "category": "Website",
'author': "Tecnativa, " "author": "Tecnativa, " "Odoo Community Association (OCA)",
"Odoo Community Association (OCA)", "website": "https://github.com/OCA/web",
'website': 'https://www.tecnativa.com', "license": "AGPL-3",
'license': 'AGPL-3', "depends": ["product"],
'depends': [ "data": ["templates/assets.xml"],
'product', "qweb": ["static/src/xml/one2many_product_picker.xml"],
], "installable": True,
'data': [ "auto_install": False,
'templates/assets.xml',
],
'qweb': [
'static/src/xml/one2many_product_picker.xml',
],
'installable': True,
'auto_install': False,
} }

View File

@ -25,7 +25,7 @@ msgstr "Afegir"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:208 #: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:193
#, python-format #, python-format
msgid "All" msgid "All"
msgstr "Tot" msgstr "Tot"
@ -114,7 +114,7 @@ msgstr "Imatge variant mitjana ( calculada)"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:361 #: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:341
#, python-format #, python-format
msgid "[No widget %s]" msgid "[No widget %s]"
msgstr "[Sense widget %s]" msgstr "[Sense widget %s]"

View File

@ -6,58 +6,97 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 12.0\n" "Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-04 13:10+0000\n" "POT-Creation-Date: 2022-01-25 18:49+0000\n"
"PO-Revision-Date: 2021-05-04 17:17+0200\n" "PO-Revision-Date: 2022-01-25 19:51+0100\n"
"Last-Translator: claudiagn <claudia.gargallo@qubiq.es>\n" "Last-Translator: <>\n"
"Language-Team: none\n" "Language-Team: \n"
"Language: es\n" "Language: es\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: \n"
"X-Generator: Poedit 2.4.1\n" "X-Generator: Poedit 3.0\n"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:6 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:9
#, python-format
msgid "Accept"
msgstr "Aceptar"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:5
#, python-format #, python-format
msgid "Add" msgid "Add"
msgstr "Añadir" msgstr "Añadir"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:208 #: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:258
#, python-format #, python-format
msgid "All" msgid "All"
msgstr "Todo" msgstr "Todo"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:35 #: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:72
#, python-format
msgid "By Name"
msgstr "Por Nombre"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:12
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:20
#, python-format
msgid "Discard"
msgstr "Descartar"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:61
#, python-format #, python-format
msgid "Groups" msgid "Groups"
msgstr "Grupos" msgstr "Grupos"
#. module: web_widget_one2many_product_picker
#: model:ir.model,name:web_widget_one2many_product_picker.model_ir_http
msgid "HTTP Routing"
msgstr "Ruta HTTP"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:43 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:207
#, python-format
msgid "LOADING..."
msgstr "CARGANDO..."
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:73
#, python-format #, python-format
msgid "Lines" msgid "Lines"
msgstr "Líneas" msgstr "Líneas"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:67 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:92
#, python-format #, python-format
msgid "Load More" msgid "Load More"
msgstr "Cargar más" msgstr "Cargar más"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml:11 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml:4
#, python-format #, python-format
msgid "Price:" msgid "Price"
msgstr "Precio:" msgstr "Precio"
#. module: web_widget_one2many_product_picker
#: model:ir.model,name:web_widget_one2many_product_picker.model_product_pricelist_item
msgid "Pricelist Item"
msgstr "Item de Lista de precios"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#: model:ir.model,name:web_widget_one2many_product_picker.model_product_product #: model:ir.model,name:web_widget_one2many_product_picker.model_product_product
@ -66,62 +105,28 @@ msgstr "Producto"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:13 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:18
#, python-format #, python-format
msgid "Remove" msgid "Remove"
msgstr "Eliminar" msgstr "Eliminar"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:23 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:41
#, python-format #, python-format
msgid "Search..." msgid "Search..."
msgstr "Buscar..." msgstr "Buscar..."
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:58 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:81
#, python-format #, python-format
msgid "Subtotal:" msgid "Subtotal:"
msgstr "Subtotal:" msgstr "Subtotal:"
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,help:web_widget_one2many_product_picker.field_product_product__image_variant_medium
msgid ""
"This field holds the image used as image for the product variantor product "
"image medium, limited to 512x512px."
msgstr ""
"Aquest camp conté la imatge que s'utilitza com a imatge per al mitjà "
"d'imatge del producte que varia el producte, limitada a 512x512px."
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,help:web_widget_one2many_product_picker.field_product_product__image_variant_big
msgid ""
"This field holds the image used as image for the product variantor product "
"image, limited to 1024x1024px."
msgstr ""
"Aquest camp conté la imatge que s'utilitza com a imatge per al mitjà "
"d'imatge del producte que varia el producte, limitada a 1024x1024px."
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,field_description:web_widget_one2many_product_picker.field_product_product__image_variant_big
msgid "Variant Image Big (Computed)"
msgstr "Imagen variante grande (calculada)"
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,field_description:web_widget_one2many_product_picker.field_product_product__image_variant_medium
msgid "Variant Image Medium (Computed)"
msgstr "Imagen variante media (calculada)"
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:361 #: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:606
#, python-format #, python-format
msgid "[No widget %s]" msgid "[No widget %s]"
msgstr "[Sin widget %s]" msgstr "[Sin widget %s]"
#~ msgid "HTTP Routing"
#~ msgstr "Enrutamiento HTTP"
#~ msgid "Pricelist Item"
#~ msgstr "Elemento de tarifa"

View File

@ -23,7 +23,7 @@ msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:208 #: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:193
#, python-format #, python-format
msgid "All" msgid "All"
msgstr "" msgstr ""
@ -108,7 +108,7 @@ msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:361 #: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:341
#, python-format #, python-format
msgid "[No widget %s]" msgid "[No widget %s]"
msgstr "" msgstr ""

View File

@ -6,6 +6,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 12.0\n" "Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-01-25 18:49+0000\n"
"PO-Revision-Date: 2022-01-25 18:49+0000\n"
"Last-Translator: <>\n" "Last-Translator: <>\n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -15,44 +17,83 @@ msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:6 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:9
#, python-format
msgid "Accept"
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:5
#, python-format #, python-format
msgid "Add" msgid "Add"
msgstr "" msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:208 #: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:258
#, python-format #, python-format
msgid "All" msgid "All"
msgstr "" msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:35 #: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:72
#, python-format
msgid "By Name"
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:12
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:20
#, python-format
msgid "Discard"
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:61
#, python-format #, python-format
msgid "Groups" msgid "Groups"
msgstr "" msgstr ""
#. module: web_widget_one2many_product_picker
#: model:ir.model,name:web_widget_one2many_product_picker.model_ir_http
msgid "HTTP Routing"
msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:43 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:207
#, python-format
msgid "LOADING..."
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:73
#, python-format #, python-format
msgid "Lines" msgid "Lines"
msgstr "" msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:67 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:92
#, python-format #, python-format
msgid "Load More" msgid "Load More"
msgstr "" msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml:11 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml:4
#, python-format #, python-format
msgid "Price:" msgid "Price"
msgstr ""
#. module: web_widget_one2many_product_picker
#: model:ir.model,name:web_widget_one2many_product_picker.model_product_pricelist_item
msgid "Pricelist Item"
msgstr "" msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
@ -62,48 +103,28 @@ msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:13 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:18
#, python-format #, python-format
msgid "Remove" msgid "Remove"
msgstr "" msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:23 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:41
#, python-format #, python-format
msgid "Search..." msgid "Search..."
msgstr "" msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:58 #: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:81
#, python-format #, python-format
msgid "Subtotal:" msgid "Subtotal:"
msgstr "" msgstr ""
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,help:web_widget_one2many_product_picker.field_product_product__image_variant_medium
msgid "This field holds the image used as image for the product variantor product image medium, limited to 512x512px."
msgstr ""
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,help:web_widget_one2many_product_picker.field_product_product__image_variant_big
msgid "This field holds the image used as image for the product variantor product image, limited to 1024x1024px."
msgstr ""
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,field_description:web_widget_one2many_product_picker.field_product_product__image_variant_big
msgid "Variant Image Big (Computed)"
msgstr ""
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,field_description:web_widget_one2many_product_picker.field_product_product__image_variant_medium
msgid "Variant Image Medium (Computed)"
msgstr ""
#. module: web_widget_one2many_product_picker #. module: web_widget_one2many_product_picker
#. openerp-web #. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:361 #: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:606
#, python-format #, python-format
msgid "[No widget %s]" msgid "[No widget %s]"
msgstr "" msgstr ""

View File

@ -1,2 +1 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). from . import base
from . import product_product

View File

@ -0,0 +1,76 @@
# Copyright 2022 Tecnativa - Alexandre D. Díaz
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import _, api, models
from odoo.exceptions import ValidationError
class BaseModel(models.BaseModel):
_inherit = "base"
@api.model_create_multi
def create(self, vals_list):
"""Avoid create lines that have a product currently used when use the product
picker"""
relation = self.env.context.get("product_picker_relation")
if relation == self._name and len(vals_list):
product_field = self.env.context.get("product_picker_product_field")
product_ids = [
values[product_field] for values in vals_list if product_field in values
]
if len(product_ids) != len(set(product_ids)):
raise ValidationError(
_("Can't create the %s: Duplicated product! (Inside query)")
% relation
)
relation_field = self.env.context.get("product_picker_relation_field")
# All records have the same 'relation id' when created with the product
# picker
relation_id = vals_list[0][relation_field]
has_product = (
self.search(
[
(relation_field, "=", relation_id),
(product_field, "in", product_ids),
],
count=True,
limit=1,
)
!= 0
)
if has_product:
raise ValidationError(
_("Can't create the %s: Duplicated product! (Already in database)")
% relation
)
return super().create(vals_list)
def write(self, values):
"""Avoid write lines that have a product currently used when use the product
picker"""
relation = self.env.context.get("product_picker_relation")
product_field = self.env.context.get("product_picker_product_field")
if relation == self._name and product_field in values:
relation_field = self.env.context.get("product_picker_relation_field")
relation_id = (
values[relation_field]
if relation_field in values
else self.get(relation_field)
)
product_id = values[product_field]
has_product = (
self.search(
[
(relation_field, "=", relation_id),
(product_field, "=", product_id),
],
count=True,
limit=1,
)
!= 0
)
if has_product:
raise ValidationError(
_("Can't write the %s: Duplicated product! (Already in database)")
% relation
)
return super().write(values)

View File

@ -1,36 +0,0 @@
# Copyright 2020 Tecnativa - Alexandre D. Díaz
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from odoo import api, fields, models, tools
class ProductProduct(models.Model):
_inherit = 'product.product'
image_variant_medium = fields.Binary(
"Variant Image Medium (Computed)",
compute='_compute_variant_image',
help="This field holds the image used as image for the product variant"
"or product image medium, limited to 512x512px.",
)
image_variant_big = fields.Binary(
"Variant Image Big (Computed)",
compute='_compute_variant_image',
help="This field holds the image used as image for the product variant"
"or product image, limited to 1024x1024px.",
)
@api.depends('image_variant', 'product_tmpl_id.image')
def _compute_variant_image(self):
for record in self:
if record.image_variant:
resized_images = tools.image_get_resized_images(
record.image_variant,
return_big=False,
return_small=False,
avoid_resize_medium=True)
record.image_variant_medium = resized_images['image_medium']
record.image_variant_big = record.image_variant
else:
record.image_variant_medium = record.product_tmpl_id.image_medium
record.image_variant_big = record.product_tmpl_id.image

View File

@ -5,7 +5,6 @@ You need to define the view fields. The view must be of ``form`` type.
Widget options: Widget options:
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
* records_per_page > Integer -> Used to control the load more behaviour (16 by default)
* groups > Array of dictionaries -> Declare the groups * groups > Array of dictionaries -> Declare the groups
* name -> The group name * name -> The group name
@ -16,6 +15,9 @@ Widget options:
* name -> The field name to order * name -> The field name to order
* asc -> Flag to use 'asc' order * asc -> Flag to use 'asc' order
* records_per_page > Integer -> Used to control the load more behaviour (16 by default)
* active -> Boolean -> Select the default group to use ('false' by default = 'All' group)
* currency_field > Model field used to format monetary values ('currency_id' by default) * currency_field > Model field used to format monetary values ('currency_id' by default)
* field_map > Dictionary: * field_map > Dictionary:
@ -26,7 +28,7 @@ Widget options:
* price_unit -> The field that represent a price_unit ('price_unit' 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) * 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) * search > Array of dictionaries (defines to use name_search by default)
* name -> The name to display * name -> The name to display
* domain -> The domain to use * domain -> The domain to use
@ -34,11 +36,15 @@ Widget options:
* $search -> Replaces it with the current value of the searchbox * $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 * $number_search -> Replaces all the leaf with the current value of the searchbox as a number
* name_search_value -> Enables the use of 'name_search' instead of 'search_read' and defines the value to search ('$search' by default)
* operator -> Operator used in 'name_search' ('ilike' by default)
* edit_discount > Enable/Disable discount edits (False by default) * edit_discount > Enable/Disable discount edits (False by default)
* edit_price > Enable/Disable price edits (True by default) * edit_price > Enable/Disable price edits (True by default)
* show_discount > Enable/Disable display discount (False by default) * show_discount > Enable/Disable display discount (False by default)
* show_subtotal > Enable/Disable show subtotal (True by default) * show_subtotal > Enable/Disable show subtotal (True by default)
* auto_save > Enable/Disable auto save (False by default) * auto_save > Enable/Disable auto save (False by default)
* auto_save_delay > The time (in milliseconds) to wait after the last interaction before launching the autosave (1500 by default)
* all_domain > The domain used in 'All' section ([] by default) * all_domain > The domain used in 'All' section ([] by default)
If using auto save feature, you should keep in mind that the "Save" and "Discard" buttons If using auto save feature, you should keep in mind that the "Save" and "Discard" buttons
@ -46,7 +52,9 @@ Widget options:
modify/create a record with the widget. modify/create a record with the widget.
* ignore_warning > Enable/Disable display onchange warnings (False by default) * ignore_warning > Enable/Disable display onchange warnings (False by default)
* instant_search > Enable/Disable instant search mode (False by default)
* trigger_refresh_fields > Fields in the main record that dispatch a widget refresh (["partner_id", "currency_id"] by default) * trigger_refresh_fields > Fields in the main record that dispatch a widget refresh (["partner_id", "currency_id"] by default)
* auto_focus > Keep the focus on the search box after performing a search (True by default)
All widget options are optional. All widget options are optional.
Notice that you can call '_' method to use translations. This only can be used with this widget. Notice that you can call '_' method to use translations. This only can be used with this widget.

View File

@ -1,2 +1,3 @@
* Translations in the xml 'options' attribute of the field that use the widget can't be exported automatically to be translated * 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. * The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects
* sale.order onchanges that affects to "order_line" subfields will be ommitted to increase the performance

View File

@ -3,7 +3,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" /> <meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
<title>Web Widget One2Many Product Picker</title> <title>Web Widget One2Many Product Picker</title>
<style type="text/css"> <style type="text/css">
@ -406,8 +406,6 @@ You need to define the view fields. The view must be of <tt class="docutils lite
<div class="section" id="widget-options"> <div class="section" id="widget-options">
<h2><a class="toc-backref" href="#id3">Widget options:</a></h2> <h2><a class="toc-backref" href="#id3">Widget options:</a></h2>
<ul> <ul>
<li><p class="first">records_per_page &gt; Integer -&gt; Used to control the load more behaviour (16 by default)</p>
</li>
<li><p class="first">groups &gt; Array of dictionaries -&gt; Declare the groups</p> <li><p class="first">groups &gt; Array of dictionaries -&gt; Declare the groups</p>
<blockquote> <blockquote>
<ul> <ul>
@ -425,6 +423,10 @@ You need to define the view fields. The view must be of <tt class="docutils lite
</ul> </ul>
</blockquote> </blockquote>
</li> </li>
<li><p class="first">records_per_page &gt; Integer -&gt; Used to control the load more behaviour (16 by default)</p>
</li>
<li><p class="first">active -&gt; Boolean -&gt; Select the default group to use (false by default = All group)</p>
</li>
</ul> </ul>
</blockquote> </blockquote>
</li> </li>
@ -442,7 +444,7 @@ You need to define the view fields. The view must be of <tt class="docutils lite
</ul> </ul>
</blockquote> </blockquote>
</li> </li>
<li><p class="first">search &gt; Array of dictionaries or Array of triplets ([[field_map.name, ilike, $search]] by default)</p> <li><p class="first">search &gt; Array of dictionaries (defines to use name_search by default)</p>
<blockquote> <blockquote>
<ul> <ul>
<li><p class="first">name -&gt; The name to display</p> <li><p class="first">name -&gt; The name to display</p>
@ -455,6 +457,10 @@ You need to define the view fields. The view must be of <tt class="docutils lite
</ul> </ul>
</blockquote> </blockquote>
</li> </li>
<li><p class="first">name_search_value -&gt; Enables the use of name_search instead of search_read and defines the value to search ($search by default)</p>
</li>
<li><p class="first">operator -&gt; Operator used in name_search (ilike by default)</p>
</li>
</ul> </ul>
</blockquote> </blockquote>
</li> </li>
@ -468,6 +474,8 @@ You need to define the view fields. The view must be of <tt class="docutils lite
</li> </li>
<li><p class="first">auto_save &gt; Enable/Disable auto save (False by default)</p> <li><p class="first">auto_save &gt; Enable/Disable auto save (False by default)</p>
</li> </li>
<li><p class="first">auto_save_delay &gt; The time (in milliseconds) to wait after the last interaction before launching the autosave (1500 by default)</p>
</li>
<li><p class="first">all_domain &gt; The domain used in All section ([] by default)</p> <li><p class="first">all_domain &gt; The domain used in All section ([] by default)</p>
<p>If using auto save feature, you should keep in mind that the “Save” and “Discard” buttons <p>If using auto save feature, you should keep in mind that the “Save” and “Discard” buttons
will lose part of its functionality as the document will be saved every time you will lose part of its functionality as the document will be saved every time you
@ -475,8 +483,12 @@ modify/create a record with the widget.</p>
</li> </li>
<li><p class="first">ignore_warning &gt; Enable/Disable display onchange warnings (False by default)</p> <li><p class="first">ignore_warning &gt; Enable/Disable display onchange warnings (False by default)</p>
</li> </li>
<li><p class="first">instant_search &gt; Enable/Disable instant search mode (False by default)</p>
</li>
<li><p class="first">trigger_refresh_fields &gt; Fields in the main record that dispatch a widget refresh ([“partner_id”, “currency_id”] by default)</p> <li><p class="first">trigger_refresh_fields &gt; Fields in the main record that dispatch a widget refresh ([“partner_id”, “currency_id”] by default)</p>
</li> </li>
<li><p class="first">auto_focus &gt; Keep the focus on the search box after performing a search (True by default)</p>
</li>
</ul> </ul>
<p>All widget options are optional. <p>All widget options are optional.
Notice that you can call _ method to use translations. This only can be used with this widget.</p> Notice that you can call _ method to use translations. This only can be used with this widget.</p>
@ -597,7 +609,8 @@ accept changes.</p>
<h1><a class="toc-backref" href="#id10">Known issues / Roadmap</a></h1> <h1><a class="toc-backref" href="#id10">Known issues / Roadmap</a></h1>
<ul class="simple"> <ul class="simple">
<li>Translations in the xml options attribute of the field that use the widget cant be exported automatically to be translated</li> <li>Translations in the xml options attribute of the field that use the widget cant be exported automatically to be translated</li>
<li>The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects.</li> <li>The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects</li>
<li>sale.order onchanges that affects to “order_line” subfields will be ommitted to increase the performance</li>
</ul> </ul>
</div> </div>
<div class="section" id="bug-tracker"> <div class="section" id="bug-tracker">

View File

@ -1,21 +1,37 @@
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.tools", function ( odoo.define("web_widget_one2many_product_picker.tools", function(require) {
require
) {
"use strict"; "use strict";
var field_utils = require("web.field_utils"); var field_utils = require("web.field_utils");
/**
* Truncate floats
*
* @param {Number} value
* @param {Object} field_info
* @param {Array} digist
* @returns {Number}
*/
function float(value, field_info, digist) {
var options = digist && {digist: digist};
return field_utils.format.float(value, field_info, options);
}
/** /**
* Calculate the price with discount * Calculate the price with discount
* *
* @param {Number} price * @param {Number} price
* @param {Number} discount * @param {Number} discount
* @param {Array} digist
* @returns {Number} * @returns {Number}
*/ */
function priceReduce (price, discount) { function priceReduce(price, discount, digist) {
return price * (1.0 - discount / 100.0); var price_reduce = price * (1.0 - discount / 100.0);
if (digist) {
return float(price_reduce, undefined, digist);
}
return price_reduce;
} }
/** /**
@ -29,10 +45,7 @@ odoo.define("web_widget_one2many_product_picker.tools", function (
* @returns {String} * @returns {String}
*/ */
function monetary(value, field_info, currency_field, data) { function monetary(value, field_info, currency_field, data) {
return field_utils.format.monetary( return field_utils.format.monetary(value, field_info, {
value,
field_info,
{
data: data, data: data,
currency_field: currency_field, currency_field: currency_field,
field_digits: true, field_digits: true,
@ -41,7 +54,7 @@ odoo.define("web_widget_one2many_product_picker.tools", function (
return { return {
monetary: monetary, monetary: monetary,
float: float,
priceReduce: priceReduce, priceReduce: priceReduce,
}; };
}); });

View File

@ -1,3 +1,4 @@
/* global py */
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", function( odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", function(
@ -8,9 +9,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
var core = require("web.core"); var core = require("web.core");
var Widget = require("web.Widget"); var Widget = require("web.Widget");
var widgetRegistry = require("web.widget_registry"); var widgetRegistry = require("web.widget_registry");
var ProductPickerQuickCreateFormView = require( var ProductPickerQuickCreateFormView = require("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView")
"web_widget_one2many_product_picker.ProductPickerQuickCreateFormView" .ProductPickerQuickCreateFormView;
).ProductPickerQuickCreateFormView;
var qweb = core.qweb; var qweb = core.qweb;
@ -23,10 +23,6 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
"/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml", "/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml",
], ],
custom_events: {
reload_view: "_onReloadView",
},
/** /**
* @override * @override
*/ */
@ -52,53 +48,52 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
*/ */
start: function() { start: function() {
var self = this; var self = this;
var def1 = this._super.apply(this, arguments); return this._super.apply(this, arguments).then(function() {
var form_arch = this._generateFormArch(); var form_arch = self._generateFormArch();
var fieldsView = { var fieldsView = {
arch: form_arch, arch: form_arch,
fields: this.fields, fields: self.fields,
viewFields: this.fields, viewFields: self.fields,
base_model: this.basicFieldParams.field.relation, base_model: self.basicFieldParams.field.relation,
type: "form", type: "form",
model: this.basicFieldParams.field.relation, model: self.basicFieldParams.field.relation,
}; };
var node_context = this.node.attr("context") || "{}"; var node_context = self.node.attr("context") || "{}";
this.nodeContext = py.eval(node_context, { self.nodeContext = py.eval(node_context, {
active_id: this.res_id || false, active_id: self.res_id || false,
}); });
var refinedContext = _.extend( var refinedContext = _.extend(
{}, {},
this.main_state.getContext(), self.main_state.getContext(),
this.nodeContext); self.nodeContext
_.extend(refinedContext, this.editContext); );
this.formView = new ProductPickerQuickCreateFormView(fieldsView, { _.extend(refinedContext, self.editContext);
self.formView = new ProductPickerQuickCreateFormView(fieldsView, {
context: refinedContext, context: refinedContext,
compareKey: this.compareKey, compareKey: self.compareKey,
fieldMap: this.fieldMap, fieldMap: self.fieldMap,
modelName: this.basicFieldParams.field.relation, modelName: self.basicFieldParams.field.relation,
userContext: this.getSession().user_context, userContext: self.getSession().user_context,
ids: this.res_id ? [this.res_id] : [], ids: self.res_id ? [self.res_id] : [],
currentId: this.res_id || undefined, currentId: self.res_id || undefined,
mode: this.res_id && this.readonly ? "readonly" : "edit", mode: self.res_id && self.readonly ? "readonly" : "edit",
recordID: this.id, recordID: self.id,
index: 0, index: 0,
parentID: this.basicFieldParams.parentID, parentID: self.basicFieldParams.parentID,
default_buttons: false, default_buttons: false,
withControlPanel: false, withControlPanel: false,
model: this.basicFieldParams.model, model: self.basicFieldParams.model,
mainRecordData: this.getParent().getParent().state, mainRecordData: self.getParent().getParent().state,
}); });
// if (this.id) { return self.formView.getController(self).then(function(controller) {
// this.basicFieldParams.model.save(this.id, {savePoint: true});
// }
var def2 = this.formView.getController(this).then(function (controller) {
self.controller = controller; self.controller = controller;
self.$el.empty(); self.$el.empty();
self.controller.appendTo(self.$el); self.controller.appendTo(self.$el);
self.trigger_up("back_form_loaded");
return controller;
});
}); });
return $.when(def1, def2);
}, },
on_attach_callback: function() { on_attach_callback: function() {
@ -112,7 +107,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
* @returns {String} * @returns {String}
*/ */
_generateFormArch: function() { _generateFormArch: function() {
var template = "<templates><t t-name='One2ManyProductPicker.QuickCreateForm'>"; var template =
"<templates><t t-name='One2ManyProductPicker.QuickCreateForm'>";
template += this.basicFieldParams.field.views.form.arch; template += this.basicFieldParams.field.views.form.arch;
template += "</t></templates>"; template += "</t></templates>";
qweb.add_template(template); qweb.add_template(template);
@ -121,39 +117,12 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
record_search: this.searchRecord, record_search: this.searchRecord,
}); });
}, },
/**
* @private
* @param {CustomEvent} evt
*/
_onReloadView: function (evt) {
this.editContext = {
'ignore_onchanges': [this.compareKey],
'base_record_id': evt.data.baseRecordID || null,
'base_record_res_id': evt.data.baseRecordResID || null,
'base_record_compare_value': evt.data.baseRecordCompareValue || null,
};
if (evt.data.baseRecordCompareValue === evt.data.compareValue) {
this.res_id = evt.data.baseRecordResID;
this.id = evt.data.baseRecordID;
this.start();
} else {
var self = this;
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.res_id = state.res_id;
self.id = state.id;
self.start();
});
});
}
},
}); });
widgetRegistry.add("product_picker_quick_create_form", ProductPickerQuickCreateForm); widgetRegistry.add(
"product_picker_quick_create_form",
ProductPickerQuickCreateForm
);
return ProductPickerQuickCreateForm; return ProductPickerQuickCreateForm;
}); });

View File

@ -1,8 +1,8 @@
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView", function ( odoo.define(
require "web_widget_one2many_product_picker.ProductPickerQuickCreateFormView",
) { function(require) {
"use strict"; "use strict";
/** /**
@ -18,9 +18,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
BasicModel.include({ BasicModel.include({
_applyOnChange: function(values, record, viewType) { _applyOnChange: function(values, record, viewType) {
var vt = viewType || record.viewType;
// Ignore changes by record context 'ignore_onchanges' fields // Ignore changes by record context 'ignore_onchanges' fields
if ('ignore_onchanges' in record.context) { if ("ignore_onchanges" in record.context) {
var ignore_changes = record.context.ignore_onchanges; var ignore_changes = record.context.ignore_onchanges;
for (var index in ignore_changes) { for (var index in ignore_changes) {
var field_name = ignore_changes[index]; var field_name = ignore_changes[index];
@ -32,23 +31,21 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
}, },
}); });
var ProductPickerQuickCreateFormRenderer = var ProductPickerQuickCreateFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend(
QuickCreateFormView.prototype.config.Renderer.extend(
{ {
/** /**
* @override * @override
*/ */
start: function() { start: function() {
this.$el.addClass( this.$el.addClass(
"oe_one2many_product_picker_form_view o_xxs_form_view"); "oe_one2many_product_picker_form_view o_xxs_form_view"
);
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
} }
); );
var ProductPickerQuickCreateFormController = var ProductPickerQuickCreateFormController = QuickCreateFormView.prototype.config.Controller.extend(
QuickCreateFormView.prototype.config.Controller.extend(
{ {
events: _.extend({}, QuickCreateFormView.prototype.events, { events: _.extend({}, QuickCreateFormView.prototype.events, {
"click .oe_record_add": "_onClickAdd", "click .oe_record_add": "_onClickAdd",
@ -65,18 +62,27 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
this._super.apply(this, arguments); this._super.apply(this, arguments);
}, },
/**
* @override
*/
_applyChanges: function() {
var self = this;
return this._super.apply(this, arguments).then(function() {
self._updateButtons();
});
},
/** /**
* Create or accept changes * Create or accept changes
*/ */
auto: function() { auto: function() {
var record = this.model.get(this.handle); var record = this.model.get(this.handle);
if (record.context.has_changes_confirmed || typeof record.context.has_changes_confirmed === "undefined") { if (!record.context.has_changes_unconfirmed) {
return; return;
} }
var state = this._getRecordState(); if (this.model.isNew(record.id)) {
if (state === "new") {
this._add(); this._add();
} else if (state === "dirty") { } else if (this.model.isDirty(record.id)) {
this._change(); this._change();
} }
}, },
@ -86,13 +92,15 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* - record: Normal * - record: Normal
* - new: Is a new record * - new: Is a new record
* - dirty: Has changes * - dirty: Has changes
*
* @returns {Object}
*/ */
_getRecordState: function() { _getRecordState: function() {
var record = this.model.get(this.handle); var record = this.model.get(this.handle);
var state = "record"; var state = "record";
if (this.model.isNew(record.id)) { if (this.model.isNew(record.id) && this.model.isPureVirtual(record.id)) {
state = "new"; state = "new";
} else if (record.isDirty()) { } else if (record.context.has_changes_unconfirmed) {
state = "dirty"; state = "dirty";
} }
if (state === "new") { if (state === "new") {
@ -118,11 +126,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private * @private
*/ */
_updateButtons: function() { _updateButtons: function() {
this.$el.find( this.$el.find(".oe_one2many_product_picker_form_buttons").remove();
".oe_one2many_product_picker_form_buttons").remove();
this.$el.find(".o_form_view").append( this.$el.find(".o_form_view").append(
qweb.render( qweb.render("One2ManyProductPicker.QuickCreate.FormButtons", {
"One2ManyProductPicker.QuickCreate.FormButtons", {
state: this._getRecordState(), state: this._getRecordState(),
}) })
); );
@ -151,7 +157,6 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private * @private
*/ */
_enableQuickCreate: function() { _enableQuickCreate: function() {
// Allows to create again // Allows to create again
this._disabled = false; this._disabled = false;
this.$el.removeClass("o_disabled"); this.$el.removeClass("o_disabled");
@ -162,7 +167,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
/** /**
* @private * @private
* @param {Array[String]} fields_changed * @param {Array} fields_changed
* @returns {Boolean} * @returns {Boolean}
*/ */
_needReloadCard: function(fields_changed) { _needReloadCard: function(fields_changed) {
@ -184,53 +189,18 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @param {ChangeEvent} ev * @param {ChangeEvent} ev
*/ */
_onFieldChanged: function(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); this._super.apply(this, arguments);
if (!_.isEmpty(ev.data.changes)) { if (!_.isEmpty(ev.data.changes)) {
if (this.model.isPureVirtual(this.handle)) { if (this.model.isPureVirtual(this.handle)) {
this.model.unsetDirty(this.handle); this.model.unsetDirty(this.handle);
} }
this.model.updateRecordContext(this.handle, { this.model.updateRecordContext(this.handle, {
has_changes_confirmed: false, has_changes_unconfirmed: true,
}); });
this.trigger_up("quick_record_updated", { this.trigger_up("quick_record_updated", {
changes: ev.data.changes, changes: ev.data.changes,
highlight: {qty: true},
}); });
this._updateButtons();
}
} }
}, },
@ -238,15 +208,15 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @returns {Deferred} * @returns {Deferred}
*/ */
_add: function() { _add: function() {
var self = this;
if (this._disabled) { if (this._disabled) {
// Don't do anything if we are already creating a record // Don't do anything if we are already creating a record
return $.Deferred(); return $.Deferred().resolve();
} }
this.model.updateRecordContext(this.handle, { this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true, need_notify: true,
modified: true,
}); });
var self = this;
this._disableQuickCreate(); this._disableQuickCreate();
return this.saveRecord(this.handle, { return this.saveRecord(this.handle, {
stayInEdit: true, stayInEdit: true,
@ -255,90 +225,138 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
viewType: "form", viewType: "form",
}).then(function() { }).then(function() {
var record = self.model.get(self.handle); var record = self.model.get(self.handle);
self.model.updateRecordContext(self.handle, {saving: true}); self.model.updateRecordContext(record.id, {
has_changes_unconfirmed: false,
lazy_qty: record.data[self.fieldMap.product_uom_qty],
});
self.trigger_up("block_card", {status: true});
self.trigger_up("modify_quick_record", {
id: record.id,
});
self.trigger_up("restore_flip_card", { self.trigger_up("restore_flip_card", {
success_callback: function() { success_callback: function() {
self.trigger_up("create_quick_record", { self.trigger_up("create_quick_record", {
id: record.id, id: record.id,
callback: function () { on_onchange: function() {
self.model.unsetDirty(self.handle); self.trigger_up("block_card", {status: false});
self._enableQuickCreate(); self._enableQuickCreate();
}, },
}); });
}, },
block: true,
}); });
}); });
}, },
_remove: function() { _remove: function() {
var self = this;
if (this._disabled) { if (this._disabled) {
return $.Deferred(); return $.Deferred().resolve();
} }
this.model.updateRecordContext(this.handle, {
need_notify: true,
modified: true,
});
this._disableQuickCreate(); this._disableQuickCreate();
this.trigger_up("restore_flip_card", {block: true});
var record = this.model.get(this.handle); var record = this.model.get(this.handle);
this.trigger_up("list_record_remove", { this.trigger_up("block_card", {status: true});
this.trigger_up("modify_quick_record", {
id: record.id, id: record.id,
}); });
this.trigger_up("restore_flip_card", {
success_callback: function() {
self.trigger_up("list_record_remove", {
id: record.id,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
},
});
}, },
_change: function() { _change: function() {
var self = this; var self = this;
if (this._disabled) { if (this._disabled) {
// Don't do anything if we are already creating a record // Don't do anything if we are already creating a record
return $.Deferred(); return $.Deferred().resolve();
}
var record = self.model.get(self.handle);
if (!this.model.isDirty(this.handle) || !record.context.has_changes_unconfirmed) {
this.trigger_up("restore_flip_card");
return $.Deferred().resolve();
} }
this._disableQuickCreate();
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
var record = this.model.get(this.handle);
this.trigger_up("restore_flip_card", { this.model.updateRecordContext(this.handle, {
need_notify: true,
modified: true,
});
this._disableQuickCreate();
// SaveRecord used to make a save point.
return this.saveRecord(this.handle, {
stayInEdit: true,
reload: true,
savePoint: true,
viewType: "form",
}).then(function() {
record = self.model.get(self.handle);
self.model.updateRecordContext(record.id, {
has_changes_unconfirmed: false,
lazy_qty: record.data[self.fieldMap.product_uom_qty],
});
self.trigger_up("block_card", {status: true});
self.trigger_up("modify_quick_record", {
id: record.id,
});
self.trigger_up("restore_flip_card", {
success_callback: function() { success_callback: function() {
self.trigger_up("update_quick_record", { self.trigger_up("update_quick_record", {
id: record.id, id: record.id,
callback: function () { on_onchange: function() {
self.model.unsetDirty(self.handle); self.trigger_up("block_card", {status: false});
self._enableQuickCreate(); self._enableQuickCreate();
} },
}); });
}, },
block: true, });
}); });
}, },
_discard: function() { _discard: function() {
var self = this; var self = this;
if (this._disabled) { if (this._disabled) {
// Don't do anything if we are already creating a record // Don't do anything if we are already creating a record
return $.Deferred(); return $.Deferred().resolve();
} }
var record = self.model.get(self.handle);
if (!this.model.isDirty(this.handle) || !record.context.has_changes_unconfirmed) {
this.trigger_up("restore_flip_card");
return $.Deferred().resolve();
}
this.model.updateRecordContext(this.handle, {
has_changes_unconfirmed: false,
});
this._disableQuickCreate(); this._disableQuickCreate();
var record = this.model.get(this.handle); // Rollback to restore the save point
this.model.discardChanges(this.handle, { this.model.discardChanges(this.handle, {
rollback: true, rollback: true,
}); });
this.trigger_up("quick_record_updated", { return this.update({}, {reload: false}).then(function() {
record = self.model.get(self.handle);
self.trigger_up("quick_record_updated", {
changes: record.data, changes: record.data,
}); });
if (this.model.isNew(record.id)) { self.trigger_up("restore_flip_card", {
this.update({}, {reload: false}); success_callback: function() {
this.trigger_up("restore_flip_card");
this._updateButtons();
this._enableQuickCreate();
} else {
this.update({}, {reload: false}).then(function () {
self.model.unsetDirty(self.handle);
self.trigger_up("restore_flip_card");
self._updateButtons(); self._updateButtons();
self._enableQuickCreate(); self._enableQuickCreate();
});
} }
});
});
}, },
/** /**
@ -402,4 +420,5 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
ProductPickerQuickCreateFormController: ProductPickerQuickCreateFormController, ProductPickerQuickCreateFormController: ProductPickerQuickCreateFormController,
ProductPickerQuickCreateFormView: ProductPickerQuickCreateFormView, ProductPickerQuickCreateFormView: ProductPickerQuickCreateFormView,
}; };
}); }
);

View File

@ -1,15 +1,16 @@
/* global py */
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm", function ( odoo.define(
require "web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm",
) { function(require) {
"use strict"; "use strict";
var core = require("web.core"); var core = require("web.core");
var Widget = require("web.Widget"); var Widget = require("web.Widget");
var ProductPickerQuickModifPriceFormView = require( var widgetRegistry = require("web.widget_registry");
"web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView" var ProductPickerQuickModifPriceFormView = require("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView")
).ProductPickerQuickModifPriceFormView; .ProductPickerQuickModifPriceFormView;
var qweb = core.qweb; var qweb = core.qweb;
@ -22,18 +23,24 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
"/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml", "/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml",
], ],
events: {
"click .oe_record_change": "_onClickChange",
"click .oe_record_discard": "_onClickDiscard",
},
/** /**
* @override * @override
*/ */
init: function(parent, options) { init: function(parent, options) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.trigger_up("pause_auto_save");
this.state = options.state; this.state = options.state;
this.main_state = options.main_state; this.main_state = options.main_state;
this.node = options.node; this.node = options.node;
this.fields = options.fields; this.fields = options.fields;
this.fieldMap = options.fieldMap; this.fieldMap = options.fieldMap;
this.searchRecord = options.searchRecord; this.searchRecord = options.searchRecord;
this.fieldsInfo = options.fieldsInfo; this.fieldsInfo = _.extend({}, options.fieldsInfo);
this.readonly = options.readonly; this.readonly = options.readonly;
this.basicFieldParams = options.basicFieldParams; this.basicFieldParams = options.basicFieldParams;
this.canEditPrice = options.canEditPrice; this.canEditPrice = options.canEditPrice;
@ -42,6 +49,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
this.res_id = this.state && this.state.res_id; this.res_id = this.state && this.state.res_id;
this.id = this.state && this.state.id; this.id = this.state && this.state.id;
this.editContext = {}; this.editContext = {};
this._fieldsInvisible = [];
}, },
/** /**
@ -49,49 +57,69 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
*/ */
start: function() { start: function() {
var self = this; var self = this;
var def1 = this._super.apply(this, arguments); this._super.apply(this, arguments).then(function() {
var fieldsView = { var fieldsView = {
arch: this._generateFormArch(), arch: self._generateFormArch(),
fields: this.fields, fields: self.fields,
viewFields: this.fields, viewFields: self.fields,
base_model: this.basicFieldParams.field.relation, base_model: self.basicFieldParams.field.relation,
type: "form", type: "form",
model: this.basicFieldParams.field.relation, model: self.basicFieldParams.field.relation,
}; };
this.formView = new ProductPickerQuickModifPriceFormView(fieldsView, {
context: this.main_state.getContext(), var node_context = self.node.attr("context") || "{}";
fieldMap: this.fieldMap, self.nodeContext = py.eval(node_context, {
modelName: this.basicFieldParams.field.relation, active_id: self.res_id || false,
userContext: this.getSession().user_context, });
ids: this.res_id ? [this.res_id] : [], var refinedContext = _.extend(
currentId: this.res_id || undefined, {},
mode: this.res_id && this.readonly ? "readonly" : "edit", self.main_state.getContext(),
recordID: this.id, self.nodeContext
);
_.extend(refinedContext, self.editContext);
self.formView = new ProductPickerQuickModifPriceFormView(
fieldsView,
{
context: refinedContext,
fieldMap: self.fieldMap,
modelName: self.basicFieldParams.field.relation,
userContext: self.getSession().user_context,
ids: self.res_id ? [self.res_id] : [],
currentId: self.res_id || undefined,
mode: self.res_id && self.readonly ? "readonly" : "edit",
recordID: self.id,
index: 0, index: 0,
parentID: this.basicFieldParams.parentID, parentID: self.basicFieldParams.parentID,
default_buttons: true, default_buttons: true,
withControlPanel: false, withControlPanel: false,
model: this.basicFieldParams.model, model: self.basicFieldParams.model,
parentRecordData: this.basicFieldParams.recordData, parentRecordData: self.getParent().getParent().state,
currencyField: this.currencyField, currencyField: self.currencyField,
disable_autofocus: true, disable_autofocus: true,
});
if (this.id) {
this.basicFieldParams.model.save(this.id, {savePoint: true});
} }
var def2 = this.formView.getController(this).then(function (controller) { );
if (self.id) {
self.basicFieldParams.model.save(self.id, {savePoint: true});
}
return self.formView.getController(self).then(function(controller) {
self.controller = controller; self.controller = controller;
self.$el.empty(); self.$(".modal-body").empty();
self.controller.appendTo(self.$el); self.controller.appendTo(self.$(".modal-body"));
self.$el.on("hidden.bs.modal", self._onModalHidden.bind(self));
self.$el.find('.oe_record_change').removeClass('d-none');
return controller;
});
}); });
return $.when(def1, def2);
}, },
/** /**
* @override * @override
*/ */
destroy: function() { destroy: function() {
this._restoreNoFetch();
this.trigger_up("resume_auto_save");
this.$el.off("hidden.bs.modal");
this._super.apply(this, arguments); this._super.apply(this, arguments);
}, },
@ -110,23 +138,41 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
template += this.basicFieldParams.field.views.form.arch; template += this.basicFieldParams.field.views.form.arch;
template += "</t></templates>"; template += "</t></templates>";
qweb.add_template(template); qweb.add_template(template);
var $arch = $(qweb.render("One2ManyProductPicker.QuickModifPrice.Form", { var $arch = $(
qweb.render("One2ManyProductPicker.QuickModifPrice.Form", {
field_map: this.fieldMap, field_map: this.fieldMap,
record_search: this.searchRecord, record_search: this.searchRecord,
})); })
);
var field_names = Object.keys(wanted_field_states); var field_names = Object.keys(
this.basicFieldParams.field.views.form.fields
);
var gen_arch = "<form><group>"; var gen_arch = "<form><group>";
for (var index in field_names) { for (var index in field_names) {
var field_name = field_names[index]; var field_name = field_names[index];
var $field = $arch.find("field[name='" + field_name + "']"); var $field = $arch.find("field[name='" + field_name + "']");
var modifiers = var modifiers = $field.attr("modifiers")
$field.attr("modifiers") ? JSON.parse($field.attr("modifiers")) : {}; ? JSON.parse($field.attr("modifiers"))
modifiers.invisible = false; : {};
if (!modifiers.invisible && !(field_name in wanted_field_states)) {
this._fieldsInvisible.push(field_name);
}
modifiers.invisible = !(field_name in wanted_field_states);
modifiers.readonly = wanted_field_states[field_name]; modifiers.readonly = wanted_field_states[field_name];
$field.attr("modifiers", JSON.stringify(modifiers)); $field.attr("modifiers", JSON.stringify(modifiers));
$field.attr("invisible", "0"); $field.attr("invisible", modifiers.invisible ? "1" : "0");
$field.attr("readonly", wanted_field_states[field_name]?"1":"0"); $field.attr(
"readonly",
wanted_field_states[field_name] ? "1" : "0"
);
if (
[this.fieldMap.price_unit, this.fieldMap.product].indexOf(
field_name
) !== -1
) {
$field.attr("force_save", "1");
}
gen_arch += $field[0].outerHTML; gen_arch += $field[0].outerHTML;
} }
gen_arch += "</group></form>"; gen_arch += "</group></form>";
@ -146,7 +192,64 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
wantedFieldState[this.fieldMap.price_unit] = !this.canEditPrice; wantedFieldState[this.fieldMap.price_unit] = !this.canEditPrice;
return wantedFieldState; return wantedFieldState;
}, },
/**
* @private
*/
_onClickDiscard: function(ev) {
if (this.controller) {
this._hideControlButtons(true);
this.controller._onClickDiscard(ev);
}
},
/**
* @private
*/
_onClickChange: function(ev) {
var self = this;
if (!this.controller) {
return;
}
self._hideControlButtons(true);
this.controller._onClickChange(ev).then(function(res) {
if (res) {
self.$el.modal("hide");
} else {
self._hideControlButtons(false);
}
});
},
/**
* @private
*/
_onModalHidden: function() {
this.destroy();
},
_hideControlButtons: function(status) {
this.$el.find('.oe_record_change').toggleClass('d-none', status);
this.$el.find('.oe_record_discard').toggleClass('d-none', status);
},
/**
* @private
*/
_restoreNoFetch: function() {
var record = this.basicFieldParams.model.get(this.id);
for (var field_name of this._fieldsInvisible) {
record.fieldsInfo[record.viewType][field_name].__no_fetch = false;
}
this._fieldsInvisible = [];
},
}); });
widgetRegistry.add(
"product_picker_quick_modif_price_form",
ProductPickerQuickModifPriceForm
);
return ProductPickerQuickModifPriceForm; return ProductPickerQuickModifPriceForm;
}); }
);

View File

@ -1,8 +1,8 @@
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView", function ( odoo.define(
require "web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView",
) { function(require) {
"use strict"; "use strict";
/** /**
@ -16,8 +16,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
var qweb = core.qweb; var qweb = core.qweb;
var ProductPickerQuickModifPriceFormRenderer = var ProductPickerQuickModifPriceFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend(
QuickCreateFormView.prototype.config.Renderer.extend(
{ {
/** /**
* @override * @override
@ -25,27 +24,13 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
start: function() { start: function() {
var self = this; var self = this;
this.$el.addClass( this.$el.addClass(
"oe_one2many_product_picker_form_view o_xxs_form_view"); "oe_one2many_product_picker_form_view o_xxs_form_view"
);
return this._super.apply(this, arguments).then(function() { return this._super.apply(this, arguments).then(function() {
self._appendPrice(); 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 * @private
*/ */
@ -58,23 +43,18 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
} }
); );
var ProductPickerQuickModifPriceFormController = var ProductPickerQuickModifPriceFormController = QuickCreateFormView.prototype.config.Controller.extend(
QuickCreateFormView.prototype.config.Controller.extend(
{ {
events: _.extend({}, QuickCreateFormView.prototype.events, {
"click .oe_record_change": "_onClickChange",
"click .oe_record_discard": "_onClickDiscard",
}),
/** /**
* @override * @override
*/ */
init: function(parent, model, renderer, params) { init: function(parent, model, renderer, params) {
this.fieldMap = params.fieldMap; this.fieldMap = params.fieldMap;
this.context = params.context; this.context = params.context;
this._super.apply(this, arguments);
this.currencyField = params.currencyField; this.currencyField = params.currencyField;
this.parentRecordData = params.parentRecordData; this.parentRecordData = params.parentRecordData;
this.fields = parent.state.fields;
this._super.apply(this, arguments);
}, },
/** /**
@ -83,105 +63,119 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
start: function() { start: function() {
var self = this; var self = this;
return this._super.apply(this, arguments).then(function() { return this._super.apply(this, arguments).then(function() {
self._updatePrice(); var record = self.model.get(self.handle);
self._updatePrice(record.data);
}); });
}, },
/** /**
* @override * @override
*/ */
_onFieldChanged: function () { _onFieldChanged: function(ev) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this._updatePrice(); if (!_.isEmpty(ev.data.changes)) {
this.model.updateRecordContext(this.handle, {
has_changes_unconfirmed: true,
});
}
},
/**
* @override
*/
_applyChanges: function() {
return this._super.apply(this, arguments).then(() => {
var record = this.model.get(this.handle);
this._updatePrice(record.data);
});
}, },
/** /**
* @private * @private
* @param {Object} values
*/ */
_updatePrice: function () { _updatePrice: function(values) {
var record = this.model.get(this.handle);
var price_reduce = tools.priceReduce( var price_reduce = tools.priceReduce(
record.data[this.fieldMap.price_unit], values[this.fieldMap.price_unit],
record.data[this.fieldMap.discount]); values[this.fieldMap.discount]
this.renderer.$el.find(".oe_price").html( );
this.renderer.$el
.find(".oe_price")
.html(
tools.monetary( tools.monetary(
price_reduce, price_reduce,
this.getParent().state.fields[this.fieldMap.price_unit], this.fields[this.fieldMap.price_unit],
this.currencyField, this.currencyField,
record values
) )
); );
}, },
/**
* @private
*/
_disableQuickCreate: function () {
// 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 () {
// 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 * @private
* @param {MouseEvent} ev * @param {MouseEvent} ev
*/ */
_onClickChange: function(ev) { _onClickChange: function(ev) {
var self = this; var self = this;
var def = $.Deferred();
ev.stopPropagation(); ev.stopPropagation();
this.model.updateRecordContext(this.handle, { this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true, has_changes_unconfirmed: false,
}); });
var is_virtual = this.model.isPureVirtual(this.handle);
if (!this.model.isDirty(this.handle)) {
return def.resolve();
}
this._disableQuickCreate();
this.trigger_up("quick_record_updated", {
changes: _.extend(
{},
this.model.localData[this.handle].data,
this.model.localData[this.handle]._changes
),
highlight: {price: true},
});
this.model.updateRecordContext(this.handle, {
need_notify: true,
modified: true,
});
this.trigger_up("block_card", {status: true});
this.trigger_up("modify_quick_record", {
id: this.handle,
});
var is_virtual = this.model.isPureVirtual(this.handle);
// If is a 'pure virtual' record, save it in the selected list // If is a 'pure virtual' record, save it in the selected list
if (is_virtual) { if (is_virtual) {
if (this.model.isDirty(this.handle)) {
this._disableQuickCreate();
this.saveRecord(this.handle, { this.saveRecord(this.handle, {
stayInEdit: true, stayInEdit: true,
reload: true, reload: true,
savePoint: true, savePoint: true,
viewType: "form", viewType: "form",
}).then(function() { }).then(function() {
self._enableQuickCreate(); def.resolve(true);
var record = self.model.get(self.handle);
self.model.unsetDirty(self.handle);
self.trigger_up("create_quick_record", { self.trigger_up("create_quick_record", {
id: record.id, id: self.handle,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
}); });
self.getParent().destroy();
}); });
} else { } else {
this.getParent().destroy(); def.resolve(true);
}
} else {
// If is a "normal" record, update it // If is a "normal" record, update it
var record = this.model.get(this.handle);
this.trigger_up("update_quick_record", { this.trigger_up("update_quick_record", {
id: record.id, id: this.handle,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
}); });
self.model.unsetDirty(self.handle);
this.getParent().destroy();
} }
return def;
}, },
/** /**
@ -189,15 +183,52 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
* @param {MouseEvent} ev * @param {MouseEvent} ev
*/ */
_onClickDiscard: function(ev) { _onClickDiscard: function(ev) {
var self = this;
ev.stopPropagation(); ev.stopPropagation();
if (!this.model.isDirty(this.handle)) {
return true;
}
this.model.discardChanges(this.handle, { this.model.discardChanges(this.handle, {
rollback: true, rollback: true,
}); });
var record = this.model.get(this.handle); this.model.updateRecordContext(this.handle, {
this.trigger_up("update_quick_record", { need_notify: false,
id: record.id, modified: false,
}); });
this.getParent().destroy(); this.trigger_up("block_card", {status: true});
this.trigger_up("modify_quick_record", {
id: this.handle,
});
this.trigger_up("update_quick_record", {
id: this.handle,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
return true;
},
/**
* @private
*/
_disableQuickCreate: function() {
// Ensures that the record won't be created twice
this.$el.addClass("o_disabled");
this.$("input:not(:disabled),button:not(:disabled)")
.addClass("o_temporarily_disabled")
.attr("disabled", "disabled");
},
/**
* @private
*/
_enableQuickCreate: function() {
// Allows to create again
this.$el.removeClass("o_disabled");
this.$("input.o_temporarily_disabled,button.o_temporarily_disabled")
.removeClass("o_temporarily_disabled")
.attr("disabled", false);
}, },
} }
); );
@ -222,4 +253,5 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
ProductPickerQuickModifPriceFormController: ProductPickerQuickModifPriceFormController, ProductPickerQuickModifPriceFormController: ProductPickerQuickModifPriceFormController,
ProductPickerQuickModifPriceFormView: ProductPickerQuickModifPriceFormView, ProductPickerQuickModifPriceFormView: ProductPickerQuickModifPriceFormView,
}; };
}); }
);

View File

@ -1,32 +1,28 @@
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", function ( odoo.define(
require "web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
) { function(require) {
"use strict"; "use strict";
var core = require("web.core"); var core = require("web.core");
var BasicRenderer = require("web.BasicRenderer"); var BasicRenderer = require("web.BasicRenderer");
var One2ManyProductPickerRecord = require( var One2ManyProductPickerRecord = require("web_widget_one2many_product_picker.One2ManyProductPickerRecord");
"web_widget_one2many_product_picker.One2ManyProductPickerRecord");
var ProductPickerQuickCreateForm = require(
"web_widget_one2many_product_picker.ProductPickerQuickCreateForm");
var qweb = core.qweb; var qweb = core.qweb;
/* This is the renderer of the main widget */ /* This is the renderer of the main widget */
var One2ManyProductPickerRenderer = BasicRenderer.extend({ var One2ManyProductPickerRenderer = BasicRenderer.extend({
className: 'oe_one2many_product_picker_view', className: "oe_one2many_product_picker_view",
events: { events: {
'click #productPickerLoadMore': '_onClickLoadMore', "click #productPickerLoadMore": "_onClickLoadMore",
}, },
custom_events: { custom_events: {
'record_flip': '_onRecordFlip', record_flip: "_onRecordFlip",
}, },
DELAY_GET_RECORDS: 150, _instant_search_onchange_delay: 250,
MIN_PERC_GET_RECORDS: 0.9,
/** /**
* @override * @override
@ -35,16 +31,14 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.widgets = []; this.widgets = [];
this.recordOptions = _.extend({}, params.record_options, { this.recordOptions = _.extend({}, params.record_options, {
viewType: 'One2ManyProductPicker', viewType: "One2ManyProductPicker",
}); });
// Workaround: Odoo initilize this class so we need do this to // Workaround: Odoo initilize this class so we need do this to
// 'receive' more arguments. // 'receive' more arguments.
this.options = parent.options; this.options = parent.options;
this.mode = parent.mode; this.mode = parent.mode;
this.search_data = parent._searchRecords;
this.search_group = parent._activeSearchGroup; this.search_group = parent._activeSearchGroup;
this.last_search_data_count = parent._lastSearchRecordsCount;
}, },
/** /**
@ -52,7 +46,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
*/ */
on_attach_callback: function() { on_attach_callback: function() {
this._isInDom = true; this._isInDom = true;
_.invoke(this.widgets, 'on_attach_callback'); _.invoke(_.compact(this.widgets), "on_attach_callback");
}, },
/** /**
@ -60,7 +54,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
*/ */
on_detach_callback: function() { on_detach_callback: function() {
this._isInDom = false; this._isInDom = false;
_.invoke(this.widgets, 'on_detach_callback'); _.invoke(_.compact(this.widgets), "on_detach_callback");
}, },
/** /**
@ -71,14 +65,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
}, },
/** /**
* @param {Object} search_data * @param {Object} search_group
*/ */
updateSearchData: function (search_data, count, search_group) { updateSearchGroup: function(search_group) {
this.search_data = search_data;
this.last_search_data_count = count;
this.search_group = search_group; this.search_group = search_group;
this._loadMoreWorking = false;
this.$btnLoadMore.attr("disabled", false);
}, },
/** /**
@ -107,208 +97,368 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
}); });
}, },
/** canBeUpdated: function() {
* Recreate the given widget by the state id var model = this.getParent().getBasicFieldParams().model;
* for (var widget of this.widgets) {
* @param {String} state_id if (!widget.state) {
* @param {Object} new_state return false;
*/ }
updateRecord: function (state_id, new_state) { var record = model.localData[widget.state.id];
for (var eb = this.widgets.length-1; eb>=0; --eb) { if (record.context.in_timeout) {
var widget = this.widgets[eb]; return false;
if (widget.state.id === state_id) {
widget.recreate(new_state);
break;
} }
} }
return true;
}, },
/** /**
* Because this widget doesn't support comments/sections line types
* we need check if the line is valid to be shown.
*
* @private * @private
* @param {Array[Object]} states * @param {Object} state
* @returns {Boolean}
*/
_isValidLineState: function(state) {
return (
state &&
state.data[this.options.field_map.product] &&
state.data[this.options.field_map.product].data &&
typeof state.data[this.options.field_map.product].data.id !==
"undefined"
);
},
getProductIdFromState: function(state) {
return (
state &&
state.data[this.options.field_map.product] &&
state.data[this.options.field_map.product].data &&
state.data[this.options.field_map.product].data.id
);
},
getWidgetsByProduct: function(product_id) {
var self = this;
return _.filter(this.widgets, function(item) {
return (
self.getProductIdFromState(item.state) === product_id ||
item.recordSearch.id === product_id
);
});
},
getWidgetsWithoutOnchange: function(product_id) {
var model = this.getParent().getBasicFieldParams().model;
return _.filter(this.widgets, function(item) {
return (
model.localData[item.state.id] && model.localData[item.state.id].context.not_onchange
);
});
},
/**
* When destroy states we need check if pure virtual records
* are affected to recreate a new one because this widget can't
* remove pure virtual records.
*
* @private
* @param {Array} states
* @returns {Array}
*/
_processStatesToDestroy: function(old_states) {
var self = this;
// States to remove
// In 12.0, Odoo generates new ids for the states, so
// all states will be removed and restored because it's
// not possible identify a record without this id
var to_destroy_ids = [];
for (var index in old_states) {
var old_state = old_states[index];
if (!this._isValidLineState(old_state)) {
continue;
}
var in_current_state = _.some(this.state.data, function(state) {
return (
self._isValidLineState(state) && (state.id === old_state.id)
);
});
if (!in_current_state) {
to_destroy_ids.push(old_state.id);
}
}
var model = this.getParent().getBasicFieldParams().model;
var to_destroy = [];
for (var widget of this.widgets) {
if (!widget) {
continue;
}
// Verify that doesn't exists any dead widget
// This is necessary beceause auto-save uses
// ADD + SAVE that generates two different
// state ids
var state_has_onchange =
widget.state && !widget.state.context.not_onchange;
var state_has_modified =
widget.state && !widget.state.context.modified;
if (
!state_has_modified &&
state_has_onchange &&
!model.isPureVirtual(widget.state.id)
) {
to_destroy.push(widget);
continue;
}
for (var index_destroy in to_destroy_ids) {
const state_id = to_destroy_ids[index_destroy];
if (widget.state.id === state_id) {
to_destroy.push(widget);
break;
}
}
}
return to_destroy;
},
/**
* We need check current states to ensure that doesn't exists duplications,
* update the existing and add the new ones.
*
* @private
* @returns {Array}
*/
_processCurrentStates: function(old_states) {
var to_destroy = this._processStatesToDestroy(old_states);
// Records to Update or Create
var model = this.getParent().getBasicFieldParams().model;
var to_add = [];
for (var index in this.state.data) {
var state = this.state.data[index];
if (!this._isValidLineState(state)) {
continue;
}
var exists = false;
var search_record_index = false;
var search_record = false;
for (var widget of this.widgets) {
if (!widget) {
// Already processed widget (deleted)
continue;
}
var record = model.get(widget.state.id);
// Re-use widgets is possible
var is_to_destroy = _.findIndex(to_destroy, widget) >= 0;
var is_widget_usable =
widget.state.id === state.id || widget.recordSearch.id ===
state.data[this.options.field_map.product].data.id;
if (is_widget_usable) {
if (is_to_destroy) {
to_destroy = _.without(to_destroy, widget);
}
if (record) {
model.updateRecordContext(state.id, {
lazy_qty: record.context.lazy_qty || 0,
saving: record.context.saving || false,
need_notify: record.context.need_notify || false,
need_save: record.context.need_save || false,
});
}
// Ensure use the updated state
widget.recreate(state);
exists = true;
break;
} else if (
widget.state &&
!model.isPureVirtual(widget.state.id) &&
widget.recordSearch.id ===
state.data[this.options.field_map.product].data.id
) {
search_record = widget.recordSearch;
const in_search_records = _.some(
this.search_records,
function(item) {
return item.id === search_record.id;
}
);
if (in_search_records) {
// Is a new record (can be other record for the same 'search record')
search_record_index = widget.state.id;
}
}
}
// Add to create the new record
if (!exists && search_record_index) {
var new_search_record = _.extend({}, search_record, {
__id: state.id,
});
to_add.push([
[new_search_record],
{
no_attach_widgets: true,
no_process_records: true,
position: search_record_index,
},
]);
}
}
return [to_add.reverse(), to_destroy];
},
/**
* This method checks and appends the missing
* 'pure virtual' records
*
* @returns {Deferred} * @returns {Deferred}
*/ */
_removeRecords: function (states, new_states) { checkVirtualRecords: function() {
var defs = []; if (this.search_group.name === "main_lines") {
var to_destroy = []; return $.when();
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) {
to_destroy.push(widget);
delete this.widgets[e];
} }
var tasks = [];
var to_add = this._processVirtualRecords();
for (var params of to_add) {
tasks.push(this.appendSearchRecords.apply(this, params)[0]);
} }
return $.when(tasks);
},
/**
* This method checks the current widgets to generate the
* missing 'pure virtual' record objects.
*
* @returns {Array}
*/
_processVirtualRecords: function() {
var model = this.getParent().getBasicFieldParams().model;
var products_done = [];
var to_add = [];
for (var search_record of this.search_records) {
var widgets = this.getWidgetsByProduct(search_record.id);
if (_.isEmpty(widgets)) {
to_add.push([
[_.omit(search_record, "__id")],
{
no_attach_widgets: true,
no_process_records: true,
},
]);
continue;
} }
// If doesn't exists other records with the same product, we need // Only add 'pure virtual' records if don't have a line
// create a 'pure virtual' record again. var existing_widgets = _.filter(widgets, function(widget) {
for (var index_destroy in to_destroy) { return !widget.isMarkedToDestroy();
var widget_destroyed = to_destroy[index_destroy]; });
var widget_product_id = widget_destroyed.state var need_virtual = !_.some(existing_widgets, function(widget) {
.data[this.options.field_map.product].data.id; return widget.state && !model.isPureVirtual(widget.state.id);
var found = false; });
// If already exists a widget for the product don't try create a new one
for (var eb = this.widgets.length-1; eb>=0; --eb) {
var widget = this.widgets[eb];
if ( if (
widget && need_virtual &&
widget.state && products_done.indexOf(search_record.id) === -1
widget.state.data[this.options.field_map.product].data.id === widget_product_id
) { ) {
found = true; var has_virtual = _.some(existing_widgets, function(widget) {
break; return (
} !widget.state ||
} (widget.state && model.isPureVirtual(widget.state.id))
if (!found) {
// Get the new state ID if exists to link it with the new record
var new_state_id = undefined;
for (var eb = new_states.length-1; eb>=0; --eb) {
var state = new_states[eb];
if (
state.data[this.options.field_map.product].data.id === widget_product_id
) {
new_state_id = state.id;
break;
}
}
var search_record = _.find(this.search_data, {id: widget_product_id});
var new_search_record = _.extend({}, search_record, {__id: new_state_id});
var search_record_index = widget_destroyed.$el.index();
defs.push(
this.appendSearchRecords(
[new_search_record],
false,
true,
search_record_index
)[0]
); );
});
if (!has_virtual) {
var search_record_index = _.max(widgets, function(widget) {
return widget.$el.index();
}).state.id;
to_add.push([
[_.omit(search_record, "__id")],
{
no_attach_widgets: true,
no_process_records: true,
position: search_record_index,
},
]);
products_done.push(search_record.id);
} }
} }
}
_.invoke(to_destroy, "destroy"); return to_add;
return $.when(defs);
}, },
/** /**
* When the state change this method tries to update current records, delete * When the state change this method tries to update current records, delete
* or update them. * or update them.
* Thanks to this we don't need re-render 'pure virtual' records. * Thanks to this we don't need re-render 'pure virtual' records.
* NOTE: The first load of the records don't trigger this method.
* *
* @private * @private
* @param {Object} old_states * @param {Object} old_states
* @returns {Deferred} * @returns {Deferred}
*/ */
_updateStateRecords: function(old_states) { _updateStateRecords: function(old_states) {
var self = this;
// States to remove var record_defs = this._processCurrentStates(old_states);
var states_to_destroy = []; var to_add_current = record_defs[0];
for (var index in old_states) { var to_destroy = record_defs[1];
var old_state = old_states[index]; _.invoke(to_destroy, "markToDestroy");
var found = false; var currentTasks = [];
for (var e in this.state.data) { for (var params of to_add_current) {
var current_state = this.state.data[e]; currentTasks.push(this.appendSearchRecords.apply(this, params)[0]);
if (current_state.id === old_state.id) {
found = true;
break;
}
}
if (!found) {
states_to_destroy.push(old_state);
}
} }
this.state.data = _.compact(this.state.data); return $.when(currentTasks)
this._removeRecords(states_to_destroy, this.state.data); .then(function() {
return self.checkVirtualRecords();
// Records to Update or Create })
var defs = []; .then(function() {
var to_destroy = []; var widgets_to_destroy = _.filter(self.widgets, function(
for (var index in this.state.data) { widget
var state = this.state.data[index];
var exists = false;
var search_record_index = -1;
var search_record = false;
for (var e = this.widgets.length-1; e>=0; --e) {
var widget = this.widgets[e];
if (!widget || !widget.state) {
// Already processed widget (deleted)
continue;
}
if (widget.state.id === state.id) {
widget.recreate(state);
exists = true;
break;
} else if (
widget.recordSearch.id === state.data[this.options.field_map.product].data.id
) { ) {
return widget.isMarkedToDestroy();
// Is a new record (a replace for pure virtual)
search_record_index = widget.$el.index();
search_record = widget.recordSearch;
var model = this.getParent().getBasicFieldParams().model;
var record = model.get(widget.state.id);
model.updateRecordContext(state.id, {
lazy_qty: record.context.lazy_qty || 0,
}); });
} self.widgets = _.difference(self.widgets, widgets_to_destroy);
_.invoke(widgets_to_destroy, "destroy");
return true;
});
},
// Remove "pure virtual" records that have the same product that the new record clearRecords: function() {
if ( _.invoke(_.compact(this.widgets), "destroy");
widget.is_virtual && this.widgets = [];
widget.state.data[this.options.field_map.product].data.id === state.data[this.options.field_map.product].data.id if (this.$recordsContainer) {
) { this.$recordsContainer.empty();
to_destroy.push(widget);
delete this.widgets[e];
} }
}
this.state.data = _.compact(this.state.data);
// Need add a new one?
if (!exists && search_record_index !== -1) {
var new_search_record = _.extend({}, search_record, {__id: state.id});
defs.push(this.appendSearchRecords([new_search_record], false, true, search_record_index)[0]);
}
}
_.invoke(to_destroy, "destroy");
return $.when(defs);
}, },
/** /**
* @override * @override
*/ */
_renderView: function() { _renderView: function() {
var self = this; _.invoke(_.compact(this.widgets), "destroy");
var oldWidgets = _.compact(this.widgets);
this.widgets = []; this.widgets = [];
this.$recordsContainer = $("<DIV/>", { this.$recordsContainer = $("<DIV/>", {
class: "w-100 row", class: "w-100 row",
}); });
this.$extraButtonsContainer = $(qweb.render("One2ManyProductPicker.ExtraButtons")); this.$extraButtonsContainer = $(
this.$btnLoadMore = this.$extraButtonsContainer.find("#productPickerLoadMore"); qweb.render("One2ManyProductPicker.ExtraButtons")
this.search_data = this._sort_search_data(this.search_data); );
return $.Deferred(function (d) { this.$btnLoadMore = this.$extraButtonsContainer.find(
var defs = self.appendSearchRecords(self.search_data, true); "#productPickerLoadMore"
defs[0].then(function () { );
_.invoke(oldWidgets, "destroy"); // This.search_data = this._sort_search_data(this.search_data);
self.$el.empty(); this.$el.empty();
self.$el.append(self.$recordsContainer); this.$el.append(this.$recordsContainer);
self.$el.append(self.$extraButtonsContainer); this.$el.append(this.$extraButtonsContainer);
self.showLoadMore(self.last_search_data_count >= self.options.records_per_page); // This.showLoadMore(
if (self._isInDom) { // this.last_search_data_count >= this.options.records_per_page
_.invoke(self.widgets, "on_attach_callback"); // );
} return this._super.apply(this, arguments);
d.resolve(defs[1]);
});
});
}, },
/** /**
* @private
* @param {Array} datas * @param {Array} datas
* @returns {Array} * @returns {Array}
*/ */
@ -320,14 +470,21 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
for (var index_state in this.state.data) { for (var index_state in this.state.data) {
var state_data = this.state.data[index_state]; var state_data = this.state.data[index_state];
if (state_data.data[field_name].res_id === data.id) { if (
this._isValidLineState(state_data) &&
state_data.data[field_name].res_id === data.id
) {
data._order_value = state_data.res_id; data._order_value = state_data.res_id;
} }
} }
} }
var sorted_datas = _.chain(datas).sortBy('_order_value').map(function(item) { var sorted_datas = _.chain(datas)
return _.omit(item, '_order_value'); .sortBy("_order_value")
}).value().reverse(); .map(function(item) {
return _.omit(item, "_order_value");
})
.value()
.reverse();
return sorted_datas; return sorted_datas;
} }
return datas; return datas;
@ -338,37 +495,86 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
* Link a current state with the 'search record'. * Link a current state with the 'search record'.
* *
* @private * @private
* @param {Array[Object]} results * @param {Array} results
* @returns {Array[Object]} * @returns {Array}
*/ */
_processSearchRecords: function(results) { _processSearchRecords: function(results) {
var model = this.getParent().getBasicFieldParams().model;
var field_name = this.options.field_map.product; var field_name = this.options.field_map.product;
var records = []; var records = [];
var states = [];
var test_values = function(field_value, record_search) {
return (
(typeof field_value === "object" &&
field_value.data.id === record_search.id) ||
field_value === record_search.id
);
};
for (var index in results) { for (var index in results) {
var record_search = results[index]; var record_search = results[index];
var state_data_found = false;
var widget_created = false;
for (var index_data in this.state.data) { for (var index_data in this.state.data) {
var state_record = this.state.data[index_data]; var state_record = this.state.data[index_data];
var field = state_record.data[field_name]; var field_value = state_record.data[field_name];
if ( if (
(typeof field === "object" && field.data.id === record_search.id) || !this._isValidLineState(state_record) ||
field === record_search.id !test_values(field_value, record_search)
) { ) {
continue;
}
widget_created = true;
// At this point the result has a state (line)
// Search if already exists a widget using the state
var widget = _.find(this.widgets, function(widget) {
return (
!widget.isMarkedToDestroy() &&
widget.state &&
widget.state.id === state_record.id
);
});
if (widget) {
// Don't need create a new widget (record)
states.push(widget.state);
} else {
// Need create a new widget linked with the state
records.push( records.push(
_.extend({}, record_search, { _.extend({}, record_search, {
__id: state_record.id, __id: state_record.id,
}) })
); );
state_data_found = true; states.push(state_record);
} }
} }
if (!state_data_found) { if (widget_created) {
records.push(record_search); continue;
}
var widgets = this.getWidgetsByProduct(record_search.id);
// Only can exists 'pure virtual' if no 'lines' assigned
if (_.isEmpty(widgets)) {
var has_virtual = _.some(widgets, function(widget) {
return (
!widget.isMarkedToDestroy() &&
(!widget.state ||
(widget.state &&
model.isPureVirtual(widget.state.id)))
);
});
if (!has_virtual) {
// The result need a 'pure virtual' record
records.push(_.omit(record_search, "__id"));
}
} }
} }
return records; return {
records: records,
states: states,
};
}, },
/** /**
@ -410,15 +616,19 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
* Generates the 'Product Card' per record. * Generates the 'Product Card' per record.
* *
* @private * @private
* @param {Array[Object]} search_records * @param {Array} search_records
* @param {Boolean} no_process_records * @param {Object} options
* @param {Number} position * @returns {Array}
*/ */
_appendSearchRecords: function (search_records, no_process_records, position) { _appendSearchRecords: function(search_records, options) {
var self = this; var self = this;
var processed_records = var processed_info =
no_process_records?search_records:this._processSearchRecords(search_records); !options.no_process_records &&
_.each(processed_records, function (search_record) { this._processSearchRecords(search_records);
var records_to_add =
(processed_info && processed_info.records) || search_records;
_.each(records_to_add, function(search_record) {
// Get record state (if can)
var state_data = self._getRecordDataById(search_record.__id); var state_data = self._getRecordDataById(search_record.__id);
var widget_options = self._getRecordOptions(search_record); var widget_options = self._getRecordOptions(search_record);
widget_options.renderer_widget_index = self.widgets.length; widget_options.renderer_widget_index = self.widgets.length;
@ -432,26 +642,31 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
// Simulate new lines to dispatch get_default & onchange's to get the // Simulate new lines to dispatch get_default & onchange's to get the
// relevant data to print. This case increase the TTI time. // relevant data to print. This case increase the TTI time.
if (!state_data) { if (!state_data) {
var defVirtualState = ProductPickerRecord.generateVirtualState(); var defVirtualState = ProductPickerRecord.generateVirtualState({
if (defVirtualState.state() === "pending") { onchange_delay: self.options.instant_search
? self._instant_search_onchange_delay
: 0,
});
self.defsVirtualState.push(defVirtualState); self.defsVirtualState.push(defVirtualState);
} }
}
// At this point the widget will use the existing state (line) or // At this point the widget will use the existing state (line) or
// the search data. Using search data instead of waiting for // a simple state data. Using simple state data instead of waiting for
// simulated state gives a low FCP time. // complete state (default + onchange) gives a low FCP time.
var def = ProductPickerRecord.appendTo(self.$recordsContainer) var def = ProductPickerRecord.appendTo(self.$recordsContainer).then(
.then(function (widget, widget_position) { function(widget, widget_position) {
if (typeof widget_position !== "undefined") { if (typeof widget_position !== "undefined") {
var $elm = this.$el.find("> div > div:nth("+widget_position+")"); var $elm = self.$el.find(
`[data-card-id="${widget_position}"]:first`
);
widget.$el.insertAfter($elm); widget.$el.insertAfter($elm);
} }
}.bind(self, ProductPickerRecord, position)); }.bind(self, ProductPickerRecord, options.position)
if (def.state() === "pending") { );
self.defs.push(def); self.defs.push(def);
}
}); });
return records_to_add;
}, },
/** /**
@ -464,31 +679,84 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
/** /**
* Append search records to the view * Append search records to the view
* *
* @param {Array[Object]} search_records * @param {Array} search_records
* @param {Boolean} no_attach_widgets * @param {Object} options
* @param {Boolean} no_process_records * @returns {Array}
* @param {Number} position
* @returns {Array[Deferred]}
*/ */
appendSearchRecords: function (search_records, no_attach_widgets, no_process_records, position) { appendSearchRecords: function(search_records, options = {}) {
var self = this; var self = this;
if (options.clear) {
this.clearRecords();
}
this.trigger_up("loading_records"); this.trigger_up("loading_records");
this.defs = []; this.defs = [];
this.defsVirtualState = []; this.defsVirtualState = [];
var cur_widget_index = this.widgets.length; var cur_widget_index = this.widgets.length;
this._appendSearchRecords(search_records, no_process_records, position); this._appendSearchRecords(search_records, options);
var defs = this.defs; var defs = this.defs;
delete this.defs; delete this.defs;
var defsVirtualState = this.defsVirtualState; var defsVirtualState = this.defsVirtualState;
delete this.defsVirtualState; delete this.defsVirtualState;
return [ return [
$.when.apply($, defs).then(function () { $.when(defs).then(function() {
if (!no_attach_widgets && self._isInDom) { if (!options.no_attach_widgets && self._isInDom) {
var new_widgets = self.widgets.slice(cur_widget_index); var new_widgets = self.widgets.slice(cur_widget_index);
_.invoke(new_widgets, "on_attach_callback"); _.invoke(new_widgets, "on_attach_callback");
} }
// Destroy unused
if (options.cleanup) {
self.search_records = _.compact(search_records);
var widgets_to_destroy = _.filter(self.widgets, function(
widget
) {
return (
widget.isMarkedToDestroy() ||
!_.some(self.search_records, function(
search_record
) {
return (
search_record.id ===
widget.recordSearch.id &&
!_.some(self.widgets, function(
comp_widget
) {
return (
comp_widget !== widget &&
comp_widget.state &&
comp_widget.recordSearch.id ===
widget.recordSearch.id
);
})
);
})
);
});
_.invoke(widgets_to_destroy, "destroy");
self.widgets = _.difference(
self.widgets,
widgets_to_destroy
);
} else {
self.search_records = self.search_records || [];
for (var search_record of search_records) {
var has_search_record = _.some(
self.search_records,
function(item) {
return (
item.id === search_record.id &&
item.__id === search_record.__id
);
}
);
if (!has_search_record) {
self.search_records.push(search_record);
}
}
}
return true;
}), }),
$.when.apply($, defsVirtualState).then(function () { $.when(defsVirtualState).then(function() {
self.trigger_up("loading_records", {finished: true}); self.trigger_up("loading_records", {finished: true});
}), }),
]; ];
@ -500,7 +768,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
_onClickLoadMore: function() { _onClickLoadMore: function() {
this.$btnLoadMore.attr("disabled", true); this.$btnLoadMore.attr("disabled", true);
this.trigger_up("load_more"); this.trigger_up("load_more");
this._loadMoreWorking = true;
}, },
/** /**
@ -513,19 +780,18 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
var $actived_card = this.$el.find(".active"); var $actived_card = this.$el.find(".active");
if (widget.$card.hasClass("active")) { if (widget.$card.hasClass("active")) {
widget.$card.removeClass("active"); widget.$card.removeClass("active");
widget.$card.find('.oe_flip_card_front').removeClass("d-none"); widget.$card.find(".oe_flip_card_front").removeClass("d-none");
} else { } else {
var self = widget;
widget.defs = []; widget.defs = [];
widget._processWidgetFields(widget.$back); widget._processWidgetFields(widget.$back);
widget._processWidgets(widget.$back); widget._processWidgets(widget.$back);
widget._processDynamicFields(); widget._processDynamicFields();
$.when(widget.defs).then(function() { $.when(widget.defs).then(function() {
$actived_card.removeClass("active"); $actived_card.removeClass("active");
$actived_card.find('.oe_flip_card_front').removeClass("d-none"); $actived_card.find(".oe_flip_card_front").removeClass("d-none");
self.$card.addClass("active"); widget.$card.addClass("active");
setTimeout(function() { setTimeout(function() {
self.$('.oe_flip_card_front').addClass("d-none"); widget.$(".oe_flip_card_front").addClass("d-none");
}, 200); }, 200);
}); });
} }
@ -535,22 +801,33 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
* Handle card flip. * Handle card flip.
* Used to create/update the record * Used to create/update the record
* *
* @private
* @param {CustomEvent} evt * @param {CustomEvent} evt
*/ */
_onRecordFlip: function(evt) { _onRecordFlip: function(evt) {
var prev_widget_index = evt.data.prev_widget_index; var prev_widget_index = evt.data.prev_widget_index;
if (typeof prev_widget_index !== "undefined") { if (
typeof prev_widget_index !== "undefined" &&
this.widgets[prev_widget_index]
) {
// Only check 'back' widgets so there is where the form was created // Only check 'back' widgets so there is where the form was created
for (var index in this.widgets[prev_widget_index].widgets.back) { for (var index in this.widgets[prev_widget_index].widgets.back) {
var widget = this.widgets[prev_widget_index].widgets.back[index]; var widget = this.widgets[prev_widget_index].widgets.back[
if (widget instanceof ProductPickerQuickCreateForm) { index
];
if (
widget.controller &&
widget.className ===
"oe_one2many_product_picker_quick_create"
) {
widget.controller.auto(); widget.controller.auto();
} }
} }
this.widgets[prev_widget_index].recreate();
} }
} },
}); });
return One2ManyProductPickerRenderer; return One2ManyProductPickerRenderer;
}); }
);

View File

@ -1,33 +0,0 @@
// Copyright 2021 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.BasicController", function(require) {
"use strict";
const BasicController = require("web.BasicController");
BasicController.include({
/**
* This is necessary to refresh 'one2many_product_picker' when some 'trigger_refresh_fields' fields changes.
*
* @override
*/
_confirmChange: function(id, fields, e) {
id = id || this.handle;
var self = this;
return this._super.apply(this, arguments).then(function () {
if (self.renderer && !_.isEmpty(self.renderer.allFieldWidgets)) {
var product_picker_widgets = _.filter(
self.renderer.allFieldWidgets[id],
item => item.attrs.widget === "one2many_product_picker"
);
_.invoke(
product_picker_widgets,
"onDocumentConfirmChanges",
fields,
e
);
}
});
},
});
});

View File

@ -4,22 +4,50 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
"use strict"; "use strict";
var BasicModel = require("web.BasicModel"); var BasicModel = require("web.BasicModel");
var FieldOne2ManyProductPicker = require("web_widget_one2many_product_picker.FieldOne2ManyProductPicker");
BasicModel.include({ BasicModel.include({
/** /**
* @param {Number/String} handle * @override
* @param {Object} context
*/ */
updateRecordContext: function (handle, context) { init: function() {
this.localData[handle].context = _.extend( this._super.apply(this, arguments);
{},
this.localData[handle].context,
context);
}, },
/** /**
* @param {Number/String} id * This is necessary because 'pure virtual' records
* can be destroyed at any time.
*
* @param {String} id
* @returns {Boolean}
*/
exists: function(id) {
return !_.isEmpty(this.localData[id]);
},
/**
* @param {String} id
* @param {Object} context
*/
updateRecordContext: function(id, context) {
this.localData[id].context = _.extend(
{},
this.localData[id].context,
context
);
},
/**
* @param {String} id
* @returns {Boolean}
*/
isSaving: function(id) {
var data = this.localData[id];
return data._virtual || false;
},
/**
* @param {String} id
* @returns {Boolean} * @returns {Boolean}
*/ */
isPureVirtual: function(id) { isPureVirtual: function(id) {
@ -28,7 +56,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
}, },
/** /**
* @param {Number/String} id * @param {String} id
* @param {Boolean} status * @param {Boolean} status
*/ */
setPureVirtual: function(id, status) { setPureVirtual: function(id, status) {
@ -41,7 +69,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
}, },
/** /**
* @param {Number/String} id * @param {String} id
*/ */
unsetDirty: function(id) { unsetDirty: function(id) {
var data = this.localData[id]; var data = this.localData[id];
@ -51,26 +79,217 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
}); });
}, },
/**
* 'Pure virtual' records are not used by other
* elements so can be removed safesly
*
* @param {String} id
* @returns {Boolean}
*/
removeVirtualRecord: function(id) { removeVirtualRecord: function(id) {
if (!this.isPureVirtual(id)) { if (!this.isPureVirtual(id)) {
return false; return false;
} }
const data = this.localData[id]; var data = this.localData[id];
const to_remove = []; var to_remove = [];
this._visitChildren(data, (item) => { this._visitChildren(data, function(item) {
to_remove.push(item.id); to_remove.push(item.id);
}); });
to_remove.reverse(); to_remove.reverse();
for (let remove_id of to_remove) { for (var remove_id of to_remove) {
this.removeLine(remove_id); this.removeLine(remove_id);
delete this.localData[remove_id]; delete this.localData[remove_id];
} }
return true;
}, },
/** /**
* Generates a virtual records without link it * This is a cloned method from Odoo framework.
* Virtual records are processed in two parts,
* this is the second part and here we trigger onchange
* process.
*
* @param {Object} record
* @param {Object} params
* @returns {Deferred}
*/
_makeDefaultRecordNoDatapoint: function(record, params) {
var self = this;
var targetView = params.viewType;
var fieldsInfo = params.fieldsInfo;
var fieldNames = Object.keys(fieldsInfo[targetView]);
var fields_key = _.without(fieldNames, "__last_update");
// Fields that are present in the originating view, that need to be initialized
// Hence preventing their value to crash when getting back to the originating view
var parentRecord =
params.parentID && this.localData[params.parentID].type === "list"
? this.localData[params.parentID]
: null;
if (parentRecord && parentRecord.viewType in parentRecord.fieldsInfo) {
var originView = parentRecord.viewType;
fieldNames = _.union(
fieldNames,
Object.keys(parentRecord.fieldsInfo[originView])
);
}
return this._rpc(
{
model: record.model,
method: "default_get",
args: [fields_key],
context: params.context,
},
{
shadow: true,
}
).then(function(result) {
// Interrupt point (used in instant search)
if (!self.exists(record.id)) {
return $.Deferred().reject();
}
// We want to overwrite the default value of the handle field (if any),
// in order for new lines to be added at the correct position.
// -> This is a rare case where the defaul_get from the server
// will be ignored by the view for a certain field (usually "sequence").
var overrideDefaultFields = self._computeOverrideDefaultFields(
params.parentID,
params.position
);
if (overrideDefaultFields) {
result[overrideDefaultFields.field] = overrideDefaultFields.value;
}
return self
.applyDefaultValues(record.id, result, {fieldNames: fieldNames})
.then(function() {
if (!self.exists(record.id)) {
return $.Deferred().reject();
}
var def = $.Deferred();
// Interrupt point (used in instant search)
if (!self.exists(record.id)) {
return $.Deferred().reject();
}
var always = function() {
if (record._warning) {
if (params.allowWarning) {
delete record._warning;
} else {
def.reject();
}
}
def.resolve();
};
self._performOnChange(record, fields_key)
.then(always)
.always(always);
return def;
})
.then(function() {
if (!self.exists(record.id)) {
return $.Deferred().reject();
}
return self._fetchRelationalData(record);
})
.then(function() {
if (!self.exists(record.id)) {
return $.Deferred().reject();
}
return self._postprocess(record);
})
.then(function() {
if (!self.exists(record.id)) {
return $.Deferred().reject();
}
// Save initial changes, so they can be restored later,
// if we need to discard.
self.save(record.id, {savePoint: true});
return record.id;
});
});
},
/**
* Virtual records are processed in two parts,
* this is the first part and here we create
* the state (without aditional process)
*
* @param {String} listID
* @param {Object} options
* @returns {Object}
*/
createVirtualDatapoint: function(listID, options) {
var list = this.localData[listID];
var context = _.extend({}, this._getContext(list), options.context);
var position = options ? options.position : "top";
var params = {
context: context,
fields: list.fields,
fieldsInfo: list.fieldsInfo,
parentID: list.id,
position: position,
viewType: list.viewType,
allowWarning: true,
doNotSetDirty: true,
postonchange_values: options.onchange_values,
};
var targetView = params.viewType;
var fields = params.fields;
var fieldsInfo = params.fieldsInfo;
// Fields that are present in the originating view, that need to be initialized
// Hence preventing their value to crash when getting back to the originating view
var parentRecord =
params.parentID && this.localData[params.parentID].type === "list"
? this.localData[params.parentID]
: null;
if (parentRecord && parentRecord.viewType in parentRecord.fieldsInfo) {
var originView = parentRecord.viewType;
fieldsInfo[targetView] = _.defaults(
{},
fieldsInfo[targetView],
parentRecord.fieldsInfo[originView]
);
fields = _.defaults({}, fields, parentRecord.fields);
}
var record = this._makeDataPoint({
modelName: list.model,
fields: fields,
fieldsInfo: fieldsInfo,
context: params.context,
parentID: params.parentID,
res_ids: params.res_ids,
viewType: targetView,
});
this.setPureVirtual(record.id, true);
this.updateRecordContext(record.id, {
ignore_warning: true,
not_onchange: true, // To know is the record has the initial onchange applied
shadow: true, // To avoid show the loading backdrop
});
return {
record: record,
params: params,
};
},
/**
* Generates a virtual records without hard-link to any model.
* *
* @param {Integer/String} listID * @param {Integer/String} listID
* @param {Object} options * @param {Object} options
@ -79,9 +298,13 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
createVirtualRecord: function(listID, options) { createVirtualRecord: function(listID, options) {
var self = this; var self = this;
var list = this.localData[listID]; var list = this.localData[listID];
var context = _.extend({}, this._getContext(list), options.context); var context = _.extend(
{shadow: true},
this._getContext(list),
options.context
);
var position = options?options.position:'top'; var position = options ? options.position : "top";
var params = { var params = {
context: context, context: context,
fields: list.fields, fields: list.fields,
@ -93,48 +316,276 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
doNotSetDirty: true, doNotSetDirty: true,
}; };
return $.Deferred(function (d) { return this._makeDefaultRecord(list.model, params).then(function(recordID) {
self._makeDefaultRecord(list.model, params)
.then(function (recordID) {
self.setPureVirtual(recordID, true); self.setPureVirtual(recordID, true);
self.updateRecordContext(recordID, {ignore_warning: true}); self.updateRecordContext(recordID, {
if (options.data) { ignore_warning: true,
self._applyChange( not_onchange: true,
recordID, shadow: true,
options.data,
params
).then(function () {
d.resolve(self.get(recordID));
});
} else {
d.resolve(self.get(recordID));
}
}); });
return {
record: self.get(recordID),
params: params,
};
}); });
}, },
/** /**
* Adds support to avoid show onchange warnings. * Adds support to avoid show onchange warnings.
* The implementation is a pure hack that clone * The implementation is a pure hack that clone
* the context and do a monkey path the * the context and do a monkey patch to the
* 'trigger_up' method. * 'trigger_up' method.
* *
* @override * @override
*/ */
_performOnChange: function (record, fields, viewType) { _performOnChange: function(record) {
if (record.context && record.context.ignore_warning) { if (record && record.context) {
record.context.not_onchange = false;
var this_mp = _.clone(this); var this_mp = _.clone(this);
var super_call = this.trigger_up; if (record.context.shadow) {
this_mp.trigger_up = function (event_name, data) { // Force use 'shadow'
if (event_name === 'warning' && data.type === "dialog") { var super__rpc_call = this._rpc;
return; // Do nothing this_mp._rpc = function(params, options) {
} options = options || {};
return super_call.apply(this, arguments); options.shadow = true;
return super__rpc_call.call(this, params, options);
}.bind(this); }.bind(this);
}
if (record.context.ignore_warning) {
var super_trigger_up_call = this.trigger_up;
// Avoid show warnings
this_mp.trigger_up = function(event_name, data) {
if (event_name === "warning" && data.type === "dialog") {
// Do nothing
return;
}
return super_trigger_up_call.apply(this, arguments);
}.bind(this);
}
return this._super.apply(this_mp, arguments); return this._super.apply(this_mp, arguments);
} }
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
});
/**
* Because records can be removed at any time we
* need check if the record still existing.
* Necessary for 'instant search' feature.
*
* @override
*/
_applyOnChange: function(values, record) {
if (!this.exists(record.id)) {
return $.Deferred().reject();
}
return this._super.apply(this, arguments);
},
/**
* Allow add multiple records at the same time
*
* @override
*/
_applyX2ManyChange: function(record, fieldName, command) {
if (command.operation === "ADD_MULTIPLE") {
var self = this;
var localID =
(record._changes && record._changes[fieldName]) ||
record.data[fieldName];
var list = this.localData[localID];
list._changes = list._changes || [];
// For now, we are in the context of a one2many field
// the command should look like this:
// { operation: 'ADD', ids: [id0,id1,id2,...] }
// The corresponding record may contain value for fields that
// are unknown in the list (e.g. fields that are in the
// subrecord form view but not in the kanban or list view), so
// to ensure that onchanges are correctly handled, we extend the
// list's fields with those in the created record
_.each(command.ids, function(id) {
var newRecord = self.localData[id];
_.defaults(list.fields, newRecord.fields);
_.defaults(list.fieldsInfo, newRecord.fieldsInfo);
newRecord.fields = list.fields;
newRecord.fieldsInfo = list.fieldsInfo;
newRecord.viewType = list.viewType;
list._cache[newRecord.res_id] = newRecord.id;
list._changes.push(command);
});
}
return this._super.apply(this, arguments);
},
/**
* This is necessary to avoid calculate onchanges that
* affects to order_line.
*
* @override
*/
_buildOnchangeSpecs: function(record) {
var specs = this._super.apply(this, arguments);
// This is necessary to improve the performance
if (record.model === "sale.order" && specs) {
// Its a change from product picker?
// WORKAROUND: Done in this way to reutilice odoo methods
var need_clean = false;
var parent_controller = this.getParent();
if (
parent_controller &&
parent_controller.renderer &&
!_.isEmpty(parent_controller.renderer.allFieldWidgets)
) {
var order_line_widget = _.find(
parent_controller.renderer.allFieldWidgets[
parent_controller.handle
],
{name: "order_line"}
);
need_clean =
order_line_widget instanceof FieldOne2ManyProductPicker &&
!_.isEmpty(this.localData[record.data.order_line]);
}
if (need_clean) {
var new_specs = _.clone(specs);
for (var key in specs) {
if (key.startsWith("order_line.")) {
delete new_specs[key];
}
}
return new_specs;
}
}
return specs;
},
/**
* @param {String} recordID
* @returns {Boolean}
*/
hasChanges: function(recordID) {
var record = this.localData[recordID];
return record && !_.isEmpty(record._changes);
},
/**
* @param {Object} model_fields
* @param {String} model
* @param {String} search_val
* @param {Array} domain
* @param {Array} fields
* @param {Object} orderby
* @param {String} operator
* @param {Number} limit
* @param {Number} offset
* @param {Object} context
* @returns {Deferred}
*/
fetchNameSearchFull: function(
model_fields,
model,
search_val,
domain,
fields,
orderby,
operator,
limit,
offset,
context
) {
var self = this;
return this._rpc({
model: model,
method: "name_search",
kwargs: {
name: search_val,
args: domain || [],
operator: operator || "ilike",
limit: this.limit,
context: context || {},
},
}).then(function(results) {
var record_ids = results.map(function(item) {
return item[0];
});
return self.fetchGenericRecords(
model_fields,
model,
[["id", "in", record_ids]],
fields,
orderby,
limit,
offset,
context
);
});
},
/**
* @param {Object} model_fields
* @param {String} model
* @param {Array} domain
* @param {Array} fields
* @param {Array} orderby
* @param {Number} limit
* @param {Number} offset
* @param {Object} context
* @returns {Deferred}
*/
fetchGenericRecords: function(
model_fields,
model,
domain,
fields,
orderby,
limit,
offset,
context
) {
var self = this;
return this._rpc({
model: model,
method: "search_read",
fields: fields,
domain: domain,
limit: limit,
offset: offset,
orderBy: orderby,
kwargs: {context: context},
}).then(function(result) {
for (var index in result) {
var record = result[index];
for (var fieldName in record) {
var field = model_fields[fieldName];
if (field.type !== "many2one") {
record[fieldName] = self._parseServerValue(
model_fields[fieldName],
record[fieldName]
);
}
}
}
return result;
});
},
fetchModelFieldsInfo: function(model) {
return this._rpc({
model: model,
method: "fields_get",
args: [
false,
[
"store",
"searchable",
"type",
"string",
"relation",
"selection",
"related",
],
],
context: this.getSession().user_context,
});
},
});
}); });

View File

@ -0,0 +1,56 @@
// Copyright 2021 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.FormController", function(require) {
"use strict";
var FormController = require("web.FormController");
FormController.include({
custom_events: _.extend({}, FormController.prototype.custom_events, {
using_product_picker: '_onUsingProductPicker',
}),
/**
* Disable product picker while saving
*
* @override
*/
saveRecord: function() {
var self = this;
this.renderer.invokeProductPicker(this.handle, "onDocumentSave", true);
return this._super.apply(this, arguments).always(function(changedFields) {
self.renderer.invokeProductPicker(self.handle, "onDocumentSave", false);
return changedFields;
});
},
/**
* This is necessary to refresh 'one2many_product_picker' when some 'trigger_refresh_fields' fields changes.
*
* @override
*/
_confirmChange: function(id, fields, e) {
var self = this;
id = id || this.handle;
return this._super.apply(this, arguments).then(function(resetWidgets) {
if (self.renderer) {
self.renderer.invokeProductPicker(id, "onDocumentConfirmChanges", fields, e);
}
return resetWidgets;
});
},
/**
* @private
* @param {CustomEvent} ev
*/
_onUsingProductPicker: function(ev) {
this.model.updateRecordContext(this.handle, {
product_picker_field: ev.data.field,
product_picker_product_field: ev.data.product_field,
product_picker_relation: ev.data.relation,
product_picker_relation_field: ev.data.relation_field,
});
}
});
});

View File

@ -0,0 +1,31 @@
/* 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.FormRenderer", function(require) {
"use strict";
var FormRenderer = require("web.FormRenderer");
FormRenderer.include({
/**
* Invoke the selected method on all product picer defined
*/
invokeProductPicker: function(recordID, method_name, ...params) {
if (_.isEmpty(this.allFieldWidgets)) {
return;
}
var product_picker_widgets = _.filter(
this.allFieldWidgets[recordID],
function(item) {
return item.attrs.widget === "one2many_product_picker";
}
);
_.invoke(
product_picker_widgets,
method_name,
...params
);
},
});
});

View File

@ -1,28 +1,26 @@
/* global py */ /* global py */
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.BasicView", function (require) { odoo.define("web_widget_one2many_product_picker.FormView", function(require) {
"use strict"; "use strict";
var core = require("web.core"); var core = require("web.core");
var pyUtils = require("web.py_utils"); var pyUtils = require("web.py_utils");
var BasicView = require("web.BasicView"); var FormView = require("web.FormView");
var _t = core._t; var _t = core._t;
// Add ref to _() -> _t() call // Add ref to _() -> _t() call
var PY_t = new py.PY_def.fromJSON(function() { var PY_t = new py.PY_def.fromJSON(function() {
var args = py.PY_parseArgs(arguments, ['str']); var args = py.PY_parseArgs(arguments, ["str"]);
return py.str.fromJSON(_t(args.str.toJSON())); return py.str.fromJSON(_t(args.str.toJSON()));
}); });
BasicView.include({ FormView.include({
/** /**
* @override * @override
*/ */
_processField: function(viewType, field, attrs) { _processField: function(viewType, field, attrs) {
/** /**
* We need process 'options' attribute to handle translations and * We need process 'options' attribute to handle translations and
* special replacements * special replacements
@ -31,15 +29,16 @@ odoo.define("web_widget_one2many_product_picker.BasicView", function (require) {
attrs.widget === "one2many_product_picker" && attrs.widget === "one2many_product_picker" &&
!_.isObject(attrs.options) !_.isObject(attrs.options)
) { ) {
attrs.options = attrs.options ? pyUtils.py_eval(attrs.options, { attrs.options = attrs.options
? pyUtils.py_eval(attrs.options, {
_: PY_t, _: PY_t,
// Hack: This allow use $number_search out of an string // Hack: This allow use $number_search out of an string
number_search: '$number_search', number_search: "$number_search",
}) : {}; })
: {};
} }
return this._super(viewType, field, attrs); return this._super(viewType, field, attrs);
}, },
}); });
}); });

View File

@ -1,4 +1,21 @@
.oe_field_one2many_product_picker { .oe_field_one2many_product_picker {
/** FIX AUTO-SCROLL ON SAMSUNG DEVICES **/
* {
overflow-anchor: none !important;
scroll-snap-stop: normal !important;
overscroll-behavior: unset !important;
scroll-behavior: unset !important;
}
.container, header, footer, .container-fluid, body, div, span, section {
overflow-anchor: none !important;
scroll-snap-stop: normal !important;
overscroll-behavior: unset !important;
scroll-behavior: unset !important;
}
/** END FIX **/
&.oe_field_one2many_product_picker_maximized { &.oe_field_one2many_product_picker_maximized {
position: fixed; position: fixed;
top: 0; top: 0;
@ -15,6 +32,17 @@
} }
} }
&.disabled {
.badge {
filter: opacity(0.6);
}
pointer-events: none;
> div.loading {
display: inline-block;
}
}
> div { > div {
width: unset !important; width: unset !important;
} }
@ -30,6 +58,19 @@
} }
} }
div.loading {
position: absolute;
background-color: white;
padding: 0.5em;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 2px solid gray;
z-index: 2;
box-shadow: 2px 2px 5px;
display: none;
}
.o_cp_buttons { .o_cp_buttons {
width: 100%; width: 100%;
@ -45,7 +86,10 @@
} }
.oe_one2many_product_picker_view { .oe_one2many_product_picker_view {
@include make-grid-columns($columns: 24, $breakpoints: $one2many-product-picker-grid-breakpoints); @include make-grid-columns(
$columns: 24,
$breakpoints: $one2many-product-picker-grid-breakpoints
);
overflow: auto; overflow: auto;
@ -54,16 +98,32 @@
margin-right: 0; margin-right: 0;
} }
.o_catch_attention {
animation: none;
outline: 1px solid fade-in(theme-color("warning"), 0.5);
}
.oe_flip_card { .oe_flip_card {
user-select: none; user-select: none;
background-color: transparent; background-color: transparent;
perspective: 1000px; perspective: 1000px;
transition: top $one2many-product-picker-transition-3d-time, left $one2many-product-picker-transition-3d-time, width $one2many-product-picker-transition-3d-time, height $one2many-product-picker-transition-3d-time; transition: top $one2many-product-picker-transition-3d-time,
left $one2many-product-picker-transition-3d-time,
width $one2many-product-picker-transition-3d-time,
height $one2many-product-picker-transition-3d-time;
height: $one2many-product-picker-card-min-height; height: $one2many-product-picker-card-min-height;
&.blocked {
.badge {
filter: opacity(0.7);
}
div.loading {
display: inline-block;
}
}
&.disabled { &.disabled {
filter: grayscale(100%); filter: grayscale(1)
opacity: 0.5;
} }
&.oe_flip_card_maximized { &.oe_flip_card_maximized {
@ -77,11 +137,6 @@
.oe_flip_card_inner { .oe_flip_card_inner {
height: 100% !important; height: 100% !important;
box-shadow: 0px 0px 15px; box-shadow: 0px 0px 15px;
.img-fluid {
transform: translateY(-50%) !important;
top: 50%;
position: relative;
}
.oe_one2many_product_picker_title { .oe_one2many_product_picker_title {
font-size: 1.95rem !important; font-size: 1.95rem !important;
} }
@ -93,11 +148,13 @@
} }
} }
.o_field_widget, .oe_one2many_product_picker_form_buttons .btn { .o_field_widget,
.oe_one2many_product_picker_form_buttons .btn {
transform: scale($one2many-product-picker-zoom-scale); transform: scale($one2many-product-picker-zoom-scale);
margin-bottom: 1.3em !important; margin-bottom: 1.3em !important;
} }
.o_field_widget, .w-100 { .o_field_widget,
.w-100 {
width: 100% / $one2many-product-picker-zoom-scale !important; width: 100% / $one2many-product-picker-zoom-scale !important;
} }
} }
@ -129,9 +186,19 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: $one2many-product-picker-card-min-height; height: $one2many-product-picker-card-min-height;
transition: transform $one2many-product-picker-transition-3d-time, height $one2many-product-picker-transition-3d-time/2 ease-in-out $one2many-product-picker-transition-3d-time/2; transition: transform $one2many-product-picker-transition-3d-time,
height $one2many-product-picker-transition-3d-time/2 ease-in-out
$one2many-product-picker-transition-3d-time/2;
transform-style: preserve-3d; transform-style: preserve-3d;
.img-fluid {
transform: translate(-50%, -50%) !important;
top: 50%;
left: 50%;
z-index: -1;
position: absolute;
}
.position-absolute { .position-absolute {
z-index: 1; z-index: 1;
} }
@ -140,8 +207,19 @@
font-size: 1rem; font-size: 1rem;
} }
.indicator_zones {
display: inline-flex;
flex-direction: column;
max-width: 50%;
align-items: flex-start;
> span.badge {
font-size: 0.8rem;
}
}
.badge_price { .badge_price {
top: 50%; top: 55%;
right: -2px; right: -2px;
transform: translateY(-50%); transform: translateY(-50%);
display: grid; display: grid;
@ -180,10 +258,19 @@
.o_form_view.o_form_nosheet { .o_form_view.o_form_nosheet {
padding: $one2many-product-picker-card-form-padding; padding: $one2many-product-picker-card-form-padding;
.o_field_widget .o_input_dropdown > input { .o_field_widget {
&:not(.widget_numeric_step) {
max-width: 95%;
}
.o_input_dropdown > input {
height: unset; height: unset;
} }
} }
.btn.w-100 {
max-width: 95%;
}
}
} }
.oe_flip_card_front { .oe_flip_card_front {
@ -201,6 +288,17 @@
top: 50%; top: 50%;
left: 0; left: 0;
transform: translateY(-50%); transform: translateY(-50%);
width: 100%;
.oe_one2many_product_picker_form_buttons {
display: flex;
padding: 0 3px;
justify-content: center;
.oe_record_remove {
flex-grow: 1;
}
}
} }
} }
@ -213,25 +311,46 @@
font-size: 0.95rem; font-size: 0.95rem;
z-index: 0; z-index: 0;
} }
.add_product, .product_qty, .price_unit { .add_product,
.product_qty,
.price_unit {
cursor: pointer; cursor: pointer;
} }
} }
} }
}
.oe_product_picker_quick_modif_price { /** Extra tools **/
width: 80%; .icon-waiting {
max-width: $one2many-product-picker-quick-modif-price-max-width;
margin: auto;
position: absolute; position: absolute;
top: 50%;
left: 50%; left: 50%;
top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
border: 1px solid $border-color; }
z-index: 55;
.oe_one2many_product_picker_form_buttons { /* clears the 'X' from Internet Explorer */
text-align: center; input[type="search"]::-ms-clear {
display: none;
width: 0;
height: 0;
}
input[type="search"]::-ms-reveal {
display: none;
width: 0;
height: 0;
}
/* clears the 'X' from Chrome */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
display: none;
}
/** Modal Price **/
.oe_product_picker_quick_modif_price {
.modal-body {
min-height: 7em;
}
} }
} }

View File

@ -1,58 +1,81 @@
<template> <template>
<t t-name="One2ManyProductPicker.ControlPanelButtons"> <t t-name="One2ManyProductPicker.ControlPanelButtons">
<div class="text-center mx-auto"> <div class="text-center mx-auto">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<t t-if="search_category_names"> <t t-if="search_category_names">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button
class="btn btn-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<i class="fa fa-search" /> <i class="fa fa-search" />
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
<t t-foreach="search_category_names" t-as="name"> <t t-foreach="search_category_names" t-as="name">
<a t-attf-class="dropdown-item search_mode_option {{name_index == search_mode ? 'active' : ''}}" href="#" t-esc="name" /> <a
t-attf-class="dropdown-item search_mode_option {{name_index == search_mode ? 'active' : ''}}"
href="#"
t-esc="name"
/>
</t> </t>
</div> </div>
</t> </t>
<t t-else=""> <t t-else="">
<button type="button" t-attf-class="btn btn-secondary btn-lg input-group-button"> <button
type="button"
t-attf-class="btn btn-secondary btn-lg input-group-button"
>
<i class="fa fa-search" /> <i class="fa fa-search" />
</button> </button>
</t> </t>
</div> </div>
<input type="search" class="form-control form-control-lg oe_search_input" placeholder="Search..." aria-label="Search..." aria-describedby="btnGroupAddon2" /> <input
type="search"
class="form-control form-control-lg oe_search_input"
placeholder="Search..."
aria-label="Search..."
aria-describedby="btnGroupAddon2"
/>
<div class="input-group-append"> <div class="input-group-append">
<button type="button" t-attf-class="btn btn-secondary btn-lg input-group-button oe_search_erase"> <button
<i class="fa fa-eraser" /> id="product_picker_clear_input"
class='o_fullscreen btn btn-secondary'
>
<i class='fa fa-eraser' />
</button>
<button
id="product_picker_maximize"
class='o_fullscreen btn btn-primary'
>
<i class='fa fa-expand' />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</t> </t>
<t t-name="One2ManyProductPicker.ControlPanelGroupButtons"> <t t-name="One2ManyProductPicker.ControlPanelGroupButtons">
<div class="oe_one2many_product_picker_groups"> <div class="oe_one2many_product_picker_groups">
<div class="btn-group" role="group" aria-label="Groups"> <div class="btn-group" role="group" aria-label="Groups">
<t t-foreach="groups" t-as="group"> <t t-foreach="groups" t-as="group">
<button type="button" t-att-name="group.name" t-attf-class="btn btn-lg btn-secondary rounded-0 border-top-0 {{group.active &amp;&amp; 'active' || ''}} oe_btn_search_group" t-att-data-group="group_index"> <button
type="button"
t-att-name="group.name"
t-attf-class="btn btn-lg btn-secondary rounded-0 border-top-0 {{group.active &amp;&amp; 'active' || ''}} oe_btn_search_group"
t-att-data-group="group_index"
>
<t t-esc="group.string" /> <t t-esc="group.string" />
</button> </button>
</t> </t>
</div> </div>
<button type="button" class="btn btn-light btn-lg oe_btn_lines"> <button type="button" class="btn btn-light btn-lg oe_btn_lines">
Lines Lines
<span class="ml-1 badge badge-light">0</span> <span class="ml-1 badge badge-light">0</span>
</button> </button>
</div> </div>
</t> </t>
<t t-name="One2ManyProductPicker.ButtonMaximize">
<button if="product_picker_maximize" class='o_fullscreen btn btn-primary position-absolute border border-dark py-1 px-2'>
<i class='fa fa-expand' />
</button>
</t>
<t t-name="One2ManyProductPicker.Total"> <t t-name="One2ManyProductPicker.Total">
<div id="product_picker_total" class="text-right"> <div id="product_picker_total" class="text-right">
<h2> <h2>
@ -61,62 +84,163 @@
</h2> </h2>
</div> </div>
</t> </t>
<t t-name="One2ManyProductPicker.ExtraButtons"> <t t-name="One2ManyProductPicker.ExtraButtons">
<div class="w-100 row"> <div class="w-100 row">
<button id="productPickerLoadMore" class="btn btn-lg btn-secondary m-auto d-none">Load More</button> <button
id="productPickerLoadMore"
class="btn btn-lg btn-secondary m-auto d-none"
>Load More</button>
</div> </div>
</t> </t>
<t t-name="One2ManyProductPicker.ActionButton">
<t t-name="One2ManyProductPicker.FlipCard"> <div class="safezone d-inline-block float-left m-0 pb-2 pr-2 text-left">
<div class="oe_flip_container p-1 col-12 col-sm-8 col-md-6 col-lg-4 col-xl-3 col-xxl-2"> <t t-if="need_notify || need_save || is_saving">
<div t-attf-class="oe_flip_card {{!state &amp;&amp; 'disabled' || (auto_save &amp;&amp; (!is_virtual || is_saving) &amp;&amp; !state.data.id &amp;&amp; 'blocked') || ''}}"> <span
<div class="oe_flip_card_inner text-center"> class="badge record_saving badge-warning font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"
<div t-attf-class="oe_flip_card_front p-0 {{((modified || is_saving) &amp;&amp; 'border-warning') || (state &amp;&amp; !is_virtual &amp;&amp; 'border-success') || ''}}"> ><span class="lazy_product_qty" t-esc="lazy_qty || '1'" /> x <t
<t t-if="state"> t-esc="state.data[field_map[field_uom]].data.display_name"
<t t-if="!is_virtual"> /></span>
<div class="safezone position-absolute m-0 pb-2 pr-2 text-left">
<span t-att-data-field="field_map.product_uom_qty" t-attf-data-esc="str({{field_map.product_uom_qty}}) + ' x ' + {{field_map.product_uom}}.data.display_name" t-attf-class="badge {{modified &amp;&amp; 'badge-warning' || 'badge-success'}} font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty" />
</div>
</t> </t>
<t t-elif="is_saving"> <t t-elif="!is_virtual">
<div class="safezone position-absolute m-0 pb-2 pr-2 text-left"> <span
<span class="badge record_saving badge-warning font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"><span class="lazy_product_qty">1</span> x <t t-esc="state.data[field_map.product_uom].data.display_name"/></span> t-attf-class="badge record_saving {{modified &amp;&amp; 'badge-warning' || 'badge-success'}} font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"
</div> ><span
t-att-data-field="field_map[field_uom_qty]"
t-attf-data-esc="str(floatFixed('{{field_map[field_uom_qty]}}'))"
class="lazy_product_qty"
/> x <span
t-att-data-field="field_map[field_uom]"
t-attf-data-esc="obj.{{field_map[field_uom]}}.data.display_name"
/></span>
</t> </t>
<t t-else=""> <t t-else="">
<div class="safezone position-absolute m-0 pb-2 pr-2 text-left"> <span
<span class="badge badge-primary font-weight-bold rounded-0 mt-0 px-2 py-3 add_product"><i class="fa fa-plus"></i> 1 <t t-esc="state.data[field_map.product_uom].data.display_name"/></span> class="badge badge-primary font-weight-bold rounded-0 mt-0 px-2 py-3 add_product"
><i class="fa fa-plus" /><span
class="lazy_product_qty"
t-esc="lazy_qty || '1'"
/> x <t
t-esc="state.data[field_map[field_uom]].data.display_name"
/></span>
</t>
</div> </div>
</t> </t>
<t t-name="One2ManyProductPicker.PriceZone">
<div class="position-absolute m-0 text-left badge_price"> <div class="position-absolute m-0 text-left badge_price">
<t t-if="show_discount"> <t t-if="show_discount">
<span t-att-data-field="field_map.discount" t-attf-data-esc="'-' + str({{field_map.discount}}) +'%'" class="badge badge-dark discount_price font-weight-bold rounded-0 mt-1 p-2" /> <span
<span t-att-data-field="field_map.price_unit" t-attf-data-esc="'{{monetary('price_unit',true)}}'" class="badge font-weight-bold rounded-0 original_price" /> t-att-data-field="field_map.discount"
<span class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2" /> t-attf-data-esc="str(obj.{{field_map.discount}} * -1.0) +'%'"
class="badge badge-dark discount_price font-weight-bold rounded-0 mt-1 p-2"
/>
<span
t-att-data-field="field_map.price_unit"
data-esc="str(monetary('price_unit'))"
class="badge font-weight-bold rounded-0 original_price"
/>
<span
t-if="has_onchange"
class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2"
/>
</t> </t>
<t t-else=""> <t t-else="has_onchange">
<span t-att-data-field="field_map.price_unit" t-attf-data-esc="'{{monetary('price_unit',true)}}'" class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2" /> <span
t-att-data-field="field_map.price_unit"
data-esc="str(monetary('price_unit'))"
class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2"
/>
</t> </t>
</div> </div>
<span data-field="display_name" class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1" data-esc="display_name" /> </t>
<img alt="" class="img img-fluid" t-att-src="image(state.data[field_map.product].data.id,'image_variant_medium')" t-att-data-src-alt="image(state.data[field_map.product].data.id,'image_variant_big')" /> <t t-name="One2ManyProductPicker.FlipCard.Front">
<div
t-attf-class="oe_flip_card_front p-0 {{((need_notify || need_save || modified || is_saving) &amp;&amp; 'border-warning') || (state &amp;&amp; !is_virtual &amp;&amp; 'border-success') || ''}}"
>
<t t-if="state">
<div class="indicator_zones float-left">
<t t-call="One2ManyProductPicker.ActionButton" />
<t t-call="One2ManyProductPicker.PriceZone" />
</div>
<span
data-field="display_name"
class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1"
data-esc="obj.display_name"
/>
<img
alt=""
class="img img-fluid"
t-att-src="image(state.data[field_map.product].data.id,'image_medium')"
t-att-data-src-alt="image(state.data[field_map.product].data.id,'image')"
/>
</t> </t>
<t t-else=""> <t t-else="">
<span class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1" t-esc="record_search.display_name" /> <span
<img alt="" class="img img-fluid" t-att-src="image(record_search.id,'image_variant_medium')" t-att-data-src-alt="image(record_search.id,'image_variant_big')" /> class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1"
t-esc="record_search.display_name"
/>
<img
alt=""
class="img img-fluid"
t-att-src="image(record_search.id,'image_medium')"
t-att-data-src-alt="image(record_search.id,'image')"
/>
</t> </t>
</div> </div>
</t>
<t t-name="One2ManyProductPicker.FlipCard.Back">
<div class="oe_flip_card_back"> <div class="oe_flip_card_back">
<widget name="product_picker_quick_create_form" t-att-compare-key="field_map.product_uom" /> <span class="icon-waiting">
<i class="fa fa-cog fa-spin fa-3x fa-fw" />
</span>
<widget
name="product_picker_quick_create_form"
t-att-compare-key="field_map.product_uom"
/>
</div>
</t>
<t t-name="One2ManyProductPicker.FlipCard">
<div
class="oe_flip_container p-1 col-12 col-sm-6 col-md-4 col-lg-4 col-xl-4 col-xxl-2"
t-att-data-card-id="state &amp;&amp; state.id || record_search.id"
>
<div t-attf-class="oe_flip_card {{!state &amp;&amp; 'disabled' || ''}}">
<div class="loading">LOADING...</div>
<div class="oe_flip_card_inner text-center">
<t t-call="One2ManyProductPicker.FlipCard.Front" />
<t t-call="One2ManyProductPicker.FlipCard.Back" />
</div>
</div>
</div>
</t>
<t t-name="One2ManyProductPicker.QuickModifPrice.Modal">
<div
class="oe_product_picker_quick_modif_price modal fade"
id="One2ManyProductPickerQuickModifPrice"
tabindex="-1"
role="dialog"
aria-labelledby="One2ManyProductPickerQuickModifPriceLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body p-0">
<span class="icon-waiting">
<i class="fa fa-cog fa-spin fa-3x fa-fw" />
</span>
</div>
<div class="modal-footer">
<button class="btn btn-success oe_record_change mr-auto d-none">
<i class="fa fa-check" />
</button>
<button
class="btn btn-warning oe_record_discard"
data-dismiss="modal"
>
<i class="fa fa-times" />
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</t> </t>
<t t-name="One2ManyProductPicker.QuickModifPricePopup">
<div class="oe_product_picker_quick_modif_price shadow" />
</t>
</template> </template>

View File

@ -1,18 +1,25 @@
<template> <template>
<t t-name="One2ManyProductPicker.QuickCreate.FormButtons"> <t t-name="One2ManyProductPicker.QuickCreate.FormButtons">
<div class="oe_one2many_product_picker_form_buttons"> <div class="oe_one2many_product_picker_form_buttons">
<t t-if="state == 'new'"> <t t-if="state == 'new'">
<button t-attf-class="btn btn-primary oe_record_add">Add</button> <button t-attf-class="btn btn-primary oe_record_add w-100">Add</button>
</t> </t>
<t t-elif="state == 'dirty'"> <t t-elif="state == 'dirty'">
<button class="btn btn-success oe_record_change mr-2"><i class="fa fa-check" /></button> <button class="btn btn-success oe_record_change w-50">
<button class="btn btn-warning oe_record_discard ml-2"><i class="fa fa-times" /></button> <i class="fa fa-check" /> Accept
</button>
<button class="btn btn-warning oe_record_discard ml-1 w-50">
<i class="fa fa-times" /> Discard
</button>
</t> </t>
<t t-else=""> <t t-else="">
<button class="btn btn-danger oe_record_remove w-100"><i class="fa fa-trash"/> Remove</button> <button class="btn btn-danger oe_record_remove w-50"><i
class="fa fa-trash"
/> Remove</button>
<button class="btn btn-warning oe_record_discard ml-1 w-50">
<i class="fa fa-times" /> Discard
</button>
</t> </t>
</div> </div>
</t> </t>
</template> </template>

View File

@ -1,14 +1,8 @@
<template> <template>
<t t-name="One2ManyProductPicker.QuickModifPrice.Price">
<t t-name="One2ManyProductPicker.QuickModifPrice.FormButtons"> <div class="text-left">
<div class="oe_one2many_product_picker_form_buttons"> <strong>Price</strong>
<button class="btn btn-success oe_record_change mr-2"><i class="fa fa-check" /></button> <div class="oe_price" />
<button class="btn btn-warning oe_record_discard ml-2"><i class="fa fa-times" /></button>
</div> </div>
</t> </t>
<t t-name="One2ManyProductPicker.QuickModifPrice.Price">
<div class="text-left"><strong>Price:</strong> <div class="oe_price" /></div>
</t>
</template> </template>

View File

@ -1,19 +1,20 @@
/* global QUnit */ /* global QUnit */
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define('web_widget_one2many_product_picker.widget_tests', function (require) { odoo.define("web_widget_one2many_product_picker.widget_tests", function(require) {
"use strict"; "use strict";
var FormView = require('web.FormView'); var FormView = require("web.FormView");
var testUtils = require('web.test_utils'); var testUtils = require("web.test_utils");
var createView = testUtils.createView; var createView = testUtils.createView;
var getArch = function() { var getArch = function() {
return '<form>' + return (
"<form>" +
'<field name="currency_id" invisible="1" />' + '<field name="currency_id" invisible="1" />' +
'<field name="line_ids" widget="one2many_product_picker" options="{\'groups\': [{\'name\': \'Desk\', \'domain\': [(\'name\', \'ilike\', \'%desk%\')], \'order\': {\'name\': \'id\', \'asc\': true}}, {\'name\': \'Chairs\', \'domain\': [(\'name\', \'ilike\', \'%chair%\')]}]}">' + "<field name=\"line_ids\" widget=\"one2many_product_picker\" options=\"{'groups': [{'name': 'Desk', 'domain': [('name', 'ilike', '%desk%')], 'order': {'name': 'id', 'asc': true}}, {'name': 'Chairs', 'domain': [('name', 'ilike', '%chair%')]}]}\">" +
'<kanban>' + "<kanban>" +
'<field name="name" />' + '<field name="name" />' +
'<field name="product_id" />' + '<field name="product_id" />' +
'<field name="price_reduce" />' + '<field name="price_reduce" />' +
@ -21,37 +22,87 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require
'<field name="foo_id" />' + '<field name="foo_id" />' +
'<field name="product_uom_qty" />' + '<field name="product_uom_qty" />' +
'<field name="product_uom" />' + '<field name="product_uom" />' +
'</kanban>' + "</kanban>" +
'</field>' + "</field>" +
'</form>'; "</form>"
);
}; };
QUnit.module('Web Widget One2Many Product Picker', { QUnit.module(
"Web Widget One2Many Product Picker",
{
beforeEach: function() { beforeEach: function() {
this.data = { this.data = {
foo: { foo: {
fields: { fields: {
currency_id: {string: "Currency", type: "many2one", relation: "currency"}, currency_id: {
line_ids: {string: "Lines Test", type: "one2many", relation: "line", relation_field: "foo_id"}, string: "Currency",
type: "many2one",
relation: "currency",
},
line_ids: {
string: "Lines Test",
type: "one2many",
relation: "line",
relation_field: "foo_id",
},
display_name: {string: "Display Name", type: "char"}, display_name: {string: "Display Name", type: "char"},
}, },
records: [ 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: { line: {
fields: { fields: {
name: {string: "Product Name", type: "string"}, name: {string: "Product Name", type: "string"},
product_id: {string: "Product", type: "many2one", relation: "product"}, product_id: {
product_uom: {string: "UoM", type: "many2one", relation: "uom"}, string: "Product",
type: "many2one",
relation: "product",
},
product_uom: {
string: "UoM",
type: "many2one",
relation: "uom",
},
product_uom_qty: {string: "Qty", type: "integer"}, product_uom_qty: {string: "Qty", type: "integer"},
price_unit: {string: "Product Price", type: "float"}, price_unit: {string: "Product Price", type: "float"},
price_reduce: {string: "Product Price Reduce", type: "float"}, price_reduce: {
foo_id: {string: "Parent", type: "many2one", relation: "foo"}, string: "Product Price Reduce",
type: "float",
},
foo_id: {
string: "Parent",
type: "many2one",
relation: "foo",
},
}, },
records: [ 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}, id: 1,
name: "Large Cabinet",
product_id: 1,
product_uom: 1,
product_uom_qty: 3,
price_unit: 9.99,
price_reduce: 9.0,
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.0,
foo_id: 1,
},
], ],
}, },
product: { product: {
@ -60,47 +111,60 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require
display_name: {string: "Display Name", type: "char"}, display_name: {string: "Display Name", type: "char"},
list_price: {string: "Price", type: "float"}, list_price: {string: "Price", type: "float"},
image_medium: {string: "Image Medium", type: "binary"}, image_medium: {string: "Image Medium", type: "binary"},
uom_category_id: {string: "Category", type: "many2one", relation: "uom_category"}, uom_category_id: {
string: "Category",
type: "many2one",
relation: "uom_category",
},
}, },
records: [ records: [
{id: 1, name: "Large Cabinet", display_name: "Large Cabinet", list_price: 9.99, image_medium: "", uom_category_id: 1}, {
{id: 2, name: "Cabinet with Doors", display_name: "Cabinet with Doors", list_price: 42.0, image_medium: "", uom_category_id: 1}, id: 1,
name: "Large Cabinet",
display_name: "Large Cabinet",
list_price: 9.99,
image_medium: "",
uom_category_id: 1,
},
{
id: 2,
name: "Cabinet with Doors",
display_name: "Cabinet with Doors",
list_price: 42.0,
image_medium: "",
uom_category_id: 1,
},
], ],
}, },
uom_category: { uom_category: {
fields: { fields: {
display_name: {string: "Display Name", type: "char"}, display_name: {string: "Display Name", type: "char"},
}, },
records: [ records: [{id: 1, display_name: "Unit(s)"}],
{id: 1, display_name: "Unit(s)"},
],
}, },
uom: { uom: {
fields: { fields: {
name: {string: "Name", type: "char"}, name: {string: "Name", type: "char"},
}, },
records: [ records: [{id: 1, name: "Unit(s)"}],
{id: 1, name: "Unit(s)"},
],
}, },
currency: { currency: {
fields: { fields: {
name: {string: "Name", type: "char"}, name: {string: "Name", type: "char"},
symbol: {string: "Symbol", type: "char"}, symbol: {string: "Symbol", type: "char"},
}, },
records: [ records: [{id: 1, name: "Eur", symbol: "€"}],
{id: 1, name: "Eur", symbol: "€"},
],
}, },
}; };
}, },
}, function () { },
QUnit.test('Load widget', function (assert) { function() {
QUnit.test("Load widget", function(assert) {
assert.expect(4); assert.expect(4);
var form = createView({ var form = createView({
View: FormView, View: FormView,
model: 'foo', model: "foo",
data: this.data, data: this.data,
arch: getArch(), arch: getArch(),
res_id: 1, res_id: 1,
@ -109,28 +173,56 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require
index: 0, index: 0,
}, },
mockRPC: function(route, args) { mockRPC: function(route, args) {
if (route === '/web/dataset/call_kw/foo/read') { if (route === "/web/dataset/call_kw/foo/read") {
assert.deepEqual(args.args[1], ['currency_id', 'line_ids', 'display_name'], assert.deepEqual(
'should only read "currency_id", "line_ids" and "display_name"'); args.args[1],
["currency_id", "line_ids", "display_name"],
'should only read "currency_id", "line_ids" and "display_name"'
);
return $.when(this.data.foo.records); return $.when(this.data.foo.records);
} else if (route === '/web/dataset/call_kw/line/read') { } else if (route === "/web/dataset/call_kw/line/read") {
assert.deepEqual(args.args[1], ['name', 'product_id', 'price_reduce', 'price_unit', 'foo_id', 'product_uom_qty', 'product_uom'], assert.deepEqual(
'should only read "name", "product_id", "price_reduce", "price_unit", "foo_id", "product_uom_qty" and "product_uom"'); args.args[1],
[
"name",
"product_id",
"price_reduce",
"price_unit",
"foo_id",
"product_uom_qty",
"product_uom",
],
'should only read "name", "product_id", "price_reduce", "price_unit", "foo_id", "product_uom_qty" and "product_uom"'
);
return $.when(this.data.line.records); return $.when(this.data.line.records);
} else if (route === '/web/dataset/call_kw/product/search_read') { } else if (
assert.deepEqual(args.kwargs.fields, ['id', 'uom_id', 'display_name', 'uom_category_id', 'image_medium', 'list_price'], route === "/web/dataset/call_kw/product/search_read"
'should only read "id", "uom_id", "display_name", "uom_category_id", "image_medium" and "list_price"'); ) {
assert.deepEqual(
args.kwargs.fields,
[
"id",
"uom_id",
"display_name",
"uom_category_id",
"image_medium",
"list_price",
],
'should only read "id", "uom_id", "display_name", "uom_category_id", "image_medium" and "list_price"'
);
return $.when(this.data.product.records); return $.when(this.data.product.records);
} }
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
}); });
assert.ok(form.$('.oe_field_one2many_product_picker').is(':visible'), assert.ok(
"should have a visible one2many product picker"); form.$(".oe_field_one2many_product_picker").is(":visible"),
"should have a visible one2many product picker"
);
form.destroy(); form.destroy();
}); });
}); }
);
}); });

View File

@ -2,38 +2,85 @@
<odoo> <odoo>
<template id="_assets_backend_helpers" inherit_id="web._assets_backend_helpers"> <template id="_assets_backend_helpers" inherit_id="web._assets_backend_helpers">
<xpath expr="."> <xpath expr=".">
<link type="text/css" rel="stylesheet" <link
href="/web_widget_one2many_product_picker/static/src/scss/_variables.scss"/> type="text/css"
rel="stylesheet"
href="/web_widget_one2many_product_picker/static/src/scss/_variables.scss"
/>
</xpath> </xpath>
</template> </template>
<template id="_assets_bootstrap" inherit_id="web._assets_bootstrap"> <template id="_assets_bootstrap" inherit_id="web._assets_bootstrap">
<xpath expr="link[2]"> <xpath expr="link[2]">
<link type="text/css" rel="stylesheet" href="/web_widget_one2many_product_picker/static/src/scss/main_variables.scss"/> <link
type="text/css"
rel="stylesheet"
href="/web_widget_one2many_product_picker/static/src/scss/main_variables.scss"
/>
</xpath> </xpath>
</template> </template>
<template id="assets_backend" name="account assets" inherit_id="web.assets_backend"> <template id="assets_backend" name="account assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside"> <xpath expr="." position="inside">
<link type="text/css" rel="stylesheet" <link
href="/web_widget_one2many_product_picker/static/src/scss/one2many_product_picker.scss"/> type="text/css"
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/tools.js"></script> rel="stylesheet"
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js"></script> href="/web_widget_one2many_product_picker/static/src/scss/one2many_product_picker.scss"
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/renderer.js"></script> />
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form_view.js"></script> <script
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form.js"></script> type="text/javascript"
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form_view.js"></script> src="/web_widget_one2many_product_picker/static/src/js/tools.js"
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form.js"></script> />
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/basic_view.js"></script> <script
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/basic_model.js"></script> type="text/javascript"
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/basic_controller.js"></script> src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js"
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js"></script> />
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/renderer.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form_view.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form_view.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/basic_model.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/form_view.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/form_controller.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/form_renderer.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js"
/>
</xpath> </xpath>
</template> </template>
<template id="qunit_suite" name="base_import_tests" inherit_id="web.qunit_suite"> <template id="qunit_suite" name="base_import_tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside"> <xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/tests/widget_tests.js"></script> <script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/tests/widget_tests.js"
/>
</xpath> </xpath>
</template> </template>
</odoo> </odoo>