vanilla 18.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:48:04 +02:00
parent 0a7ae8db93
commit 5454004ff9
1963 changed files with 1187893 additions and 919508 deletions

View file

@ -15,40 +15,14 @@ pip install odoo-bringout-oca-ocb-web
## Dependencies ## Dependencies
This addon depends on:
- base - base
## Manifest Information
- **Name**: Web
- **Version**: 1.0
- **Category**: Hidden
- **License**: LGPL-3
- **Installable**: False
## Modifications
This package has been modified from the original OCA/OCB source:
- Removed proprietary mobile app download buttons (Google Play Store, Apple App Store) from base_setup configuration views
## Source ## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `web`. - Repository: https://github.com/OCA/OCB
- Branch: 18.0
- Path: addons/web
## License ## License
This package maintains the original LGPL-3 license from the upstream Odoo project. This package preserves the original LGPL-3 license.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -1,12 +1,14 @@
[project] [project]
name = "odoo-bringout-oca-ocb-web" name = "odoo-bringout-oca-ocb-web"
version = "16.0.0" version = "16.0.0"
description = "Web - Odoo addon" description = "Web -
Odoo addon
"
authors = [ authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" } { name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
] ]
dependencies = [ dependencies = [
"odoo-bringout-oca-ocb-base>=16.0.0", "odoo-bringout-oca-ocb-base>=18.0.0",
"requests>=2.25.1" "requests>=2.25.1"
] ]
readme = "README.md" readme = "README.md"
@ -22,8 +24,8 @@ classifiers = [
] ]
[project.urls] [project.urls]
homepage = "https://github.com/bringout/odoo-bringout-oca-ocb-web" homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/odoo-bringout-oca-ocb-web" repository = "https://github.com/bringout/0"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]

View file

@ -18,8 +18,8 @@ This module provides the core of the Odoo Web Client.
'views/webclient_templates.xml', 'views/webclient_templates.xml',
'views/report_templates.xml', 'views/report_templates.xml',
'views/base_document_layout_views.xml', 'views/base_document_layout_views.xml',
'views/partner_view.xml',
'views/speedscope_template.xml', 'views/speedscope_template.xml',
'views/lazy_assets.xml',
'views/neutralize_views.xml', 'views/neutralize_views.xml',
'data/ir_attachment.xml', 'data/ir_attachment.xml',
'data/report_layout.xml', 'data/report_layout.xml',
@ -37,74 +37,48 @@ This module provides the core of the Odoo Web Client.
# 3) an arbitrary name, relevant to the content of the bundle. # 3) an arbitrary name, relevant to the content of the bundle.
# #
# Examples: # Examples:
# > web.assets_common = assets common to backend clients and others # > web_editor.assets_snippets_menu = assets needed by components defined in the "web_editor" module.
# (not frontend).
# > web_editor.assets_wysiwyg = assets needed by components defined in the "web_editor" module.
# Warning: Layouts using "assets_frontend" assets do not have the 'web.assets_emoji': [
# "assets_common" assets anymore. So, if it make sense, files added in 'web/static/src/core/emoji_picker/emoji_data.js'
# "assets_common" should also be added in "assets_frontend". ],
# TODO in the future, probably remove "assets_common" definition 'web.assets_backend': [
# entirely and let all "main" bundles evolve on their own, including the
# files they need in their bundle.
'web.assets_common': [
('include', 'web._assets_helpers'), ('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss', 'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss', 'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.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/src/libs/fontawesome/css/font-awesome.css',
'web/static/lib/odoo_ui_icons/*', '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/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/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/polyfills/clipboard.js',
'web/static/src/boot.js',
'web/static/src/session.js',
'web/static/src/legacy/js/core/cookie_utils.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/popper/popper.js',
'web/static/lib/bootstrap/js/dist/util/index.js',
'web/static/lib/bootstrap/js/dist/dom/data.js', '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/event-handler.js',
'web/static/lib/bootstrap/js/dist/dom/manipulator.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/dom/selector-engine.js',
'web/static/lib/bootstrap/js/dist/util/config.js',
'web/static/lib/bootstrap/js/dist/util/component-functions.js',
'web/static/lib/bootstrap/js/dist/util/backdrop.js',
'web/static/lib/bootstrap/js/dist/util/focustrap.js',
'web/static/lib/bootstrap/js/dist/util/sanitizer.js',
'web/static/lib/bootstrap/js/dist/util/scrollbar.js',
'web/static/lib/bootstrap/js/dist/util/swipe.js',
'web/static/lib/bootstrap/js/dist/util/template-factory.js',
'web/static/lib/bootstrap/js/dist/base-component.js', 'web/static/lib/bootstrap/js/dist/base-component.js',
'web/static/lib/bootstrap/js/dist/alert.js', 'web/static/lib/bootstrap/js/dist/alert.js',
'web/static/lib/bootstrap/js/dist/button.js', 'web/static/lib/bootstrap/js/dist/button.js',
@ -118,81 +92,21 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/bootstrap/js/dist/scrollspy.js', 'web/static/lib/bootstrap/js/dist/scrollspy.js',
'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/tab.js',
'web/static/lib/bootstrap/js/dist/toast.js', 'web/static/lib/bootstrap/js/dist/toast.js',
'web/static/lib/tempusdominus/tempusdominus.js', 'web/static/src/libs/bootstrap.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/dompurify/DOMpurify.js',
'web/static/lib/bootstrap/scss/_variables.scss',
('include', 'web._assets_bootstrap'),
'base/static/src/css/modules.css', 'base/static/src/css/modules.css',
'web/static/src/core/utils/transitions.scss', 'web/static/src/core/utils/transitions.scss',
'web/static/src/core/**/*', 'web/static/src/model/**/*',
'web/static/src/search/**/*', 'web/static/src/search/**/*',
'web/static/src/webclient/icons.scss', # variables required in list_controller.scss 'web/static/src/webclient/icons.scss', # variables required in list_controller.scss
'web/static/src/views/**/*', 'web/static/src/views/**/*',
('remove', 'web/static/src/views/graph/**'),
('remove', 'web/static/src/views/pivot/**'),
'web/static/src/webclient/**/*', '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/webclient/clickbot/clickbot.js'), # lazy loaded
('remove', 'web/static/src/views/form/button_box/*.scss'), ('remove', 'web/static/src/views/form/button_box/*.scss'),
@ -201,109 +115,48 @@ This module provides the core of the Odoo Web Client.
'web/static/src/webclient/actions/reports/*.js', 'web/static/src/webclient/actions/reports/*.js',
'web/static/src/webclient/actions/reports/*.xml', '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/src/scss/ace.scss',
'web/static/lib/py.js/lib/py.js', 'web/static/src/scss/base_document_layout.scss',
'web/static/lib/py.js/lib/py_extras.js',
'web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js',
'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/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/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', 'base/static/src/scss/res_partner.scss',
# Form style should be computed before # Form style should be computed before
'web/static/src/views/form/button_box/*.scss', '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 # Don't include dark mode files in light mode
('remove', 'web/static/src/**/*.dark.scss'), ('remove', 'web/static/src/**/*.dark.scss'),
], ],
"web.assets_backend_legacy_lazy": [ 'web.assets_backend_lazy': [
("include", "web._assets_helpers"), ('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'), ('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss', 'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss', 'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
'web/static/src/views/graph/**',
'web/static/src/views/pivot/**',
],
'web.assets_backend_lazy_dark': [
('include', 'web.assets_backend_lazy'),
],
'web.assets_web': [
('include', 'web.assets_backend'),
'web/static/src/main.js',
'web/static/src/start.js',
], ],
'web.assets_frontend_minimal': [ 'web.assets_frontend_minimal': [
'web/static/src/legacy/js/promise_extension.js', 'web/static/src/polyfills/object.js',
'web/static/src/boot.js', 'web/static/src/polyfills/array.js',
'web/static/src/module_loader.js',
'web/static/src/session.js', 'web/static/src/session.js',
'web/static/src/legacy/js/core/cookie_utils.js', 'web/static/src/core/browser/cookie.js',
'web/static/src/legacy/js/core/menu.js', 'web/static/src/core/utils/ui.js',
'web/static/src/legacy/js/core/minimal_dom.js', 'web/static/src/legacy/js/public/minimal_dom.js',
'web/static/src/legacy/js/public/lazyloader.js', 'web/static/src/legacy/js/public/lazyloader.js',
], ],
'web.assets_frontend': [ 'web.assets_frontend': [
@ -318,61 +171,44 @@ This module provides the core of the Odoo Web Client.
'web/static/src/scss/pre_variables.scss', 'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss', 'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
'web/static/lib/luxon/luxon.js', '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/src/libs/fontawesome/css/font-awesome.css',
'web/static/lib/odoo_ui_icons/*', '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/webclient/navbar/navbar.scss',
'web/static/src/legacy/scss/ui.scss', 'web/static/src/scss/animation.scss',
'web/static/src/legacy/scss/mimetypes.scss', 'web/static/src/scss/base_frontend.scss',
'web/static/src/legacy/scss/modal.scss', 'web/static/src/scss/fontawesome_overridden.scss',
'web/static/src/legacy/scss/animation.scss', 'web/static/src/scss/mimetypes.scss',
'web/static/src/legacy/scss/datepicker.scss', 'web/static/src/scss/ui.scss',
'web/static/src/legacy/scss/daterangepicker.scss', 'web/static/src/views/fields/translation_dialog.scss',
'web/static/src/legacy/scss/banner.scss', 'web/static/src/views/fields/signature/signature_field.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/scss/base_frontend.scss', 'web/static/src/legacy/scss/ui.scss',
'web/static/src/legacy/scss/lazyloader.scss',
('include', 'web.assets_frontend_minimal'), ('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/owl.js',
'web/static/lib/owl/odoo_module.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/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/popper/popper.js',
'web/static/lib/bootstrap/js/dist/util/index.js',
'web/static/lib/bootstrap/js/dist/dom/data.js', '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/event-handler.js',
'web/static/lib/bootstrap/js/dist/dom/manipulator.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/dom/selector-engine.js',
'web/static/lib/bootstrap/js/dist/util/config.js',
'web/static/lib/bootstrap/js/dist/util/component-functions.js',
'web/static/lib/bootstrap/js/dist/util/backdrop.js',
'web/static/lib/bootstrap/js/dist/util/focustrap.js',
'web/static/lib/bootstrap/js/dist/util/sanitizer.js',
'web/static/lib/bootstrap/js/dist/util/scrollbar.js',
'web/static/lib/bootstrap/js/dist/util/swipe.js',
'web/static/lib/bootstrap/js/dist/util/template-factory.js',
'web/static/lib/bootstrap/js/dist/base-component.js', 'web/static/lib/bootstrap/js/dist/base-component.js',
'web/static/lib/bootstrap/js/dist/alert.js', 'web/static/lib/bootstrap/js/dist/alert.js',
'web/static/lib/bootstrap/js/dist/button.js', 'web/static/lib/bootstrap/js/dist/button.js',
@ -386,152 +222,133 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/bootstrap/js/dist/scrollspy.js', 'web/static/lib/bootstrap/js/dist/scrollspy.js',
'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/tab.js',
'web/static/lib/bootstrap/js/dist/toast.js', 'web/static/lib/bootstrap/js/dist/toast.js',
'web/static/lib/tempusdominus/tempusdominus.js', 'web/static/src/libs/bootstrap.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/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/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/env.js',
'web/static/src/core/utils/transitions.scss', # included early because used by other files 'web/static/src/core/utils/transitions.scss', # included early because used by other files
'web/static/src/core/**/*', 'web/static/src/core/**/*', # Note that 'web/static/src/core/utils/ui.js' is included in assets_frontend_minimal already
('remove', 'web/static/src/core/commands/**/*'), ('remove', 'web/static/src/core/commands/**/*'),
('remove', 'web/static/src/core/debug/debug_menu.js'), ('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/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.js',
'web/static/src/legacy/js/public/public_root_instance.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/public/public_widget.js',
'web/static/src/legacy/legacy_promise_error_handler.js', 'web/static/src/legacy/js/public/signin.js',
'web/static/src/legacy/legacy_rpc_error_handler.js',
'web/static/src/legacy/js/fields/field_utils.js',
('include', 'web.frontend_legacy'),
], ],
'web.assets_frontend_lazy': [ 'web.assets_frontend_lazy': [
('include', 'web.assets_frontend'), ('include', 'web.assets_frontend'),
# Remove assets_frontend_minimal # Remove assets_frontend_minimal
('remove', 'web/static/src/legacy/js/promise_extension.js'), ('remove', 'web/static/src/module_loader.js'),
('remove', 'web/static/src/boot.js'),
('remove', 'web/static/src/session.js'), ('remove', 'web/static/src/session.js'),
('remove', 'web/static/src/legacy/js/core/cookie_utils.js'), ('remove', 'web/static/src/core/browser/cookie.js'),
('remove', 'web/static/src/legacy/js/core/menu.js'), ('remove', 'web/static/src/core/utils/ui.js'),
('remove', 'web/static/src/legacy/js/core/minimal_dom.js'), ('remove', 'web/static/src/legacy/js/public/minimal_dom.js'),
('remove', 'web/static/src/legacy/js/public/lazyloader.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.
# Bundlized, the size is reduced to 5300≈ lines.
'web.pdf_js_lib': [
'web/static/lib/pdfjs/build/pdf.js',
'web/static/lib/pdfjs/build/pdf.worker.js',
],
'web.report_assets_common': [ 'web.report_assets_common': [
('include', 'web._assets_helpers'), # Include some helpers early to define $enable-rfs before
# _mixin file and use primary_variables in bs_overridden_report
'web/static/src/scss/functions.scss',
'web/static/src/scss/utils.scss',
('include', 'web._assets_primary_variables'),
('include', 'web._assets_secondary_variables'),
'web/static/src/webclient/actions/reports/bootstrap_overridden_report.scss', 'web/static/src/webclient/actions/reports/bootstrap_overridden_report.scss',
('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss', 'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss', 'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap'), ('include', 'web._assets_bootstrap_backend'),
('remove', 'web/static/src/scss/utilities_custom_backend.scss'),
('remove', 'web/static/src/scss/bootstrap_review_backend.scss'),
('after', 'web/static/src/scss/utilities_custom.scss', 'web/static/src/webclient/actions/reports/utilities_custom_report.scss'),
'web/static/lib/popper/popper.js',
'web/static/lib/bootstrap/js/dist/util/index.js',
'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/util/config.js',
'web/static/lib/bootstrap/js/dist/util/component-functions.js',
'web/static/lib/bootstrap/js/dist/util/backdrop.js',
'web/static/lib/bootstrap/js/dist/util/focustrap.js',
'web/static/lib/bootstrap/js/dist/util/sanitizer.js',
'web/static/lib/bootstrap/js/dist/util/scrollbar.js',
'web/static/lib/bootstrap/js/dist/util/swipe.js',
'web/static/lib/bootstrap/js/dist/util/template-factory.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', 'base/static/src/css/description.css',
'web/static/src/libs/fontawesome/css/font-awesome.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/lib/odoo_ui_icons/*',
'web/static/fonts/fonts.scss', 'web/static/fonts/fonts.scss',
'web/static/src/webclient/actions/reports/bootstrap_review_report.scss',
'web/static/src/webclient/actions/reports/report.scss', 'web/static/src/webclient/actions/reports/report.scss',
'web/static/src/webclient/actions/reports/layout_assets/layout_standard.scss', 'web/static/src/webclient/actions/reports/report_tables.scss',
'web/static/src/webclient/actions/reports/layout_assets/layout_background.scss', 'web/static/src/webclient/actions/reports/layout_assets/layout_*.scss',
'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/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.report_assets_pdf': [
'web/static/src/webclient/actions/reports/reset.min.css', 'web/static/src/webclient/actions/reports/reset.min.css',
], ],
'web.ace_lib': [
"web/static/lib/ace/ace.js",
"web/static/lib/ace/mode-javascript.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",
],
# ---------------------------------------------------------------------
# "DIRECT PRINT" BUNDLE
# ---------------------------------------------------------------------
"web.assets_web_print": [
'web/static/src/scss/functions.scss',
'web/static/src/scss/primary_variables_print.scss',
'web/static/src/**/*.print_variables.scss',
('include', 'web.assets_backend'),
],
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# COLOR SCHEME BUNDLES # COLOR SCHEME BUNDLES
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
"web.dark_mode_assets_common": [ "web.assets_web_dark": [
('include', 'web.assets_common'), ('include', 'web.assets_web'),
],
"web.dark_mode_assets_backend": [
('include', 'web.assets_backend'),
'web/static/src/**/*.dark.scss', '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 # SUB BUNDLES
@ -542,14 +359,25 @@ This module provides the core of the Odoo Web Client.
# Their naming conventions are similar to those of the main bundles, # Their naming conventions are similar to those of the main bundles,
# with the addition of a prefixed underscore to reflect the "private" # with the addition of a prefixed underscore to reflect the "private"
# aspect. # 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._assets_primary_variables': [
'web/static/src/scss/primary_variables.scss', 'web/static/src/scss/primary_variables.scss',
'web/static/src/**/**/*.variables.scss', 'web/static/src/**/*.variables.scss',
'base/static/src/scss/onboarding.variables.scss',
], ],
'web._assets_secondary_variables': [ 'web._assets_secondary_variables': [
'web/static/src/scss/secondary_variables.scss', 'web/static/src/scss/secondary_variables.scss',
@ -557,20 +385,33 @@ This module provides the core of the Odoo Web Client.
'web._assets_helpers': [ 'web._assets_helpers': [
'web/static/lib/bootstrap/scss/_functions.scss', 'web/static/lib/bootstrap/scss/_functions.scss',
'web/static/lib/bootstrap/scss/_mixins.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/mixins_forwardport.scss',
'web/static/src/scss/bs_mixins_overrides.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_primary_variables'),
('include', 'web._assets_secondary_variables'), ('include', 'web._assets_secondary_variables'),
], ],
'web._assets_jquery': [
'web/static/lib/jquery/jquery.js',
'web/static/src/legacy/js/libs/jquery.js',
],
'web._assets_bootstrap': [ 'web._assets_bootstrap': [
'web/static/src/scss/import_bootstrap.scss', 'web/static/src/scss/import_bootstrap.scss',
'web/static/src/scss/helpers_backport.scss',
'web/static/src/scss/utilities_custom.scss', 'web/static/src/scss/utilities_custom.scss',
'web/static/lib/bootstrap/scss/utilities/_api.scss', 'web/static/lib/bootstrap/scss/utilities/_api.scss',
'web/static/src/scss/bootstrap_review.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._assets_backend_helpers': [
'web/static/src/scss/bootstrap_overridden.scss', 'web/static/src/scss/bootstrap_overridden.scss',
'web/static/src/scss/bs_mixins_overrides_backend.scss', 'web/static/src/scss/bs_mixins_overrides_backend.scss',
@ -579,117 +420,134 @@ This module provides the core of the Odoo Web Client.
'web/static/src/scss/bootstrap_overridden_frontend.scss', '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 # TESTS BUNDLES
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
'web.assets_tests': [ 'web.assets_tests': [
# No tours are defined in web, but the bundle "assets_tests" is # the bundle "assets_tests" is first called in web.
# first called in web. 'web/static/tests/legacy/helpers/cleanup.js',
'web/static/tests/legacy/helpers/test_utils_file.js', 'web/static/tests/legacy/helpers/utils.js',
'web/static/tests/helpers/cleanup.js', 'web/static/tests/legacy/utils.js',
'web/static/tests/helpers/utils.js', 'web/static/tests/tours/**/*'
'web/static/tests/utils.js',
], ],
# remove this bundle alongside the owl2 compatibility layer 'web.__assets_tests_call__': [
'web.tests_assets_common': [ 'web/static/tests/legacy/ignore_missing_deps_start.js',
('include', 'web.assets_common'), ('include', 'web.assets_tests'),
('after', 'web/static/src/owl2_compatibility/app.js', 'web/static/tests/owl2_compatibility_app.js'), 'web/static/tests/legacy/ignore_missing_deps_stop.js',
],
# Assets for test framework and setup
'web.assets_unit_tests_setup': [
'web/static/src/module_loader.js',
'web/static/lib/owl/owl.js',
'web/static/lib/owl/odoo_module.js',
'web/static/lib/hoot/**/*',
'web/static/lib/hoot-dom/**/*',
('remove', 'web/static/lib/hoot/ui/hoot_style.css'),
('remove', 'web/static/lib/hoot/tests/**/*'),
# Applied here to allow libs above to be loaded normally
'web/static/tests/_framework/hoot_module_loader.js',
# Assets for features to test (views, services, fields, ...)
# Typically includes most files in 'web.web.assets_backend'
('include', 'web.assets_backend'),
('include', 'web.assets_backend_lazy'),
'web/static/src/public/public_component_service.js',
'web/static/src/webclient/clickbot/clickbot.js',
],
# Lazy-loaded assets needed by test framework when not in headless mode
'web.assets_unit_tests_setup_ui': [
"web/static/lib/diff_match_patch/diff_match_patch.js",
"web/static/lib/prismjs/prism.js",
],
# Unit test files
'web.assets_unit_tests': [
'web/static/tests/**/*',
('remove', 'web/static/tests/tours/**/*'),
('remove', 'web/static/tests/legacy/**/*'), # to remove when all legacy tests are ported
], ],
'web.tests_assets': [ 'web.tests_assets': [
('include', 'web.assets_backend'),
('include', 'web.assets_backend_lazy'),
'web/static/src/public/public_component_service.js',
'web/static/tests/legacy/patch_translations.js',
'web/static/lib/qunit/qunit-2.9.1.css', 'web/static/lib/qunit/qunit-2.9.1.css',
'web/static/lib/qunit/qunit-2.9.1.js', 'web/static/lib/qunit/qunit-2.9.1.js',
'web/static/tests/legacy/helpers/**/*', 'web/static/tests/legacy/legacy_tests/helpers/**/*',
('remove', 'web/static/tests/legacy/helpers/test_utils_tests.js'), ('remove', 'web/static/tests/legacy/legacy_tests/helpers/test_utils_tests.js'),
'web/static/tests/legacy/legacy_setup.js',
'web/static/lib/fullcalendar/core/main.css', ('include', 'web._assets_jquery'),
'web/static/lib/fullcalendar/daygrid/main.css',
'web/static/lib/fullcalendar/timegrid/main.css', 'web/static/lib/fullcalendar/core/index.global.js',
'web/static/lib/fullcalendar/list/main.css', 'web/static/lib/fullcalendar/interaction/index.global.js',
'web/static/lib/fullcalendar/core/main.js', 'web/static/lib/fullcalendar/daygrid/index.global.js',
'web/static/lib/fullcalendar/moment/main.js', 'web/static/lib/fullcalendar/timegrid/index.global.js',
'web/static/lib/fullcalendar/interaction/main.js', 'web/static/lib/fullcalendar/list/index.global.js',
'web/static/lib/fullcalendar/daygrid/main.js', 'web/static/lib/fullcalendar/luxon3/index.global.js',
'web/static/lib/fullcalendar/timegrid/main.js',
'web/static/lib/fullcalendar/list/main.js',
'web/static/lib/fullcalendar/luxon/main.js',
'web/static/lib/zxing-library/zxing-library.js', 'web/static/lib/zxing-library/zxing-library.js',
'web/static/lib/ace/ace.js', 'web/static/lib/ace/ace.js',
'web/static/lib/ace/javascript_highlight_rules.js',
'web/static/lib/ace/mode-python.js', 'web/static/lib/ace/mode-python.js',
'web/static/lib/ace/mode-xml.js', 'web/static/lib/ace/mode-xml.js',
'web/static/lib/ace/mode-js.js', 'web/static/lib/ace/mode-javascript.js',
'web/static/lib/ace/mode-qweb.js', 'web/static/lib/ace/mode-qweb.js',
'web/static/lib/nearest/jquery.nearest.js', 'web/static/lib/ace/theme-monokai.js',
'web/static/lib/daterangepicker/daterangepicker.js',
'web/static/src/legacy/js/libs/daterangepicker.js',
'web/static/lib/stacktracejs/stacktrace.js', 'web/static/lib/stacktracejs/stacktrace.js',
'web/static/lib/Chart/Chart.js', ('include', "web.chartjs_lib"),
'web/static/lib/signature_pad/signature_pad.umd.js',
'/web/static/lib/daterangepicker/daterangepicker.js', 'web/static/tests/legacy/helpers/**/*.js',
'web/static/tests/legacy/views/helpers.js',
# 'web/static/tests/legacy/main_tests.js', 'web/static/tests/legacy/search/helpers.js',
'web/static/tests/helpers/**/*.js', 'web/static/tests/legacy/views/calendar/helpers.js',
'web/static/tests/utils.js', 'web/static/tests/legacy/webclient/**/helpers.js',
'web/static/tests/views/helpers.js', 'web/static/tests/legacy/qunit.js',
'web/static/tests/search/helpers.js', 'web/static/tests/legacy/main.js',
'web/static/tests/views/calendar/helpers.js', 'web/static/tests/legacy/mock_server_tests.js',
'web/static/tests/webclient/**/helpers.js', 'web/static/tests/legacy/setup.js',
'web/static/tests/qunit.js', 'web/static/tests/legacy/utils.js',
'web/static/tests/main.js', 'web/static/src/webclient/clickbot/clickbot.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.qunit_suite_tests': [ 'web.qunit_suite_tests': [
'web/static/tests/env_tests.js', 'web/static/tests/legacy/core/**/*.js',
'web/static/tests/reactivity_tests.js', 'web/static/tests/legacy/search/**/*.js',
'web/static/tests/core/**/*.js', ('remove', 'web/static/tests/legacy/search/helpers.js'),
'web/static/tests/l10n/**/*.js', 'web/static/tests/legacy/views/**/*.js',
'web/static/tests/search/**/*.js', ('remove', 'web/static/tests/legacy/views/helpers.js'),
('remove', 'web/static/tests/search/helpers.js'), ('remove', 'web/static/tests/legacy/views/calendar/helpers.js'),
'web/static/tests/views/**/*.js', 'web/static/tests/legacy/webclient/**/*.js',
('remove', 'web/static/tests/views/helpers.js'), ('remove', 'web/static/tests/legacy/webclient/**/helpers.js'),
('remove', 'web/static/tests/views/calendar/helpers.js'), 'web/static/tests/legacy/public/**/*.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'),
('include', 'web.frontend_legacy_tests'), # Legacy
'web/static/tests/legacy/legacy_tests/**/*.js',
('remove', 'web/static/tests/legacy/legacy_tests/helpers/**/*.js'),
], ],
'web.qunit_mobile_suite_tests': [ 'web.qunit_mobile_suite_tests': [
'web/static/tests/mobile/**/*.js', 'web/static/tests/legacy/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',
], ],
'web.assets_clickbot': [
# Used during the transition of the web architecture 'web/static/src/webclient/clickbot/clickbot.js',
'web.frontend_legacy_tests': [ ],
'web/static/tests/legacy/frontend/*.js', "web.chartjs_lib" : [
'/web/static/lib/Chart/Chart.js',
'/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
],
"web.fullcalendar_lib" : [
'/web/static/lib/fullcalendar/core/index.global.js',
'/web/static/lib/fullcalendar/core/locales-all.global.js',
'/web/static/lib/fullcalendar/interaction/index.global.js',
'/web/static/lib/fullcalendar/daygrid/index.global.js',
'/web/static/lib/fullcalendar/luxon3/index.global.js',
'/web/static/lib/fullcalendar/timegrid/index.global.js',
'/web/static/lib/fullcalendar/list/index.global.js',
], ],
}, },
'bootstrap': True, # load translations for login screen, 'bootstrap': True, # load translations for login screen,

View file

@ -6,12 +6,28 @@ from . import database
from . import dataset from . import dataset
from . import domain from . import domain
from . import export from . import export
from . import json
from . import home from . import home
from . import model
from . import pivot from . import pivot
from . import profiling from . import profiling
from . import report from . import report
from . import session from . import session
from . import vcard
from . import view from . import view
from . import webclient from . import webclient
from . import webmanifest
from . import main # deprecated
def __getattr__(attr):
if attr != 'main':
raise AttributeError(f"Module {__name__!r} has not attribute {attr!r}.")
import sys # noqa: PLC0415
mod = __name__ + '.main'
if main := sys.modules.get(mod):
return main
# can't use relative import as that triggers a getattr first
import odoo.addons.web.controllers.main as main # noqa: PLC0415
return main

View file

@ -1,43 +1,111 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging import logging
from odoo import _
from odoo.exceptions import UserError, MissingError, AccessError
from odoo.http import Controller, request, route from odoo.http import Controller, request, route
from .utils import clean_action from .utils import clean_action
from werkzeug.exceptions import BadRequest
_logger = logging.getLogger(__name__) class MissingActionError(UserError):
"""Missing Action.
.. admonition:: Example
When you try to read on a non existing record.
"""
class Action(Controller): class Action(Controller):
@route('/web/action/load', type='json', auth="user") @route('/web/action/load', type='json', auth='user', readonly=True)
def load(self, action_id, additional_context=None): def load(self, action_id, context=None):
if context:
request.update_context(**context)
Actions = request.env['ir.actions.actions'] Actions = request.env['ir.actions.actions']
value = False
try: try:
action_id = int(action_id) action_id = int(action_id)
except ValueError: except ValueError:
try: try:
if '.' in action_id:
action = request.env.ref(action_id) action = request.env.ref(action_id)
assert action._name.startswith('ir.actions.') assert action._name.startswith('ir.actions.')
else:
action = Actions.sudo().search([('path', '=', action_id)], limit=1)
assert action
action_id = action.id action_id = action.id
except Exception: except Exception as exc:
action_id = 0 # force failed read raise MissingActionError(_("The action “%s” does not exist.", action_id)) from exc
base_action = Actions.browse([action_id]).sudo().read(['type']) base_action = Actions.browse([action_id]).sudo().read(['type'])
if base_action: if not base_action:
raise MissingActionError(_("The action “%s” does not exist", action_id))
action_type = base_action[0]['type'] action_type = base_action[0]['type']
if action_type == 'ir.actions.report': if action_type == 'ir.actions.report':
request.update_context(bin_size=True) request.update_context(bin_size=True)
if additional_context: if action_type == 'ir.actions.act_window':
request.update_context(**additional_context) result = request.env[action_type].sudo().browse([action_id])._get_action_dict()
action = request.env[action_type].sudo().browse([action_id]).read() return clean_action(result, env=request.env) if result else False
if action: result = request.env[action_type].sudo().browse([action_id]).read()
value = clean_action(action[0], env=request.env) return clean_action(result[0], env=request.env) if result else False
return value
@route('/web/action/run', type='json', auth="user") @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]) action = request.env['ir.actions.server'].browse([action_id])
result = action.run() result = action.run()
return clean_action(result, env=action.env) if result else False return clean_action(result, env=action.env) if result else False
@route('/web/action/load_breadcrumbs', type='json', auth='user', readonly=True)
def load_breadcrumbs(self, actions):
results = []
for idx, action in enumerate(actions):
record_id = action.get('resId')
try:
if action.get('action'):
act = self.load(action.get('action'))
if act['type'] == 'ir.actions.server':
if act['path']:
act = request.env['ir.actions.server'].browse(act['id']).run()
else:
results.append({'error': 'A server action must have a path to be restored'})
continue
if not act.get('display_name'):
act['display_name'] = act['name']
# client actions don't have multi-record views, so we can't go further to the next controller
if act['type'] == 'ir.actions.client' and idx + 1 < len(actions) and action.get('action') == actions[idx + 1].get('action'):
results.append({'error': 'Client actions don\'t have multi-record views'})
continue
if record_id:
# some actions may not have a res_model (e.g. a client action)
if record_id == 'new':
results.append({'display_name': _("New")})
elif act['res_model']:
results.append({'display_name': request.env[act['res_model']].browse(record_id).display_name})
else:
results.append({'display_name': act['display_name']})
else:
if act.get('res_model') and act['type'] != 'ir.actions.client':
request.env[act['res_model']].check_access('read')
# action shouldn't be available on its own if it doesn't have multi-record views
name = act['display_name'] if any(view[1] != 'form' and view[1] != 'search' for view in act['views']) else None
else:
name = act['display_name']
results.append({'display_name': name})
elif action.get('model'):
Model = request.env[action.get('model')]
if record_id:
if record_id == 'new':
results.append({'display_name': _("New")})
else:
results.append({'display_name': Model.browse(record_id).display_name})
else:
# This case cannot be produced by the web client
raise BadRequest('Actions with a model should also have a resId')
else:
raise BadRequest('Actions should have either an action (id or path) or a model')
except (MissingActionError, MissingError, AccessError) as exc:
results.append({'error': str(exc)})
return results

View file

@ -8,6 +8,7 @@ import logging
import os import os
import unicodedata import unicodedata
from contextlib import nullcontext
try: try:
from werkzeug.utils import send_file from werkzeug.utils import send_file
except ImportError: except ImportError:
@ -15,14 +16,13 @@ except ImportError:
import odoo import odoo
import odoo.modules.registry import odoo.modules.registry
from odoo import http, _ from odoo import SUPERUSER_ID, _, http, api
from odoo.addons.base.models.assetsbundle import ANY_UNIQUE
from odoo.exceptions import AccessError, UserError from odoo.exceptions import AccessError, UserError
from odoo.http import request, Response 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 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.image import image_guess_size_from_field_name
from odoo.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -59,19 +59,21 @@ class Binary(http.Controller):
)) ))
raise http.request.not_found() raise http.request.not_found()
@http.route(['/web/content', @http.route([
'/web/content',
'/web/content/<string:xmlid>', '/web/content/<string:xmlid>',
'/web/content/<string:xmlid>/<string:filename>', '/web/content/<string:xmlid>/<string:filename>',
'/web/content/<int:id>', '/web/content/<int:id>',
'/web/content/<int:id>/<string:filename>', '/web/content/<int:id>/<string:filename>',
'/web/content/<string:model>/<int:id>/<string:field>', '/web/content/<string:model>/<int:id>/<string:field>',
'/web/content/<string:model>/<int:id>/<string:field>/<string:filename>'], type='http', auth="public") '/web/content/<string:model>/<int:id>/<string:field>/<string:filename>',
], type='http', auth='public', readonly=True)
# pylint: disable=redefined-builtin,invalid-name # pylint: disable=redefined-builtin,invalid-name
def content_common(self, xmlid=None, model='ir.attachment', id=None, field='raw', def content_common(self, xmlid=None, model='ir.attachment', id=None, field='raw',
filename=None, filename_field='name', mimetype=None, unique=False, filename=None, filename_field='name', mimetype=None, unique=False,
download=False, access_token=None, nocache=False): download=False, access_token=None, nocache=False):
with replace_exceptions(UserError, by=request.not_found()): with replace_exceptions(UserError, by=request.not_found()):
record = request.env['ir.binary']._find_record(xmlid, model, id and int(id), access_token) record = request.env['ir.binary']._find_record(xmlid, model, id and int(id), access_token, field=field)
stream = request.env['ir.binary']._get_stream_from(record, field, filename, filename_field, mimetype) stream = request.env['ir.binary']._get_stream_from(record, field, filename, filename_field, mimetype)
if request.httprequest.args.get('access_token'): if request.httprequest.args.get('access_token'):
stream.public = True stream.public = True
@ -85,33 +87,68 @@ class Binary(http.Controller):
return stream.get_response(**send_file_kwargs) return stream.get_response(**send_file_kwargs)
@http.route(['/web/assets/debug/<string:filename>', @http.route([
'/web/assets/debug/<path:extra>/<string:filename>', '/web/assets/<string:unique>/<string:filename>'], type='http', auth="public", readonly=True)
'/web/assets/<int:id>/<string:filename>', def content_assets(self, filename=None, unique=ANY_UNIQUE, nocache=False, assets_params=None):
'/web/assets/<int:id>-<string:unique>/<string:filename>', env = request.env # readonly
'/web/assets/<int:id>-<string:unique>/<path:extra>/<string:filename>'], type='http', auth="public") assets_params = assets_params or {}
# pylint: disable=redefined-builtin,invalid-name assert isinstance(assets_params, dict)
def content_assets(self, id=None, filename=None, unique=False, extra=None, nocache=False): debug_assets = unique == 'debug'
if not id: if unique in ('any', '%'):
domain = [('url', '!=', False), ('res_model', '=', 'ir.ui.view'), unique = ANY_UNIQUE
('res_id', '=', 0), ('create_uid', '=', odoo.SUPERUSER_ID)] attachment = None
if extra: if unique != 'debug':
domain += [('url', '=like', f'/web/assets/%/{extra}/{filename}')] url = env['ir.asset']._get_asset_bundle_url(filename, unique, assets_params)
else: assert not '%' in url
domain += [ domain = [
('url', '=like', f'/web/assets/%/{filename}'), ('public', '=', True),
('url', 'not like', f'/web/assets/%/%/{filename}') ('url', '!=', False),
('url', '=like', url),
('res_model', '=', 'ir.ui.view'),
('res_id', '=', 0),
('create_uid', '=', SUPERUSER_ID),
] ]
attachments = request.env['ir.attachment'].sudo().search_read(domain, fields=['id'], limit=1) attachment = env['ir.attachment'].sudo().search(domain, limit=1)
if not attachments: if not attachment:
# try to generate one
if env.cr.readonly:
env.cr.rollback() # reset state to detect newly generated assets
cursor_manager = env.registry.cursor(readonly=False)
else:
# if we don't have a replica, the cursor is not readonly, use the same one to avoid a rollback
cursor_manager = nullcontext(env.cr)
with cursor_manager as rw_cr:
rw_env = api.Environment(rw_cr, env.user.id, {})
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() raise request.not_found()
id = attachments[0]['id'] bundle_name, rtl, asset_type = rw_env['ir.asset']._parse_bundle_name(filename, debug_assets)
with replace_exceptions(UserError, by=request.not_found()): css = asset_type == 'css'
record = request.env['ir.binary']._find_record(res_id=int(id)) js = asset_type == 'js'
stream = request.env['ir.binary']._get_stream_from(record, 'raw', filename) bundle = rw_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 = env['ir.attachment'].sudo().browse(bundle.css().id)
elif js and bundle.javascripts:
attachment = env['ir.attachment'].sudo().browse(bundle.js().id)
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 = env['ir.binary']._get_stream_from(attachment, 'raw', filename)
send_file_kwargs = {'as_attachment': False, 'content_security_policy': None} send_file_kwargs = {'as_attachment': False, 'content_security_policy': None}
if unique: if unique and unique != 'debug':
send_file_kwargs['immutable'] = True send_file_kwargs['immutable'] = True
send_file_kwargs['max_age'] = http.STATIC_CACHE_LONG send_file_kwargs['max_age'] = http.STATIC_CACHE_LONG
if nocache: if nocache:
@ -119,7 +156,8 @@ class Binary(http.Controller):
return stream.get_response(**send_file_kwargs) return stream.get_response(**send_file_kwargs)
@http.route(['/web/image', @http.route([
'/web/image',
'/web/image/<string:xmlid>', '/web/image/<string:xmlid>',
'/web/image/<string:xmlid>/<string:filename>', '/web/image/<string:xmlid>/<string:filename>',
'/web/image/<string:xmlid>/<int:width>x<int:height>', '/web/image/<string:xmlid>/<int:width>x<int:height>',
@ -135,14 +173,15 @@ class Binary(http.Controller):
'/web/image/<int:id>-<string:unique>', '/web/image/<int:id>-<string:unique>',
'/web/image/<int:id>-<string:unique>/<string:filename>', '/web/image/<int:id>-<string:unique>/<string:filename>',
'/web/image/<int:id>-<string:unique>/<int:width>x<int:height>', '/web/image/<int:id>-<string:unique>/<int:width>x<int:height>',
'/web/image/<int:id>-<string:unique>/<int:width>x<int:height>/<string:filename>'], type='http', auth="public") '/web/image/<int:id>-<string:unique>/<int:width>x<int:height>/<string:filename>',
], type='http', auth='public', readonly=True)
# pylint: disable=redefined-builtin,invalid-name # pylint: disable=redefined-builtin,invalid-name
def content_image(self, xmlid=None, model='ir.attachment', id=None, field='raw', def content_image(self, xmlid=None, model='ir.attachment', id=None, field='raw',
filename_field='name', filename=None, mimetype=None, unique=False, filename_field='name', filename=None, mimetype=None, unique=False,
download=False, width=0, height=0, crop=False, access_token=None, download=False, width=0, height=0, crop=False, access_token=None,
nocache=False): nocache=False):
try: try:
record = request.env['ir.binary']._find_record(xmlid, model, id and int(id), access_token) record = request.env['ir.binary']._find_record(xmlid, model, id and int(id), access_token, field=field)
stream = request.env['ir.binary']._get_image_stream_from( stream = request.env['ir.binary']._get_image_stream_from(
record, field, filename=filename, filename_field=filename_field, record, field, filename=filename, filename_field=filename_field,
mimetype=mimetype, width=int(width), height=int(height), crop=crop, mimetype=mimetype, width=int(width), height=int(height), crop=crop,
@ -190,7 +229,7 @@ class Binary(http.Controller):
try: try:
attachment = Model.create({ attachment = Model.create({
'name': filename, 'name': filename,
'datas': base64.encodebytes(ufile.read()), 'raw': ufile.read(),
'res_model': model, 'res_model': model,
'res_id': int(id) 'res_id': int(id)
}) })
@ -203,7 +242,7 @@ class Binary(http.Controller):
else: else:
args.append({ args.append({
'filename': clean(filename), 'filename': clean(filename),
'mimetype': ufile.content_type, 'mimetype': attachment.mimetype,
'id': attachment.id, 'id': attachment.id,
'size': attachment.file_size 'size': attachment.file_size
}) })
@ -217,31 +256,29 @@ class Binary(http.Controller):
def company_logo(self, dbname=None, **kw): def company_logo(self, dbname=None, **kw):
imgname = 'logo' imgname = 'logo'
imgext = '.png' imgext = '.png'
placeholder = functools.partial(get_resource_path, 'web', 'static', 'img')
dbname = request.db dbname = request.db
uid = (request.session.uid if dbname else None) or odoo.SUPERUSER_ID uid = (request.session.uid if dbname else None) or odoo.SUPERUSER_ID
if not dbname: 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: else:
try: try:
# create an empty registry
registry = odoo.modules.registry.Registry(dbname)
with registry.cursor() as cr:
company = int(kw['company']) if kw and kw.get('company') else False company = int(kw['company']) if kw and kw.get('company') else False
if company: if company:
cr.execute("""SELECT logo_web, write_date request.env.cr.execute("""
SELECT logo_web, write_date
FROM res_company FROM res_company
WHERE id = %s WHERE id = %s
""", (company,)) """, (company,))
else: else:
cr.execute("""SELECT c.logo_web, c.write_date request.env.cr.execute("""
SELECT c.logo_web, c.write_date
FROM res_users u FROM res_users u
LEFT JOIN res_company c LEFT JOIN res_company c
ON c.id = u.company_id ON c.id = u.company_id
WHERE u.id = %s WHERE u.id = %s
""", (uid,)) """, (uid,))
row = cr.fetchone() row = request.env.cr.fetchone()
if row and row[0]: if row and row[0]:
image_base64 = base64.b64decode(row[0]) image_base64 = base64.b64decode(row[0])
image_data = io.BytesIO(image_base64) image_data = io.BytesIO(image_base64)
@ -258,13 +295,17 @@ class Binary(http.Controller):
response_class=Response, response_class=Response,
) )
else: 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: except Exception:
response = http.Stream.from_path(placeholder(imgname + imgext)).get_response() _logger.warning("While retrieving the company logo, using the Odoo logo instead", exc_info=True)
response = http.Stream.from_path(file_path(f'web/static/img/{imgname}{imgext}')).get_response()
return response return response
@http.route(['/web/sign/get_fonts', '/web/sign/get_fonts/<string:fontname>'], type='json', auth='public') @http.route([
'/web/sign/get_fonts',
'/web/sign/get_fonts/<string:fontname>',
], type='json', auth='none')
def get_fonts(self, fontname=None): def get_fonts(self, fontname=None):
"""This route will return a list of base64 encoded fonts. """This route will return a list of base64 encoded fonts.
@ -276,7 +317,7 @@ class Binary(http.Controller):
""" """
supported_exts = ('.ttf', '.otf', '.woff', '.woff2') supported_exts = ('.ttf', '.otf', '.woff', '.woff2')
fonts = [] fonts = []
fonts_directory = file_path(os.path.join('web', 'static', 'fonts', 'sign')) fonts_directory = file_path('web/static/fonts/sign')
if fontname: if fontname:
font_path = os.path.join(fonts_directory, fontname) font_path = os.path.join(fonts_directory, fontname)
with file_open(font_path, 'rb', filter_ext=supported_exts) as font_file: with file_open(font_path, 'rb', filter_ext=supported_exts) as font_file:

View file

@ -75,13 +75,14 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd]) dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd])
try: try:
if not re.match(DBNAME_PATTERN, name): 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 could be = "False" which is actually True in python
country_code = post.get('country_code') or False 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']]) dispatch_rpc('db', 'create_database', [master_pwd, name, bool(post.get('demo')), lang, password, post['login'], country_code, post['phone']])
request.session.authenticate(name, post['login'], password) credential = {'login': post['login'], 'password': password, 'type': 'password'}
request.session.authenticate(name, credential)
request.session.db = name request.session.db = name
return request.redirect('/web') return request.redirect('/odoo')
except Exception as e: except Exception as e:
_logger.exception("Database creation error.") _logger.exception("Database creation error.")
error = "Database creation error: %s" % (str(e) or repr(e)) error = "Database creation error: %s" % (str(e) or repr(e))
@ -94,7 +95,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd]) dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd])
try: try:
if not re.match(DBNAME_PATTERN, new_name): 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]) dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name, neutralize_database])
if request.db == name: if request.db == name:
request.env.cr.close() # duplicating a database leads to an unusable cursor request.env.cr.close() # duplicating a database leads to an unusable cursor
@ -126,6 +127,8 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd]) dispatch_rpc('db', 'change_admin_password', ["admin", master_pwd])
try: try:
odoo.service.db.check_super(master_pwd) odoo.service.db.check_super(master_pwd)
if name not in http.db_list():
raise Exception("Database %r is not known" % name)
ts = datetime.datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") ts = datetime.datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S")
filename = "%s_%s.%s" % (name, ts, backup_format) filename = "%s_%s.%s" % (name, ts, backup_format)
headers = [ headers = [
@ -140,7 +143,7 @@ class Database(http.Controller):
error = "Database backup error: %s" % (str(e) or repr(e)) error = "Database backup error: %s" % (str(e) or repr(e))
return self._render_template(error=error) 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): def restore(self, master_pwd, backup_file, name, copy=False, neutralize_database=False):
insecure = odoo.tools.config.verify_admin_password('admin') insecure = odoo.tools.config.verify_admin_password('admin')
if insecure and master_pwd: if insecure and master_pwd:

View file

@ -2,6 +2,7 @@
import logging import logging
import warnings import warnings
from werkzeug.exceptions import NotFound
from odoo import http from odoo import http
from odoo.api import call_kw from odoo.api import call_kw
@ -15,42 +16,36 @@ _logger = logging.getLogger(__name__)
class DataSet(http.Controller): class DataSet(http.Controller):
@http.route('/web/dataset/search_read', type='json', auth="user") def _call_kw_readonly(self):
def search_read(self, model, fields=False, offset=0, limit=False, domain=None, sort=None): params = request.get_json_data()['params']
return request.env[model].web_search_read(domain, fields, offset=offset, limit=limit, order=sort) try:
model_class = request.registry[params['model']]
except KeyError as e:
raise NotFound() from e
method_name = params['method']
for cls in model_class.mro():
method = getattr(cls, method_name, None)
if method is not None and hasattr(method, '_readonly'):
return method._readonly
return False
@http.route('/web/dataset/load', type='json', auth="user") @http.route(['/web/dataset/call_kw', '/web/dataset/call_kw/<path:path>'], type='json', auth="user", readonly=_call_kw_readonly)
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): def call_kw(self, model, method, args, kwargs, path=None):
return self._call_kw(model, method, args, kwargs) Model = request.env[model]
get_public_method(Model, method)
return call_kw(request.env[model], method, args, kwargs)
@http.route('/web/dataset/call_button', type='json', auth="user") @http.route(['/web/dataset/call_button', '/web/dataset/call_button/<path:path>'], type='json', auth="user", readonly=_call_kw_readonly)
def call_button(self, model, method, args, kwargs): def call_button(self, model, method, args, kwargs, path=None):
action = self._call_kw(model, method, args, kwargs) Model = request.env[model]
get_public_method(Model, method)
action = call_kw(request.env[model], method, args, kwargs)
if isinstance(action, dict) and action.get('type') != '': if isinstance(action, dict) and action.get('type') != '':
return clean_action(action, env=request.env) return clean_action(action, env=request.env)
return False return False
@http.route('/web/dataset/resequence', type='json', auth="user") @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 """ Re-sequences a number of records in the model, by their ids
The re-sequencing starts at the first model of ``ids``, the sequence The re-sequencing starts at the first model of ``ids``, the sequence
@ -64,6 +59,8 @@ class DataSet(http.Controller):
starting the resequencing from an arbitrary number, starting the resequencing from an arbitrary number,
defaults to ``0`` defaults to ``0``
""" """
if context:
request.update_context(**context)
m = request.env[model] m = request.env[model]
if not m.fields_get([field]): if not m.fields_get([field]):
return False return False

View file

@ -1,5 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import csv
import datetime import datetime
import functools import functools
import io import io
@ -11,14 +11,11 @@ from collections import OrderedDict
from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import InternalServerError
import odoo
import odoo.modules.registry
from odoo import http from odoo import http
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.http import content_disposition, request from odoo.http import content_disposition, request
from odoo.tools import lazy_property, osutil, pycompat from odoo.tools import lazy_property, osutil
from odoo.tools.misc import xlsxwriter from odoo.tools.misc import xlsxwriter
from odoo.tools.translate import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -64,31 +61,29 @@ class GroupsTreeNode:
build a leaf. The entire tree is built by inserting all leaves. build a leaf. The entire tree is built by inserting all leaves.
""" """
def __init__(self, model, fields, groupby, groupby_type, root=None): def __init__(self, model, fields, groupby, groupby_type, read_context):
self._model = model self._model = model
self._export_field_names = fields # exported field names (e.g. 'journal_id', 'account_id/name', ...) self._export_field_names = fields # exported field names (e.g. 'journal_id', 'account_id/name', ...)
self._groupby = groupby self._groupby = groupby
self._groupby_type = groupby_type self._groupby_type = groupby_type
self._read_context = read_context
self.count = 0 # Total number of records in the subtree self.count = 0 # Total number of records in the subtree
self.children = OrderedDict() self.children = OrderedDict()
self.data = [] # Only leaf nodes have data self.data = [] # Only leaf nodes have data
if root: def _get_aggregate(self, field_name, data, aggregator):
self.insert_leaf(root)
def _get_aggregate(self, field_name, data, group_operator):
# When exporting one2many fields, multiple data lines might be exported for one record. # When exporting one2many fields, multiple data lines might be exported for one record.
# Blank cells of additionnal lines are filled with an empty string. This could lead to '' being # Blank cells of additionnal lines are filled with an empty string. This could lead to '' being
# aggregated with an integer or float. # aggregated with an integer or float.
data = (value for value in data if value != '') data = (value for value in data if value != '')
if group_operator == 'avg': if aggregator == 'avg':
return self._get_avg_aggregate(field_name, data) return self._get_avg_aggregate(field_name, data)
aggregate_func = OPERATOR_MAPPING.get(group_operator) aggregate_func = OPERATOR_MAPPING.get(aggregator)
if not aggregate_func: if not aggregate_func:
_logger.warning("Unsupported export of group_operator '%s' for field %s on model %s", group_operator, field_name, self._model._name) _logger.warning("Unsupported export of aggregator '%s' for field %s on model %s", aggregator, field_name, self._model._name)
return return
if self.data: if self.data:
@ -108,12 +103,12 @@ class GroupsTreeNode:
for field_name in self._export_field_names: for field_name in self._export_field_names:
if field_name == '.id': if field_name == '.id':
field_name = 'id' field_name = 'id'
if '/' in field_name: if '/' in field_name or field_name not in self._model:
# Currently no support of aggregated value for nested record fields # Currently no support of aggregated value for nested record fields
# e.g. line_ids/analytic_line_ids/amount # e.g. line_ids/analytic_line_ids/amount
continue continue
field = self._model._fields[field_name] field = self._model._fields[field_name]
if field.group_operator: if field.aggregator:
aggregated_field_names.append(field_name) aggregated_field_names.append(field_name)
return aggregated_field_names return aggregated_field_names
@ -130,7 +125,7 @@ class GroupsTreeNode:
if field_name in self._get_aggregated_field_names(): if field_name in self._get_aggregated_field_names():
field = self._model._fields[field_name] field = self._model._fields[field_name]
aggregated_values[field_name] = self._get_aggregate(field_name, field_data, field.group_operator) aggregated_values[field_name] = self._get_aggregate(field_name, field_data, field.aggregator)
return aggregated_values return aggregated_values
@ -143,7 +138,7 @@ class GroupsTreeNode:
:return: the child node :return: the child node
""" """
if key not in self.children: if key not in self.children:
self.children[key] = GroupsTreeNode(self._model, self._export_field_names, self._groupby, self._groupby_type) self.children[key] = GroupsTreeNode(self._model, self._export_field_names, self._groupby, self._groupby_type, self._read_context)
return self.children[key] return self.children[key]
def insert_leaf(self, group): def insert_leaf(self, group):
@ -167,30 +162,39 @@ class GroupsTreeNode:
# Update count value and aggregated value. # Update count value and aggregated value.
node.count += count node.count += count
records = records.with_context(self._read_context)
node.data = records.export_data(self._export_field_names).get('datas', []) node.data = records.export_data(self._export_field_names).get('datas', [])
return records return records
class ExportXlsxWriter: class ExportXlsxWriter:
def __init__(self, field_names, row_count=0): def __init__(self, fields, columns_headers, row_count):
self.field_names = field_names self.fields = fields
self.columns_headers = columns_headers
self.output = io.BytesIO() self.output = io.BytesIO()
self.workbook = xlsxwriter.Workbook(self.output, {'in_memory': True}) self.workbook = xlsxwriter.Workbook(self.output, {'in_memory': True})
self.base_style = self.workbook.add_format({'text_wrap': True})
self.header_style = self.workbook.add_format({'bold': True}) self.header_style = self.workbook.add_format({'bold': True})
self.header_bold_style = self.workbook.add_format({'text_wrap': True, 'bold': True, 'bg_color': '#e9ecef'})
self.date_style = self.workbook.add_format({'text_wrap': True, 'num_format': 'yyyy-mm-dd'}) self.date_style = self.workbook.add_format({'text_wrap': True, 'num_format': 'yyyy-mm-dd'})
self.datetime_style = self.workbook.add_format({'text_wrap': True, 'num_format': 'yyyy-mm-dd hh:mm:ss'}) self.datetime_style = self.workbook.add_format({'text_wrap': True, 'num_format': 'yyyy-mm-dd hh:mm:ss'})
self.base_style = self.workbook.add_format({'text_wrap': True})
# FIXME: Should depends of the field digits
self.float_style = self.workbook.add_format({'text_wrap': True, 'num_format': '#,##0.00'})
# FIXME: Should depends of the currency field for each row (also maybe add the currency symbol)
decimal_places = request.env['res.currency']._read_group([], aggregates=['decimal_places:max'])[0][0]
self.monetary_style = self.workbook.add_format({'text_wrap': True, 'num_format': f'#,##0.{(decimal_places or 2) * "0"}'})
header_bold_props = {'text_wrap': True, 'bold': True, 'bg_color': '#e9ecef'}
self.header_bold_style = self.workbook.add_format(header_bold_props)
self.header_bold_style_float = self.workbook.add_format(dict(**header_bold_props, num_format='#,##0.00'))
self.header_bold_style_monetary = self.workbook.add_format(dict(**header_bold_props, num_format=f'#,##0.{(decimal_places or 2) * "0"}'))
self.worksheet = self.workbook.add_worksheet() self.worksheet = self.workbook.add_worksheet()
self.value = False self.value = False
self.float_format = '#,##0.00'
decimal_places = [res['decimal_places'] for res in
request.env['res.currency'].search_read([], ['decimal_places'])]
self.monetary_format = f'#,##0.{max(decimal_places or [2]) * "0"}'
if row_count > self.worksheet.xls_rowmax: if row_count > self.worksheet.xls_rowmax:
raise UserError(_('There are too many rows (%s rows, limit: %s) to export as Excel 2007-2013 (.xlsx) format. Consider splitting the export.') % (row_count, self.worksheet.xls_rowmax)) raise UserError(request.env._('There are too many rows (%(count)s rows, limit: %(limit)s) to export as Excel 2007-2013 (.xlsx) format. Consider splitting the export.', count=row_count, limit=self.worksheet.xls_rowmax))
def __enter__(self): def __enter__(self):
self.write_header() self.write_header()
@ -201,9 +205,9 @@ class ExportXlsxWriter:
def write_header(self): def write_header(self):
# Write main header # Write main header
for i, fieldname in enumerate(self.field_names): for i, column_header in enumerate(self.columns_headers):
self.write(0, i, fieldname, self.header_style) self.write(0, i, column_header, self.header_style)
self.worksheet.set_column(0, max(0, len(self.field_names) - 1), 30) # around 220 pixels self.worksheet.set_column(0, max(0, len(self.columns_headers) - 1), 30) # around 220 pixels
def close(self): def close(self):
self.workbook.close() self.workbook.close()
@ -222,15 +226,15 @@ class ExportXlsxWriter:
# here. xlsxwriter does not support bytes values in Python 3 -> # here. xlsxwriter does not support bytes values in Python 3 ->
# assume this is base64 and decode to a string, if this # assume this is base64 and decode to a string, if this
# fails note that you can't export # fails note that you can't export
cell_value = pycompat.to_text(cell_value) cell_value = cell_value.decode()
except UnicodeDecodeError: 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]) raise UserError(request.env._("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.columns_headers[column])) from None
elif isinstance(cell_value, (list, tuple)): elif isinstance(cell_value, (list, tuple, dict)):
cell_value = pycompat.to_text(cell_value) cell_value = str(cell_value)
if isinstance(cell_value, str): if isinstance(cell_value, str):
if len(cell_value) > self.worksheet.xls_strmax: if len(cell_value) > self.worksheet.xls_strmax:
cell_value = _("The content of this cell is too long for an XLSX file (more than %s characters). Please use the CSV format for this export.", self.worksheet.xls_strmax) cell_value = request.env._("The content of this cell is too long for an XLSX file (more than %s characters). Please use the CSV format for this export.", self.worksheet.xls_strmax)
else: else:
cell_value = cell_value.replace("\r", " ") cell_value = cell_value.replace("\r", " ")
elif isinstance(cell_value, datetime.datetime): elif isinstance(cell_value, datetime.datetime):
@ -238,20 +242,17 @@ class ExportXlsxWriter:
elif isinstance(cell_value, datetime.date): elif isinstance(cell_value, datetime.date):
cell_style = self.date_style cell_style = self.date_style
elif isinstance(cell_value, float): elif isinstance(cell_value, float):
cell_style.set_num_format(self.float_format) field = self.fields[column]
cell_style = self.monetary_style if field['type'] == 'monetary' else self.float_style
self.write(row, column, cell_value, cell_style) self.write(row, column, cell_value, cell_style)
class GroupExportXlsxWriter(ExportXlsxWriter): class GroupExportXlsxWriter(ExportXlsxWriter):
def __init__(self, fields, row_count=0):
super().__init__([f['label'].strip() for f in fields], row_count)
self.fields = fields
def write_group(self, row, column, group_name, group, group_depth=0): def write_group(self, row, column, group_name, group, group_depth=0):
group_name = group_name[1] if isinstance(group_name, tuple) and len(group_name) > 1 else group_name group_name = group_name[1] if isinstance(group_name, tuple) and len(group_name) > 1 else group_name
if group._groupby_type[group_depth] != 'boolean': if group._groupby_type[group_depth] != 'boolean':
group_name = group_name or _("Undefined") group_name = group_name or request.env._("Undefined")
row, column = self._write_group_header(row, column, group_name, group, group_depth) row, column = self._write_group_header(row, column, group_name, group, group_depth)
# Recursively write sub-groups # Recursively write sub-groups
@ -276,19 +277,20 @@ class GroupExportXlsxWriter(ExportXlsxWriter):
for field in self.fields[1:]: # No aggregates allowed in the first column because of the group title for field in self.fields[1:]: # No aggregates allowed in the first column because of the group title
column += 1 column += 1
aggregated_value = aggregates.get(field['name']) aggregated_value = aggregates.get(field['name'])
if field.get('type') == 'monetary': header_style = self.header_bold_style
self.header_bold_style.set_num_format(self.monetary_format) if field['type'] == 'monetary':
elif field.get('type') == 'float': header_style = self.header_bold_style_monetary
self.header_bold_style.set_num_format(self.float_format) elif field['type'] == 'float':
header_style = self.header_bold_style_float
else: else:
aggregated_value = str(aggregated_value if aggregated_value is not None else '') aggregated_value = str(aggregated_value if aggregated_value is not None else '')
self.write(row, column, aggregated_value, self.header_bold_style) self.write(row, column, aggregated_value, header_style)
return row + 1, 0 return row + 1, 0
class Export(http.Controller): class Export(http.Controller):
@http.route('/web/export/formats', type='json', auth="user") @http.route('/web/export/formats', type='json', auth='user', readonly=True)
def formats(self): def formats(self):
""" Returns all valid export formats """ Returns all valid export formats
@ -300,87 +302,151 @@ class Export(http.Controller):
{'tag': 'csv', 'label': 'CSV'}, {'tag': 'csv', 'label': 'CSV'},
] ]
def fields_get(self, model): def _get_property_fields(self, fields, model, domain=()):
""" Return property fields existing for the `domain` """
property_fields = {}
Model = request.env[model] Model = request.env[model]
fields = Model.fields_get() for fname, field in fields.items():
return fields if field.get('type') != 'properties':
continue
@http.route('/web/export/get_fields', type='json', auth="user") definition_record = field['definition_record']
def get_fields(self, model, prefix='', parent_name='', definition_record_field = field['definition_record_field']
target_model = Model.env[Model._fields[definition_record].comodel_name]
domain_definition = [(definition_record_field, '!=', False)]
# Depends of the records selected to avoid showing useless Properties
if domain:
self_subquery = Model.with_context(active_test=False)._search(domain)
field_to_get = Model._field_to_sql(Model._table, definition_record, self_subquery)
domain_definition.append(('id', 'in', self_subquery.subselect(field_to_get)))
definition_records = target_model.search_fetch(
domain_definition, [definition_record_field, 'display_name'],
order='id', # Avoid complex order
)
for record in definition_records:
for definition in record[definition_record_field]:
# definition = {
# 'name': 'aa34746a6851ee4e',
# 'string': 'Partner',
# 'type': 'many2one',
# 'comodel': 'test_new_api.partner',
# 'default': [1337, 'Bob'],
# }
if (
definition['type'] == 'separator' or
(
definition['type'] in ('many2one', 'many2many')
and definition.get('comodel') not in Model.env
)
):
continue
id_field = f"{fname}.{definition['name']}"
property_fields[id_field] = {
'type': definition['type'],
'string': Model.env._(
"%(property_string)s (%(parent_name)s)",
property_string=definition['string'], parent_name=record.display_name,
),
'default_export_compatible': field['default_export_compatible'],
}
if definition['type'] in ('many2one', 'many2many'):
property_fields[id_field]['relation'] = definition['comodel']
return property_fields
@http.route('/web/export/get_fields', type='json', auth='user', readonly=True)
def get_fields(self, model, domain, prefix='', parent_name='',
import_compat=True, parent_field_type=None, import_compat=True, parent_field_type=None,
parent_field=None, exclude=None): parent_field=None, exclude=None):
fields = self.fields_get(model) Model = request.env[model]
fields = Model.fields_get(
attributes=[
'type', 'string', 'required', 'relation_field', 'default_export_compatible',
'relation', 'definition_record', 'definition_record_field', 'exportable', 'readonly',
],
)
if import_compat: if import_compat:
if parent_field_type in ['many2one', 'many2many']: if parent_field_type in ['many2one', 'many2many']:
rec_name = request.env[model]._rec_name_fallback() rec_name = Model._rec_name_fallback()
fields = {'id': fields['id'], rec_name: fields[rec_name]} fields = {'id': fields['id'], rec_name: fields[rec_name]}
else: else:
fields['.id'] = {**fields['id']} fields['.id'] = {**fields['id']}
fields['id']['string'] = _('External ID') fields['id']['string'] = request.env._('External ID')
if parent_field: if not Model._is_an_ordinary_table():
parent_field['string'] = _('External ID') fields.pop("id", None)
elif parent_field:
parent_field['string'] = request.env._('External ID')
fields['id'] = parent_field fields['id'] = parent_field
fields['id']['type'] = parent_field['field_type']
fields_sequence = sorted(fields.items(), exportable_fields = {}
key=lambda field: odoo.tools.ustr(field[1].get('string', '').lower())) for field_name, field in fields.items():
if import_compat and field_name != 'id':
records = []
for field_name, field in fields_sequence:
if import_compat and not field_name == 'id':
if exclude and field_name in exclude: if exclude and field_name in exclude:
continue continue
if field.get('type') in ('properties', 'properties_definition'):
continue
if field.get('readonly'): 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): if not field.get('exportable', True):
continue continue
exportable_fields[field_name] = field
exportable_fields.update(self._get_property_fields(fields, model, domain=domain))
fields_sequence = sorted(exportable_fields.items(), key=lambda field: field[1]['string'].lower())
result = []
for field_name, field in fields_sequence:
ident = prefix + ('/' if prefix else '') + field_name ident = prefix + ('/' if prefix else '') + field_name
val = ident val = ident
if field_name == 'name' and import_compat and parent_field_type in ['many2one', 'many2many']: if field_name == 'name' and import_compat and parent_field_type in ['many2one', 'many2many']:
# Add name field when expand m2o and m2m fields in import-compatible mode # Add name field when expand m2o and m2m fields in import-compatible mode
val = prefix val = prefix
name = parent_name + (parent_name and '/' or '') + field['string'] name = parent_name + (parent_name and '/' or '') + field['string']
record = {'id': ident, 'string': name, field_dict = {
'value': val, 'children': False, 'id': ident,
'string': name,
'value': val,
'children': False,
'field_type': field.get('type'), 'field_type': field.get('type'),
'required': field.get('required'), 'required': field.get('required'),
'relation_field': field.get('relation_field'), 'relation_field': field.get('relation_field'),
'default_export': import_compat and field.get('default_export_compatible')} 'default_export': import_compat and field.get('default_export_compatible')
records.append(record) }
if len(ident.split('/')) < 3 and 'relation' in field: if len(ident.split('/')) < 3 and 'relation' in field:
ref = field.pop('relation') field_dict['value'] += '/id'
record['value'] += '/id' field_dict['params'] = {
record['params'] = {'model': ref, 'prefix': ident, 'name': name, 'parent_field': field} 'model': field['relation'],
record['children'] = True 'prefix': ident,
'name': name,
'parent_field': field,
}
field_dict['children'] = True
return records result.append(field_dict)
@http.route('/web/export/namelist', type='json', auth="user") return result
@http.route('/web/export/namelist', type='json', auth='user', readonly=True)
def namelist(self, model, export_id): def namelist(self, model, export_id):
# TODO: namelist really has no reason to be in Python (although itertools.groupby helps) export = request.env['ir.exports'].browse([export_id])
export = request.env['ir.exports'].browse([export_id]).read()[0] return self.fields_info(model, export.export_fields.mapped('name'))
export_fields_list = request.env['ir.exports.line'].browse(export['export_fields']).read()
fields_data = self.fields_info(
model, [f['name'] for f in export_fields_list])
return [
{'name': field['name'], 'label': fields_data[field['name']]}
for field in export_fields_list if field['name'] in fields_data
]
def fields_info(self, model, export_fields): def fields_info(self, model, export_fields):
info = {} field_info = []
fields = self.fields_get(model) fields = request.env[model].fields_get(
attributes=[
'type', 'string', 'required', 'relation_field', 'default_export_compatible',
'relation', 'definition_record', 'definition_record_field',
],
)
fields.update(self._get_property_fields(fields, model))
if ".id" in export_fields: if ".id" in export_fields:
fields['.id'] = fields.get('id', {'string': 'ID'}) fields['.id'] = fields.get('id', {'string': 'ID'})
@ -418,20 +484,32 @@ class Export(http.Controller):
subfields = list(subfields) subfields = list(subfields)
if length == 2: if length == 2:
# subfields is a seq of $base/*rest, and not loaded yet # subfields is a seq of $base/*rest, and not loaded yet
info.update(self.graft_subfields( field_info.extend(
fields[base]['relation'], base, fields[base]['string'], self.graft_subfields(
subfields fields[base]['relation'], base, fields[base]['string'], subfields
)) ),
)
elif base in fields: elif base in fields:
info[base] = fields[base]['string'] field_dict = fields[base]
field_info.append({
'id': base,
'string': field_dict['string'],
'field_type': field_dict['type'],
})
return info indexes_dict = {fname: i for i, fname in enumerate(export_fields)}
return sorted(field_info, key=lambda field_dict: indexes_dict[field_dict['id']])
def graft_subfields(self, model, prefix, prefix_string, fields): def graft_subfields(self, model, prefix, prefix_string, fields):
export_fields = [field.split('/', 1)[1] for field in fields] export_fields = [field.split('/', 1)[1] for field in fields]
return ( return (
(prefix + '/' + k, prefix_string + '/' + v) dict(
for k, v in self.fields_info(model, export_fields).items()) field_info,
id=f"{prefix}/{field_info['id']}",
string=f"{prefix_string}/{field_info['string']}",
)
for field_info in self.fields_info(model, export_fields)
)
class ExportFormat(object): class ExportFormat(object):
@ -455,7 +533,7 @@ class ExportFormat(object):
model_description = request.env['ir.model']._get(base).name model_description = request.env['ir.model']._get(base).name
return f"{model_description} ({base})" return f"{model_description} ({base})"
def from_data(self, fields, rows): def from_data(self, fields, columns_headers, rows):
""" Conversion method from Odoo's export data to whatever the """ Conversion method from Odoo's export data to whatever the
current export class outputs current export class outputs
@ -466,7 +544,7 @@ class ExportFormat(object):
""" """
raise NotImplementedError() raise NotImplementedError()
def from_group_data(self, fields, groups): def from_group_data(self, fields, columns_headers, groups):
raise NotImplementedError() raise NotImplementedError()
def base(self, data): def base(self, data):
@ -488,21 +566,24 @@ class ExportFormat(object):
if not import_compat and groupby: if not import_compat and groupby:
groupby_type = [Model._fields[x.split(':')[0]].type for x in groupby] groupby_type = [Model._fields[x.split(':')[0]].type for x in groupby]
domain = [('id', 'in', ids)] if ids else domain 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) read_context = Model.env.context
if ids:
Model = Model.with_context(active_test=False)
groups_data = Model.read_group(domain, ['__count'], groupby, lazy=False)
# read_group(lazy=False) returns a dict only for final groups (with actual data), # 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. # not for intermediary groups. The full group tree must be re-constructed.
tree = GroupsTreeNode(Model, field_names, groupby, groupby_type) tree = GroupsTreeNode(Model, field_names, groupby, groupby_type, read_context)
records = Model.browse() records = Model.browse()
for leaf in groups_data: for leaf in groups_data:
records |= tree.insert_leaf(leaf) records |= tree.insert_leaf(leaf)
response_data = self.from_group_data(fields, tree) response_data = self.from_group_data(fields, columns_headers, tree)
else: else:
records = Model.browse(ids) if ids else Model.search(domain, offset=0, limit=False, order=False) records = Model.browse(ids) if ids else Model.search(domain, offset=0, limit=False, order=False)
export_data = records.export_data(field_names).get('datas', []) export_data = records.export_data(field_names).get('datas', [])
response_data = self.from_data(columns_headers, export_data) response_data = self.from_data(fields, columns_headers, export_data)
_logger.info( _logger.info(
"User %d exported %d %r records from %s. Fields: %s. %s: %s", "User %d exported %d %r records from %s. Fields: %s. %s: %s",
@ -522,8 +603,8 @@ class ExportFormat(object):
class CSVExport(ExportFormat, http.Controller): class CSVExport(ExportFormat, http.Controller):
@http.route('/web/export/csv', type='http', auth="user") @http.route('/web/export/csv', type='http', auth='user')
def index(self, data): def web_export_csv(self, data):
try: try:
return self.base(data) return self.base(data)
except Exception as exc: except Exception as exc:
@ -543,31 +624,35 @@ class CSVExport(ExportFormat, http.Controller):
def extension(self): def extension(self):
return '.csv' return '.csv'
def from_group_data(self, fields, groups): def from_group_data(self, fields, columns_headers, groups):
raise UserError(_("Exporting grouped data to csv is not supported.")) raise UserError(request.env._("Exporting grouped data to csv is not supported."))
def from_data(self, fields, rows): def from_data(self, fields, columns_headers, rows):
fp = io.BytesIO() fp = io.StringIO()
writer = pycompat.csv_writer(fp, quoting=1) writer = csv.writer(fp, quoting=1)
writer.writerow(fields) writer.writerow(columns_headers)
for data in rows: for data in rows:
row = [] row = []
for d in data: for d in data:
if d is None or d is False:
d = ''
elif isinstance(d, bytes):
d = d.decode()
# Spreadsheet apps tend to detect formulas on leading =, + and - # Spreadsheet apps tend to detect formulas on leading =, + and -
if isinstance(d, str) and d.startswith(('=', '-', '+')): if isinstance(d, str) and d.startswith(('=', '-', '+')):
d = "'" + d d = "'" + d
row.append(pycompat.to_text(d)) row.append(d)
writer.writerow(row) writer.writerow(row)
return fp.getvalue() return fp.getvalue()
class ExcelExport(ExportFormat, http.Controller): class ExcelExport(ExportFormat, http.Controller):
@http.route('/web/export/xlsx', type='http', auth="user") @http.route('/web/export/xlsx', type='http', auth='user')
def index(self, data): def web_export_xlsx(self, data):
try: try:
return self.base(data) return self.base(data)
except Exception as exc: except Exception as exc:
@ -587,16 +672,16 @@ class ExcelExport(ExportFormat, http.Controller):
def extension(self): def extension(self):
return '.xlsx' return '.xlsx'
def from_group_data(self, fields, groups): def from_group_data(self, fields, columns_headers, groups):
with GroupExportXlsxWriter(fields, groups.count) as xlsx_writer: with GroupExportXlsxWriter(fields, columns_headers, groups.count) as xlsx_writer:
x, y = 1, 0 x, y = 1, 0
for group_name, group in groups.children.items(): for group_name, group in groups.children.items():
x, y = xlsx_writer.write_group(x, y, group_name, group) x, y = xlsx_writer.write_group(x, y, group_name, group)
return xlsx_writer.value return xlsx_writer.value
def from_data(self, fields, rows): def from_data(self, fields, columns_headers, rows):
with ExportXlsxWriter(fields, len(rows)) as xlsx_writer: with ExportXlsxWriter(fields, columns_headers, len(rows)) as xlsx_writer:
for row_index, row in enumerate(rows): for row_index, row in enumerate(rows):
for cell_index, cell_value in enumerate(row): for cell_index, cell_value in enumerate(row):
xlsx_writer.write_cell(row_index + 1, cell_index, cell_value) xlsx_writer.write_cell(row_index + 1, cell_index, cell_value)

View file

@ -4,16 +4,18 @@ import json
import logging import logging
import psycopg2 import psycopg2
import odoo.exceptions
import odoo
import odoo.modules.registry import odoo.modules.registry
from odoo import http from odoo import http
from odoo.exceptions import AccessError from odoo.exceptions import AccessError
from odoo.http import request from odoo.http import request
from odoo.service import security from odoo.service import security
from odoo.tools import ustr
from odoo.tools.translate import _ from odoo.tools.translate import _
from .utils import ensure_db, _get_login_redirect_url, is_user_internal from .utils import (
ensure_db,
_get_login_redirect_url,
is_user_internal,
)
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -24,6 +26,7 @@ SIGN_UP_REQUEST_PARAMS = {'db', 'login', 'debug', 'token', 'message', 'error', '
'redirect', 'redirect_hostname', 'email', 'name', 'partner_id', 'redirect', 'redirect_hostname', 'email', 'name', 'partner_id',
'password', 'confirm_password', 'city', 'country_id', 'lang', 'signup_email'} 'password', 'confirm_password', 'city', 'country_id', 'lang', 'signup_email'}
LOGIN_SUCCESSFUL_PARAMS = set() LOGIN_SUCCESSFUL_PARAMS = set()
CREDENTIAL_PARAMS = ['login', 'password', 'type']
class Home(http.Controller): class Home(http.Controller):
@ -32,19 +35,22 @@ class Home(http.Controller):
def index(self, s_action=None, db=None, **kw): def index(self, s_action=None, db=None, **kw):
if request.db and request.session.uid and not is_user_internal(request.session.uid): if request.db and request.session.uid and not is_user_internal(request.session.uid):
return request.redirect_query('/web/login_successful', query=request.params) return request.redirect_query('/web/login_successful', query=request.params)
return request.redirect_query('/web', query=request.params) return request.redirect_query('/odoo', query=request.params)
def _web_client_readonly(self):
return False
# ideally, this route should be `auth="user"` but that don't work in non-monodb mode. # ideally, this route should be `auth="user"` but that don't work in non-monodb mode.
@http.route('/web', type='http', auth="none") @http.route(['/web', '/odoo', '/odoo/<path:subpath>', '/scoped_app/<path:subpath>'], type='http', auth="none", readonly=_web_client_readonly)
def web_client(self, s_action=None, **kw): def web_client(self, s_action=None, **kw):
# Ensure we have both a database and a user # Ensure we have both a database and a user
ensure_db() ensure_db()
if not request.session.uid: if not request.session.uid:
return request.redirect('/web/login', 303) return request.redirect_query('/web/login', query={'redirect': request.httprequest.full_path}, code=303)
if kw.get('redirect'): if kw.get('redirect'):
return request.redirect(kw.get('redirect'), 303) return request.redirect(kw.get('redirect'), 303)
if not security.check_session(request.session, request.env): if not security.check_session(request.session, request.env, request):
raise http.SessionExpiredException("Session expired") raise http.SessionExpiredException("Session expired")
if not is_user_internal(request.session.uid): if not is_user_internal(request.session.uid):
return request.redirect('/web/login_successful', 303) return request.redirect('/web/login_successful', 303)
@ -55,6 +61,8 @@ class Home(http.Controller):
# Restore the user on the environment, it was lost due to auth="none" # Restore the user on the environment, it was lost due to auth="none"
request.update_env(user=request.session.uid) request.update_env(user=request.session.uid)
try: try:
if request.env.user:
request.env.user._on_webclient_bootstrap()
context = request.env['ir.http'].webclient_rendering_context() context = request.env['ir.http'].webclient_rendering_context()
response = request.render('web.webclient_bootstrap', qcontext=context) response = request.render('web.webclient_bootstrap', qcontext=context)
response.headers['X-Frame-Options'] = 'DENY' response.headers['X-Frame-Options'] = 'DENY'
@ -62,15 +70,19 @@ class Home(http.Controller):
except AccessError: except AccessError:
return request.redirect('/web/login?error=access') return request.redirect('/web/login?error=access')
@http.route('/web/webclient/load_menus/<string:unique>', type='http', auth='user', methods=['GET']) @http.route('/web/webclient/load_menus/<string:unique>', type='http', auth='user', methods=['GET'], readonly=True)
def web_load_menus(self, unique): def web_load_menus(self, unique, lang=None):
""" """
Loads the menus for the webclient 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 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) :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) menus = request.env["ir.ui.menu"].load_web_menus(request.session.debug)
body = json.dumps(menus, default=ustr) body = json.dumps(menus)
response = request.make_response(body, [ response = request.make_response(body, [
# this method must specify a content-type application/json instead of using the default text/html set because # this method must specify a content-type application/json instead of using the default text/html set because
# the type of the route is set to HTTP, but the rpc is made with a get and expects JSON # the type of the route is set to HTTP, but the rpc is made with a get and expects JSON
@ -82,7 +94,7 @@ class Home(http.Controller):
def _login_redirect(self, uid, redirect=None): def _login_redirect(self, uid, redirect=None):
return _get_login_redirect_url(uid, redirect) return _get_login_redirect_url(uid, redirect)
@http.route('/web/login', type='http', auth="none") @http.route('/web/login', type='http', auth='none', readonly=False)
def web_login(self, redirect=None, **kw): def web_login(self, redirect=None, **kw):
ensure_db() ensure_db()
request.params['login_success'] = False request.params['login_success'] = False
@ -107,9 +119,11 @@ class Home(http.Controller):
if request.httprequest.method == 'POST': if request.httprequest.method == 'POST':
try: try:
uid = request.session.authenticate(request.db, request.params['login'], request.params['password']) credential = {key: value for key, value in request.params.items() if key in CREDENTIAL_PARAMS and value}
credential.setdefault('type', 'password')
auth_info = request.session.authenticate(request.db, credential)
request.params['login_success'] = True request.params['login_success'] = True
return request.redirect(self._login_redirect(uid, redirect=redirect)) return request.redirect(self._login_redirect(auth_info['uid'], redirect=redirect))
except odoo.exceptions.AccessDenied as e: except odoo.exceptions.AccessDenied as e:
if e.args == odoo.exceptions.AccessDenied().args: if e.args == odoo.exceptions.AccessDenied().args:
values['error'] = _("Wrong login/password") values['error'] = _("Wrong login/password")
@ -137,13 +151,13 @@ class Home(http.Controller):
valid_values = {k: v for k, v in kwargs.items() if k in LOGIN_SUCCESSFUL_PARAMS} valid_values = {k: v for k, v in kwargs.items() if k in LOGIN_SUCCESSFUL_PARAMS}
return request.render('web.login_successful', valid_values) return request.render('web.login_successful', valid_values)
@http.route('/web/become', type='http', auth='user', sitemap=False) @http.route('/web/become', type='http', auth='user', sitemap=False, readonly=True)
def switch_to_admin(self): def switch_to_admin(self):
uid = request.env.user.id uid = request.env.user.id
if request.env.user._is_system(): if request.env.user._is_system():
uid = request.session.uid = odoo.SUPERUSER_ID uid = request.session.uid = odoo.SUPERUSER_ID
# invalidate session token cache as we've changed the uid # 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) request.session.session_token = security.compute_session_token(request.session, request.env)
return request.redirect(self._login_redirect(uid)) return request.redirect(self._login_redirect(uid))
@ -165,11 +179,16 @@ class Home(http.Controller):
('Cache-Control', 'no-store')] ('Cache-Control', 'no-store')]
return request.make_response(data, headers, status=status) 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): def _get_allowed_robots_routes(self):
"""Override this method to return a list of allowed routes. """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 :return: A list of URL paths that should be allowed by robots.txt
Examples: ['/social_instagram/', '/sitemap.xml', '/web/'] Examples: ['/social_instagram/', '/sitemap.xml', '/web/']

View file

@ -1,54 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import warnings import warnings
from odoo import http warnings.warn(
from odoo.tools import lazy f"{__name__!r} has been deprecated since 18.0 and is completely "
from odoo.addons.web.controllers import ( "empty, all controllers and utility functions were moved to sibling "
action, binary, database, dataset, export, home, report, session, "submodules in Odoo 16",
utils, view, webclient, DeprecationWarning,
stacklevel=2,
) )
_MOVED_TO_MAP = {
'_get_login_redirect_url': utils,
'_local_web_translations': webclient,
'Action': action,
'allow_empty_iterable': export,
'Binary': binary,
'clean': binary,
'clean_action': utils,
'content_disposition': http,
'CONTENT_MAXAGE': webclient,
'CSVExport': export,
'Database': database,
'DataSet': dataset,
'DBNAME_PATTERN': database,
'ensure_db': utils,
'ExcelExport': export,
'Export': export,
'ExportFormat': export,
'ExportXlsxWriter': export,
'fix_view_modes': utils,
'generate_views': utils,
'GroupExportXlsxWriter': export,
'GroupsTreeNode': export,
'Home': home,
'none_values_filtered': export,
'OPERATOR_MAPPING': export,
'ReportController': report,
'Session': session,
'SIGN_UP_REQUEST_PARAMS': home,
'View': view,
'WebClient': webclient,
}
def __getattr__(attr):
module = _MOVED_TO_MAP.get(attr)
if not module:
raise AttributeError(f"Module {__name__!r} has not attribute {attr!r}.")
@lazy
def only_one_warn():
warnings.warn(f"{__name__!r} has been split over multiple files, you'll find {attr!r} at {module.__name__!r}", DeprecationWarning, stacklevel=4)
return getattr(module, attr)
return only_one_warn

View file

@ -9,17 +9,13 @@ from werkzeug.datastructures import FileStorage
from odoo import http, _ from odoo import http, _
from odoo.http import content_disposition, request from odoo.http import content_disposition, request
from odoo.tools import ustr, osutil from odoo.tools import osutil
from odoo.tools.misc import xlsxwriter from odoo.tools.misc import xlsxwriter
class TableExporter(http.Controller): class TableExporter(http.Controller):
@http.route('/web/pivot/check_xlsxwriter', type='json', auth='none') @http.route('/web/pivot/export_xlsx', type='http', auth="user", readonly=True)
def check_xlsxwriter(self):
return xlsxwriter is not None
@http.route('/web/pivot/export_xlsx', type='http', auth="user")
def export_xlsx(self, data, **kw): def export_xlsx(self, data, **kw):
jdata = json.load(data) if isinstance(data, FileStorage) else json.loads(data) jdata = json.load(data) if isinstance(data, FileStorage) else json.loads(data)
output = io.BytesIO() output = io.BytesIO()
@ -93,7 +89,7 @@ class TableExporter(http.Controller):
# Step 4: writing data # Step 4: writing data
x = 0 x = 0
for row in jdata['rows']: for row in jdata['rows']:
worksheet.write(y, x, row['indent'] * ' ' + ustr(row['title']), header_plain) worksheet.write(y, x, f"{row['indent'] * ' '}{row['title']}", header_plain)
for cell in row['values']: for cell in row['values']:
x = x + 1 x = x + 1
if cell.get('is_bold', False): if cell.get('is_bold', False):

View file

@ -20,7 +20,10 @@ class Profiling(Controller):
except UserError as e: except UserError as e:
return Response(response='error: %s' % e, status=500, mimetype='text/plain') return Response(response='error: %s' % e, status=500, mimetype='text/plain')
@route(['/web/speedscope', '/web/speedscope/<model("ir.profile"):profile>'], type='http', sitemap=False, auth='user') @route([
'/web/speedscope',
'/web/speedscope/<model("ir.profile"):profile>',
], type='http', sitemap=False, auth='user', readonly=True)
def speedscope(self, profile=None): def speedscope(self, profile=None):
# don't server speedscope index if profiling is not enabled # don't server speedscope index if profiling is not enabled
if not request.env['ir.profile']._enabled_until(): if not request.env['ir.profile']._enabled_until():

View file

@ -23,7 +23,7 @@ class ReportController(http.Controller):
@http.route([ @http.route([
'/report/<converter>/<reportname>', '/report/<converter>/<reportname>',
'/report/<converter>/<reportname>/<docids>', '/report/<converter>/<reportname>/<docids>',
], type='http', auth='user', website=True) ], type='http', auth='user', website=True, readonly=True)
def report_routes(self, reportname, docids=None, converter=None, **data): def report_routes(self, reportname, docids=None, converter=None, **data):
report = request.env['ir.actions.report'] report = request.env['ir.actions.report']
context = dict(request.env.context) context = dict(request.env.context)
@ -52,7 +52,10 @@ class ReportController(http.Controller):
#------------------------------------------------------ #------------------------------------------------------
# Misc. route utils # Misc. route utils
#------------------------------------------------------ #------------------------------------------------------
@http.route(['/report/barcode', '/report/barcode/<barcode_type>/<path:value>'], type='http', auth="public") @http.route([
'/report/barcode',
'/report/barcode/<barcode_type>/<path:value>',
], type='http', auth='public', readonly=True)
def report_barcode(self, barcode_type, value, **kwargs): def report_barcode(self, barcode_type, value, **kwargs):
"""Contoller able to render barcode images thanks to reportlab. """Contoller able to render barcode images thanks to reportlab.
Samples:: Samples::
@ -81,10 +84,14 @@ class ReportController(http.Controller):
except (ValueError, AttributeError): except (ValueError, AttributeError):
raise werkzeug.exceptions.HTTPException(description='Cannot convert into barcode.') raise werkzeug.exceptions.HTTPException(description='Cannot convert into barcode.')
return request.make_response(barcode, headers=[('Content-Type', 'image/png')]) return request.make_response(barcode, headers=[
('Content-Type', 'image/png'),
('Cache-Control', f'public, max-age={http.STATIC_CACHE_LONG}, immutable'),
])
@http.route(['/report/download'], type='http', auth="user") @http.route(['/report/download'], type='http', auth="user")
def report_download(self, data, context=None, token=None): # pylint: disable=unused-argument # pylint: disable=unused-argument
def report_download(self, data, context=None, token=None, readonly=True):
"""This function is used by 'action_manager_report.js' in order to trigger the download of """This function is used by 'action_manager_report.js' in order to trigger the download of
a pdf/controller report. a pdf/controller report.
@ -133,7 +140,7 @@ class ReportController(http.Controller):
else: else:
return return
except Exception as e: 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) se = http.serialize_exception(e)
error = { error = {
'code': 200, 'code': 200,
@ -143,6 +150,6 @@ class ReportController(http.Controller):
res = request.make_response(html_escape(json.dumps(error))) res = request.make_response(html_escape(json.dumps(error)))
raise werkzeug.exceptions.InternalServerError(response=res) from e raise werkzeug.exceptions.InternalServerError(response=res) from e
@http.route(['/report/check_wkhtmltopdf'], type='json', auth="user") @http.route(['/report/check_wkhtmltopdf'], type='json', auth='user', readonly=True)
def check_wkhtmltopdf(self): def check_wkhtmltopdf(self):
return request.env['ir.actions.report'].get_wkhtmltopdf_state() return request.env['ir.actions.report'].get_wkhtmltopdf_state()

View file

@ -20,7 +20,7 @@ _logger = logging.getLogger(__name__)
class Session(http.Controller): class Session(http.Controller):
@http.route('/web/session/get_session_info', type='json', auth="user") @http.route('/web/session/get_session_info', type='json', auth='user', readonly=True)
def get_session_info(self): def get_session_info(self):
# Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@) # Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@)
request.session.touch() request.session.touch()
@ -28,10 +28,15 @@ class Session(http.Controller):
@http.route('/web/session/authenticate', type='json', auth="none") @http.route('/web/session/authenticate', type='json', auth="none")
def authenticate(self, db, login, password, base_location=None): def authenticate(self, db, login, password, base_location=None):
if request.db and request.db != db:
request.env.cr.close()
elif request.db:
request.env.cr.rollback()
if not http.db_filter([db]): if not http.db_filter([db]):
raise AccessError("Database not found.") raise AccessError("Database not found.")
pre_uid = request.session.authenticate(db, login, password) credential = {'login': login, 'password': password, 'type': 'password'}
if pre_uid != request.session.uid: auth_info = request.session.authenticate(db, credential)
if auth_info['uid'] != request.session.uid:
# Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@) and Android # Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@) and Android
# Correct behavior should be to raise AccessError("Renewing an expired session for user that has multi-factor-authentication is not supported. Please use /web/login instead.") # Correct behavior should be to raise AccessError("Renewing an expired session for user that has multi-factor-authentication is not supported. Please use /web/login instead.")
return {'uid': None} return {'uid': None}
@ -46,7 +51,7 @@ class Session(http.Controller):
http.root.session_store.rotate(request.session, env) http.root.session_store.rotate(request.session, env)
request.future_response.set_cookie( request.future_response.set_cookie(
'session_id', request.session.sid, 'session_id', request.session.sid,
max_age=http.SESSION_LIFETIME, httponly=True max_age=http.get_session_max_inactivity(env), httponly=True
) )
return env['ir.http'].session_info() return env['ir.http'].session_info()
@ -57,16 +62,16 @@ class Session(http.Controller):
except Exception as e: except Exception as e:
return {"error": e, "title": _("Languages")} return {"error": e, "title": _("Languages")}
@http.route('/web/session/modules', type='json', auth="user") @http.route('/web/session/modules', type='json', auth='user', readonly=True)
def modules(self): def modules(self):
# return all installed modules. Web client is smart enough to not load a module twice # return all installed modules. Web client is smart enough to not load a module twice
return list(request.env.registry._init_modules) return list(request.env.registry._init_modules)
@http.route('/web/session/check', type='json', auth="user") @http.route('/web/session/check', type='json', auth='user', readonly=True)
def check(self): def check(self):
return # ir.http@_authenticate does the job return # ir.http@_authenticate does the job
@http.route('/web/session/account', type='json', auth="user") @http.route('/web/session/account', type='json', auth='user', readonly=True)
def account(self): def account(self):
ICP = request.env['ir.config_parameter'].sudo() ICP = request.env['ir.config_parameter'].sudo()
params = { params = {
@ -77,11 +82,11 @@ class Session(http.Controller):
} }
return 'https://accounts.odoo.com/oauth2/auth?' + url_encode(params) return 'https://accounts.odoo.com/oauth2/auth?' + url_encode(params)
@http.route('/web/session/destroy', type='json', auth="user") @http.route('/web/session/destroy', type='json', auth='user', readonly=True)
def destroy(self): def destroy(self):
request.session.logout() request.session.logout()
@http.route('/web/session/logout', type='http', auth="none") @http.route('/web/session/logout', type='http', auth='none', readonly=True)
def logout(self, redirect='/web'): def logout(self, redirect='/odoo'):
request.session.logout(keep_db=True) request.session.logout(keep_db=True)
return request.redirect(redirect, 303) return request.redirect(redirect, 303)

View file

@ -1,11 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import copy import collections
import hashlib
import io
import logging import logging
import re
from collections import OrderedDict, defaultdict
import babel.messages.pofile import babel.messages.pofile
import werkzeug import werkzeug
@ -13,10 +9,9 @@ import werkzeug.exceptions
import werkzeug.utils import werkzeug.utils
import werkzeug.wrappers import werkzeug.wrappers
import werkzeug.wsgi import werkzeug.wsgi
from lxml import etree
from werkzeug.urls import iri_to_uri from werkzeug.urls import iri_to_uri
from odoo.tools.translate import JAVASCRIPT_TRANSLATION_COMMENT, WEB_TRANSLATION_COMMENT from odoo.tools.translate import JAVASCRIPT_TRANSLATION_COMMENT
from odoo.tools.misc import file_open from odoo.tools.misc import file_open
from odoo import http from odoo import http
from odoo.http import request from odoo.http import request
@ -27,8 +22,8 @@ _logger = logging.getLogger(__name__)
def clean_action(action, env): def clean_action(action, env):
action_type = action.setdefault('type', 'ir.actions.act_window_close') action_type = action.setdefault('type', 'ir.actions.act_window_close')
if action_type == 'ir.actions.act_window': if action_type == 'ir.actions.act_window' and not action.get('views'):
action = fix_view_modes(action) generate_views(action)
# When returning an action, keep only relevant fields/properties # When returning an action, keep only relevant fields/properties
readable_fields = env[action['type']]._get_readable_fields() readable_fields = env[action['type']]._get_readable_fields()
@ -106,43 +101,7 @@ def ensure_db(redirect='/web/database/selector', db=None):
werkzeug.exceptions.abort(request.redirect(request.httprequest.url, 302)) werkzeug.exceptions.abort(request.redirect(request.httprequest.url, 302))
def fix_view_modes(action): # I think generate_views should go into js ActionManager
""" For historical reasons, Odoo has weird dealings in relation to
view_mode and the view_type attribute (on window actions):
* one of the view modes is ``tree``, which stands for both list views
and tree views
* the choice is made by checking ``view_type``, which is either
``form`` for a list view or ``tree`` for an actual tree view
This methods simply folds the view_type into view_mode by adding a
new view mode ``list`` which is the result of the ``tree`` view_mode
in conjunction with the ``form`` view_type.
TODO: this should go into the doc, some kind of "peculiarities" section
:param dict action: an action descriptor
:returns: nothing, the action is modified in place
"""
if not action.get('views'):
generate_views(action)
if action.pop('view_type', 'form') != 'form':
return action
if 'view_mode' in action:
action['view_mode'] = ','.join(
mode if mode != 'tree' else 'list'
for mode in action['view_mode'].split(','))
action['views'] = [
[id, mode if mode != 'tree' else 'list']
for id, mode in action['views']
]
return action
# I think generate_views,fix_view_modes should go into js ActionManager
def generate_views(action): def generate_views(action):
""" """
While the server generates a sequence called "views" computing dependencies While the server generates a sequence called "views" computing dependencies
@ -181,12 +140,100 @@ def generate_views(action):
action['views'] = [(view_id, view_modes[0])] action['views'] = [(view_id, view_modes[0])]
def get_action(env, path_part):
"""
Get a ir.actions.actions() given an action typically found in a
"/odoo"-like url.
The action can take one of the following forms:
* "action-" followed by a record id
* "action-" followed by a xmlid
* "m-" followed by a model name (act_window's res_model)
* a dotted model name (act_window's res_model)
* a path (ir.action's path)
"""
Actions = env['ir.actions.actions']
if path_part.startswith('action-'):
someid = path_part.removeprefix('action-')
if someid.isdigit(): # record id
action = Actions.sudo().browse(int(someid)).exists()
elif '.' in someid: # xml id
action = env.ref(someid, False)
if not action or not action._name.startswith('ir.actions'):
action = Actions
else:
action = Actions
elif path_part.startswith('m-') or '.' in path_part:
model = path_part.removeprefix('m-')
if model in env and not env[model]._abstract:
action = env['ir.actions.act_window'].sudo().search([
('res_model', '=', model)], limit=1)
if not action:
action = env['ir.actions.act_window'].new(
env[model].get_formview_action()
)
else:
action = Actions
else:
action = Actions.sudo().search([('path', '=', path_part)])
if action and action._name == 'ir.actions.actions':
action_type = action.read(['type'])[0]['type']
action = env[action_type].browse(action.id)
return action
def get_action_triples(env, path, *, start_pos=0):
"""
Extract the triples (active_id, action, record_id) from a "/odoo"-like path.
>>> env = ...
>>> list(get_action_triples(env, "/all-tasks/5/project.project/1/tasks"))
[
# active_id, action, record_id
( None, ir.actions.act_window(...), 5 ), # all-tasks
( 5, ir.actions.act_window(...), 1 ), # project.project
( 1, ir.actions.act_window(...), None ), # tasks
]
"""
parts = collections.deque(path.strip('/').split('/'))
active_id = None
record_id = None
while parts:
if not parts:
e = "expected action at word {} but found nothing"
raise ValueError(e.format(path.count('/') + start_pos))
action_name = parts.popleft()
action = get_action(env, action_name)
if not action:
e = f"expected action at word {{}} but found “{action_name}"
raise ValueError(e.format(path.count('/') - len(parts) + start_pos))
record_id = None
if parts:
if parts[0] == 'new':
parts.popleft()
record_id = None
elif parts[0].isdigit():
record_id = int(parts.popleft())
yield (active_id, action, record_id)
if len(parts) > 1 and parts[0].isdigit(): # new active id
active_id = int(parts.popleft())
elif record_id:
active_id = record_id
def _get_login_redirect_url(uid, redirect=None): def _get_login_redirect_url(uid, redirect=None):
""" Decide if user requires a specific post-login redirect, e.g. for 2FA, or if they are """ Decide if user requires a specific post-login redirect, e.g. for 2FA, or if they are
fully logged and can proceed to the requested URL fully logged and can proceed to the requested URL
""" """
if request.session.uid: # fully logged if request.session.uid: # fully logged
return redirect or ('/web' if is_user_internal(request.session.uid) return redirect or ('/odoo' if is_user_internal(request.session.uid)
else '/web/login_successful') else '/web/login_successful')
# partial session (MFA) # partial session (MFA)
@ -212,7 +259,6 @@ def _local_web_translations(trans_file):
except Exception: except Exception:
return return
for x in po: for x in po:
if x.id and x.string and (JAVASCRIPT_TRANSLATION_COMMENT in x.auto_comments if x.id and x.string and JAVASCRIPT_TRANSLATION_COMMENT in x.auto_comments:
or WEB_TRANSLATION_COMMENT in x.auto_comments):
messages.append({'id': x.id, 'string': x.string}) messages.append({'id': x.id, 'string': x.string})
return messages return messages

View file

@ -1,6 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # 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.http import Controller, route, request
from odoo.tools.translate import _
class View(Controller): class View(Controller):
@ -14,6 +16,12 @@ class View(Controller):
:param str arch: the edited arch of the custom view :param str arch: the edited arch of the custom view
:returns: dict with acknowledged operation (result set to True) :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 %(view)s does not belong to user %(user)s",
view=custom_id,
user=self.env.user.login,
))
custom_view.write({'arch': arch}) custom_view.write({'arch': arch})
return {'result': True} return {'result': True}

View file

@ -13,53 +13,17 @@ import werkzeug.wsgi
import odoo import odoo
import odoo.modules.registry import odoo.modules.registry
from odoo import http 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.http import request
from odoo.tools import lazy from odoo.tools.misc import file_path
from odoo.tools.misc import file_open
from .utils import _local_web_translations from .utils import _local_web_translations
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@lazy
def CONTENT_MAXAGE():
warnings.warn("CONTENT_MAXAGE is a deprecated alias to odoo.http.STATIC_CACHE_LONG", DeprecationWarning)
return http.STATIC_CACHE_LONG
MOMENTJS_LANG_CODES_MAP = {
"sr_RS": "sr_cyrl",
"sr@latin": "sr"
}
class WebClient(http.Controller): class WebClient(http.Controller):
@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}'),
])
@http.route('/web/webclient/bootstrap_translations', type='json', auth="none") @http.route('/web/webclient/bootstrap_translations', type='json', auth="none")
def bootstrap_translations(self, mods=None): def bootstrap_translations(self, mods=None):
""" Load local translations from *.po files, as a temporary solution """ Load local translations from *.po files, as a temporary solution
@ -80,7 +44,7 @@ class WebClient(http.Controller):
for addon_name in mods: for addon_name in mods:
manifest = get_manifest(addon_name) manifest = get_manifest(addon_name)
if manifest and manifest['bootstrap']: 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: if not f_name:
continue continue
translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)} translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
@ -88,7 +52,7 @@ class WebClient(http.Controller):
return {"modules": translations_per_module, return {"modules": translations_per_module,
"lang_parameters": None} "lang_parameters": None}
@http.route('/web/webclient/translations/<string:unique>', type='http', auth="public", cors="*") @http.route('/web/webclient/translations/<string:unique>', type='http', auth='public', cors='*', readonly=True)
def translations(self, unique, mods=None, lang=None): def translations(self, unique, mods=None, lang=None):
""" """
Load the translations for the specified language and modules Load the translations for the specified language and modules
@ -103,18 +67,19 @@ class WebClient(http.Controller):
elif mods is None: elif mods is None:
mods = list(request.env.registry._init_modules) + (odoo.conf.server_wide_modules or []) 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) translations_per_module, lang_params = request.env["ir.http"].get_translations_for_webclient(mods, lang)
body = json.dumps({ body = {
'lang': lang_params and lang_params["code"], 'lang': lang,
'lang_parameters': lang_params, 'lang_parameters': lang_params,
'modules': translations_per_module, 'modules': translations_per_module,
'multi_lang': len(request.env['res.lang'].sudo().get_installed()) > 1, 'multi_lang': len(request.env['res.lang'].sudo().get_installed()) > 1,
}) }
response = request.make_response(body, [ # The type of the route is set to HTTP, but the rpc is made with a get and expects JSON
# this method must specify a content-type application/json instead of using the default text/html set because response = request.make_json_response(body, [
# the type of the route is set to HTTP, but the rpc is made with a get and expects JSON
('Content-Type', 'application/json'),
('Cache-Control', f'public, max-age={http.STATIC_CACHE_LONG}'), ('Cache-Control', f'public, max-age={http.STATIC_CACHE_LONG}'),
]) ])
return response return response
@ -123,32 +88,31 @@ class WebClient(http.Controller):
def version_info(self): def version_info(self):
return odoo.service.common.exp_version() return odoo.service.common.exp_version()
@http.route('/web/tests', type='http', auth="user") @http.route('/web/tests', type='http', auth='user', readonly=True)
def unit_tests_suite(self, mod=None, **kwargs):
return request.render('web.unit_tests_suite', {'session_info': {'view_info': request.env['ir.ui.view'].get_view_info()}})
@http.route('/web/tests/legacy', type='http', auth='user', readonly=True)
def test_suite(self, mod=None, **kwargs): def test_suite(self, mod=None, **kwargs):
return request.render('web.qunit_suite') return request.render('web.qunit_suite', {'session_info': {'view_info': request.env['ir.ui.view'].get_view_info()}})
@http.route('/web/tests/mobile', type='http', auth="none") @http.route('/web/tests/legacy/mobile', type='http', auth="none")
def test_mobile_suite(self, mod=None, **kwargs): def test_mobile_suite(self, mod=None, **kwargs):
return request.render('web.qunit_mobile_suite') return request.render('web.qunit_mobile_suite', {'session_info': {'view_info': request.env['ir.ui.view'].get_view_info()}})
@http.route('/web/benchmarks', type='http', auth="none") @http.route('/web/bundle/<string:bundle_name>', auth='public', methods=['GET'], readonly=True)
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): def bundle(self, bundle_name, **bundle_params):
""" """
Request the definition of a bundle, including its javascript and css bundled assets Request the definition of a bundle, including its javascript and css bundled assets
""" """
if 'lang' in bundle_params: if 'lang' in bundle_params:
request.update_context(lang=bundle_params['lang']) request.update_context(lang=request.env['res.lang']._get_code(bundle_params['lang']))
debug = bundle_params.get('debug', request.session.debug) debug = bundle_params.get('debug', request.session.debug)
files = request.env["ir.qweb"]._get_asset_nodes(bundle_name, debug=debug, js=True, css=True) files = request.env["ir.qweb"]._get_asset_nodes(bundle_name, debug=debug, js=True, css=True)
data = [{ data = [{
"type": tag, "type": tag,
"src": attrs.get("src") or attrs.get("data-src") or attrs.get('href'), "src": attrs.get("src") or attrs.get("data-src") or attrs.get('href'),
"content": content, } for tag, attrs in files]
} for tag, attrs, content in files]
return request.make_json_response(data) return request.make_json_response(data)

View file

@ -26,6 +26,24 @@
<field name="view_id" ref="web.external_layout_bold"/> <field name="view_id" ref="web.external_layout_bold"/>
</record> </record>
<record id="report_layout_bubble" model="report.layout">
<field name="name">Bubble</field>
<field name="sequence">6</field>
<field name="view_id" ref="web.external_layout_bubble"/>
</record>
<record id="report_layout_wave" model="report.layout">
<field name="name">Wave</field>
<field name="sequence">7</field>
<field name="view_id" ref="web.external_layout_wave"/>
</record>
<record id="report_layout_folder" model="report.layout">
<field name="name">Folder</field>
<field name="sequence">8</field>
<field name="view_id" ref="web.external_layout_folder"/>
</record>
<record id="asset_styles_company_report" model="ir.attachment"> <record id="asset_styles_company_report" model="ir.attachment">
<field name="datas" model="res.company" eval="obj()._get_asset_style_b64()"/> <field name="datas" model="res.company" eval="obj()._get_asset_style_b64()"/>
<field name="mimetype">text/scss</field> <field name="mimetype">text/scss</field>

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 #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo 16.0\n" "Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-01-25 10:43+0000\n" "POT-Creation-Date: 2024-02-07 10:24+0000\n"
"PO-Revision-Date: 2016-03-12 06:25+0000\n" "PO-Revision-Date: 2016-03-12 06:25+0000\n"
"Last-Translator: Martin Trigaux\n" "Last-Translator: Martin Trigaux\n"
"Language-Team: Spanish (Chile) (http://www.transifex.com/odoo/odoo-9/" "Language-Team: Spanish (Chile) (http://www.transifex.com/odoo/odoo-9/"
@ -25,64 +25,40 @@ msgstr ""
#. module: web #. module: web
#: model_terms:ir.ui.view,arch_db:web.report_invoice_wizard_preview #: model_terms:ir.ui.view,arch_db:web.report_invoice_wizard_preview
msgid "<strong>Untaxed Amount</strong>" msgid "<strong>Untaxed Amount</strong>"
msgstr "<strong>Total neto</strong>" msgstr "<strong>Monto neto</strong>"
#. module: web #. module: web
#. odoo-javascript #. 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 #: code:addons/web/static/src/views/form/status_bar_buttons/status_bar_buttons.xml:0
#, python-format
msgid "Action" msgid "Action"
msgstr "Acción" msgstr "Acción"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0 #: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.js:0
#: code:addons/web/static/src/legacy/xml/kanban.xml:0
#: code:addons/web/static/src/views/fields/x2many/x2many_field.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_column_quick_create.xml:0
#: code:addons/web/static/src/views/kanban/kanban_record_quick_create.xml:0 #: code:addons/web/static/src/views/kanban/kanban_record_quick_create.xml:0
#, python-format
msgid "Add" msgid "Add"
msgstr "Agregar" msgstr "Agregar"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0 #: code:addons/web/static/src/core/datetime/datetime_picker_popover.xml: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
#, python-format
msgid "Apply" msgid "Apply"
msgstr "Aplicar" msgstr "Aplicar"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js:0 #: 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/core/signature/signature_dialog.xml:0
#: code:addons/web/static/src/legacy/js/core/dialog.js: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/search/search_bar_menu/search_bar_menu.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/views/calendar/quick_create/calendar_quick_create.xml: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/list/list_confirmation_dialog.xml:0
#: code:addons/web/static/src/views/view_dialogs/export_data_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 #: code:addons/web/static/src/webclient/settings_form_view/fields/upgrade_dialog.xml:0
#: model_terms:ir.ui.view,arch_db:web.view_base_document_layout #: model_terms:ir.ui.view,arch_db:web.view_base_document_layout
#, python-format
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
@ -91,20 +67,18 @@ msgstr "Cancelar"
#: code:addons/web/static/src/core/debug/debug_menu_items.xml:0 #: 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/dialog/dialog.xml:0
#: code:addons/web/static/src/core/domain_selector_dialog/domain_selector_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/install_prompt/install_prompt.xml:0
#: code:addons/web/static/src/core/model_field_selector/model_field_selector_popover.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/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/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/fields/relational_utils.xml:0
#: code:addons/web/static/src/views/kanban/kanban_column_examples_dialog.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 #: code:addons/web/static/src/views/view_dialogs/export_data_dialog.xml:0
#: code:addons/web/static/src/views/view_dialogs/form_view_dialog.xml:0 #: code:addons/web/static/src/views/view_dialogs/form_view_dialog.xml:0
#: code:addons/web/static/src/views/view_dialogs/select_create_dialog.xml:0 #: code:addons/web/static/src/views/view_dialogs/select_create_dialog.xml:0
#, python-format
msgid "Close" msgid "Close"
msgstr "Cerrar" msgstr "Cerrar"
@ -112,160 +86,114 @@ msgstr "Cerrar"
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js:0 #: 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/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 #: code:addons/web/static/src/views/list/list_confirmation_dialog.js:0
#, python-format
msgid "Confirmation" msgid "Confirmation"
msgstr "Confirmación" msgstr "Confirmación"
#. module: web #. module: web
#. odoo-javascript #. 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/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/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/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" msgid "Create"
msgstr "Crear" msgstr "Crear"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/utils/dates.js:0 #: code:addons/web/static/src/search/utils/dates.js:0
#: code:addons/web/static/src/views/calendar/calendar_controller.js:0 #: code:addons/web/static/src/views/calendar/calendar_controller.js:0
#, python-format
msgid "Day" msgid "Day"
msgstr "Día" msgstr "Día"
#. module: web #. module: web
#. odoo-javascript #. 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/binary/binary_field.xml:0
#: code:addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.xml:0 #: code:addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.xml:0
#, python-format
msgid "Download" msgid "Download"
msgstr "Descargar" msgstr "Descargar"
#. module: web #. module: web
#. odoo-javascript #. 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/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/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/binary/binary_field.xml:0
#: code:addons/web/static/src/views/fields/image/image_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/fields/pdf_viewer/pdf_viewer_field.xml:0
#: code:addons/web/static/src/views/kanban/kanban_header.js:0
#: code:addons/web/static/src/views/kanban/kanban_record_quick_create.xml:0 #: code:addons/web/static/src/views/kanban/kanban_record_quick_create.xml:0
#, python-format
msgid "Edit" msgid "Edit"
msgstr "Editar" msgstr "Editar"
#. module: web #. module: web
#. odoo-javascript #. 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 #: 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:ir.model.fields,field_description:web.field_base_document_layout__email
#: model_terms:ir.ui.view,arch_db:web.login #: model_terms:ir.ui.view,arch_db:web.login
#, python-format
msgid "Email" msgid "Email"
msgstr "Email" msgstr "Email"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/core/file_upload/file_upload_service.js:0 #: code:addons/web/static/src/core/file_upload/file_upload_service.js:0
#: code:addons/web/static/src/views/relational_model.js:0
#, python-format
msgid "Error" msgid "Error"
msgstr "Error" msgstr "Error"
#. module: web #. module: web
#. odoo-javascript #. 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/list/list_controller.js:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.xml:0 #: code:addons/web/static/src/views/view_dialogs/export_data_dialog.xml:0
#, python-format
msgid "Export" msgid "Export"
msgstr "Exportar" msgstr "Exportar"
#. module: web #. module: web
#. odoo-javascript #. 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 #: code:addons/web/static/src/views/view_dialogs/export_data_dialog.js:0
#, python-format
msgid "Export Data" msgid "Export Data"
msgstr "Exportar datos" msgstr "Exportar datos"
#. module: web #. module: web
#. odoo-javascript #. 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" msgid "Favorites"
msgstr "Favoritos" msgstr "Favoritos"
#. module: web #. module: web
#. odoo-javascript #. 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" msgid "Filters"
msgstr "Filtros" 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 #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/views/graph/graph_view.js:0 #: code:addons/web/static/src/views/graph/graph_view.js:0
#, python-format
msgid "Graph" msgid "Graph"
msgstr "Gráfico" msgstr "Gráfico"
#. module: web #. module: web
#. odoo-javascript #. 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
#, python-format #: code:addons/web/static/src/views/pivot/pivot_group_by_menu.xml:0
msgid "Group By" msgid "Group By"
msgstr "Agrupar por" 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 #. module: web
#. odoo-javascript #. 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/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/image_field.js:0
#: code:addons/web/static/src/views/fields/image_url/image_url_field.js:0 #: code:addons/web/static/src/views/fields/image_url/image_url_field.js:0
#: code:addons/web/static/src/views/fields/image_url/image_url_field.xml:0 #: code:addons/web/static/src/views/fields/image_url/image_url_field.xml:0
#, python-format
msgid "Image" msgid "Image"
msgstr "Imagen" msgstr "Imagen"
#. module: web #. module: web
#. odoo-python #. odoo-python
#: code:addons/web/controllers/session.py:0 #: code:addons/web/controllers/session.py:0
#, python-format
msgid "Languages" msgid "Languages"
msgstr "Idiomas" 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 #. module: web
#: model_terms:ir.ui.view,arch_db:web.login #: model_terms:ir.ui.view,arch_db:web.login
msgid "Log in" msgid "Log in"
@ -273,43 +201,33 @@ msgstr "Usuario"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/utils/dates.js:0 #: code:addons/web/static/src/search/utils/dates.js:0
#: code:addons/web/static/src/views/calendar/calendar_controller.js:0 #: code:addons/web/static/src/views/calendar/calendar_controller.js:0
#, python-format
msgid "Month" msgid "Month"
msgstr "Mes" msgstr "Mes"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/views/basic/basic_model.js:0
#: code:addons/web/static/src/views/form/form_controller.js:0 #: code:addons/web/static/src/views/form/form_controller.js:0
#: code:addons/web/static/src/views/form/form_controller.xml:0 #: code:addons/web/static/src/views/form/form_controller.xml:0
#: code:addons/web/static/src/views/kanban/kanban_controller.xml:0 #: code:addons/web/static/src/views/kanban/kanban_controller.xml:0
#: code:addons/web/static/src/views/list/list_controller.xml:0 #: code:addons/web/static/src/views/list/list_controller.xml:0
#: code:addons/web/static/src/views/view_dialogs/select_create_dialog.xml:0 #: code:addons/web/static/src/views/view_dialogs/select_create_dialog.xml:0
#, python-format
msgid "New" msgid "New"
msgstr "Nuevo" msgstr "Nuevo"
#. module: web #. module: web
#. odoo-javascript #. 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/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/list/list_renderer.js:0
#: code:addons/web/static/src/views/pivot/pivot_model.js:0 #: code:addons/web/static/src/views/pivot/pivot_model.js:0
#, python-format
msgid "No" msgid "No"
msgstr "No" msgstr "No"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/views/view_button/view_button.xml:0 #: code:addons/web/static/src/views/view_button/view_button.xml:0
#, python-format
msgid "Object:" msgid "Object:"
msgstr "Objeto:" msgstr "Objeto:"
@ -317,30 +235,13 @@ msgstr "Objeto:"
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js:0 #: 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/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/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/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/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/views/view_dialogs/form_view_dialog.xml:0
#: code:addons/web/static/src/webclient/actions/action_dialog.xml:0
#, python-format
msgid "Ok" msgid "Ok"
msgstr "Aceptar" 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: "
#. module: web #. module: web
#: model_terms:ir.ui.view,arch_db:web.login #: model_terms:ir.ui.view,arch_db:web.login
msgid "Password" msgid "Password"
@ -348,24 +249,13 @@ msgstr "Contraseña"
#. module: web #. module: web
#. odoo-javascript #. 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 #: code:addons/web/static/src/views/view_dialogs/export_data_dialog.js:0
#, python-format
msgid "Please enter save field list name" msgid "Please enter save field list name"
msgstr "Por favor, introduzca el nombre de la lista de campos a guardar" msgstr "Por favor, introduzca el nombre de la lista de campos a guardar"
#. module: web #. module: web
#. odoo-javascript #. 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 #: code:addons/web/static/src/views/view_dialogs/export_data_dialog.js:0
#, python-format
msgid "Please select fields to save export list..." msgid "Please select fields to save export list..."
msgstr "" msgstr ""
"Por favor, seleccione los campos para guardar la lista de exportación..." "Por favor, seleccione los campos para guardar la lista de exportación..."
@ -373,53 +263,42 @@ msgstr ""
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/webclient/user_menu/user_menu_items.js:0 #: code:addons/web/static/src/webclient/user_menu/user_menu_items.js:0
#, python-format
msgid "Preferences" msgid "Preferences"
msgstr "Preferencias" msgstr "Preferencias"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/components/action_menus.js:0 #: code:addons/web/static/src/core/file_viewer/file_viewer.xml:0
#: code:addons/web/static/src/search/action_menus/action_menus.xml: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 #: code:addons/web/static/src/webclient/actions/reports/report_action.xml:0
#, python-format
msgid "Print" msgid "Print"
msgstr "Imprimir" msgstr "Imprimir"
#. module: web #. module: web
#. odoo-javascript #. 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/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.js:0
#: code:addons/web/static/src/views/fields/relational_utils.xml:0 #: code:addons/web/static/src/views/fields/relational_utils.xml:0
#: code:addons/web/static/src/views/form/form_controller.xml:0 #: code:addons/web/static/src/views/form/form_controller.xml:0
#, python-format
msgid "Remove" msgid "Remove"
msgstr "Eliminar" msgstr "Eliminar"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/core/domain_selector_dialog/domain_selector_dialog.xml:0 #: code:addons/web/static/src/search/custom_favorite_item/custom_favorite_item.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/views/fields/relational_utils.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/fields/translation_dialog.xml:0
#: code:addons/web/static/src/views/form/form_controller.xml:0 #: code:addons/web/static/src/views/form/form_controller.xml:0
#: code:addons/web/static/src/views/list/list_controller.xml:0 #: code:addons/web/static/src/views/list/list_controller.xml:0
#: code:addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.xml:0 #: code:addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.xml:0
#: model_terms:ir.ui.view,arch_db:web.view_base_document_layout #: model_terms:ir.ui.view,arch_db:web.view_base_document_layout
#, python-format
msgid "Save" msgid "Save"
msgstr "Guardar" msgstr "Guardar"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/core/debug/debug_menu_items.xml:0 #: code:addons/web/static/src/core/debug/debug_menu_items.xml:0
#, python-format
msgid "Save default" msgid "Save default"
msgstr "Guardar por defecto" msgstr "Guardar por defecto"
@ -427,147 +306,74 @@ msgstr "Guardar por defecto"
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/views/graph/graph_model.js:0 #: code:addons/web/static/src/views/graph/graph_model.js:0
#: code:addons/web/static/src/views/pivot/pivot_model.js:0 #: code:addons/web/static/src/views/pivot/pivot_model.js:0
#, python-format
msgid "Total" msgid "Total"
msgstr "Total" msgstr "Total"
#. module: web #. module: web
#. odoo-python
#. odoo-javascript #. odoo-javascript
#. odoo-python
#: code:addons/web/controllers/export.py:0 #: code:addons/web/controllers/export.py:0
#: code:addons/web/static/src/views/calendar/calendar_model.js:0 #: code:addons/web/static/src/views/calendar/calendar_model.js:0
#: code:addons/web/static/src/views/graph/graph_model.js:0
#, python-format
msgid "Undefined" msgid "Undefined"
msgstr "Sin definir" msgstr "Sin definir"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/core/errors/error_dialogs.js:0 #: 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/model/relational_model/dynamic_list.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_controller.js:0 #: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.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/views/fields/domain/domain_field.xml: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" msgid "Warning"
msgstr "Aviso" msgstr "Aviso"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/utils/dates.js:0 #: 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_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.js:0
#, python-format #: code:addons/web/static/src/views/calendar/calendar_controller.xml:0
msgid "Week" msgid "Week"
msgstr "Semana" msgstr "Semana"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/utils/dates.js:0 #: code:addons/web/static/src/search/utils/dates.js:0
#: code:addons/web/static/src/views/calendar/calendar_controller.js:0 #: code:addons/web/static/src/views/calendar/calendar_controller.js:0
#, python-format
msgid "Year" msgid "Year"
msgstr "Año" msgstr "Año"
#. module: web #. module: web
#. odoo-javascript #. 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/search/search_bar/search_bar.js:0
#: code:addons/web/static/src/views/fields/field_tooltip.xml: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/list/list_renderer.js:0
#: code:addons/web/static/src/views/pivot/pivot_model.js:0 #: code:addons/web/static/src/views/pivot/pivot_model.js:0
#, python-format
msgid "Yes" msgid "Yes"
msgstr "Sí" msgstr "Sí"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/core/domain_selector/domain_selector_operators.js:0 #: code:addons/web/static/src/core/tree_editor/tree_editor_operator_editor.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" msgid "contains"
msgstr "contiene" msgstr "contiene"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0 #: code:addons/web/static/src/core/tree_editor/tree_editor_operator_editor.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "doesn't contain"
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/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" msgid "is"
msgstr "es" msgstr "es"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0 #: code:addons/web/static/src/core/tree_editor/tree_editor_operator_editor.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/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" msgid "is not"
msgstr "no es" msgstr "no es"
#. module: web #. module: web
#. odoo-javascript #. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0 #: code:addons/web/static/src/core/domain_selector/utils.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0 #: code:addons/web/static/src/core/tree_editor/utils.js:0
#, python-format
msgid "is not equal to"
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/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 #: code:addons/web/static/src/search/search_model.js:0
#, python-format
msgid "or" msgid "or"
msgstr "o" msgstr "o"

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

@ -5,5 +5,9 @@ from . import ir_qweb_fields
from . import ir_http from . import ir_http
from . import ir_model from . import ir_model
from . import ir_ui_menu from . import ir_ui_menu
from . import ir_ui_view
from . import models from . import models
from . import base_document_layout from . import base_document_layout
from . import res_config_settings
from . import res_partner
from . import res_users

View file

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import markupsafe
import os import os
from markupsafe import Markup from markupsafe import Markup
from math import ceil
from odoo import api, fields, models, tools from odoo import api, fields, models
from odoo.addons.base.models.ir_qweb_fields import nl2br from odoo.addons.base.models.ir_qweb_fields import nl2br
from odoo.modules import get_resource_path from odoo.tools import html2plaintext, is_html_empty, image as tools
from odoo.tools import file_path, html2plaintext, is_html_empty from odoo.tools.misc import file_path
try: try:
import sass as libsass import sass as libsass
@ -47,7 +47,7 @@ class BaseDocumentLayout(models.TransientModel):
if 'company_name' not in address_format: if 'company_name' not in address_format:
address_format = '%(company_name)s\n' + address_format address_format = '%(company_name)s\n' + address_format
company_data['company_name'] = company_data['company_name'] or company.name company_data['company_name'] = company_data['company_name'] or company.name
return Markup(nl2br(address_format)) % company_data return nl2br(address_format) % company_data
def _clean_address_format(self, address_format, company_data): def _clean_address_format(self, address_format, company_data):
missing_company_data = [k for k, v in company_data.items() if not v] missing_company_data = [k for k, v in company_data.items() if not v]
@ -126,22 +126,29 @@ class BaseDocumentLayout(models.TransientModel):
for wizard in self: for wizard in self:
if wizard.report_layout_id: if wizard.report_layout_id:
if wizard.env.context.get('bin_size'):
# guarantees that bin_size is always set to False, # guarantees that bin_size is always set to False,
# so the logo always contains the bin data instead of the binary size # so the logo always contains the bin data instead of the binary size
if wizard.env.context.get('bin_size'): wizard = wizard.with_context(bin_size=False)
wizard_with_logo = wizard.with_context(bin_size=False) wizard.preview = wizard.env['ir.ui.view']._render_template(
else: wizard._get_preview_template(),
wizard_with_logo = wizard wizard._get_render_information(styles),
preview_css = markupsafe.Markup(self._get_css_for_preview(styles, wizard_with_logo.id)) )
ir_ui_view = wizard_with_logo.env['ir.ui.view']
wizard.preview = ir_ui_view._render_template('web.report_invoice_wizard_preview', {
'company': wizard_with_logo,
'preview_css': preview_css,
'is_html_empty': is_html_empty,
})
else: else:
wizard.preview = False wizard.preview = False
def _get_preview_template(self):
return 'web.report_invoice_wizard_preview'
def _get_render_information(self, styles):
self.ensure_one()
preview_css = self._get_css_for_preview(styles, self.id)
return {
'company': self,
'preview_css': preview_css,
'is_html_empty': is_html_empty,
}
@api.onchange('company_id') @api.onchange('company_id')
def _onchange_company_id(self): def _onchange_company_id(self):
for wizard in self: for wizard in self:
@ -217,7 +224,7 @@ class BaseDocumentLayout(models.TransientModel):
return False, False return False, False
base_w, base_h = image.size base_w, base_h = image.size
w = int(50 * base_w / base_h) w = ceil(50 * base_w / base_h)
h = 50 h = 50
# Converts to RGBA (if already RGBA, this is a noop) # Converts to RGBA (if already RGBA, this is a noop)
@ -251,14 +258,6 @@ class BaseDocumentLayout(models.TransientModel):
return tools.rgb_to_hex(primary), tools.rgb_to_hex(secondary) 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): def document_layout_save(self):
# meant to be overridden # meant to be overridden
return self.env.context.get('report_action') or {'type': 'ir.actions.act_window_close'} return self.env.context.get('report_action') or {'type': 'ir.actions.act_window_close'}
@ -306,10 +305,10 @@ class BaseDocumentLayout(models.TransientModel):
precision = 8 precision = 8
output_style = 'expanded' output_style = 'expanded'
bootstrap_path = get_resource_path('web', 'static', 'lib', 'bootstrap', 'scss') bootstrap_path = file_path('web/static/lib/bootstrap/scss')
try: try:
return libsass.compile( compiled_css = libsass.compile(
string=scss_source, string=scss_source,
include_paths=[ include_paths=[
bootstrap_path, bootstrap_path,
@ -318,6 +317,7 @@ class BaseDocumentLayout(models.TransientModel):
output_style=output_style, output_style=output_style,
precision=precision, precision=precision,
) )
return Markup(compiled_css) if isinstance(compiled_css, Markup) else compiled_css
except libsass.CompileError as e: except libsass.CompileError as e:
raise libsass.CompileError(e.args[0]) raise libsass.CompileError(e.args[0])

View file

@ -2,17 +2,14 @@
import hashlib import hashlib
import json import json
import logging
import odoo import odoo
from odoo import api, http, models from odoo import api, models, fields
from odoo.http import request from odoo.http import request, DEFAULT_MAX_CONTENT_LENGTH
from odoo.tools import file_open, image_process, ustr from odoo.tools import ormcache, config
from odoo.tools.misc import str2bool from odoo.tools.misc import str2bool
_logger = logging.getLogger(__name__)
""" """
Debug mode is stored in session and should always be a string. 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 It can be activated with an URL query string `debug=<mode>` where mode
@ -42,6 +39,12 @@ class Http(models.AbstractModel):
# timeit has been done to check the optimum method # timeit has been done to check the optimum method
return any(bot in user_agent for bot in cls.bots) return any(bot in user_agent for bot in cls.bots)
@classmethod
def _sanitize_cookies(cls, cookies):
super()._sanitize_cookies(cookies)
if cids := cookies.get('cids'):
cookies['cids'] = '-'.join(cids.split(','))
@classmethod @classmethod
def _handle_debug(cls): def _handle_debug(cls):
debug = request.httprequest.args.get('debug') debug = request.httprequest.args.get('debug')
@ -58,6 +61,11 @@ class Http(models.AbstractModel):
super()._pre_dispatch(rule, args) super()._pre_dispatch(rule, args)
cls._handle_debug() 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): def webclient_rendering_context(self):
return { return {
'menu_data': request.env['ir.ui.menu'].load_menus(request.session.debug), 'menu_data': request.env['ir.ui.menu'].load_menus(request.session.debug),
@ -79,24 +87,29 @@ class Http(models.AbstractModel):
IrConfigSudo = self.env['ir.config_parameter'].sudo() IrConfigSudo = self.env['ir.config_parameter'].sudo()
max_file_upload_size = int(IrConfigSudo.get_param( max_file_upload_size = int(IrConfigSudo.get_param(
'web.max_file_upload_size', 'web.max_file_upload_size',
default=128 * 1024 * 1024, # 128MiB default=DEFAULT_MAX_CONTENT_LENGTH,
)) ))
mods = odoo.conf.server_wide_modules or [] mods = odoo.conf.server_wide_modules or []
if request.db: if request.db:
mods = list(request.registry._init_modules) + mods mods = list(request.registry._init_modules) + mods
is_internal_user = user._is_internal()
session_info = { session_info = {
"uid": session_uid, "uid": session_uid,
"is_system": user._is_system() if session_uid else False, "is_system": user._is_system() if session_uid else False,
"is_admin": user._is_admin() 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, "user_context": user_context,
"db": self.env.cr.dbname, "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": version_info.get('server_version'),
"server_version_info": version_info.get('server_version_info'), "server_version_info": version_info.get('server_version_info'),
"support_url": "https://www.odoo.com/buy", "support_url": "https://www.odoo.com/buy",
"name": user.name, "name": user.name,
"username": user.login, "username": user.login,
"quick_login": str2bool(IrConfigSudo.get_param('web.quick_login', default=True), True),
"partner_write_date": fields.Datetime.to_string(user.partner_id.write_date),
"partner_display_name": user.partner_id.display_name, "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, "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=''), "web.base.url": IrConfigSudo.get_param('web.base.url', default=''),
"active_ids_limit": int(IrConfigSudo.get_param('web.active_ids_limit', default='20000')), "active_ids_limit": int(IrConfigSudo.get_param('web.active_ids_limit', default='20000')),
@ -114,20 +127,25 @@ class Http(models.AbstractModel):
'bundle_params': { 'bundle_params': {
'lang': request.session.context['lang'], 'lang': request.session.context['lang'],
}, },
'test_mode': bool(config['test_enable'] or config['test_file']),
'view_info': self.env['ir.ui.view'].get_view_info(),
} }
if request.session.debug: if request.session.debug:
session_info['bundle_params']['debug'] = 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 # 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') # 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 # to avoid access errors and unnecessary information, it is only included for users
# with access to the backend ('internal'-type users) # with access to the backend ('internal'-type users)
menus = self.env['ir.ui.menu'].with_context(lang=request.session.context['lang']).load_menus(request.session.debug) menus = self.env['ir.ui.menu'].with_context(lang=request.session.context['lang']).load_menus(request.session.debug)
ordered_menus = {str(k): v for k, v in menus.items()} ordered_menus = {str(k): v for k, v in menus.items()}
menu_json_utf8 = json.dumps(ordered_menus, default=ustr, sort_keys=True).encode() menu_json_utf8 = json.dumps(ordered_menus, sort_keys=True).encode()
session_info['cache_hashes'].update({ session_info['cache_hashes'].update({
"load_menus": hashlib.sha512(menu_json_utf8).hexdigest()[:64], # sha512/256 "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({ session_info.update({
# current_company should be default_company # current_company should be default_company
"user_companies": { "user_companies": {
@ -137,8 +155,19 @@ class Http(models.AbstractModel):
'id': comp.id, 'id': comp.id,
'name': comp.name, 'name': comp.name,
'sequence': comp.sequence, '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 } 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, "show_effect": True,
"display_switch_company_menu": user.has_group('base.group_multi_company') and len(user.company_ids) > 1, "display_switch_company_menu": user.has_group('base.group_multi_company') and len(user.company_ids) > 1,
@ -152,16 +181,21 @@ class Http(models.AbstractModel):
session_info = { session_info = {
'is_admin': user._is_admin() if session_uid else False, 'is_admin': user._is_admin() if session_uid else False,
'is_system': user._is_system() if session_uid else False, 'is_system': user._is_system() if session_uid else False,
'is_public': user._is_public(),
"is_internal_user": user._is_internal(),
'is_website_user': user._is_public() if session_uid else False, 'is_website_user': user._is_public() if session_uid else False,
'user_id': user.id if session_uid else False, 'uid': session_uid,
'is_frontend': True, 'is_frontend': True,
'profile_session': request.session.profile_session, 'profile_session': request.session.profile_session,
'profile_collectors': request.session.profile_collectors, 'profile_collectors': request.session.profile_collectors,
'profile_params': request.session.profile_params, 'profile_params': request.session.profile_params,
'show_effect': bool(request.env['ir.config_parameter'].sudo().get_param('base_setup.show_effect')), 'show_effect': bool(request.env['ir.config_parameter'].sudo().get_param('base_setup.show_effect')),
'currencies': self.get_currencies(),
'quick_login': str2bool(request.env['ir.config_parameter'].sudo().get_param('web.quick_login', default=True), True),
'bundle_params': { 'bundle_params': {
'lang': request.session.context['lang'], 'lang': request.session.context['lang'],
}, },
'test_mode': bool(config['test_enable'] or config['test_file']),
} }
if request.session.debug: if request.session.debug:
session_info['bundle_params']['debug'] = request.session.debug session_info['bundle_params']['debug'] = request.session.debug
@ -173,7 +207,11 @@ class Http(models.AbstractModel):
}) })
return session_info return session_info
@ormcache()
def get_currencies(self): def get_currencies(self):
Currency = self.env['res.currency'] Currency = self.env['res.currency']
currencies = Currency.search([]).read(['symbol', 'position', 'decimal_places']) 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} 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 = [] accessible_models = []
not_accessible_models = [] not_accessible_models = []
for model in models: for model in models:
if self._check_model_access(model): if self._is_valid_for_model_selector(model):
accessible_models.append(model) accessible_models.append(model)
else: else:
not_accessible_models.append({"display_name": model, "model": model}) not_accessible_models.append({"display_name": model, "model": model})
@ -34,9 +34,15 @@ class IrModel(models.Model):
} for model in records] } for model in records]
@api.model @api.model
def _check_model_access(self, model): def _is_valid_for_model_selector(self, model):
return (self.env.user._is_internal() and model in self.env model = self.env.get(model)
and self.env[model].check_access_rights("read", raise_exception=False)) return (
self.env.user._is_internal()
and model is not None
and model.has_access("read")
and not model._transient
and not model._abstract
)
@api.model @api.model
def get_available_models(self): def get_available_models(self):
@ -44,5 +50,48 @@ class IrModel(models.Model):
Return the list of models the current user has access to, with their Return the list of models the current user has access to, with their
corresponding display name. 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) return self._display_name_for(accessible_models)
def _get_definitions(self, model_names):
model_definitions = {}
for model_name in model_names:
model = self.env[model_name]
# get fields, relational fields are kept only if the related model is in model_names
fields_data_by_fname = {
fname: field_data
for fname, field_data in model.fields_get(
attributes={
'definition_record_field', 'definition_record', 'aggregator',
'name', 'readonly', 'related', 'relation', 'required', 'searchable',
'selection', 'sortable', 'store', 'string', 'tracking', 'type',
},
).items()
if field_data.get('selectable', True) and (
not field_data.get('relation') or field_data['relation'] in model_names
)
}
fields_data_by_fname = {
fname: field_data
for fname, field_data in fields_data_by_fname.items()
if not field_data.get('related') or field_data['related'].split('.')[0] in fields_data_by_fname
}
for fname, field_data in fields_data_by_fname.items():
if fname in model._fields:
inverse_fields = [
field for field in model.pool.field_inverses[model._fields[fname]]
if field.model_name in model_names
]
if inverse_fields:
field_data['inverse_fname_by_model_name'] = {field.model_name: field.name for field in inverse_fields}
if field_data['type'] == 'many2one_reference':
field_data['model_name_ref_fname'] = model._fields[fname].model_field
model_definitions[model_name] = {
'description': model._description,
'fields': fields_data_by_fname,
'inherit': [model_name for model_name in model._inherit_module if model_name in model_names],
'order': model._order,
'parent_name': model._parent_name,
'rec_name': model._rec_name,
}
return model_definitions

View file

@ -6,8 +6,7 @@ from collections import OrderedDict
from werkzeug.urls import url_quote from werkzeug.urls import url_quote
from markupsafe import Markup 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 from odoo.tools import html_escape as escape
@ -36,7 +35,7 @@ class Image(models.AbstractModel):
if max_width or max_height: if max_width or max_height:
max_size = '%sx%s' % (max_width, 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 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']]: if options.get('filename-field') and options['filename-field'] in record and record[options['filename-field']]:
@ -106,9 +105,9 @@ class Image(models.AbstractModel):
for name, value in atts.items(): for name, value in atts.items():
if value: if value:
img.append(' ') img.append(' ')
img.append(escape(pycompat.to_text(name))) img.append(escape(name))
img.append('="') img.append('="')
img.append(escape(pycompat.to_text(value))) img.append(escape(value))
img.append('"') img.append('"')
img.append('/>') img.append('/>')

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