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
This addon depends on:
- 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
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
This package maintains the original LGPL-3 license from the upstream Odoo project.
## 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
This package preserves the original LGPL-3 license.

View file

@ -1,12 +1,14 @@
[project]
name = "odoo-bringout-oca-ocb-web"
version = "16.0.0"
description = "Web - Odoo addon"
description = "Web -
Odoo addon
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-base>=16.0.0",
"odoo-bringout-oca-ocb-base>=18.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -16,14 +18,14 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/odoo-bringout-oca-ocb-web"
repository = "https://github.com/bringout/odoo-bringout-oca-ocb-web"
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]

View file

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

View file

@ -6,12 +6,28 @@ from . import database
from . import dataset
from . import domain
from . import export
from . import json
from . import home
from . import model
from . import pivot
from . import profiling
from . import report
from . import session
from . import vcard
from . import view
from . import webclient
from . import webmanifest
from . import main # deprecated
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.
import logging
from odoo import _
from odoo.exceptions import UserError, MissingError, AccessError
from odoo.http import Controller, request, route
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):
@route('/web/action/load', type='json', auth="user")
def load(self, action_id, additional_context=None):
@route('/web/action/load', type='json', auth='user', readonly=True)
def load(self, action_id, context=None):
if context:
request.update_context(**context)
Actions = request.env['ir.actions.actions']
value = False
try:
action_id = int(action_id)
except ValueError:
try:
action = request.env.ref(action_id)
assert action._name.startswith('ir.actions.')
if '.' in action_id:
action = request.env.ref(action_id)
assert action._name.startswith('ir.actions.')
else:
action = Actions.sudo().search([('path', '=', action_id)], limit=1)
assert action
action_id = action.id
except Exception:
action_id = 0 # force failed read
except Exception as exc:
raise MissingActionError(_("The action “%s” does not exist.", action_id)) from exc
base_action = Actions.browse([action_id]).sudo().read(['type'])
if base_action:
action_type = base_action[0]['type']
if action_type == 'ir.actions.report':
request.update_context(bin_size=True)
if additional_context:
request.update_context(**additional_context)
action = request.env[action_type].sudo().browse([action_id]).read()
if action:
value = clean_action(action[0], env=request.env)
return value
if not base_action:
raise MissingActionError(_("The action “%s” does not exist", action_id))
action_type = base_action[0]['type']
if action_type == 'ir.actions.report':
request.update_context(bin_size=True)
if action_type == 'ir.actions.act_window':
result = request.env[action_type].sudo().browse([action_id])._get_action_dict()
return clean_action(result, env=request.env) if result else False
result = request.env[action_type].sudo().browse([action_id]).read()
return clean_action(result[0], env=request.env) if result else False
@route('/web/action/run', type='json', auth="user")
def run(self, action_id):
def run(self, action_id, context=None):
if context:
request.update_context(**context)
action = request.env['ir.actions.server'].browse([action_id])
result = action.run()
return clean_action(result, env=action.env) if result else False
@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 unicodedata
from contextlib import nullcontext
try:
from werkzeug.utils import send_file
except ImportError:
@ -15,14 +16,13 @@ except ImportError:
import odoo
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.http import request, Response
from odoo.modules import get_resource_path
from odoo.tools import file_open, file_path, replace_exceptions, str2bool
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.image import image_guess_size_from_field_name
from odoo.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__)
@ -59,19 +59,21 @@ class Binary(http.Controller):
))
raise http.request.not_found()
@http.route(['/web/content',
@http.route([
'/web/content',
'/web/content/<string:xmlid>',
'/web/content/<string:xmlid>/<string:filename>',
'/web/content/<int:id>',
'/web/content/<int:id>/<string:filename>',
'/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
def content_common(self, xmlid=None, model='ir.attachment', id=None, field='raw',
filename=None, filename_field='name', mimetype=None, unique=False,
download=False, access_token=None, nocache=False):
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)
if request.httprequest.args.get('access_token'):
stream.public = True
@ -85,33 +87,68 @@ class Binary(http.Controller):
return stream.get_response(**send_file_kwargs)
@http.route(['/web/assets/debug/<string:filename>',
'/web/assets/debug/<path:extra>/<string:filename>',
'/web/assets/<int:id>/<string:filename>',
'/web/assets/<int:id>-<string:unique>/<string:filename>',
'/web/assets/<int:id>-<string:unique>/<path:extra>/<string:filename>'], type='http', auth="public")
# pylint: disable=redefined-builtin,invalid-name
def content_assets(self, id=None, filename=None, unique=False, extra=None, nocache=False):
if not id:
domain = [('url', '!=', False), ('res_model', '=', 'ir.ui.view'),
('res_id', '=', 0), ('create_uid', '=', odoo.SUPERUSER_ID)]
if extra:
domain += [('url', '=like', f'/web/assets/%/{extra}/{filename}')]
@http.route([
'/web/assets/<string:unique>/<string:filename>'], type='http', auth="public", readonly=True)
def content_assets(self, filename=None, unique=ANY_UNIQUE, nocache=False, assets_params=None):
env = request.env # readonly
assets_params = assets_params or {}
assert isinstance(assets_params, dict)
debug_assets = unique == 'debug'
if unique in ('any', '%'):
unique = ANY_UNIQUE
attachment = None
if unique != 'debug':
url = env['ir.asset']._get_asset_bundle_url(filename, unique, assets_params)
assert not '%' in url
domain = [
('public', '=', True),
('url', '!=', False),
('url', '=like', url),
('res_model', '=', 'ir.ui.view'),
('res_id', '=', 0),
('create_uid', '=', SUPERUSER_ID),
]
attachment = env['ir.attachment'].sudo().search(domain, limit=1)
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:
domain += [
('url', '=like', f'/web/assets/%/{filename}'),
('url', 'not like', f'/web/assets/%/%/{filename}')
]
attachments = request.env['ir.attachment'].sudo().search_read(domain, fields=['id'], limit=1)
if not attachments:
raise request.not_found()
id = attachments[0]['id']
with replace_exceptions(UserError, by=request.not_found()):
record = request.env['ir.binary']._find_record(res_id=int(id))
stream = request.env['ir.binary']._get_stream_from(record, 'raw', filename)
# 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()
bundle_name, rtl, asset_type = rw_env['ir.asset']._parse_bundle_name(filename, debug_assets)
css = asset_type == 'css'
js = asset_type == 'js'
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}
if unique:
if unique and unique != 'debug':
send_file_kwargs['immutable'] = True
send_file_kwargs['max_age'] = http.STATIC_CACHE_LONG
if nocache:
@ -119,7 +156,8 @@ class Binary(http.Controller):
return stream.get_response(**send_file_kwargs)
@http.route(['/web/image',
@http.route([
'/web/image',
'/web/image/<string:xmlid>',
'/web/image/<string:xmlid>/<string:filename>',
'/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>/<string:filename>',
'/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
def content_image(self, xmlid=None, model='ir.attachment', id=None, field='raw',
filename_field='name', filename=None, mimetype=None, unique=False,
download=False, width=0, height=0, crop=False, access_token=None,
nocache=False):
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(
record, field, filename=filename, filename_field=filename_field,
mimetype=mimetype, width=int(width), height=int(height), crop=crop,
@ -190,7 +229,7 @@ class Binary(http.Controller):
try:
attachment = Model.create({
'name': filename,
'datas': base64.encodebytes(ufile.read()),
'raw': ufile.read(),
'res_model': model,
'res_id': int(id)
})
@ -203,7 +242,7 @@ class Binary(http.Controller):
else:
args.append({
'filename': clean(filename),
'mimetype': ufile.content_type,
'mimetype': attachment.mimetype,
'id': attachment.id,
'size': attachment.file_size
})
@ -217,54 +256,56 @@ class Binary(http.Controller):
def company_logo(self, dbname=None, **kw):
imgname = 'logo'
imgext = '.png'
placeholder = functools.partial(get_resource_path, 'web', 'static', 'img')
dbname = request.db
uid = (request.session.uid if dbname else None) or odoo.SUPERUSER_ID
if not dbname:
response = http.Stream.from_path(placeholder(imgname + imgext)).get_response()
response = http.Stream.from_path(file_path('web/static/img/logo.png')).get_response()
else:
try:
# create an empty registry
registry = odoo.modules.registry.Registry(dbname)
with registry.cursor() as cr:
company = int(kw['company']) if kw and kw.get('company') else False
if company:
cr.execute("""SELECT logo_web, write_date
FROM res_company
WHERE id = %s
""", (company,))
else:
cr.execute("""SELECT c.logo_web, c.write_date
FROM res_users u
LEFT JOIN res_company c
ON c.id = u.company_id
WHERE u.id = %s
""", (uid,))
row = cr.fetchone()
if row and row[0]:
image_base64 = base64.b64decode(row[0])
image_data = io.BytesIO(image_base64)
mimetype = guess_mimetype(image_base64, default='image/png')
imgext = '.' + mimetype.split('/')[1]
if imgext == '.svg+xml':
imgext = '.svg'
response = send_file(
image_data,
request.httprequest.environ,
download_name=imgname + imgext,
mimetype=mimetype,
last_modified=row[1],
response_class=Response,
)
else:
response = http.Stream.from_path(placeholder('nologo.png')).get_response()
company = int(kw['company']) if kw and kw.get('company') else False
if company:
request.env.cr.execute("""
SELECT logo_web, write_date
FROM res_company
WHERE id = %s
""", (company,))
else:
request.env.cr.execute("""
SELECT c.logo_web, c.write_date
FROM res_users u
LEFT JOIN res_company c
ON c.id = u.company_id
WHERE u.id = %s
""", (uid,))
row = request.env.cr.fetchone()
if row and row[0]:
image_base64 = base64.b64decode(row[0])
image_data = io.BytesIO(image_base64)
mimetype = guess_mimetype(image_base64, default='image/png')
imgext = '.' + mimetype.split('/')[1]
if imgext == '.svg+xml':
imgext = '.svg'
response = send_file(
image_data,
request.httprequest.environ,
download_name=imgname + imgext,
mimetype=mimetype,
last_modified=row[1],
response_class=Response,
)
else:
response = http.Stream.from_path(file_path('web/static/img/nologo.png')).get_response()
except Exception:
response = http.Stream.from_path(placeholder(imgname + imgext)).get_response()
_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
@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):
"""This route will return a list of base64 encoded fonts.
@ -276,7 +317,7 @@ class Binary(http.Controller):
"""
supported_exts = ('.ttf', '.otf', '.woff', '.woff2')
fonts = []
fonts_directory = file_path(os.path.join('web', 'static', 'fonts', 'sign'))
fonts_directory = file_path('web/static/fonts/sign')
if fontname:
font_path = os.path.join(fonts_directory, fontname)
with file_open(font_path, 'rb', filter_ext=supported_exts) as font_file:

View file

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

View file

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

View file

@ -1,5 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import csv
import datetime
import functools
import io
@ -11,14 +11,11 @@ from collections import OrderedDict
from werkzeug.exceptions import InternalServerError
import odoo
import odoo.modules.registry
from odoo import http
from odoo.exceptions import UserError
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.translate import _
_logger = logging.getLogger(__name__)
@ -64,31 +61,29 @@ class GroupsTreeNode:
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._export_field_names = fields # exported field names (e.g. 'journal_id', 'account_id/name', ...)
self._groupby = groupby
self._groupby_type = groupby_type
self._read_context = read_context
self.count = 0 # Total number of records in the subtree
self.children = OrderedDict()
self.data = [] # Only leaf nodes have data
if root:
self.insert_leaf(root)
def _get_aggregate(self, field_name, data, group_operator):
def _get_aggregate(self, field_name, data, aggregator):
# 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
# aggregated with an integer or float.
data = (value for value in data if value != '')
if group_operator == 'avg':
if aggregator == 'avg':
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:
_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
if self.data:
@ -108,12 +103,12 @@ class GroupsTreeNode:
for field_name in self._export_field_names:
if 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
# e.g. line_ids/analytic_line_ids/amount
continue
field = self._model._fields[field_name]
if field.group_operator:
if field.aggregator:
aggregated_field_names.append(field_name)
return aggregated_field_names
@ -130,7 +125,7 @@ class GroupsTreeNode:
if field_name in self._get_aggregated_field_names():
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
@ -143,7 +138,7 @@ class GroupsTreeNode:
:return: the child node
"""
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]
def insert_leaf(self, group):
@ -167,30 +162,39 @@ class GroupsTreeNode:
# Update count value and aggregated value.
node.count += count
records = records.with_context(self._read_context)
node.data = records.export_data(self._export_field_names).get('datas', [])
return records
class ExportXlsxWriter:
def __init__(self, field_names, row_count=0):
self.field_names = field_names
def __init__(self, fields, columns_headers, row_count):
self.fields = fields
self.columns_headers = columns_headers
self.output = io.BytesIO()
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_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.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.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:
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):
self.write_header()
@ -201,9 +205,9 @@ class ExportXlsxWriter:
def write_header(self):
# Write main header
for i, fieldname in enumerate(self.field_names):
self.write(0, i, fieldname, self.header_style)
self.worksheet.set_column(0, max(0, len(self.field_names) - 1), 30) # around 220 pixels
for i, column_header in enumerate(self.columns_headers):
self.write(0, i, column_header, self.header_style)
self.worksheet.set_column(0, max(0, len(self.columns_headers) - 1), 30) # around 220 pixels
def close(self):
self.workbook.close()
@ -222,15 +226,15 @@ class ExportXlsxWriter:
# here. xlsxwriter does not support bytes values in Python 3 ->
# assume this is base64 and decode to a string, if this
# fails note that you can't export
cell_value = pycompat.to_text(cell_value)
cell_value = cell_value.decode()
except UnicodeDecodeError:
raise UserError(_("Binary fields can not be exported to Excel unless their content is base64-encoded. That does not seem to be the case for %s.", self.field_names)[column])
elif isinstance(cell_value, (list, tuple)):
cell_value = pycompat.to_text(cell_value)
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, dict)):
cell_value = str(cell_value)
if isinstance(cell_value, str):
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:
cell_value = cell_value.replace("\r", " ")
elif isinstance(cell_value, datetime.datetime):
@ -238,20 +242,17 @@ class ExportXlsxWriter:
elif isinstance(cell_value, datetime.date):
cell_style = self.date_style
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)
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):
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':
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)
# 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
column += 1
aggregated_value = aggregates.get(field['name'])
if field.get('type') == 'monetary':
self.header_bold_style.set_num_format(self.monetary_format)
elif field.get('type') == 'float':
self.header_bold_style.set_num_format(self.float_format)
header_style = self.header_bold_style
if field['type'] == 'monetary':
header_style = self.header_bold_style_monetary
elif field['type'] == 'float':
header_style = self.header_bold_style_float
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
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):
""" Returns all valid export formats
@ -300,87 +302,151 @@ class Export(http.Controller):
{'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]
fields = Model.fields_get()
return fields
for fname, field in fields.items():
if field.get('type') != 'properties':
continue
@http.route('/web/export/get_fields', type='json', auth="user")
def get_fields(self, model, prefix='', parent_name='',
definition_record = field['definition_record']
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,
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 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]}
else:
fields['.id'] = {**fields['id']}
fields['id']['string'] = _('External ID')
fields['id']['string'] = request.env._('External ID')
if parent_field:
parent_field['string'] = _('External ID')
if not Model._is_an_ordinary_table():
fields.pop("id", None)
elif parent_field:
parent_field['string'] = request.env._('External ID')
fields['id'] = parent_field
fields['id']['type'] = parent_field['field_type']
fields_sequence = sorted(fields.items(),
key=lambda field: odoo.tools.ustr(field[1].get('string', '').lower()))
records = []
for field_name, field in fields_sequence:
if import_compat and not field_name == 'id':
exportable_fields = {}
for field_name, field in fields.items():
if import_compat and field_name != 'id':
if exclude and field_name in exclude:
continue
if field.get('type') in ('properties', 'properties_definition'):
continue
if field.get('readonly'):
# If none of the field's states unsets readonly, skip the field
if all(dict(attrs).get('readonly', True)
for attrs in field.get('states', {}).values()):
continue
continue
if not field.get('exportable', True):
continue
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
val = ident
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
val = prefix
name = parent_name + (parent_name and '/' or '') + field['string']
record = {'id': ident, 'string': name,
'value': val, 'children': False,
'field_type': field.get('type'),
'required': field.get('required'),
'relation_field': field.get('relation_field'),
'default_export': import_compat and field.get('default_export_compatible')}
records.append(record)
field_dict = {
'id': ident,
'string': name,
'value': val,
'children': False,
'field_type': field.get('type'),
'required': field.get('required'),
'relation_field': field.get('relation_field'),
'default_export': import_compat and field.get('default_export_compatible')
}
if len(ident.split('/')) < 3 and 'relation' in field:
ref = field.pop('relation')
record['value'] += '/id'
record['params'] = {'model': ref, 'prefix': ident, 'name': name, 'parent_field': field}
record['children'] = True
field_dict['value'] += '/id'
field_dict['params'] = {
'model': field['relation'],
'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):
# TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
export = request.env['ir.exports'].browse([export_id]).read()[0]
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
]
export = request.env['ir.exports'].browse([export_id])
return self.fields_info(model, export.export_fields.mapped('name'))
def fields_info(self, model, export_fields):
info = {}
fields = self.fields_get(model)
field_info = []
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:
fields['.id'] = fields.get('id', {'string': 'ID'})
@ -418,20 +484,32 @@ class Export(http.Controller):
subfields = list(subfields)
if length == 2:
# subfields is a seq of $base/*rest, and not loaded yet
info.update(self.graft_subfields(
fields[base]['relation'], base, fields[base]['string'],
subfields
))
field_info.extend(
self.graft_subfields(
fields[base]['relation'], base, fields[base]['string'], subfields
),
)
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):
export_fields = [field.split('/', 1)[1] for field in fields]
return (
(prefix + '/' + k, prefix_string + '/' + v)
for k, v in self.fields_info(model, export_fields).items())
dict(
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):
@ -455,7 +533,7 @@ class ExportFormat(object):
model_description = request.env['ir.model']._get(base).name
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
current export class outputs
@ -466,7 +544,7 @@ class ExportFormat(object):
"""
raise NotImplementedError()
def from_group_data(self, fields, groups):
def from_group_data(self, fields, columns_headers, groups):
raise NotImplementedError()
def base(self, data):
@ -488,21 +566,24 @@ class ExportFormat(object):
if not import_compat and groupby:
groupby_type = [Model._fields[x.split(':')[0]].type for x in groupby]
domain = [('id', 'in', ids)] if ids else domain
groups_data = Model.with_context(active_test=False).read_group(domain, [x if x != '.id' else 'id' for x in field_names], groupby, lazy=False)
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),
# 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()
for leaf in groups_data:
records |= tree.insert_leaf(leaf)
response_data = self.from_group_data(fields, tree)
response_data = self.from_group_data(fields, columns_headers, tree)
else:
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', [])
response_data = self.from_data(columns_headers, export_data)
response_data = self.from_data(fields, columns_headers, export_data)
_logger.info(
"User %d exported %d %r records from %s. Fields: %s. %s: %s",
@ -522,8 +603,8 @@ class ExportFormat(object):
class CSVExport(ExportFormat, http.Controller):
@http.route('/web/export/csv', type='http', auth="user")
def index(self, data):
@http.route('/web/export/csv', type='http', auth='user')
def web_export_csv(self, data):
try:
return self.base(data)
except Exception as exc:
@ -543,31 +624,35 @@ class CSVExport(ExportFormat, http.Controller):
def extension(self):
return '.csv'
def from_group_data(self, fields, groups):
raise UserError(_("Exporting grouped data to csv is not supported."))
def from_group_data(self, fields, columns_headers, groups):
raise UserError(request.env._("Exporting grouped data to csv is not supported."))
def from_data(self, fields, rows):
fp = io.BytesIO()
writer = pycompat.csv_writer(fp, quoting=1)
def from_data(self, fields, columns_headers, rows):
fp = io.StringIO()
writer = csv.writer(fp, quoting=1)
writer.writerow(fields)
writer.writerow(columns_headers)
for data in rows:
row = []
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 -
if isinstance(d, str) and d.startswith(('=', '-', '+')):
d = "'" + d
row.append(pycompat.to_text(d))
row.append(d)
writer.writerow(row)
return fp.getvalue()
class ExcelExport(ExportFormat, http.Controller):
@http.route('/web/export/xlsx', type='http', auth="user")
def index(self, data):
@http.route('/web/export/xlsx', type='http', auth='user')
def web_export_xlsx(self, data):
try:
return self.base(data)
except Exception as exc:
@ -587,16 +672,16 @@ class ExcelExport(ExportFormat, http.Controller):
def extension(self):
return '.xlsx'
def from_group_data(self, fields, groups):
with GroupExportXlsxWriter(fields, groups.count) as xlsx_writer:
def from_group_data(self, fields, columns_headers, groups):
with GroupExportXlsxWriter(fields, columns_headers, groups.count) as xlsx_writer:
x, y = 1, 0
for group_name, group in groups.children.items():
x, y = xlsx_writer.write_group(x, y, group_name, group)
return xlsx_writer.value
def from_data(self, fields, rows):
with ExportXlsxWriter(fields, len(rows)) as xlsx_writer:
def from_data(self, fields, columns_headers, rows):
with ExportXlsxWriter(fields, columns_headers, len(rows)) as xlsx_writer:
for row_index, row in enumerate(rows):
for cell_index, cell_value in enumerate(row):
xlsx_writer.write_cell(row_index + 1, cell_index, cell_value)

View file

@ -4,16 +4,18 @@ import json
import logging
import psycopg2
import odoo
import odoo.exceptions
import odoo.modules.registry
from odoo import http
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.service import security
from odoo.tools import ustr
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__)
@ -24,6 +26,7 @@ SIGN_UP_REQUEST_PARAMS = {'db', 'login', 'debug', 'token', 'message', 'error', '
'redirect', 'redirect_hostname', 'email', 'name', 'partner_id',
'password', 'confirm_password', 'city', 'country_id', 'lang', 'signup_email'}
LOGIN_SUCCESSFUL_PARAMS = set()
CREDENTIAL_PARAMS = ['login', 'password', 'type']
class Home(http.Controller):
@ -32,19 +35,22 @@ class Home(http.Controller):
def index(self, s_action=None, db=None, **kw):
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', 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.
@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):
# Ensure we have both a database and a user
ensure_db()
if not request.session.uid:
return request.redirect('/web/login', 303)
return request.redirect_query('/web/login', query={'redirect': request.httprequest.full_path}, code=303)
if kw.get('redirect'):
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")
if not is_user_internal(request.session.uid):
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"
request.update_env(user=request.session.uid)
try:
if request.env.user:
request.env.user._on_webclient_bootstrap()
context = request.env['ir.http'].webclient_rendering_context()
response = request.render('web.webclient_bootstrap', qcontext=context)
response.headers['X-Frame-Options'] = 'DENY'
@ -62,15 +70,19 @@ class Home(http.Controller):
except AccessError:
return request.redirect('/web/login?error=access')
@http.route('/web/webclient/load_menus/<string:unique>', type='http', auth='user', methods=['GET'])
def web_load_menus(self, unique):
@http.route('/web/webclient/load_menus/<string:unique>', type='http', auth='user', methods=['GET'], readonly=True)
def web_load_menus(self, unique, lang=None):
"""
Loads the menus for the webclient
:param unique: this parameters is not used, but mandatory: it is used by the HTTP stack to make a unique request
:param lang: language in which the menus should be loaded (only works if language is installed)
:return: the menus (including the images in Base64)
"""
if lang:
request.update_context(lang=lang)
menus = request.env["ir.ui.menu"].load_web_menus(request.session.debug)
body = json.dumps(menus, default=ustr)
body = json.dumps(menus)
response = request.make_response(body, [
# 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
@ -82,7 +94,7 @@ class Home(http.Controller):
def _login_redirect(self, uid, redirect=None):
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):
ensure_db()
request.params['login_success'] = False
@ -107,9 +119,11 @@ class Home(http.Controller):
if request.httprequest.method == 'POST':
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
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:
if e.args == odoo.exceptions.AccessDenied().args:
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}
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):
uid = request.env.user.id
if request.env.user._is_system():
uid = request.session.uid = odoo.SUPERUSER_ID
# invalidate session token cache as we've changed the uid
request.env['res.users'].clear_caches()
request.env.registry.clear_cache()
request.session.session_token = security.compute_session_token(request.session, request.env)
return request.redirect(self._login_redirect(uid))
@ -165,11 +179,16 @@ class Home(http.Controller):
('Cache-Control', 'no-store')]
return request.make_response(data, headers, status=status)
@http.route(['/robots.txt'], type='http', auth="none")
def robots(self, **kwargs):
allowed_routes = self._get_allowed_robots_routes()
robots_content = ["User-agent: *", "Disallow: /"]
robots_content.extend(f"Allow: {route}" for route in allowed_routes)
return request.make_response("\n".join(robots_content), [('Content-Type', 'text/plain')])
def _get_allowed_robots_routes(self):
"""Override this method to return a list of allowed routes.
By default this controller does not serve robots.txt so all routes
are implicitly open but we want any module to be able to append
to this list, in case the website module is installed.
:return: A list of URL paths that should be allowed by robots.txt
Examples: ['/social_instagram/', '/sitemap.xml', '/web/']

View file

@ -1,54 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import warnings
from odoo import http
from odoo.tools import lazy
from odoo.addons.web.controllers import (
action, binary, database, dataset, export, home, report, session,
utils, view, webclient,
warnings.warn(
f"{__name__!r} has been deprecated since 18.0 and is completely "
"empty, all controllers and utility functions were moved to sibling "
"submodules in Odoo 16",
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.http import content_disposition, request
from odoo.tools import ustr, osutil
from odoo.tools import osutil
from odoo.tools.misc import xlsxwriter
class TableExporter(http.Controller):
@http.route('/web/pivot/check_xlsxwriter', type='json', auth='none')
def check_xlsxwriter(self):
return xlsxwriter is not None
@http.route('/web/pivot/export_xlsx', type='http', auth="user")
@http.route('/web/pivot/export_xlsx', type='http', auth="user", readonly=True)
def export_xlsx(self, data, **kw):
jdata = json.load(data) if isinstance(data, FileStorage) else json.loads(data)
output = io.BytesIO()
@ -93,7 +89,7 @@ class TableExporter(http.Controller):
# Step 4: writing data
x = 0
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']:
x = x + 1
if cell.get('is_bold', False):

View file

@ -20,7 +20,10 @@ class Profiling(Controller):
except UserError as e:
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):
# don't server speedscope index if profiling is not enabled
if not request.env['ir.profile']._enabled_until():

View file

@ -23,7 +23,7 @@ class ReportController(http.Controller):
@http.route([
'/report/<converter>/<reportname>',
'/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):
report = request.env['ir.actions.report']
context = dict(request.env.context)
@ -52,7 +52,10 @@ class ReportController(http.Controller):
#------------------------------------------------------
# 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):
"""Contoller able to render barcode images thanks to reportlab.
Samples::
@ -81,10 +84,14 @@ class ReportController(http.Controller):
except (ValueError, AttributeError):
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")
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
a pdf/controller report.
@ -133,7 +140,7 @@ class ReportController(http.Controller):
else:
return
except Exception as e:
_logger.exception("Error while generating report %s", reportname)
_logger.warning("Error while generating report %s", reportname, exc_info=True)
se = http.serialize_exception(e)
error = {
'code': 200,
@ -143,6 +150,6 @@ class ReportController(http.Controller):
res = request.make_response(html_escape(json.dumps(error)))
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):
return request.env['ir.actions.report'].get_wkhtmltopdf_state()

View file

@ -20,7 +20,7 @@ _logger = logging.getLogger(__name__)
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):
# Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@)
request.session.touch()
@ -28,10 +28,15 @@ class Session(http.Controller):
@http.route('/web/session/authenticate', type='json', auth="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]):
raise AccessError("Database not found.")
pre_uid = request.session.authenticate(db, login, password)
if pre_uid != request.session.uid:
credential = {'login': login, 'password': password, 'type': 'password'}
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
# 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}
@ -46,7 +51,7 @@ class Session(http.Controller):
http.root.session_store.rotate(request.session, env)
request.future_response.set_cookie(
'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()
@ -57,16 +62,16 @@ class Session(http.Controller):
except Exception as e:
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):
# return all installed modules. Web client is smart enough to not load a module twice
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):
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):
ICP = request.env['ir.config_parameter'].sudo()
params = {
@ -77,11 +82,11 @@ class Session(http.Controller):
}
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):
request.session.logout()
@http.route('/web/session/logout', type='http', auth="none")
def logout(self, redirect='/web'):
@http.route('/web/session/logout', type='http', auth='none', readonly=True)
def logout(self, redirect='/odoo'):
request.session.logout(keep_db=True)
return request.redirect(redirect, 303)

View file

@ -1,11 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import copy
import hashlib
import io
import collections
import logging
import re
from collections import OrderedDict, defaultdict
import babel.messages.pofile
import werkzeug
@ -13,10 +9,9 @@ import werkzeug.exceptions
import werkzeug.utils
import werkzeug.wrappers
import werkzeug.wsgi
from lxml import etree
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 import http
from odoo.http import request
@ -27,8 +22,8 @@ _logger = logging.getLogger(__name__)
def clean_action(action, env):
action_type = action.setdefault('type', 'ir.actions.act_window_close')
if action_type == 'ir.actions.act_window':
action = fix_view_modes(action)
if action_type == 'ir.actions.act_window' and not action.get('views'):
generate_views(action)
# When returning an action, keep only relevant fields/properties
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))
def fix_view_modes(action):
""" 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
# I think generate_views should go into js ActionManager
def generate_views(action):
"""
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])]
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):
""" 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
"""
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')
# partial session (MFA)
@ -212,7 +259,6 @@ def _local_web_translations(trans_file):
except Exception:
return
for x in po:
if x.id and x.string and (JAVASCRIPT_TRANSLATION_COMMENT in x.auto_comments
or WEB_TRANSLATION_COMMENT in x.auto_comments):
if x.id and x.string and JAVASCRIPT_TRANSLATION_COMMENT in x.auto_comments:
messages.append({'id': x.id, 'string': x.string})
return messages

View file

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

View file

@ -13,53 +13,17 @@ import werkzeug.wsgi
import odoo
import odoo.modules.registry
from odoo import http
from odoo.modules import get_manifest, get_resource_path
from odoo.modules import get_manifest
from odoo.http import request
from odoo.tools import lazy
from odoo.tools.misc import file_open
from odoo.tools.misc import file_path
from .utils import _local_web_translations
_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):
@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")
def bootstrap_translations(self, mods=None):
""" Load local translations from *.po files, as a temporary solution
@ -80,7 +44,7 @@ class WebClient(http.Controller):
for addon_name in mods:
manifest = get_manifest(addon_name)
if manifest and manifest['bootstrap']:
f_name = get_resource_path(addon_name, 'i18n', f'{lang}.po')
f_name = file_path(f'{addon_name}/i18n/{lang}.po')
if not f_name:
continue
translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
@ -88,7 +52,7 @@ class WebClient(http.Controller):
return {"modules": translations_per_module,
"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):
"""
Load the translations for the specified language and modules
@ -103,18 +67,19 @@ class WebClient(http.Controller):
elif mods is None:
mods = list(request.env.registry._init_modules) + (odoo.conf.server_wide_modules or [])
if lang and lang not in {code for code, _ in request.env['res.lang'].sudo().get_installed()}:
lang = None
translations_per_module, lang_params = request.env["ir.http"].get_translations_for_webclient(mods, lang)
body = json.dumps({
'lang': lang_params and lang_params["code"],
body = {
'lang': lang,
'lang_parameters': lang_params,
'modules': translations_per_module,
'multi_lang': len(request.env['res.lang'].sudo().get_installed()) > 1,
})
response = request.make_response(body, [
# 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
('Content-Type', 'application/json'),
}
# The type of the route is set to HTTP, but the rpc is made with a get and expects JSON
response = request.make_json_response(body, [
('Cache-Control', f'public, max-age={http.STATIC_CACHE_LONG}'),
])
return response
@ -123,32 +88,31 @@ class WebClient(http.Controller):
def version_info(self):
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):
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):
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")
def benchmarks(self, mod=None, **kwargs):
return request.render('web.benchmark_suite')
@http.route('/web/bundle/<string:bundle_name>', auth="public", methods=["GET"])
@http.route('/web/bundle/<string:bundle_name>', auth='public', methods=['GET'], readonly=True)
def bundle(self, bundle_name, **bundle_params):
"""
Request the definition of a bundle, including its javascript and css bundled assets
"""
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)
files = request.env["ir.qweb"]._get_asset_nodes(bundle_name, debug=debug, js=True, css=True)
data = [{
"type": tag,
"src": attrs.get("src") or attrs.get("data-src") or attrs.get('href'),
"content": content,
} for tag, attrs, content in files]
} for tag, attrs in files]
return request.make_json_response(data)

View file

@ -26,6 +26,24 @@
<field name="view_id" ref="web.external_layout_bold"/>
</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">
<field name="datas" model="res.company" eval="obj()._get_asset_style_b64()"/>
<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
msgid ""
msgstr ""
"Project-Id-Version: Odoo 16.0\n"
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-01-25 10:43+0000\n"
"POT-Creation-Date: 2024-02-07 10:24+0000\n"
"PO-Revision-Date: 2016-03-12 06:25+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: Spanish (Chile) (http://www.transifex.com/odoo/odoo-9/"
@ -25,64 +25,40 @@ msgstr ""
#. module: web
#: model_terms:ir.ui.view,arch_db:web.report_invoice_wizard_preview
msgid "<strong>Untaxed Amount</strong>"
msgstr "<strong>Total neto</strong>"
msgstr "<strong>Monto neto</strong>"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/components/action_menus.js:0
#: code:addons/web/static/src/search/action_menus/action_menus.xml:0
#: code:addons/web/static/src/views/form/status_bar_buttons/status_bar_buttons.xml:0
#, python-format
msgid "Action"
msgstr "Acción"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0
#: code:addons/web/static/src/legacy/xml/kanban.xml:0
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.js:0
#: code:addons/web/static/src/views/fields/x2many/x2many_field.js:0
#: code:addons/web/static/src/views/kanban/kanban_column_quick_create.xml:0
#: code:addons/web/static/src/views/kanban/kanban_record_quick_create.xml:0
#, python-format
msgid "Add"
msgstr "Agregar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0
#, python-format
msgid "Add: "
msgstr "Agregar: "
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/basic_fields.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.xml:0
#: code:addons/web/static/src/search/group_by_menu/custom_group_by_item.xml:0
#: code:addons/web/static/src/views/fields/daterange/daterange_field.js:0
#, python-format
#: code:addons/web/static/src/core/datetime/datetime_picker_popover.xml:0
msgid "Apply"
msgstr "Aplicar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js:0
#: code:addons/web/static/src/core/errors/error_dialogs.xml:0
#: code:addons/web/static/src/core/signature/signature_dialog.xml:0
#: code:addons/web/static/src/legacy/js/core/dialog.js:0
#: code:addons/web/static/src/legacy/js/fields/basic_fields.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_confirm_dialog.js:0
#: code:addons/web/static/src/legacy/js/views/signature_dialog.js:0
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/xml/control_panel.xml:0
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.js:0
#: code:addons/web/static/src/views/calendar/quick_create/calendar_quick_create.xml:0
#: code:addons/web/static/src/views/fields/daterange/daterange_field.js:0
#: code:addons/web/static/src/views/list/list_confirmation_dialog.xml:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.xml:0
#: code:addons/web/static/src/webclient/settings_form_view/fields/upgrade_dialog.xml:0
#: model_terms:ir.ui.view,arch_db:web.view_base_document_layout
#, python-format
msgid "Cancel"
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/dialog/dialog.xml:0
#: code:addons/web/static/src/core/domain_selector_dialog/domain_selector_dialog.xml:0
#: code:addons/web/static/src/core/errors/error_dialogs.xml:0
#: code:addons/web/static/src/core/file_viewer/file_viewer.xml:0
#: code:addons/web/static/src/core/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/notifications/notification.xml:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column_quick_create.js:0
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/legacy/js/widgets/domain_selector_dialog.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/legacy/xml/dialog.xml:0
#: code:addons/web/static/src/views/fields/dynamic_placeholder_popover.xml:0
#: code:addons/web/static/src/views/fields/relational_utils.xml:0
#: code:addons/web/static/src/views/kanban/kanban_column_examples_dialog.xml:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.xml:0
#: 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
#, python-format
msgid "Close"
msgstr "Cerrar"
@ -112,160 +86,114 @@ msgstr "Cerrar"
#. odoo-javascript
#: code:addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js:0
#: code:addons/web/static/src/legacy/js/core/dialog.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_confirm_dialog.js:0
#: code:addons/web/static/src/views/calendar/calendar_controller.js:0
#: code:addons/web/static/src/views/list/list_confirmation_dialog.js:0
#, python-format
msgid "Confirmation"
msgstr "Confirmación"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/relational_fields.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_controller.js:0
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/views/calendar/calendar_year/calendar_year_popover.xml:0
#: code:addons/web/static/src/views/calendar/quick_create/calendar_quick_create.xml:0
#: code:addons/web/static/src/views/fields/many2one/many2one_field.xml:0
#: code:addons/web/static/src/views/kanban/kanban_renderer.js:0
#, python-format
#: code:addons/web/static/src/views/kanban/kanban_record_quick_create.js:0
msgid "Create"
msgstr "Crear"
#. module: web
#. 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/views/calendar/calendar_controller.js:0
#, python-format
msgid "Day"
msgstr "Día"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/core/file_viewer/file_viewer.xml:0
#: code:addons/web/static/src/views/fields/binary/binary_field.xml:0
#: code:addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.xml:0
#, python-format
msgid "Download"
msgstr "Descargar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/legacy/xml/kanban.xml:0
#: code:addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.xml:0
#: code:addons/web/static/src/views/calendar/quick_create/calendar_quick_create.xml:0
#: code:addons/web/static/src/views/fields/binary/binary_field.xml:0
#: code:addons/web/static/src/views/fields/image/image_field.xml:0
#: code:addons/web/static/src/views/fields/pdf_viewer/pdf_viewer_field.xml:0
#: code:addons/web/static/src/views/kanban/kanban_header.js:0
#: code:addons/web/static/src/views/kanban/kanban_record_quick_create.xml:0
#, python-format
msgid "Edit"
msgstr "Editar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/basic_fields.js:0
#: code:addons/web/static/src/views/fields/email/email_field.js:0
#: model:ir.model.fields,field_description:web.field_base_document_layout__email
#: model_terms:ir.ui.view,arch_db:web.login
#, python-format
msgid "Email"
msgstr "Email"
#. module: web
#. odoo-javascript
#: 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"
msgstr "Error"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/views/list/list_controller.js:0
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/views/list/list_controller.js:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.xml:0
#, python-format
msgid "Export"
msgstr "Exportar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.js:0
#, python-format
msgid "Export Data"
msgstr "Exportar datos"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/search/favorite_menu/favorite_menu.xml:0
#, python-format
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.xml:0
msgid "Favorites"
msgstr "Favoritos"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/search/filter_menu/filter_menu.xml:0
#, python-format
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.xml:0
msgid "Filters"
msgstr "Filtros"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/views/form/form_view.js:0
#, python-format
msgid "Form"
msgstr "Formulario"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/views/graph/graph_view.js:0
#, python-format
msgid "Graph"
msgstr "Gráfico"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/search/group_by_menu/group_by_menu.xml:0
#, python-format
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.xml:0
#: code:addons/web/static/src/views/pivot/pivot_group_by_menu.xml:0
msgid "Group By"
msgstr "Agrupar por"
#. module: web
#: model:ir.model.fields,field_description:web.field_base_document_layout__id
msgid "ID"
msgstr "ID (identificación)"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/fields/basic_fields.js:0
#: code:addons/web/static/src/core/file_viewer/file_viewer.xml:0
#: code:addons/web/static/src/views/fields/attachment_image/attachment_image_field.xml:0
#: code:addons/web/static/src/views/fields/image/image_field.js:0
#: code:addons/web/static/src/views/fields/image_url/image_url_field.js:0
#: code:addons/web/static/src/views/fields/image_url/image_url_field.xml:0
#, python-format
msgid "Image"
msgstr "Imagen"
#. module: web
#. odoo-python
#: code:addons/web/controllers/session.py:0
#, python-format
msgid "Languages"
msgstr "Idiomas"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/views/list/list_view.js:0
#, python-format
msgid "List"
msgstr "Lista"
#. module: web
#: model_terms:ir.ui.view,arch_db:web.login
msgid "Log in"
@ -273,43 +201,33 @@ msgstr "Usuario"
#. module: web
#. 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/views/calendar/calendar_controller.js:0
#, python-format
msgid "Month"
msgstr "Mes"
#. module: web
#. 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.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/view_dialogs/select_create_dialog.xml:0
#, python-format
msgid "New"
msgstr "Nuevo"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_bar.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_renderer.js:0
#: code:addons/web/static/src/search/search_bar/search_bar.js:0
#: code:addons/web/static/src/views/kanban/kanban_renderer.js:0
#: code:addons/web/static/src/views/kanban/kanban_header.js:0
#: code:addons/web/static/src/views/list/list_renderer.js:0
#: code:addons/web/static/src/views/pivot/pivot_model.js:0
#, python-format
msgid "No"
msgstr "No"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/views/view_button/view_button.xml:0
#, python-format
msgid "Object:"
msgstr "Objeto:"
@ -317,30 +235,13 @@ msgstr "Objeto:"
#. odoo-javascript
#: code:addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js:0
#: code:addons/web/static/src/core/dialog/dialog.xml:0
#: code:addons/web/static/src/core/errors/error_dialogs.xml:0
#: code:addons/web/static/src/legacy/js/core/dialog.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_confirm_dialog.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/legacy/xml/control_panel.xml:0
#: code:addons/web/static/src/public/error_notifications.js:0
#: code:addons/web/static/src/views/calendar/calendar_year/calendar_year_popover.xml:0
#: code:addons/web/static/src/views/list/list_confirmation_dialog.xml:0
#: code:addons/web/static/src/views/view_dialogs/form_view_dialog.xml:0
#: code:addons/web/static/src/webclient/actions/action_dialog.xml:0
#, python-format
msgid "Ok"
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
#: model_terms:ir.ui.view,arch_db:web.login
msgid "Password"
@ -348,24 +249,13 @@ msgstr "Contraseña"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.js:0
#, python-format
msgid "Please enter save field list name"
msgstr "Por favor, introduzca el nombre de la lista de campos a guardar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#, python-format
msgid "Please select fields to export..."
msgstr "Por favor, seleccione los campos a exportar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/widgets/data_export.js:0
#: code:addons/web/static/src/views/view_dialogs/export_data_dialog.js:0
#, python-format
msgid "Please select fields to save export list..."
msgstr ""
"Por favor, seleccione los campos para guardar la lista de exportación..."
@ -373,53 +263,42 @@ msgstr ""
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/webclient/user_menu/user_menu_items.js:0
#, python-format
msgid "Preferences"
msgstr "Preferencias"
#. module: web
#. 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/cog_menu/cog_menu.xml:0
#: code:addons/web/static/src/webclient/actions/reports/report_action.xml:0
#, python-format
msgid "Print"
msgstr "Imprimir"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/search_bar/search_bar.xml:0
#: code:addons/web/static/src/views/fields/relational_utils.js:0
#: code:addons/web/static/src/views/fields/relational_utils.xml:0
#: code:addons/web/static/src/views/form/form_controller.xml:0
#, python-format
msgid "Remove"
msgstr "Eliminar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/domain_selector_dialog/domain_selector_dialog.xml:0
#: code:addons/web/static/src/legacy/js/views/view_dialogs.js:0
#: code:addons/web/static/src/legacy/js/widgets/domain_selector_dialog.js:0
#: code:addons/web/static/src/legacy/js/widgets/translation_dialog.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/favorite_menu/custom_favorite_item.xml:0
#: code:addons/web/static/src/search/custom_favorite_item/custom_favorite_item.xml:0
#: code:addons/web/static/src/views/fields/relational_utils.xml:0
#: code:addons/web/static/src/views/fields/translation_dialog.xml:0
#: code:addons/web/static/src/views/form/form_controller.xml:0
#: 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
#: model_terms:ir.ui.view,arch_db:web.view_base_document_layout
#, python-format
msgid "Save"
msgstr "Guardar"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/debug/debug_menu_items.xml:0
#, python-format
msgid "Save default"
msgstr "Guardar por defecto"
@ -427,147 +306,74 @@ msgstr "Guardar por defecto"
#. odoo-javascript
#: code:addons/web/static/src/views/graph/graph_model.js:0
#: code:addons/web/static/src/views/pivot/pivot_model.js:0
#, python-format
msgid "Total"
msgstr "Total"
#. module: web
#. odoo-python
#. odoo-javascript
#. odoo-python
#: code:addons/web/controllers/export.py: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"
msgstr "Sin definir"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/errors/error_dialogs.js:0
#: code:addons/web/static/src/legacy/js/views/basic/basic_controller.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_controller.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/favorite_menu/favorite_menu.js:0
#: code:addons/web/static/src/model/relational_model/dynamic_list.js:0
#: code:addons/web/static/src/search/search_bar_menu/search_bar_menu.js:0
#: code:addons/web/static/src/views/fields/domain/domain_field.xml:0
#: code:addons/web/static/src/views/fields/translation_button.js:0
#: code:addons/web/static/src/views/list/list_controller.js:0
#, python-format
msgid "Warning"
msgstr "Aviso"
#. module: web
#. 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/views/calendar/calendar_common/calendar_common_renderer.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"
msgstr "Semana"
#. module: web
#. 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/views/calendar/calendar_controller.js:0
#, python-format
msgid "Year"
msgstr "Año"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_bar.js:0
#: code:addons/web/static/src/legacy/js/views/kanban/kanban_column.js:0
#: code:addons/web/static/src/legacy/js/views/list/list_renderer.js:0
#: code:addons/web/static/src/legacy/xml/base.xml:0
#: code:addons/web/static/src/search/search_bar/search_bar.js:0
#: code:addons/web/static/src/views/fields/field_tooltip.xml:0
#: code:addons/web/static/src/views/kanban/kanban_renderer.js:0
#: code:addons/web/static/src/views/kanban/kanban_header.js:0
#: code:addons/web/static/src/views/list/list_renderer.js:0
#: code:addons/web/static/src/views/pivot/pivot_model.js:0
#, python-format
msgid "Yes"
msgstr "Sí"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/domain_selector/domain_selector_operators.js:0
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/legacy/js/widgets/domain_selector.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
#: code:addons/web/static/src/core/tree_editor/tree_editor_operator_editor.js:0
msgid "contains"
msgstr "contiene"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "doesn't contain"
msgstr "no contiene"
#. 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
#: code:addons/web/static/src/core/tree_editor/tree_editor_operator_editor.js:0
msgid "is"
msgstr "es"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "is equal to"
msgstr "es igual a"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/core/domain_selector/fields/domain_selector_boolean_field.js:0
#: code:addons/web/static/src/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
#: code:addons/web/static/src/core/tree_editor/tree_editor_operator_editor.js:0
msgid "is not"
msgstr "no es"
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/legacy/js/control_panel/search_utils.js:0
#: code:addons/web/static/src/search/filter_menu/custom_filter_item.js:0
#, python-format
msgid "is not equal to"
msgstr "es distinto de"
#. 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/core/domain_selector/utils.js:0
#: code:addons/web/static/src/core/tree_editor/utils.js:0
#: code:addons/web/static/src/search/search_model.js:0
#, python-format
msgid "or"
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_model
from . import ir_ui_menu
from . import ir_ui_view
from . import models
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 -*-
import markupsafe
import os
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.modules import get_resource_path
from odoo.tools import file_path, html2plaintext, is_html_empty
from odoo.tools import html2plaintext, is_html_empty, image as tools
from odoo.tools.misc import file_path
try:
import sass as libsass
@ -47,7 +47,7 @@ class BaseDocumentLayout(models.TransientModel):
if 'company_name' not in address_format:
address_format = '%(company_name)s\n' + address_format
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):
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:
if wizard.report_layout_id:
# guarantees that bin_size is always set to False,
# so the logo always contains the bin data instead of the binary size
if wizard.env.context.get('bin_size'):
wizard_with_logo = wizard.with_context(bin_size=False)
else:
wizard_with_logo = wizard
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,
})
# guarantees that bin_size is always set to False,
# so the logo always contains the bin data instead of the binary size
wizard = wizard.with_context(bin_size=False)
wizard.preview = wizard.env['ir.ui.view']._render_template(
wizard._get_preview_template(),
wizard._get_render_information(styles),
)
else:
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')
def _onchange_company_id(self):
for wizard in self:
@ -217,7 +224,7 @@ class BaseDocumentLayout(models.TransientModel):
return False, False
base_w, base_h = image.size
w = int(50 * base_w / base_h)
w = ceil(50 * base_w / base_h)
h = 50
# Converts to RGBA (if already RGBA, this is a noop)
@ -251,14 +258,6 @@ class BaseDocumentLayout(models.TransientModel):
return tools.rgb_to_hex(primary), tools.rgb_to_hex(secondary)
@api.model
def action_open_base_document_layout(self, action_ref=None):
if not action_ref:
action_ref = 'web.action_base_document_layout_configurator'
res = self.env["ir.actions.actions"]._for_xml_id(action_ref)
self.env[res["res_model"]].check_access_rights('write')
return res
def document_layout_save(self):
# meant to be overridden
return self.env.context.get('report_action') or {'type': 'ir.actions.act_window_close'}
@ -306,10 +305,10 @@ class BaseDocumentLayout(models.TransientModel):
precision = 8
output_style = 'expanded'
bootstrap_path = get_resource_path('web', 'static', 'lib', 'bootstrap', 'scss')
bootstrap_path = file_path('web/static/lib/bootstrap/scss')
try:
return libsass.compile(
compiled_css = libsass.compile(
string=scss_source,
include_paths=[
bootstrap_path,
@ -318,6 +317,7 @@ class BaseDocumentLayout(models.TransientModel):
output_style=output_style,
precision=precision,
)
return Markup(compiled_css) if isinstance(compiled_css, Markup) else compiled_css
except libsass.CompileError as e:
raise libsass.CompileError(e.args[0])

View file

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

View file

@ -19,7 +19,7 @@ class IrModel(models.Model):
accessible_models = []
not_accessible_models = []
for model in models:
if self._check_model_access(model):
if self._is_valid_for_model_selector(model):
accessible_models.append(model)
else:
not_accessible_models.append({"display_name": model, "model": model})
@ -34,9 +34,15 @@ class IrModel(models.Model):
} for model in records]
@api.model
def _check_model_access(self, model):
return (self.env.user._is_internal() and model in self.env
and self.env[model].check_access_rights("read", raise_exception=False))
def _is_valid_for_model_selector(self, model):
model = self.env.get(model)
return (
self.env.user._is_internal()
and model is not None
and model.has_access("read")
and not model._transient
and not model._abstract
)
@api.model
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
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)
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 markupsafe import Markup
from odoo import api, models
from odoo.tools import pycompat
from odoo import api, models, fields
from odoo.tools import html_escape as escape
@ -36,7 +35,7 @@ class Image(models.AbstractModel):
if max_width or max_height:
max_size = '%sx%s' % (max_width, max_height)
sha = hashlib.sha512(str(getattr(record, '__last_update')).encode('utf-8')).hexdigest()[:7]
sha = hashlib.sha512(str(getattr(record, 'write_date', fields.Datetime.now())).encode('utf-8')).hexdigest()[:7]
max_size = '' if max_size is None else '/%s' % max_size
if options.get('filename-field') and options['filename-field'] in record and record[options['filename-field']]:
@ -106,9 +105,9 @@ class Image(models.AbstractModel):
for name, value in atts.items():
if value:
img.append(' ')
img.append(escape(pycompat.to_text(name)))
img.append(escape(name))
img.append('="')
img.append(escape(pycompat.to_text(value)))
img.append(escape(value))
img.append('"')
img.append('/>')

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