vanilla 17.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:47:08 +02:00
parent d72e748793
commit a9bcec8e91
1986 changed files with 1613876 additions and 568976 deletions

View file

@ -18,8 +18,8 @@ This module provides the core of the Odoo Web Client.
'views/webclient_templates.xml',
'views/report_templates.xml',
'views/base_document_layout_views.xml',
'views/partner_view.xml',
'views/speedscope_template.xml',
'views/lazy_assets.xml',
'views/neutralize_views.xml',
'data/ir_attachment.xml',
'data/report_layout.xml',
@ -37,69 +37,33 @@ This module provides the core of the Odoo Web Client.
# 3) an arbitrary name, relevant to the content of the bundle.
#
# Examples:
# > web.assets_common = assets common to backend clients and others
# (not frontend).
# > web_editor.assets_wysiwyg = assets needed by components defined in the "web_editor" module.
# > web_editor.assets_legacy_wysiwyg = assets needed by components defined in the "web_editor" module.
# Warning: Layouts using "assets_frontend" assets do not have the
# "assets_common" assets anymore. So, if it make sense, files added in
# "assets_common" should also be added in "assets_frontend".
# TODO in the future, probably remove "assets_common" definition
# entirely and let all "main" bundles evolve on their own, including the
# files they need in their bundle.
'web.assets_common': [
'web.assets_emoji': [
'web/static/src/core/emoji_picker/emoji_data.js'
],
'web.assets_backend': [
('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
('include', 'web._assets_bootstrap_backend'),
('include', 'web._assets_core'),
'web/static/src/legacy/scss/tempusdominus_overridden.scss',
'web/static/lib/tempusdominus/tempusdominus.scss',
'web/static/lib/jquery.ui/jquery-ui.css',
'web/static/src/libs/fontawesome/css/font-awesome.css',
'web/static/lib/odoo_ui_icons/*',
'web/static/lib/select2/select2.css',
'web/static/lib/select2-bootstrap-css/select2-bootstrap.css',
'web/static/lib/daterangepicker/daterangepicker.css',
'web/static/src/webclient/navbar/navbar.scss',
'web/static/src/scss/animation.scss',
'web/static/src/scss/fontawesome_overridden.scss',
'web/static/src/scss/mimetypes.scss',
'web/static/src/scss/ui.scss',
'web/static/src/views/fields/translation_dialog.scss',
'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/mimetypes.scss',
'web/static/src/legacy/scss/modal.scss',
'web/static/src/legacy/scss/animation.scss',
'web/static/src/legacy/scss/datepicker.scss',
'web/static/src/legacy/scss/daterangepicker.scss',
'web/static/src/legacy/scss/banner.scss',
'web/static/src/legacy/scss/colorpicker.scss',
'web/static/src/legacy/scss/popover.scss',
'web/static/src/legacy/scss/translation_dialog.scss',
'web/static/src/legacy/scss/keyboard.scss',
'web/static/src/legacy/scss/name_and_signature.scss',
'web/static/src/legacy/scss/web.zoomodoo.scss',
'web/static/src/legacy/scss/fontawesome_overridden.scss',
'web/static/src/legacy/js/promise_extension.js',
'web/static/src/boot.js',
'web/static/src/session.js',
'web/static/src/legacy/js/core/cookie_utils.js',
'web/static/src/polyfills/clipboard.js',
'web/static/lib/underscore/underscore.js',
'web/static/lib/underscore.string/lib/underscore.string.js',
'web/static/lib/moment/moment.js',
'web/static/lib/luxon/luxon.js',
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
'web/static/src/owl2_compatibility/*.js',
'web/static/src/legacy/js/component_extension.js',
'web/static/src/legacy/legacy_component.js',
'web/static/lib/jquery/jquery.js',
'web/static/lib/jquery.ui/jquery-ui.js',
'web/static/lib/jquery/jquery.browser.js',
'web/static/lib/jquery.blockUI/jquery.blockUI.js',
'web/static/lib/jquery.hotkeys/jquery.hotkeys.js',
'web/static/lib/jquery.placeholder/jquery.placeholder.js',
'web/static/lib/jquery.form/jquery.form.js',
'web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js',
'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js',
'web/static/lib/popper/popper.js',
'web/static/lib/bootstrap/js/dist/dom/data.js',
'web/static/lib/bootstrap/js/dist/dom/event-handler.js',
@ -118,81 +82,17 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/bootstrap/js/dist/scrollspy.js',
'web/static/lib/bootstrap/js/dist/tab.js',
'web/static/lib/bootstrap/js/dist/toast.js',
'web/static/lib/tempusdominus/tempusdominus.js',
'web/static/lib/select2/select2.js',
'web/static/lib/clipboard/clipboard.js',
'web/static/lib/jSignature/jSignatureCustom.js',
'web/static/lib/qweb/qweb2.js',
'web/static/src/legacy/js/assets.js',
'web/static/src/legacy/js/libs/autocomplete.js',
'web/static/src/legacy/js/libs/bootstrap.js',
'web/static/src/legacy/js/libs/content-disposition.js',
'web/static/src/legacy/js/libs/download.js',
'web/static/src/legacy/js/libs/jquery.js',
'web/static/src/legacy/js/libs/moment.js',
'web/static/src/legacy/js/libs/underscore.js',
'web/static/src/legacy/js/libs/pdfjs.js',
'web/static/src/legacy/js/libs/zoomodoo.js',
'web/static/src/legacy/js/libs/jSignatureCustom.js',
'web/static/src/legacy/js/core/abstract_service.js',
'web/static/src/legacy/js/core/abstract_storage_service.js',
'web/static/src/legacy/js/core/ajax.js',
'web/static/src/legacy/js/core/browser_detection.js',
'web/static/src/legacy/js/core/bus.js',
'web/static/src/legacy/js/core/class.js',
'web/static/src/legacy/js/core/collections.js',
'web/static/src/legacy/js/core/concurrency.js',
'web/static/src/legacy/js/core/dialog.js',
'web/static/src/legacy/xml/dialog.xml',
'web/static/src/legacy/js/core/owl_dialog.js',
'web/static/src/legacy/js/core/popover.js',
'web/static/src/legacy/js/core/minimal_dom.js',
'web/static/src/legacy/js/core/dom.js',
'web/static/src/legacy/js/core/local_storage.js',
'web/static/src/legacy/js/core/mixins.js',
'web/static/src/legacy/js/core/qweb.js',
'web/static/src/legacy/js/core/ram_storage.js',
'web/static/src/legacy/js/core/registry.js',
'web/static/src/legacy/js/core/rpc.js',
'web/static/src/legacy/js/core/service_mixins.js',
'web/static/src/legacy/js/core/session.js',
'web/static/src/legacy/js/core/session_storage.js',
'web/static/src/legacy/js/core/time.js',
'web/static/src/legacy/js/core/translation.js',
'web/static/src/legacy/js/core/utils.js',
'web/static/src/legacy/js/core/widget.js',
'web/static/src/legacy/js/services/ajax_service.js',
'web/static/src/legacy/js/services/config.js',
'web/static/src/legacy/js/services/core.js',
'web/static/src/legacy/js/services/local_storage_service.js',
'web/static/src/legacy/js/services/session_storage_service.js',
'web/static/src/legacy/js/common_env.js',
'web/static/src/legacy/js/widgets/name_and_signature.js',
'web/static/src/legacy/xml/name_and_signature.xml',
'web/static/src/legacy/js/core/smooth_scroll_on_drag.js',
'web/static/src/legacy/js/widgets/colorpicker.js',
'web/static/src/legacy/xml/colorpicker.xml',
'web/static/src/legacy/js/widgets/translation_dialog.js',
'web/static/src/legacy/xml/translation_dialog.xml',
],
'web.assets_backend': [
('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
('include', 'web._assets_bootstrap'),
'base/static/src/css/modules.css',
'web/static/src/core/utils/transitions.scss',
'web/static/src/core/**/*',
'web/static/src/model/**/*',
'web/static/src/search/**/*',
'web/static/src/webclient/icons.scss', # variables required in list_controller.scss
'web/static/src/views/**/*',
'web/static/src/webclient/**/*',
('remove', 'web/static/src/webclient/navbar/navbar.scss'), # already in assets_common
('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded
('remove', 'web/static/src/views/form/button_box/*.scss'),
@ -201,108 +101,33 @@ This module provides the core of the Odoo Web Client.
'web/static/src/webclient/actions/reports/*.js',
'web/static/src/webclient/actions/reports/*.xml',
'web/static/src/env.js',
'web/static/src/libs/pdfjs.js',
'web/static/lib/jquery.scrollTo/jquery.scrollTo.js',
'web/static/lib/py.js/lib/py.js',
'web/static/lib/py.js/lib/py_extras.js',
'web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js',
'web/static/src/scss/ace.scss',
'web/static/src/scss/base_document_layout.scss',
'web/static/src/legacy/scss/domain_selector.scss',
'web/static/src/legacy/scss/model_field_selector.scss',
'web/static/src/legacy/scss/dropdown.scss',
'web/static/src/legacy/scss/tooltip.scss',
'web/static/src/legacy/scss/switch_company_menu.scss',
'web/static/src/legacy/scss/ace.scss',
'web/static/src/legacy/scss/fields.scss',
'web/static/src/legacy/scss/views.scss',
'web/static/src/legacy/scss/form_view.scss',
'web/static/src/legacy/scss/list_view.scss',
'web/static/src/legacy/scss/kanban_dashboard.scss',
'web/static/src/legacy/scss/kanban_examples_dialog.scss',
'web/static/src/legacy/scss/kanban_column_progressbar.scss',
'web/static/src/legacy/scss/kanban_view.scss',
'web/static/src/legacy/scss/data_export.scss',
'base/static/src/scss/onboarding.scss',
'web/static/src/legacy/scss/attachment_preview.scss',
'web/static/src/legacy/scss/base_document_layout.scss',
'web/static/src/legacy/scss/special_fields.scss',
'web/static/src/legacy/scss/fields_extra.scss',
'web/static/src/legacy/scss/form_view_extra.scss',
'web/static/src/legacy/scss/list_view_extra.scss',
'web/static/src/legacy/scss/color_picker.scss',
'base/static/src/scss/res_partner.scss',
# Form style should be computed before
'web/static/src/views/form/button_box/*.scss',
'web/static/src/legacy/action_adapters.js',
'web/static/src/legacy/debug_manager.js',
'web/static/src/legacy/legacy_service_provider.js',
'web/static/src/legacy/legacy_client_actions.js',
'web/static/src/legacy/legacy_dialog.js',
'web/static/src/legacy/legacy_load_views.js',
'web/static/src/legacy/legacy_views.js',
'web/static/src/legacy/legacy_promise_error_handler.js',
'web/static/src/legacy/legacy_rpc_error_handler.js',
'web/static/src/legacy/root_widget.js',
'web/static/src/legacy/systray_menu.js',
'web/static/src/legacy/systray_menu_item.js',
'web/static/src/legacy/backend_utils.js',
'web/static/src/legacy/utils.js',
'web/static/src/legacy/web_client.js',
'web/static/src/legacy/js/_deprecated/*',
'web/static/src/legacy/js/chrome/*',
'web/static/src/legacy/js/components/*',
'web/static/src/legacy/js/control_panel/*',
'web/static/src/legacy/js/core/domain.js',
'web/static/src/legacy/js/core/mvc.js',
'web/static/src/legacy/js/core/py_utils.js',
'web/static/src/legacy/js/core/context.js',
'web/static/src/legacy/js/core/misc.js',
'web/static/src/legacy/js/fields/*',
'web/static/src/legacy/js/services/data_manager.js',
'web/static/src/legacy/js/services/session.js',
'web/static/src/legacy/js/tools/tools.js',
'web/static/src/legacy/js/views/**/*',
'web/static/src/legacy/js/widgets/data_export.js',
'web/static/src/legacy/js/widgets/date_picker.js',
'web/static/src/legacy/js/widgets/domain_selector_dialog.js',
'web/static/src/legacy/js/widgets/domain_selector.js',
'web/static/src/legacy/js/widgets/iframe_widget.js',
'web/static/src/legacy/js/widgets/model_field_selector.js',
'web/static/src/legacy/js/widgets/model_field_selector_popover.js',
'web/static/src/legacy/js/widgets/ribbon.js',
'web/static/src/legacy/js/widgets/week_days.js',
'web/static/src/legacy/js/widgets/signature.js',
'web/static/src/legacy/js/widgets/attach_document.js',
'web/static/src/legacy/js/apps.js',
'web/static/src/legacy/js/env.js',
'web/static/src/legacy/js/model.js',
'web/static/src/legacy/js/owl_compatibility.js',
'web/static/src/legacy/xml/base.xml',
'web/static/src/legacy/xml/ribbon.xml',
'web/static/src/legacy/xml/control_panel.xml',
'web/static/src/legacy/xml/fields.xml',
'web/static/src/legacy/xml/kanban.xml',
'web/static/src/legacy/xml/search_panel.xml',
'web/static/src/legacy/xml/week_days.xml',
# Don't include dark mode files in light mode
('remove', 'web/static/src/**/*.dark.scss'),
],
"web.assets_backend_legacy_lazy": [
("include", "web._assets_helpers"),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web.assets_web': [
('include', 'web.assets_backend'),
'web/static/src/main.js',
'web/static/src/start.js',
],
'web.assets_frontend_minimal': [
'web/static/src/legacy/js/promise_extension.js',
'web/static/src/boot.js',
'web/static/src/polyfills/object.js',
'web/static/src/polyfills/array.js',
'web/static/src/module_loader.js',
'web/static/src/session.js',
'web/static/src/legacy/js/core/cookie_utils.js',
'web/static/src/legacy/js/core/menu.js',
'web/static/src/core/browser/cookie.js',
'web/static/src/legacy/js/core/minimal_dom.js',
'web/static/src/legacy/js/public/lazyloader.js',
],
@ -320,54 +145,31 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/luxon/luxon.js',
('include', 'web._assets_bootstrap'),
('include', 'web._assets_bootstrap_frontend'),
'web/static/src/legacy/scss/tempusdominus_overridden.scss',
'web/static/lib/tempusdominus/tempusdominus.scss',
'web/static/lib/jquery.ui/jquery-ui.css',
'web/static/src/libs/fontawesome/css/font-awesome.css',
'web/static/lib/odoo_ui_icons/*',
'web/static/lib/select2/select2.css',
'web/static/lib/select2-bootstrap-css/select2-bootstrap.css',
'web/static/lib/daterangepicker/daterangepicker.css',
'web/static/src/webclient/navbar/navbar.scss',
'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/mimetypes.scss',
'web/static/src/legacy/scss/modal.scss',
'web/static/src/legacy/scss/animation.scss',
'web/static/src/legacy/scss/datepicker.scss',
'web/static/src/legacy/scss/daterangepicker.scss',
'web/static/src/legacy/scss/banner.scss',
'web/static/src/legacy/scss/colorpicker.scss',
'web/static/src/legacy/scss/popover.scss',
'web/static/src/legacy/scss/translation_dialog.scss',
'web/static/src/legacy/scss/keyboard.scss',
'web/static/src/legacy/scss/name_and_signature.scss',
'web/static/src/legacy/scss/web.zoomodoo.scss',
'web/static/src/legacy/scss/fontawesome_overridden.scss',
'web/static/src/scss/animation.scss',
'web/static/src/scss/base_frontend.scss',
'web/static/src/scss/fontawesome_overridden.scss',
'web/static/src/scss/mimetypes.scss',
'web/static/src/scss/ui.scss',
'web/static/src/views/fields/translation_dialog.scss',
'web/static/src/views/fields/signature/signature_field.scss',
'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/modal.scss',
'web/static/src/legacy/scss/base_frontend.scss',
'web/static/src/legacy/scss/lazyloader.scss',
('include', 'web.assets_frontend_minimal'),
'web/static/lib/underscore/underscore.js',
'web/static/lib/underscore.string/lib/underscore.string.js',
'web/static/lib/moment/moment.js',
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
'web/static/src/owl2_compatibility/*.js',
'web/static/src/legacy/js/component_extension.js',
'web/static/src/legacy/legacy_component.js',
'web/static/lib/jquery/jquery.js',
'web/static/lib/jquery.ui/jquery-ui.js',
'web/static/lib/jquery/jquery.browser.js',
'web/static/lib/jquery.blockUI/jquery.blockUI.js',
'web/static/lib/jquery.hotkeys/jquery.hotkeys.js',
'web/static/lib/jquery.placeholder/jquery.placeholder.js',
'web/static/lib/jquery.form/jquery.form.js',
'web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js',
'web/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js',
'web/static/lib/popper/popper.js',
'web/static/lib/bootstrap/js/dist/dom/data.js',
'web/static/lib/bootstrap/js/dist/dom/event-handler.js',
@ -386,100 +188,46 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/bootstrap/js/dist/scrollspy.js',
'web/static/lib/bootstrap/js/dist/tab.js',
'web/static/lib/bootstrap/js/dist/toast.js',
'web/static/lib/tempusdominus/tempusdominus.js',
'web/static/lib/select2/select2.js',
'web/static/lib/clipboard/clipboard.js',
'web/static/lib/jSignature/jSignatureCustom.js',
'web/static/lib/qweb/qweb2.js',
'web/static/src/legacy/js/assets.js',
'web/static/src/legacy/js/libs/autocomplete.js',
'web/static/src/legacy/js/libs/bootstrap.js',
'web/static/src/legacy/js/libs/content-disposition.js',
'web/static/src/legacy/js/libs/download.js',
'web/static/src/legacy/js/libs/jquery.js',
'web/static/src/legacy/js/libs/moment.js',
'web/static/src/legacy/js/libs/underscore.js',
'web/static/src/legacy/js/libs/pdfjs.js',
'web/static/src/legacy/js/libs/zoomodoo.js',
'web/static/src/legacy/js/libs/jSignatureCustom.js',
'web/static/src/legacy/js/core/abstract_service.js',
'web/static/src/legacy/js/core/abstract_storage_service.js',
'web/static/src/legacy/js/core/ajax.js',
'web/static/src/legacy/js/core/browser_detection.js',
'web/static/src/legacy/js/core/bus.js',
'web/static/src/legacy/js/core/class.js',
'web/static/src/legacy/js/core/collections.js',
'web/static/src/legacy/js/core/concurrency.js',
'web/static/src/legacy/js/core/dialog.js',
'web/static/src/legacy/xml/dialog.xml',
'web/static/src/legacy/js/core/owl_dialog.js',
'web/static/src/legacy/js/core/popover.js',
'web/static/src/legacy/js/core/dom.js',
'web/static/src/legacy/js/core/local_storage.js',
'web/static/src/legacy/js/core/menu.js',
'web/static/src/legacy/js/core/mixins.js',
'web/static/src/legacy/js/core/qweb.js',
'web/static/src/legacy/js/core/ram_storage.js',
'web/static/src/legacy/js/core/registry.js',
'web/static/src/legacy/js/core/rpc.js',
'web/static/src/legacy/js/core/service_mixins.js',
'web/static/src/legacy/js/core/session.js',
'web/static/src/legacy/js/core/session_storage.js',
'web/static/src/legacy/js/core/time.js',
'web/static/src/legacy/js/core/translation.js',
'web/static/src/legacy/js/core/utils.js',
'web/static/src/legacy/js/core/widget.js',
'web/static/src/legacy/js/services/ajax_service.js',
'web/static/src/legacy/js/services/config.js',
'web/static/src/legacy/js/services/core.js',
'web/static/src/legacy/js/services/local_storage_service.js',
'web/static/src/legacy/js/services/session_storage_service.js',
'web/static/src/legacy/js/common_env.js',
'web/static/src/legacy/js/widgets/name_and_signature.js',
'web/static/src/legacy/xml/name_and_signature.xml',
'web/static/src/legacy/js/core/smooth_scroll_on_drag.js',
'web/static/src/legacy/js/widgets/colorpicker.js',
'web/static/src/legacy/xml/colorpicker.xml',
'web/static/src/legacy/js/widgets/translation_dialog.js',
'web/static/src/legacy/xml/translation_dialog.xml',
'web/static/src/env.js',
'web/static/src/core/utils/transitions.scss', # included early because used by other files
'web/static/src/core/**/*',
('remove', 'web/static/src/core/commands/**/*'),
('remove', 'web/static/src/core/debug/debug_menu.js'),
('remove', 'web/static/src/core/file_viewer/file_viewer.dark.scss'),
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
'web/static/src/core/commands/default_providers.js',
'web/static/src/core/commands/command_palette.js',
'web/static/src/public/error_notifications.js',
'web/static/src/public/public_component_service.js',
'web/static/src/public/datetime_picker_widget.js',
'web/static/src/libs/pdfjs.js',
'web/static/src/legacy/utils.js',
'web/static/src/legacy/js/core/misc.js',
'web/static/src/legacy/js/owl_compatibility.js',
'web/static/src/legacy/js/services/session.js',
'web/static/src/legacy/js/public/public_env.js',
'web/static/src/legacy/js/public/public_root.js',
'web/static/src/legacy/js/public/public_root_instance.js',
'web/static/src/legacy/js/public/public_widget.js',
'web/static/src/legacy/legacy_promise_error_handler.js',
'web/static/src/legacy/legacy_rpc_error_handler.js',
'web/static/src/legacy/js/fields/field_utils.js',
'web/static/src/legacy/js/public/signin.js',
('include', 'web.frontend_legacy'),
],
'web.assets_frontend_lazy': [
('include', 'web.assets_frontend'),
# Remove assets_frontend_minimal
('remove', 'web/static/src/legacy/js/promise_extension.js'),
('remove', 'web/static/src/boot.js'),
('remove', 'web/static/src/module_loader.js'),
('remove', 'web/static/src/session.js'),
('remove', 'web/static/src/legacy/js/core/cookie_utils.js'),
('remove', 'web/static/src/legacy/js/core/menu.js'),
('remove', 'web/static/src/core/browser/cookie.js'),
('remove', 'web/static/src/legacy/js/core/minimal_dom.js'),
('remove', 'web/static/src/legacy/js/public/lazyloader.js'),
],
'web.assets_backend_prod_only': [
'web/static/src/main.js',
'web/static/src/start.js',
'web/static/src/legacy/legacy_setup.js',
],
# Optional Bundle for PDFJS lib
# Since PDFJS is quite huge (80000≈ lines), please only load it when it is necessary.
# For now, it is only use to display the PDF slide Viewer during an embed.
@ -495,10 +243,29 @@ This module provides the core of the Odoo Web Client.
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
('include', 'web._assets_bootstrap'),
('include', 'web._assets_bootstrap_backend'),
'web/static/lib/bootstrap/js/dist/dom/data.js',
'web/static/lib/bootstrap/js/dist/dom/event-handler.js',
'web/static/lib/bootstrap/js/dist/dom/manipulator.js',
'web/static/lib/bootstrap/js/dist/dom/selector-engine.js',
'web/static/lib/bootstrap/js/dist/base-component.js',
'web/static/lib/bootstrap/js/dist/alert.js',
'web/static/lib/bootstrap/js/dist/button.js',
'web/static/lib/bootstrap/js/dist/carousel.js',
'web/static/lib/bootstrap/js/dist/collapse.js',
'web/static/lib/bootstrap/js/dist/dropdown.js',
'web/static/lib/bootstrap/js/dist/modal.js',
'web/static/lib/bootstrap/js/dist/offcanvas.js',
'web/static/lib/bootstrap/js/dist/tooltip.js',
'web/static/lib/bootstrap/js/dist/popover.js',
'web/static/lib/bootstrap/js/dist/scrollspy.js',
'web/static/lib/bootstrap/js/dist/tab.js',
'web/static/lib/bootstrap/js/dist/toast.js',
'base/static/src/css/description.css',
'web/static/src/libs/fontawesome/css/font-awesome.css',
'web/static/src/scss/fontawesome_overridden.scss',
'web/static/lib/odoo_ui_icons/*',
'web/static/fonts/fonts.scss',
@ -508,30 +275,29 @@ This module provides the core of the Odoo Web Client.
'web/static/src/webclient/actions/reports/layout_assets/layout_boxed.scss',
'web/static/src/webclient/actions/reports/layout_assets/layout_clean.scss',
'web/static/asset_styles_company_report.scss',
'web/static/src/legacy/js/services/session.js',
'web/static/src/legacy/js/public/public_root.js',
'web/static/src/legacy/js/public/public_root_instance.js',
'web/static/src/legacy/js/public/public_widget.js',
'web/static/src/legacy/js/report/report.js',
],
'web.report_assets_pdf': [
'web/static/src/webclient/actions/reports/reset.min.css',
],
'web.ace_lib': [
"web/static/lib/ace/ace.js",
"web/static/lib/ace/mode-js.js",
"web/static/lib/ace/javascript_highlight_rules.js",
"web/static/lib/ace/mode-xml.js",
"web/static/lib/ace/mode-qweb.js",
"web/static/lib/ace/mode-python.js",
"web/static/lib/ace/mode-scss.js",
"web/static/lib/ace/theme-monokai.js",
],
# ---------------------------------------------------------------------
# COLOR SCHEME BUNDLES
# ---------------------------------------------------------------------
"web.dark_mode_assets_common": [
('include', 'web.assets_common'),
],
"web.dark_mode_assets_backend": [
('include', 'web.assets_backend'),
"web.assets_web_dark": [
('include', 'web.assets_web'),
'web/static/src/**/*.dark.scss',
],
"web.dark_mode_variables": [
('before', 'base/static/src/scss/onboarding.variables.scss', 'base/static/src/scss/onboarding.variables.dark.scss'),
],
# ---------------------------------------------------------------------
# SUB BUNDLES
@ -542,14 +308,25 @@ This module provides the core of the Odoo Web Client.
# Their naming conventions are similar to those of the main bundles,
# with the addition of a prefixed underscore to reflect the "private"
# aspect.
#
# Examples:
# > web._assets_helpers = define assets needed in most main bundles
# Bare javascript essentials: module loader, core folder and core libs
'web._assets_core': [
# module loader
'web/static/src/module_loader.js',
# libs
'web/static/lib/luxon/luxon.js',
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
# core
'web/static/src/env.js',
'web/static/src/session.js',
'web/static/src/core/utils/transitions.scss',
'web/static/src/core/**/*',
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'), # always lazy-loaded
],
'web._assets_primary_variables': [
'web/static/src/scss/primary_variables.scss',
'web/static/src/**/**/*.variables.scss',
'base/static/src/scss/onboarding.variables.scss',
'web/static/src/**/*.variables.scss',
],
'web._assets_secondary_variables': [
'web/static/src/scss/secondary_variables.scss',
@ -557,9 +334,10 @@ This module provides the core of the Odoo Web Client.
'web._assets_helpers': [
'web/static/lib/bootstrap/scss/_functions.scss',
'web/static/lib/bootstrap/scss/_mixins.scss',
'web/static/src/scss/functions.scss',
'web/static/src/scss/mixins_forwardport.scss',
'web/static/src/scss/bs_mixins_overrides.scss',
'web/static/src/legacy/scss/utils.scss',
'web/static/src/scss/utils.scss',
('include', 'web._assets_primary_variables'),
('include', 'web._assets_secondary_variables'),
@ -571,6 +349,15 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/bootstrap/scss/utilities/_api.scss',
'web/static/src/scss/bootstrap_review.scss',
],
'web._assets_bootstrap_backend': [
('include', 'web._assets_bootstrap'),
('after', 'web/static/src/scss/utilities_custom.scss', 'web/static/src/scss/utilities_custom_backend.scss'),
'web/static/src/scss/bootstrap_review_backend.scss',
],
'web._assets_bootstrap_frontend': [
('include', 'web._assets_bootstrap'),
'web/static/src/scss/bootstrap_review_frontend.scss',
],
'web._assets_backend_helpers': [
'web/static/src/scss/bootstrap_overridden.scss',
'web/static/src/scss/bs_mixins_overrides_backend.scss',
@ -579,11 +366,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/scss/bootstrap_overridden_frontend.scss',
],
# Used during the transition of the web architecture
'web.frontend_legacy': [
'web/static/src/legacy/frontend/**/*',
],
# ---------------------------------------------------------------------
# TESTS BUNDLES
# ---------------------------------------------------------------------
@ -591,29 +373,30 @@ This module provides the core of the Odoo Web Client.
'web.assets_tests': [
# No tours are defined in web, but the bundle "assets_tests" is
# first called in web.
'web/static/tests/legacy/helpers/test_utils_file.js',
'web/static/tests/helpers/cleanup.js',
'web/static/tests/helpers/utils.js',
'web/static/tests/utils.js',
],
# remove this bundle alongside the owl2 compatibility layer
'web.tests_assets_common': [
('include', 'web.assets_common'),
('after', 'web/static/src/owl2_compatibility/app.js', 'web/static/tests/owl2_compatibility_app.js'),
'web.__assets_tests_call__': [
'web/static/tests/ignore_missing_deps_start.js',
('include', 'web.assets_tests'),
'web/static/tests/ignore_missing_deps_stop.js',
],
'web.tests_assets': [
('include', 'web.assets_backend'),
'web/static/src/public/public_component_service.js',
'web/static/tests/patch_translations.js',
'web/static/lib/qunit/qunit-2.9.1.css',
'web/static/lib/qunit/qunit-2.9.1.js',
'web/static/tests/legacy/helpers/**/*',
('remove', 'web/static/tests/legacy/helpers/test_utils_tests.js'),
'web/static/tests/legacy/legacy_setup.js',
'web/static/lib/fullcalendar/core/main.css',
'web/static/lib/fullcalendar/daygrid/main.css',
'web/static/lib/fullcalendar/timegrid/main.css',
'web/static/lib/fullcalendar/list/main.css',
'web/static/lib/fullcalendar/core/main.js',
'web/static/lib/fullcalendar/moment/main.js',
'web/static/lib/fullcalendar/interaction/main.js',
'web/static/lib/fullcalendar/daygrid/main.js',
'web/static/lib/fullcalendar/timegrid/main.js',
@ -628,17 +411,13 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/ace/mode-xml.js',
'web/static/lib/ace/mode-js.js',
'web/static/lib/ace/mode-qweb.js',
'web/static/lib/nearest/jquery.nearest.js',
'web/static/lib/daterangepicker/daterangepicker.js',
'web/static/src/legacy/js/libs/daterangepicker.js',
'web/static/lib/ace/theme-monokai.js',
'web/static/lib/stacktracejs/stacktrace.js',
'web/static/lib/Chart/Chart.js',
('include', "web.chartjs_lib"),
'web/static/lib/jSignature/jSignatureCustom.js',
'web/static/src/libs/jSignatureCustom.js',
'/web/static/lib/daterangepicker/daterangepicker.js',
# 'web/static/tests/legacy/main_tests.js',
'web/static/tests/helpers/**/*.js',
'web/static/tests/utils.js',
'web/static/tests/views/helpers.js',
'web/static/tests/search/helpers.js',
'web/static/tests/views/calendar/helpers.js',
@ -647,50 +426,39 @@ This module provides the core of the Odoo Web Client.
'web/static/tests/main.js',
'web/static/tests/mock_server_tests.js',
'web/static/tests/setup.js',
# These 2 lines below are taken from web.assets_frontend
# They're required for the web.frontend_legacy to work properly
# It is expected to add other lines coming from the web.assets_frontend
# if we need to add more and more legacy stuff that would require other scss or js.
('include', 'web._assets_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
('include', 'web.frontend_legacy'),
("include", "web.assets_backend_legacy_lazy"),
'web/static/tests/utils.js',
'web/static/src/webclient/clickbot/clickbot.js',
],
'web.qunit_suite_tests': [
'web/static/tests/env_tests.js',
'web/static/tests/dependencies_tests.js',
'web/static/tests/reactivity_tests.js',
'web/static/tests/core/**/*.js',
'web/static/tests/l10n/**/*.js',
'web/static/tests/search/**/*.js',
'web/static/tests/model/**/*.js',
('remove', 'web/static/tests/search/helpers.js'),
'web/static/tests/views/**/*.js',
('remove', 'web/static/tests/views/helpers.js'),
('remove', 'web/static/tests/views/calendar/helpers.js'),
'web/static/tests/webclient/**/*.js',
('remove', 'web/static/tests/webclient/**/helpers.js'),
'web/static/tests/legacy/**/*.js',
('remove', 'web/static/tests/legacy/**/*_mobile_tests.js'),
('remove', 'web/static/tests/legacy/**/*_benchmarks.js'),
('remove', 'web/static/tests/legacy/helpers/**/*.js'),
('remove', 'web/static/tests/legacy/legacy_setup.js'),
'web/static/tests/public/**/*.js',
('include', 'web.frontend_legacy_tests'),
# Legacy
'web/static/tests/legacy/**/*.js',
('remove', 'web/static/tests/legacy/helpers/**/*.js'),
],
'web.qunit_mobile_suite_tests': [
'web/static/tests/mobile/**/*.js',
'web/static/tests/legacy/fields/basic_fields_mobile_tests.js',
'web/static/tests/legacy/fields/relational_fields_mobile_tests.js',
'web/static/tests/legacy/components/dropdown_menu_mobile_tests.js',
],
# Used during the transition of the web architecture
'web.frontend_legacy_tests': [
'web/static/tests/legacy/frontend/*.js',
'web.assets_clickbot': [
'web/static/src/webclient/clickbot/clickbot.js',
],
"web.chartjs_lib" : [
'/web/static/lib/Chart/Chart.js',
'/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
]
},
'bootstrap': True, # load translations for login screen,
'license': 'LGPL-3',

View file

@ -11,7 +11,9 @@ from . import pivot
from . import profiling
from . import report
from . import session
from . import vcard
from . import view
from . import webclient
from . import webmanifest
from . import main # deprecated

View file

@ -1,6 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import _
from odoo.exceptions import MissingError
from odoo.http import Controller, request, route
from .utils import clean_action
@ -21,8 +23,8 @@ class Action(Controller):
action = request.env.ref(action_id)
assert action._name.startswith('ir.actions.')
action_id = action.id
except Exception:
action_id = 0 # force failed read
except Exception as exc:
raise MissingError(_("The action %r does not exist.", action_id)) from exc
base_action = Actions.browse([action_id]).sudo().read(['type'])
if base_action:
@ -37,7 +39,9 @@ class Action(Controller):
return value
@route('/web/action/run', type='json', auth="user")
def run(self, action_id):
def run(self, action_id, context=None):
if context:
request.update_context(**context)
action = request.env['ir.actions.server'].browse([action_id])
result = action.run()
return clean_action(result, env=action.env) if result else False

View file

@ -15,14 +15,13 @@ except ImportError:
import odoo
import odoo.modules.registry
from odoo import http, _
from odoo import SUPERUSER_ID, _, http
from odoo.addons.base.models.assetsbundle import ANY_UNIQUE
from odoo.exceptions import AccessError, UserError
from odoo.http import request, Response
from odoo.modules import get_resource_path
from odoo.tools import file_open, file_path, replace_exceptions, str2bool
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.image import image_guess_size_from_field_name
from odoo.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__)
@ -85,33 +84,59 @@ class Binary(http.Controller):
return stream.get_response(**send_file_kwargs)
@http.route(['/web/assets/debug/<string:filename>',
'/web/assets/debug/<path:extra>/<string:filename>',
'/web/assets/<int:id>/<string:filename>',
'/web/assets/<int:id>-<string:unique>/<string:filename>',
'/web/assets/<int:id>-<string:unique>/<path:extra>/<string:filename>'], type='http', auth="public")
# pylint: disable=redefined-builtin,invalid-name
def content_assets(self, id=None, filename=None, unique=False, extra=None, nocache=False):
if not id:
domain = [('url', '!=', False), ('res_model', '=', 'ir.ui.view'),
('res_id', '=', 0), ('create_uid', '=', odoo.SUPERUSER_ID)]
if extra:
domain += [('url', '=like', f'/web/assets/%/{extra}/{filename}')]
else:
domain += [
('url', '=like', f'/web/assets/%/{filename}'),
('url', 'not like', f'/web/assets/%/%/{filename}')
]
attachments = request.env['ir.attachment'].sudo().search_read(domain, fields=['id'], limit=1)
if not attachments:
raise request.not_found()
id = attachments[0]['id']
with replace_exceptions(UserError, by=request.not_found()):
record = request.env['ir.binary']._find_record(res_id=int(id))
stream = request.env['ir.binary']._get_stream_from(record, 'raw', filename)
send_file_kwargs = {'as_attachment': False, 'content_security_policy': None}
if unique:
@http.route([
'/web/assets/<string:unique>/<string:filename>'], type='http', auth="public")
def content_assets(self, filename=None, unique=ANY_UNIQUE, nocache=False, assets_params=None):
assets_params = assets_params or {}
assert isinstance(assets_params, dict)
debug_assets = unique == 'debug'
if unique in ('any', '%'):
unique = ANY_UNIQUE
attachment = None
if unique != 'debug':
url = request.env['ir.asset']._get_asset_bundle_url(filename, unique, assets_params)
assert not '%' in url
domain = [
('public', '=', True),
('url', '!=', False),
('url', '=like', url),
('res_model', '=', 'ir.ui.view'),
('res_id', '=', 0),
('create_uid', '=', SUPERUSER_ID),
]
attachment = request.env['ir.attachment'].sudo().search(domain, limit=1)
if not attachment:
# try to generate one
try:
if filename.endswith('.map'):
_logger.error(".map should have been generated through debug assets, (version %s most likely outdated)", unique)
raise request.not_found()
bundle_name, rtl, asset_type = request.env['ir.asset']._parse_bundle_name(filename, debug_assets)
css = asset_type == 'css'
js = asset_type == 'js'
bundle = request.env['ir.qweb']._get_asset_bundle(
bundle_name,
css=css,
js=js,
debug_assets=debug_assets,
rtl=rtl,
assets_params=assets_params,
)
# check if the version matches. If not, redirect to the last version
if not debug_assets and unique != ANY_UNIQUE and unique != bundle.get_version(asset_type):
return request.redirect(bundle.get_link(asset_type))
if css and bundle.stylesheets:
attachment = bundle.css()
elif js and bundle.javascripts:
attachment = bundle.js()
except ValueError as e:
_logger.warning("Parsing asset bundle %s has failed: %s", filename, e)
raise request.not_found() from e
if not attachment:
raise request.not_found()
stream = request.env['ir.binary']._get_stream_from(attachment, 'raw', filename)
send_file_kwargs = {'as_attachment': False}
if unique and unique != 'debug':
send_file_kwargs['immutable'] = True
send_file_kwargs['max_age'] = http.STATIC_CACHE_LONG
if nocache:
@ -190,7 +215,7 @@ class Binary(http.Controller):
try:
attachment = Model.create({
'name': filename,
'datas': base64.encodebytes(ufile.read()),
'raw': ufile.read(),
'res_model': model,
'res_id': int(id)
})
@ -203,7 +228,7 @@ class Binary(http.Controller):
else:
args.append({
'filename': clean(filename),
'mimetype': ufile.content_type,
'mimetype': attachment.mimetype,
'id': attachment.id,
'size': attachment.file_size
})
@ -217,12 +242,11 @@ class Binary(http.Controller):
def company_logo(self, dbname=None, **kw):
imgname = 'logo'
imgext = '.png'
placeholder = functools.partial(get_resource_path, 'web', 'static', 'img')
dbname = request.db
uid = (request.session.uid if dbname else None) or odoo.SUPERUSER_ID
if not dbname:
response = http.Stream.from_path(placeholder(imgname + imgext)).get_response()
response = http.Stream.from_path(file_path('web/static/img/logo.png')).get_response()
else:
try:
# create an empty registry
@ -258,9 +282,9 @@ class Binary(http.Controller):
response_class=Response,
)
else:
response = http.Stream.from_path(placeholder('nologo.png')).get_response()
response = http.Stream.from_path(file_path('web/static/img/nologo.png')).get_response()
except Exception:
response = http.Stream.from_path(placeholder(imgname + imgext)).get_response()
response = http.Stream.from_path(file_path(f'web/static/img/{imgname}{imgext}')).get_response()
return response
@ -276,7 +300,7 @@ class Binary(http.Controller):
"""
supported_exts = ('.ttf', '.otf', '.woff', '.woff2')
fonts = []
fonts_directory = file_path(os.path.join('web', 'static', 'fonts', 'sign'))
fonts_directory = file_path('web/static/fonts/sign')
if fontname:
font_path = os.path.join(fonts_directory, fontname)
with file_open(font_path, 'rb', filter_ext=supported_exts) as font_file:

View file

@ -75,7 +75,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd])
try:
if not re.match(DBNAME_PATTERN, name):
raise Exception(_('Invalid database name. Only alphanumerical characters, underscore, hyphen and dot are allowed.'))
raise Exception(_('Houston, we have a database naming issue! Make sure you only use letters, numbers, underscores, hyphens, or dots in the database name, and you\'ll be golden.'))
# country code could be = "False" which is actually True in python
country_code = post.get('country_code') or False
dispatch_rpc('db', 'create_database', [master_pwd, name, bool(post.get('demo')), lang, password, post['login'], country_code, post['phone']])
@ -94,7 +94,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd])
try:
if not re.match(DBNAME_PATTERN, new_name):
raise Exception(_('Invalid database name. Only alphanumerical characters, underscore, hyphen and dot are allowed.'))
raise Exception(_('Houston, we have a database naming issue! Make sure you only use letters, numbers, underscores, hyphens, or dots in the database name, and you\'ll be golden.'))
dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name, neutralize_database])
if request.db == name:
request.env.cr.close() # duplicating a database leads to an unusable cursor
@ -140,7 +140,7 @@ class Database(http.Controller):
error = "Database backup error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False, max_content_length=None)
def restore(self, master_pwd, backup_file, name, copy=False, neutralize_database=False):
insecure = odoo.tools.config.verify_admin_password('admin')
if insecure and master_pwd:

View file

@ -15,29 +15,11 @@ _logger = logging.getLogger(__name__)
class DataSet(http.Controller):
@http.route('/web/dataset/search_read', type='json', auth="user")
def search_read(self, model, fields=False, offset=0, limit=False, domain=None, sort=None):
return request.env[model].web_search_read(domain, fields, offset=offset, limit=limit, order=sort)
@http.route('/web/dataset/load', type='json', auth="user")
def load(self, model, id, fields):
warnings.warn("the route /web/dataset/load is deprecated and will be removed in Odoo 17. Use /web/dataset/call_kw with method 'read' and a list containing the id as args instead", DeprecationWarning)
value = {}
r = request.env[model].browse([id]).read()
if r:
value = r[0]
return {'value': value}
def _call_kw(self, model, method, args, kwargs):
Model = request.env[model]
get_public_method(Model, method) # Don't use the result, call_kw will redo the getattr
return call_kw(Model, method, args, kwargs)
@http.route('/web/dataset/call', type='json', auth="user")
def call(self, model, method, args, domain_id=None, context_id=None):
warnings.warn("the route /web/dataset/call is deprecated and will be removed in Odoo 17. Use /web/dataset/call_kw with empty kwargs instead", DeprecationWarning)
return self._call_kw(model, method, args, {})
@http.route(['/web/dataset/call_kw', '/web/dataset/call_kw/<path:path>'], type='json', auth="user")
def call_kw(self, model, method, args, kwargs, path=None):
return self._call_kw(model, method, args, kwargs)
@ -50,7 +32,7 @@ class DataSet(http.Controller):
return False
@http.route('/web/dataset/resequence', type='json', auth="user")
def resequence(self, model, ids, field='sequence', offset=0):
def resequence(self, model, ids, field='sequence', offset=0, context=None):
""" Re-sequences a number of records in the model, by their ids
The re-sequencing starts at the first model of ``ids``, the sequence
@ -64,6 +46,8 @@ class DataSet(http.Controller):
starting the resequencing from an arbitrary number,
defaults to ``0``
"""
if context:
request.update_context(**context)
m = request.env[model]
if not m.fields_get([field]):
return False

View file

@ -225,7 +225,7 @@ class ExportXlsxWriter:
cell_value = pycompat.to_text(cell_value)
except UnicodeDecodeError:
raise UserError(_("Binary fields can not be exported to Excel unless their content is base64-encoded. That does not seem to be the case for %s.", self.field_names)[column])
elif isinstance(cell_value, (list, tuple)):
elif isinstance(cell_value, (list, tuple, dict)):
cell_value = pycompat.to_text(cell_value)
if isinstance(cell_value, str):
@ -335,10 +335,7 @@ class Export(http.Controller):
if field.get('type') in ('properties', 'properties_definition'):
continue
if field.get('readonly'):
# If none of the field's states unsets readonly, skip the field
if all(dict(attrs).get('readonly', True)
for attrs in field.get('states', {}).values()):
continue
continue
if not field.get('exportable', True):
continue
@ -488,7 +485,7 @@ class ExportFormat(object):
if not import_compat and groupby:
groupby_type = [Model._fields[x.split(':')[0]].type for x in groupby]
domain = [('id', 'in', ids)] if ids else domain
groups_data = Model.with_context(active_test=False).read_group(domain, [x if x != '.id' else 'id' for x in field_names], groupby, lazy=False)
groups_data = Model.with_context(active_test=False).read_group(domain, ['__count'], groupby, lazy=False)
# read_group(lazy=False) returns a dict only for final groups (with actual data),
# not for intermediary groups. The full group tree must be re-constructed.
@ -523,7 +520,7 @@ class ExportFormat(object):
class CSVExport(ExportFormat, http.Controller):
@http.route('/web/export/csv', type='http', auth="user")
def index(self, data):
def web_export_csv(self, data):
try:
return self.base(data)
except Exception as exc:
@ -567,7 +564,7 @@ class CSVExport(ExportFormat, http.Controller):
class ExcelExport(ExportFormat, http.Controller):
@http.route('/web/export/xlsx', type='http', auth="user")
def index(self, data):
def web_export_xlsx(self, data):
try:
return self.base(data)
except Exception as exc:

View file

@ -41,7 +41,7 @@ class Home(http.Controller):
# Ensure we have both a database and a user
ensure_db()
if not request.session.uid:
return request.redirect('/web/login', 303)
return request.redirect_query('/web/login', query=request.params, code=303)
if kw.get('redirect'):
return request.redirect(kw.get('redirect'), 303)
if not security.check_session(request.session, request.env):
@ -63,12 +63,16 @@ class Home(http.Controller):
return request.redirect('/web/login?error=access')
@http.route('/web/webclient/load_menus/<string:unique>', type='http', auth='user', methods=['GET'])
def web_load_menus(self, unique):
def web_load_menus(self, unique, lang=None):
"""
Loads the menus for the webclient
:param unique: this parameters is not used, but mandatory: it is used by the HTTP stack to make a unique request
:param lang: language in which the menus should be loaded (only works if language is installed)
:return: the menus (including the images in Base64)
"""
if lang:
request.update_context(lang=lang)
menus = request.env["ir.ui.menu"].load_web_menus(request.session.debug)
body = json.dumps(menus, default=ustr)
response = request.make_response(body, [
@ -143,7 +147,7 @@ class Home(http.Controller):
if request.env.user._is_system():
uid = request.session.uid = odoo.SUPERUSER_ID
# invalidate session token cache as we've changed the uid
request.env['res.users'].clear_caches()
request.env.registry.clear_cache()
request.session.session_token = security.compute_session_token(request.session, request.env)
return request.redirect(self._login_redirect(uid))
@ -165,11 +169,16 @@ class Home(http.Controller):
('Cache-Control', 'no-store')]
return request.make_response(data, headers, status=status)
@http.route(['/robots.txt'], type='http', auth="none")
def robots(self, **kwargs):
allowed_routes = self._get_allowed_robots_routes()
robots_content = ["User-agent: *", "Disallow: /"]
robots_content.extend(f"Allow: {route}" for route in allowed_routes)
return request.make_response("\n".join(robots_content), [('Content-Type', 'text/plain')])
def _get_allowed_robots_routes(self):
"""Override this method to return a list of allowed routes.
By default this controller does not serve robots.txt so all routes
are implicitly open but we want any module to be able to append
to this list, in case the website module is installed.
:return: A list of URL paths that should be allowed by robots.txt
Examples: ['/social_instagram/', '/sitemap.xml', '/web/']

View file

@ -133,7 +133,7 @@ class ReportController(http.Controller):
else:
return
except Exception as e:
_logger.exception("Error while generating report %s", reportname)
_logger.warning("Error while generating report %s", reportname, exc_info=True)
se = http.serialize_exception(e)
error = {
'code': 200,

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import importlib.util
import io
import zipfile
import odoo.http as http
from odoo.exceptions import UserError
from odoo.http import request, content_disposition
class Partner(http.Controller):
@http.route(['/web_enterprise/partner/<model("res.partner"):partner>/vcard',
'/web/partner/vcard'], type='http', auth="user")
def download_vcard(self, partner_ids=None, partner=None, **kwargs):
if importlib.util.find_spec('vobject') is None:
raise UserError('vobject library is not installed')
if partner_ids:
partner_ids = list(filter(None, (int(pid) for pid in partner_ids.split(',') if pid.isdigit())))
partners = request.env['res.partner'].browse(partner_ids)
if len(partners) > 1:
with io.BytesIO() as buffer:
with zipfile.ZipFile(buffer, 'w') as zipf:
for partner in partners:
filename = f"{partner.name or partner.email}.vcf"
content = partner._get_vcard_file()
zipf.writestr(filename, content)
return request.make_response(buffer.getvalue(), [
('Content-Type', 'application/zip'),
('Content-Length', len(content)),
('Content-Disposition', content_disposition('Contacts.zip'))
])
if partner or partners:
partner = partner or partners
content = partner._get_vcard_file()
return request.make_response(content, [
('Content-Type', 'text/vcard'),
('Content-Length', len(content)),
('Content-Disposition', content_disposition(f"{partner.name or partner.email}.vcf")),
])
return request.not_found()

View file

@ -1,6 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import AccessError
from odoo.http import Controller, route, request
from odoo.tools.translate import _
class View(Controller):
@ -14,6 +16,8 @@ class View(Controller):
:param str arch: the edited arch of the custom view
:returns: dict with acknowledged operation (result set to True)
"""
custom_view = request.env['ir.ui.view.custom'].browse(custom_id)
custom_view = request.env['ir.ui.view.custom'].sudo().browse(custom_id)
if not custom_view.user_id == request.env.user:
raise AccessError(_("Custom view %s does not belong to user %s", custom_id, self.env.user.login))
custom_view.write({'arch': arch})
return {'result': True}

View file

@ -13,10 +13,10 @@ import werkzeug.wsgi
import odoo
import odoo.modules.registry
from odoo import http
from odoo.modules import get_manifest, get_resource_path
from odoo.modules import get_manifest
from odoo.http import request
from odoo.tools import lazy
from odoo.tools.misc import file_open
from odoo.tools.misc import file_open, file_path
from .utils import _local_web_translations
@ -29,32 +29,11 @@ def CONTENT_MAXAGE():
return http.STATIC_CACHE_LONG
MOMENTJS_LANG_CODES_MAP = {
"sr_RS": "sr_cyrl",
"sr@latin": "sr"
}
class WebClient(http.Controller):
# FIXME: to be removed in master, deprecated since momentjs removal in commit 4327c062d820
@http.route('/web/webclient/locale/<string:lang>', type='http', auth="none")
def load_locale(self, lang):
lang = MOMENTJS_LANG_CODES_MAP.get(lang, lang)
magic_file_finding = [lang.replace("_", '-').lower(), lang.split('_')[0]]
for code in magic_file_finding:
try:
return http.Response(
werkzeug.wsgi.wrap_file(
request.httprequest.environ,
file_open(f'web/static/lib/moment/locale/{code}.js', 'rb')
),
content_type='application/javascript; charset=utf-8',
headers=[('Cache-Control', f'max-age={http.STATIC_CACHE}')],
direct_passthrough=True,
)
except IOError:
_logger.debug("No moment locale for code %s", code)
return request.make_response("", headers=[
('Content-Type', 'application/javascript'),
('Cache-Control', f'max-age={http.STATIC_CACHE}'),
@ -80,7 +59,7 @@ class WebClient(http.Controller):
for addon_name in mods:
manifest = get_manifest(addon_name)
if manifest and manifest['bootstrap']:
f_name = get_resource_path(addon_name, 'i18n', f'{lang}.po')
f_name = file_path(f'{addon_name}/i18n/{lang}.po')
if not f_name:
continue
translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
@ -103,10 +82,13 @@ class WebClient(http.Controller):
elif mods is None:
mods = list(request.env.registry._init_modules) + (odoo.conf.server_wide_modules or [])
if lang and lang not in {code for code, _ in request.env['res.lang'].sudo().get_installed()}:
lang = None
translations_per_module, lang_params = request.env["ir.http"].get_translations_for_webclient(mods, lang)
body = json.dumps({
'lang': lang_params and lang_params["code"],
'lang': lang,
'lang_parameters': lang_params,
'modules': translations_per_module,
'multi_lang': len(request.env['res.lang'].sudo().get_installed()) > 1,
@ -131,10 +113,6 @@ class WebClient(http.Controller):
def test_mobile_suite(self, mod=None, **kwargs):
return request.render('web.qunit_mobile_suite')
@http.route('/web/benchmarks', type='http', auth="none")
def benchmarks(self, mod=None, **kwargs):
return request.render('web.benchmark_suite')
@http.route('/web/bundle/<string:bundle_name>', auth="public", methods=["GET"])
def bundle(self, bundle_name, **bundle_params):
"""
@ -148,7 +126,6 @@ class WebClient(http.Controller):
data = [{
"type": tag,
"src": attrs.get("src") or attrs.get("data-src") or attrs.get('href'),
"content": content,
} for tag, attrs, content in files]
} for tag, attrs in files]
return request.make_json_response(data)

View file

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import json
import mimetypes
from odoo import http
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.tools import ustr, file_open
class WebManifest(http.Controller):
def _get_shortcuts(self):
module_names = ['mail', 'crm', 'project', 'project_todo']
try:
module_ids = request.env['ir.module.module'].search([('state', '=', 'installed'), ('name', 'in', module_names)]) \
.sorted(key=lambda r: module_names.index(r["name"]))
except AccessError:
return []
menu_roots = request.env['ir.ui.menu'].get_user_roots()
datas = request.env['ir.model.data'].sudo().search([('model', '=', 'ir.ui.menu'),
('res_id', 'in', menu_roots.ids),
('module', 'in', module_names)])
shortcuts = []
for module in module_ids:
data = datas.filtered(lambda res: res.module == module.name)
if data:
shortcuts.append({
'name': module.display_name,
'url': '/web#menu_id=%s' % data.mapped('res_id')[0],
'description': module.summary,
'icons': [{
'sizes': '100x100',
'src': module.icon,
'type': mimetypes.guess_type(module.icon)[0] or 'image/png'
}]
})
return shortcuts
@http.route('/web/manifest.webmanifest', type='http', auth='public', methods=['GET'])
def webmanifest(self):
""" Returns a WebManifest describing the metadata associated with a web application.
Using this metadata, user agents can provide developers with means to create user
experiences that are more comparable to that of a native application.
"""
web_app_name = request.env['ir.config_parameter'].sudo().get_param('web.web_app_name', 'Odoo')
manifest = {
'name': web_app_name,
'scope': '/web',
'start_url': '/web',
'display': 'standalone',
'background_color': '#714B67',
'theme_color': '#714B67',
'prefer_related_applications': False,
}
icon_sizes = ['192x192', '512x512']
manifest['icons'] = [{
'src': '/web/static/img/odoo-icon-%s.png' % size,
'sizes': size,
'type': 'image/png',
} for size in icon_sizes]
manifest['shortcuts'] = self._get_shortcuts()
body = json.dumps(manifest, default=ustr)
response = request.make_response(body, [
('Content-Type', 'application/manifest+json'),
])
return response
@http.route('/web/service-worker.js', type='http', auth='public', methods=['GET'])
def service_worker(self):
response = request.make_response(
self._get_service_worker_content(),
[
('Content-Type', 'text/javascript'),
('Service-Worker-Allowed', '/web'),
]
)
return response
def _get_service_worker_content(self):
""" Returns a ServiceWorker javascript file scoped for the backend (aka. '/web')
"""
with file_open('web/static/src/service_worker.js') as f:
body = f.read()
return body
def _icon_path(self):
return 'web/static/img/odoo-icon-192x192.png'
@http.route('/web/offline', type='http', auth='public', methods=['GET'])
def offline(self):
""" Returns the offline page delivered by the service worker """
return request.render('web.webclient_offline', {
'odoo_icon': base64.b64encode(file_open(self._icon_path(), 'rb').read())
})

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~12.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-09-23 11:33+0000\n"
"PO-Revision-Date: 2019-09-23 11:33+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es_BO\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: web
#: model_terms:ir.ui.view,arch_db:web.external_layout_background
msgid ""
"<i class=\"fa fa-building-o\" role=\"img\" aria-label=\"Fiscal number\"/>"
msgstr "<i class=\"fa fa-building-o\" role=\"img\" aria-label=\"NIT\"/>"

View file

@ -6,9 +6,9 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Odoo 16.0\n"
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-01-25 10:43+0000\n"
"POT-Creation-Date: 2024-02-05 16:24+0000\n"
"PO-Revision-Date: 2016-03-12 06:25+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: Spanish (Chile) (http://www.transifex.com/odoo/odoo-9/"
@ -25,12 +25,11 @@ msgstr ""
#. module: web
#: model_terms:ir.ui.view,arch_db:web.report_invoice_wizard_preview
msgid "<strong>Untaxed Amount</strong>"
msgstr "<strong>Total neto</strong>"
msgstr "<strong>Monto neto</strong>"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/components/action_menus.js:0
#: code:addons/web/static/src/search/action_menus/action_menus.xml:0
#: code:addons/web/static/src/views/form/status_bar_buttons/status_bar_buttons.xml:0
#, python-format
msgid "Action"
@ -38,8 +37,7 @@ msgstr "Acción"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0
#: code:addons/web/static/src/legacy/xml/kanban.xml:0
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.js:0
#: code:addons/web/static/src/views/fields/x2many/x2many_field.js:0
#: code:addons/web/static/src/views/kanban/kanban_column_quick_create.xml:0
#: code:addons/web/static/src/views/kanban/kanban_record_quick_create.xml:0
@ -49,17 +47,8 @@ msgstr "Agregar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0
#, python-format
msgid "Add: "
msgstr "Agregar: "
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/basic_fields.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.xml:0
#: code:addons/web/static/src/search/group_by_menu/custom_group_by_item.xml:0
#: code:addons/web/static/src/views/fields/daterange/daterange_field.js:0
#: code:addons/web/static/src/core/datetime/datetime_picker_popover.xml:0
#: code:addons/web/static/src/search/custom_group_by_item/custom_group_by_item.xml:0
#, python-format
msgid "Apply"
msgstr "Aplicar"
@ -67,17 +56,10 @@ msgstr "Aplicar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js:0
#: code:addons/web/static/src/core/errors/error_dialogs.xml:0
#: code:addons/web/static/src/core/signature/signature_dialog.xml:0
#: code:addons/web/static/src/legacy/js/core/dialog.js:0
#: code:addons/web/static/src/legacy/js/fields/basic_fields.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_confirm_dialog.js:0
#: code:addons/web/static/src/legacy/js/views/signature_dialog.js:0
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/xml/control_panel.xml:0
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.js:0
#: code:addons/web/static/src/views/calendar/quick_create/calendar_quick_create.xml:0
#: code:addons/web/static/src/views/fields/daterange/daterange_field.js:0
#: code:addons/web/static/src/views/list/list_confirmation_dialog.xml:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.xml:0
#: code:addons/web/static/src/webclient/settings_form_view/fields/upgrade_dialog.xml:0
@ -91,14 +73,14 @@ msgstr "Cancelar"
#: code:addons/web/static/src/core/debug/debug_menu_items.xml:0
#: code:addons/web/static/src/core/dialog/dialog.xml:0
#: code:addons/web/static/src/core/domain_selector_dialog/domain_selector_dialog.xml:0
#: code:addons/web/static/src/core/errors/error_dialogs.xml:0
#: code:addons/web/static/src/core/file_viewer/file_viewer.xml:0
#: code:addons/web/static/src/core/model_field_selector/model_field_selector_popover.xml:0
#: code:addons/web/static/src/core/notifications/notification.xml:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column_quick_create.js:0
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/legacy/js/widgets/domain_selector_dialog.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/legacy/xml/dialog.xml:0
#: code:addons/web/static/src/views/fields/dynamic_placeholder_popover.xml:0
#: code:addons/web/static/src/views/fields/relational_utils.xml:0
#: code:addons/web/static/src/views/kanban/kanban_column_examples_dialog.xml:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.xml:0
@ -112,7 +94,6 @@ msgstr "Cerrar"
#. odoo-javascript
#: code:addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js:0
#: code:addons/web/static/src/legacy/js/core/dialog.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_confirm_dialog.js:0
#: code:addons/web/static/src/views/calendar/calendar_controller.js:0
#: code:addons/web/static/src/views/list/list_confirmation_dialog.js:0
#, python-format
@ -122,17 +103,22 @@ msgstr "Confirmación"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_controller.js:0
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/views/calendar/calendar_year/calendar_year_popover.xml:0
#: code:addons/web/static/src/views/calendar/quick_create/calendar_quick_create.xml:0
#: code:addons/web/static/src/views/fields/many2one/many2one_field.xml:0
#: code:addons/web/static/src/views/kanban/kanban_renderer.js:0
#: code:addons/web/static/src/views/kanban/kanban_record_quick_create.js:0
#, python-format
msgid "Create"
msgstr "Crear"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0
#, python-format
msgid "Create: "
msgstr "Crear: "
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
@ -144,7 +130,7 @@ msgstr "Día"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/core/file_viewer/file_viewer.xml:0
#: code:addons/web/static/src/views/fields/binary/binary_field.xml:0
#: code:addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.xml:0
#, python-format
@ -153,13 +139,12 @@ msgstr "Descargar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/legacy/xml/kanban.xml:0
#: code:addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.xml:0
#: code:addons/web/static/src/views/calendar/quick_create/calendar_quick_create.xml:0
#: code:addons/web/static/src/views/fields/binary/binary_field.xml:0
#: code:addons/web/static/src/views/fields/image/image_field.xml:0
#: code:addons/web/static/src/views/fields/pdf_viewer/pdf_viewer_field.xml:0
#: code:addons/web/static/src/views/kanban/kanban_header.xml:0
#: code:addons/web/static/src/views/kanban/kanban_record_quick_create.xml:0
#, python-format
msgid "Edit"
@ -167,7 +152,6 @@ msgstr "Editar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/basic_fields.js:0
#: code:addons/web/static/src/views/fields/email/email_field.js:0
#: model:ir.model.fields,field_description:web.field_base_document_layout__email
#: model_terms:ir.ui.view,arch_db:web.login
@ -185,8 +169,6 @@ msgstr "Error"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/views/list/list_controller.js:0
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/views/list/list_controller.js:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.xml:0
#, python-format
@ -195,7 +177,6 @@ msgstr "Exportar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.js:0
#, python-format
msgid "Export Data"
@ -203,25 +184,18 @@ msgstr "Exportar datos"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/search/favorite_menu/favorite_menu.xml:0
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.xml:0
#, python-format
msgid "Favorites"
msgstr "Favoritos"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/search/filter_menu/filter_menu.xml:0
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.xml:0
#, python-format
msgid "Filters"
msgstr "Filtros"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/views/form/form_view.js:0
#, python-format
msgid "Form"
msgstr "Formulario"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/views/graph/graph_view.js:0
@ -231,19 +205,15 @@ msgstr "Gráfico"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/search/group_by_menu/group_by_menu.xml:0
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.xml:0
#: code:addons/web/static/src/views/pivot/pivot_group_by_menu.xml:0
#, python-format
msgid "Group By"
msgstr "Agrupar por"
#. module: web
#: model:ir.model.fields,field_description:web.field_base_document_layout__id
msgid "ID"
msgstr "ID (identificación)"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/basic_fields.js:0
#: code:addons/web/static/src/core/file_viewer/file_viewer.xml:0
#: code:addons/web/static/src/views/fields/attachment_image/attachment_image_field.xml:0
#: code:addons/web/static/src/views/fields/image/image_field.js:0
#: code:addons/web/static/src/views/fields/image_url/image_url_field.js:0
@ -259,13 +229,6 @@ msgstr "Imagen"
msgid "Languages"
msgstr "Idiomas"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/views/list/list_view.js:0
#, python-format
msgid "List"
msgstr "Lista"
#. module: web
#: model_terms:ir.ui.view,arch_db:web.login
msgid "Log in"
@ -294,11 +257,8 @@ msgstr "Nuevo"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_bar.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_renderer.js:0
#: code:addons/web/static/src/search/search_bar/search_bar.js:0
#: code:addons/web/static/src/views/kanban/kanban_renderer.js:0
#: code:addons/web/static/src/views/kanban/kanban_header.js:0
#: code:addons/web/static/src/views/list/list_renderer.js:0
#: code:addons/web/static/src/views/pivot/pivot_model.js:0
#, python-format
@ -307,7 +267,6 @@ msgstr "No"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/views/view_button/view_button.xml:0
#, python-format
msgid "Object:"
@ -317,15 +276,10 @@ msgstr "Objeto:"
#. odoo-javascript
#: code:addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js:0
#: code:addons/web/static/src/core/dialog/dialog.xml:0
#: code:addons/web/static/src/core/errors/error_dialogs.xml:0
#: code:addons/web/static/src/legacy/js/core/dialog.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_confirm_dialog.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/legacy/xml/control_panel.xml:0
#: code:addons/web/static/src/public/error_notifications.js:0
#: code:addons/web/static/src/views/calendar/calendar_year/calendar_year_popover.xml:0
#: code:addons/web/static/src/views/list/list_confirmation_dialog.xml:0
#: code:addons/web/static/src/views/view_dialogs/form_view_dialog.xml:0
#: code:addons/web/static/src/webclient/actions/action_dialog.xml:0
#, python-format
@ -335,8 +289,6 @@ msgstr "Aceptar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0
#: code:addons/web/static/src/legacy/js/views/form/form_controller.js:0
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#, python-format
msgid "Open: "
msgstr "Abrir: "
@ -348,7 +300,6 @@ msgstr "Contraseña"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.js:0
#, python-format
msgid "Please enter save field list name"
@ -356,14 +307,6 @@ msgstr "Por favor, introduzca el nombre de la lista de campos a guardar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#, python-format
msgid "Please select fields to export..."
msgstr "Por favor, seleccione los campos a exportar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.js:0
#, python-format
msgid "Please select fields to save export list..."
@ -379,8 +322,10 @@ msgstr "Preferencias"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/file_viewer/file_viewer.xml:0
#: code:addons/web/static/src/legacy/js/components/action_menus.js:0
#: code:addons/web/static/src/search/action_menus/action_menus.xml:0
#: code:addons/web/static/src/search/cog_menu/cog_menu.xml:0
#: code:addons/web/static/src/webclient/actions/reports/report_action.xml:0
#, python-format
msgid "Print"
@ -388,8 +333,6 @@ msgstr "Imprimir"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/search_bar/search_bar.xml:0
#: code:addons/web/static/src/views/fields/relational_utils.js:0
#: code:addons/web/static/src/views/fields/relational_utils.xml:0
@ -400,12 +343,8 @@ msgstr "Eliminar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/domain_selector_dialog/domain_selector_dialog.xml:0
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/js/widgets/domain_selector_dialog.js:0
#: code:addons/web/static/src/legacy/js/widgets/translation_dialog.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/favorite_menu/custom_favorite_item.xml:0
#: code:addons/web/static/src/search/custom_favorite_item/custom_favorite_item.xml:0
#: code:addons/web/static/src/views/fields/relational_utils.xml:0
#: code:addons/web/static/src/views/fields/translation_dialog.xml:0
#: code:addons/web/static/src/views/form/form_controller.xml:0
@ -444,12 +383,8 @@ msgstr "Sin definir"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/errors/error_dialogs.js:0
#: code:addons/web/static/src/legacy/js/views/basic/basic_controller.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_controller.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/favorite_menu/favorite_menu.js:0
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.js:0
#: code:addons/web/static/src/views/fields/domain/domain_field.xml:0
#: code:addons/web/static/src/views/fields/translation_button.js:0
#: code:addons/web/static/src/views/list/list_controller.js:0
#, python-format
msgid "Warning"
@ -461,6 +396,7 @@ msgstr "Aviso"
#: code:addons/web/static/src/search/utils/dates.js:0
#: code:addons/web/static/src/views/calendar/calendar_common/calendar_common_renderer.js:0
#: code:addons/web/static/src/views/calendar/calendar_controller.js:0
#: code:addons/web/static/src/views/calendar/calendar_controller.xml:0
#, python-format
msgid "Week"
msgstr "Semana"
@ -476,13 +412,9 @@ msgstr "Año"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_bar.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_renderer.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/search_bar/search_bar.js:0
#: code:addons/web/static/src/views/fields/field_tooltip.xml:0
#: code:addons/web/static/src/views/kanban/kanban_renderer.js:0
#: code:addons/web/static/src/views/kanban/kanban_header.js:0
#: code:addons/web/static/src/views/list/list_renderer.js:0
#: code:addons/web/static/src/views/pivot/pivot_model.js:0
#, python-format
@ -494,7 +426,6 @@ msgstr "Sí"
#: code:addons/web/static/src/core/domain_selector/domain_selector_operators.js:0
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/legacy/js/widgets/domain_selector.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "contains"
msgstr "contiene"
@ -502,7 +433,6 @@ msgstr "contiene"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "doesn't contain"
msgstr "no contiene"
@ -510,19 +440,16 @@ msgstr "no contiene"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "greater than"
msgstr "mayor que"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/domain_selector/domain_selector_leaf_node.xml:0
#: code:addons/web/static/src/core/domain_selector/fields/domain_selector_boolean_field.js:0
#: code:addons/web/static/src/core/domain_selector/domain_selector_operators.js:0
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/legacy/js/widgets/domain_selector.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "is"
msgstr "es"
@ -530,17 +457,15 @@ msgstr "es"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "is equal to"
msgstr "es igual a"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/domain_selector/fields/domain_selector_boolean_field.js:0
#: code:addons/web/static/src/core/domain_selector/domain_selector_operators.js:0
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/legacy/js/widgets/domain_selector.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "is not"
msgstr "no es"
@ -548,7 +473,6 @@ msgstr "no es"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "is not equal to"
msgstr "es distinto de"
@ -556,17 +480,14 @@ msgstr "es distinto de"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "less than"
msgstr "menor que"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/domain_selector/domain_selector_leaf_node.xml:0
#: code:addons/web/static/src/legacy/js/views/action_model.js:0
#: code:addons/web/static/src/core/domain_selector/utils.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.xml:0
#: code:addons/web/static/src/search/search_model.js:0
#, python-format
msgid "or"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -7,3 +7,6 @@ from . import ir_model
from . import ir_ui_menu
from . import models
from . import base_document_layout
from . import res_config_settings
from . import res_partner
from . import res_users

View file

@ -2,12 +2,13 @@
import markupsafe
import os
from markupsafe import Markup
from math import ceil
from odoo import api, fields, models, tools
from odoo.addons.base.models.ir_qweb_fields import nl2br
from odoo.modules import get_resource_path
from odoo.tools import file_path, html2plaintext, is_html_empty
from odoo.tools import html2plaintext, is_html_empty
from odoo.tools.misc import file_path
try:
import sass as libsass
@ -217,7 +218,7 @@ class BaseDocumentLayout(models.TransientModel):
return False, False
base_w, base_h = image.size
w = int(50 * base_w / base_h)
w = ceil(50 * base_w / base_h)
h = 50
# Converts to RGBA (if already RGBA, this is a noop)
@ -251,14 +252,6 @@ class BaseDocumentLayout(models.TransientModel):
return tools.rgb_to_hex(primary), tools.rgb_to_hex(secondary)
@api.model
def action_open_base_document_layout(self, action_ref=None):
if not action_ref:
action_ref = 'web.action_base_document_layout_configurator'
res = self.env["ir.actions.actions"]._for_xml_id(action_ref)
self.env[res["res_model"]].check_access_rights('write')
return res
def document_layout_save(self):
# meant to be overridden
return self.env.context.get('report_action') or {'type': 'ir.actions.act_window_close'}
@ -306,7 +299,7 @@ class BaseDocumentLayout(models.TransientModel):
precision = 8
output_style = 'expanded'
bootstrap_path = get_resource_path('web', 'static', 'lib', 'bootstrap', 'scss')
bootstrap_path = file_path('web/static/lib/bootstrap/scss')
try:
return libsass.compile(

View file

@ -2,17 +2,14 @@
import hashlib
import json
import logging
import odoo
from odoo import api, http, models
from odoo.http import request
from odoo.tools import file_open, image_process, ustr
from odoo import api, models
from odoo.http import request, DEFAULT_MAX_CONTENT_LENGTH
from odoo.tools import ormcache, ustr
from odoo.tools.misc import str2bool
_logger = logging.getLogger(__name__)
"""
Debug mode is stored in session and should always be a string.
It can be activated with an URL query string `debug=<mode>` where mode
@ -58,6 +55,11 @@ class Http(models.AbstractModel):
super()._pre_dispatch(rule, args)
cls._handle_debug()
@classmethod
def _post_logout(cls):
super()._post_logout()
request.future_response.set_cookie('cids', max_age=0)
def webclient_rendering_context(self):
return {
'menu_data': request.env['ir.ui.menu'].load_menus(request.session.debug),
@ -79,24 +81,27 @@ class Http(models.AbstractModel):
IrConfigSudo = self.env['ir.config_parameter'].sudo()
max_file_upload_size = int(IrConfigSudo.get_param(
'web.max_file_upload_size',
default=128 * 1024 * 1024, # 128MiB
default=DEFAULT_MAX_CONTENT_LENGTH,
))
mods = odoo.conf.server_wide_modules or []
if request.db:
mods = list(request.registry._init_modules) + mods
is_internal_user = user.has_group('base.group_user')
session_info = {
"uid": session_uid,
"is_system": user._is_system() if session_uid else False,
"is_admin": user._is_admin() if session_uid else False,
"is_public": user._is_public(),
"is_internal_user": is_internal_user,
"user_context": user_context,
"db": self.env.cr.dbname,
"user_settings": self.env['res.users.settings']._find_or_create_for_user(user)._res_users_settings_format(),
"server_version": version_info.get('server_version'),
"server_version_info": version_info.get('server_version_info'),
"support_url": "https://www.odoo.com/buy",
"name": user.name,
"username": user.login,
"partner_display_name": user.partner_id.display_name,
"company_id": user.company_id.id if session_uid else None, # YTI TODO: Remove this from the user context
"partner_id": user.partner_id.id if session_uid and user.partner_id else None,
"web.base.url": IrConfigSudo.get_param('web.base.url', default=''),
"active_ids_limit": int(IrConfigSudo.get_param('web.active_ids_limit', default='20000')),
@ -117,7 +122,7 @@ class Http(models.AbstractModel):
}
if request.session.debug:
session_info['bundle_params']['debug'] = request.session.debug
if self.env.user.has_group('base.group_user'):
if is_internal_user:
# the following is only useful in the context of a webclient bootstrapping
# but is still included in some other calls (e.g. '/web/session/authenticate')
# to avoid access errors and unnecessary information, it is only included for users
@ -128,6 +133,9 @@ class Http(models.AbstractModel):
session_info['cache_hashes'].update({
"load_menus": hashlib.sha512(menu_json_utf8).hexdigest()[:64], # sha512/256
})
# We need sudo since a user may not have access to ancestor companies
disallowed_ancestor_companies_sudo = user.company_ids.sudo().parent_ids - user.company_ids
all_companies_in_hierarchy_sudo = disallowed_ancestor_companies_sudo + user.company_ids
session_info.update({
# current_company should be default_company
"user_companies": {
@ -137,8 +145,19 @@ class Http(models.AbstractModel):
'id': comp.id,
'name': comp.name,
'sequence': comp.sequence,
'child_ids': (comp.child_ids & all_companies_in_hierarchy_sudo).ids,
'parent_id': comp.parent_id.id,
} for comp in user.company_ids
},
'disallowed_ancestor_companies': {
comp.id: {
'id': comp.id,
'name': comp.name,
'sequence': comp.sequence,
'child_ids': (comp.child_ids & all_companies_in_hierarchy_sudo).ids,
'parent_id': comp.parent_id.id,
} for comp in disallowed_ancestor_companies_sudo
},
},
"show_effect": True,
"display_switch_company_menu": user.has_group('base.group_multi_company') and len(user.company_ids) > 1,
@ -152,6 +171,7 @@ class Http(models.AbstractModel):
session_info = {
'is_admin': user._is_admin() if session_uid else False,
'is_system': user._is_system() if session_uid else False,
'is_public': user._is_public(),
'is_website_user': user._is_public() if session_uid else False,
'user_id': user.id if session_uid else False,
'is_frontend': True,
@ -159,6 +179,7 @@ class Http(models.AbstractModel):
'profile_collectors': request.session.profile_collectors,
'profile_params': request.session.profile_params,
'show_effect': bool(request.env['ir.config_parameter'].sudo().get_param('base_setup.show_effect')),
'currencies': self.get_currencies(),
'bundle_params': {
'lang': request.session.context['lang'],
},
@ -173,7 +194,11 @@ class Http(models.AbstractModel):
})
return session_info
@ormcache()
def get_currencies(self):
Currency = self.env['res.currency']
currencies = Currency.search([]).read(['symbol', 'position', 'decimal_places'])
return {c['id']: {'symbol': c['symbol'], 'position': c['position'], 'digits': [69,c['decimal_places']]} for c in currencies}
currencies = Currency.search_fetch([], ['symbol', 'position', 'decimal_places'])
return {
c.id: {'symbol': c.symbol, 'position': c.position, 'digits': [69, c.decimal_places]}
for c in currencies
}

View file

@ -19,7 +19,7 @@ class IrModel(models.Model):
accessible_models = []
not_accessible_models = []
for model in models:
if self._check_model_access(model):
if self._is_valid_for_model_selector(model):
accessible_models.append(model)
else:
not_accessible_models.append({"display_name": model, "model": model})
@ -34,9 +34,15 @@ class IrModel(models.Model):
} for model in records]
@api.model
def _check_model_access(self, model):
return (self.env.user._is_internal() and model in self.env
and self.env[model].check_access_rights("read", raise_exception=False))
def _is_valid_for_model_selector(self, model):
model = self.env.get(model)
return (
self.env.user._is_internal()
and model is not None
and model.check_access_rights("read", raise_exception=False)
and not model._transient
and not model._abstract
)
@api.model
def get_available_models(self):
@ -44,5 +50,5 @@ class IrModel(models.Model):
Return the list of models the current user has access to, with their
corresponding display name.
"""
accessible_models = [model for model in self.pool.keys() if self._check_model_access(model)]
accessible_models = [model for model in self.pool if self._is_valid_for_model_selector(model)]
return self._display_name_for(accessible_models)

View file

@ -6,7 +6,7 @@ from collections import OrderedDict
from werkzeug.urls import url_quote
from markupsafe import Markup
from odoo import api, models
from odoo import api, models, fields
from odoo.tools import pycompat
from odoo.tools import html_escape as escape
@ -36,7 +36,7 @@ class Image(models.AbstractModel):
if max_width or max_height:
max_size = '%sx%s' % (max_width, max_height)
sha = hashlib.sha512(str(getattr(record, '__last_update')).encode('utf-8')).hexdigest()[:7]
sha = hashlib.sha512(str(getattr(record, 'write_date', fields.Datetime.now())).encode('utf-8')).hexdigest()[:7]
max_size = '' if max_size is None else '/%s' % max_size
if options.get('filename-field') and options['filename-field'] in record and record[options['filename-field']]:

View file

@ -32,6 +32,7 @@ class IrUiMenu(models.Model):
"actionModel": False,
"webIcon": None,
"webIconData": None,
"webIconDataMimetype": None,
"backgroundImage": menu.get('backgroundImage'),
}
else:
@ -57,6 +58,7 @@ class IrUiMenu(models.Model):
"actionModel": action_model,
"webIcon": menu['web_icon'],
"webIconData": menu['web_icon_data'],
"webIconDataMimetype": menu['web_icon_data_mimetype'],
}
return web_menus

View file

@ -1,14 +1,19 @@
# -*- coding: utf-8 -*-
from typing import Dict, List
import babel.dates
import pytz
from lxml import etree
import base64
import copy
import itertools
import json
import pytz
from odoo import _, _lt, api, fields, models
from odoo.fields import Command
from odoo.models import BaseModel, NewId
from odoo.osv.expression import AND, TRUE_DOMAIN, normalize_domain
from odoo.tools import date_utils, lazy, OrderedSet
from odoo.tools.misc import get_lang
from odoo.tools import date_utils, unique
from odoo.tools.misc import OrderedSet, get_lang
from odoo.exceptions import UserError
from collections import defaultdict
@ -33,37 +38,20 @@ DISPLAY_DATE_FORMATS = {
}
class IrActionsActWindowView(models.Model):
_inherit = 'ir.actions.act_window.view'
view_mode = fields.Selection(selection_add=[
('qweb', 'QWeb')
], ondelete={'qweb': 'cascade'})
class Base(models.AbstractModel):
_inherit = 'base'
@api.model
def web_search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, count_limit=None):
"""
Performs a search_read and a search_count.
def web_search_read(self, domain, specification, offset=0, limit=None, order=None, count_limit=None):
records = self.search_fetch(domain, specification.keys(), offset=offset, limit=limit, order=order)
values_records = records.web_read(specification)
return self._format_web_search_read_results(domain, values_records, offset, limit, count_limit)
:param domain: search domain
:param fields: list of fields to read
:param limit: maximum number of records to read
:param offset: number of records to skip
:param order: columns to sort results
:return: {
'records': array of read records (result of a call to 'search_read')
'length': number of records matching the domain (result of a call to 'search_count')
}
"""
records = self.search_read(domain, fields, offset=offset, limit=limit, order=order)
def _format_web_search_read_results(self, domain, records, offset=0, limit=None, count_limit=None):
if not records:
return {
'length': 0,
'records': []
'records': [],
}
current_length = len(records) + offset
limit_reached = len(records) == limit
@ -75,15 +63,166 @@ class Base(models.AbstractModel):
length = current_length
return {
'length': length,
'records': records
'records': records,
}
def web_save(self, vals, specification: Dict[str, Dict], next_id=None) -> List[Dict]:
if self:
self.write(vals)
else:
self = self.create(vals)
if next_id:
self = self.browse(next_id)
return self.with_context(bin_size=True).web_read(specification)
def web_read(self, specification: Dict[str, Dict]) -> List[Dict]:
fields_to_read = list(specification) or ['id']
if fields_to_read == ['id']:
# if we request to read only the ids, we have them already so we can build the return dictionaries immediately
# this also avoid a call to read on the co-model that might have different access rules
values_list = [{'id': id_} for id_ in self._ids]
else:
values_list: List[Dict] = self.read(fields_to_read, load=None)
if not values_list:
return values_list
def cleanup(vals: Dict) -> Dict:
""" Fixup vals['id'] of a new record. """
if not vals['id']:
vals['id'] = vals['id'].origin or False
return vals
for field_name, field_spec in specification.items():
field = self._fields.get(field_name)
if field is None:
continue
if field.type == 'many2one':
if 'fields' not in field_spec:
for values in values_list:
if isinstance(values[field_name], NewId):
values[field_name] = values[field_name].origin
continue
co_records = self[field_name]
if 'context' in field_spec:
co_records = co_records.with_context(**field_spec['context'])
extra_fields = dict(field_spec['fields'])
extra_fields.pop('display_name', None)
many2one_data = {
vals['id']: cleanup(vals)
for vals in co_records.web_read(extra_fields)
}
if 'display_name' in field_spec['fields']:
for rec in co_records.sudo():
many2one_data[rec.id]['display_name'] = rec.display_name
for values in values_list:
if values[field_name] is False:
continue
vals = many2one_data[values[field_name]]
values[field_name] = vals['id'] and vals
elif field.type in ('one2many', 'many2many'):
if not field_spec:
continue
co_records = self[field_name]
if 'order' in field_spec and field_spec['order']:
co_records = co_records.with_context(active_test=False).search(
[('id', 'in', co_records.ids)], order=field_spec['order'],
).with_context(co_records.env.context) # Reapply previous context
order_key = {
co_record.id: index
for index, co_record in enumerate(co_records)
}
for values in values_list:
# filter out inaccessible corecords in case of "cache pollution"
values[field_name] = [id_ for id_ in values[field_name] if id_ in order_key]
values[field_name] = sorted(values[field_name], key=order_key.__getitem__)
if 'context' in field_spec:
co_records = co_records.with_context(**field_spec['context'])
if 'fields' in field_spec:
if field_spec.get('limit') is not None:
limit = field_spec['limit']
ids_to_read = OrderedSet(
id_
for values in values_list
for id_ in values[field_name][:limit]
)
co_records = co_records.browse(ids_to_read)
x2many_data = {
vals['id']: vals
for vals in co_records.web_read(field_spec['fields'])
}
for values in values_list:
values[field_name] = [x2many_data.get(id_) or {'id': id_} for id_ in values[field_name]]
elif field.type in ('reference', 'many2one_reference'):
if not field_spec:
continue
values_by_id = {
vals['id']: vals
for vals in values_list
}
for record in self:
if not record[field_name]:
continue
if field.type == 'reference':
co_record = record[field_name]
else: # field.type == 'many2one_reference'
co_record = self.env[record[field.model_field]].browse(record[field_name])
if 'context' in field_spec:
co_record = co_record.with_context(**field_spec['context'])
if 'fields' in field_spec:
reference_read = co_record.web_read(field_spec['fields'])
if any(fname != 'id' for fname in field_spec['fields']):
# we can infer that if we can read fields for the co-record, it exists
co_record_exists = bool(reference_read)
else:
co_record_exists = co_record.exists()
else:
# If there are no fields to read (field_spec.get('fields') --> None) and we web_read ids, it will
# not actually read the records so we do not know if they exist.
# This ensures the record actually exists
co_record_exists = co_record.exists()
record_values = values_by_id[record.id]
if not co_record_exists:
record_values[field_name] = False
if field.type == 'many2one_reference':
record_values[field.model_field] = False
continue
if 'fields' in field_spec:
record_values[field_name] = reference_read[0]
if field.type == 'reference':
record_values[field_name]['id'] = {
'id': co_record.id,
'model': co_record._name
}
return values_list
@api.model
def web_read_group(self, domain, fields, groupby, limit=None, offset=0, orderby=False,
lazy=True, expand=False, expand_limit=None, expand_orderby=False):
def web_read_group(self, domain, fields, groupby, limit=None, offset=0, orderby=False, lazy=True):
"""
Returns the result of a read_group (and optionally search for and read records inside each
group), and the total number of groups matching the search domain.
Returns the result of a read_group and the total number of groups matching the search domain.
:param domain: search domain
:param fields: list of fields to read (see ``fields``` param of ``read_group``)
@ -92,29 +231,23 @@ class Base(models.AbstractModel):
:param offset: see ``offset`` param of ``read_group``
:param orderby: see ``orderby`` param of ``read_group``
:param lazy: see ``lazy`` param of ``read_group``
:param expand: if true, and groupby only contains one field, read records inside each group
:param expand_limit: maximum number of records to read in each group
:param expand_orderby: order to apply when reading records in each group
:return: {
'groups': array of read groups
'length': total number of groups
}
"""
groups = self._web_read_group(domain, fields, groupby, limit, offset, orderby, lazy, expand,
expand_limit, expand_orderby)
groups = self._web_read_group(domain, fields, groupby, limit, offset, orderby, lazy)
if not groups:
length = 0
elif limit and len(groups) == limit:
# We need to fetch all groups to know the total number
# this cannot be done all at once to avoid MemoryError
length = limit
chunk_size = 100000
while True:
more = len(self.read_group(domain, ['display_name'], groupby, offset=length, limit=chunk_size, lazy=True))
length += more
if more < chunk_size:
break
annoted_groupby = self._read_group_get_annoted_groupby(groupby, lazy=lazy)
length = limit + len(self._read_group(
domain,
groupby=annoted_groupby.values(),
offset=limit,
))
else:
length = len(groups) + offset
return {
@ -123,23 +256,14 @@ class Base(models.AbstractModel):
}
@api.model
def _web_read_group(self, domain, fields, groupby, limit=None, offset=0, orderby=False,
lazy=True, expand=False, expand_limit=None, expand_orderby=False):
def _web_read_group(self, domain, fields, groupby, limit=None, offset=0, orderby=False, lazy=True):
"""
Performs a read_group and optionally a web_search_read for each group.
See ``web_read_group`` for params description.
:returns: array of groups
"""
groups = self.read_group(domain, fields, groupby, offset=offset, limit=limit,
orderby=orderby, lazy=lazy)
if expand and len(groupby) == 1:
for group in groups:
group['__data'] = self.web_search_read(domain=group['__domain'], fields=fields,
offset=0, limit=expand_limit,
order=expand_orderby)
return groups
@api.model
@ -156,8 +280,9 @@ class Base(models.AbstractModel):
:return a dictionnary mapping group_by values to dictionnaries mapping
progress bar field values to the related number of records
"""
group_by_fname = group_by.partition(':')[0]
field_type = self._fields[group_by_fname].type
group_by_fullname = group_by.partition(':')[0]
group_by_fieldname = group_by_fullname.split(".")[0] # split on "." in case we group on a property
field_type = self._fields[group_by_fieldname].type
if field_type == 'selection':
selection_labels = dict(self.fields_get()[group_by]['selection'])
@ -185,26 +310,40 @@ class Base(models.AbstractModel):
try:
fname = progress_bar['field']
return self.read_group(domain, [fname], [group_by, fname], lazy=False)
except UserError:
except ValueError:
# possibly failed because of grouping on or aggregating non-stored
# field; fallback on alternative implementation
pass
# Workaround to match read_group's infrastructure
# TO DO in master: harmonize this function and readgroup to allow factorization
group_by_name = group_by.partition(':')[0]
group_by_fullname = group_by.partition(':')[0]
group_by_fieldname = group_by_fullname.split(".")[0] # split on "." in case we group on a property
group_by_modifier = group_by.partition(':')[2] or 'month'
records_values = self.search_read(domain or [], [progress_bar['field'], group_by_name])
field_type = self._fields[group_by_name].type
records_values = self.search_read(domain or [], [progress_bar['field'], group_by_fieldname])
field_type = self._fields[group_by_fieldname].type
for record_values in records_values:
group_by_value = record_values.pop(group_by_name)
group_by_value = record_values.pop(group_by_fieldname)
property_name = group_by_fullname.partition('.')[2]
if field_type == "properties" and group_by_value:
group_by_value = next(
(definition['value'] for definition in group_by_value
if definition['name'] == property_name),
False,
)
# Again, imitating what _read_group_format_result and _read_group_prepare_data do
if group_by_value and field_type in ['date', 'datetime']:
locale = get_lang(self.env).code
group_by_value = date_utils.start_of(fields.Datetime.to_datetime(group_by_value), group_by_modifier)
group_by_value = fields.Datetime.to_datetime(group_by_value)
if group_by_modifier != 'week':
# start_of(v, 'week') does not take into account the locale
# to determine the first day of the week; this part is not
# necessary, since the formatting below handles the locale
# as expected, and outputs correct results
group_by_value = date_utils.start_of(group_by_value, group_by_modifier)
group_by_value = pytz.timezone('UTC').localize(group_by_value)
tz_info = None
if field_type == 'datetime' and self._context.get('tz') in pytz.all_timezones:
@ -225,32 +364,6 @@ class Base(models.AbstractModel):
return records_values
##### qweb view hooks #####
@api.model
def qweb_render_view(self, view_id, domain):
assert view_id
return self.env['ir.qweb']._render(
view_id,
{
'model': self,
'domain': domain,
# not necessarily necessary as env is already part of the
# non-minimal qcontext
'context': self.env.context,
'records': lazy(self.search, domain),
})
@api.model
def _get_view(self, view_id=None, view_type='form', **options):
arch, view = super()._get_view(view_id, view_type, **options)
# avoid leaking the raw (un-rendered) template, also avoids bloating
# the response payload for no reason. Only send the root node,
# to send attributes such as `js_class`.
if view_type == 'qweb':
root = arch
arch = etree.Element('qweb', root.attrib)
return arch, view
@api.model
def _search_panel_field_image(self, field_name, **kwargs):
"""
@ -779,6 +892,232 @@ class Base(models.AbstractModel):
return { 'values': field_range, }
def onchange(self, values: Dict, field_names: List[str], fields_spec: Dict):
"""
Perform an onchange on the given fields, and return the result.
:param values: dictionary mapping field names to values on the form view,
giving the current state of modification
:param field_names: names of the modified fields
:param fields_spec: dictionary specifying the fields in the view,
just like the one used by :meth:`web_read`; it is used to format
the resulting values
When creating a record from scratch, the client should call this with an
empty list as ``field_names``. In that case, the method first adds
default values to ``values``, computes the remaining fields, applies
onchange methods to them, and return all the fields in ``fields_spec``.
The result is a dictionary with two optional keys. The key ``"value"``
is used to return field values that should be modified on the caller.
The corresponding value is a dict mapping field names to their value,
in the format of :meth:`web_read`, except for x2many fields, where the
value is a list of commands to be applied on the caller's field value.
The key ``"warning"`` provides a warning message to the caller. The
corresponding value is a dictionary like::
{
"title": "Be careful!", # subject of message
"message": "Blah blah blah.", # full warning message
"type": "dialog", # how to display the warning
}
"""
# this is for tests using `Form`
self.env.flush_all()
env = self.env
cache = env.cache
first_call = not field_names
if any(fname not in self._fields for fname in field_names):
return {}
if first_call:
field_names = [fname for fname in values if fname != 'id']
missing_names = [fname for fname in fields_spec if fname not in values]
defaults = self.default_get(missing_names)
for field_name in missing_names:
values[field_name] = defaults.get(field_name, False)
if field_name in defaults:
field_names.append(field_name)
# prefetch x2many lines: this speeds up the initial snapshot by avoiding
# computing fields on new records as much as possible, as that can be
# costly and is not necessary at all
self.fetch(fields_spec.keys())
for field_name, field_spec in fields_spec.items():
field = self._fields[field_name]
if field.type not in ('one2many', 'many2many'):
continue
sub_fields_spec = field_spec.get('fields') or {}
if sub_fields_spec and values.get(field_name):
# retrieve all line ids in commands
line_ids = OrderedSet(self[field_name].ids)
for cmd in values[field_name]:
if cmd[0] in (Command.UPDATE, Command.LINK):
line_ids.add(cmd[1])
elif cmd[0] == Command.SET:
line_ids.update(cmd[2])
# prefetch stored fields on lines
lines = self[field_name].browse(line_ids)
lines.fetch(sub_fields_spec.keys())
# copy the cache of lines to their corresponding new records;
# this avoids computing computed stored fields on new_lines
new_lines = lines.browse(map(NewId, line_ids))
for field_name in sub_fields_spec:
field = lines._fields[field_name]
line_values = [
field.convert_to_cache(line[field_name], new_line, validate=False)
for new_line, line in zip(new_lines, lines)
]
cache.update(new_lines, field, line_values)
# Isolate changed values, to handle inconsistent data sent from the
# client side: when a form view contains two one2many fields that
# overlap, the lines that appear in both fields may be sent with
# different data. Consider, for instance:
#
# foo_ids: [line with value=1, ...]
# bar_ids: [line with value=1, ...]
#
# If value=2 is set on 'line' in 'bar_ids', the client sends
#
# foo_ids: [line with value=1, ...]
# bar_ids: [line with value=2, ...]
#
# The idea is to put 'foo_ids' in cache first, so that the snapshot
# contains value=1 for line in 'foo_ids'. The snapshot is then updated
# with the value of `bar_ids`, which will contain value=2 on line.
#
# The issue also occurs with other fields. For instance, an onchange on
# a move line has a value for the field 'move_id' that contains the
# values of the move, among which the one2many that contains the line
# itself, with old values!
#
initial_values = dict(values)
changed_values = {fname: initial_values.pop(fname) for fname in field_names}
# do not force delegate fields to False
for parent_name in self._inherits.values():
if not initial_values.get(parent_name, True):
initial_values.pop(parent_name)
# create a new record with initial values
if self:
# fill in the cache of record with the values of self
cache_values = {fname: self[fname] for fname in fields_spec}
record = self.new(cache_values, origin=self)
# apply initial values on top of the values of self
record._update_cache(initial_values)
else:
# set changed values to null in initial_values; not setting them
# triggers default_get() on the new record when creating snapshot0
initial_values.update(dict.fromkeys(field_names, False))
record = self.new(initial_values, origin=self)
# make parent records match with the form values; this ensures that
# computed fields on parent records have all their dependencies at
# their expected value
for field_name in initial_values:
field = self._fields.get(field_name)
if field and field.inherited:
parent_name, field_name = field.related.split('.', 1)
if parent := record[parent_name]:
parent._update_cache({field_name: record[field_name]})
# make a snapshot based on the initial values of record
snapshot0 = RecordSnapshot(record, fields_spec, fetch=(not first_call))
# store changed values in cache; also trigger recomputations based on
# subfields (e.g., line.a has been modified, line.b is computed stored
# and depends on line.a, but line.b is not in the form view)
record._update_cache(changed_values)
# update snapshot0 with changed values
for field_name in field_names:
snapshot0.fetch(field_name)
# Determine which field(s) should be triggered an onchange. On the first
# call, 'names' only contains fields with a default. If 'self' is a new
# line in a one2many field, 'names' also contains the one2many's inverse
# field, and that field may not be in nametree.
todo = list(unique(itertools.chain(field_names, fields_spec))) if first_call else list(field_names)
done = set()
# mark fields to do as modified to trigger recomputations
protected = [self._fields[fname] for fname in field_names]
with self.env.protecting(protected, record):
record.modified(todo)
for field_name in todo:
field = self._fields[field_name]
if field.inherited:
# modifying an inherited field should modify the parent
# record accordingly; because we don't actually assign the
# modified field on the record, the modification on the
# parent record has to be done explicitly
parent = record[field.related.split('.')[0]]
parent[field_name] = record[field_name]
result = {'warnings': OrderedSet()}
# process names in order
while todo:
# apply field-specific onchange methods
for field_name in todo:
record._apply_onchange_methods(field_name, result)
done.add(field_name)
if not env.context.get('recursive_onchanges', True):
break
# determine which fields to process for the next pass
todo = [
field_name
for field_name in fields_spec
if field_name not in done and snapshot0.has_changed(field_name)
]
# make the snapshot with the final values of record
snapshot1 = RecordSnapshot(record, fields_spec)
# determine values that have changed by comparing snapshots
result['value'] = snapshot1.diff(snapshot0, force=first_call)
# format warnings
warnings = result.pop('warnings')
if len(warnings) == 1:
title, message, type_ = warnings.pop()
if not type_:
type_ = 'dialog'
result['warning'] = dict(title=title, message=message, type=type_)
elif len(warnings) > 1:
# concatenate warning titles and messages
title = _("Warnings")
message = '\n\n'.join([warn_title + '\n\n' + warn_message for warn_title, warn_message, warn_type in warnings])
result['warning'] = dict(title=title, message=message, type='dialog')
return result
def web_override_translations(self, values):
"""
This method is used to override all the modal translations of the given fields
with the provided value for each field.
:param values: dictionary of the translations to apply for each field name
ex: { "field_name": "new_value" }
"""
self.ensure_one()
for field_name in values:
field = self._fields[field_name]
if field.translate is True:
translations = {lang: False for lang, _ in self.env['res.lang'].get_installed()}
translations['en_US'] = values[field_name]
translations[self.env.lang or 'en_US'] = values[field_name]
self.update_field_translations(field_name, translations)
class ResCompany(models.Model):
_inherit = 'res.company'
@ -815,3 +1154,114 @@ class ResCompany(models.Model):
b64_val = self._get_asset_style_b64()
if b64_val != asset_attachment.datas:
asset_attachment.write({'datas': b64_val})
class RecordSnapshot(dict):
""" A dict with the values of a record, following a prefix tree. """
__slots__ = ['record', 'fields_spec']
def __init__(self, record: BaseModel, fields_spec: Dict, fetch=True):
# put record in dict to include it when comparing snapshots
super().__init__()
self.record = record
self.fields_spec = fields_spec
if fetch:
for name in fields_spec:
self.fetch(name)
def __eq__(self, other: 'RecordSnapshot'):
return self.record == other.record and super().__eq__(other)
def fetch(self, field_name):
""" Set the value of field ``name`` from the record's value. """
if self.record._fields[field_name].type in ('one2many', 'many2many'):
# x2many fields are serialized as a dict of line snapshots
lines = self.record[field_name]
if 'context' in self.fields_spec[field_name]:
lines = lines.with_context(**self.fields_spec[field_name]['context'])
sub_fields_spec = self.fields_spec[field_name].get('fields') or {}
self[field_name] = {line.id: RecordSnapshot(line, sub_fields_spec) for line in lines}
else:
self[field_name] = self.record[field_name]
def has_changed(self, field_name) -> bool:
""" Return whether a field on the record has changed. """
if field_name not in self:
return True
if self.record._fields[field_name].type not in ('one2many', 'many2many'):
return self[field_name] != self.record[field_name]
return self[field_name].keys() != set(self.record[field_name]._ids) or any(
line_snapshot.has_changed(subname)
for line_snapshot in self[field_name].values()
for subname in self.fields_spec[field_name].get('fields') or {}
)
def diff(self, other: 'RecordSnapshot', force=False):
""" Return the values in ``self`` that differ from ``other``. """
# determine fields to return
simple_fields_spec = {}
x2many_fields_spec = {}
for field_name, field_spec in self.fields_spec.items():
if field_name == 'id':
continue
if not force and other.get(field_name) == self[field_name]:
continue
field = self.record._fields[field_name]
if field.type in ('one2many', 'many2many'):
x2many_fields_spec[field_name] = field_spec
else:
simple_fields_spec[field_name] = field_spec
# use web_read() for simple fields
[result] = self.record.web_read(simple_fields_spec)
# discard the NewId from the dict
result.pop('id')
# for x2many fields: serialize value as commands
for field_name, field_spec in x2many_fields_spec.items():
commands = []
self_value = self[field_name]
other_value = {} if force else other.get(field_name) or {}
if any(other_value):
# other may be a snapshot for a real record, adapt its x2many ids
other_value = {NewId(id_): snap for id_, snap in other_value.items()}
# commands for removed lines
field = self.record._fields[field_name]
remove = Command.delete if field.type == 'one2many' else Command.unlink
for id_ in other_value:
if id_ not in self_value:
commands.append(remove(id_.origin or id_.ref or 0))
# commands for modified or extra lines
for id_, line_snapshot in self_value.items():
if not force and id_ in other_value:
# existing line: check diff and send update
line_diff = line_snapshot.diff(other_value[id_])
if line_diff:
commands.append(Command.update(id_.origin or id_.ref or 0, line_diff))
elif not id_.origin:
# new line: send diff from scratch
line_diff = line_snapshot.diff({})
commands.append((Command.CREATE, id_.origin or id_.ref or 0, line_diff))
else:
# link line: send data to client
base_line = line_snapshot.record._origin
[base_data] = base_line.web_read(field_spec.get('fields') or {})
commands.append((Command.LINK, base_line.id, base_data))
# check diff and send update
base_snapshot = RecordSnapshot(base_line, field_spec.get('fields') or {})
line_diff = line_snapshot.diff(base_snapshot)
if line_diff:
commands.append(Command.update(id_.origin, line_diff))
if commands:
result[field_name] = commands
return result

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
web_app_name = fields.Char('Web App Name', config_parameter='web.web_app_name')

Some files were not shown because too many files have changed in this diff Show more