mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-18 05:32:03 +02:00
vanilla 18.0
This commit is contained in:
parent
0a7ae8db93
commit
5454004ff9
1963 changed files with 1187893 additions and 919508 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/']
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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\"/>"
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue