mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-19 16:51:59 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 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: 19.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>=19.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"]
|
||||
|
|
|
|||
|
|
@ -15,12 +15,16 @@ This module provides the core of the Odoo Web Client.
|
|||
'auto_install': True,
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/web_security.xml',
|
||||
'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/memory_template.xml',
|
||||
'views/speedscope_config_wizard.xml',
|
||||
'views/neutralize_views.xml',
|
||||
'views/ir_ui_view_views.xml',
|
||||
'data/ir_attachment.xml',
|
||||
'data/report_layout.xml',
|
||||
],
|
||||
|
|
@ -37,74 +41,47 @@ 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/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/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/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 +95,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 +118,46 @@ 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/scss/ace.scss',
|
||||
'web/static/src/scss/base_document_layout.scss',
|
||||
|
||||
'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/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',
|
||||
'base/static/src/scss/res_users.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/polyfills/set.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 +172,42 @@ 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/legacy/scss/base_frontend.scss',
|
||||
'web/static/src/legacy/scss/lazyloader.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',
|
||||
|
||||
('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 +221,131 @@ 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'),
|
||||
'web/static/src/public/error_notifications.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/**/*.js',
|
||||
'web/static/src/public/**/*.xml',
|
||||
('remove', 'web/static/src/public/database_manager.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',
|
||||
|
||||
('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/mode-json.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 +356,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 +382,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,119 +417,141 @@ 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/polyfills/set.js',
|
||||
'web/static/src/public/**/*.js',
|
||||
'web/static/src/public/**/*.xml',
|
||||
'web/static/tests/public/**/*.xml',
|
||||
('remove', 'web/static/src/public/database_manager.js'),
|
||||
('remove', 'web/static/src/public/error_notifications.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/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/src/legacy/js/core/class.js',
|
||||
'web/static/src/legacy/js/public/minimal_dom.js',
|
||||
'web/static/src/legacy/js/public/public_widget.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.assets_clickbot': [
|
||||
'web/static/src/webclient/clickbot/clickbot.js',
|
||||
],
|
||||
|
||||
# Used during the transition of the web architecture
|
||||
'web.frontend_legacy_tests': [
|
||||
'web/static/tests/legacy/frontend/*.js',
|
||||
"web.chartjs_lib" : [
|
||||
'/web/static/lib/Chart/Chart.js',
|
||||
'/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
|
||||
],
|
||||
"web.fullcalendar_lib" : [
|
||||
'/web/static/lib/fullcalendar/core/index.global.js',
|
||||
'/web/static/lib/fullcalendar/core/locales-all.global.js',
|
||||
'/web/static/lib/fullcalendar/interaction/index.global.js',
|
||||
'/web/static/lib/fullcalendar/daygrid/index.global.js',
|
||||
'/web/static/lib/fullcalendar/luxon3/index.global.js',
|
||||
'/web/static/lib/fullcalendar/timegrid/index.global.js',
|
||||
'/web/static/lib/fullcalendar/list/index.global.js',
|
||||
],
|
||||
},
|
||||
'bootstrap': True, # load translations for login screen,
|
||||
'author': 'Odoo S.A.',
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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='jsonrpc', 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):
|
||||
@route('/web/action/run', type='jsonrpc', auth="user")
|
||||
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='jsonrpc', 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,69 @@ 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, autoprefix = 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,
|
||||
autoprefix=autoprefix,
|
||||
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 +157,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 +174,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 +230,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 +243,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 +257,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='jsonrpc', auth='none')
|
||||
def get_fonts(self, fontname=None):
|
||||
"""This route will return a list of base64 encoded fonts.
|
||||
|
||||
|
|
@ -276,7 +318,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:
|
||||
|
|
@ -285,7 +327,7 @@ class Binary(http.Controller):
|
|||
else:
|
||||
font_filenames = sorted([fn for fn in os.listdir(fonts_directory) if fn.endswith(supported_exts)])
|
||||
for filename in font_filenames:
|
||||
font_file = file_open(os.path.join(fonts_directory, filename), 'rb', filter_ext=supported_exts)
|
||||
font = base64.b64encode(font_file.read())
|
||||
with file_open(os.path.join(fonts_directory, filename), 'rb', filter_ext=supported_exts) as font_file:
|
||||
font = base64.b64encode(font_file.read())
|
||||
fonts.append(font)
|
||||
return fonts
|
||||
|
|
|
|||
|
|
@ -75,13 +75,17 @@ 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)
|
||||
request.session.db = name
|
||||
return request.redirect('/web')
|
||||
credential = {'login': post['login'], 'password': password, 'type': 'password'}
|
||||
with odoo.modules.registry.Registry(name).cursor() as cr:
|
||||
env = odoo.api.Environment(cr, None, {})
|
||||
request.session.authenticate(env, credential)
|
||||
request._save_session(env)
|
||||
request.session.db = name
|
||||
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 +98,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
|
||||
|
|
@ -120,19 +124,22 @@ class Database(http.Controller):
|
|||
return self._render_template(error=error)
|
||||
|
||||
@http.route('/web/database/backup', type='http', auth="none", methods=['POST'], csrf=False)
|
||||
def backup(self, master_pwd, name, backup_format='zip'):
|
||||
def backup(self, master_pwd, name, backup_format='zip', filestore=True):
|
||||
filestore = str2bool(filestore)
|
||||
insecure = odoo.tools.config.verify_admin_password('admin')
|
||||
if insecure and master_pwd:
|
||||
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 = [
|
||||
('Content-Type', 'application/octet-stream; charset=binary'),
|
||||
('Content-Disposition', content_disposition(filename)),
|
||||
]
|
||||
dump_stream = odoo.service.db.dump_db(name, None, backup_format)
|
||||
dump_stream = odoo.service.db.dump_db(name, None, backup_format, filestore)
|
||||
response = Response(dump_stream, headers=headers, direct_passthrough=True)
|
||||
return response
|
||||
except Exception as e:
|
||||
|
|
@ -140,7 +147,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:
|
||||
|
|
@ -168,7 +175,7 @@ class Database(http.Controller):
|
|||
error = "Master password update error: %s" % (str(e) or repr(e))
|
||||
return self._render_template(error=error)
|
||||
|
||||
@http.route('/web/database/list', type='json', auth='none')
|
||||
@http.route('/web/database/list', type='jsonrpc', auth='none')
|
||||
def list(self):
|
||||
"""
|
||||
Used by Mobile application for listing database
|
||||
|
|
|
|||
|
|
@ -1,73 +1,41 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from odoo import http
|
||||
from odoo.api import call_kw
|
||||
from odoo.http import request
|
||||
from odoo.service.model import get_public_method
|
||||
from odoo.service.model import call_kw
|
||||
from odoo.service.server import thread_local
|
||||
|
||||
from .utils import clean_action
|
||||
|
||||
|
||||
_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, rule, args):
|
||||
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='jsonrpc', auth="user", readonly=_call_kw_readonly)
|
||||
def call_kw(self, model, method, args, kwargs, path=None):
|
||||
return self._call_kw(model, method, args, kwargs)
|
||||
if path != f'{model}.{method}':
|
||||
thread_local.rpc_model_method = f'{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='jsonrpc', auth="user", readonly=_call_kw_readonly)
|
||||
def call_button(self, model, method, args, kwargs, path=None):
|
||||
if path != f'{model}.{method}':
|
||||
thread_local.rpc_model_method = f'{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):
|
||||
""" Re-sequences a number of records in the model, by their ids
|
||||
|
||||
The re-sequencing starts at the first model of ``ids``, the sequence
|
||||
number is incremented by one after each record and starts at ``offset``
|
||||
|
||||
:param ids: identifiers of the records to resequence, in the new sequence order
|
||||
:type ids: list(id)
|
||||
:param str field: field used for sequence specification, defaults to
|
||||
"sequence"
|
||||
:param int offset: sequence number for first record in ``ids``, allows
|
||||
starting the resequencing from an arbitrary number,
|
||||
defaults to ``0``
|
||||
"""
|
||||
m = request.env[model]
|
||||
if not m.fields_get([field]):
|
||||
return False
|
||||
# python 2.6 has no start parameter
|
||||
for i, record in enumerate(m.browse(ids)):
|
||||
record.write({field: i + offset})
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.http import Controller, request
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import SQL
|
||||
from odoo.tools.misc import mute_logger
|
||||
|
||||
|
||||
class Domain(Controller):
|
||||
|
||||
@http.route('/web/domain/validate', type='json', auth="user")
|
||||
@http.route('/web/domain/validate', type='jsonrpc', auth="user")
|
||||
def validate(self, model, domain):
|
||||
""" Parse `domain` and verify that it can be used to search on `model`
|
||||
:return: True when the domain is valid, otherwise False
|
||||
|
|
@ -21,14 +22,14 @@ class Domain(Controller):
|
|||
# go through the motions of preparing the final SQL for the domain,
|
||||
# so that anything invalid will raise an exception.
|
||||
query = Model.sudo()._search(domain)
|
||||
sql, params = query.select()
|
||||
|
||||
# Execute the search in EXPLAIN mode, to have the query parser
|
||||
# verify it. EXPLAIN will make sure the query is never actually executed
|
||||
# An alternative to EXPLAIN would be a LIMIT 0 clause, but the semantics
|
||||
# of a falsy `limit` parameter when calling _search() do not permit it.
|
||||
sql = SQL("EXPLAIN %s", query.select())
|
||||
with mute_logger('odoo.sql_db'):
|
||||
request.env.cr.execute(f"EXPLAIN {sql}", params)
|
||||
request.env.cr.execute(sql)
|
||||
return True
|
||||
except Exception: # pylint: disable=broad-except
|
||||
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
|
||||
|
|
@ -7,18 +7,14 @@ import itertools
|
|||
import json
|
||||
import logging
|
||||
import operator
|
||||
from collections import OrderedDict
|
||||
from collections import defaultdict, 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.misc import xlsxwriter
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools import osutil
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
|
@ -59,12 +55,12 @@ OPERATOR_MAPPING = {
|
|||
|
||||
class GroupsTreeNode:
|
||||
"""
|
||||
This class builds an ordered tree of groups from the result of a `read_group(lazy=False)`.
|
||||
The `read_group` returns a list of dictionnaries and each dictionnary is used to
|
||||
This class builds an ordered tree of groups from the result of a `formatted_read_group`.
|
||||
The `formatted_read_group` returns a list of dictionnaries and each dictionnary is used to
|
||||
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):
|
||||
self._model = model
|
||||
self._export_field_names = fields # exported field names (e.g. 'journal_id', 'account_id/name', ...)
|
||||
self._groupby = groupby
|
||||
|
|
@ -74,21 +70,18 @@ class GroupsTreeNode:
|
|||
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,17 +101,17 @@ 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
|
||||
|
||||
# Lazy property to memoize aggregated values of children nodes to avoid useless recomputations
|
||||
@lazy_property
|
||||
@functools.cached_property
|
||||
def aggregated_values(self):
|
||||
|
||||
aggregated_values = {}
|
||||
|
|
@ -130,7 +123,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
|
||||
|
||||
|
|
@ -138,7 +131,7 @@ class GroupsTreeNode:
|
|||
"""
|
||||
Return the child identified by `key`.
|
||||
If it doesn't exists inserts a default node and returns it.
|
||||
:param key: child key identifier (groupby value as returned by read_group,
|
||||
:param key: child key identifier (groupby value as returned by formatted_read_group,
|
||||
usually (id, display_name))
|
||||
:return: the child node
|
||||
"""
|
||||
|
|
@ -146,17 +139,14 @@ class GroupsTreeNode:
|
|||
self.children[key] = GroupsTreeNode(self._model, self._export_field_names, self._groupby, self._groupby_type)
|
||||
return self.children[key]
|
||||
|
||||
def insert_leaf(self, group):
|
||||
def insert_leaf(self, group, data):
|
||||
"""
|
||||
Build a leaf from `group` and insert it in the tree.
|
||||
:param group: dict as returned by `read_group(lazy=False)`
|
||||
:param group: dict as returned by `formatted_read_group`
|
||||
"""
|
||||
leaf_path = [group.get(groupby_field) for groupby_field in self._groupby]
|
||||
domain = group.pop('__domain')
|
||||
count = group.pop('__count')
|
||||
|
||||
records = self._model.search(domain, offset=0, limit=False, order=False)
|
||||
|
||||
# Follow the path from the top level group to the deepest
|
||||
# group which actually contains the records' data.
|
||||
node = self # root
|
||||
|
|
@ -167,30 +157,38 @@ class GroupsTreeNode:
|
|||
# Update count value and aggregated value.
|
||||
node.count += count
|
||||
|
||||
node.data = records.export_data(self._export_field_names).get('datas', [])
|
||||
return records
|
||||
node.data = data
|
||||
|
||||
|
||||
class ExportXlsxWriter:
|
||||
|
||||
def __init__(self, field_names, row_count=0):
|
||||
self.field_names = field_names
|
||||
def __init__(self, fields, columns_headers, row_count):
|
||||
import xlsxwriter # noqa: PLC0415
|
||||
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 +199,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 +220,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 +236,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,111 +271,181 @@ 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='jsonrpc', auth='user', readonly=True)
|
||||
def formats(self):
|
||||
""" Returns all valid export formats
|
||||
|
||||
:returns: for each export format, a pair of identifier and printable name
|
||||
:rtype: [(str, str)]
|
||||
"""
|
||||
try:
|
||||
import xlsxwriter # noqa: F401, PLC0415
|
||||
xlsx_error = None
|
||||
except ModuleNotFoundError:
|
||||
xlsx_error = "XlsxWriter 0.9.3 required"
|
||||
return [
|
||||
{'tag': 'xlsx', 'label': 'XLSX', 'error': None if xlsxwriter else "XlsxWriter 0.9.3 required"},
|
||||
{'tag': 'xlsx', 'label': 'XLSX', 'error': xlsx_error},
|
||||
{'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_orm.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='jsonrpc', 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='jsonrpc', 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 +483,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 +532,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 +543,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):
|
||||
|
|
@ -484,25 +561,53 @@ class ExportFormat(object):
|
|||
else:
|
||||
columns_headers = [val['label'].strip() for val in fields]
|
||||
|
||||
records = Model.browse(ids) if ids else Model.search(domain)
|
||||
|
||||
groupby = params.get('groupby')
|
||||
if not import_compat and groupby:
|
||||
export_data = records.export_data(['.id'] + field_names).get('datas', [])
|
||||
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_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)
|
||||
records = Model.browse()
|
||||
for leaf in groups_data:
|
||||
records |= tree.insert_leaf(leaf)
|
||||
if ids:
|
||||
domain = [('id', 'in', ids)]
|
||||
SearchModel = Model.with_context(active_test=False)
|
||||
else:
|
||||
SearchModel = Model
|
||||
groups_data = SearchModel.formatted_read_group(domain, groupby, ['__count', 'id:array_agg'])
|
||||
|
||||
response_data = self.from_group_data(fields, tree)
|
||||
# Build a map from record ID to its export rows
|
||||
record_rows = {}
|
||||
current_id = None
|
||||
for row in export_data:
|
||||
if row[0]: # First column is the record ID
|
||||
current_id = int(row[0])
|
||||
record_rows[current_id] = []
|
||||
record_rows[current_id].append(row[1:])
|
||||
|
||||
# To preserve the natural model order, base the data order on the result of `export_data`,
|
||||
# which comes from a `Model.search`
|
||||
|
||||
# 1. Map each record ID to its group index
|
||||
groups = [group['id:array_agg'] for group in groups_data]
|
||||
record_to_group = defaultdict(list)
|
||||
for group_index, ids in enumerate(groups):
|
||||
for record_id in ids:
|
||||
record_to_group[record_id].append(group_index)
|
||||
|
||||
# 2. Iterate on the result of `export_data` and assign each data to its right group
|
||||
grouped_rows = [[] for _ in groups]
|
||||
for record_id, rows in record_rows.items():
|
||||
for group_index in record_to_group[record_id]:
|
||||
grouped_rows[group_index].extend(rows)
|
||||
|
||||
# 3. Insert one leaf per group, providing the group information and its data
|
||||
for group_info, group_rows in zip(groups_data, grouped_rows):
|
||||
tree.insert_leaf(group_info, group_rows)
|
||||
|
||||
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,14 +627,14 @@ 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:
|
||||
_logger.exception("Exception during request handling.")
|
||||
payload = json.dumps({
|
||||
'code': 200,
|
||||
'code': 0,
|
||||
'message': "Odoo Server Error",
|
||||
'data': http.serialize_exception(exc)
|
||||
})
|
||||
|
|
@ -543,37 +648,41 @@ 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:
|
||||
_logger.exception("Exception during request handling.")
|
||||
payload = json.dumps({
|
||||
'code': 200,
|
||||
'code': 0,
|
||||
'message': "Odoo Server Error",
|
||||
'data': http.serialize_exception(exc)
|
||||
})
|
||||
|
|
@ -587,16 +696,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,18 +4,22 @@ import json
|
|||
import logging
|
||||
import psycopg2
|
||||
|
||||
|
||||
import odoo
|
||||
import odoo.api
|
||||
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 odoo.tools.misc import hmac
|
||||
from odoo.tools.translate import _, LazyTranslate
|
||||
from .utils import (
|
||||
ensure_db,
|
||||
_get_login_redirect_url,
|
||||
is_user_internal,
|
||||
)
|
||||
|
||||
_lt = LazyTranslate(__name__)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -24,6 +28,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 +37,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, rule, args):
|
||||
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,22 +63,37 @@ 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()
|
||||
|
||||
# Add the browser_cache_secret here and not in session_info() to ensure that it is only in
|
||||
# the webclient page, which is cache-control: "no-store" (see below)
|
||||
# Reuse session security related fields, to change the key when a security event
|
||||
# occurs for the user, like a password or 2FA change.
|
||||
hmac_payload = request.env.user._session_token_get_values() # already ordered
|
||||
session_info = context.get("session_info")
|
||||
session_info['browser_cache_secret'] = hmac(request.env(su=True), "browser_cache_key", hmac_payload)
|
||||
|
||||
response = request.render('web.webclient_bootstrap', qcontext=context)
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
response.headers['Cache-Control'] = 'no-store'
|
||||
return response
|
||||
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', type='http', auth='user', methods=['GET'], readonly=True)
|
||||
def web_load_menus(self, 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 +105,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, list_as_website_content=_lt("Login"))
|
||||
def web_login(self, redirect=None, **kw):
|
||||
ensure_db()
|
||||
request.params['login_success'] = False
|
||||
|
|
@ -107,9 +130,13 @@ 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')
|
||||
if request.env['res.users']._should_captcha_login(credential):
|
||||
request.env['ir.http']._verify_request_recaptcha_token('login')
|
||||
auth_info = request.session.authenticate(request.env, 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 +164,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 +192,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/']
|
||||
|
|
|
|||
354
odoo-bringout-oca-ocb-web/web/controllers/json.py
Normal file
354
odoo-bringout-oca-ocb-web/web/controllers/json.py
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
from http import HTTPStatus
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import psycopg2.errors
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from lxml import etree
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
from odoo import http
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.fields import Domain
|
||||
from odoo.http import request
|
||||
from odoo.models import check_object_name
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from .utils import get_action_triples
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebJsonController(http.Controller):
|
||||
|
||||
# for /json, the route should work in a browser, therefore type=http
|
||||
@http.route('/json/<path:subpath>', auth='user', type='http', readonly=True)
|
||||
def web_json(self, subpath, **kwargs):
|
||||
self._check_json_route_active()
|
||||
return request.redirect(
|
||||
f'/json/1/{subpath}?{urlencode(kwargs)}',
|
||||
HTTPStatus.TEMPORARY_REDIRECT
|
||||
)
|
||||
|
||||
@http.route('/json/1/<path:subpath>', auth='bearer', type='http', readonly=True)
|
||||
def web_json_1(self, subpath, **kwargs):
|
||||
"""Simple JSON representation of the views.
|
||||
|
||||
Get the JSON representation of the action/view as it would be shown
|
||||
in the web client for the same /odoo `subpath`.
|
||||
|
||||
Behaviour:
|
||||
- When, the action resolves to a pair (Action, id), `form` view_type.
|
||||
Otherwise when it resolves to (Action, None), use the given view_type
|
||||
or the preferred one.
|
||||
- View form uses `web_read`.
|
||||
- If a groupby is given, use a read group.
|
||||
Views pivot, graph redirect to a canonical URL with a groupby.
|
||||
- Otherwise use a search read.
|
||||
- If any parameter is missing, redirect to the canonical URL (one where
|
||||
all parameters are set).
|
||||
|
||||
:param subpath: Path to the (window) action to execute
|
||||
:param view_type: View type from which we generate the parameters
|
||||
:param domain: The domain for searches
|
||||
:param offset: Offset for search
|
||||
:param limit: Limit for search
|
||||
:param groupby: Comma-separated string; when set, executes a `web_read_group`
|
||||
and groups by the given fields
|
||||
:param fields: Comma-separates aggregates for the "group by" query
|
||||
:param start_date: When applicable, minimum date (inclusive bound)
|
||||
:param end_date: When applicable, maximum date (exclusive bound)
|
||||
"""
|
||||
self._check_json_route_active()
|
||||
if not request.env.user.has_group('base.group_allow_export'):
|
||||
raise AccessError(request.env._("You need export permissions to use the /json route"))
|
||||
|
||||
# redirect when the computed kwargs and the kwargs from the URL are different
|
||||
param_list = set(kwargs)
|
||||
|
||||
def check_redirect():
|
||||
# when parameters were added, redirect
|
||||
if set(param_list) == set(kwargs):
|
||||
return None
|
||||
# for domains, make chars as safe
|
||||
encoded_kwargs = urlencode(kwargs, safe="()[], '\"")
|
||||
return request.redirect(
|
||||
f'/json/1/{subpath}?{encoded_kwargs}',
|
||||
HTTPStatus.TEMPORARY_REDIRECT
|
||||
)
|
||||
|
||||
# Get the action
|
||||
env = request.env
|
||||
action, context, eval_context, record_id = self._get_action(subpath)
|
||||
model = env[action.res_model].with_context(context)
|
||||
|
||||
# Get the view
|
||||
view_type = kwargs.get('view_type')
|
||||
if not view_type and record_id:
|
||||
view_type = 'form'
|
||||
view_id, view_type = get_view_id_and_type(action, view_type)
|
||||
view = model.get_view(view_id, view_type)
|
||||
spec = model._get_fields_spec(view)
|
||||
|
||||
# Simple case: form view with record
|
||||
if view_type == 'form' or record_id:
|
||||
if redirect := check_redirect():
|
||||
return redirect
|
||||
if not record_id:
|
||||
raise BadRequest(env._("Missing record id"))
|
||||
res = model.browse(int(record_id)).web_read(spec)[0]
|
||||
return request.make_json_response(res)
|
||||
|
||||
# Find domain and limits
|
||||
domains = [safe_eval(action.domain or '[]', eval_context)]
|
||||
if 'domain' in kwargs:
|
||||
# for the user-given domain, use only literal-eval instead of safe_eval
|
||||
user_domain = ast.literal_eval(kwargs.get('domain') or '[]')
|
||||
domains.append(user_domain)
|
||||
else:
|
||||
default_domain = get_default_domain(model, action, context, eval_context)
|
||||
if default_domain and not Domain(default_domain).is_true():
|
||||
kwargs['domain'] = repr(list(default_domain))
|
||||
domains.append(default_domain)
|
||||
try:
|
||||
limit = int(kwargs.get('limit', 0)) or action.limit
|
||||
offset = int(kwargs.get('offset', 0))
|
||||
except ValueError as exc:
|
||||
raise BadRequest(exc.args[0]) from exc
|
||||
if 'offset' not in kwargs:
|
||||
kwargs['offset'] = offset
|
||||
if 'limit' not in kwargs:
|
||||
kwargs['limit'] = limit
|
||||
|
||||
# Additional info from the view
|
||||
view_tree = etree.fromstring(view['arch'])
|
||||
|
||||
# Add date domain for some view types
|
||||
if view_type in ('calendar', 'gantt', 'cohort'):
|
||||
try:
|
||||
start_date = date.fromisoformat(kwargs['start_date'])
|
||||
end_date = date.fromisoformat(kwargs['end_date'])
|
||||
except ValueError as exc:
|
||||
raise BadRequest(exc.args[0]) from exc
|
||||
except KeyError:
|
||||
start_date = end_date = None
|
||||
date_domain = get_date_domain(start_date, end_date, view_tree)
|
||||
domains.append(date_domain)
|
||||
if 'start_date' not in kwargs or end_date not in kwargs:
|
||||
kwargs.update({
|
||||
'start_date': date_domain[0][2].isoformat(),
|
||||
'end_date': date_domain[1][2].isoformat(),
|
||||
})
|
||||
|
||||
# Add explicitly activity fields for an activity view
|
||||
if view_type == 'activity':
|
||||
domains.append([('activity_ids', '!=', False)])
|
||||
# add activity fields
|
||||
for field_name, field in model._fields.items():
|
||||
if field_name.startswith('activity_') and field_name not in spec and model._has_field_access(field, 'read'):
|
||||
spec[field_name] = {}
|
||||
|
||||
# Group by
|
||||
groupby, fields = get_groupby(view_tree, kwargs.get('groupby'), kwargs.get('fields'))
|
||||
if fields:
|
||||
aggregates = [
|
||||
f"{fname}:{model._fields[fname].aggregator}" if ':' not in fname else fname
|
||||
for fname in fields
|
||||
]
|
||||
else:
|
||||
aggregates = ['__count']
|
||||
|
||||
if groupby is not None and not kwargs.get('groupby'):
|
||||
# add arguments to kwargs
|
||||
kwargs['groupby'] = ','.join(groupby)
|
||||
if 'fields' not in kwargs and fields:
|
||||
kwargs['fields'] = ','.join(fields)
|
||||
if groupby is None and fields:
|
||||
# add fields to the spec
|
||||
for field in fields:
|
||||
spec.setdefault(field, {})
|
||||
|
||||
# Last checks before the query
|
||||
if redirect := check_redirect():
|
||||
return redirect
|
||||
domain = Domain.AND(domains)
|
||||
# Reading a group or a list
|
||||
if groupby:
|
||||
res = model.web_read_group(
|
||||
domain,
|
||||
aggregates=aggregates,
|
||||
groupby=groupby,
|
||||
limit=limit,
|
||||
)
|
||||
# pop '__domain' key
|
||||
for value in res['groups']:
|
||||
del value['__extra_domain']
|
||||
else:
|
||||
res = model.web_search_read(
|
||||
domain,
|
||||
spec,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
return request.make_json_response(res)
|
||||
|
||||
def _check_json_route_active(self):
|
||||
# experimental route, only enabled in demo mode or when explicitly set
|
||||
if not (request.env.ref('base.module_base').demo
|
||||
or request.env['ir.config_parameter'].sudo().get_param('web.json.enabled')):
|
||||
raise NotFound()
|
||||
|
||||
def _get_action(self, subpath):
|
||||
def get_action_triples_():
|
||||
try:
|
||||
yield from get_action_triples(request.env, subpath, start_pos=1)
|
||||
except ValueError as exc:
|
||||
raise BadRequest(exc.args[0]) from exc
|
||||
|
||||
context = dict(request.env.context)
|
||||
active_id, action, record_id = list(get_action_triples_())[-1]
|
||||
action = action.sudo()
|
||||
if action.usage == 'ir_actions_server' and action.path:
|
||||
# force read-only evaluation of action_data
|
||||
try:
|
||||
with action.pool.cursor(readonly=True) as ro_cr:
|
||||
if not ro_cr.readonly:
|
||||
ro_cr.connection.set_session(readonly=True)
|
||||
assert ro_cr.readonly
|
||||
action_data = action.with_env(action.env(cr=ro_cr, su=False)).run()
|
||||
except psycopg2.errors.ReadOnlySqlTransaction as e:
|
||||
# never retry on RO connection, just leave
|
||||
raise AccessError(action.env._("Unsupported server action")) from e
|
||||
except ValueError as e:
|
||||
# safe_eval wraps the error into a ValueError (as str)
|
||||
if "ReadOnlySqlTransaction" not in e.args[0]:
|
||||
raise
|
||||
raise AccessError(action.env._("Unsupported server action")) from e
|
||||
# transform data into a new record
|
||||
action = action.env[action_data['type']]
|
||||
action = action.new(action_data, origin=action.browse(action_data.pop('id')))
|
||||
if action._name != 'ir.actions.act_window':
|
||||
e = f"{action._name} are not supported server-side"
|
||||
raise BadRequest(e)
|
||||
eval_context = dict(
|
||||
action._get_eval_context(action),
|
||||
active_id=active_id,
|
||||
context=context,
|
||||
allowed_company_ids=request.env.user.company_ids.ids,
|
||||
)
|
||||
# update the context and return
|
||||
context.update(safe_eval(action.context, eval_context))
|
||||
return action, context, eval_context, record_id
|
||||
|
||||
|
||||
def get_view_id_and_type(action, view_type: str | None) -> tuple[int | None, str]:
|
||||
"""Extract the view id from the action"""
|
||||
assert action._name == 'ir.actions.act_window'
|
||||
view_modes = action.view_mode.split(',')
|
||||
if not view_type:
|
||||
view_type = view_modes[0]
|
||||
|
||||
try:
|
||||
view_id = next(view_id for view_id, action_view_type in action.views if view_type == action_view_type)
|
||||
except StopIteration:
|
||||
if view_type not in view_modes:
|
||||
raise BadRequest(request.env._(
|
||||
"Invalid view type '%(view_type)s' for action id=%(action)s",
|
||||
view_type=view_type,
|
||||
action=action.id,
|
||||
)) from None
|
||||
view_id = False
|
||||
return view_id, view_type
|
||||
|
||||
|
||||
def get_default_domain(model, action, context, eval_context):
|
||||
for ir_filter in model.env['ir.filters'].get_filters(model._name, action._origin.id):
|
||||
if ir_filter['is_default']:
|
||||
# user filters, static parsing only
|
||||
domain_str = ir_filter['domain']
|
||||
domain_str = re.sub(r'\buid\b', str(model.env.uid), domain_str)
|
||||
default_domain = ast.literal_eval(domain_str)
|
||||
break
|
||||
else:
|
||||
def filters_from_context():
|
||||
view_tree = None
|
||||
for key, value in context.items():
|
||||
if key.startswith('search_default_') and value:
|
||||
filter_name = key[15:]
|
||||
if not check_object_name(filter_name):
|
||||
raise ValueError(model.env._("Invalid default search filter name for %s", key))
|
||||
if view_tree is None:
|
||||
view = model.get_view(action.search_view_id.id, 'search')
|
||||
view_tree = etree.fromstring(view['arch'])
|
||||
if (element := view_tree.find(Rf'.//filter[@name="{filter_name}"]')) is not None:
|
||||
# parse the domain
|
||||
if domain := element.attrib.get('domain'):
|
||||
yield domain
|
||||
# not parsing context['group_by']
|
||||
|
||||
default_domain = Domain.AND(
|
||||
safe_eval(domain, eval_context)
|
||||
for domain in filters_from_context()
|
||||
)
|
||||
return default_domain
|
||||
|
||||
|
||||
def get_date_domain(start_date, end_date, view_tree):
|
||||
if not start_date or not end_date:
|
||||
start_date = date.today() + relativedelta(day=1)
|
||||
end_date = start_date + relativedelta(months=1)
|
||||
date_field = view_tree.attrib.get('date_start')
|
||||
if not date_field:
|
||||
raise ValueError("Could not find the date field in the view")
|
||||
return [(date_field, '>=', start_date), (date_field, '<', end_date)]
|
||||
|
||||
|
||||
def get_groupby(view_tree, groupby=None, fields=None):
|
||||
"""Parse the given groupby and fields and fallback to the view if not provided.
|
||||
|
||||
Return the groupby as a list when given.
|
||||
Otherwise find groupby and fields from the view.
|
||||
|
||||
:param view_tree: The xml tree of the view
|
||||
:param groupby: string or None
|
||||
:param fields: string or None
|
||||
"""
|
||||
if groupby:
|
||||
groupby = groupby.split(',')
|
||||
if fields:
|
||||
fields = fields.split(',')
|
||||
else:
|
||||
fields = None
|
||||
if groupby is not None:
|
||||
return groupby, fields
|
||||
|
||||
if view_tree.tag in ('pivot', 'graph'):
|
||||
# extract groupby from the view if we don't have any
|
||||
field_by_type = defaultdict(list)
|
||||
for element in view_tree.findall(r'./field'):
|
||||
field_name = element.attrib.get('name')
|
||||
if element.attrib.get('invisible', '') in ('1', 'true'):
|
||||
field_by_type['invisible'].append(field_name)
|
||||
else:
|
||||
field_by_type[element.attrib.get('type', 'normal')].append(field_name)
|
||||
# not reading interval from the attribute
|
||||
groupby = [
|
||||
*field_by_type.get('row', ()),
|
||||
*field_by_type.get('col', ()),
|
||||
*field_by_type.get('normal', ()),
|
||||
]
|
||||
if fields is None:
|
||||
fields = field_by_type.get('measure', [])
|
||||
return groupby, fields
|
||||
if view_tree.attrib.get('default_group_by'):
|
||||
# in case the kanban view (or other) defines a default grouping
|
||||
# return the field name so it is added to the spec
|
||||
field = view_tree.attrib.get('default_group_by')
|
||||
return (None, [field] if field else [])
|
||||
return None, None
|
||||
|
|
@ -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
|
||||
|
|
|
|||
14
odoo-bringout-oca-ocb-web/web/controllers/model.py
Normal file
14
odoo-bringout-oca-ocb-web/web/controllers/model.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from json import loads, dumps
|
||||
from odoo.http import Controller, request, route
|
||||
|
||||
|
||||
class Model(Controller):
|
||||
@route("/web/model/get_definitions", methods=["POST"], type="http", auth="user")
|
||||
def get_model_definitions(self, model_names, **kwargs):
|
||||
return request.make_response(
|
||||
dumps(
|
||||
request.env["ir.model"]._get_definitions(loads(model_names)),
|
||||
)
|
||||
)
|
||||
|
|
@ -1,26 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import deque
|
||||
import io
|
||||
import json
|
||||
from collections import deque
|
||||
|
||||
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.misc import xlsxwriter
|
||||
from odoo.tools import osutil
|
||||
|
||||
|
||||
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):
|
||||
import xlsxwriter # noqa: PLC0415
|
||||
jdata = json.load(data) if isinstance(data, FileStorage) else json.loads(data)
|
||||
output = io.BytesIO()
|
||||
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
||||
|
|
@ -31,7 +25,6 @@ class TableExporter(http.Controller):
|
|||
bold = workbook.add_format({'bold': True})
|
||||
|
||||
measure_count = jdata['measure_count']
|
||||
origin_count = jdata['origin_count']
|
||||
|
||||
# Step 1: writing col group headers
|
||||
col_group_headers = jdata['col_group_headers']
|
||||
|
|
@ -45,11 +38,11 @@ class TableExporter(http.Controller):
|
|||
for header in header_row:
|
||||
while (carry and carry[0]['x'] == x):
|
||||
cell = carry.popleft()
|
||||
for j in range(measure_count * (2 * origin_count - 1)):
|
||||
for j in range(measure_count):
|
||||
worksheet.write(y, x+j, '', header_plain)
|
||||
if cell['height'] > 1:
|
||||
carry.append({'x': x, 'height': cell['height'] - 1})
|
||||
x = x + measure_count * (2 * origin_count - 1)
|
||||
x = x + measure_count
|
||||
for j in range(header['width']):
|
||||
worksheet.write(y, x + j, header['title'] if j == 0 else '', header_plain)
|
||||
if header['height'] > 1:
|
||||
|
|
@ -57,11 +50,11 @@ class TableExporter(http.Controller):
|
|||
x = x + header['width']
|
||||
while (carry and carry[0]['x'] == x):
|
||||
cell = carry.popleft()
|
||||
for j in range(measure_count * (2 * origin_count - 1)):
|
||||
for j in range(measure_count):
|
||||
worksheet.write(y, x+j, '', header_plain)
|
||||
if cell['height'] > 1:
|
||||
carry.append({'x': x, 'height': cell['height'] - 1})
|
||||
x = x + measure_count * (2 * origin_count - 1)
|
||||
x = x + measure_count
|
||||
x, y = 1, y + 1
|
||||
|
||||
# Step 2: writing measure headers
|
||||
|
|
@ -72,28 +65,15 @@ class TableExporter(http.Controller):
|
|||
for measure in measure_headers:
|
||||
style = header_bold if measure['is_bold'] else header_plain
|
||||
worksheet.write(y, x, measure['title'], style)
|
||||
for i in range(1, 2 * origin_count - 1):
|
||||
worksheet.write(y, x+i, '', header_plain)
|
||||
x = x + (2 * origin_count - 1)
|
||||
x = x + 1
|
||||
x, y = 1, y + 1
|
||||
# set minimum width of cells to 16 which is around 88px
|
||||
worksheet.set_column(0, len(measure_headers), 16)
|
||||
|
||||
# Step 3: writing origin headers
|
||||
origin_headers = jdata['origin_headers']
|
||||
|
||||
if origin_headers:
|
||||
worksheet.write(y, 0, '', header_plain)
|
||||
for origin in origin_headers:
|
||||
style = header_bold if origin['is_bold'] else header_plain
|
||||
worksheet.write(y, x, origin['title'], style)
|
||||
x = x + 1
|
||||
y = y + 1
|
||||
|
||||
# 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):
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import base64
|
||||
import json
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import Controller, request, Response, route
|
||||
from odoo.http import Controller, request, Response, route, content_disposition
|
||||
|
||||
|
||||
class Profiling(Controller):
|
||||
|
||||
|
|
@ -20,15 +22,58 @@ 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')
|
||||
def speedscope(self, profile=None):
|
||||
# don't server speedscope index if profiling is not enabled
|
||||
if not request.env['ir.profile']._enabled_until():
|
||||
return request.not_found()
|
||||
@route([
|
||||
'/web/speedscope/<profile>',
|
||||
], type='http', sitemap=False, auth='user', readonly=True)
|
||||
def speedscope(self, profile=None, action=False, **kwargs):
|
||||
profiles = request.env['ir.profile'].browse(int(p) for p in profile.split(',')).exists()
|
||||
profile_str = profile
|
||||
if not profiles:
|
||||
raise request.not_found()
|
||||
params = kwargs or profiles._default_profile_params()
|
||||
speedscope_result = profiles._generate_speedscope(profiles._parse_params(params))
|
||||
if action == 'speedscope_download_json':
|
||||
headers = [
|
||||
('Content-Type', 'application/json'),
|
||||
('X-Content-Type-Options', 'nosniff'),
|
||||
('Content-Disposition', content_disposition(f'profile_{profile_str}.json')),
|
||||
]
|
||||
return request.make_response(speedscope_result, headers)
|
||||
icp = request.env['ir.config_parameter']
|
||||
context = {
|
||||
'profile': profile,
|
||||
'profiles': profiles,
|
||||
'speedscope_base64': base64.b64encode(speedscope_result).decode('utf-8'),
|
||||
'url_root': request.httprequest.url_root,
|
||||
'cdn': icp.sudo().get_param('speedscope_cdn', "https://cdn.jsdelivr.net/npm/speedscope@1.13.0/dist/release/")
|
||||
}
|
||||
return request.render('web.view_speedscope_index', context)
|
||||
response = request.render('web.view_speedscope_index', context)
|
||||
if action == 'speedscope_download_html':
|
||||
response.headers['Content-Disposition'] = content_disposition(f'profile_{profile_str}.html')
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['Content-Type'] = 'text/html'
|
||||
return response
|
||||
|
||||
@route([
|
||||
'/web/profile_config/<profile>',
|
||||
], type='http', sitemap=False, auth='user', readonly=True)
|
||||
def profile_config(self, profile=None, action=False, **kwargs):
|
||||
profile_str = profile
|
||||
profiles = request.env['ir.profile'].browse(int(p) for p in profile_str.split(',')).exists()
|
||||
if not profiles:
|
||||
raise request.not_found()
|
||||
|
||||
if action == 'memory_open':
|
||||
memory_profile = profiles._generate_memory_profile(profiles._parse_params(kwargs))
|
||||
encoded_memory_profile = json.dumps(memory_profile).encode('utf_8')
|
||||
context = {
|
||||
'profile': profiles,
|
||||
'memory_graph': base64.b64encode(encoded_memory_profile).decode('utf-8'),
|
||||
}
|
||||
return request.render('web.view_memory', context)
|
||||
|
||||
context = {
|
||||
'default_params': profiles._default_profile_params(),
|
||||
'profile_str': profile_str,
|
||||
'profiles': profiles,
|
||||
}
|
||||
return request.render('web.config_speedscope_index', context)
|
||||
|
|
|
|||
|
|
@ -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::
|
||||
|
|
@ -68,8 +71,8 @@ class ReportController(http.Controller):
|
|||
:param height: Pixel height of the barcode
|
||||
:param humanreadable: Accepted values: 0 (default) or 1. 1 will insert the readable value
|
||||
at the bottom of the output image
|
||||
:param quiet: Accepted values: 0 (default) or 1. 1 will display white
|
||||
margins on left and right.
|
||||
:param quiet: Accepted values: 0 or 1 (default). 1 will display white
|
||||
margins on left and right for barcodes and on all sides for QR codes.
|
||||
:param mask: The mask code to be used when rendering this QR-code.
|
||||
Masks allow adding elements on top of the generated image,
|
||||
such as the Swiss cross in the center of QR-bill codes.
|
||||
|
|
@ -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,16 +140,16 @@ 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,
|
||||
'code': 0,
|
||||
'message': "Odoo Server Error",
|
||||
'data': se
|
||||
}
|
||||
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='jsonrpc', auth='user', readonly=True)
|
||||
def check_wkhtmltopdf(self):
|
||||
return request.env['ir.actions.report'].get_wkhtmltopdf_state()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import json
|
|||
import logging
|
||||
import operator
|
||||
|
||||
from contextlib import ExitStack
|
||||
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
import odoo
|
||||
|
|
@ -20,53 +22,55 @@ _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='jsonrpc', auth='user', readonly=True)
|
||||
def get_session_info(self):
|
||||
# Crapy workaround for unupdatable Odoo Mobile App iOS (Thanks Apple :@)
|
||||
request.session.touch()
|
||||
return request.env['ir.http'].session_info()
|
||||
|
||||
@http.route('/web/session/authenticate', type='json', auth="none")
|
||||
@http.route('/web/session/authenticate', type='jsonrpc', auth="none", readonly=False)
|
||||
def authenticate(self, db, login, password, base_location=None):
|
||||
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:
|
||||
# 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}
|
||||
raise AccessError("Database not found.") # pylint: disable=missing-gettext
|
||||
|
||||
request.session.db = db
|
||||
registry = odoo.modules.registry.Registry(db)
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, request.session.uid, request.session.context)
|
||||
if not request.db:
|
||||
# request._save_session would not update the session_token
|
||||
# as it lacks an environment, rotating the session myself
|
||||
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
|
||||
)
|
||||
return env['ir.http'].session_info()
|
||||
with ExitStack() as stack:
|
||||
if not request.db or request.db != db:
|
||||
# Use a new env only when no db on the request, which means the env was not set on in through `_serve_db`
|
||||
# or the db is different than the request db
|
||||
cr = stack.enter_context(odoo.modules.registry.Registry(db).cursor())
|
||||
env = odoo.api.Environment(cr, None, {})
|
||||
else:
|
||||
env = request.env
|
||||
|
||||
@http.route('/web/session/get_lang_list', type='json', auth="none")
|
||||
credential = {'login': login, 'password': password, 'type': 'password'}
|
||||
auth_info = request.session.authenticate(env, 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}
|
||||
|
||||
request.session.db = db
|
||||
request._save_session(env)
|
||||
|
||||
return env['ir.http'].with_user(request.session.uid).session_info()
|
||||
|
||||
@http.route('/web/session/get_lang_list', type='jsonrpc', auth="none")
|
||||
def get_lang_list(self):
|
||||
try:
|
||||
return http.dispatch_rpc('db', 'list_lang', []) or []
|
||||
except Exception as e:
|
||||
return {"error": e, "title": _("Languages")}
|
||||
|
||||
@http.route('/web/session/modules', type='json', auth="user")
|
||||
@http.route('/web/session/modules', type='jsonrpc', 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='jsonrpc', 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='jsonrpc', auth='user', readonly=True)
|
||||
def account(self):
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
params = {
|
||||
|
|
@ -77,11 +81,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='jsonrpc', 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
|
||||
|
|
|
|||
48
odoo-bringout-oca-ocb-web/web/controllers/vcard.py
Normal file
48
odoo-bringout-oca-ocb-web/web/controllers/vcard.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
import odoo.http as http
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import request, content_disposition
|
||||
|
||||
|
||||
class Partner(http.Controller):
|
||||
|
||||
@http.route(['/web_enterprise/partner/<model("res.partner"):partner>/vcard',
|
||||
'/web/partner/vcard'], type='http', auth="user")
|
||||
def download_vcard(self, partner_ids=None, partner=None, **kwargs):
|
||||
if importlib.util.find_spec('vobject') is None:
|
||||
raise UserError(self.env._('vobject library is not installed'))
|
||||
|
||||
if partner_ids:
|
||||
partner_ids = list(filter(None, (int(pid) for pid in partner_ids.split(',') if pid.isdigit())))
|
||||
partners = request.env['res.partner'].browse(partner_ids)
|
||||
if len(partners) > 1:
|
||||
with io.BytesIO() as buffer:
|
||||
with zipfile.ZipFile(buffer, 'w') as zipf:
|
||||
for partner in partners:
|
||||
filename = f"{partner.name or partner.email}.vcf"
|
||||
content = partner._get_vcard_file()
|
||||
zipf.writestr(filename, content)
|
||||
|
||||
return request.make_response(buffer.getvalue(), [
|
||||
('Content-Type', 'application/zip'),
|
||||
('Content-Length', len(content)),
|
||||
('Content-Disposition', content_disposition('Contacts.zip'))
|
||||
])
|
||||
|
||||
if partner or partners:
|
||||
partner = partner or partners
|
||||
content = partner._get_vcard_file()
|
||||
return request.make_response(content, [
|
||||
('Content-Type', 'text/vcard'),
|
||||
('Content-Length', len(content)),
|
||||
('Content-Disposition', content_disposition(f"{partner.name or partner.email}.vcf")),
|
||||
])
|
||||
|
||||
return request.not_found()
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
# 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):
|
||||
|
||||
@route('/web/view/edit_custom', type='json', auth="user")
|
||||
@route('/web/view/edit_custom', type='jsonrpc', auth="user")
|
||||
def edit_custom(self, custom_id, arch):
|
||||
"""
|
||||
Edit a custom view
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,21 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import werkzeug
|
||||
import werkzeug.exceptions
|
||||
import werkzeug.utils
|
||||
import werkzeug.wrappers
|
||||
import werkzeug.wsgi
|
||||
|
||||
import odoo
|
||||
import odoo.modules.registry
|
||||
import odoo.tools
|
||||
from odoo import http
|
||||
from odoo.modules import get_manifest, get_resource_path
|
||||
from odoo.modules import 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")
|
||||
@http.route('/web/webclient/bootstrap_translations', type='jsonrpc', auth="none")
|
||||
def bootstrap_translations(self, mods=None):
|
||||
""" Load local translations from *.po files, as a temporary solution
|
||||
until we have established a valid session. This is meant only
|
||||
|
|
@ -72,15 +27,15 @@ class WebClient(http.Controller):
|
|||
lang = request.env.context['lang'].partition('_')[0]
|
||||
|
||||
if mods is None:
|
||||
mods = odoo.conf.server_wide_modules or []
|
||||
mods = odoo.tools.config['server_wide_modules']
|
||||
if request.db:
|
||||
mods = request.env.registry._init_modules.union(mods)
|
||||
|
||||
translations_per_module = {}
|
||||
for addon_name in mods:
|
||||
manifest = get_manifest(addon_name)
|
||||
manifest = Manifest.for_addon(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,67 +43,73 @@ class WebClient(http.Controller):
|
|||
return {"modules": translations_per_module,
|
||||
"lang_parameters": None}
|
||||
|
||||
@http.route('/web/webclient/translations/<string:unique>', type='http', auth="public", cors="*")
|
||||
def translations(self, unique, mods=None, lang=None):
|
||||
@http.route('/web/webclient/translations', type='http', auth='public', cors='*', readonly=True)
|
||||
def translations(self, hash=None, mods=None, lang=None):
|
||||
"""
|
||||
Load the translations for the specified language and modules
|
||||
|
||||
:param unique: this parameters is not used, but mandatory: it is used by the HTTP stack to make a unique request
|
||||
:param hash: translations hash, which identifies a version of translations. This method only returns translations if their hash differs from the received one
|
||||
:param mods: the modules, a comma separated list
|
||||
:param lang: the language of the user
|
||||
:return:
|
||||
"""
|
||||
if mods:
|
||||
mods = mods.split(',')
|
||||
elif mods is None:
|
||||
mods = list(request.env.registry._init_modules) + (odoo.conf.server_wide_modules or [])
|
||||
else:
|
||||
mods = request.env.registry._init_modules.union(odoo.tools.config['server_wide_modules'])
|
||||
|
||||
translations_per_module, lang_params = request.env["ir.http"].get_translations_for_webclient(mods, lang)
|
||||
if lang and lang not in {code for code, _ in request.env['res.lang'].sudo().get_installed()}:
|
||||
lang = None
|
||||
|
||||
body = json.dumps({
|
||||
'lang': lang_params and lang_params["code"],
|
||||
'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'),
|
||||
current_hash = request.env["ir.http"].with_context(cache_translation_data=True)._get_web_translations_hash(mods, lang)
|
||||
|
||||
body = {
|
||||
'lang': lang,
|
||||
'hash': current_hash,
|
||||
}
|
||||
if current_hash != hash:
|
||||
if 'translation_data' in request.env.cr.cache:
|
||||
# ormcache of _get_web_translations_hash was cold and fill the translation_data cache
|
||||
body.update(request.env.cr.cache.pop('translation_data'))
|
||||
else:
|
||||
# ormcache of _get_web_translations_hash was hot
|
||||
translations_per_module, lang_params = request.env["ir.http"]._get_translations_for_webclient(mods, lang)
|
||||
body.update({
|
||||
'lang_parameters': lang_params,
|
||||
'modules': translations_per_module,
|
||||
'multi_lang': len(request.env['res.lang'].sudo().get_installed()) > 1,
|
||||
})
|
||||
|
||||
# The type of the route is set to HTTP, but the rpc is made with a get and expects JSON
|
||||
return request.make_json_response(body, [
|
||||
('Cache-Control', f'public, max-age={http.STATIC_CACHE_LONG}'),
|
||||
])
|
||||
return response
|
||||
|
||||
@http.route('/web/webclient/version_info', type='json', auth="none")
|
||||
@http.route('/web/webclient/version_info', type='jsonrpc', auth="none")
|
||||
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")
|
||||
def test_mobile_suite(self, mod=None, **kwargs):
|
||||
return request.render('web.qunit_mobile_suite')
|
||||
|
||||
@http.route('/web/benchmarks', type='http', auth="none")
|
||||
def benchmarks(self, mod=None, **kwargs):
|
||||
return request.render('web.benchmark_suite')
|
||||
|
||||
@http.route('/web/bundle/<string:bundle_name>', auth="public", methods=["GET"])
|
||||
@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)
|
||||
|
|
|
|||
185
odoo-bringout-oca-ocb-web/web/controllers/webmanifest.py
Normal file
185
odoo-bringout-oca-ocb-web/web/controllers/webmanifest.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import base64
|
||||
import mimetypes
|
||||
|
||||
from urllib.parse import unquote, urlencode
|
||||
|
||||
from odoo import http, modules
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.http import request
|
||||
from odoo.tools import file_open, file_path
|
||||
from odoo.tools.image import image_process
|
||||
|
||||
|
||||
class WebManifest(http.Controller):
|
||||
|
||||
def _get_shortcuts(self):
|
||||
module_names = ['mail', 'crm', 'project', 'project_todo']
|
||||
try:
|
||||
module_ids = request.env['ir.module.module'].search([('state', '=', 'installed'), ('name', 'in', module_names)]) \
|
||||
.sorted(key=lambda r: module_names.index(r["name"]))
|
||||
except AccessError:
|
||||
return []
|
||||
menu_roots = request.env['ir.ui.menu'].get_user_roots()
|
||||
datas = request.env['ir.model.data'].sudo().search([('model', '=', 'ir.ui.menu'),
|
||||
('res_id', 'in', menu_roots.ids),
|
||||
('module', 'in', module_names)])
|
||||
shortcuts = []
|
||||
for module in module_ids:
|
||||
data = datas.filtered(lambda res: res.module == module.name)
|
||||
if data:
|
||||
shortcuts.append({
|
||||
'name': module.display_name,
|
||||
'url': '/odoo?menu_id=%s' % data.mapped('res_id')[0],
|
||||
'description': module.summary,
|
||||
'icons': [{
|
||||
'sizes': '100x100',
|
||||
'src': module.icon,
|
||||
'type': mimetypes.guess_type(module.icon)[0] or 'image/png'
|
||||
}]
|
||||
})
|
||||
return shortcuts
|
||||
|
||||
def _get_webmanifest(self):
|
||||
web_app_name = request.env['ir.config_parameter'].sudo().get_param('web.web_app_name', 'Odoo')
|
||||
manifest = {
|
||||
'name': web_app_name,
|
||||
'scope': '/odoo',
|
||||
'start_url': '/odoo',
|
||||
'display': 'standalone',
|
||||
'background_color': '#714B67',
|
||||
'theme_color': '#714B67',
|
||||
'prefer_related_applications': False,
|
||||
}
|
||||
icon_sizes = ['192x192', '512x512']
|
||||
manifest['icons'] = [{
|
||||
'src': '/web/static/img/odoo-icon-%s.png' % size,
|
||||
'sizes': size,
|
||||
'type': 'image/png',
|
||||
} for size in icon_sizes]
|
||||
manifest['shortcuts'] = self._get_shortcuts()
|
||||
return manifest
|
||||
|
||||
@http.route('/web/manifest.webmanifest', type='http', auth='public', methods=['GET'], readonly=True)
|
||||
def webmanifest(self):
|
||||
""" Returns a WebManifest describing the metadata associated with a web application.
|
||||
Using this metadata, user agents can provide developers with means to create user
|
||||
experiences that are more comparable to that of a native application.
|
||||
"""
|
||||
return request.make_json_response(self._get_webmanifest(), {
|
||||
'Content-Type': 'application/manifest+json'
|
||||
})
|
||||
|
||||
@http.route('/web/service-worker.js', type='http', auth='public', methods=['GET'], readonly=True)
|
||||
def service_worker(self):
|
||||
response = request.make_response(
|
||||
self._get_service_worker_content(),
|
||||
[
|
||||
('Content-Type', 'text/javascript'),
|
||||
('Service-Worker-Allowed', '/odoo'),
|
||||
]
|
||||
)
|
||||
return response
|
||||
|
||||
def _get_service_worker_content(self):
|
||||
""" Returns a ServiceWorker javascript file scoped for the backend (aka. '/odoo')
|
||||
"""
|
||||
with file_open('web/static/src/service_worker.js') as f:
|
||||
body = f.read()
|
||||
return body
|
||||
|
||||
def _icon_path(self):
|
||||
return 'web/static/img/odoo-icon-192x192.png'
|
||||
|
||||
@http.route('/odoo/offline', type='http', auth='public', methods=['GET'], readonly=True)
|
||||
def offline(self):
|
||||
""" Returns the offline page delivered by the service worker """
|
||||
return request.render('web.webclient_offline', {
|
||||
'odoo_icon': base64.b64encode(file_open(self._icon_path(), 'rb').read())
|
||||
})
|
||||
|
||||
@http.route('/scoped_app', type='http', auth='public', methods=['GET'])
|
||||
def scoped_app(self, app_id, path='', app_name=''):
|
||||
""" Returns the app shortcut page to install the app given in parameters """
|
||||
app_name = unquote(app_name) if app_name else self._get_scoped_app_name(app_id)
|
||||
path = f"/{unquote(path)}"
|
||||
scoped_app_values = {
|
||||
'app_id': app_id,
|
||||
'apple_touch_icon': '/web/static/img/odoo-icon-ios.png',
|
||||
'app_name': app_name,
|
||||
'path': path,
|
||||
'safe_manifest_url': "/web/manifest.scoped_app_manifest?" + urlencode({
|
||||
'app_id': app_id,
|
||||
'path': path,
|
||||
'app_name': app_name
|
||||
})
|
||||
}
|
||||
return request.render('web.webclient_scoped_app', scoped_app_values)
|
||||
|
||||
@http.route('/scoped_app_icon_png', type='http', auth='public', methods=['GET'])
|
||||
def scoped_app_icon_png(self, app_id, add_padding=False):
|
||||
""" Returns an app icon created with a fixed size in PNG. It is required for Safari PWAs """
|
||||
# To begin, we take the first icon available for the app
|
||||
app_icon = self._get_scoped_app_icons(app_id)[0]
|
||||
|
||||
if app_icon['type'] == "image/svg+xml":
|
||||
# We don't handle SVG images here, let's look for the module icon if possible
|
||||
manifest = modules.Manifest.for_addon(app_id, display_warning=False)
|
||||
add_padding = True
|
||||
if manifest and manifest['icon']:
|
||||
icon_src = manifest['icon']
|
||||
else:
|
||||
icon_src = f"/{self._icon_path()}"
|
||||
else:
|
||||
icon_src = app_icon['src']
|
||||
if not add_padding:
|
||||
# A valid icon is explicitly provided, we can use it directly
|
||||
return request.redirect(app_icon['src'])
|
||||
|
||||
# Now that we have the image source, we can generate a PNG image
|
||||
with file_open(icon_src.removeprefix('/'), 'rb') as file:
|
||||
image = image_process(file.read(), size=(180, 180), expand=True, colorize=(255, 255, 255), padding=16)
|
||||
return request.make_response(image, headers=[('Content-Type', 'image/png')])
|
||||
|
||||
@http.route('/web/manifest.scoped_app_manifest', type='http', auth='public', methods=['GET'])
|
||||
def scoped_app_manifest(self, app_id, path, app_name=''):
|
||||
""" Returns a WebManifest dedicated to the scope of the given app. A custom scope and start
|
||||
url are set to make sure no other installed PWA can overlap the scope (e.g. /odoo)
|
||||
"""
|
||||
path = unquote(path)
|
||||
app_name = unquote(app_name) if app_name else self._get_scoped_app_name(app_id)
|
||||
webmanifest = {
|
||||
'icons': self._get_scoped_app_icons(app_id),
|
||||
'name': app_name,
|
||||
'scope': path,
|
||||
'start_url': path,
|
||||
'display': 'standalone',
|
||||
'background_color': '#714B67',
|
||||
'theme_color': '#714B67',
|
||||
'prefer_related_applications': False,
|
||||
'shortcuts': self._get_scoped_app_shortcuts(app_id)
|
||||
}
|
||||
return request.make_json_response(webmanifest, {
|
||||
'Content-Type': 'application/manifest+json'
|
||||
})
|
||||
|
||||
def _get_scoped_app_shortcuts(self, app_id):
|
||||
return []
|
||||
|
||||
def _get_scoped_app_name(self, app_id):
|
||||
manifest = modules.Manifest.for_addon(app_id, display_warning=False)
|
||||
if manifest:
|
||||
return manifest['name']
|
||||
return app_id
|
||||
|
||||
def _get_scoped_app_icons(self, app_id):
|
||||
try:
|
||||
file_path(f'{app_id}/static/description/icon.svg')
|
||||
src = f'{app_id}/static/description/icon.svg'
|
||||
except FileNotFoundError:
|
||||
src = self._icon_path()
|
||||
return [{
|
||||
'src': f"/{src}",
|
||||
'sizes': 'any',
|
||||
'type': mimetypes.guess_type(src)[0] or 'image/png'
|
||||
}]
|
||||
|
|
@ -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
27320
odoo-bringout-oca-ocb-web/web/i18n/es_419.po
Normal file
27320
odoo-bringout-oca-ocb-web/web/i18n/es_419.po
Normal file
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
26789
odoo-bringout-oca-ocb-web/web/i18n/ku.po
Normal file
26789
odoo-bringout-oca-ocb-web/web/i18n/ku.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
26792
odoo-bringout-oca-ocb-web/web/i18n/my.po
Normal file
26792
odoo-bringout-oca-ocb-web/web/i18n/my.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
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