diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-02-12 18:34:43 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-02-12 18:34:43 +0100 |
| commit | adeea2a88db4443b2b426877a28bb1471c2f5db1 (patch) | |
| tree | 2f43fecc9649947d3061e8c482219fa167b3385a /addons/web | |
| parent | 317f5f11112266128ac2292ecfafcd92cda74fd0 (diff) | |
| parent | 0f02dcb63918228d48df50f2d5805b7fda25c14a (diff) | |
[MERGE] forward port branch saas-11.2 up to 0f02dcb6391
Diffstat (limited to 'addons/web')
32 files changed, 754 insertions, 257 deletions
diff --git a/addons/web/i18n/de.po b/addons/web/i18n/de.po index c995bce1747..e5a7ba5c1ab 100644 --- a/addons/web/i18n/de.po +++ b/addons/web/i18n/de.po @@ -4,34 +4,35 @@ # # Translators: # Henry Mineehen <info@mineehen.de>, 2017 -# Martin Trigaux <mat@odoo.com>, 2017 +# Martin Trigaux, 2017 # darenkster <inactive+darenkster@transifex.com>, 2017 -# Andi <flimflam@salasar.de>, 2017 +# Andi, 2017 # Ralf Hilgenstock <rh@dialoge.info>, 2017 # Ermin Trevisan <trevi@twanda.com>, 2017 # Anja Funk <anja.funk@afimage.de>, 2017 -# Wolfgang Taferner <wolfgang.transifex@service.wt-io-it.at>, 2017 +# Wolfgang Taferner, 2017 # Thorsten Brandt <thorsten.brandt@web.de>, 2017 # Thorsten Vocks <thorsten.vocks@openbig.org>, 2017 # Fabian Liesch <fabian.liesch@gmail.com>, 2017 # Stefan Hartenfels <stefan.hartenfels@camptocamp.com>, 2017 # JackTheHunter <Maximilian.N98@gmail.com>, 2017 -# Niki Waibel <niki.waibel@gmail.com>, 2017 +# Niki Waibel, 2017 # Mathias Neef <mn@copado.de>, 2017 # Krakataua Katarajtu <shlok@hotmail.de>, 2017 # Rudolf Schnapka <rs@techno-flex.de>, 2017 # Tina Rittmüller <tr@ife.de>, 2017 -# Renzo Meister <info@jamotion.ch>, 2017 +# Renzo Meister, 2017 # aNj <anj2j@yahoo.de>, 2017 # AppleMentalist <felix.schnatbaum@gmail.com>, 2017 # DE T1 <e2f8846@yahoo.com>, 2017 +# e2f <projects@e2f.com>, 2018 msgid "" msgstr "" "Project-Id-Version: Odoo Server saas~11.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-12-13 14:37+0000\n" "PO-Revision-Date: 2017-12-13 14:37+0000\n" -"Last-Translator: DE T1 <e2f8846@yahoo.com>, 2017\n" +"Last-Translator: e2f <projects@e2f.com>, 2018\n" "Language-Team: German (https://www.transifex.com/odoo/teams/41243/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -244,12 +245,12 @@ msgstr "" #. module: web #: model:ir.ui.view,arch_db:web.menu_secondary msgid "<span class=\"oe_logo_edit\">Edit Company data</span>" -msgstr "" +msgstr "<span class=\"oe_logo_edit\"/>" #. module: web #: model:ir.ui.view,arch_db:web.menu_secondary msgid "<span>Odoo</span>" -msgstr "" +msgstr "<span/>" #. module: web #. openerp-web @@ -2911,7 +2912,7 @@ msgid "" "Your installation of Wkhtmltopdf seems to be broken. The report will be " "shown in html.<br><br><a href=\"http://wkhtmltopdf.org/\" " "target=\"_blank\">wkhtmltopdf.org</a>" -msgstr "" +msgstr "<br><br><a href=\"http://wkhtmltopdf.org/\" target=\"_blank\"></a>" #. module: web #. openerp-web diff --git a/addons/web/i18n/es.po b/addons/web/i18n/es.po index b8fdb9fd654..ef95370064a 100644 --- a/addons/web/i18n/es.po +++ b/addons/web/i18n/es.po @@ -4,8 +4,8 @@ # # Translators: # Ana Juaristi <ajuaristio@gmail.com>, 2016 -# Martin Trigaux <mat@odoo.com>, 2016 -# Antonio Trueba <atgayol@gmail.com>, 2016 +# Martin Trigaux, 2016 +# Antonio Trueba, 2016 # Pedro M. Baeza <pedro.baeza@gmail.com>, 2016 # José Vicente <txusev@gmail.com>, 2016 # Jesús Alan Ramos Rodríguez <alan.ramos@jarsa.com.mx>, 2016 @@ -29,13 +29,14 @@ # Esteban Echeverry <tebanep@nubark.com>, 2017 # Gelo Joga Landoo <gj@landoo.es>, 2017 # Cristian Salamea <ovnicraft@gmail.com>, 2017 +# e2f <projects@e2f.com>, 2018 msgid "" msgstr "" "Project-Id-Version: Odoo Server saas~11.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-12-13 14:37+0000\n" "PO-Revision-Date: 2017-12-13 14:37+0000\n" -"Last-Translator: Cristian Salamea <ovnicraft@gmail.com>, 2017\n" +"Last-Translator: e2f <projects@e2f.com>, 2018\n" "Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -62,7 +63,7 @@ msgstr " [Yo]" #: code:addons/web/static/src/js/chrome/search_filters.js:226 #, python-format msgid " and " -msgstr "" +msgstr "y" #. module: web #. openerp-web @@ -92,7 +93,7 @@ msgstr "No se pudo cargar la vista" #: code:addons/web/static/src/xml/base.xml:612 #, python-format msgid "# Code editor" -msgstr "" +msgstr "# editor de código" #. module: web #. openerp-web @@ -214,7 +215,7 @@ msgstr "'%s' no es un entero correcto" #: code:addons/web/static/src/js/fields/field_utils.js:483 #, python-format msgid "'%s' is not a correct monetary field" -msgstr "" +msgstr "\"%s\" no es un campo monetario correcto" #. module: web #. openerp-web @@ -242,7 +243,7 @@ msgstr "(sin etiqueta)" #: code:addons/web/static/src/js/fields/field_utils.js:239 #, python-format msgid "1 record" -msgstr "" +msgstr "1 registro" #. module: web #: model:ir.ui.view,arch_db:web.menu_secondary @@ -262,20 +263,23 @@ msgid "" "A popup window with your report was blocked. You may need to change your " "browser settings to allow popup windows for this page." msgstr "" +"Se ha bloqueado una ventana emergente que contenía su informe. Puede cambiar" +" los ajustes de su navegador para permitir ventanas emergentes en esta " +"página." #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:589 #, python-format msgid "ALL" -msgstr "" +msgstr "TODO" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:590 #, python-format msgid "ANY" -msgstr "" +msgstr "CUALQUIERA" #. module: web #. openerp-web @@ -378,7 +382,7 @@ msgstr "Añadir grupo personalizado" #: code:addons/web/static/src/xml/base.xml:597 #, python-format msgid "Add filter" -msgstr "" +msgstr "Añadir filtro" #. module: web #. openerp-web @@ -392,14 +396,14 @@ msgstr "Añadir nueva columna" #: code:addons/web/static/src/xml/base.xml:657 #, python-format msgid "Add new value" -msgstr "" +msgstr "Añadir valor nuevo" #. module: web #. openerp-web #: code:addons/web/static/src/js/fields/basic_fields.js:1576 #, python-format msgid "Add to Favorites" -msgstr "" +msgstr "Añadir a favoritos" #. module: web #. openerp-web @@ -457,7 +461,7 @@ msgstr "Todos los usuarios" #: code:addons/web/static/src/xml/base.xml:146 #, python-format msgid "An error occurred" -msgstr "" +msgstr "Se ha producido un error" #. module: web #. openerp-web @@ -467,6 +471,8 @@ msgid "" "An unknown CORS error occured. The error probably originates from a " "JavaScript file served from a different origin." msgstr "" +"Se ha producido un error desconocido de CORS. El error es debido " +"probablemente a un archivo de JavaScript procedente de un origen distinto." #. module: web #. openerp-web @@ -563,6 +569,8 @@ msgid "" "Binary fields can not be exported to Excel unless their content is " "base64-encoded. That does not seem to be the case for %s." msgstr "" +"No es posible exportar los campos binarios a Excel a menos que su contenido " +"tenga codificación de base64. Ese no parece ser el caso de %s." #. module: web #. openerp-web @@ -626,7 +634,7 @@ msgstr "Cancelar" #: code:addons/web/static/src/js/views/graph/graph_renderer.js:68 #, python-format msgid "Cannot render chart with mode : " -msgstr "" +msgstr "No es posible renderizar el gráfico con el modo:" #. module: web #. openerp-web @@ -744,7 +752,7 @@ msgstr "¡Copiado!" #: code:addons/web/static/src/xml/base.xml:144 #, python-format msgid "Copy the full error to clipboard" -msgstr "" +msgstr "Copiar el error al completo en el portapapeles" #. module: web #. openerp-web @@ -791,7 +799,7 @@ msgstr "Crear" #: code:addons/web/static/src/js/views/form/form_controller.js:479 #, python-format msgid "Create " -msgstr "" +msgstr "Crear" #. module: web #. openerp-web @@ -926,7 +934,7 @@ msgstr "Descartar" #: code:addons/web/static/src/js/widgets/data_export.js:336 #, python-format msgid "Do you really want to delete this export template?" -msgstr "" +msgstr "¿Seguro que desea eliminar esta plantilla de exportación?" #. module: web #. openerp-web @@ -954,7 +962,7 @@ msgstr "Dominio" #: code:addons/web/static/src/js/widgets/domain_selector.js:520 #, python-format msgid "Domain error" -msgstr "" +msgstr "Error de dominio" #. module: web #. openerp-web @@ -1016,7 +1024,7 @@ msgstr "Editar columna" #: code:addons/web/static/src/xml/base.xml:565 #, python-format msgid "Edit Domain" -msgstr "" +msgstr "Editar dominio" #. module: web #. openerp-web @@ -1070,7 +1078,7 @@ msgstr "Calendarios de todos" #: code:addons/web/static/src/js/views/calendar/calendar_model.js:511 #, python-format msgid "Everything" -msgstr "" +msgstr "Todo" #. module: web #. openerp-web @@ -1307,7 +1315,7 @@ msgstr "" #: code:addons/web/static/src/js/core/dialog.js:250 #, python-format msgid "I am sure about this." -msgstr "" +msgstr "Estoy seguro de esto." #. module: web #. openerp-web @@ -1361,7 +1369,7 @@ msgstr "" #: code:addons/web/static/src/xml/base.xml:563 #, python-format msgid "Invalid domain" -msgstr "" +msgstr "Dominio inválido" #. module: web #. openerp-web @@ -1369,21 +1377,21 @@ msgstr "" #: code:addons/web/static/src/xml/base.xml:694 #, python-format msgid "Invalid field chain" -msgstr "" +msgstr "Cadena de campos inválida" #. module: web #. openerp-web #: code:addons/web/static/src/js/views/graph/graph_renderer.js:67 #, python-format msgid "Invalid mode for chart" -msgstr "" +msgstr "Modo de gráfico inválido" #. module: web #. openerp-web #: code:addons/web/static/src/js/widgets/debug_manager.js:208 #, python-format msgid "JS Mobile Tests" -msgstr "" +msgstr "Pruebas de JS Mobile" #. module: web #. openerp-web @@ -1491,7 +1499,7 @@ msgstr "" #. module: web #: model:ir.ui.view,arch_db:web.external_layout_boxed msgid "Mail:" -msgstr "" +msgstr "Correo:" #. module: web #: model:ir.ui.view,arch_db:web.login_layout @@ -1511,21 +1519,21 @@ msgstr "Gestionar Filtros" #: code:addons/web/static/src/xml/base.xml:596 #, python-format msgid "Match" -msgstr "" +msgstr "Combinar" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:603 #, python-format msgid "Match records with" -msgstr "" +msgstr "Combinar registros con" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:601 #, python-format msgid "Match records with the following rule:" -msgstr "" +msgstr "Combinar registros con la siguiente regla:" #. module: web #. openerp-web @@ -1634,7 +1642,7 @@ msgstr "Mi cuenta Odoo.com" #: code:addons/web/static/src/xml/base.xml:591 #, python-format msgid "NONE" -msgstr "" +msgstr "NINGUNO" #. module: web #. openerp-web @@ -1728,7 +1736,7 @@ msgstr "Metadatos no disponibles" #: code:addons/web/static/src/js/fields/field_utils.js:237 #, python-format msgid "No records" -msgstr "" +msgstr "No hay registros" #. module: web #. openerp-web @@ -1780,7 +1788,7 @@ msgstr "Odoo Apps estará disponible en breve" #: code:addons/web/static/src/js/services/crash_manager.js:156 #, python-format msgid "Odoo Client Error" -msgstr "" +msgstr "Error del cliente de Odoo" #. module: web #. openerp-web @@ -1794,14 +1802,14 @@ msgstr "Odoo Enterprise" #: code:addons/web/static/src/js/services/crash_manager.js:118 #, python-format msgid "Odoo Error" -msgstr "" +msgstr "Error de Odoo" #. module: web #. openerp-web #: code:addons/web/static/src/js/services/crash_manager.js:58 #, python-format msgid "Odoo Session Expired" -msgstr "" +msgstr "Sesión de Odoo expirada" #. module: web #. openerp-web @@ -1809,7 +1817,7 @@ msgstr "" #: code:addons/web/static/src/js/services/crash_manager.js:197 #, python-format msgid "Odoo Warning" -msgstr "" +msgstr "Advertencia de Odoo" #. module: web #. openerp-web @@ -1864,6 +1872,8 @@ msgstr "El único valor válido es un entero." msgid "" "Only employee can access this database. Please contact the administrator." msgstr "" +"Solo los empleados pueden acceder a esta base de datos. Contacte con el " +"administrador." #. module: web #. openerp-web @@ -1910,13 +1920,13 @@ msgid "" " <span class=\"page\"/>\n" " of\n" " <span class=\"topage\"/>" -msgstr "" +msgstr "<span class=\"page\"/><span class=\"topage\"/>" #. module: web #: model:ir.ui.view,arch_db:web.external_layout_boxed #: model:ir.ui.view,arch_db:web.external_layout_standard msgid "Page: <span class=\"page\"/> / <span class=\"topage\"/>" -msgstr "" +msgstr "<span class=\"page\"/><span class=\"topage\"/>" #. module: web #: model:ir.ui.view,arch_db:web.login @@ -1969,7 +1979,7 @@ msgstr "Pivote" #: code:addons/web/static/src/js/views/list/list_renderer.js:327 #, python-format msgid "Please click on the \"save\" button first" -msgstr "" +msgstr "Haga clic primero en el botón \"Guardar\"" #. module: web #. openerp-web @@ -2025,6 +2035,8 @@ msgstr "" msgid "" "Please use the copy button to report the error to your support service." msgstr "" +"Utilice el botón \"Copiar\" para informar al servicio de asistencia del " +"error." #. module: web #: model:ir.ui.view,arch_db:web.menu_secondary @@ -2080,7 +2092,7 @@ msgstr "" #: code:addons/web/static/src/js/widgets/model_field_selector.js:455 #, python-format msgid "Relation not allowed" -msgstr "" +msgstr "Relación no permitida" #. module: web #. openerp-web @@ -2108,7 +2120,7 @@ msgstr "Eliminar Todo" #: code:addons/web/static/src/js/fields/basic_fields.js:1576 #, python-format msgid "Remove from Favorites" -msgstr "" +msgstr "Eliminar de favoritos" #. module: web #. openerp-web @@ -2136,14 +2148,14 @@ msgstr "Restaurar" #: code:addons/web/static/src/xml/kanban.xml:27 #, python-format msgid "Restore Records" -msgstr "" +msgstr "Recuperar registros" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:223 #, python-format msgid "Run JS Mobile Tests" -msgstr "" +msgstr "Ejecutar pruebas de JS Mobile" #. module: web #. openerp-web @@ -2255,7 +2267,7 @@ msgstr "Buscar: " #: code:addons/web/static/src/xml/base.xml:151 #, python-format msgid "See details" -msgstr "" +msgstr "Ver detalles" #. module: web #. openerp-web @@ -2276,7 +2288,7 @@ msgstr "Escoge <i class=\"fa fa-database\"/>" #: code:addons/web/static/src/xml/base.xml:567 #, python-format msgid "Select a model to add a filter." -msgstr "" +msgstr "Seleccione un modelo para añadir un filtro." #. module: web #. openerp-web @@ -2394,7 +2406,7 @@ msgstr "Activar" #: code:addons/web/static/src/js/widgets/domain_selector.js:567 #, python-format msgid "Syntax error" -msgstr "" +msgstr "Error de sintaxis" #. module: web #. openerp-web @@ -2408,7 +2420,7 @@ msgstr "Tome un minuto para ir por un café,<br />porque está cargando ..." #: code:addons/web/static/src/js/widgets/switch_company_menu.js:44 #, python-format msgid "Tap on the list to change company" -msgstr "" +msgstr "Pulse en la lista para cambiar la empresa" #. module: web #. openerp-web @@ -2429,7 +2441,7 @@ msgstr "Tel.:" #: code:addons/web/static/src/js/widgets/domain_selector.js:567 #, python-format msgid "The domain you entered is not properly formed" -msgstr "" +msgstr "El dominio introducido no se encuentra debidamente formado" #. module: web #. openerp-web @@ -2439,6 +2451,8 @@ msgid "" "The field chain is not valid. Did you maybe use a non-existing field name or" " followed a non-relational field?" msgstr "" +"La cadena de campos no es válida. ¿Es posible que haya utilizado un nombre " +"de campo inexistente o seguido un campo no relacional?" #. module: web #. openerp-web @@ -2477,6 +2491,8 @@ msgid "" "The record has been modified, your changes will be discarded. Do you want to" " proceed?" msgstr "" +"El registro ha sido modificado. Sus cambios se descartarán. ¿Desea " +"continuar?" #. module: web #. openerp-web @@ -2503,6 +2519,9 @@ msgid "" "There are too many rows (%s rows, limit: 65535) to export as Excel 97-2003 " "(.xls) format. Consider splitting the export." msgstr "" +"Hay demasiadas filas (%s filas; límite: 65535) para llevar a cabo una " +"exportación correcta a Excel 97-2003 en formato .xls. Considere dividir la " +"exportación." #. module: web #. openerp-web @@ -2516,7 +2535,7 @@ msgstr "Hubo un problema subiendo el fichero" #: code:addons/web/static/src/js/widgets/domain_selector.js:459 #, python-format msgid "This domain is not supported." -msgstr "" +msgstr "Este dominio no es compatible." #. module: web #. openerp-web @@ -2656,7 +2675,7 @@ msgstr "Indefinido" #: code:addons/web/static/src/js/chrome/abstract_web_client.js:187 #, python-format msgid "Unknown CORS error" -msgstr "" +msgstr "Error de CORS desconocido" #. module: web #. openerp-web @@ -2677,7 +2696,7 @@ msgstr "Sin nombre" #: code:addons/web/static/src/js/views/pivot/pivot_view.js:96 #, python-format msgid "Untitled" -msgstr "" +msgstr "Sin título" #. module: web #. openerp-web @@ -2765,23 +2784,23 @@ msgstr "Aviso" #. module: web #: model:ir.ui.view,arch_db:web.benchmark_suite msgid "Web Benchmarks" -msgstr "" +msgstr "Criterios de red" #. module: web #: model:ir.ui.view,arch_db:web.qunit_mobile_suite msgid "Web Mobile Tests" -msgstr "" +msgstr "Pruebas de red móvil" #. module: web #: model:ir.ui.view,arch_db:web.qunit_suite msgid "Web Tests" -msgstr "" +msgstr "Pruebas de red" #. module: web #: model:ir.ui.view,arch_db:web.external_layout_boxed #: model:ir.ui.view,arch_db:web.external_layout_standard msgid "Web:" -msgstr "" +msgstr "Red:" #. module: web #. openerp-web @@ -2803,7 +2822,7 @@ msgstr "Semana " #: code:addons/web/static/src/js/widgets/rainbow_man.js:45 #, python-format msgid "Well Done!" -msgstr "" +msgstr "¡Bien hecho!" #. module: web #. openerp-web @@ -2867,6 +2886,7 @@ msgstr "Está creando un nuevo %s, ¿está seguro de que no existe ya?" #, python-format msgid "You cannot follow relations for this field chain construction" msgstr "" +"No puede seguir las relaciones de esta construcción de cadena de campos" #. module: web #: code:addons/web/controllers/main.py:787 @@ -2936,7 +2956,7 @@ msgstr "" #: code:addons/web/static/src/js/views/kanban/kanban_record.js:199 #, python-format msgid "[No widget %s]" -msgstr "" +msgstr "[No hay widget %s]" #. module: web #. openerp-web @@ -2978,7 +2998,7 @@ msgstr "Hace una hora" #: code:addons/web/static/src/xml/base.xml:596 #, python-format msgid "all records" -msgstr "" +msgstr "todos los registros" #. module: web #: model:ir.model,name:web.model_base @@ -2990,7 +3010,7 @@ msgstr "base" #: code:addons/web/static/src/js/widgets/domain_selector.js:28 #, python-format msgid "child of" -msgstr "" +msgstr "descendiente de" #. module: web #. openerp-web @@ -3050,14 +3070,14 @@ msgstr "es" #: code:addons/web/static/src/js/chrome/search_filters.js:183 #, python-format msgid "is after" -msgstr "" +msgstr "está detrás de" #. module: web #. openerp-web #: code:addons/web/static/src/js/chrome/search_filters.js:184 #, python-format msgid "is before" -msgstr "" +msgstr "está antes de" #. module: web #. openerp-web @@ -3168,7 +3188,7 @@ msgstr "menor o igual que" #: code:addons/web/static/src/xml/base.xml:686 #, python-format msgid "not" -msgstr "" +msgstr "No" #. module: web #. openerp-web @@ -3189,35 +3209,35 @@ msgstr "no es un número válido" #: code:addons/web/static/src/js/widgets/domain_selector.js:24 #, python-format msgid "not contains" -msgstr "" +msgstr "no contiene" #. module: web #. openerp-web #: code:addons/web/static/src/js/widgets/domain_selector.js:26 #, python-format msgid "not in" -msgstr "" +msgstr "no en" #. module: web #. openerp-web #: code:addons/web/static/src/js/widgets/domain_selector.js:658 #, python-format msgid "not set (false)" -msgstr "" +msgstr "no fijado (falso)" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:605 #, python-format msgid "of the following rules:" -msgstr "" +msgstr "de la siguientes reglas:" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:620 #, python-format msgid "of:" -msgstr "" +msgstr "de:" #. module: web #. openerp-web @@ -3232,14 +3252,14 @@ msgstr "o" #: code:addons/web/static/src/js/widgets/domain_selector.js:29 #, python-format msgid "parent of" -msgstr "" +msgstr "matriz de" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:561 #, python-format msgid "record(s)" -msgstr "" +msgstr "registro(s)" #. module: web #. openerp-web @@ -3253,11 +3273,11 @@ msgstr "restante)" #: code:addons/web/static/src/xml/base.xml:686 #, python-format msgid "set" -msgstr "" +msgstr "fijado" #. module: web #. openerp-web #: code:addons/web/static/src/js/widgets/domain_selector.js:658 #, python-format msgid "set (true)" -msgstr "" +msgstr "fijado (verdadero)" diff --git a/addons/web/i18n/fr.po b/addons/web/i18n/fr.po index b3b0001596c..507e9abdf53 100644 --- a/addons/web/i18n/fr.po +++ b/addons/web/i18n/fr.po @@ -6,25 +6,26 @@ # Xavier Belmere <Info@cartmeleon.com>, 2016 # Lionel Sausin <ls@numerigraphe.com>, 2017 # Melanie Bernard <mbe@odoo.com>, 2017 -# Martin Trigaux <mat@odoo.com>, 2017 +# Martin Trigaux, 2017 # Olivier Lenoir <olivier.lenoir@free.fr>, 2017 # Maxime Chambreuil <mchambreuil@ursainfosystems.com>, 2017 # Olivier Dony <odo@odoo.com>, 2017 # Richard Mathot <rim@odoo.com>, 2017 # Eloïse Stilmant <est@odoo.com>, 2017 # Jérémy Donas <LeDistordu@users.noreply.github.com>, 2017 -# Florian Hatat <mininet@wanadoo.fr>, 2017 +# Florian Hatat, 2017 # Christophe CHAUVET <christophe.chauvet@gmail.com>, 2017 # Fabien Pinckaers <fp@openerp.com>, 2017 -# Jerther <jerther@gmail.com>, 2017 +# Jerther, 2017 # Rémi CAZENAVE <remi@le-filament.com>, 2017 +# e2f <projects@e2f.com>, 2018 msgid "" msgstr "" "Project-Id-Version: Odoo Server saas~11.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-12-13 14:37+0000\n" "PO-Revision-Date: 2017-12-13 14:37+0000\n" -"Last-Translator: Rémi CAZENAVE <remi@le-filament.com>, 2017\n" +"Last-Translator: e2f <projects@e2f.com>, 2018\n" "Language-Team: French (https://www.transifex.com/odoo/teams/41243/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1899,7 +1900,7 @@ msgid "" " <span class=\"page\"/>\n" " of\n" " <span class=\"topage\"/>" -msgstr "" +msgstr "<span class=\"page\"/><span class=\"topage\"/>" #. module: web #: model:ir.ui.view,arch_db:web.external_layout_boxed @@ -2916,7 +2917,7 @@ msgid "" "Your installation of Wkhtmltopdf seems to be broken. The report will be " "shown in html.<br><br><a href=\"http://wkhtmltopdf.org/\" " "target=\"_blank\">wkhtmltopdf.org</a>" -msgstr "" +msgstr "<br><br><a href=\"http://wkhtmltopdf.org/\" target=\"_blank\"></a>" #. module: web #. openerp-web diff --git a/addons/web/static/src/js/_deprecated/data.js b/addons/web/static/src/js/_deprecated/data.js index be5496c55c7..eb70f330076 100644 --- a/addons/web/static/src/js/_deprecated/data.js +++ b/addons/web/static/src/js/_deprecated/data.js @@ -314,6 +314,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, { */ init: function (parent, model, context) { mixins.PropertiesMixin.init.call(this); + this.setParent(parent); this.model = model; this.context = context || {}; this.index = null; diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index 2dd69d7426c..b4b192c1ea9 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -245,6 +245,10 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, { * identified by strings. Each time a title part is changed, all parts * are gathered, ordered by alphabetical order and displayed in the title * of the window separated by ``-``. + * + * @private + * @param {string} part + * @param {string} title */ set_title_part: function (part, title) { var tmp = _.clone(this.get("title_part")); diff --git a/addons/web/static/src/js/chrome/control_panel.js b/addons/web/static/src/js/chrome/control_panel.js index fb8bd5dec6c..16b22761581 100644 --- a/addons/web/static/src/js/chrome/control_panel.js +++ b/addons/web/static/src/js/chrome/control_panel.js @@ -79,7 +79,7 @@ var ControlPanel = Widget.extend({ this.template = template; } - this.bus = new Bus(); + this.bus = new Bus(this); this.bus.on("update", this, this.update); }, /** diff --git a/addons/web/static/src/js/chrome/root_widget.js b/addons/web/static/src/js/chrome/root_widget.js new file mode 100644 index 00000000000..9cf0515b134 --- /dev/null +++ b/addons/web/static/src/js/chrome/root_widget.js @@ -0,0 +1,7 @@ +odoo.define('root.widget', function (require) { +"use strict"; + +var webClient = require('web.web_client'); + +return webClient; +}); diff --git a/addons/web/static/src/js/core/abstract_service.js b/addons/web/static/src/js/core/abstract_service.js index cbd6c1322ca..14efef09b39 100644 --- a/addons/web/static/src/js/core/abstract_service.js +++ b/addons/web/static/src/js/core/abstract_service.js @@ -6,7 +6,6 @@ var Mixins = require('web.mixins'); var ServicesMixin = require('web.ServicesMixin'); var AbstractService = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { - Services: [], dependencies: [], name: null, init: function (parent) { @@ -15,13 +14,5 @@ var AbstractService = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, { }, }); -var realExtend = AbstractService.extend; - -AbstractService.extend = function () { - var Service = realExtend.apply(this, arguments); - AbstractService.prototype.Services.push(Service); - return Service; -}; - return AbstractService; }); diff --git a/addons/web/static/src/js/core/bus.js b/addons/web/static/src/js/core/bus.js index 92278e0f1d6..5257453edab 100644 --- a/addons/web/static/src/js/core/bus.js +++ b/addons/web/static/src/js/core/bus.js @@ -10,8 +10,9 @@ var mixins = require('web.mixins'); * @class Bus */ return Class.extend(mixins.EventDispatcherMixin, { - init: function () { - mixins.EventDispatcherMixin.init.call(this, arguments); + init: function (parent) { + mixins.EventDispatcherMixin.init.call(this); + this.setParent(parent); }, }); diff --git a/addons/web/static/src/js/core/registry.js b/addons/web/static/src/js/core/registry.js index d70c09e4ae0..f925b0ea30a 100644 --- a/addons/web/static/src/js/core/registry.js +++ b/addons/web/static/src/js/core/registry.js @@ -25,6 +25,7 @@ var Registry = Class.extend({ */ init: function (mapping) { this.map = Object.create(mapping || null); + this.listeners = []; // listening callbacks on newly added items. }, //-------------------------------------------------------------------------- @@ -34,12 +35,17 @@ var Registry = Class.extend({ /** * Add a key (and a value) to the registry. * + * Notify the listeners on newly added item in the registry. + * * @param {string} key * @param {any} value * @returns {Registry} can be used to chain add calls. */ add: function (key, value) { this.map[key] = value; + _.each(this.listeners, function (callback) { + callback(key, value); + }); return this; }, /** @@ -89,6 +95,14 @@ var Registry = Class.extend({ } return null; }, + /** + * Register a callback to execute when items are added to the registry. + * + * @param {function} callback function with parameters (key, value). + */ + onAdd: function (callback) { + this.listeners.push(callback); + }, }); return Registry; diff --git a/addons/web/static/src/js/core/service_mixins.js b/addons/web/static/src/js/core/service_mixins.js index d5abf727704..4ab0a361988 100644 --- a/addons/web/static/src/js/core/service_mixins.js +++ b/addons/web/static/src/js/core/service_mixins.js @@ -1,58 +1,86 @@ odoo.define('web.ServiceProviderMixin', function (require) { "use strict"; -var AbstractService = require('web.AbstractService'); -var utils = require('web.utils'); +var core = require('web.core'); var ServiceProviderMixin = { - services: {}, - init: function () { + services: {}, // dict containing deployed service instances + UndeployedServices: {}, // dict containing classes of undeployed services + /** + * @override + */ + init: function (parent) { + var self = this; // to properly instantiate services with this as parent, this mixin // assumes that it is used along the EventDispatcherMixin, and that // EventDispatchedMixin's init is called first - var self = this; - // as EventDispatcherMixin's init is already called, this handler has to - // be bound manually + // as EventDispatcherMixin's init is already called, this handler has + // to be bound manually this.on('call_service', this, this._call_service.bind(this)); - var sortedServices = this._sortServices(AbstractService.prototype.Services); - _.each(sortedServices, function (Service) { - var service = new Service(self); - self.services[service.name] = service; + // add already registered services from the service registry + _.each(core.serviceRegistry.map, function (Service, serviceName) { + self.UndeployedServices[serviceName] = Service; + }); + this._deployServices(); + + // listen on newly added services + core.serviceRegistry.onAdd(function (serviceName, Service) { + self.UndeployedServices[serviceName] = Service; + self._deployServices(); }); }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _deployServices: function () { + var self = this; + var done = false; + while (!done) { + var Service = _.find(this.UndeployedServices, function (Service) { + // no missing dependency + return !_.some(Service.prototype.dependencies, function (depName) { + return !self.services[depName]; + }); + }); + if (Service) { + this.services[Service.prototype.name] = new Service(this); + delete this.UndeployedServices[Service.prototype.name]; + } else { + done = true; + } + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Call the 'service', using data from the 'event' that + * has triggered the service call. + * + * For the ajax service, the arguments are extended with + * the target so that it can call back the caller. + * + * @private + * @param {OdooEvent} event + */ _call_service: function (event) { - var service = this.services[event.data.service]; var args = event.data.args || []; - // ajax service uses an extra 'target' argument for rpc if (event.data.service === 'ajax' && event.data.method === 'rpc') { + // ajax service uses an extra 'target' argument for rpc args = args.concat(event.target); } + var service = this.services[event.data.service]; var result = service[event.data.method].apply(service, args); event.data.callback(result); }, - _sortServices: function (services) { - var nodes = {}; - // Create nodes (services) - _.each(services, function (Service) { - nodes[Service.prototype.name] = Service.prototype.dependencies; - }); - var sorted; - try { - sorted = utils.topologicalSort(nodes); - } catch (err) { - console.warn('topologicalSort Error:', err.message); - sorted = nodes; - } - // Sort services based on sorted - // Note: we convert sorted to an object key=>index for efficiency - var sortedObj = _.invert(_.object(_.pairs(sorted))); - sorted = _.sortBy(services, function (Service) { - return sortedObj[Service.prototype.name]; - }); - - return sorted; - }, }; return ServiceProviderMixin; @@ -69,6 +97,11 @@ var rpc = require('web.rpc'); * @name ServicesMixin */ var ServicesMixin = { + /** + * @param {string} service + * @param {string} method + * @return {any} result of the service called + */ call: function (service, method) { var args = Array.prototype.slice.call(arguments, 2); var result; diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js index e17b1b0c7fc..4760867e99f 100644 --- a/addons/web/static/src/js/core/session.js +++ b/addons/web/static/src/js/core/session.js @@ -29,7 +29,8 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { "override_session" is set to true. */ init: function (parent, origin, options) { - mixins.EventDispatcherMixin.init.call(this, parent); + mixins.EventDispatcherMixin.init.call(this); + this.setParent(parent); options = options || {}; this.module_list = (options.modules && options.modules.slice()) || (window.odoo._modules && window.odoo._modules.slice()) || []; this.server = null; diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index 0b4af722203..1bdd9e70a69 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -301,37 +301,6 @@ var utils = { return fn(mod, Math.floor(x)); }, /** - * Topological sort of nodes - * - * @param {Object} nodes where key is name of node, and value is list of - * names of dependent nodes. - * @param {string[]} nodes[name] list of named dependencies of node having - * 'name' as its name. - * @return {string[]} topological sort of nodes by their names. - * @throws {Error} if there is at least one circular - * dependency between nodes - */ - topologicalSort: function (nodes) { - var sorted = []; - var unresolved = nodes; - while (!_.isEmpty(unresolved)) { - // Get all nodes that have no dependency - var resolvable = _.pick(unresolved, function (dependencies, name) { - return _.isEmpty(_.difference(dependencies, sorted)); - }); - if (_.isEmpty(resolvable)) { - throw new Error("Circular dependency detected"); - } - // Append them to sorted - sorted = _.union(sorted, _.keys(resolvable)); - // Remove these nodes from unresolved - _.each(resolvable, function (dependencies, name) { - unresolved = _.omit(unresolved, name); - }); - } - return sorted; - }, - /** * performs a half up rounding with a fixed amount of decimals, correcting for float loss of precision * See the corresponding float_round() in server/tools/float_utils.py for more info * @param {Number} value the value to be rounded diff --git a/addons/web/static/src/js/services/ajax_service.js b/addons/web/static/src/js/services/ajax_service.js index 480549347d4..3446f651a59 100644 --- a/addons/web/static/src/js/services/ajax_service.js +++ b/addons/web/static/src/js/services/ajax_service.js @@ -2,6 +2,7 @@ odoo.define('web.AjaxService', function (require) { "use strict"; var AbstractService = require('web.AbstractService'); +var core = require('web.core'); var session = require('web.session'); var AjaxService = AbstractService.extend({ @@ -21,6 +22,8 @@ var AjaxService = AbstractService.extend({ }, }); +core.serviceRegistry.add('ajax', AjaxService); + return AjaxService; }); diff --git a/addons/web/static/src/js/services/core.js b/addons/web/static/src/js/services/core.js index 73071a2a65d..522d6b07302 100644 --- a/addons/web/static/src/js/services/core.js +++ b/addons/web/static/src/js/services/core.js @@ -41,6 +41,7 @@ return { crash_registry: new Registry(), search_filters_registry: new Registry(), search_widgets_registry: new Registry(), + serviceRegistry: new Registry(), /** * @type {String} */ diff --git a/addons/web/static/src/js/services/local_storage_service.js b/addons/web/static/src/js/services/local_storage_service.js index a4527026238..7a8c803a0f9 100644 --- a/addons/web/static/src/js/services/local_storage_service.js +++ b/addons/web/static/src/js/services/local_storage_service.js @@ -2,6 +2,7 @@ odoo.define('web.LocalStorageService', function (require) { 'use strict'; var AbstractService = require('web.AbstractService'); +var core = require('web.core'); var localStorage = require('web.local_storage'); var LocalStorageService = AbstractService.extend({ @@ -20,6 +21,8 @@ var LocalStorageService = AbstractService.extend({ } }); +core.serviceRegistry.add('local_storage', LocalStorageService); + return LocalStorageService; }); diff --git a/addons/web/static/src/js/services/report_service.js b/addons/web/static/src/js/services/report_service.js index a2bb8759a24..cfd62c0499f 100644 --- a/addons/web/static/src/js/services/report_service.js +++ b/addons/web/static/src/js/services/report_service.js @@ -6,6 +6,7 @@ odoo.define('web.ReportService', function (require) { */ var AbstractService = require('web.AbstractService'); +var core = require('web.core'); var ReportService = AbstractService.extend({ name: 'report', @@ -28,6 +29,8 @@ var ReportService = AbstractService.extend({ }, }); +core.serviceRegistry.add('report', ReportService); + return ReportService; }); diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index a696f05bffe..9853f279794 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -181,21 +181,23 @@ var AbstractView = Class.extend({ */ getController: function (parent) { var self = this; + // check if a model already exists, as if not, one will be created and + // we'll have to set the controller as its parent + var alreadyHasModel = !!this.model; return $.when(this._loadData(parent), ajax.loadLibs(this)).then(function () { - var model = self.getModel(); - var state = model.get(arguments[0]); + var state = self.model.get(arguments[0]); var renderer = self.getRenderer(parent, state); var Controller = self.Controller || self.config.Controller; var controllerParams = _.extend({ initialState: state, }, self.controllerParams); - var controller = new Controller(parent, model, renderer, controllerParams); + var controller = new Controller(parent, self.model, renderer, controllerParams); renderer.setParent(controller); - if (!self.model) { + if (!alreadyHasModel) { // if we have a model, it already has a parent. Otherwise, we // set the controller, so the rpcs from the model actually work - model.setParent(controller); + self.model.setParent(controller); } return controller; }); diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index c4dff8fad2e..2ddadfb0027 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -514,7 +514,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { // TODO this will tell the renderer to rerender the widget that // asked for the discard but will unfortunately lose the click // made on another row if any - self._confirmChange(self.handle, [ev.data.fieldName], ev) + self._confirmChange(recordID, [ev.data.fieldName], ev) .always(ev.data.onSuccess); }) .fail(ev.data.onFailure); diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js index f0d41f31f49..977674c022a 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -30,6 +30,7 @@ var BasicRenderer = AbstractRenderer.extend({ this.activeActions = params.activeActions; this.viewType = params.viewType; this.mode = params.mode || 'readonly'; + this.widgets = []; }, /** * This method has two responsabilities: find every invalid fields in the @@ -95,6 +96,9 @@ var BasicRenderer = AbstractRenderer.extend({ return this._render().then(_.constant([])); } + // reset all widgets (from the <widget> tag) if any: + _.invoke(this.widgets, 'updateState', state); + var defs = []; // Reset all the field widgets that are marked as changed and the ones @@ -254,7 +258,10 @@ var BasicRenderer = AbstractRenderer.extend({ if ('readonly' in modifiers && element.widget) { var mode = modifiers.readonly ? 'readonly' : modifiersData.baseMode; if (mode !== element.widget.mode) { - self._rerenderFieldWidget(element.widget, record, mode); + self._rerenderFieldWidget(element.widget, record, { + keepBaseMode: true, + mode: mode, + }); return; // Rerendering already applied the modifiers, no need to go further } } @@ -406,12 +413,25 @@ var BasicRenderer = AbstractRenderer.extend({ * @param {Object} record * @param {jQuery|AbstractField} [element] * @param {Object} [options] - * @param {Object} [options.callback] - the callback to call on registration - * and on modifiers updates + * @param {Object} [options.callback] the callback to call on registration + * and on modifiers updates + * @param {boolean} [options.keepBaseMode=false] this function registers the + * 'baseMode' of the node; this is a field widget specific settings which + * represents the generic mode of the widget, regardless of its modifiers + * (the interesting case is the list view: all widgets are supposed to be + * in the baseMode 'readonly', except the ones that are in the line that + * is currently being edited). + * With option 'keepBaseMode' set to true, the current baseMode of the + * node isn't overridden (this is particularily useful when a field widget + * is re-rendered because its readonly modifier changed, as in this case, + * we don't want to change its base mode). + * @param {string} [options.mode] the 'baseMode' of the node is set to this + * value (if not given, it is set to this.mode, the mode of the renderer) * @returns {Object} for code efficiency, returns the last evaluated - * modifiers for the given node and record. + * modifiers for the given node and record. */ _registerModifiers: function (node, record, element, options) { + options = options || {}; // Check if we already registered the modifiers for the given node // If yes, this is simply an update of the related element // If not, check the modifiers to see if it needs registration @@ -428,12 +448,11 @@ var BasicRenderer = AbstractRenderer.extend({ this.allModifiersData.push(modifiersData); } } - // we register here the base mode of the node. This is a field widget - // specific settings which represents the generic mode for the widget, - // regardless of its modifiers. The interesting case is the list view: - // all widgets are supposed to be in the baseMode 'readonly', except the - // ones that are in the line that is currently being edited. - modifiersData.baseMode = (options && options.mode) || this.mode; + + // Compute node's base mode if necessary + if (!options.keepBaseMode) { + modifiersData.baseMode = options.mode || this.mode; + } // Evaluate if necessary if (!modifiersData.evaluatedModifiers[record.id]) { @@ -497,13 +516,16 @@ var BasicRenderer = AbstractRenderer.extend({ * @private * @param {Object} node * @param {Object} record - * @param {Object} [options] + * @param {Object} [options] passed to @_registerModifiers + * @param {string} [options.mode] either 'edit' or 'readonly' (defaults to + * this.mode, the mode of the renderer) * @returns {jQueryElement} */ _renderFieldWidget: function (node, record, options) { + options = options || {}; var fieldName = node.attrs.name; // Register the node-associated modifiers - var mode = options && options.mode || this.mode; + var mode = options.mode || this.mode; var modifiers = this._registerModifiers(node, record, null, options); // Initialize and register the widget // Readonly status is known as the modifiers have just been registered @@ -545,7 +567,8 @@ var BasicRenderer = AbstractRenderer.extend({ && !element.widget.isSet() )); }, - mode: mode + keepBaseMode: !!options.keepBaseMode, + mode: mode, }); self._postProcessField(widget, node); }); @@ -596,6 +619,8 @@ var BasicRenderer = AbstractRenderer.extend({ var Widget = widgetRegistry.get(node.attrs.name); var widget = new Widget(this, record, node); + this.widgets.push(widget); + // Prepare widget rendering and save the related deferred var def = widget._widgetRenderAndInsert(function () {}); if (def.state() === 'pending') { @@ -616,11 +641,11 @@ var BasicRenderer = AbstractRenderer.extend({ * @private * @param {Widget} widget * @param {Object} record - * @param {string} mode either 'readonly' or 'edit' + * @param {Object} [options] options passed to @_renderFieldWidget */ - _rerenderFieldWidget: function (widget, record, mode) { + _rerenderFieldWidget: function (widget, record, options) { // Render the new field widget - var $el = this._renderFieldWidget(widget.__node, record, {mode: mode}); + var $el = this._renderFieldWidget(widget.__node, record, options); widget.$el.replaceWith($el); // Destroy the old widget and position the new one at the old one's diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index fffdb66b509..601752ce532 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -71,6 +71,7 @@ var ListRenderer = BasicRenderer.extend({ */ updateState: function (state, params) { this._processColumns(params.columnInvisibleFields || {}); + this.selection = []; return this._super.apply(this, arguments); }, @@ -202,9 +203,11 @@ var ListRenderer = BasicRenderer.extend({ var field = self.state.fields[column.attrs.name]; var value = aggregateValues[column.attrs.name].value; var help = aggregateValues[column.attrs.name].help; - var formattedValue = field_utils.format[field.type](value, field, { - escape: true, - }); + var formatFunc = field_utils.format[column.attrs.widget]; + if (!formatFunc) { + formatFunc = field_utils.format[field.type]; + } + var formattedValue = formatFunc(value, field, {escape: true}); $cell.addClass('o_list_number').attr('title', help).html(formattedValue); } return $cell; diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index 7eb2ba4bc6b..99a5e4ee334 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -545,7 +545,6 @@ var PivotModel = AbstractModel.extend({ var data = Array.prototype.slice.call(arguments); if (data[0][0].__count === 0) { self.data.has_data = false; - return; } self._prepareData(data); }); diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 23e59b2f03d..1ac0e56b7bd 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -1333,6 +1333,7 @@ QUnit.module('ActionManager', { actions: this.actions, archs: this.archs, data: this.data, + services: [ReportService], mockRPC: function (route, args) { assert.step(args.method || route); if (route === '/report/check_wkhtmltopdf') { @@ -1340,7 +1341,6 @@ QUnit.module('ActionManager', { } return this._super.apply(this, arguments); }, - services: [ReportService], session: { get_file: function (params) { assert.step(params.url); @@ -1373,6 +1373,7 @@ QUnit.module('ActionManager', { actions: this.actions, archs: this.archs, data: this.data, + services: [ReportService], mockRPC: function (route, args) { assert.step(args.method || route); if (route === '/report/check_wkhtmltopdf') { @@ -1380,7 +1381,6 @@ QUnit.module('ActionManager', { } return this._super.apply(this, arguments); }, - services: [ReportService], session: { get_file: function (params) { assert.step(params.url); @@ -1427,6 +1427,7 @@ QUnit.module('ActionManager', { actions: this.actions, archs: this.archs, data: this.data, + services: [ReportService], mockRPC: function (route, args) { assert.step(args.method || route); if (route === '/report/check_wkhtmltopdf') { @@ -1437,7 +1438,6 @@ QUnit.module('ActionManager', { } return this._super.apply(this, arguments); }, - services: [ReportService], session: { get_file: function (params) { assert.step(params.url); // should not be called diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 0a1fd9128e6..ecedd4c5bc5 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -6271,7 +6271,7 @@ QUnit.module('relational_fields', { } if (args.method === 'write') { assert.deepEqual(args.args[1].timmy, [[6, false, [12]]], - 'should properly write ids') + 'should properly write ids'); } return this._super.apply(this, arguments); }, @@ -9354,7 +9354,7 @@ QUnit.module('relational_fields', { $input = form.$('.o_field_many2manytags span[data-id=13]'); // selects 'red' tag $input.click(); // opens the colorpicker dropdown assert.ok(form.$('.o_field_many2manytags span[data-id=13] .o_colorpicker .o_checkbox'), "should have a checkbox in the colorpicker dropdown menu"); - + $input.click(); var $checkBox = form.$('.o_field_many2manytags span[data-id=13] .o_colorpicker .o_checkbox input'); assert.notOk($checkBox.is(':checked'), "should have unticked checkbox in colorpicker dropdown menu"); diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index d08beffb977..2d8e719ade4 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -19,7 +19,6 @@ var core = require('web.core'); var dom = require('web.dom'); var session = require('web.session'); var MockServer = require('web.MockServer'); -var utils = require('web.utils'); var Widget = require('web.Widget'); var DebouncedField = basic_fields.DebouncedField; @@ -218,6 +217,7 @@ function createAsyncView(params) { return view.getController(widget).then(function (view) { // override the view's 'destroy' so that it calls 'destroy' on the widget // instead, as the widget is the parent of the view and the mockServer. + view.__destroy = view.destroy; view.destroy = function () { // remove the override to properly destroy the view and its children // when it will be called the second time (by its parent) @@ -400,7 +400,7 @@ function addMockEnvironment(widget, params) { // Dispatch service calls // Note: some services could call other services at init, // Which is why we have to init services after that - var services = {}; + var services = {ajax: null}; // mocked ajax service already loaded intercept(widget, 'call_service', function (ev) { var args, result; if (ev.data.service === 'ajax') { @@ -415,36 +415,22 @@ function addMockEnvironment(widget, params) { } ev.data.callback(result); }); - // Instantiate services - // Note: ensure topological sort of services based on their dependencies - var sortServices = function (services) { - // Create nodes (services), with ajax already loaded - var nodes = { ajax: [] }; - _.each(services, function (Service) { - nodes[Service.prototype.name] = Service.prototype.dependencies; + + // Deploy services + var done = false; + while (!done) { + var index = _.findIndex(params.services, function (Service) { + return !_.some(Service.prototype.dependencies, function (depName) { + return !_.has(services, depName); + }); }); - var sorted; - try { - sorted = utils.topologicalSort(nodes); - } catch (err) { - console.warn('topologicalSort Error:', err.message); - sorted = nodes; + if (index !== -1) { + var Service = params.services.splice(index, 1)[0]; + services[Service.prototype.name] = new Service(widget); + } else { + done = true; } - // Remove ajax from sorted - sorted = _.without(sorted, 'ajax'); - // Sort services based on sorted - // Note: we convert sorted to an object key=>index for efficiency - var sortedObj = _.invert(_.object(_.pairs(sorted))); - sorted = _.sortBy(services, function (Service) { - return sortedObj[Service.prototype.name]; - }); - return sorted; - }; - var sortedServices = sortServices(params.services); - _.each(sortedServices, function (Service) { - var service = new Service(widget); - services[service.name] = service; - }); + } intercept(widget, 'load_action', function (event) { mockServer.performRpc('/web/action/load', { @@ -680,47 +666,83 @@ function removeSrcAttribute($el, widget) { var patches = {}; /** - * Patches a given Class with the given properties. + * Patches a given Class or Object with the given properties. * - * @param {Class} Klass + * @param {Class|Object} target * @param {Object} props */ -function patch (Klass, props) { +function patch (target, props) { var patchID = _.uniqueId('patch_'); - Klass.__patchID = patchID; + target.__patchID = patchID; patches[patchID] = { - Klass: Klass, + target: target, otherPatchedProps: [], ownPatchedProps: [], }; - _.each(props, function (value, key) { - if (Klass.prototype.hasOwnProperty(key)) { - patches[patchID].ownPatchedProps.push({ - key: key, - initialValue: Klass.prototype[key], - }); - } else { - patches[patchID].otherPatchedProps.push(key); - } - }); - Klass.include(props); + if (target.prototype) { + _.each(props, function (value, key) { + if (target.prototype.hasOwnProperty(key)) { + patches[patchID].ownPatchedProps.push({ + key: key, + initialValue: target.prototype[key], + }); + } else { + patches[patchID].otherPatchedProps.push(key); + } + }); + target.include(props); + } else { + _.each(props, function (value, key) { + if (key in target) { + var oldValue = target[key]; + patches[patchID].ownPatchedProps.push({ + key: key, + initialValue: oldValue, + }); + if (typeof value === 'function') { + target[key] = function () { + var oldSuper = this._super; + this._super = oldValue; + var result = value.apply(this, arguments); + if (oldSuper === undefined) { + delete this._super; + } else { + this._super = oldSuper; + } + return result; + }; + } else { + target[key] = value; + } + } else { + patches[patchID].otherPatchedProps.push(key); + target[key] = value; + } + }); + } } /** - * Unpatches a given Class. + * Unpatches a given Class or Object. * - * @param {Class} Klass + * @param {Class|Object} target */ -function unpatch(Klass) { - var patchID = Klass.__patchID; +function unpatch(target) { + var patchID = target.__patchID; var patch = patches[patchID]; _.each(patch.ownPatchedProps, function (p) { - Klass[p.key] = p.initialValue; - }); - _.each(patch.otherPatchedProps, function (key) { - delete Klass.prototype[key]; + target[p.key] = p.initialValue; }); + if (target.prototype) { + _.each(patch.otherPatchedProps, function (key) { + delete target.prototype[key]; + }); + } else { + _.each(patch.otherPatchedProps, function (key) { + delete target[key]; + }); + } delete patches[patchID]; - delete Klass.__patchID; + delete target.__patchID; } // Loading static files cannot be properly simulated when their real content is diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 3bc412910c2..478f53331b4 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -5,6 +5,7 @@ var CalendarView = require('web.CalendarView'); var CalendarRenderer = require('web.CalendarRenderer'); var Dialog = require('web.Dialog'); var fieldUtils = require('web.field_utils'); +var mixins = require('web.mixins'); var testUtils = require('web.test_utils'); var session = require('web.session'); @@ -1629,6 +1630,63 @@ QUnit.module('Views', { calendar.destroy(); }); + QUnit.test('check if the view destroys all widgets and instances', function (assert) { + assert.expect(1); + + var instanceNumber = 0; + testUtils.patch(mixins.ParentedMixin, { + init: function () { + instanceNumber++; + return this._super.apply(this, arguments); + }, + destroy: function () { + if (!this.isDestroyed()) { + instanceNumber--; + } + return this._super.apply(this, arguments); + } + }); + + var params = { + View: CalendarView, + model: 'event', + data: this.data, + arch: + '<calendar class="o_calendar_test" '+ + 'event_open_popup="true" '+ + 'date_start="start_date" '+ + 'all_day="allday" '+ + 'mode="week" '+ + 'attendee="partner_ids" '+ + 'color="partner_id">'+ + '<field name="name"/>'+ + '<filter name="user_id" avatar_field="image"/>'+ + '<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+ + '</calendar>', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + }; + + var calendar = createView(params); + calendar.destroy(); + + var initialInstanceNumber = instanceNumber; + instanceNumber = 0; + + calendar = createView(params); + + // call destroy function of controller to ensure that it correctly destroys everything + calendar.__destroy(); + + assert.strictEqual(instanceNumber, initialInstanceNumber + 3, "every widget must be destroyed exept the parent"); + + calendar.destroy(); + + testUtils.unpatch(mixins.ParentedMixin); + }); + QUnit.test('create an event (async dialog) [REQUIRE FOCUS]', function (assert) { assert.expect(3); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 37420e0f076..5b0dd69ddd4 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -7,6 +7,7 @@ var config = require('web.config'); var core = require('web.core'); var fieldRegistry = require('web.field_registry'); var FormView = require('web.FormView'); +var mixins = require('web.mixins'); var pyeval = require('web.pyeval'); var testUtils = require('web.test_utils'); var widgetRegistry = require('web.widget_registry'); @@ -629,7 +630,7 @@ QUnit.module('Views', { }); QUnit.test('readonly attrs on fields are re-evaluated on field change', function (assert) { - assert.expect(3); + assert.expect(4); var form = createView({ View: FormView, @@ -655,6 +656,9 @@ QUnit.module('Views', { form.$('.o_field_boolean input').click(); assert.strictEqual(form.$('span[name="foo"]').length, 1, "the foo field widget should have been rerendered to now be readonly again"); + form.$('.o_field_boolean input').click(); + assert.strictEqual(form.$('input[name="foo"]').length, 1, + "the foo field widget should have been rerendered to now be editable again"); form.destroy(); }); @@ -6249,6 +6253,41 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('basic support for widgets', function (assert) { + assert.expect(1); + + var MyWidget = Widget.extend({ + init: function (parent, dataPoint) { + this.data = dataPoint.data; + }, + start: function () { + this.$el.text(this.data.foo + "!"); + }, + updateState: function (dataPoint) { + this.$el.text(dataPoint.data.foo + "!"); + }, + }); + widgetRegistry.add('test', MyWidget); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="foo"/>' + + '<widget name="test"/>' + + '</form>', + }); + + form.$('input[name="foo"]').val("I am alive").trigger('input'); + assert.strictEqual(form.$('.o_widget').text(), 'I am alive!', + "widget should have been updated"); + + form.destroy(); + delete widgetRegistry.map.test; + }); + + QUnit.test('bounce edit button in readonly mode', function (assert) { assert.expect(3); @@ -6360,6 +6399,84 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('check if the view destroys all widgets and instances', function (assert) { + assert.expect(1); + + var instanceNumber = 0; + testUtils.patch(mixins.ParentedMixin, { + init: function () { + instanceNumber++; + return this._super.apply(this, arguments); + }, + destroy: function () { + if (!this.isDestroyed()) { + instanceNumber--; + } + return this._super.apply(this, arguments); + } + }); + + var params = { + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<field name="display_name"/>' + + '<field name="foo"/>' + + '<field name="bar"/>' + + '<field name="int_field"/>' + + '<field name="qux"/>' + + '<field name="trululu"/>' + + '<field name="timmy"/>' + + '<field name="product_id"/>' + + '<field name="priority"/>' + + '<field name="state"/>' + + '<field name="date"/>' + + '<field name="datetime"/>' + + '<field name="product_ids"/>' + + '<field name="p">' + + '<tree default_order="foo desc">' + + '<field name="display_name"/>' + + '<field name="foo"/>' + + '</tree>' + + '</field>' + + '</sheet>' + + '</form>', + archs: { + 'partner,false,form': + '<form string="Partner">' + + '<sheet>' + + '<group>' + + '<field name="foo"/>' + + '</group>' + + '</sheet>' + + '</form>', + "partner_type,false,list": '<tree><field name="name"/></tree>', + 'product,false,list': '<tree><field name="display_name"/></tree>', + + }, + res_id: 1, + }; + + var form = createView(params); + form.destroy(); + + var initialInstanceNumber = instanceNumber; + instanceNumber = 0; + + form = createView(params); + + // call destroy function of controller to ensure that it correctly destroys everything + form.__destroy(); + + assert.strictEqual(instanceNumber, initialInstanceNumber + 3, "every widget must be destroyed exept the parent"); + + form.destroy(); + + testUtils.unpatch(mixins.ParentedMixin); + }); + QUnit.test('edition in form view on a "noCache" model', function (assert) { assert.expect(4); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 0bea1567d49..f5c31c05c8b 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -5,6 +5,7 @@ var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar'); var kanbanExamplesRegistry = require('web.kanban_examples_registry'); var KanbanRenderer = require('web.KanbanRenderer'); var KanbanView = require('web.KanbanView'); +var mixins = require('web.mixins'); var testUtils = require('web.test_utils'); var Widget = require('web.Widget'); var widgetRegistry = require('web.widget_registry'); @@ -3346,6 +3347,62 @@ QUnit.module('Views', { kanban.destroy(); }); + + QUnit.test('check if the view destroys all widgets and instances', function (assert) { + assert.expect(1); + + var instanceNumber = 0; + testUtils.patch(mixins.ParentedMixin, { + init: function () { + instanceNumber++; + return this._super.apply(this, arguments); + }, + destroy: function () { + if (!this.isDestroyed()) { + instanceNumber--; + } + return this._super.apply(this, arguments); + } + }); + + var params = { + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban string="Partners">' + + '<field name="foo"/>' + + '<field name="bar"/>' + + '<field name="int_field"/>' + + '<field name="qux"/>' + + '<field name="product_id"/>' + + '<field name="category_ids"/>' + + '<field name="state"/>' + + '<field name="date"/>' + + '<field name="datetime"/>' + + '<templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates>' + + '</kanban>', + }; + + var kanban = createView(params); + kanban.destroy(); + + var initialInstanceNumber = instanceNumber; + instanceNumber = 0; + + kanban = createView(params); + + // call destroy function of controller to ensure that it correctly destroys everything + kanban.__destroy(); + + assert.strictEqual(instanceNumber, initialInstanceNumber + 3, "every widget must be destroyed exept the parent"); + + kanban.destroy(); + + testUtils.unpatch(mixins.ParentedMixin); + }); + }); }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index cfe00bf9141..4f22945250a 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -7,6 +7,7 @@ var core = require('web.core'); var basicFields = require('web.basic_fields'); var FormView = require('web.FormView'); var ListView = require('web.ListView'); +var mixins = require('web.mixins'); var testUtils = require('web.test_utils'); var widgetRegistry = require('web.widget_registry'); var Widget = require('web.Widget'); @@ -583,6 +584,40 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('selection is reset on reload', function (assert) { + assert.expect(5); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree>' + + '<field name="foo"/>' + + '<field name="int_field" sum="Sum"/>' + + '</tree>', + }); + + assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', + "total should be 32 (no record selected)"); + + // select first record + var $firstRowSelector = list.$('tbody .o_list_record_selector input').first(); + $firstRowSelector.click(); + assert.ok($firstRowSelector.is(':checked'), "first row should be selected"); + assert.strictEqual(list.$('tfoot td:nth(2)').text(), '10', + "total should be 10 (first record selected)"); + + // reload + list.reload(); + $firstRowSelector = list.$('tbody .o_list_record_selector input').first(); + assert.notOk($firstRowSelector.is(':checked'), + "first row should no longer be selected"); + assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', + "total should be 32 (no more record selected)"); + + list.destroy(); + }); + QUnit.test('aggregates are computed correctly', function (assert) { assert.expect(4); @@ -661,6 +696,25 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('aggregates are formatted according to field widget', function (assert) { + assert.expect(1); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree>' + + '<field name="foo"/>' + + '<field name="qux" widget="float_time" sum="Sum"/>' + + '</tree>', + }); + + assert.strictEqual(list.$('tfoot td:nth(2)').text(), '19:24', + "total should be formatted as a float_time"); + + list.destroy(); + }); + QUnit.test('groups can be sorted on aggregates', function (assert) { assert.expect(10); @@ -1930,7 +1984,7 @@ QUnit.module('Views', { }); QUnit.test('readonly attrs on fields are re-evaluated on field change', function (assert) { - assert.expect(7); + assert.expect(9); var list = createView({ View: ListView, @@ -1964,9 +2018,14 @@ QUnit.module('Views', { assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 3, "the foo field widget parent cell should now be readonly again"); - // Reswitch the cell to editable and save the row list.$('tbody tr:nth(0) td:nth(2) input').click(); - list.$('thead').click(); + assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"]').length, 1, + "the foo field widget should have been rerendered as editable again"); + assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 2, + "the foo field widget parent cell should not be readonly again"); + + // Click outside to leave edition mode + list.$el.click(); assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 2, "there should be 2 readonly foo cells in readonly mode"); @@ -2772,6 +2831,28 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('pressing ESC discard the current line changes (with required)', function (assert) { + assert.expect(3); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree editable="top"><field name="foo" required="1"/></tree>', + }); + + list.$buttons.find('.o_list_button_add').click(); + + list.$('input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE}); + assert.strictEqual(list.$('tr.o_data_row').length, 4, + "should have 4 data row in list"); + assert.strictEqual(list.$('tr.o_data_row.o_selected_row').length, 0, + "no rows should be selected"); + assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'), + "should not have a visible save button"); + list.destroy(); + }); + QUnit.test('field with password attribute', function (assert) { assert.expect(2); @@ -3223,6 +3304,61 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('check if the view destroys all widgets and instances', function (assert) { + assert.expect(1); + + var instanceNumber = 0; + testUtils.patch(mixins.ParentedMixin, { + init: function () { + instanceNumber++; + return this._super.apply(this, arguments); + }, + destroy: function () { + if (!this.isDestroyed()) { + instanceNumber--; + } + return this._super.apply(this, arguments); + } + }); + + var params = { + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree string="Partners">' + + '<field name="foo"/>' + + '<field name="bar"/>' + + '<field name="date"/>' + + '<field name="int_field"/>' + + '<field name="qux"/>' + + '<field name="m2o"/>' + + '<field name="o2m"/>' + + '<field name="m2m"/>' + + '<field name="amount"/>' + + '<field name="currency_id"/>' + + '<field name="datetime"/>' + + '<field name="reference"/>' + + '</tree>', + }; + + var list = createView(params); + list.destroy(); + + var initialInstanceNumber = instanceNumber; + instanceNumber = 0; + + list = createView(params); + + // call destroy function of controller to ensure that it correctly destroys everything + list.__destroy(); + + assert.strictEqual(instanceNumber, initialInstanceNumber + 3, "every widget must be destroyed exept the parent"); + + list.destroy(); + + testUtils.unpatch(mixins.ParentedMixin); + }); + QUnit.test('list view on a "noCache" model', function (assert) { assert.expect(8); diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 94d4bcf36b8..1fcc03ee483 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -1,7 +1,6 @@ odoo.define('web.pivot_tests', function (require) { "use strict"; -var Context = require('web.Context'); var core = require('web.core'); var PivotView = require('web.PivotView'); var testUtils = require('web.test_utils'); @@ -739,7 +738,33 @@ QUnit.module('Views', { }, }); - $('.o_pivot_download').click(); + pivot.$buttons.find('.o_pivot_download').click(); + pivot.destroy(); + }); + + QUnit.test('can download a file without data', function (assert) { + assert.expect(1); + + this.data.partner.records = []; + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="date" interval="month" type="col"/>' + + '<field name="foo" type="measure"/>' + + '</pivot>', + session: { + get_file: function (args) { + assert.strictEqual(args.url, '/web/pivot/export_xls', + "should call get_file with correct parameters"); + args.complete(); + }, + }, + }); + + pivot.$buttons.find('.o_pivot_download').click(); pivot.destroy(); }); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index 08ddd2c91c2..014435194f3 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -21,7 +21,6 @@ QUnit.module('Views', { ], }, }; - }, }, function () { diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 35ded8170cf..bfd3bbb751e 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -155,6 +155,7 @@ <script type="text/javascript" src="/web/static/src/js/chrome/control_panel.js"></script> <script type="text/javascript" src="/web/static/src/js/chrome/abstract_web_client.js"></script> <script type="text/javascript" src="/web/static/src/js/chrome/web_client.js"></script> + <script type="text/javascript" src="/web/static/src/js/chrome/root_widget.js"></script> <script type="text/javascript" src="/web/static/src/js/_deprecated/data.js"></script> <script type="text/javascript" src="/web/static/src/js/core/context.js"></script> <script type="text/javascript" src="/web/static/src/js/core/misc.js"></script> |
