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;
} }
/** /**
@ -28,20 +44,17 @@ odoo.define("web_widget_one2many_product_picker.tools", function (
* @param {Object} data * @param {Object} data
* @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, data: data,
field_info, currency_field: currency_field,
{ field_digits: true,
data: data, });
currency_field: currency_field,
field_digits: true,
});
} }
return { return {
monetary: monetary, monetary: monetary,
float: float,
priceReduce: priceReduce, priceReduce: priceReduce,
}; };
}); });

View File

@ -1,6 +1,7 @@
/* 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(
require require
) { ) {
"use strict"; "use strict";
@ -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,14 +23,10 @@ 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
*/ */
init: function (parent, options) { init: function(parent, options) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.state = options.state; this.state = options.state;
this.main_state = options.main_state; this.main_state = options.main_state;
@ -50,58 +46,57 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
/** /**
* @override * @override
*/ */
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(
{},
self.main_state.getContext(),
self.nodeContext
);
_.extend(refinedContext, self.editContext);
self.formView = new ProductPickerQuickCreateFormView(fieldsView, {
context: refinedContext,
compareKey: self.compareKey,
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,
parentID: self.basicFieldParams.parentID,
default_buttons: false,
withControlPanel: false,
model: self.basicFieldParams.model,
mainRecordData: self.getParent().getParent().state,
});
return self.formView.getController(self).then(function(controller) {
self.controller = controller;
self.$el.empty();
self.controller.appendTo(self.$el);
self.trigger_up("back_form_loaded");
return controller;
});
}); });
var refinedContext = _.extend(
{},
this.main_state.getContext(),
this.nodeContext);
_.extend(refinedContext, this.editContext);
this.formView = new ProductPickerQuickCreateFormView(fieldsView, {
context: refinedContext,
compareKey: this.compareKey,
fieldMap: this.fieldMap,
modelName: this.basicFieldParams.field.relation,
userContext: this.getSession().user_context,
ids: this.res_id ? [this.res_id] : [],
currentId: this.res_id || undefined,
mode: this.res_id && this.readonly ? "readonly" : "edit",
recordID: this.id,
index: 0,
parentID: this.basicFieldParams.parentID,
default_buttons: false,
withControlPanel: false,
model: this.basicFieldParams.model,
mainRecordData: this.getParent().getParent().state,
});
// if (this.id) {
// this.basicFieldParams.model.save(this.id, {savePoint: true});
// }
var def2 = this.formView.getController(this).then(function (controller) {
self.controller = controller;
self.$el.empty();
self.controller.appendTo(self.$el);
});
return $.when(def1, def2);
}, },
on_attach_callback: function () { on_attach_callback: function() {
if (this.controller) { if (this.controller) {
this.controller.autofocus(); this.controller.autofocus();
} }
@ -111,8 +106,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
* @private * @private
* @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,54 +1,51 @@
// 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";
/** /**
* This file defines the QuickCreateFormView, an extension of the FormView that * This file defines the QuickCreateFormView, an extension of the FormView that
* is used by the RecordQuickCreate in One2ManyProductPicker views. * is used by the RecordQuickCreate in One2ManyProductPicker views.
*/ */
var QuickCreateFormView = require("web.QuickCreateFormView"); var QuickCreateFormView = require("web.QuickCreateFormView");
var BasicModel = require("web.BasicModel"); var BasicModel = require("web.BasicModel");
var core = require("web.core"); var core = require("web.core");
var qweb = core.qweb; var qweb = core.qweb;
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]; delete values[field_name];
delete values[field_name]; }
delete record.context.ignore_onchanges;
} }
delete record.context.ignore_onchanges; return this._super(values, record, viewType);
} },
return this._super(values, record, viewType); });
},
});
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",
@ -57,7 +54,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
"click .oe_record_discard": "_onClickDiscard", "click .oe_record_discard": "_onClickDiscard",
}), }),
init: function (parent, model, renderer, params) { init: function(parent, model, renderer, params) {
this.compareKey = params.compareKey; this.compareKey = params.compareKey;
this.fieldMap = params.fieldMap; this.fieldMap = params.fieldMap;
this.context = params.context; this.context = params.context;
@ -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") {
@ -117,15 +125,13 @@ 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(), })
}) );
);
if (this._disabled) { if (this._disabled) {
this._disableQuickCreate(); this._disableQuickCreate();
@ -135,7 +141,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
/** /**
* @private * @private
*/ */
_disableQuickCreate: function () { _disableQuickCreate: function() {
if (!this.$el) { if (!this.$el) {
return; return;
} }
@ -150,8 +156,7 @@ 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,10 +167,10 @@ 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) {
for (var index in fields_changed) { for (var index in fields_changed) {
var field = fields_changed[index]; var field = fields_changed[index];
if (field === this.fieldMap[this.compareKey]) { if (field === this.fieldMap[this.compareKey]) {
@ -183,169 +188,182 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private * @private
* @param {ChangeEvent} ev * @param {ChangeEvent} ev
*/ */
_onFieldChanged: function (ev) { _onFieldChanged: function(ev) {
var fields_changed = Object.keys(ev.data.changes); this._super.apply(this, arguments);
if (this._needReloadCard(fields_changed)) { if (!_.isEmpty(ev.data.changes)) {
var field = ev.data.changes[fields_changed[0]]; if (this.model.isPureVirtual(this.handle)) {
var new_value = false; this.model.unsetDirty(this.handle);
if (typeof field === "object") {
new_value = field.id;
} else {
new_value = field;
}
var reload_values = {
compareValue: new_value,
};
var record = this.model.get(this.handle);
if (!('base_record_id' in record.context)) {
var old_value = record.data[this.compareKey];
if (typeof old_value === 'object') {
old_value = old_value.data.id;
}
reload_values.baseRecordID = record.id;
reload_values.baseRecordResID = record.ref;
reload_values.baseRecordCompareValue = old_value;
} else {
reload_values.baseRecordID =
record.context.base_record_id;
reload_values.baseRecordResID =
record.context.base_record_res_id;
reload_values.baseRecordCompareValue =
record.context.base_record_compare_value;
}
this.trigger_up("reload_view", reload_values);
// Discard current change
ev.data.changes = {};
} else {
this._super.apply(this, arguments);
if (!_.isEmpty(ev.data.changes)) {
if (this.model.isPureVirtual(this.handle)) {
this.model.unsetDirty(this.handle);
}
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: false,
});
this.trigger_up("quick_record_updated", {
changes: ev.data.changes,
});
this._updateButtons();
} }
this.model.updateRecordContext(this.handle, {
has_changes_unconfirmed: true,
});
this.trigger_up("quick_record_updated", {
changes: ev.data.changes,
highlight: {qty: true},
});
} }
}, },
/** /**
* @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,
reload: true, reload: true,
savePoint: true, savePoint: true,
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,
}); });
},
_change: function () {
var self = this;
if (this._disabled) {
// Don't do anything if we are already creating a record
return $.Deferred();
}
this._disableQuickCreate();
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
var record = this.model.get(this.handle);
this.trigger_up("restore_flip_card", { this.trigger_up("restore_flip_card", {
success_callback: function () { success_callback: function() {
self.trigger_up("update_quick_record", { self.trigger_up("list_record_remove", {
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 () { _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.model.updateRecordContext(this.handle, {
need_notify: true,
modified: true,
});
this._disableQuickCreate(); this._disableQuickCreate();
var record = this.model.get(this.handle); // 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() {
self.trigger_up("update_quick_record", {
id: record.id,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
},
});
});
},
_discard: function() {
var self = this;
if (this._disabled) {
// Don't do anything if we are already creating a record
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();
// 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() {
changes: record.data, record = self.model.get(self.handle);
}); self.trigger_up("quick_record_updated", {
if (this.model.isNew(record.id)) { changes: record.data,
this.update({}, {reload: false});
this.trigger_up("restore_flip_card");
this._updateButtons();
this._enableQuickCreate();
} else {
this.update({}, {reload: false}).then(function () {
self.model.unsetDirty(self.handle);
self.trigger_up("restore_flip_card");
self._updateButtons();
self._enableQuickCreate();
}); });
} self.trigger_up("restore_flip_card", {
success_callback: function() {
self._updateButtons();
self._enableQuickCreate();
}
});
});
}, },
/** /**
* @private * @private
* @param {MouseEvent} ev * @param {MouseEvent} ev
*/ */
_onClickAdd: function (ev) { _onClickAdd: function(ev) {
ev.stopPropagation(); ev.stopPropagation();
this._add(); this._add();
}, },
@ -354,7 +372,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private * @private
* @param {MouseEvent} ev * @param {MouseEvent} ev
*/ */
_onClickRemove: function (ev) { _onClickRemove: function(ev) {
ev.stopPropagation(); ev.stopPropagation();
this._remove(); this._remove();
}, },
@ -363,7 +381,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private * @private
* @param {MouseEvent} ev * @param {MouseEvent} ev
*/ */
_onClickChange: function (ev) { _onClickChange: function(ev) {
ev.stopPropagation(); ev.stopPropagation();
this._change(); this._change();
}, },
@ -372,34 +390,35 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private * @private
* @param {MouseEvent} ev * @param {MouseEvent} ev
*/ */
_onClickDiscard: function (ev) { _onClickDiscard: function(ev) {
ev.stopPropagation(); ev.stopPropagation();
this._discard(); this._discard();
}, },
} }
); );
var ProductPickerQuickCreateFormView = QuickCreateFormView.extend({ var ProductPickerQuickCreateFormView = QuickCreateFormView.extend({
config: _.extend({}, QuickCreateFormView.prototype.config, { config: _.extend({}, QuickCreateFormView.prototype.config, {
Renderer: ProductPickerQuickCreateFormRenderer, Renderer: ProductPickerQuickCreateFormRenderer,
Controller: ProductPickerQuickCreateFormController, Controller: ProductPickerQuickCreateFormController,
}), }),
/** /**
* @override * @override
*/ */
init: function (viewInfo, params) { init: function(viewInfo, params) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.controllerParams.compareKey = params.compareKey; this.controllerParams.compareKey = params.compareKey;
this.controllerParams.fieldMap = params.fieldMap; this.controllerParams.fieldMap = params.fieldMap;
this.controllerParams.context = params.context; this.controllerParams.context = params.context;
this.controllerParams.mainRecordData = params.mainRecordData; this.controllerParams.mainRecordData = params.mainRecordData;
}, },
}); });
return { return {
ProductPickerQuickCreateFormRenderer: ProductPickerQuickCreateFormRenderer, ProductPickerQuickCreateFormRenderer: ProductPickerQuickCreateFormRenderer,
ProductPickerQuickCreateFormController: ProductPickerQuickCreateFormController, ProductPickerQuickCreateFormController: ProductPickerQuickCreateFormController,
ProductPickerQuickCreateFormView: ProductPickerQuickCreateFormView, ProductPickerQuickCreateFormView: ProductPickerQuickCreateFormView,
}; };
}); }
);

View File

@ -1,152 +1,255 @@
/* 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;
/**
* This widget render a Form. Used by FieldOne2ManyProductPicker
*/
var ProductPickerQuickModifPriceForm = Widget.extend({
className: "oe_one2many_product_picker_quick_modif_price",
xmlDependencies: [
"/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml",
],
/** /**
* @override * This widget render a Form. Used by FieldOne2ManyProductPicker
*/ */
init: function (parent, options) { var ProductPickerQuickModifPriceForm = Widget.extend({
this._super.apply(this, arguments); className: "oe_one2many_product_picker_quick_modif_price",
this.state = options.state; xmlDependencies: [
this.main_state = options.main_state; "/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml",
this.node = options.node; ],
this.fields = options.fields;
this.fieldMap = options.fieldMap;
this.searchRecord = options.searchRecord;
this.fieldsInfo = options.fieldsInfo;
this.readonly = options.readonly;
this.basicFieldParams = options.basicFieldParams;
this.canEditPrice = options.canEditPrice;
this.canEditDiscount = options.canEditDiscount;
this.currencyField = options.currencyField;
this.res_id = this.state && this.state.res_id;
this.id = this.state && this.state.id;
this.editContext = {};
},
/** events: {
* @override "click .oe_record_change": "_onClickChange",
*/ "click .oe_record_discard": "_onClickDiscard",
start: function () { },
var self = this;
var def1 = this._super.apply(this, arguments);
var fieldsView = {
arch: this._generateFormArch(),
fields: this.fields,
viewFields: this.fields,
base_model: this.basicFieldParams.field.relation,
type: "form",
model: this.basicFieldParams.field.relation,
};
this.formView = new ProductPickerQuickModifPriceFormView(fieldsView, {
context: this.main_state.getContext(),
fieldMap: this.fieldMap,
modelName: this.basicFieldParams.field.relation,
userContext: this.getSession().user_context,
ids: this.res_id ? [this.res_id] : [],
currentId: this.res_id || undefined,
mode: this.res_id && this.readonly ? "readonly" : "edit",
recordID: this.id,
index: 0,
parentID: this.basicFieldParams.parentID,
default_buttons: true,
withControlPanel: false,
model: this.basicFieldParams.model,
parentRecordData: this.basicFieldParams.recordData,
currencyField: this.currencyField,
disable_autofocus: true,
});
if (this.id) {
this.basicFieldParams.model.save(this.id, {savePoint: true});
}
var def2 = this.formView.getController(this).then(function (controller) {
self.controller = controller;
self.$el.empty();
self.controller.appendTo(self.$el);
});
return $.when(def1, def2); /**
}, * @override
*/
init: function(parent, options) {
this._super.apply(this, arguments);
this.trigger_up("pause_auto_save");
this.state = options.state;
this.main_state = options.main_state;
this.node = options.node;
this.fields = options.fields;
this.fieldMap = options.fieldMap;
this.searchRecord = options.searchRecord;
this.fieldsInfo = _.extend({}, options.fieldsInfo);
this.readonly = options.readonly;
this.basicFieldParams = options.basicFieldParams;
this.canEditPrice = options.canEditPrice;
this.canEditDiscount = options.canEditDiscount;
this.currencyField = options.currencyField;
this.res_id = this.state && this.state.res_id;
this.id = this.state && this.state.id;
this.editContext = {};
this._fieldsInvisible = [];
},
/** /**
* @override * @override
*/ */
destroy: function () { start: function() {
this._super.apply(this, arguments); var self = this;
}, this._super.apply(this, arguments).then(function() {
var fieldsView = {
arch: self._generateFormArch(),
fields: self.fields,
viewFields: self.fields,
base_model: self.basicFieldParams.field.relation,
type: "form",
model: self.basicFieldParams.field.relation,
};
on_attach_callback: function () { var node_context = self.node.attr("context") || "{}";
// Do nothing self.nodeContext = py.eval(node_context, {
}, active_id: self.res_id || false,
});
var refinedContext = _.extend(
{},
self.main_state.getContext(),
self.nodeContext
);
_.extend(refinedContext, self.editContext);
/** self.formView = new ProductPickerQuickModifPriceFormView(
* @private fieldsView,
* @returns {String} {
*/ context: refinedContext,
_generateFormArch: function () { fieldMap: self.fieldMap,
var wanted_field_states = this._getWantedFieldState(); modelName: self.basicFieldParams.field.relation,
var template = userContext: self.getSession().user_context,
"<templates><t t-name='One2ManyProductPicker.QuickModifPrice.Form'>"; ids: self.res_id ? [self.res_id] : [],
template += this.basicFieldParams.field.views.form.arch; currentId: self.res_id || undefined,
template += "</t></templates>"; mode: self.res_id && self.readonly ? "readonly" : "edit",
qweb.add_template(template); recordID: self.id,
var $arch = $(qweb.render("One2ManyProductPicker.QuickModifPrice.Form", { index: 0,
field_map: this.fieldMap, parentID: self.basicFieldParams.parentID,
record_search: this.searchRecord, default_buttons: true,
})); withControlPanel: false,
model: self.basicFieldParams.model,
parentRecordData: self.getParent().getParent().state,
currencyField: self.currencyField,
disable_autofocus: true,
}
);
if (self.id) {
self.basicFieldParams.model.save(self.id, {savePoint: true});
}
return self.formView.getController(self).then(function(controller) {
self.controller = controller;
self.$(".modal-body").empty();
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;
});
});
},
var field_names = Object.keys(wanted_field_states); /**
var gen_arch = "<form><group>"; * @override
for (var index in field_names) { */
var field_name = field_names[index]; destroy: function() {
var $field = $arch.find("field[name='"+field_name+"']"); this._restoreNoFetch();
var modifiers = this.trigger_up("resume_auto_save");
$field.attr("modifiers") ? JSON.parse($field.attr("modifiers")) : {}; this.$el.off("hidden.bs.modal");
modifiers.invisible = false; this._super.apply(this, arguments);
modifiers.readonly = wanted_field_states[field_name]; },
$field.attr("modifiers", JSON.stringify(modifiers));
$field.attr("invisible", "0");
$field.attr("readonly", wanted_field_states[field_name]?"1":"0");
gen_arch += $field[0].outerHTML;
}
gen_arch += "</group></form>";
return gen_arch;
},
/** on_attach_callback: function() {
* This method returns the wanted fields to be displayed in the view. // Do nothing
* {field_name: readonly_state} },
*
* @private
* @returns {Object}
*/
_getWantedFieldState: function () {
var wantedFieldState = {};
wantedFieldState[this.fieldMap.discount] = !this.canEditDiscount;
wantedFieldState[this.fieldMap.price_unit] = !this.canEditPrice;
return wantedFieldState;
},
});
return ProductPickerQuickModifPriceForm; /**
}); * @private
* @returns {String}
*/
_generateFormArch: function() {
var wanted_field_states = this._getWantedFieldState();
var template =
"<templates><t t-name='One2ManyProductPicker.QuickModifPrice.Form'>";
template += this.basicFieldParams.field.views.form.arch;
template += "</t></templates>";
qweb.add_template(template);
var $arch = $(
qweb.render("One2ManyProductPicker.QuickModifPrice.Form", {
field_map: this.fieldMap,
record_search: this.searchRecord,
})
);
var field_names = Object.keys(
this.basicFieldParams.field.views.form.fields
);
var gen_arch = "<form><group>";
for (var index in field_names) {
var field_name = field_names[index];
var $field = $arch.find("field[name='" + field_name + "']");
var modifiers = $field.attr("modifiers")
? JSON.parse($field.attr("modifiers"))
: {};
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];
$field.attr("modifiers", JSON.stringify(modifiers));
$field.attr("invisible", modifiers.invisible ? "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 += "</group></form>";
return gen_arch;
},
/**
* This method returns the wanted fields to be displayed in the view.
* {field_name: readonly_state}
*
* @private
* @returns {Object}
*/
_getWantedFieldState: function() {
var wantedFieldState = {};
wantedFieldState[this.fieldMap.discount] = !this.canEditDiscount;
wantedFieldState[this.fieldMap.price_unit] = !this.canEditPrice;
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;
}
);

View File

@ -1,55 +1,40 @@
// 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";
/** /**
* This file defines the QuickCreateFormView, an extension of the FormView that * This file defines the QuickCreateFormView, an extension of the FormView that
* is used by the RecordQuickCreate in One2ManyProductPicker views. * is used by the RecordQuickCreate in One2ManyProductPicker views.
*/ */
var QuickCreateFormView = require("web.QuickCreateFormView"); var QuickCreateFormView = require("web.QuickCreateFormView");
var core = require("web.core"); var core = require("web.core");
var tools = require("web_widget_one2many_product_picker.tools"); var tools = require("web_widget_one2many_product_picker.tools");
var qweb = core.qweb; var qweb = core.qweb;
var ProductPickerQuickModifPriceFormRenderer = var ProductPickerQuickModifPriceFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend(
QuickCreateFormView.prototype.config.Renderer.extend(
{ {
/** /**
* @override * @override
*/ */
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 * @private
*/ */
_appendButtons: function () { _appendPrice: function() {
this.$el.find(
".oe_one2many_product_picker_form_buttons").remove();
this.$el.append(
qweb.render(
"One2ManyProductPicker.QuickModifPrice.FormButtons", {
mode: this.mode,
})
);
},
/**
* @private
*/
_appendPrice: function () {
this.$el.find(".oe_price").remove(); this.$el.find(".oe_price").remove();
this.$el.append( this.$el.append(
qweb.render("One2ManyProductPicker.QuickModifPrice.Price") qweb.render("One2ManyProductPicker.QuickModifPrice.Price")
@ -58,70 +43,179 @@ 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);
}, },
/** /**
* @override * @override
*/ */
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(
tools.monetary(
price_reduce,
this.getParent().state.fields[this.fieldMap.price_unit],
this.currencyField,
record
)
); );
this.renderer.$el
.find(".oe_price")
.html(
tools.monetary(
price_reduce,
this.fields[this.fieldMap.price_unit],
this.currencyField,
values
)
);
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickChange: function(ev) {
var self = this;
var def = $.Deferred();
ev.stopPropagation();
this.model.updateRecordContext(this.handle, {
has_changes_unconfirmed: false,
});
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_virtual) {
this.saveRecord(this.handle, {
stayInEdit: true,
reload: true,
savePoint: true,
viewType: "form",
}).then(function() {
def.resolve(true);
self.trigger_up("create_quick_record", {
id: self.handle,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
});
} else {
def.resolve(true);
// If is a "normal" record, update it
this.trigger_up("update_quick_record", {
id: this.handle,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
}
return def;
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickDiscard: function(ev) {
var self = this;
ev.stopPropagation();
if (!this.model.isDirty(this.handle)) {
return true;
}
this.model.discardChanges(this.handle, {
rollback: true,
});
this.model.updateRecordContext(this.handle, {
need_notify: false,
modified: false,
});
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 * @private
*/ */
_disableQuickCreate: function () { _disableQuickCreate: function() {
// Ensures that the record won't be created twice // Ensures that the record won't be created twice
this._disabled = true;
this.$el.addClass("o_disabled"); this.$el.addClass("o_disabled");
this.$("input:not(:disabled)") this.$("input:not(:disabled),button:not(:disabled)")
.addClass("o_temporarily_disabled") .addClass("o_temporarily_disabled")
.attr("disabled", "disabled"); .attr("disabled", "disabled");
}, },
@ -129,97 +223,35 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
/** /**
* @private * @private
*/ */
_enableQuickCreate: function () { _enableQuickCreate: function() {
// Allows to create again // Allows to create again
this._disabled = false;
this.$el.removeClass("o_disabled"); this.$el.removeClass("o_disabled");
this.$("input.o_temporarily_disabled") this.$("input.o_temporarily_disabled,button.o_temporarily_disabled")
.removeClass("o_temporarily_disabled") .removeClass("o_temporarily_disabled")
.attr("disabled", false); .attr("disabled", false);
}, },
/**
* @private
* @param {MouseEvent} ev
*/
_onClickChange: function (ev) {
var self = this;
ev.stopPropagation();
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
var is_virtual = this.model.isPureVirtual(this.handle);
// If is a 'pure virtual' record, save it in the selected list
if (is_virtual) {
if (this.model.isDirty(this.handle)) {
this._disableQuickCreate();
this.saveRecord(this.handle, {
stayInEdit: true,
reload: true,
savePoint: true,
viewType: "form",
}).then(function () {
self._enableQuickCreate();
var record = self.model.get(self.handle);
self.model.unsetDirty(self.handle);
self.trigger_up("create_quick_record", {
id: record.id,
});
self.getParent().destroy();
});
} else {
this.getParent().destroy();
}
} else {
// If is a "normal" record, update it
var record = this.model.get(this.handle);
this.trigger_up("update_quick_record", {
id: record.id,
});
self.model.unsetDirty(self.handle);
this.getParent().destroy();
}
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickDiscard: function (ev) {
ev.stopPropagation();
this.model.discardChanges(this.handle, {
rollback: true,
});
var record = this.model.get(this.handle);
this.trigger_up("update_quick_record", {
id: record.id,
});
this.getParent().destroy();
},
} }
); );
var ProductPickerQuickModifPriceFormView = QuickCreateFormView.extend({ var ProductPickerQuickModifPriceFormView = QuickCreateFormView.extend({
config: _.extend({}, QuickCreateFormView.prototype.config, { config: _.extend({}, QuickCreateFormView.prototype.config, {
Renderer: ProductPickerQuickModifPriceFormRenderer, Renderer: ProductPickerQuickModifPriceFormRenderer,
Controller: ProductPickerQuickModifPriceFormController, Controller: ProductPickerQuickModifPriceFormController,
}), }),
init: function (viewInfo, params) { init: function(viewInfo, params) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.controllerParams.fieldMap = params.fieldMap; this.controllerParams.fieldMap = params.fieldMap;
this.controllerParams.context = params.context; this.controllerParams.context = params.context;
this.controllerParams.parentRecordData = params.parentRecordData; this.controllerParams.parentRecordData = params.parentRecordData;
this.controllerParams.currencyField = params.currencyField; this.controllerParams.currencyField = params.currencyField;
}, },
}); });
return { return {
ProductPickerQuickModifPriceFormRenderer: ProductPickerQuickModifPriceFormRenderer, ProductPickerQuickModifPriceFormRenderer: ProductPickerQuickModifPriceFormRenderer,
ProductPickerQuickModifPriceFormController: ProductPickerQuickModifPriceFormController, ProductPickerQuickModifPriceFormController: ProductPickerQuickModifPriceFormController,
ProductPickerQuickModifPriceFormView: ProductPickerQuickModifPriceFormView, ProductPickerQuickModifPriceFormView: ProductPickerQuickModifPriceFormView,
}; };
}); }
);

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

@ -1,37 +1,65 @@
// 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.BasicModel", function (require) { 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} * @returns {Boolean}
*/ */
isPureVirtual: function (id) { 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]; var data = this.localData[id];
return data._virtual || false; return data._virtual || false;
}, },
/** /**
* @param {Number/String} id * @param {String} id
* @returns {Boolean}
*/
isPureVirtual: function(id) {
var data = this.localData[id];
return data._virtual || false;
},
/**
* @param {String} id
* @param {Boolean} status * @param {Boolean} status
*/ */
setPureVirtual: function (id, status) { setPureVirtual: function(id, status) {
var data = this.localData[id]; var data = this.localData[id];
if (status) { if (status) {
data._virtual = true; data._virtual = true;
@ -41,47 +69,242 @@ 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];
data._isDirty = false; data._isDirty = false;
this._visitChildren(data, function (r) { this._visitChildren(data, function(r) {
r._isDirty = false; r._isDirty = false;
}); });
}, },
removeVirtualRecord: function (id) { /**
* 'Pure virtual' records are not used by other
* elements so can be removed safesly
*
* @param {String} id
* @returns {Boolean}
*/
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
* @returns {Deferred} * @returns {Deferred}
*/ */
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) self.setPureVirtual(recordID, true);
.then(function (recordID) { self.updateRecordContext(recordID, {
self.setPureVirtual(recordID, true); ignore_warning: true,
self.updateRecordContext(recordID, {ignore_warning: true}); not_onchange: true,
if (options.data) { shadow: true,
self._applyChange( });
recordID, return {
options.data, record: self.get(recordID),
params params: params,
).then(function () { };
d.resolve(self.get(recordID));
});
} else {
d.resolve(self.get(recordID));
}
});
}); });
}, },
/** /**
* 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;
}.bind(this); return super__rpc_call.call(this, params, options);
}.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
_: PY_t, ? pyUtils.py_eval(attrs.options, {
_: 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,6 +1,6 @@
$one2many-product-picker-grid-breakpoints: map-merge( $one2many-product-picker-grid-breakpoints: map-merge(
$grid-breakpoints, $grid-breakpoints,
( (
xxl: 1440px, xxl: 1440px,
) )
); );

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,8 +258,17 @@
.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 {
height: unset; &:not(.widget_numeric_step) {
max-width: 95%;
}
.o_input_dropdown > input {
height: unset;
}
}
.btn.w-100 {
max-width: 95%;
} }
} }
} }
@ -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; position: absolute;
margin: auto; left: 50%;
position: absolute; top: 50%;
top: 50%; transform: translate(-50%, -50%);
left: 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>
</t>
<t t-name="One2ManyProductPicker.ActionButton">
<div class="safezone d-inline-block float-left m-0 pb-2 pr-2 text-left">
<t t-if="need_notify || need_save || is_saving">
<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" t-esc="lazy_qty || '1'" /> x <t
t-esc="state.data[field_map[field_uom]].data.display_name"
/></span>
</t>
<t t-elif="!is_virtual">
<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"
><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-else="">
<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>
</t>
<t t-name="One2ManyProductPicker.PriceZone">
<div class="position-absolute m-0 text-left badge_price">
<t t-if="show_discount">
<span
t-att-data-field="field_map.discount"
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-else="has_onchange">
<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>
</div>
</t>
<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-else="">
<span
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>
</div>
</t>
<t t-name="One2ManyProductPicker.FlipCard.Back">
<div class="oe_flip_card_back">
<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> </div>
</t> </t>
<t t-name="One2ManyProductPicker.FlipCard"> <t t-name="One2ManyProductPicker.FlipCard">
<div class="oe_flip_container p-1 col-12 col-sm-8 col-md-6 col-lg-4 col-xl-3 col-xxl-2"> <div
<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') || ''}}"> 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"> <div class="oe_flip_card_inner text-center">
<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') || ''}}"> <t t-call="One2ManyProductPicker.FlipCard.Front" />
<t t-if="state"> <t t-call="One2ManyProductPicker.FlipCard.Back" />
<t t-if="!is_virtual"> </div>
<div class="safezone position-absolute m-0 pb-2 pr-2 text-left"> </div>
<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>
</div> </t>
</t> <t t-name="One2ManyProductPicker.QuickModifPrice.Modal">
<t t-elif="is_saving"> <div
<div class="safezone position-absolute m-0 pb-2 pr-2 text-left"> class="oe_product_picker_quick_modif_price modal fade"
<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> id="One2ManyProductPickerQuickModifPrice"
</div> tabindex="-1"
</t> role="dialog"
<t t-else=""> aria-labelledby="One2ManyProductPickerQuickModifPriceLabel"
<div class="safezone position-absolute m-0 pb-2 pr-2 text-left"> aria-hidden="true"
<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> >
</div> <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
</t> <div class="modal-content">
<div class="position-absolute m-0 text-left badge_price"> <div class="modal-body p-0">
<t t-if="show_discount"> <span class="icon-waiting">
<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" /> <i class="fa fa-cog fa-spin fa-3x fa-fw" />
<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" /> </span>
<span class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2" />
</t>
<t t-else="">
<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" />
</t>
</div>
<span data-field="display_name" class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1" data-esc="display_name" />
<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 t-else="">
<span 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_variant_medium')" t-att-data-src-alt="image(record_search.id,'image_variant_big')" />
</t>
</div> </div>
<div class="oe_flip_card_back"> <div class="modal-footer">
<widget name="product_picker_quick_create_form" t-att-compare-key="field_map.product_uom" /> <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,136 +1,228 @@
/* 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 (
'<field name="currency_id" invisible="1" />' + "<form>" +
'<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="currency_id" invisible="1" />' +
'<kanban>' + "<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="name" />' + "<kanban>" +
'<field name="product_id" />' + '<field name="name" />' +
'<field name="price_reduce" />' + '<field name="product_id" />' +
'<field name="price_unit" />' + '<field name="price_reduce" />' +
'<field name="foo_id" />' + '<field name="price_unit" />' +
'<field name="product_uom_qty" />' + '<field name="foo_id" />' +
'<field name="product_uom" />' + '<field name="product_uom_qty" />' +
'</kanban>' + '<field name="product_uom" />' +
'</field>' + "</kanban>" +
'</form>'; "</field>" +
"</form>"
);
}; };
QUnit.module('Web Widget One2Many Product Picker', { QUnit.module(
beforeEach: function () { "Web Widget One2Many Product Picker",
this.data = { {
foo: { beforeEach: function() {
fields: { this.data = {
currency_id: {string: "Currency", type: "many2one", relation: "currency"}, foo: {
line_ids: {string: "Lines Test", type: "one2many", relation: "line", relation_field: "foo_id"}, fields: {
display_name: {string: "Display Name", type: "char"}, currency_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"},
},
records: [
{
id: 1,
line_ids: [1, 2],
currency_id: 1,
display_name: "FT01",
},
],
}, },
records: [ line: {
{id: 1, line_ids: [1, 2], currency_id: 1, display_name: "FT01"}, fields: {
], name: {string: "Product Name", type: "string"},
}, product_id: {
line: { string: "Product",
fields: { type: "many2one",
name: {string: "Product Name", type: "string"}, relation: "product",
product_id: {string: "Product", type: "many2one", relation: "product"}, },
product_uom: {string: "UoM", type: "many2one", relation: "uom"}, product_uom: {
product_uom_qty: {string: "Qty", type: "integer"}, string: "UoM",
price_unit: {string: "Product Price", type: "float"}, type: "many2one",
price_reduce: {string: "Product Price Reduce", type: "float"}, relation: "uom",
foo_id: {string: "Parent", type: "many2one", relation: "foo"}, },
product_uom_qty: {string: "Qty", type: "integer"},
price_unit: {string: "Product Price", type: "float"},
price_reduce: {
string: "Product Price Reduce",
type: "float",
},
foo_id: {
string: "Parent",
type: "many2one",
relation: "foo",
},
},
records: [
{
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,
},
],
}, },
records: [ product: {
{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}, fields: {
{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}, name: {string: "Product name", type: "char"},
], display_name: {string: "Display Name", type: "char"},
}, list_price: {string: "Price", type: "float"},
product: { image_medium: {string: "Image Medium", type: "binary"},
fields: { uom_category_id: {
name: {string : "Product name", type: "char"}, string: "Category",
display_name: {string : "Display Name", type: "char"}, type: "many2one",
list_price: {string: "Price", type: "float"}, relation: "uom_category",
image_medium: {string: "Image Medium", type: "binary"}, },
uom_category_id: {string: "Category", type: "many2one", relation: "uom_category"}, },
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,
},
],
}, },
records: [ uom_category: {
{id: 1, name: "Large Cabinet", display_name: "Large Cabinet", list_price: 9.99, image_medium: "", uom_category_id: 1}, fields: {
{id: 2, name: "Cabinet with Doors", display_name: "Cabinet with Doors", list_price: 42.0, image_medium: "", uom_category_id: 1}, display_name: {string: "Display Name", type: "char"},
], },
}, records: [{id: 1, display_name: "Unit(s)"}],
uom_category: {
fields: {
display_name: {string : "Display Name", type: "char"},
}, },
records: [ uom: {
{id: 1, display_name: "Unit(s)"}, fields: {
], name: {string: "Name", type: "char"},
}, },
uom: { records: [{id: 1, name: "Unit(s)"}],
fields: {
name: {string: "Name", type: "char"},
}, },
records: [ currency: {
{id: 1, name: "Unit(s)"}, fields: {
], name: {string: "Name", type: "char"},
}, symbol: {string: "Symbol", type: "char"},
currency: { },
fields: { records: [{id: 1, name: "Eur", symbol: "€"}],
name: {string: "Name", type: "char"},
symbol: {string: "Symbol", type: "char"},
}, },
records: [ };
{id: 1, name: "Eur", symbol: "€"}, },
],
},
};
}, },
}, function () { function() {
QUnit.test('Load widget', function (assert) { 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,
viewOptions: { viewOptions: {
ids: [1], ids: [1],
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],
return $.when(this.data.foo.records); ["currency_id", "line_ids", "display_name"],
} else if (route === '/web/dataset/call_kw/line/read') { 'should only read "currency_id", "line_ids" and "display_name"'
assert.deepEqual(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.foo.records);
return $.when(this.data.line.records); } else if (route === "/web/dataset/call_kw/line/read") {
} else if (route === '/web/dataset/call_kw/product/search_read') { assert.deepEqual(
assert.deepEqual(args.kwargs.fields, ['id', 'uom_id', 'display_name', 'uom_category_id', 'image_medium', 'list_price'], args.args[1],
'should only read "id", "uom_id", "display_name", "uom_category_id", "image_medium" and "list_price"'); [
return $.when(this.data.product.records); "name",
} "product_id",
return this._super.apply(this, arguments); "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);
} else if (
route === "/web/dataset/call_kw/product/search_read"
) {
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 this._super.apply(this, arguments);
},
});
assert.ok(
form.$(".oe_field_one2many_product_picker").is(":visible"),
"should have a visible one2many product picker"
);
form.destroy();
}); });
}
assert.ok(form.$('.oe_field_one2many_product_picker').is(':visible'), );
"should have a visible one2many product picker");
form.destroy();
});
});
}); });

View File

@ -1,39 +1,86 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8" ?>
<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>