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:
~~~~~~~~~~~~~~~
* records_per_page > Integer -> Used to control the load more behaviour (16 by default)
* groups > Array of dictionaries -> Declare the groups
* name -> The group name
@ -58,6 +57,9 @@ Widget options:
* name -> The field name to 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)
* field_map > Dictionary:
@ -68,7 +70,7 @@ Widget options:
* price_unit -> The field that represent a price_unit ('price_unit' by default)
* discount -> The field that represent a discount ('discount' by default)
* search > Array of dictionaries or Array of 'triplets' ([[field_map.name, 'ilike', '$search']] by default)
* search > Array of dictionaries (defines to use name_search by default)
* name -> The name to display
* domain -> The domain to use
@ -76,11 +78,15 @@ Widget options:
* $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
* 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_price > Enable/Disable price edits (True by default)
* show_discount > Enable/Disable display discount (False by default)
* show_subtotal > Enable/Disable show subtotal (True by default)
* 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)
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.
* 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)
* auto_focus > Keep the focus on the search box after performing a search (True by default)
All widget options are optional.
Notice that you can call '_' method to use translations. This only can be used with this widget.
@ -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
* 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
===========

View File

@ -2,23 +2,16 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Web Widget One2Many Product Picker',
'summary': 'Widget to select products on one2many fields',
'version': '12.0.2.4.2',
'category': 'Website',
'author': "Tecnativa, "
"Odoo Community Association (OCA)",
'website': 'https://www.tecnativa.com',
'license': 'AGPL-3',
'depends': [
'product',
],
'data': [
'templates/assets.xml',
],
'qweb': [
'static/src/xml/one2many_product_picker.xml',
],
'installable': True,
'auto_install': False,
"name": "Web Widget One2Many Product Picker",
"summary": "Widget to select products on one2many fields",
"version": "12.0.2.4.2",
"category": "Website",
"author": "Tecnativa, " "Odoo Community Association (OCA)",
"website": "https://github.com/OCA/web",
"license": "AGPL-3",
"depends": ["product"],
"data": ["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
#. 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
msgid "All"
msgstr "Tot"
@ -114,7 +114,7 @@ msgstr "Imatge variant mitjana ( calculada)"
#. module: web_widget_one2many_product_picker
#. 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
msgid "[No widget %s]"
msgstr "[Sense widget %s]"

View File

@ -6,58 +6,97 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-05-04 13:10+0000\n"
"PO-Revision-Date: 2021-05-04 17:17+0200\n"
"Last-Translator: claudiagn <claudia.gargallo@qubiq.es>\n"
"Language-Team: none\n"
"POT-Creation-Date: 2022-01-25 18:49+0000\n"
"PO-Revision-Date: 2022-01-25 19:51+0100\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Poedit 2.4.1\n"
"Plural-Forms: \n"
"X-Generator: Poedit 3.0\n"
#. module: web_widget_one2many_product_picker
#. 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
msgid "Add"
msgstr "Añadir"
#. module: web_widget_one2many_product_picker
#. 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
msgid "All"
msgstr "Todo"
#. module: web_widget_one2many_product_picker
#. 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
msgid "Groups"
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
#. 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
msgid "Lines"
msgstr "Líneas"
#. module: web_widget_one2many_product_picker
#. 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
msgid "Load More"
msgstr "Cargar más"
#. module: web_widget_one2many_product_picker
#. 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
msgid "Price:"
msgstr "Precio:"
msgid "Price"
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
#: model:ir.model,name:web_widget_one2many_product_picker.model_product_product
@ -66,62 +105,28 @@ msgstr "Producto"
#. module: web_widget_one2many_product_picker
#. 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
msgid "Remove"
msgstr "Eliminar"
#. module: web_widget_one2many_product_picker
#. 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
msgid "Search..."
msgstr "Buscar..."
#. module: web_widget_one2many_product_picker
#. 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
msgid "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
#. 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
msgid "[No 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
#. 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
msgid "All"
msgstr ""
@ -108,7 +108,7 @@ msgstr ""
#. module: web_widget_one2many_product_picker
#. 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
msgid "[No widget %s]"
msgstr ""

View File

@ -6,6 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.0\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"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@ -15,44 +17,83 @@ 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: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
msgid "Add"
msgstr ""
#. module: web_widget_one2many_product_picker
#. 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
msgid "All"
msgstr ""
#. module: web_widget_one2many_product_picker
#. 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
msgid "Groups"
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
#. 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
msgid "Lines"
msgstr ""
#. module: web_widget_one2many_product_picker
#. 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
msgid "Load More"
msgstr ""
#. module: web_widget_one2many_product_picker
#. 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
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 ""
#. module: web_widget_one2many_product_picker
@ -62,48 +103,28 @@ 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:13
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:18
#, python-format
msgid "Remove"
msgstr ""
#. module: web_widget_one2many_product_picker
#. 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
msgid "Search..."
msgstr ""
#. module: web_widget_one2many_product_picker
#. 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
msgid "Subtotal:"
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
#. 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
msgid "[No widget %s]"
msgstr ""

View File

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

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:
~~~~~~~~~~~~~~~
* records_per_page > Integer -> Used to control the load more behaviour (16 by default)
* groups > Array of dictionaries -> Declare the groups
* name -> The group name
@ -16,6 +15,9 @@ Widget options:
* name -> The field name to 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)
* field_map > Dictionary:
@ -26,7 +28,7 @@ Widget options:
* price_unit -> The field that represent a price_unit ('price_unit' by default)
* discount -> The field that represent a discount ('discount' by default)
* search > Array of dictionaries or Array of 'triplets' ([[field_map.name, 'ilike', '$search']] by default)
* search > Array of dictionaries (defines to use name_search by default)
* name -> The name to display
* domain -> The domain to use
@ -34,11 +36,15 @@ Widget options:
* $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
* 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_price > Enable/Disable price edits (True by default)
* show_discount > Enable/Disable display discount (False by default)
* show_subtotal > Enable/Disable show subtotal (True by default)
* 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)
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.
* 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)
* auto_focus > Keep the focus on the search box after performing a search (True by default)
All widget options are optional.
Notice that you can call '_' method to use translations. This only can be used with this widget.

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
* 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">
<head>
<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>
<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">
<h2><a class="toc-backref" href="#id3">Widget options:</a></h2>
<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>
<blockquote>
<ul>
@ -425,6 +423,10 @@ You need to define the view fields. The view must be of <tt class="docutils lite
</ul>
</blockquote>
</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>
</blockquote>
</li>
@ -442,7 +444,7 @@ You need to define the view fields. The view must be of <tt class="docutils lite
</ul>
</blockquote>
</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>
<ul>
<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>
</blockquote>
</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>
</blockquote>
</li>
@ -468,6 +474,8 @@ You need to define the view fields. The view must be of <tt class="docutils lite
</li>
<li><p class="first">auto_save &gt; Enable/Disable auto save (False by default)</p>
</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>
<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
@ -475,8 +483,12 @@ modify/create a record with the widget.</p>
</li>
<li><p class="first">ignore_warning &gt; Enable/Disable display onchange warnings (False by default)</p>
</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>
<li><p class="first">auto_focus &gt; Keep the focus on the search box after performing a search (True by default)</p>
</li>
</ul>
<p>All widget options are optional.
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>
<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>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>
</div>
<div class="section" id="bug-tracker">

View File

@ -1,21 +1,37 @@
// 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.tools", function (
require
) {
odoo.define("web_widget_one2many_product_picker.tools", function(require) {
"use strict";
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
*
* @param {Number} price
* @param {Number} discount
* @param {Array} digist
* @returns {Number}
*/
function priceReduce (price, discount) {
return price * (1.0 - discount / 100.0);
function priceReduce(price, discount, digist) {
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
* @returns {String}
*/
function monetary (value, field_info, currency_field, data) {
return field_utils.format.monetary(
value,
field_info,
{
data: data,
currency_field: currency_field,
field_digits: true,
});
function monetary(value, field_info, currency_field, data) {
return field_utils.format.monetary(value, field_info, {
data: data,
currency_field: currency_field,
field_digits: true,
});
}
return {
monetary: monetary,
float: float,
priceReduce: priceReduce,
};
});

View File

@ -1,6 +1,7 @@
/* 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.ProductPickerQuickCreateForm", function (
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", function(
require
) {
"use strict";
@ -8,9 +9,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
var core = require("web.core");
var Widget = require("web.Widget");
var widgetRegistry = require("web.widget_registry");
var ProductPickerQuickCreateFormView = require(
"web_widget_one2many_product_picker.ProductPickerQuickCreateFormView"
).ProductPickerQuickCreateFormView;
var ProductPickerQuickCreateFormView = require("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView")
.ProductPickerQuickCreateFormView;
var qweb = core.qweb;
@ -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",
],
custom_events: {
reload_view: "_onReloadView",
},
/**
* @override
*/
init: function (parent, options) {
init: function(parent, options) {
this._super.apply(this, arguments);
this.state = options.state;
this.main_state = options.main_state;
@ -50,58 +46,57 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
/**
* @override
*/
start: function () {
start: function() {
var self = this;
var def1 = this._super.apply(this, arguments);
var form_arch = this._generateFormArch();
var fieldsView = {
arch: form_arch,
fields: this.fields,
viewFields: this.fields,
base_model: this.basicFieldParams.field.relation,
type: "form",
model: this.basicFieldParams.field.relation,
};
return this._super.apply(this, arguments).then(function() {
var form_arch = self._generateFormArch();
var fieldsView = {
arch: form_arch,
fields: self.fields,
viewFields: self.fields,
base_model: self.basicFieldParams.field.relation,
type: "form",
model: self.basicFieldParams.field.relation,
};
var node_context = this.node.attr("context") || "{}";
this.nodeContext = py.eval(node_context, {
active_id: this.res_id || false,
var node_context = self.node.attr("context") || "{}";
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 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) {
this.controller.autofocus();
}
@ -111,8 +106,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
* @private
* @returns {String}
*/
_generateFormArch: function () {
var template = "<templates><t t-name='One2ManyProductPicker.QuickCreateForm'>";
_generateFormArch: function() {
var template =
"<templates><t t-name='One2ManyProductPicker.QuickCreateForm'>";
template += this.basicFieldParams.field.views.form.arch;
template += "</t></templates>";
qweb.add_template(template);
@ -121,39 +117,12 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
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;
});

View File

@ -1,54 +1,51 @@
// Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView", function (
require
) {
"use strict";
odoo.define(
"web_widget_one2many_product_picker.ProductPickerQuickCreateFormView",
function(require) {
"use strict";
/**
* This file defines the QuickCreateFormView, an extension of the FormView that
* is used by the RecordQuickCreate in One2ManyProductPicker views.
*/
/**
* This file defines the QuickCreateFormView, an extension of the FormView that
* is used by the RecordQuickCreate in One2ManyProductPicker views.
*/
var QuickCreateFormView = require("web.QuickCreateFormView");
var BasicModel = require("web.BasicModel");
var core = require("web.core");
var QuickCreateFormView = require("web.QuickCreateFormView");
var BasicModel = require("web.BasicModel");
var core = require("web.core");
var qweb = core.qweb;
var qweb = core.qweb;
BasicModel.include({
_applyOnChange: function (values, record, viewType) {
var vt = viewType || record.viewType;
// Ignore changes by record context 'ignore_onchanges' fields
if ('ignore_onchanges' in record.context) {
var ignore_changes = record.context.ignore_onchanges;
for (var index in ignore_changes) {
var field_name = ignore_changes[index];
delete values[field_name];
BasicModel.include({
_applyOnChange: function(values, record, viewType) {
// Ignore changes by record context 'ignore_onchanges' fields
if ("ignore_onchanges" in record.context) {
var ignore_changes = record.context.ignore_onchanges;
for (var index in ignore_changes) {
var field_name = ignore_changes[index];
delete values[field_name];
}
delete record.context.ignore_onchanges;
}
delete record.context.ignore_onchanges;
}
return this._super(values, record, viewType);
},
});
return this._super(values, record, viewType);
},
});
var ProductPickerQuickCreateFormRenderer =
QuickCreateFormView.prototype.config.Renderer.extend(
var ProductPickerQuickCreateFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend(
{
/**
* @override
*/
start: function () {
start: function() {
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);
},
}
);
var ProductPickerQuickCreateFormController =
QuickCreateFormView.prototype.config.Controller.extend(
var ProductPickerQuickCreateFormController = QuickCreateFormView.prototype.config.Controller.extend(
{
events: _.extend({}, QuickCreateFormView.prototype.events, {
"click .oe_record_add": "_onClickAdd",
@ -57,7 +54,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
"click .oe_record_discard": "_onClickDiscard",
}),
init: function (parent, model, renderer, params) {
init: function(parent, model, renderer, params) {
this.compareKey = params.compareKey;
this.fieldMap = params.fieldMap;
this.context = params.context;
@ -65,18 +62,27 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
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
*/
auto: function () {
auto: function() {
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;
}
var state = this._getRecordState();
if (state === "new") {
if (this.model.isNew(record.id)) {
this._add();
} else if (state === "dirty") {
} else if (this.model.isDirty(record.id)) {
this._change();
}
},
@ -86,13 +92,15 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* - record: Normal
* - new: Is a new record
* - dirty: Has changes
*
* @returns {Object}
*/
_getRecordState: function () {
_getRecordState: function() {
var record = this.model.get(this.handle);
var state = "record";
if (this.model.isNew(record.id)) {
if (this.model.isNew(record.id) && this.model.isPureVirtual(record.id)) {
state = "new";
} else if (record.isDirty()) {
} else if (record.context.has_changes_unconfirmed) {
state = "dirty";
}
if (state === "new") {
@ -117,15 +125,13 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
*
* @private
*/
_updateButtons: function () {
this.$el.find(
".oe_one2many_product_picker_form_buttons").remove();
_updateButtons: function() {
this.$el.find(".oe_one2many_product_picker_form_buttons").remove();
this.$el.find(".o_form_view").append(
qweb.render(
"One2ManyProductPicker.QuickCreate.FormButtons", {
state: this._getRecordState(),
})
);
qweb.render("One2ManyProductPicker.QuickCreate.FormButtons", {
state: this._getRecordState(),
})
);
if (this._disabled) {
this._disableQuickCreate();
@ -135,7 +141,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
/**
* @private
*/
_disableQuickCreate: function () {
_disableQuickCreate: function() {
if (!this.$el) {
return;
}
@ -150,8 +156,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
/**
* @private
*/
_enableQuickCreate: function () {
_enableQuickCreate: function() {
// Allows to create again
this._disabled = false;
this.$el.removeClass("o_disabled");
@ -162,10 +167,10 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
/**
* @private
* @param {Array[String]} fields_changed
* @param {Array} fields_changed
* @returns {Boolean}
*/
_needReloadCard: function (fields_changed) {
_needReloadCard: function(fields_changed) {
for (var index in fields_changed) {
var field = fields_changed[index];
if (field === this.fieldMap[this.compareKey]) {
@ -183,169 +188,182 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private
* @param {ChangeEvent} ev
*/
_onFieldChanged: function (ev) {
var fields_changed = Object.keys(ev.data.changes);
if (this._needReloadCard(fields_changed)) {
var field = ev.data.changes[fields_changed[0]];
var new_value = false;
if (typeof field === "object") {
new_value = field.id;
} else {
new_value = field;
}
var reload_values = {
compareValue: new_value,
};
var record = this.model.get(this.handle);
if (!('base_record_id' in record.context)) {
var old_value = record.data[this.compareKey];
if (typeof old_value === 'object') {
old_value = old_value.data.id;
}
reload_values.baseRecordID = record.id;
reload_values.baseRecordResID = record.ref;
reload_values.baseRecordCompareValue = old_value;
} else {
reload_values.baseRecordID =
record.context.base_record_id;
reload_values.baseRecordResID =
record.context.base_record_res_id;
reload_values.baseRecordCompareValue =
record.context.base_record_compare_value;
}
this.trigger_up("reload_view", reload_values);
// Discard current change
ev.data.changes = {};
} else {
this._super.apply(this, arguments);
if (!_.isEmpty(ev.data.changes)) {
if (this.model.isPureVirtual(this.handle)) {
this.model.unsetDirty(this.handle);
}
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: false,
});
this.trigger_up("quick_record_updated", {
changes: ev.data.changes,
});
this._updateButtons();
_onFieldChanged: function(ev) {
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_unconfirmed: true,
});
this.trigger_up("quick_record_updated", {
changes: ev.data.changes,
highlight: {qty: true},
});
}
},
/**
* @returns {Deferred}
*/
_add: function () {
_add: function() {
var self = this;
if (this._disabled) {
// Don't do anything if we are already creating a record
return $.Deferred();
return $.Deferred().resolve();
}
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
need_notify: true,
modified: true,
});
var self = this;
this._disableQuickCreate();
return this.saveRecord(this.handle, {
stayInEdit: true,
reload: true,
savePoint: true,
viewType: "form",
}).then(function () {
}).then(function() {
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", {
success_callback: function () {
success_callback: function() {
self.trigger_up("create_quick_record", {
id: record.id,
callback: function () {
self.model.unsetDirty(self.handle);
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
},
block: true,
});
});
},
_remove: function () {
_remove: function() {
var self = this;
if (this._disabled) {
return $.Deferred();
return $.Deferred().resolve();
}
this.model.updateRecordContext(this.handle, {
need_notify: true,
modified: true,
});
this._disableQuickCreate();
this.trigger_up("restore_flip_card", {block: true});
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,
});
},
_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", {
success_callback: function () {
self.trigger_up("update_quick_record", {
success_callback: function() {
self.trigger_up("list_record_remove", {
id: record.id,
callback: function () {
self.model.unsetDirty(self.handle);
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
}
},
});
},
block: true,
});
},
_discard: function () {
_change: function() {
var self = this;
if (this._disabled) {
// 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();
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, {
rollback: true,
});
this.trigger_up("quick_record_updated", {
changes: record.data,
});
if (this.model.isNew(record.id)) {
this.update({}, {reload: false});
this.trigger_up("restore_flip_card");
this._updateButtons();
this._enableQuickCreate();
} else {
this.update({}, {reload: false}).then(function () {
self.model.unsetDirty(self.handle);
self.trigger_up("restore_flip_card");
self._updateButtons();
self._enableQuickCreate();
return this.update({}, {reload: false}).then(function() {
record = self.model.get(self.handle);
self.trigger_up("quick_record_updated", {
changes: record.data,
});
}
self.trigger_up("restore_flip_card", {
success_callback: function() {
self._updateButtons();
self._enableQuickCreate();
}
});
});
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickAdd: function (ev) {
_onClickAdd: function(ev) {
ev.stopPropagation();
this._add();
},
@ -354,7 +372,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private
* @param {MouseEvent} ev
*/
_onClickRemove: function (ev) {
_onClickRemove: function(ev) {
ev.stopPropagation();
this._remove();
},
@ -363,7 +381,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private
* @param {MouseEvent} ev
*/
_onClickChange: function (ev) {
_onClickChange: function(ev) {
ev.stopPropagation();
this._change();
},
@ -372,34 +390,35 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private
* @param {MouseEvent} ev
*/
_onClickDiscard: function (ev) {
_onClickDiscard: function(ev) {
ev.stopPropagation();
this._discard();
},
}
);
var ProductPickerQuickCreateFormView = QuickCreateFormView.extend({
config: _.extend({}, QuickCreateFormView.prototype.config, {
Renderer: ProductPickerQuickCreateFormRenderer,
Controller: ProductPickerQuickCreateFormController,
}),
var ProductPickerQuickCreateFormView = QuickCreateFormView.extend({
config: _.extend({}, QuickCreateFormView.prototype.config, {
Renderer: ProductPickerQuickCreateFormRenderer,
Controller: ProductPickerQuickCreateFormController,
}),
/**
* @override
*/
init: function (viewInfo, params) {
this._super.apply(this, arguments);
this.controllerParams.compareKey = params.compareKey;
this.controllerParams.fieldMap = params.fieldMap;
this.controllerParams.context = params.context;
this.controllerParams.mainRecordData = params.mainRecordData;
},
});
/**
* @override
*/
init: function(viewInfo, params) {
this._super.apply(this, arguments);
this.controllerParams.compareKey = params.compareKey;
this.controllerParams.fieldMap = params.fieldMap;
this.controllerParams.context = params.context;
this.controllerParams.mainRecordData = params.mainRecordData;
},
});
return {
ProductPickerQuickCreateFormRenderer: ProductPickerQuickCreateFormRenderer,
ProductPickerQuickCreateFormController: ProductPickerQuickCreateFormController,
ProductPickerQuickCreateFormView: ProductPickerQuickCreateFormView,
};
});
return {
ProductPickerQuickCreateFormRenderer: ProductPickerQuickCreateFormRenderer,
ProductPickerQuickCreateFormController: ProductPickerQuickCreateFormController,
ProductPickerQuickCreateFormView: ProductPickerQuickCreateFormView,
};
}
);

View File

@ -1,152 +1,255 @@
/* 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.ProductPickerQuickModifPriceForm", function (
require
) {
"use strict";
odoo.define(
"web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm",
function(require) {
"use strict";
var core = require("web.core");
var Widget = require("web.Widget");
var ProductPickerQuickModifPriceFormView = require(
"web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView"
).ProductPickerQuickModifPriceFormView;
var core = require("web.core");
var Widget = require("web.Widget");
var widgetRegistry = require("web.widget_registry");
var ProductPickerQuickModifPriceFormView = require("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView")
.ProductPickerQuickModifPriceFormView;
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",
],
var qweb = core.qweb;
/**
* @override
* This widget render a Form. Used by FieldOne2ManyProductPicker
*/
init: function (parent, options) {
this._super.apply(this, arguments);
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 = 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 = {};
},
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
*/
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);
});
events: {
"click .oe_record_change": "_onClickChange",
"click .oe_record_discard": "_onClickDiscard",
},
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
*/
destroy: function () {
this._super.apply(this, arguments);
},
/**
* @override
*/
start: function() {
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 () {
// Do nothing
},
var node_context = self.node.attr("context") || "{}";
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);
/**
* @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,
}));
self.formView = new ProductPickerQuickModifPriceFormView(
fieldsView,
{
context: refinedContext,
fieldMap: self.fieldMap,
modelName: self.basicFieldParams.field.relation,
userContext: self.getSession().user_context,
ids: self.res_id ? [self.res_id] : [],
currentId: self.res_id || undefined,
mode: self.res_id && self.readonly ? "readonly" : "edit",
recordID: self.id,
index: 0,
parentID: self.basicFieldParams.parentID,
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>";
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")) : {};
modifiers.invisible = false;
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;
},
/**
* @override
*/
destroy: function() {
this._restoreNoFetch();
this.trigger_up("resume_auto_save");
this.$el.off("hidden.bs.modal");
this._super.apply(this, arguments);
},
/**
* 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;
},
});
on_attach_callback: function() {
// Do nothing
},
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
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView", function (
require
) {
"use strict";
odoo.define(
"web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView",
function(require) {
"use strict";
/**
* This file defines the QuickCreateFormView, an extension of the FormView that
* is used by the RecordQuickCreate in One2ManyProductPicker views.
*/
/**
* This file defines the QuickCreateFormView, an extension of the FormView that
* is used by the RecordQuickCreate in One2ManyProductPicker views.
*/
var QuickCreateFormView = require("web.QuickCreateFormView");
var core = require("web.core");
var tools = require("web_widget_one2many_product_picker.tools");
var QuickCreateFormView = require("web.QuickCreateFormView");
var core = require("web.core");
var tools = require("web_widget_one2many_product_picker.tools");
var qweb = core.qweb;
var qweb = core.qweb;
var ProductPickerQuickModifPriceFormRenderer =
QuickCreateFormView.prototype.config.Renderer.extend(
var ProductPickerQuickModifPriceFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend(
{
/**
* @override
*/
start: function () {
start: function() {
var self = this;
this.$el.addClass(
"oe_one2many_product_picker_form_view o_xxs_form_view");
return this._super.apply(this, arguments).then(function () {
"oe_one2many_product_picker_form_view o_xxs_form_view"
);
return this._super.apply(this, arguments).then(function() {
self._appendPrice();
self._appendButtons();
});
},
/**
* @private
*/
_appendButtons: function () {
this.$el.find(
".oe_one2many_product_picker_form_buttons").remove();
this.$el.append(
qweb.render(
"One2ManyProductPicker.QuickModifPrice.FormButtons", {
mode: this.mode,
})
);
},
/**
* @private
*/
_appendPrice: function () {
_appendPrice: function() {
this.$el.find(".oe_price").remove();
this.$el.append(
qweb.render("One2ManyProductPicker.QuickModifPrice.Price")
@ -58,70 +43,179 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
}
);
var ProductPickerQuickModifPriceFormController =
QuickCreateFormView.prototype.config.Controller.extend(
var ProductPickerQuickModifPriceFormController = QuickCreateFormView.prototype.config.Controller.extend(
{
events: _.extend({}, QuickCreateFormView.prototype.events, {
"click .oe_record_change": "_onClickChange",
"click .oe_record_discard": "_onClickDiscard",
}),
/**
* @override
*/
init: function (parent, model, renderer, params) {
init: function(parent, model, renderer, params) {
this.fieldMap = params.fieldMap;
this.context = params.context;
this._super.apply(this, arguments);
this.currencyField = params.currencyField;
this.parentRecordData = params.parentRecordData;
this.fields = parent.state.fields;
this._super.apply(this, arguments);
},
/**
* @override
*/
start: function () {
start: function() {
var self = this;
return this._super.apply(this, arguments).then(function () {
self._updatePrice();
return this._super.apply(this, arguments).then(function() {
var record = self.model.get(self.handle);
self._updatePrice(record.data);
});
},
/**
* @override
*/
_onFieldChanged: function () {
_onFieldChanged: function(ev) {
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
* @param {Object} values
*/
_updatePrice: function () {
var record = this.model.get(this.handle);
_updatePrice: function(values) {
var price_reduce = tools.priceReduce(
record.data[this.fieldMap.price_unit],
record.data[this.fieldMap.discount]);
this.renderer.$el.find(".oe_price").html(
tools.monetary(
price_reduce,
this.getParent().state.fields[this.fieldMap.price_unit],
this.currencyField,
record
)
values[this.fieldMap.price_unit],
values[this.fieldMap.discount]
);
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
*/
_disableQuickCreate: function () {
_disableQuickCreate: function() {
// Ensures that the record won't be created twice
this._disabled = true;
this.$el.addClass("o_disabled");
this.$("input:not(:disabled)")
this.$("input:not(:disabled),button:not(:disabled)")
.addClass("o_temporarily_disabled")
.attr("disabled", "disabled");
},
@ -129,97 +223,35 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
/**
* @private
*/
_enableQuickCreate: function () {
_enableQuickCreate: function() {
// Allows to create again
this._disabled = false;
this.$el.removeClass("o_disabled");
this.$("input.o_temporarily_disabled")
this.$("input.o_temporarily_disabled,button.o_temporarily_disabled")
.removeClass("o_temporarily_disabled")
.attr("disabled", false);
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickChange: function (ev) {
var self = this;
ev.stopPropagation();
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
var is_virtual = this.model.isPureVirtual(this.handle);
// If is a 'pure virtual' record, save it in the selected list
if (is_virtual) {
if (this.model.isDirty(this.handle)) {
this._disableQuickCreate();
this.saveRecord(this.handle, {
stayInEdit: true,
reload: true,
savePoint: true,
viewType: "form",
}).then(function () {
self._enableQuickCreate();
var record = self.model.get(self.handle);
self.model.unsetDirty(self.handle);
self.trigger_up("create_quick_record", {
id: record.id,
});
self.getParent().destroy();
});
} 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({
config: _.extend({}, QuickCreateFormView.prototype.config, {
Renderer: ProductPickerQuickModifPriceFormRenderer,
Controller: ProductPickerQuickModifPriceFormController,
}),
var ProductPickerQuickModifPriceFormView = QuickCreateFormView.extend({
config: _.extend({}, QuickCreateFormView.prototype.config, {
Renderer: ProductPickerQuickModifPriceFormRenderer,
Controller: ProductPickerQuickModifPriceFormController,
}),
init: function (viewInfo, params) {
this._super.apply(this, arguments);
this.controllerParams.fieldMap = params.fieldMap;
this.controllerParams.context = params.context;
this.controllerParams.parentRecordData = params.parentRecordData;
this.controllerParams.currencyField = params.currencyField;
},
});
init: function(viewInfo, params) {
this._super.apply(this, arguments);
this.controllerParams.fieldMap = params.fieldMap;
this.controllerParams.context = params.context;
this.controllerParams.parentRecordData = params.parentRecordData;
this.controllerParams.currencyField = params.currencyField;
},
});
return {
ProductPickerQuickModifPriceFormRenderer: ProductPickerQuickModifPriceFormRenderer,
ProductPickerQuickModifPriceFormController: ProductPickerQuickModifPriceFormController,
ProductPickerQuickModifPriceFormView: ProductPickerQuickModifPriceFormView,
};
});
return {
ProductPickerQuickModifPriceFormRenderer: ProductPickerQuickModifPriceFormRenderer,
ProductPickerQuickModifPriceFormController: ProductPickerQuickModifPriceFormController,
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
// 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";
var BasicModel = require("web.BasicModel");
var FieldOne2ManyProductPicker = require("web_widget_one2many_product_picker.FieldOne2ManyProductPicker");
BasicModel.include({
/**
* @param {Number/String} handle
* @param {Object} context
* @override
*/
updateRecordContext: function (handle, context) {
this.localData[handle].context = _.extend(
{},
this.localData[handle].context,
context);
init: function() {
this._super.apply(this, arguments);
},
/**
* @param {Number/String} id
* This is necessary because 'pure virtual' records
* can be destroyed at any time.
*
* @param {String} id
* @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];
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
*/
setPureVirtual: function (id, status) {
setPureVirtual: function(id, status) {
var data = this.localData[id];
if (status) {
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];
data._isDirty = false;
this._visitChildren(data, function (r) {
this._visitChildren(data, function(r) {
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)) {
return false;
}
const data = this.localData[id];
const to_remove = [];
this._visitChildren(data, (item) => {
var data = this.localData[id];
var to_remove = [];
this._visitChildren(data, function(item) {
to_remove.push(item.id);
});
to_remove.reverse();
for (let remove_id of to_remove) {
for (var remove_id of to_remove) {
this.removeLine(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 {Object} options
* @returns {Deferred}
*/
createVirtualRecord: function (listID, options) {
createVirtualRecord: function(listID, options) {
var self = this;
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 = {
context: context,
fields: list.fields,
@ -93,48 +316,276 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
doNotSetDirty: true,
};
return $.Deferred(function (d) {
self._makeDefaultRecord(list.model, params)
.then(function (recordID) {
self.setPureVirtual(recordID, true);
self.updateRecordContext(recordID, {ignore_warning: true});
if (options.data) {
self._applyChange(
recordID,
options.data,
params
).then(function () {
d.resolve(self.get(recordID));
});
} else {
d.resolve(self.get(recordID));
}
});
return this._makeDefaultRecord(list.model, params).then(function(recordID) {
self.setPureVirtual(recordID, true);
self.updateRecordContext(recordID, {
ignore_warning: true,
not_onchange: true,
shadow: true,
});
return {
record: self.get(recordID),
params: params,
};
});
},
/**
* Adds support to avoid show onchange warnings.
* 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.
*
* @override
*/
_performOnChange: function (record, fields, viewType) {
if (record.context && record.context.ignore_warning) {
_performOnChange: function(record) {
if (record && record.context) {
record.context.not_onchange = false;
var this_mp = _.clone(this);
var super_call = this.trigger_up;
this_mp.trigger_up = function (event_name, data) {
if (event_name === 'warning' && data.type === "dialog") {
return; // Do nothing
}
return super_call.apply(this, arguments);
}.bind(this);
if (record.context.shadow) {
// Force use 'shadow'
var super__rpc_call = this._rpc;
this_mp._rpc = function(params, options) {
options = options || {};
options.shadow = true;
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, 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 */
// Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.BasicView", function (require) {
odoo.define("web_widget_one2many_product_picker.FormView", function(require) {
"use strict";
var core = require("web.core");
var pyUtils = require("web.py_utils");
var BasicView = require("web.BasicView");
var FormView = require("web.FormView");
var _t = core._t;
// Add ref to _() -> _t() call
var PY_t = new py.PY_def.fromJSON(function () {
var args = py.PY_parseArgs(arguments, ['str']);
var PY_t = new py.PY_def.fromJSON(function() {
var args = py.PY_parseArgs(arguments, ["str"]);
return py.str.fromJSON(_t(args.str.toJSON()));
});
BasicView.include({
FormView.include({
/**
* @override
*/
_processField: function (viewType, field, attrs) {
_processField: function(viewType, field, attrs) {
/**
* We need process 'options' attribute to handle translations and
* special replacements
@ -31,15 +29,16 @@ odoo.define("web_widget_one2many_product_picker.BasicView", function (require) {
attrs.widget === "one2many_product_picker" &&
!_.isObject(attrs.options)
) {
attrs.options = attrs.options ? pyUtils.py_eval(attrs.options, {
_: PY_t,
attrs.options = attrs.options
? pyUtils.py_eval(attrs.options, {
_: PY_t,
// Hack: This allow use $number_search out of an string
number_search: '$number_search',
}) : {};
// Hack: This allow use $number_search out of an string
number_search: "$number_search",
})
: {};
}
return this._super(viewType, field, attrs);
},
});
});

View File

@ -1,6 +1,6 @@
$one2many-product-picker-grid-breakpoints: map-merge(
$grid-breakpoints,
(
xxl: 1440px,
)
(
xxl: 1440px,
)
);

View File

@ -1,4 +1,21 @@
.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 {
position: fixed;
top: 0;
@ -15,6 +32,17 @@
}
}
&.disabled {
.badge {
filter: opacity(0.6);
}
pointer-events: none;
> div.loading {
display: inline-block;
}
}
> div {
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 {
width: 100%;
@ -45,7 +86,10 @@
}
.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;
@ -54,16 +98,32 @@
margin-right: 0;
}
.o_catch_attention {
animation: none;
outline: 1px solid fade-in(theme-color("warning"), 0.5);
}
.oe_flip_card {
user-select: none;
background-color: transparent;
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;
&.blocked {
.badge {
filter: opacity(0.7);
}
div.loading {
display: inline-block;
}
}
&.disabled {
filter: grayscale(100%);
opacity: 0.5;
filter: grayscale(1)
}
&.oe_flip_card_maximized {
@ -77,11 +137,6 @@
.oe_flip_card_inner {
height: 100% !important;
box-shadow: 0px 0px 15px;
.img-fluid {
transform: translateY(-50%) !important;
top: 50%;
position: relative;
}
.oe_one2many_product_picker_title {
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);
margin-bottom: 1.3em !important;
}
.o_field_widget, .w-100 {
.o_field_widget,
.w-100 {
width: 100% / $one2many-product-picker-zoom-scale !important;
}
}
@ -129,9 +186,19 @@
position: relative;
width: 100%;
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;
.img-fluid {
transform: translate(-50%, -50%) !important;
top: 50%;
left: 50%;
z-index: -1;
position: absolute;
}
.position-absolute {
z-index: 1;
}
@ -140,8 +207,19 @@
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 {
top: 50%;
top: 55%;
right: -2px;
transform: translateY(-50%);
display: grid;
@ -180,8 +258,17 @@
.o_form_view.o_form_nosheet {
padding: $one2many-product-picker-card-form-padding;
.o_field_widget .o_input_dropdown > input {
height: unset;
.o_field_widget {
&: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%;
left: 0;
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;
z-index: 0;
}
.add_product, .product_qty, .price_unit {
.add_product,
.product_qty,
.price_unit {
cursor: pointer;
}
}
}
}
.oe_product_picker_quick_modif_price {
width: 80%;
max-width: $one2many-product-picker-quick-modif-price-max-width;
margin: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 1px solid $border-color;
z-index: 55;
/** Extra tools **/
.icon-waiting {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.oe_one2many_product_picker_form_buttons {
text-align: center;
/* clears the 'X' from Internet Explorer */
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>
<t t-name="One2ManyProductPicker.ControlPanelButtons">
<div class="text-center mx-auto">
<div class="input-group">
<div class="input-group-prepend">
<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" />
</button>
<div class="dropdown-menu">
<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>
</div>
</t>
<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" />
</button>
</t>
</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">
<button type="button" t-attf-class="btn btn-secondary btn-lg input-group-button oe_search_erase">
<i class="fa fa-eraser" />
<button
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>
</div>
</div>
</div>
</t>
<t t-name="One2ManyProductPicker.ControlPanelGroupButtons">
<div class="oe_one2many_product_picker_groups">
<div class="btn-group" role="group" aria-label="Groups">
<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" />
</button>
</t>
</div>
<button type="button" class="btn btn-light btn-lg oe_btn_lines">
Lines
<span class="ml-1 badge badge-light">0</span>
</button>
</div>
</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">
<div id="product_picker_total" class="text-right">
<h2>
@ -61,62 +84,163 @@
</h2>
</div>
</t>
<t t-name="One2ManyProductPicker.ExtraButtons">
<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>
</t>
<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 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') || ''}}">
<div
class="oe_flip_container p-1 col-12 col-sm-6 col-md-4 col-lg-4 col-xl-4 col-xxl-2"
t-att-data-card-id="state &amp;&amp; state.id || record_search.id"
>
<div t-attf-class="oe_flip_card {{!state &amp;&amp; 'disabled' || ''}}">
<div class="loading">LOADING...</div>
<div class="oe_flip_card_inner text-center">
<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-if="state">
<t t-if="!is_virtual">
<div class="safezone position-absolute m-0 pb-2 pr-2 text-left">
<span t-att-data-field="field_map.product_uom_qty" t-attf-data-esc="str({{field_map.product_uom_qty}}) + ' x ' + {{field_map.product_uom}}.data.display_name" t-attf-class="badge {{modified &amp;&amp; 'badge-warning' || 'badge-success'}} font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty" />
</div>
</t>
<t t-elif="is_saving">
<div class="safezone position-absolute m-0 pb-2 pr-2 text-left">
<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>
</div>
</t>
<t t-else="">
<div class="safezone position-absolute m-0 pb-2 pr-2 text-left">
<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>
</t>
<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({{field_map.discount}}) +'%'" class="badge badge-dark discount_price font-weight-bold rounded-0 mt-1 p-2" />
<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 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>
<t t-call="One2ManyProductPicker.FlipCard.Front" />
<t t-call="One2ManyProductPicker.FlipCard.Back" />
</div>
</div>
</div>
</t>
<t t-name="One2ManyProductPicker.QuickModifPrice.Modal">
<div
class="oe_product_picker_quick_modif_price modal fade"
id="One2ManyProductPickerQuickModifPrice"
tabindex="-1"
role="dialog"
aria-labelledby="One2ManyProductPickerQuickModifPriceLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body p-0">
<span class="icon-waiting">
<i class="fa fa-cog fa-spin fa-3x fa-fw" />
</span>
</div>
<div class="oe_flip_card_back">
<widget name="product_picker_quick_create_form" t-att-compare-key="field_map.product_uom" />
<div class="modal-footer">
<button class="btn btn-success oe_record_change mr-auto d-none">
<i class="fa fa-check" />
</button>
<button
class="btn btn-warning oe_record_discard"
data-dismiss="modal"
>
<i class="fa fa-times" />
</button>
</div>
</div>
</div>
</div>
</t>
<t t-name="One2ManyProductPicker.QuickModifPricePopup">
<div class="oe_product_picker_quick_modif_price shadow" />
</t>
</template>

View File

@ -1,18 +1,25 @@
<template>
<t t-name="One2ManyProductPicker.QuickCreate.FormButtons">
<div class="oe_one2many_product_picker_form_buttons">
<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-elif="state == 'dirty'">
<button class="btn btn-success oe_record_change mr-2"><i class="fa fa-check" /></button>
<button class="btn btn-warning oe_record_discard ml-2"><i class="fa fa-times" /></button>
<button class="btn btn-success oe_record_change w-50">
<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-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>
</div>
</t>
</template>

View File

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

View File

@ -1,136 +1,228 @@
/* global QUnit */
// Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define('web_widget_one2many_product_picker.widget_tests', function (require) {
odoo.define("web_widget_one2many_product_picker.widget_tests", function(require) {
"use strict";
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var FormView = require("web.FormView");
var testUtils = require("web.test_utils");
var createView = testUtils.createView;
var getArch = function () {
return '<form>' +
'<field name="currency_id" invisible="1" />' +
'<field name="line_ids" widget="one2many_product_picker" options="{\'groups\': [{\'name\': \'Desk\', \'domain\': [(\'name\', \'ilike\', \'%desk%\')], \'order\': {\'name\': \'id\', \'asc\': true}}, {\'name\': \'Chairs\', \'domain\': [(\'name\', \'ilike\', \'%chair%\')]}]}">' +
'<kanban>' +
'<field name="name" />' +
'<field name="product_id" />' +
'<field name="price_reduce" />' +
'<field name="price_unit" />' +
'<field name="foo_id" />' +
'<field name="product_uom_qty" />' +
'<field name="product_uom" />' +
'</kanban>' +
'</field>' +
'</form>';
var getArch = function() {
return (
"<form>" +
'<field name="currency_id" invisible="1" />' +
"<field name=\"line_ids\" widget=\"one2many_product_picker\" options=\"{'groups': [{'name': 'Desk', 'domain': [('name', 'ilike', '%desk%')], 'order': {'name': 'id', 'asc': true}}, {'name': 'Chairs', 'domain': [('name', 'ilike', '%chair%')]}]}\">" +
"<kanban>" +
'<field name="name" />' +
'<field name="product_id" />' +
'<field name="price_reduce" />' +
'<field name="price_unit" />' +
'<field name="foo_id" />' +
'<field name="product_uom_qty" />' +
'<field name="product_uom" />' +
"</kanban>" +
"</field>" +
"</form>"
);
};
QUnit.module('Web Widget One2Many Product Picker', {
beforeEach: function () {
this.data = {
foo: {
fields: {
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"},
QUnit.module(
"Web Widget One2Many Product Picker",
{
beforeEach: function() {
this.data = {
foo: {
fields: {
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: [
{id: 1, line_ids: [1, 2], currency_id: 1, display_name: "FT01"},
],
},
line: {
fields: {
name: {string: "Product Name", type: "string"},
product_id: {string: "Product", type: "many2one", relation: "product"},
product_uom: {string: "UoM", type: "many2one", relation: "uom"},
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"},
line: {
fields: {
name: {string: "Product Name", type: "string"},
product_id: {
string: "Product",
type: "many2one",
relation: "product",
},
product_uom: {
string: "UoM",
type: "many2one",
relation: "uom",
},
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: [
{id: 1, name: "Large Cabinet", product_id: 1, product_uom: 1, product_uom_qty: 3, price_unit: 9.99, price_reduce: 9.00, foo_id: 1},
{id: 2, name: "Cabinet with Doors", product_id: 2, product_uom: 1, product_uom_qty: 8, price_unit: 42.99, price_reduce: 40.00, foo_id: 1},
],
},
product: {
fields: {
name: {string : "Product name", type: "char"},
display_name: {string : "Display Name", type: "char"},
list_price: {string: "Price", type: "float"},
image_medium: {string: "Image Medium", type: "binary"},
uom_category_id: {string: "Category", type: "many2one", relation: "uom_category"},
product: {
fields: {
name: {string: "Product name", type: "char"},
display_name: {string: "Display Name", type: "char"},
list_price: {string: "Price", type: "float"},
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: [
{id: 1, name: "Large Cabinet", display_name: "Large Cabinet", list_price: 9.99, image_medium: "", uom_category_id: 1},
{id: 2, name: "Cabinet with Doors", display_name: "Cabinet with Doors", list_price: 42.0, image_medium: "", uom_category_id: 1},
],
},
uom_category: {
fields: {
display_name: {string : "Display Name", type: "char"},
uom_category: {
fields: {
display_name: {string: "Display Name", type: "char"},
},
records: [{id: 1, display_name: "Unit(s)"}],
},
records: [
{id: 1, display_name: "Unit(s)"},
],
},
uom: {
fields: {
name: {string: "Name", type: "char"},
uom: {
fields: {
name: {string: "Name", type: "char"},
},
records: [{id: 1, name: "Unit(s)"}],
},
records: [
{id: 1, name: "Unit(s)"},
],
},
currency: {
fields: {
name: {string: "Name", type: "char"},
symbol: {string: "Symbol", type: "char"},
currency: {
fields: {
name: {string: "Name", type: "char"},
symbol: {string: "Symbol", type: "char"},
},
records: [{id: 1, name: "Eur", symbol: "€"}],
},
records: [
{id: 1, name: "Eur", symbol: "€"},
],
},
};
};
},
},
}, function () {
QUnit.test('Load widget', function (assert) {
assert.expect(4);
function() {
QUnit.test("Load widget", function(assert) {
assert.expect(4);
var form = createView({
View: FormView,
model: 'foo',
data: this.data,
arch: getArch(),
res_id: 1,
viewOptions: {
ids: [1],
index: 0,
},
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/foo/read') {
assert.deepEqual(args.args[1], ['currency_id', 'line_ids', 'display_name'],
'should only read "currency_id", "line_ids" and "display_name"');
return $.when(this.data.foo.records);
} else if (route === '/web/dataset/call_kw/line/read') {
assert.deepEqual(args.args[1], ['name', 'product_id', 'price_reduce', 'price_unit', 'foo_id', 'product_uom_qty', 'product_uom'],
'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);
},
var form = createView({
View: FormView,
model: "foo",
data: this.data,
arch: getArch(),
res_id: 1,
viewOptions: {
ids: [1],
index: 0,
},
mockRPC: function(route, args) {
if (route === "/web/dataset/call_kw/foo/read") {
assert.deepEqual(
args.args[1],
["currency_id", "line_ids", "display_name"],
'should only read "currency_id", "line_ids" and "display_name"'
);
return $.when(this.data.foo.records);
} else if (route === "/web/dataset/call_kw/line/read") {
assert.deepEqual(
args.args[1],
[
"name",
"product_id",
"price_reduce",
"price_unit",
"foo_id",
"product_uom_qty",
"product_uom",
],
'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>
<template id="_assets_backend_helpers" inherit_id="web._assets_backend_helpers">
<xpath expr=".">
<link type="text/css" rel="stylesheet"
href="/web_widget_one2many_product_picker/static/src/scss/_variables.scss"/>
<link
type="text/css"
rel="stylesheet"
href="/web_widget_one2many_product_picker/static/src/scss/_variables.scss"
/>
</xpath>
</template>
<template id="_assets_bootstrap" inherit_id="web._assets_bootstrap">
<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>
</template>
<template id="assets_backend" name="account assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link type="text/css" rel="stylesheet"
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/tools.js"></script>
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js"></script>
<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 type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_create_form.js"></script>
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form_view.js"></script>
<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 type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/basic_model.js"></script>
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/views/basic_controller.js"></script>
<script type="text/javascript" src="/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js"></script>
<link
type="text/css"
rel="stylesheet"
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/tools.js"
/>
<script
type="text/javascript"
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/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>
</template>
<template id="qunit_suite" name="base_import_tests" inherit_id="web.qunit_suite">
<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>
</template>
</odoo>