mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-18 05:42:00 +02:00
19.0 vanilla
This commit is contained in:
parent
a2f74aefd8
commit
4a4d12c333
844 changed files with 212348 additions and 270090 deletions
|
|
@ -10,7 +10,6 @@ pip install odoo-bringout-oca-ocb-project
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
This addon depends on:
|
|
||||||
- analytic
|
- analytic
|
||||||
- base_setup
|
- base_setup
|
||||||
- mail
|
- mail
|
||||||
|
|
@ -21,34 +20,12 @@ This addon depends on:
|
||||||
- web_tour
|
- web_tour
|
||||||
- digest
|
- digest
|
||||||
|
|
||||||
## Manifest Information
|
|
||||||
|
|
||||||
- **Name**: Project
|
|
||||||
- **Version**: 1.3
|
|
||||||
- **Category**: Services/Project
|
|
||||||
- **License**: LGPL-3
|
|
||||||
- **Installable**: True
|
|
||||||
|
|
||||||
## Source
|
## Source
|
||||||
|
|
||||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `project`.
|
- Repository: https://github.com/OCA/OCB
|
||||||
|
- Branch: 19.0
|
||||||
|
- Path: addons/project
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
This package preserves the original LGPL-3 license.
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- Overview: doc/OVERVIEW.md
|
|
||||||
- Architecture: doc/ARCHITECTURE.md
|
|
||||||
- Models: doc/MODELS.md
|
|
||||||
- Controllers: doc/CONTROLLERS.md
|
|
||||||
- Wizards: doc/WIZARDS.md
|
|
||||||
- Reports: doc/REPORTS.md
|
|
||||||
- Security: doc/SECURITY.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
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@ from . import controllers
|
||||||
from . import models
|
from . import models
|
||||||
from . import report
|
from . import report
|
||||||
from . import wizard
|
from . import wizard
|
||||||
from . import populate
|
|
||||||
|
|
||||||
from odoo import api, SUPERUSER_ID
|
from odoo.tools.sql import create_index, make_identifier
|
||||||
from odoo.tools.sql import create_index
|
|
||||||
|
|
||||||
|
|
||||||
def _check_exists_collaborators_for_project_sharing(env):
|
def _check_exists_collaborators_for_project_sharing(env):
|
||||||
|
|
@ -22,22 +20,23 @@ def _check_exists_collaborators_for_project_sharing(env):
|
||||||
env['project.collaborator']._toggle_project_sharing_portal_rules(True)
|
env['project.collaborator']._toggle_project_sharing_portal_rules(True)
|
||||||
|
|
||||||
|
|
||||||
def _project_post_init(cr, registry):
|
def _project_post_init(env):
|
||||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
|
||||||
_check_exists_collaborators_for_project_sharing(env)
|
_check_exists_collaborators_for_project_sharing(env)
|
||||||
|
|
||||||
# Index to improve the performance of burndown chart.
|
# Index to improve the performance of burndown chart.
|
||||||
project_task_stage_field_id = env['ir.model.fields']._get_ids('project.task').get('stage_id')
|
project_task_stage_field_id = env['ir.model.fields']._get_ids('project.task').get('stage_id')
|
||||||
create_index(
|
create_index(
|
||||||
cr,
|
env.cr,
|
||||||
'mail_tracking_value_mail_message_id_old_value_integer_task_stage',
|
make_identifier('mail_tracking_value_mail_message_id_old_value_integer_task_stage'),
|
||||||
env['mail.tracking.value']._table,
|
env['mail.tracking.value']._table,
|
||||||
['mail_message_id', 'old_value_integer'],
|
['mail_message_id', 'old_value_integer'],
|
||||||
where=f'field={project_task_stage_field_id}'
|
where=f'field_id={project_task_stage_field_id}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def _project_uninstall_hook(cr, registry):
|
# Create analytic plan fields on project model for existing plans
|
||||||
|
env['account.analytic.plan'].search([])._sync_plan_column('project.project')
|
||||||
|
|
||||||
|
def _project_uninstall_hook(env):
|
||||||
"""Since the m2m table for the project share wizard's `partner_ids` field is not dropped at uninstall, it is
|
"""Since the m2m table for the project share wizard's `partner_ids` field is not dropped at uninstall, it is
|
||||||
necessary to ensure it is emptied, else re-installing the module will fail due to foreign keys constraints."""
|
necessary to ensure it is emptied, else re-installing the module will fail due to foreign keys constraints."""
|
||||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
|
||||||
env['project.share.wizard'].search([("partner_ids", "!=", False)]).partner_ids = False
|
env['project.share.wizard'].search([("partner_ids", "!=", False)]).partner_ids = False
|
||||||
|
|
|
||||||
|
|
@ -24,30 +24,40 @@
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'security/ir.model.access.xml',
|
'security/ir.model.access.xml',
|
||||||
'data/digest_data.xml',
|
'data/digest_data.xml',
|
||||||
'report/project_report_views.xml',
|
|
||||||
'report/project_task_burndown_chart_report_views.xml',
|
'report/project_task_burndown_chart_report_views.xml',
|
||||||
'views/analytic_views.xml',
|
'views/account_analytic_account_views.xml',
|
||||||
'views/digest_views.xml',
|
'views/digest_digest_views.xml',
|
||||||
'views/rating_rating_views.xml',
|
'views/rating_rating_views.xml',
|
||||||
'views/project_update_views.xml',
|
'views/project_update_views.xml',
|
||||||
'views/project_update_templates.xml',
|
'views/project_update_templates.xml',
|
||||||
'views/project_project_stage_views.xml',
|
'views/project_project_stage_views.xml',
|
||||||
'wizard/project_share_wizard_views.xml',
|
'wizard/project_share_wizard_views.xml',
|
||||||
'views/project_collaborator_views.xml',
|
'views/project_task_type_views.xml',
|
||||||
'views/project_views.xml',
|
'views/project_project_views.xml',
|
||||||
|
'views/project_task_views.xml',
|
||||||
|
'views/project_role_views.xml',
|
||||||
|
'views/project_tags_views.xml',
|
||||||
'views/project_milestone_views.xml',
|
'views/project_milestone_views.xml',
|
||||||
'views/res_partner_views.xml',
|
'views/res_partner_views.xml',
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'views/mail_activity_views.xml',
|
'views/mail_activity_plan_views.xml',
|
||||||
'views/project_sharing_views.xml',
|
'views/mail_activity_type_views.xml',
|
||||||
'views/project_portal_templates.xml',
|
'views/project_sharing_project_task_views.xml',
|
||||||
|
'views/project_portal_project_project_templates.xml',
|
||||||
|
'views/project_portal_project_task_templates.xml',
|
||||||
'views/project_task_templates.xml',
|
'views/project_task_templates.xml',
|
||||||
'views/project_sharing_templates.xml',
|
'views/project_sharing_project_task_templates.xml',
|
||||||
|
'report/project_report_views.xml',
|
||||||
'data/ir_cron_data.xml',
|
'data/ir_cron_data.xml',
|
||||||
'data/mail_message_subtype_data.xml',
|
'data/mail_message_subtype_data.xml',
|
||||||
'data/mail_template_data.xml',
|
'data/mail_template_data.xml',
|
||||||
'data/project_data.xml',
|
'data/project_data.xml',
|
||||||
|
'data/project_tour.xml',
|
||||||
'wizard/project_task_type_delete_views.xml',
|
'wizard/project_task_type_delete_views.xml',
|
||||||
|
'wizard/project_task_share_wizard_views.xml',
|
||||||
|
'wizard/project_project_stage_delete_views.xml',
|
||||||
|
'wizard/project_template_create_wizard.xml',
|
||||||
|
'views/project_menus.xml',
|
||||||
],
|
],
|
||||||
'demo': [
|
'demo': [
|
||||||
'data/mail_template_demo.xml',
|
'data/mail_template_demo.xml',
|
||||||
|
|
@ -60,32 +70,48 @@
|
||||||
'assets': {
|
'assets': {
|
||||||
'web.assets_backend': [
|
'web.assets_backend': [
|
||||||
'project/static/src/css/project.css',
|
'project/static/src/css/project.css',
|
||||||
|
'project/static/src/core/web/**/*',
|
||||||
'project/static/src/utils/**/*',
|
'project/static/src/utils/**/*',
|
||||||
'project/static/src/services/**/*',
|
'project/static/src/actions/client_actions.js',
|
||||||
'project/static/src/components/**/*',
|
'project/static/src/components/**/*',
|
||||||
'project/static/src/views/**/*',
|
'project/static/src/views/**/*',
|
||||||
'project/static/src/js/project_activity.js',
|
|
||||||
'project/static/src/js/project_control_panel.js',
|
|
||||||
'project/static/src/js/project_graph_view.js',
|
|
||||||
'project/static/src/js/project_pivot_view.js',
|
|
||||||
'project/static/src/js/project_rating_graph_view.js',
|
|
||||||
'project/static/src/js/project_rating_pivot_view.js',
|
|
||||||
'project/static/src/js/project_task_kanban_examples.js',
|
|
||||||
'project/static/src/js/tours/project.js',
|
'project/static/src/js/tours/project.js',
|
||||||
'project/static/src/js/widgets/*',
|
|
||||||
'project/static/src/scss/project_dashboard.scss',
|
'project/static/src/scss/project_dashboard.scss',
|
||||||
'project/static/src/scss/project_form.scss',
|
'project/static/src/scss/project_form.scss',
|
||||||
'project/static/src/scss/project_widgets.scss',
|
'project/static/src/scss/project_widgets.scss',
|
||||||
'project/static/src/xml/**/*',
|
'project/static/src/xml/**/*',
|
||||||
|
('remove', 'project/static/src/views/project_task_activity/**'),
|
||||||
|
('remove', 'project/static/src/views/project_task_graph/**'),
|
||||||
|
('remove', 'project/static/src/views/project_task_pivot/**'),
|
||||||
|
('remove', 'project/static/src/views/project_task_analysis_renderer_mixin.js'),
|
||||||
|
('remove', 'project/static/src/views/project_task_analysis_graph/**'),
|
||||||
|
('remove', 'project/static/src/views/project_task_analysis_pivot/**'),
|
||||||
|
('remove', 'project/static/src/views/burndown_chart/**'),
|
||||||
|
('remove', 'project/static/src/**/*.dark.scss'),
|
||||||
|
('remove', 'project/static/src/views/project_project_activity/**'),
|
||||||
|
],
|
||||||
|
"web.assets_web_dark": [
|
||||||
|
'project/static/src/**/*.dark.scss',
|
||||||
|
],
|
||||||
|
'web.assets_backend_lazy': [
|
||||||
|
'project/static/src/views/project_task_model_mixin',
|
||||||
|
'project/static/src/views/project_task_activity/**',
|
||||||
|
'project/static/src/views/project_task_graph/**',
|
||||||
|
'project/static/src/views/project_task_pivot/**',
|
||||||
|
'project/static/src/views/project_task_analysis_renderer_mixin.js',
|
||||||
|
'project/static/src/views/project_task_analysis_graph/**',
|
||||||
|
'project/static/src/views/project_task_analysis_pivot/**',
|
||||||
|
'project/static/src/views/burndown_chart/**',
|
||||||
|
'project/static/src/views/project_project_activity/**',
|
||||||
],
|
],
|
||||||
'web.assets_frontend': [
|
'web.assets_frontend': [
|
||||||
'project/static/src/scss/portal_rating.scss',
|
'project/static/src/scss/portal_rating.scss',
|
||||||
'project/static/src/scss/project_sharing_frontend.scss',
|
'project/static/src/interactions/*',
|
||||||
'project/static/src/js/portal_rating.js',
|
|
||||||
],
|
],
|
||||||
'web.qunit_suite_tests': [
|
'web.assets_unit_tests': [
|
||||||
'project/static/src/project_sharing/components/portal_file_input/portal_file_input.js',
|
'project/static/tests/mock_server/**/*',
|
||||||
'project/static/tests/**/*.js',
|
'project/static/tests/project_models.js',
|
||||||
|
'project/static/tests/**/*.test.js',
|
||||||
],
|
],
|
||||||
'web.assets_tests': [
|
'web.assets_tests': [
|
||||||
'project/static/tests/tours/**/*',
|
'project/static/tests/tours/**/*',
|
||||||
|
|
@ -96,30 +122,80 @@
|
||||||
|
|
||||||
'web/static/src/scss/pre_variables.scss',
|
'web/static/src/scss/pre_variables.scss',
|
||||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||||
|
'web/static/lib/bootstrap/scss/_variables-dark.scss',
|
||||||
|
'web/static/lib/bootstrap/scss/_maps.scss',
|
||||||
|
('include', 'web._assets_bootstrap_backend'),
|
||||||
|
|
||||||
('include', 'web._assets_bootstrap'),
|
'web/static/src/libs/fontawesome/css/font-awesome.css',
|
||||||
|
'web/static/lib/odoo_ui_icons/*',
|
||||||
|
'web/static/src/webclient/navbar/navbar.scss',
|
||||||
|
'web/static/src/scss/animation.scss',
|
||||||
|
'web/static/src/core/color_picker/color_picker.scss',
|
||||||
|
'web/static/src/scss/mimetypes.scss',
|
||||||
|
'web/static/src/scss/ui.scss',
|
||||||
|
'web/static/src/views/fields/translation_dialog.scss',
|
||||||
|
'web/static/src/scss/fontawesome_overridden.scss',
|
||||||
|
|
||||||
|
'web/static/src/module_loader.js',
|
||||||
|
'web/static/src/session.js',
|
||||||
|
|
||||||
|
'web/static/lib/luxon/luxon.js',
|
||||||
|
'web/static/lib/owl/owl.js',
|
||||||
|
'web/static/lib/owl/odoo_module.js',
|
||||||
|
'web/static/lib/jquery/jquery.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',
|
||||||
|
'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',
|
||||||
|
'web/static/lib/dompurify/DOMpurify.js',
|
||||||
|
'web/static/src/libs/bootstrap.js',
|
||||||
|
'web/static/src/legacy/js/libs/jquery.js',
|
||||||
|
|
||||||
'base/static/src/css/modules.css',
|
'base/static/src/css/modules.css',
|
||||||
|
|
||||||
'web/static/src/core/utils/transitions.scss',
|
'web/static/src/core/utils/transitions.scss',
|
||||||
'web/static/src/core/**/*',
|
'web/static/src/core/**/*',
|
||||||
|
'web/static/src/model/**/*',
|
||||||
'web/static/src/search/**/*',
|
'web/static/src/search/**/*',
|
||||||
'web/static/src/webclient/icons.scss', # variables required in list_controller.scss
|
'web/static/src/webclient/icons.scss', # variables required in list_controller.scss
|
||||||
'web/static/src/views/*.js',
|
'web/static/src/views/**/*.js',
|
||||||
'web/static/src/views/*.xml',
|
'web/static/src/views/*.xml',
|
||||||
'web/static/src/views/*.scss',
|
'web/static/src/views/*.scss',
|
||||||
'web/static/src/views/fields/**/*',
|
'web/static/src/views/fields/**/*',
|
||||||
('remove', 'web/static/src/views/fields/journal_dashboard_graph/**/*'), # only works with graph view in assets
|
|
||||||
'web/static/src/views/form/**/*',
|
'web/static/src/views/form/**/*',
|
||||||
'web/static/src/views/kanban/**/*',
|
'web/static/src/views/kanban/**/*',
|
||||||
'web/static/src/views/list/**/*',
|
'web/static/src/views/list/**/*',
|
||||||
'web/static/src/views/view_button/**/*',
|
'web/static/src/views/view_button/**/*',
|
||||||
|
'web/static/src/views/view_components/**/*',
|
||||||
'web/static/src/views/view_dialogs/**/*',
|
'web/static/src/views/view_dialogs/**/*',
|
||||||
'web/static/src/views/widgets/**/*',
|
'web/static/src/views/widgets/**/*',
|
||||||
'web/static/src/webclient/**/*',
|
'web/static/src/webclient/**/*',
|
||||||
('remove', 'web/static/src/webclient/navbar/navbar.scss'), # already in assets_common
|
|
||||||
('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded
|
('remove', 'web/static/src/webclient/clickbot/clickbot.js'), # lazy loaded
|
||||||
('remove', 'web/static/src/views/form/button_box/*.scss'),
|
('remove', 'web/static/src/views/form/button_box/*.scss'),
|
||||||
|
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
|
||||||
|
|
||||||
# remove the report code and whitelist only what's needed
|
# remove the report code and whitelist only what's needed
|
||||||
('remove', 'web/static/src/webclient/actions/reports/**/*'),
|
('remove', 'web/static/src/webclient/actions/reports/**/*'),
|
||||||
|
|
@ -128,99 +204,44 @@
|
||||||
|
|
||||||
'web/static/src/env.js',
|
'web/static/src/env.js',
|
||||||
|
|
||||||
'web/static/lib/jquery.scrollTo/jquery.scrollTo.js',
|
|
||||||
'web/static/lib/py.js/lib/py.js',
|
|
||||||
'web/static/lib/py.js/lib/py_extras.js',
|
|
||||||
'web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js',
|
|
||||||
|
|
||||||
'web/static/src/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',
|
|
||||||
|
|
||||||
'base/static/src/scss/res_partner.scss',
|
'base/static/src/scss/res_partner.scss',
|
||||||
|
|
||||||
# Form style should be computed before
|
# Form style should be computed before
|
||||||
'web/static/src/views/form/button_box/*.scss',
|
'web/static/src/views/form/button_box/*.scss',
|
||||||
|
|
||||||
'web/static/src/legacy/action_adapters.js',
|
'bus/static/src/**/*.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_promise_error_handler.js',
|
|
||||||
'web/static/src/legacy/legacy_rpc_error_handler.js',
|
|
||||||
'web/static/src/legacy/root_widget.js',
|
|
||||||
'web/static/src/legacy/legacy_setup.js',
|
|
||||||
'web/static/src/legacy/root_widget.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/data.js',
|
|
||||||
'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/abstract_field.js',
|
|
||||||
'web/static/src/legacy/js/fields/abstract_field_owl.js',
|
|
||||||
'web/static/src/legacy/js/_deprecated/basic_fields.js',
|
|
||||||
'web/static/src/legacy/js/fields/basic_fields.js',
|
|
||||||
'web/static/src/legacy/js/fields/basic_fields_owl.js',
|
|
||||||
'web/static/src/legacy/js/fields/field_utils.js',
|
|
||||||
'web/static/src/legacy/js/fields/relational_fields.js',
|
|
||||||
'web/static/src/legacy/js/fields/special_fields.js',
|
|
||||||
'web/static/src/legacy/js/fields/field_registry.js',
|
|
||||||
'web/static/src/legacy/js/fields/field_registry_owl.js',
|
|
||||||
'web/static/src/legacy/js/fields/field_utils.js',
|
|
||||||
'web/static/src/legacy/js/fields/field_wrapper.js',
|
|
||||||
'web/static/src/legacy/js/views/sample_server.js',
|
|
||||||
'web/static/src/legacy/js/views/abstract_model.js',
|
|
||||||
'web/static/src/legacy/js/views/basic/basic_model.js',
|
|
||||||
'web/static/src/legacy/js/views/action_model.js',
|
|
||||||
'web/static/src/legacy/js/views/view_utils.js',
|
|
||||||
'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/model_field_selector.js',
|
|
||||||
'web/static/src/legacy/js/widgets/model_field_selector_popover.js',
|
|
||||||
'web/static/src/legacy/js/env.js',
|
|
||||||
'web/static/src/legacy/js/model.js',
|
|
||||||
'web/static/src/legacy/js/owl_compatibility.js',
|
|
||||||
|
|
||||||
'web_editor/static/src/components/**/*',
|
# To be able to launch tour js in project sharing
|
||||||
'web_editor/static/src/scss/web_editor.common.scss',
|
'web_tour/static/src/js/**/*',
|
||||||
'web_editor/static/src/scss/web_editor.backend.scss',
|
'web_tour/static/src/tour_utils.js',
|
||||||
|
'web/static/lib/hoot-dom/**/*',
|
||||||
|
|
||||||
'web_editor/static/src/js/wysiwyg/dialog.js',
|
('include', 'html_editor.assets_editor'),
|
||||||
'web_editor/static/src/js/frontend/loader.js',
|
'html_editor/static/src/others/dynamic_placeholder_plugin.js',
|
||||||
'web_editor/static/src/js/backend/**/*',
|
'html_editor/static/src/backend/**/*',
|
||||||
'web_editor/static/src/xml/backend.xml',
|
'html_editor/static/src/fields/**/*',
|
||||||
|
|
||||||
'mail/static/src/scss/variables/*.scss',
|
'mail/static/src/scss/variables/*.scss',
|
||||||
'mail/static/src/widgets/**/*.scss',
|
'mail/static/src/chatter/web/form_renderer.scss',
|
||||||
|
'mail/static/src/views/fields/**/*',
|
||||||
|
|
||||||
'project/static/src/components/project_task_name_with_subtask_count_char_field/*',
|
'project/static/src/components/project_task_name_with_subtask_count_char_field/*',
|
||||||
|
'project/static/src/components/project_task_state_selection/*',
|
||||||
|
'project/static/src/components/project_many2one_field/*',
|
||||||
'project/static/src/views/project_task_form/*.scss',
|
'project/static/src/views/project_task_form/*.scss',
|
||||||
|
'project/static/src/views/project_task_kanban/*.scss',
|
||||||
|
'project/static/src/views/project_task_control_panel/*',
|
||||||
|
'project/static/src/views/project_task_model_mixin.js',
|
||||||
|
'project/static/src/views/project_task_relational_model.js',
|
||||||
|
'project/static/src/views/project_model_mixin.js',
|
||||||
|
'project/static/src/views/project_relational_model.js',
|
||||||
|
|
||||||
'project/static/src/project_sharing/search/favorite_menu/custom_favorite_item.xml',
|
('include', 'portal.assets_chatter_helpers'),
|
||||||
|
'portal/static/src/chatter/core/**/*',
|
||||||
'project/static/src/project_sharing/**/*',
|
'project/static/src/project_sharing/**/*',
|
||||||
'web/static/src/start.js',
|
'web/static/src/start.js',
|
||||||
'web/static/src/legacy/legacy_setup.js',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
'author': 'Odoo S.A.',
|
||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import portal
|
from . import portal
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
|
|
||||||
from odoo import conf, http, _
|
from odoo import http, _
|
||||||
from odoo.exceptions import AccessError, MissingError
|
from odoo.exceptions import AccessError, MissingError, UserError
|
||||||
|
from odoo.fields import Domain
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
|
|
||||||
from odoo.tools import groupby as groupbyelem
|
from odoo.tools import groupby as groupbyelem
|
||||||
|
|
||||||
from odoo.osv.expression import OR, AND
|
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
|
||||||
|
|
||||||
|
|
||||||
class ProjectCustomerPortal(CustomerPortal):
|
class ProjectCustomerPortal(CustomerPortal):
|
||||||
|
|
@ -19,10 +21,10 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
values = super()._prepare_home_portal_values(counters)
|
values = super()._prepare_home_portal_values(counters)
|
||||||
if 'project_count' in counters:
|
if 'project_count' in counters:
|
||||||
values['project_count'] = request.env['project.project'].search_count([]) \
|
values['project_count'] = request.env['project.project'].search_count([]) \
|
||||||
if request.env['project.project'].check_access_rights('read', raise_exception=False) else 0
|
if request.env['project.project'].has_access('read') else 0
|
||||||
if 'task_count' in counters:
|
if 'task_count' in counters:
|
||||||
values['task_count'] = request.env['project.task'].search_count([('project_id', '!=', False)]) \
|
values['task_count'] = request.env['project.task'].search_count([('project_id', '!=', False)])\
|
||||||
if request.env['project.task'].check_access_rights('read', raise_exception=False) else 0
|
if request.env['project.task'].has_access('read') else 0
|
||||||
return values
|
return values
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
@ -33,7 +35,7 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
domain = [('project_id', '=', project.id)]
|
domain = [('project_id', '=', project.id)]
|
||||||
# pager
|
# pager
|
||||||
url = "/my/projects/%s" % project.id
|
url = "/my/projects/%s" % project.id
|
||||||
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby, url, domain, su=bool(access_token))
|
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby, url, domain, su=bool(access_token) and request.env.user.has_group('base.group_public'), project=project)
|
||||||
# adding the access_token to the pager's url args,
|
# adding the access_token to the pager's url args,
|
||||||
# so we are not prompted for loging when switching pages
|
# so we are not prompted for loging when switching pages
|
||||||
# if access_token is None, the arg is not present in the URL
|
# if access_token is None, the arg is not present in the URL
|
||||||
|
|
@ -45,7 +47,9 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
page_name='project',
|
page_name='project',
|
||||||
pager=pager,
|
pager=pager,
|
||||||
project=project,
|
project=project,
|
||||||
|
multiple_projects=False,
|
||||||
task_url=f'projects/{project.id}/task',
|
task_url=f'projects/{project.id}/task',
|
||||||
|
preview_object=project,
|
||||||
)
|
)
|
||||||
# default value is set to 'project' in _prepare_tasks_values, so we have to set it to 'none' here.
|
# default value is set to 'project' in _prepare_tasks_values, so we have to set it to 'none' here.
|
||||||
if not groupby:
|
if not groupby:
|
||||||
|
|
@ -54,7 +58,7 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
return self._get_page_view_values(project, access_token, values, 'my_projects_history', False, **kwargs)
|
return self._get_page_view_values(project, access_token, values, 'my_projects_history', False, **kwargs)
|
||||||
|
|
||||||
def _prepare_project_domain(self):
|
def _prepare_project_domain(self):
|
||||||
return []
|
return [('is_template', '=', False)]
|
||||||
|
|
||||||
def _prepare_searchbar_sortings(self):
|
def _prepare_searchbar_sortings(self):
|
||||||
return {
|
return {
|
||||||
|
|
@ -69,8 +73,8 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
domain = self._prepare_project_domain()
|
domain = self._prepare_project_domain()
|
||||||
|
|
||||||
searchbar_sortings = self._prepare_searchbar_sortings()
|
searchbar_sortings = self._prepare_searchbar_sortings()
|
||||||
if not sortby or sortby not in searchbar_sortings:
|
if not sortby:
|
||||||
sortby = 'date'
|
sortby = 'name'
|
||||||
order = searchbar_sortings[sortby]['order']
|
order = searchbar_sortings[sortby]['order']
|
||||||
|
|
||||||
if date_begin and date_end:
|
if date_begin and date_end:
|
||||||
|
|
@ -103,56 +107,41 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
})
|
})
|
||||||
return request.render("project.portal_my_projects", values)
|
return request.render("project.portal_my_projects", values)
|
||||||
|
|
||||||
@http.route(['/my/project/<int:project_id>',
|
|
||||||
'/my/project/<int:project_id>/page/<int:page>',
|
|
||||||
'/my/project/<int:project_id>/task/<int:task_id>',
|
|
||||||
'/my/project/<int:project_id>/project_sharing'], type='http', auth="public")
|
|
||||||
def portal_project_routes_outdated(self, **kwargs):
|
|
||||||
""" Redirect the outdated routes to the new routes. """
|
|
||||||
return request.redirect(request.httprequest.full_path.replace('/my/project/', '/my/projects/'))
|
|
||||||
|
|
||||||
@http.route(['/my/task',
|
|
||||||
'/my/task/page/<int:page>',
|
|
||||||
'/my/task/<int:task_id>'], type='http', auth='public')
|
|
||||||
def portal_my_task_routes_outdated(self, **kwargs):
|
|
||||||
""" Redirect the outdated routes to the new routes. """
|
|
||||||
return request.redirect(request.httprequest.full_path.replace('/my/task', '/my/tasks'))
|
|
||||||
|
|
||||||
@http.route(['/my/projects/<int:project_id>', '/my/projects/<int:project_id>/page/<int:page>'], type='http', auth="public", website=True)
|
@http.route(['/my/projects/<int:project_id>', '/my/projects/<int:project_id>/page/<int:page>'], type='http', auth="public", website=True)
|
||||||
def portal_my_project(self, project_id=None, access_token=None, page=1, date_begin=None, date_end=None, sortby=None, search=None, search_in='content', groupby=None, task_id=None, **kw):
|
def portal_my_project(self, project_id=None, access_token=None, page=1, date_begin=None, date_end=None, sortby=None, search=None, search_in='content', groupby=None, task_id=None, **kw):
|
||||||
try:
|
try:
|
||||||
project_sudo = self._document_check_access('project.project', project_id, access_token)
|
project_sudo = self._document_check_access('project.project', project_id, access_token)
|
||||||
|
if project_sudo.is_template:
|
||||||
|
return request.redirect('/my')
|
||||||
except (AccessError, MissingError):
|
except (AccessError, MissingError):
|
||||||
return request.redirect('/my')
|
return request.redirect('/my')
|
||||||
if project_sudo.collaborator_count and project_sudo.with_user(request.env.user)._check_project_sharing_access():
|
if project_sudo.collaborator_count and project_sudo.with_user(request.env.user)._check_project_sharing_access():
|
||||||
values = {'project_id': project_id}
|
return request.redirect(f'/my/projects/{project_id}/project_sharing')
|
||||||
if task_id:
|
|
||||||
values['task_id'] = task_id
|
|
||||||
return request.render("project.project_sharing_portal", values)
|
|
||||||
project_sudo = project_sudo if access_token else project_sudo.with_user(request.env.user)
|
project_sudo = project_sudo if access_token else project_sudo.with_user(request.env.user)
|
||||||
|
if not groupby:
|
||||||
|
groupby = 'stage_id'
|
||||||
values = self._project_get_page_view_values(project_sudo, access_token, page, date_begin, date_end, sortby, search, search_in, groupby, **kw)
|
values = self._project_get_page_view_values(project_sudo, access_token, page, date_begin, date_end, sortby, search, search_in, groupby, **kw)
|
||||||
return request.render("project.portal_my_project", values)
|
return request.render("project.portal_my_project", values)
|
||||||
|
|
||||||
def _prepare_project_sharing_session_info(self, project, task=None):
|
def _get_project_sharing_company(self, project):
|
||||||
|
return project.company_id or request.env.user.company_id
|
||||||
|
|
||||||
|
def _prepare_project_sharing_session_info(self, project):
|
||||||
session_info = request.env['ir.http'].session_info()
|
session_info = request.env['ir.http'].session_info()
|
||||||
user_context = dict(request.env.context) if request.session.uid else {}
|
user_context = dict(request.env.context) if request.session.uid else {}
|
||||||
mods = conf.server_wide_modules or []
|
|
||||||
if request.env.lang:
|
if request.env.lang:
|
||||||
lang = request.env.lang
|
lang = request.env.lang
|
||||||
session_info['user_context']['lang'] = lang
|
session_info['user_context']['lang'] = lang
|
||||||
# Update Cache
|
# Update Cache
|
||||||
user_context['lang'] = lang
|
user_context['lang'] = lang
|
||||||
lang = user_context.get("lang")
|
lang = user_context.get("lang")
|
||||||
translation_hash = request.env['ir.http'].get_web_translations_hash(mods, lang)
|
|
||||||
cache_hashes = {
|
|
||||||
"translations": translation_hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
project_company = project.company_id
|
project_company = self._get_project_sharing_company(project)
|
||||||
|
|
||||||
session_info.update(
|
session_info.update(
|
||||||
cache_hashes=cache_hashes,
|
action_name="project.project_sharing_project_task_action",
|
||||||
action_name='project.project_sharing_project_task_action',
|
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
|
project_name=project.name,
|
||||||
user_companies={
|
user_companies={
|
||||||
'current_company': project_company.id,
|
'current_company': project_company.id,
|
||||||
'allowed_companies': {
|
'allowed_companies': {
|
||||||
|
|
@ -163,21 +152,25 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# FIXME: See if we prefer to give only the currency that the portal user just need to see the correct information in project sharing
|
# FIXME: See if we prefer to give only the currency that the portal user just need to see the correct information in project sharing
|
||||||
currencies=request.env['ir.http'].get_currencies(),
|
currencies=request.env['res.currency'].get_all_currencies(),
|
||||||
)
|
)
|
||||||
if task:
|
session_info['user_context'].update({
|
||||||
session_info['open_task_action'] = task.action_project_sharing_open_task()
|
'allow_milestones': project.allow_milestones,
|
||||||
|
'allow_task_dependencies': project.allow_task_dependencies,
|
||||||
|
})
|
||||||
return session_info
|
return session_info
|
||||||
|
|
||||||
@http.route("/my/projects/<int:project_id>/project_sharing", type="http", auth="user", methods=['GET'])
|
@http.route(['/my/projects/<int:project_id>/project_sharing', '/my/projects/<int:project_id>/project_sharing/<path:subpath>'], type='http', auth='user', methods=['GET'])
|
||||||
def render_project_backend_view(self, project_id, task_id=None):
|
def render_project_backend_view(self, project_id, subpath=None):
|
||||||
project = request.env['project.project'].sudo().browse(project_id)
|
project = request.env['project.project'].sudo().browse(project_id)
|
||||||
if not project.exists() or not project.with_user(request.env.user)._check_project_sharing_access():
|
if not (
|
||||||
|
project.exists()
|
||||||
|
and project.with_user(request.env.user)._check_project_sharing_access()
|
||||||
|
):
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
task = task_id and request.env['project.task'].browse(int(task_id))
|
|
||||||
return request.render(
|
return request.render(
|
||||||
'project.project_sharing_embed',
|
'project.project_sharing_portal',
|
||||||
{'session_info': self._prepare_project_sharing_session_info(project, task)},
|
{'session_info': self._prepare_project_sharing_session_info(project)},
|
||||||
)
|
)
|
||||||
|
|
||||||
@http.route('/my/projects/<int:project_id>/task/<int:task_id>', type='http', auth='public', website=True)
|
@http.route('/my/projects/<int:project_id>/task/<int:task_id>', type='http', auth='public', website=True)
|
||||||
|
|
@ -200,14 +193,15 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
try:
|
try:
|
||||||
project_sudo = self._document_check_access('project.project', project_id)
|
project_sudo = self._document_check_access('project.project', project_id)
|
||||||
task_sudo = request.env['project.task'].search([('project_id', '=', project_id), ('id', '=', task_id)]).sudo()
|
task_sudo = request.env['project.task'].search([('project_id', '=', project_id), ('id', '=', task_id)]).sudo()
|
||||||
task_domain = [('id', 'child_of', task_id), ('id', '!=', task_id)]
|
task_domain = Domain('id', 'child_of', task_id) & Domain('id', '!=', task_id)
|
||||||
searchbar_filters = self._get_my_tasks_searchbar_filters([('id', '=', task_sudo.project_id.id)], task_domain)
|
searchbar_filters = self._get_my_tasks_searchbar_filters(Domain('id', '=', task_sudo.project_id.id), task_domain)
|
||||||
|
|
||||||
if not filterby:
|
if not filterby:
|
||||||
filterby = 'all'
|
filterby = 'all'
|
||||||
domain = searchbar_filters.get(filterby, searchbar_filters.get('all'))['domain']
|
domain = Domain(searchbar_filters.get(filterby, searchbar_filters.get('all'))['domain'])
|
||||||
|
|
||||||
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby, url=f'/my/projects/{project_id}/task/{task_id}/subtasks', domain=AND([task_domain, domain]))
|
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby,
|
||||||
|
url=f'/my/projects/{project_id}/task/{task_id}/subtasks', domain=task_domain & domain)
|
||||||
values['page_name'] = 'project_subtasks'
|
values['page_name'] = 'project_subtasks'
|
||||||
|
|
||||||
# pager
|
# pager
|
||||||
|
|
@ -217,6 +211,7 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
|
|
||||||
values.update({
|
values.update({
|
||||||
'project': project_sudo,
|
'project': project_sudo,
|
||||||
|
'show_project': True,
|
||||||
'task': task_sudo,
|
'task': task_sudo,
|
||||||
'grouped_tasks': values['grouped_tasks'](pager['offset']),
|
'grouped_tasks': values['grouped_tasks'](pager['offset']),
|
||||||
'pager': pager,
|
'pager': pager,
|
||||||
|
|
@ -227,6 +222,38 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
except (AccessError, MissingError):
|
except (AccessError, MissingError):
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
|
|
||||||
|
@http.route('/my/projects/<int:project_id>/task/<int:task_id>/recurrent_tasks', type='http', auth='user', methods=['GET'], website=True)
|
||||||
|
def portal_my_project_recurrent_tasks(self, project_id, task_id, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', groupby=None, **kw):
|
||||||
|
try:
|
||||||
|
project_sudo = self._document_check_access('project.project', project_id)
|
||||||
|
task_sudo = request.env['project.task'].search([('project_id', '=', project_id), ('id', '=', task_id)], limit=1).sudo()
|
||||||
|
task_domain = Domain('id', 'in', task_sudo.recurrence_id.task_ids.ids)
|
||||||
|
searchbar_filters = self._get_my_tasks_searchbar_filters(Domain('id', '=', project_id), task_domain)
|
||||||
|
|
||||||
|
if not filterby:
|
||||||
|
filterby = 'all'
|
||||||
|
domain = Domain(searchbar_filters.get(filterby, searchbar_filters.get('all'))['domain'])
|
||||||
|
|
||||||
|
values = self._prepare_tasks_values(
|
||||||
|
page, date_begin, date_end, sortby, search, search_in, groupby,
|
||||||
|
url=f'/my/projects/{project_id}/task/{task_id}/recurrent_tasks',
|
||||||
|
domain=task_domain & domain,
|
||||||
|
)
|
||||||
|
values['page_name'] = 'project_recurrent_tasks'
|
||||||
|
pager = portal_pager(**values['pager'])
|
||||||
|
|
||||||
|
values.update({
|
||||||
|
'project': project_sudo,
|
||||||
|
'task': task_sudo,
|
||||||
|
'grouped_tasks': values['grouped_tasks'](pager['offset']),
|
||||||
|
'pager': pager,
|
||||||
|
'searchbar_filters': dict(sorted(searchbar_filters.items())),
|
||||||
|
'filterby': filterby,
|
||||||
|
})
|
||||||
|
return request.render("project.portal_my_tasks", values)
|
||||||
|
except (AccessError, MissingError):
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# My Task
|
# My Task
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
@ -245,10 +272,12 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
project_accessible = False
|
project_accessible = False
|
||||||
values = {
|
values = {
|
||||||
'page_name': page_name,
|
'page_name': page_name,
|
||||||
|
'priority_labels': dict(task._fields['priority']._description_selection(request.env)),
|
||||||
'task': task,
|
'task': task,
|
||||||
'user': request.env.user,
|
'user': request.env.user,
|
||||||
'project_accessible': project_accessible,
|
'project_accessible': project_accessible,
|
||||||
'task_link_section': [],
|
'task_link_section': [],
|
||||||
|
'preview_object': task,
|
||||||
}
|
}
|
||||||
|
|
||||||
values = self._get_page_view_values(task, access_token, values, history, False, **kwargs)
|
values = self._get_page_view_values(task, access_token, values, history, False, **kwargs)
|
||||||
|
|
@ -268,134 +297,127 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
def _task_get_searchbar_sortings(self, milestones_allowed):
|
def _task_get_searchbar_sortings(self, milestones_allowed, project=False):
|
||||||
values = {
|
values = {
|
||||||
'date': {'label': _('Newest'), 'order': 'create_date desc', 'sequence': 1},
|
'id desc': {'label': _('Newest'), 'order': 'id desc', 'sequence': 10},
|
||||||
'name': {'label': _('Title'), 'order': 'name', 'sequence': 2},
|
'name': {'label': _('Title'), 'order': 'name', 'sequence': 20},
|
||||||
'project': {'label': _('Project'), 'order': 'project_id, stage_id', 'sequence': 3},
|
'stage_id, project_id': {'label': _('Stage'), 'order': 'stage_id, project_id', 'sequence': 50},
|
||||||
'stage': {'label': _('Stage'), 'order': 'stage_id, project_id', 'sequence': 5},
|
'state': {'label': _('Status'), 'order': 'state', 'sequence': 60},
|
||||||
'status': {'label': _('Status'), 'order': 'kanban_state', 'sequence': 6},
|
'priority desc': {'label': _('Priority'), 'order': 'priority desc', 'sequence': 80},
|
||||||
'priority': {'label': _('Priority'), 'order': 'priority desc', 'sequence': 8},
|
'date_deadline asc': {'label': _('Deadline'), 'order': 'date_deadline asc', 'sequence': 90},
|
||||||
'date_deadline': {'label': _('Deadline'), 'order': 'date_deadline asc', 'sequence': 9},
|
'date_last_stage_update desc': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc', 'sequence': 110},
|
||||||
'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc', 'sequence': 11},
|
|
||||||
}
|
}
|
||||||
|
if not project:
|
||||||
|
values['project_id, stage_id'] = {'label': _('Project'), 'order': 'project_id, stage_id', 'sequence': 30}
|
||||||
if milestones_allowed:
|
if milestones_allowed:
|
||||||
values['milestone'] = {'label': _('Milestone'), 'order': 'milestone_id', 'sequence': 7}
|
values['milestone_id'] = {'label': _('Milestone'), 'order': 'milestone_id', 'sequence': 70}
|
||||||
return values
|
return values
|
||||||
|
|
||||||
def _task_get_searchbar_groupby(self, milestones_allowed):
|
def _task_get_searchbar_groupby(self, milestones_allowed, project=False):
|
||||||
values = {
|
values = {
|
||||||
'none': {'input': 'none', 'label': _('None'), 'order': 1},
|
'none': {'label': _('None'), 'sequence': 10},
|
||||||
'project': {'input': 'project', 'label': _('Project'), 'order': 2},
|
'stage_id': {'label': _('Stage'), 'sequence': 20},
|
||||||
'stage': {'input': 'stage', 'label': _('Stage'), 'order': 4},
|
'state': {'label': _('Status'), 'sequence': 40},
|
||||||
'status': {'input': 'status', 'label': _('Status'), 'order': 5},
|
'priority': {'label': _('Priority'), 'sequence': 60},
|
||||||
'priority': {'input': 'priority', 'label': _('Priority'), 'order': 7},
|
'partner_id': {'label': _('Customer'), 'sequence': 70},
|
||||||
'customer': {'input': 'customer', 'label': _('Customer'), 'order': 10},
|
|
||||||
}
|
}
|
||||||
|
if not project:
|
||||||
|
values['project_id'] = {'label': _('Project'), 'sequence': 30}
|
||||||
if milestones_allowed:
|
if milestones_allowed:
|
||||||
values['milestone'] = {'input': 'milestone', 'label': _('Milestone'), 'order': 6}
|
values['milestone_id'] = {'label': _('Milestone'), 'sequence': 50}
|
||||||
return dict(sorted(values.items(), key=lambda item: item[1]["order"]))
|
return values
|
||||||
|
|
||||||
def _task_get_groupby_mapping(self):
|
def _task_get_searchbar_inputs(self, milestones_allowed, project=False):
|
||||||
return {
|
|
||||||
'project': 'project_id',
|
|
||||||
'stage': 'stage_id',
|
|
||||||
'customer': 'partner_id',
|
|
||||||
'milestone': 'milestone_id',
|
|
||||||
'priority': 'priority',
|
|
||||||
'status': 'kanban_state',
|
|
||||||
}
|
|
||||||
|
|
||||||
def _task_get_order(self, order, groupby):
|
|
||||||
groupby_mapping = self._task_get_groupby_mapping()
|
|
||||||
field_name = groupby_mapping.get(groupby, '')
|
|
||||||
if not field_name:
|
|
||||||
return order
|
|
||||||
return '%s, %s' % (field_name, order)
|
|
||||||
|
|
||||||
def _task_get_searchbar_inputs(self, milestones_allowed):
|
|
||||||
values = {
|
values = {
|
||||||
'all': {'input': 'all', 'label': _('Search in All'), 'order': 1},
|
'name': {'input': 'name', 'label': _(
|
||||||
'content': {'input': 'content', 'label': Markup(_('Search <span class="nolabel"> (in Content)</span>')), 'order': 1},
|
'Search%(left)s Tasks%(right)s',
|
||||||
'ref': {'input': 'ref', 'label': _('Search in Ref'), 'order': 1},
|
left=Markup('<span class="nolabel">'),
|
||||||
'project': {'input': 'project', 'label': _('Search in Project'), 'order': 2},
|
right=Markup('</span>'),
|
||||||
'users': {'input': 'users', 'label': _('Search in Assignees'), 'order': 3},
|
), 'sequence': 10},
|
||||||
'stage': {'input': 'stage', 'label': _('Search in Stages'), 'order': 4},
|
'users': {'input': 'user_ids', 'label': _('Search in Assignees'), 'sequence': 20},
|
||||||
'status': {'input': 'status', 'label': _('Search in Status'), 'order': 5},
|
'stage_id': {'input': 'stage_id', 'label': _('Search in Stages'), 'sequence': 30},
|
||||||
'priority': {'input': 'priority', 'label': _('Search in Priority'), 'order': 7},
|
'status': {'input': 'status', 'label': _('Search in Status'), 'sequence': 40},
|
||||||
'message': {'input': 'message', 'label': _('Search in Messages'), 'order': 11},
|
'priority': {'input': 'priority', 'label': _('Search in Priority'), 'sequence': 60},
|
||||||
|
'partner_id': {'input': 'partner_id', 'label': _('Search in Customer'), 'sequence': 80},
|
||||||
}
|
}
|
||||||
|
if not project:
|
||||||
|
values['project_id'] = {'input': 'project_id', 'label': _('Search in Project'), 'sequence': 50}
|
||||||
if milestones_allowed:
|
if milestones_allowed:
|
||||||
values['milestone'] = {'input': 'milestone', 'label': _('Search in Milestone'), 'order': 6}
|
values['milestone_id'] = {'input': 'milestone_id', 'label': _('Search in Milestone'), 'sequence': 70}
|
||||||
|
|
||||||
return dict(sorted(values.items(), key=lambda item: item[1]["order"]))
|
return values
|
||||||
|
|
||||||
def _task_get_search_domain(self, search_in, search):
|
def _task_get_search_domain(self, search_in, search, milestones_allowed, project):
|
||||||
search_domain = []
|
if not search_in or search_in == 'name':
|
||||||
if search_in in ('content', 'all'):
|
return ['|', ('name', 'ilike', search), ('id', 'ilike', search)]
|
||||||
search_domain.append([('name', 'ilike', search)])
|
elif search_in == 'users':
|
||||||
search_domain.append([('description', 'ilike', search)])
|
|
||||||
if search_in in ('customer', 'all'):
|
|
||||||
search_domain.append([('partner_id', 'ilike', search)])
|
|
||||||
if search_in in ('message', 'all'):
|
|
||||||
search_domain.append([('message_ids.body', 'ilike', search)])
|
|
||||||
if search_in in ('stage', 'all'):
|
|
||||||
search_domain.append([('stage_id', 'ilike', search)])
|
|
||||||
if search_in in ('project', 'all'):
|
|
||||||
search_domain.append([('project_id', 'ilike', search)])
|
|
||||||
if search_in in ('ref', 'all'):
|
|
||||||
search_domain.append([('id', 'ilike', search)])
|
|
||||||
if search_in in ('milestone', 'all'):
|
|
||||||
search_domain.append([('milestone_id', 'ilike', search)])
|
|
||||||
if search_in in ('users', 'all'):
|
|
||||||
user_ids = request.env['res.users'].sudo().search([('name', 'ilike', search)])
|
user_ids = request.env['res.users'].sudo().search([('name', 'ilike', search)])
|
||||||
search_domain.append([('user_ids', 'in', user_ids.ids)])
|
return [('user_ids', 'in', user_ids.ids)]
|
||||||
if search_in in ('priority', 'all'):
|
elif search_in == 'priority':
|
||||||
search_domain.append([('priority', 'ilike', search == 'normal' and '0' or '1')])
|
priority_selections = request.env['ir.model.fields.selection'].sudo().search([
|
||||||
if search_in in ('status', 'all'):
|
('field_id.model', '=', 'project.task'),
|
||||||
search_domain.append([
|
('field_id.name', '=', 'priority'),
|
||||||
('kanban_state', 'ilike', 'normal' if search == 'In Progress' else 'done' if search == 'Ready' else 'blocked' if search == 'Blocked' else search)
|
('name', 'ilike', search),
|
||||||
])
|
])
|
||||||
return OR(search_domain)
|
if priority_selections:
|
||||||
|
return [('priority', 'in', priority_selections.mapped('value'))]
|
||||||
|
return Domain.FALSE
|
||||||
|
elif search_in == 'status':
|
||||||
|
state_selections = request.env['ir.model.fields.selection'].sudo().search([
|
||||||
|
('field_id.model', '=', 'project.task'),
|
||||||
|
('field_id.name', '=', 'state'),
|
||||||
|
('name', 'ilike', search),
|
||||||
|
])
|
||||||
|
if state_selections:
|
||||||
|
return [('state', 'in', state_selections.mapped('value'))]
|
||||||
|
return Domain.FALSE
|
||||||
|
elif search_in in self._task_get_searchbar_inputs(milestones_allowed, project):
|
||||||
|
return [(search_in, 'ilike', search)]
|
||||||
|
else:
|
||||||
|
return ['|', ('name', 'ilike', search), ('id', 'ilike', search)]
|
||||||
|
|
||||||
def _prepare_tasks_values(self, page, date_begin, date_end, sortby, search, search_in, groupby, url="/my/tasks", domain=None, su=False):
|
def _prepare_tasks_values(self, page, date_begin, date_end, sortby, search, search_in, groupby, url="/my/tasks", domain=None, su=False, project=False):
|
||||||
values = self._prepare_portal_layout_values()
|
values = self._prepare_portal_layout_values()
|
||||||
|
|
||||||
Task = request.env['project.task']
|
Task = request.env['project.task']
|
||||||
milestone_domain = AND([domain, [('allow_milestones', '=', 'True')]])
|
|
||||||
milestones_allowed = Task.sudo().search_count(milestone_domain, limit=1) == 1
|
|
||||||
searchbar_sortings = dict(sorted(self._task_get_searchbar_sortings(milestones_allowed).items(),
|
|
||||||
key=lambda item: item[1]["sequence"]))
|
|
||||||
searchbar_inputs = self._task_get_searchbar_inputs(milestones_allowed)
|
|
||||||
searchbar_groupby = self._task_get_searchbar_groupby(milestones_allowed)
|
|
||||||
|
|
||||||
if not domain:
|
domain = Domain.AND([domain or [], [('has_template_ancestor', '=', False)]])
|
||||||
domain = []
|
if not su and Task.has_access('read'):
|
||||||
if not su and Task.check_access_rights('read'):
|
domain &= Domain(request.env['ir.rule']._compute_domain(Task._name, 'read'))
|
||||||
domain = AND([domain, request.env['ir.rule']._compute_domain(Task._name, 'read')])
|
|
||||||
Task_sudo = Task.sudo()
|
Task_sudo = Task.sudo()
|
||||||
|
milestone_domain = domain & Domain('allow_milestones', '=', True) & Domain('milestone_id', '!=', False)
|
||||||
|
milestones_allowed = Task_sudo.search_count(milestone_domain, limit=1) == 1
|
||||||
|
searchbar_sortings = dict(sorted(self._task_get_searchbar_sortings(milestones_allowed, project).items(),
|
||||||
|
key=lambda item: item[1]["sequence"]))
|
||||||
|
searchbar_inputs = dict(sorted(self._task_get_searchbar_inputs(milestones_allowed, project).items(), key=lambda item: item[1]['sequence']))
|
||||||
|
searchbar_groupby = dict(sorted(self._task_get_searchbar_groupby(milestones_allowed, project).items(), key=lambda item: item[1]['sequence']))
|
||||||
|
|
||||||
# default sort by value
|
# default sort by value
|
||||||
if not sortby or sortby not in searchbar_sortings or (sortby == 'milestone' and not milestones_allowed):
|
if not sortby or (sortby == 'milestone_id' and not milestones_allowed):
|
||||||
sortby = 'date'
|
sortby = next(iter(searchbar_sortings))
|
||||||
order = searchbar_sortings[sortby]['order']
|
|
||||||
|
|
||||||
# default group by value
|
# default group by value
|
||||||
if not groupby or (groupby == 'milestone' and not milestones_allowed):
|
if not groupby or (groupby == 'milestone_id' and not milestones_allowed):
|
||||||
groupby = 'project'
|
groupby = 'project_id'
|
||||||
|
|
||||||
if date_begin and date_end:
|
if date_begin and date_end:
|
||||||
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
domain &= Domain('create_date', '>', date_begin) & Domain('create_date', '<=', date_end)
|
||||||
|
|
||||||
# search reset if needed
|
# search reset if needed
|
||||||
if not milestones_allowed and search_in == 'milestone':
|
if not milestones_allowed and search_in == 'milestone_id':
|
||||||
search_in = 'all'
|
search_in = 'all'
|
||||||
# search
|
# search
|
||||||
if search and search_in:
|
if search and search_in:
|
||||||
domain += self._task_get_search_domain(search_in, search)
|
domain &= Domain(self._task_get_search_domain(search_in, search, milestones_allowed, project))
|
||||||
|
|
||||||
# content according to pager and archive selected
|
# content according to pager and archive selected
|
||||||
order = self._task_get_order(order, groupby)
|
if groupby == 'none':
|
||||||
|
group_field = None
|
||||||
|
elif groupby == 'priority':
|
||||||
|
group_field = 'priority desc'
|
||||||
|
else:
|
||||||
|
group_field = groupby
|
||||||
|
order = '%s, %s' % (group_field, sortby) if group_field else sortby
|
||||||
|
|
||||||
def get_grouped_tasks(pager_offset):
|
def get_grouped_tasks(pager_offset):
|
||||||
tasks = Task_sudo.search(domain, order=order, limit=self._items_per_page, offset=pager_offset)
|
tasks = Task_sudo.search(domain, order=order, limit=self._items_per_page, offset=pager_offset)
|
||||||
|
|
@ -404,11 +426,9 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
tasks_project_allow_milestone = tasks.filtered(lambda t: t.allow_milestones)
|
tasks_project_allow_milestone = tasks.filtered(lambda t: t.allow_milestones)
|
||||||
tasks_no_milestone = tasks - tasks_project_allow_milestone
|
tasks_no_milestone = tasks - tasks_project_allow_milestone
|
||||||
|
|
||||||
groupby_mapping = self._task_get_groupby_mapping()
|
if groupby != 'none':
|
||||||
group = groupby_mapping.get(groupby)
|
if groupby == 'milestone_id':
|
||||||
if group:
|
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks_project_allow_milestone, itemgetter(groupby))]
|
||||||
if group == 'milestone_id':
|
|
||||||
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks_project_allow_milestone, itemgetter(group))]
|
|
||||||
|
|
||||||
if not grouped_tasks:
|
if not grouped_tasks:
|
||||||
if tasks_no_milestone:
|
if tasks_no_milestone:
|
||||||
|
|
@ -420,16 +440,16 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
grouped_tasks[len(grouped_tasks) - 1] |= tasks_no_milestone
|
grouped_tasks[len(grouped_tasks) - 1] |= tasks_no_milestone
|
||||||
|
|
||||||
else:
|
else:
|
||||||
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks, itemgetter(group))]
|
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks, itemgetter(groupby))]
|
||||||
else:
|
else:
|
||||||
grouped_tasks = [tasks] if tasks else []
|
grouped_tasks = [tasks] if tasks else []
|
||||||
|
|
||||||
task_states = dict(Task_sudo._fields['kanban_state']._description_selection(request.env))
|
task_states = dict(Task_sudo._fields['state']._description_selection(request.env))
|
||||||
if sortby == 'status':
|
if sortby == 'state':
|
||||||
if groupby == 'none' and grouped_tasks:
|
if groupby == 'none' and grouped_tasks:
|
||||||
grouped_tasks[0] = grouped_tasks[0].sorted(lambda tasks: task_states.get(tasks.kanban_state))
|
grouped_tasks[0] = grouped_tasks[0].sorted(lambda tasks: task_states.get(tasks.state))
|
||||||
else:
|
else:
|
||||||
grouped_tasks.sort(key=lambda tasks: task_states.get(tasks[0].kanban_state))
|
grouped_tasks.sort(key=lambda tasks: task_states.get(tasks[0].state))
|
||||||
return grouped_tasks
|
return grouped_tasks
|
||||||
|
|
||||||
values.update({
|
values.update({
|
||||||
|
|
@ -437,6 +457,8 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
'date_end': date_end,
|
'date_end': date_end,
|
||||||
'grouped_tasks': get_grouped_tasks,
|
'grouped_tasks': get_grouped_tasks,
|
||||||
'allow_milestone': milestones_allowed,
|
'allow_milestone': milestones_allowed,
|
||||||
|
'multiple_projects': True,
|
||||||
|
'priority_labels': dict(Task_sudo._fields['priority']._description_selection(request.env)),
|
||||||
'page_name': 'task',
|
'page_name': 'task',
|
||||||
'default_url': url,
|
'default_url': url,
|
||||||
'task_url': 'tasks',
|
'task_url': 'tasks',
|
||||||
|
|
@ -459,11 +481,11 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
|
|
||||||
def _get_my_tasks_searchbar_filters(self, project_domain=None, task_domain=None):
|
def _get_my_tasks_searchbar_filters(self, project_domain=None, task_domain=None):
|
||||||
searchbar_filters = {
|
searchbar_filters = {
|
||||||
'all': {'label': _('All'), 'domain': [('project_id', '!=', False)]},
|
'all': {'label': _('All'), 'domain': [('project_id', '!=', False), ('is_template', '=', False)]},
|
||||||
}
|
}
|
||||||
|
|
||||||
# extends filterby criteria with project the customer has access to
|
# extends filterby criteria with project the customer has access to
|
||||||
projects = request.env['project.project'].search(project_domain or [])
|
projects = request.env['project.project'].search(project_domain or [], order="id")
|
||||||
for project in projects:
|
for project in projects:
|
||||||
searchbar_filters.update({
|
searchbar_filters.update({
|
||||||
str(project.id): {'label': project.name, 'domain': [('project_id', '=', project.id)]}
|
str(project.id): {'label': project.name, 'domain': [('project_id', '=', project.id)]}
|
||||||
|
|
@ -471,19 +493,18 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
|
|
||||||
# extends filterby criteria with project (criteria name is the project id)
|
# extends filterby criteria with project (criteria name is the project id)
|
||||||
# Note: portal users can't view projects they don't follow
|
# Note: portal users can't view projects they don't follow
|
||||||
project_groups = request.env['project.task'].read_group(AND([[('project_id', 'not in', projects.ids)], task_domain or []]),
|
project_groups = request.env['project.task']._read_group(
|
||||||
['project_id'], ['project_id'])
|
Domain.AND([[('project_id', 'not in', projects.ids), ('is_template', '=', False), ('project_id', '!=', False)], task_domain or []]),
|
||||||
for group in project_groups:
|
['project_id'])
|
||||||
proj_id = group['project_id'][0] if group['project_id'] else False
|
for [project] in project_groups:
|
||||||
proj_name = group['project_id'][1] if group['project_id'] else _('Others')
|
|
||||||
searchbar_filters.update({
|
searchbar_filters.update({
|
||||||
str(proj_id): {'label': proj_name, 'domain': [('project_id', '=', proj_id)]}
|
str(project.id): {'label': project.sudo().display_name, 'domain': [('project_id', '=', project.id)]}
|
||||||
})
|
})
|
||||||
return searchbar_filters
|
return searchbar_filters
|
||||||
|
|
||||||
@http.route(['/my/tasks', '/my/tasks/page/<int:page>'], type='http', auth="user", website=True)
|
@http.route(['/my/tasks', '/my/tasks/page/<int:page>'], type='http', auth="user", website=True)
|
||||||
def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', groupby=None, **kw):
|
def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='name', groupby=None, **kw):
|
||||||
searchbar_filters = self._get_my_tasks_searchbar_filters()
|
searchbar_filters = self._get_my_tasks_searchbar_filters([('is_template', '=', False)])
|
||||||
|
|
||||||
if not filterby:
|
if not filterby:
|
||||||
filterby = 'all'
|
filterby = 'all'
|
||||||
|
|
@ -496,10 +517,12 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
pager_vals['url_args'].update(filterby=filterby)
|
pager_vals['url_args'].update(filterby=filterby)
|
||||||
pager = portal_pager(**pager_vals)
|
pager = portal_pager(**pager_vals)
|
||||||
|
|
||||||
|
grouped_tasks = values['grouped_tasks'](pager['offset'])
|
||||||
values.update({
|
values.update({
|
||||||
'grouped_tasks': values['grouped_tasks'](pager['offset']),
|
'grouped_tasks': grouped_tasks,
|
||||||
|
'show_project': True,
|
||||||
'pager': pager,
|
'pager': pager,
|
||||||
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
|
'searchbar_filters': searchbar_filters,
|
||||||
'filterby': filterby,
|
'filterby': filterby,
|
||||||
})
|
})
|
||||||
return request.render("project.portal_my_tasks", values)
|
return request.render("project.portal_my_tasks", values)
|
||||||
|
|
@ -528,3 +551,42 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||||
request.session['my_tasks_history'] = task_sudo.ids
|
request.session['my_tasks_history'] = task_sudo.ids
|
||||||
values = self._task_get_page_view_values(task_sudo, access_token, **kw)
|
values = self._task_get_page_view_values(task_sudo, access_token, **kw)
|
||||||
return request.render("project.portal_my_task", values)
|
return request.render("project.portal_my_task", values)
|
||||||
|
|
||||||
|
@http.route('/project_sharing/attachment/add_image', type='http', auth='user', methods=['POST'], website=True)
|
||||||
|
def add_image(self, name, data, res_id, access_token=None, **kwargs):
|
||||||
|
try:
|
||||||
|
task_sudo = self._document_check_access('project.task', int(res_id), access_token=access_token)
|
||||||
|
if not task_sudo.with_user(request.env.uid).project_id._check_project_sharing_access():
|
||||||
|
return request.not_found()
|
||||||
|
except (AccessError, MissingError):
|
||||||
|
raise UserError(_("The document does not exist or you do not have the rights to access it."))
|
||||||
|
|
||||||
|
IrAttachment = request.env['ir.attachment']
|
||||||
|
|
||||||
|
# Avoid using sudo when not necessary: internal users can create attachments,
|
||||||
|
# as opposed to public and portal users.
|
||||||
|
if not request.env.user._is_internal():
|
||||||
|
IrAttachment = IrAttachment.sudo()
|
||||||
|
|
||||||
|
values = IrAttachment._check_contents({
|
||||||
|
'name': name,
|
||||||
|
'datas': data,
|
||||||
|
'res_model': 'project.task',
|
||||||
|
'res_id': res_id,
|
||||||
|
'access_token': IrAttachment._generate_access_token(),
|
||||||
|
})
|
||||||
|
|
||||||
|
valid_image_mime_types = ['image/jpeg', 'image/png', 'image/bmp', 'image/tiff']
|
||||||
|
|
||||||
|
if values.get('mimetype', False) not in valid_image_mime_types:
|
||||||
|
return request.make_response(
|
||||||
|
data=json.dumps({'error': _('Only jpeg, png, bmp and tiff images are allowed as attachments.')}),
|
||||||
|
headers=[('Content-Type', 'application/json')],
|
||||||
|
status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
attachment = IrAttachment.create(values)
|
||||||
|
return request.make_response(
|
||||||
|
data=json.dumps(attachment.read(['id', 'name', 'mimetype', 'file_size', 'access_token'])[0]),
|
||||||
|
headers=[('Content-Type', 'application/json')]
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from odoo.http import request, route
|
from odoo.http import request
|
||||||
|
|
||||||
from odoo.addons.portal.controllers.mail import PortalChatter
|
from odoo.addons.portal.controllers.portal_thread import PortalChatter
|
||||||
from .portal import ProjectCustomerPortal
|
from .portal import ProjectCustomerPortal
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -25,77 +24,7 @@ class ProjectSharingChatter(PortalChatter):
|
||||||
can_access = project_sudo and res_model == 'project.task' and project_sudo.with_user(request.env.user)._check_project_sharing_access()
|
can_access = project_sudo and res_model == 'project.task' and project_sudo.with_user(request.env.user)._check_project_sharing_access()
|
||||||
task = None
|
task = None
|
||||||
if can_access:
|
if can_access:
|
||||||
task = request.env['project.task'].sudo().search([('id', '=', res_id), ('project_id', '=', project_sudo.id)])
|
task = request.env['project.task'].sudo().with_context(active_test=False).search([('id', '=', res_id), ('project_id', '=', project_sudo.id)])
|
||||||
if not can_access or not task:
|
if not can_access or not task:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
return task[task._mail_post_token_field]
|
return task[task._mail_post_token_field]
|
||||||
|
|
||||||
# ============================================================ #
|
|
||||||
# Note concerning the methods portal_chatter_(init/post/fetch)
|
|
||||||
# ============================================================ #
|
|
||||||
#
|
|
||||||
# When the project is shared to a portal user with the edit rights,
|
|
||||||
# he has the read/write access to the related tasks. So it could be
|
|
||||||
# possible to call directly the message_post method on a task.
|
|
||||||
#
|
|
||||||
# This change is considered as safe, as we only willingly expose
|
|
||||||
# records, for some assumed fields only, and this feature is
|
|
||||||
# optional and opt-in. (like the public employee model for example).
|
|
||||||
# It doesn't allow portal users to access other models, like
|
|
||||||
# a timesheet or an invoice.
|
|
||||||
#
|
|
||||||
# It could seem odd to use those routes, and converting the project
|
|
||||||
# access token into the task access token, as the user has actually
|
|
||||||
# access to the records.
|
|
||||||
#
|
|
||||||
# However, it has been decided that it was the less hacky way to
|
|
||||||
# achieve this, as:
|
|
||||||
#
|
|
||||||
# - We're reusing the existing routes, that convert all the data
|
|
||||||
# into valid arguments for the methods we use (message_post, ...).
|
|
||||||
# That way, we don't have to reinvent the wheel, duplicating code
|
|
||||||
# from mail/portal that surely will lead too desynchronization
|
|
||||||
# and inconsistencies over the time.
|
|
||||||
#
|
|
||||||
# - We don't define new routes, to do the exact same things than portal,
|
|
||||||
# considering that the portal user can use message_post for example
|
|
||||||
# because he has access to the record.
|
|
||||||
# Let's suppose that we remove this in a future development, those
|
|
||||||
# new routes won't be valid anymore.
|
|
||||||
#
|
|
||||||
# - We could have reused the mail widgets, as we already reuse the
|
|
||||||
# form/list/kanban views, etc. However, we only want to display
|
|
||||||
# the messages and allow to post. We don't need the next activities
|
|
||||||
# the followers system, etc. This required to override most of the
|
|
||||||
# mail.thread basic methods, without being sure that this would
|
|
||||||
# work with other installed applications or customizations
|
|
||||||
|
|
||||||
@route()
|
|
||||||
def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs):
|
|
||||||
project_sharing_id = kwargs.get('project_sharing_id')
|
|
||||||
if project_sharing_id:
|
|
||||||
# if there is a token in `kwargs` then it should be the access_token of the project shared
|
|
||||||
token = self._check_project_access_and_get_token(project_sharing_id, res_model, res_id, kwargs.get('token'))
|
|
||||||
if token:
|
|
||||||
del kwargs['project_sharing_id']
|
|
||||||
kwargs['token'] = token
|
|
||||||
return super().portal_chatter_init(res_model, res_id, domain=domain, limit=limit, **kwargs)
|
|
||||||
|
|
||||||
@route()
|
|
||||||
def portal_chatter_post(self, res_model, res_id, message, attachment_ids=None, attachment_tokens=None, **kw):
|
|
||||||
project_sharing_id = kw.get('project_sharing_id')
|
|
||||||
if project_sharing_id:
|
|
||||||
token = self._check_project_access_and_get_token(project_sharing_id, res_model, res_id, kw.get('token'))
|
|
||||||
if token:
|
|
||||||
del kw['project_sharing_id']
|
|
||||||
kw['token'] = token
|
|
||||||
return super().portal_chatter_post(res_model, res_id, message, attachment_ids=attachment_ids, attachment_tokens=attachment_tokens, **kw)
|
|
||||||
|
|
||||||
@route()
|
|
||||||
def portal_message_fetch(self, res_model, res_id, domain=False, limit=10, offset=0, **kw):
|
|
||||||
project_sharing_id = kw.get('project_sharing_id')
|
|
||||||
if project_sharing_id:
|
|
||||||
token = self._check_project_access_and_get_token(project_sharing_id, res_model, res_id, kw.get('token'))
|
|
||||||
if token is not None:
|
|
||||||
kw['token'] = token # Update token (either string which contains token value or False)
|
|
||||||
return super().portal_message_fetch(res_model, res_id, domain=domain, limit=limit, offset=offset, **kw)
|
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,17 @@
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<record id="digest_tip_project_0" model="digest.tip">
|
<record id="digest_tip_project_0" model="digest.tip">
|
||||||
<field name="name">Tip: Customize tasks and stages according to the project</field>
|
<field name="name">Tip: Use task states to keep track of your tasks' progression</field>
|
||||||
<field name="sequence">1200</field>
|
<field name="sequence">1200</field>
|
||||||
<field name="group_id" ref="project.group_project_manager"/>
|
<field name="group_id" ref="project.group_project_user"/>
|
||||||
<field name="tip_description" type="html">
|
<field name="tip_description" type="html">
|
||||||
<div>
|
<div>
|
||||||
<p class="tip_title">Tip: Customize tasks and stages according to the project</p>
|
<p class="tip_title">Tip: Use task states to keep track of your tasks' progression</p>
|
||||||
<p class="tip_content">Customize how tasks are named according to the project and create tailor made status messages for each step of the workflow. It helps to document your workflow: what should be done at which step.</p>
|
<p class="tip_content">
|
||||||
<img src="https://download.odoocdn.com/digests/project/static/src/img/project-custom-tasks.gif" class="illustration_border" />
|
Quickly check the status of tasks for approvals or change requests and identify those on hold until dependencies are resolved with the hourglass icon.
|
||||||
</div>
|
</p>
|
||||||
|
<img src="https://download.odoocdn.com/digests/project/static/src/img/task-state-img.png" width="720" class="illustration_border" />
|
||||||
|
</div>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
@ -26,14 +28,40 @@
|
||||||
<field name="group_id" ref="project.group_project_user"/>
|
<field name="group_id" ref="project.group_project_user"/>
|
||||||
<field name="tip_description" type="html">
|
<field name="tip_description" type="html">
|
||||||
<div>
|
<div>
|
||||||
<t t-set="project_record" t-value="object.env['project.project'].search([('alias_name', '!=', False)], limit=1, order='sequence asc')"/>
|
<t t-set="project_record" t-value="object.env['project.project'].search([('alias_name', '!=', False), ('alias_domain_id', '!=', False)], limit=1, order='sequence asc')"/>
|
||||||
<p class="tip_title">Tip: Create tasks from incoming emails</p>
|
<p class="tip_title">Tip: Create tasks from incoming emails</p>
|
||||||
<t t-if="project_record and project_record.alias_domain">
|
<t t-if="project_record.alias_email">
|
||||||
<p class="tip_content">Emails sent to <a t-attf-href="mailto:{{project_record.alias_value}}" target="_blank" style="color: #875a7b; text-decoration: none;"><t t-out="project_record.alias_value" /></a> will generate tasks in your <t t-out="project_record.name"></t> project.</p>
|
<p class="tip_content">Emails sent to <a t-attf-href="mailto:{{project_record.alias_email}}" target="_blank" style="color: #714B67; text-decoration: none;"><t t-out="project_record.alias_email" /></a> will generate tasks in your <t t-out="project_record.name"></t> project.</p>
|
||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
<p class="tip_content">Create tasks by sending an email to the email address of your project.</p>
|
<p class="tip_content">Create tasks by sending an email to the email address of your project.</p>
|
||||||
</t>
|
</t>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="digest_tip_project_2" model="digest.tip">
|
||||||
|
<field name="name">Tip: Your Own Personal Kanban</field>
|
||||||
|
<field name="sequence">3200</field>
|
||||||
|
<field name="group_id" ref="project.group_project_user"/>
|
||||||
|
<field name="tip_description" type="html">
|
||||||
|
<div>
|
||||||
|
<p class="tip_title">Tip: Your Own Personal Kanban</p>
|
||||||
|
<p class="tip_content">Use personal stages to organize your tasks and create your own workflow.</p>
|
||||||
|
<img src="https://download.odoocdn.com/digests/project/static/src/img/18-project-personal-stages.gif" width="540" class="illustration_border"/>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="digest_tip_project_3" model="digest.tip">
|
||||||
|
<field name="name">Tip: Project-Specific Fields</field>
|
||||||
|
<field name="sequence">3900</field>
|
||||||
|
<field name="group_id" ref="project.group_project_manager"/>
|
||||||
|
<field name="tip_description" type="html">
|
||||||
|
<div>
|
||||||
|
<p class="tip_title">Tip: Project-Specific Fields</p>
|
||||||
|
<p class="tip_content">Add project-specific property fields on tasks to customize your project management process.</p>
|
||||||
|
<img src="https://download.odoocdn.com/digests/project/static/src/img/18-project-property-fields.gif" width="540" class="illustration_border"/>
|
||||||
</div>
|
</div>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo noupdate="1">
|
<odoo noupdate="1">
|
||||||
<record id="ir_cron_rating_project" model="ir.cron">
|
<record id="ir_cron_rating_project" model="ir.cron">
|
||||||
<field name="name">Project: Send rating</field>
|
<field name="name">Project Stage: Send rating</field>
|
||||||
<field name="model_id" ref="project.model_project_project"/>
|
<field name="model_id" ref="project.model_project_task_type"/>
|
||||||
<field name="state">code</field>
|
<field name="state">code</field>
|
||||||
<field name="code">model._send_rating_all()</field>
|
<field name="code">model._send_rating_all()</field>
|
||||||
<field name="interval_type">days</field>
|
<field name="interval_type">days</field>
|
||||||
<field name="numbercall">-1</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="ir_cron_recurring_tasks" model="ir.cron">
|
|
||||||
<field name="name">Project: Create Recurring Tasks</field>
|
|
||||||
<field name="model_id" ref="project.model_project_task_recurrence"/>
|
|
||||||
<field name="state">code</field>
|
|
||||||
<field name="code">model._cron_create_recurring_tasks()</field>
|
|
||||||
<field name="interval_type">days</field>
|
|
||||||
<field name="numbercall">-1</field>
|
|
||||||
<field name="nextcall" eval="(DateTime.now().replace(hour=3, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
|
|
||||||
</record>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -14,31 +14,54 @@
|
||||||
<field name="default" eval="False"/>
|
<field name="default" eval="False"/>
|
||||||
<field name="description">Stage changed</field>
|
<field name="description">Stage changed</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="mt_task_blocked" model="mail.message.subtype">
|
<!-- new state subtypes-->
|
||||||
<field name="name">Task Blocked</field>
|
<record id="mt_task_in_progress" model="mail.message.subtype">
|
||||||
|
<field name="name">Task In Progress</field>
|
||||||
<field name="res_model">project.task</field>
|
<field name="res_model">project.task</field>
|
||||||
<field name="default" eval="False"/>
|
<field name="default" eval="False"/>
|
||||||
<field name="description">Task blocked</field>
|
<field name="sequence" eval="101"/>
|
||||||
|
<field name="description">Task In Progress</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="mt_task_ready" model="mail.message.subtype">
|
<record id="mt_task_changes_requested" model="mail.message.subtype">
|
||||||
<field name="name">Task Ready</field>
|
<field name="name">Changes Requested</field>
|
||||||
<field name="res_model">project.task</field>
|
<field name="res_model">project.task</field>
|
||||||
<field name="default" eval="False"/>
|
<field name="default" eval="False"/>
|
||||||
<field name="description">Task ready for Next Stage</field>
|
<field name="sequence" eval="102"/>
|
||||||
|
<field name="description">Changes Requested</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="mt_task_progress" model="mail.message.subtype">
|
<record id="mt_task_approved" model="mail.message.subtype">
|
||||||
<field name="name">Task in Progress</field>
|
<field name="name">Task Approved</field>
|
||||||
<field name="res_model">project.task</field>
|
<field name="res_model">project.task</field>
|
||||||
<field name="default" eval="False"/>
|
<field name="default" eval="False"/>
|
||||||
|
<field name="sequence" eval="103"/>
|
||||||
|
<field name="description">Task approved</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_task_canceled" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Cancelled</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="sequence" eval="104"/>
|
||||||
|
<field name="description">Task cancelled</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_task_done" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Done</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="sequence" eval="105"/>
|
||||||
|
<field name="description">Task done</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_task_waiting" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Waiting</field>
|
||||||
|
<field name="res_model">project.task</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="sequence" eval="106"/>
|
||||||
|
<field name="description">Task Waiting</field>
|
||||||
|
<field name="hidden" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="mt_task_rating" model="mail.message.subtype">
|
<record id="mt_task_rating" model="mail.message.subtype">
|
||||||
<field name="name">Task Rating</field>
|
<field name="name">Task Rating</field>
|
||||||
<field name="res_model">project.task</field>
|
<field name="res_model">project.task</field>
|
||||||
<field name="default" eval="False"/>
|
<field name="sequence" eval="108"/>
|
||||||
</record>
|
|
||||||
<record id="mt_task_dependency_change" model="mail.message.subtype">
|
|
||||||
<field name="name">Task Dependency Changes</field>
|
|
||||||
<field name="res_model">project.task</field>
|
|
||||||
<field name="default" eval="False"/>
|
<field name="default" eval="False"/>
|
||||||
<field name="hidden" eval="True"/>
|
<field name="hidden" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
@ -66,25 +89,9 @@
|
||||||
<field name="parent_id" ref="mt_task_new"/>
|
<field name="parent_id" ref="mt_task_new"/>
|
||||||
<field name="relation_field">project_id</field>
|
<field name="relation_field">project_id</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="mt_project_task_blocked" model="mail.message.subtype">
|
|
||||||
<field name="name">Task Blocked</field>
|
|
||||||
<field name="sequence">11</field>
|
|
||||||
<field name="res_model">project.project</field>
|
|
||||||
<field name="default" eval="False"/>
|
|
||||||
<field name="parent_id" ref="mt_task_blocked"/>
|
|
||||||
<field name="relation_field">project_id</field>
|
|
||||||
</record>
|
|
||||||
<record id="mt_project_task_ready" model="mail.message.subtype">
|
|
||||||
<field name="name">Task Ready</field>
|
|
||||||
<field name="sequence">12</field>
|
|
||||||
<field name="res_model">project.project</field>
|
|
||||||
<field name="default" eval="False"/>
|
|
||||||
<field name="parent_id" ref="mt_task_ready"/>
|
|
||||||
<field name="relation_field">project_id</field>
|
|
||||||
</record>
|
|
||||||
<record id="mt_project_task_stage" model="mail.message.subtype">
|
<record id="mt_project_task_stage" model="mail.message.subtype">
|
||||||
<field name="name">Task Stage Changed</field>
|
<field name="name">Task Stage Changed</field>
|
||||||
<field name="sequence">13</field>
|
<field name="sequence">16</field>
|
||||||
<field name="res_model">project.project</field>
|
<field name="res_model">project.project</field>
|
||||||
<field name="default" eval="False"/>
|
<field name="default" eval="False"/>
|
||||||
<field name="parent_id" ref="mt_task_stage"/>
|
<field name="parent_id" ref="mt_task_stage"/>
|
||||||
|
|
@ -92,28 +99,69 @@
|
||||||
</record>
|
</record>
|
||||||
<record id="mt_project_task_rating" model="mail.message.subtype">
|
<record id="mt_project_task_rating" model="mail.message.subtype">
|
||||||
<field name="name">Task Rating</field>
|
<field name="name">Task Rating</field>
|
||||||
<field name="sequence">14</field>
|
<field name="sequence">27</field>
|
||||||
<field name="res_model">project.project</field>
|
|
||||||
<field name="default" eval="True"/>
|
|
||||||
<field name="parent_id" ref="mt_task_rating"/>
|
|
||||||
<field name="relation_field">project_id</field>
|
|
||||||
</record>
|
|
||||||
<record id="mt_project_task_dependency_change" model="mail.message.subtype">
|
|
||||||
<field name="name">Task Dependency Changes</field>
|
|
||||||
<field name="sequence">15</field>
|
|
||||||
<field name="res_model">project.project</field>
|
<field name="res_model">project.project</field>
|
||||||
<field name="default" eval="False"/>
|
<field name="default" eval="False"/>
|
||||||
<field name="parent_id" ref="mt_task_dependency_change"/>
|
<field name="parent_id" ref="mt_task_rating"/>
|
||||||
<field name="relation_field">project_id</field>
|
<field name="relation_field">project_id</field>
|
||||||
<field name="hidden" eval="True"/>
|
<field name="hidden" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="mt_project_update_create" model="mail.message.subtype">
|
<record id="mt_project_update_create" model="mail.message.subtype">
|
||||||
<field name="name">Update Created</field>
|
<field name="name">Update Created</field>
|
||||||
<field name="sequence">16</field>
|
<field name="sequence">19</field>
|
||||||
<field name="res_model">project.project</field>
|
<field name="res_model">project.project</field>
|
||||||
<field name="default" eval="False"/>
|
<field name="default" eval="False"/>
|
||||||
<field name="parent_id" ref="mt_update_create"/>
|
<field name="parent_id" ref="mt_update_create"/>
|
||||||
<field name="relation_field">project_id</field>
|
<field name="relation_field">project_id</field>
|
||||||
<field name="hidden" eval="True"/>
|
<field name="hidden" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
|
<record id="mt_project_task_in_progress" model="mail.message.subtype">
|
||||||
|
<field name="name">Task In Progress</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_in_progress"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_changes_requested" model="mail.message.subtype">
|
||||||
|
<field name="name">Changes Requested</field>
|
||||||
|
<field name="sequence">21</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_changes_requested"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_approved" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Approved</field>
|
||||||
|
<field name="sequence">22</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_approved"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_canceled" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Canceled</field>
|
||||||
|
<field name="sequence">23</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_canceled"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_done" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Done</field>
|
||||||
|
<field name="sequence">24</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_done"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
</record>
|
||||||
|
<record id="mt_project_task_waiting" model="mail.message.subtype">
|
||||||
|
<field name="name">Task Waiting</field>
|
||||||
|
<field name="sequence">25</field>
|
||||||
|
<field name="res_model">project.project</field>
|
||||||
|
<field name="default" eval="False"/>
|
||||||
|
<field name="parent_id" ref="mt_task_waiting"/>
|
||||||
|
<field name="relation_field">project_id</field>
|
||||||
|
<field name="hidden" eval="True"/>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,21 @@
|
||||||
<field name="model_id" ref="project.model_project_task"/>
|
<field name="model_id" ref="project.model_project_task"/>
|
||||||
<field name="subject">Reception of {{ object.name }}</field>
|
<field name="subject">Reception of {{ object.name }}</field>
|
||||||
<field name="use_default_to" eval="True"/>
|
<field name="use_default_to" eval="True"/>
|
||||||
<field name="description">Set this template on a project's stage to automate email when tasks reach stages</field>
|
<field name="description">Automatically send an email to customers when a task reaches a specific stage in a project by setting this template on that stage</field>
|
||||||
<field name="body_html" type="html">
|
<field name="body_html" type="html">
|
||||||
<div>
|
<div>
|
||||||
Dear <t t-out="object.partner_id.name or 'customer'">Brandon Freeman</t>,<br/>
|
Dear <t t-out="object.partner_id.name or 'customer'">Brandon Freeman</t>,<br/><br/>
|
||||||
Thank you for your enquiry.<br />
|
Thank you for contacting us. We appreciate your interest in our products/services.<br/>
|
||||||
If you have any questions, please let us know.
|
Our team is currently reviewing your inquiry and will respond to your email as soon as possible.<br/>
|
||||||
<br/><br/>
|
If you have any further questions or concerns in the meantime, please do not hesitate to let us know. We are here to help.<br/><br/>
|
||||||
Thank you,
|
Thank you for your patience.<br/>
|
||||||
|
Best regards,
|
||||||
<t t-if="user.signature">
|
<t t-if="user.signature">
|
||||||
<br />
|
<br />
|
||||||
<t t-out="user.signature or ''">--<br/>Mitchell Admin</t>
|
<div>--<br/><t t-out="user.signature or ''">Mitchell Admin</t></div>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</field>
|
</field>
|
||||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
|
||||||
<field name="auto_delete" eval="True"/>
|
<field name="auto_delete" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
@ -29,9 +29,11 @@
|
||||||
<record id="rating_project_request_email_template" model="mail.template">
|
<record id="rating_project_request_email_template" model="mail.template">
|
||||||
<field name="name">Project: Task Rating Request</field>
|
<field name="name">Project: Task Rating Request</field>
|
||||||
<field name="model_id" ref="project.model_project_task"/>
|
<field name="model_id" ref="project.model_project_task"/>
|
||||||
<field name="subject">{{ object.project_id.company_id.name }}: Satisfaction Survey</field>
|
<field name="active" eval="False"/>
|
||||||
|
<field name="subject">{{ object.project_id.company_id.name or user.env.company.name }}: Satisfaction Survey</field>
|
||||||
<field name="email_from">{{ (object._rating_get_operator().email_formatted if object._rating_get_operator() else user.email_formatted) }}</field>
|
<field name="email_from">{{ (object._rating_get_operator().email_formatted if object._rating_get_operator() else user.email_formatted) }}</field>
|
||||||
<field name="partner_to" >{{ object._rating_get_partner().id }}</field>
|
<field name="partner_to" >{{ object._rating_get_partner().id }}</field>
|
||||||
|
<field name="use_default_to" eval="False"/>
|
||||||
<field name="description">Set this template on a project stage to request feedback from your customers. Enable the "customer ratings" feature on the project</field>
|
<field name="description">Set this template on a project stage to request feedback from your customers. Enable the "customer ratings" feature on the project</field>
|
||||||
<field name="body_html" type="html">
|
<field name="body_html" type="html">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -46,7 +48,7 @@
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
Hello,<br/><br/>
|
Hello,<br/><br/>
|
||||||
</t>
|
</t>
|
||||||
Please take a moment to rate our services related to the task "<strong t-out="object.name or ''">Planning and budget</strong>"
|
Please take a moment to rate our services related to the <strong t-out="object.name or ''">Planning and budget</strong> task
|
||||||
<t t-if="object._rating_get_operator().name">
|
<t t-if="object._rating_get_operator().name">
|
||||||
assigned to <strong t-out="object._rating_get_operator().name or ''">Mitchell Admin</strong>.<br/>
|
assigned to <strong t-out="object._rating_get_operator().name or ''">Mitchell Admin</strong>.<br/>
|
||||||
</t>
|
</t>
|
||||||
|
|
@ -55,27 +57,27 @@
|
||||||
</t>
|
</t>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr><td style="text-align: center;">
|
<tr><td style="text-align: center;">
|
||||||
<table border="0" cellpadding="0" cellspacing="0" width="590" summary="o_mail_notification" style="width:100%; margin: 32px 0px 32px 0px;">
|
<table border="0" cellpadding="0" cellspacing="0" width="590" style="width:100%; margin: 32px 0px 32px 0px; display: inline-table;">
|
||||||
<tr><td style="font-size: 13px;">
|
<tr><td style="font-size: 13px;text-align:center;">
|
||||||
<strong>Tell us how you feel about our service</strong><br/>
|
<strong>Tell us how you feel about our services</strong><br/>
|
||||||
<span style="font-size: 12px; opacity: 0.5; color: #454748;">(click on one of these smileys)</span>
|
<span style="font-size: 12px; opacity: 0.5; color: #454748;">(click on one of these smileys)</span>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr><td style="font-size: 13px;">
|
<tr><td style="font-size: 13px;">
|
||||||
<table style="width:100%;text-align:center;margin-top:2rem;">
|
<table style="width:100%;text-align:center;margin-top:2rem;">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a t-attf-href="/rate/{{ access_token }}/5">
|
<a t-attf-href="/rate/{{ access_token }}/5" t-att-class="'pe-none' if object._rating_get_operator() else ''">
|
||||||
<img alt="Satisfied" src="/rating/static/src/img/rating_5.png" title="Satisfied"/>
|
<img alt="Happy" src="/rating/static/src/img/rating_5.png" title="Happy"/>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a t-attf-href="/rate/{{ access_token }}/3">
|
<a t-attf-href="/rate/{{ access_token }}/3" t-att-class="'pe-none' if object._rating_get_operator() else ''">
|
||||||
<img alt="Okay" src="/rating/static/src/img/rating_3.png" title="Okay"/>
|
<img alt="Neutral" src="/rating/static/src/img/rating_3.png" title="Neutral"/>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a t-attf-href="/rate/{{ access_token }}/1">
|
<a t-attf-href="/rate/{{ access_token }}/1" t-att-class="'pe-none' if object._rating_get_operator() else ''">
|
||||||
<img alt="Dissatisfied" src="/rating/static/src/img/rating_1.png" title="Dissatisfied"/>
|
<img alt="Unhappy" src="/rating/static/src/img/rating_1.png" title="Unhappy"/>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -84,12 +86,25 @@
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr><td valign="top" style="font-size: 13px;">
|
<tr><td valign="top" style="font-size: 13px;">
|
||||||
We appreciate your feedback. It helps us to improve continuously.
|
We appreciate your feedback. It helps us improve continuously.
|
||||||
<t t-if="object.project_id.rating_status == 'stage'">
|
<t t-if="object.stage_id.rating_status == 'stage'">
|
||||||
<br/><br/><span style="margin: 0px 0px 0px 0px; font-size: 12px; opacity: 0.5; color: #454748;">This customer survey has been sent because your task has been moved to the stage <b t-out="object.stage_id.name or ''">In progress</b></span>
|
<br/><span style="margin: 0; font-size: 12px; opacity: 0.5; color: #454748;">This satisfaction survey has been sent because your task has been moved to the <b t-out="object.stage_id.name or ''">In progress</b> stage</span>
|
||||||
</t>
|
</t>
|
||||||
<t t-if="object.project_id.rating_status == 'periodic'">
|
<t t-if="object.stage_id.rating_status == 'periodic'">
|
||||||
<br/><span style="margin: 0px 0px 0px 0px; font-size: 12px; opacity: 0.5; color: #454748;">This customer survey is sent <b t-out="object.project_id.rating_status_period or ''">Weekly</b> as long as the task is in the <b t-out="object.stage_id.name or ''">In progress</b> stage.</span>
|
<br/><span style="margin: 0; font-size: 12px; opacity: 0.5; color: #454748;">This satisfaction survey is sent <b t-out="object.stage_id.rating_status_period or ''">weekly</b> as long as the task is in the <b t-out="object.stage_id.name or ''">In progress</b> stage.</span>
|
||||||
|
</t>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td><br/>Best regards,</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
<t t-out="object.project_id.company_id.name or ''">YourCompany</t>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="opacity: 0.5;">
|
||||||
|
<t t-out="object.project_id.company_id.phone or ''">1 650-123-4567</t>
|
||||||
|
<t t-if="object.project_id.company_id.email">
|
||||||
|
| <a t-attf-href="mailto:{{ object.project_id.company_id.email }}" style="text-decoration:none; color: #454748;" t-out="object.project_id.company_id.email or ''">info@yourcompany.com</a>
|
||||||
|
</t>
|
||||||
|
<t t-if="object.project_id.company_id.website">
|
||||||
|
| <a t-attf-href="{{ object.project_id.company_id.website }}" style="text-decoration:none; color: #454748;" t-out="object.project_id.company_id.website or ''">http://www.example.com</a>
|
||||||
</t>
|
</t>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -102,10 +117,20 @@
|
||||||
|
|
||||||
<!-- You have been assigned email -->
|
<!-- You have been assigned email -->
|
||||||
<template id="project_message_user_assigned">
|
<template id="project_message_user_assigned">
|
||||||
<span>Dear <t t-esc="assignee_name"/>,</span>
|
<div>
|
||||||
|
Dear <t t-esc="assignee_name"/>,
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
<span style="margin-top: 8px;">You have been assigned to the <t t-esc="model_description or 'document'"/> <t t-esc="object.display_name"/>.</span>
|
<span style="margin-top: 8px;">You have been assigned to the <t t-esc="model_description or 'document'"/> <t t-esc="object.display_name"/>.</span>
|
||||||
<br/>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- You have been invited to follow the task -->
|
||||||
|
<template id="task_invitation_follower">
|
||||||
|
<div>
|
||||||
|
Hello <t t-out="partner_name"/>,
|
||||||
|
<br/><br/>
|
||||||
|
<span style="margin-top: 8px;">You have been invited to follow Task Document : <t t-out="object.display_name"/>.</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
<field name="model_id" ref="project.model_project_project"/>
|
<field name="model_id" ref="project.model_project_project"/>
|
||||||
<field name="subject">Project status - {{ object.name }}</field>
|
<field name="subject">Project status - {{ object.name }}</field>
|
||||||
<field name="email_from">{{ (object.partner_id.email_formatted if object.partner_id else user.email_formatted) }}</field>
|
<field name="email_from">{{ (object.partner_id.email_formatted if object.partner_id else user.email_formatted) }}</field>
|
||||||
<field name="partner_to" >{{ object.partner_id.id }}</field>
|
<field name="partner_to" eval="False"/>
|
||||||
|
<field name="use_default_to" eval="True"/>
|
||||||
<field name="description">Set on project's stages to inform customers when a project reaches that stage</field>
|
<field name="description">Set on project's stages to inform customers when a project reaches that stage</field>
|
||||||
<field name="body_html" type="html">
|
<field name="body_html" type="html">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -13,12 +14,11 @@
|
||||||
It is my pleasure to let you know that we have successfully completed the project "<strong t-out="object.name or ''">Renovations</strong>".
|
It is my pleasure to let you know that we have successfully completed the project "<strong t-out="object.name or ''">Renovations</strong>".
|
||||||
<t t-if="user.signature">
|
<t t-if="user.signature">
|
||||||
<br />
|
<br />
|
||||||
<t t-out="user.signature or ''">--<br/>Mitchell Admin</t>
|
<div>--<br/><t t-out="user.signature or ''">Mitchell Admin</t></div>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<br/><span style="margin: 0px 0px 0px 0px; font-size: 12px; opacity: 0.5; color: #454748;" groups="project.group_project_stages">You are receiving this email because your project has been moved to the stage <b t-out="object.stage_id.name or ''">Done</b></span>
|
<br/><span style="margin: 0px 0px 0px 0px; font-size: 12px; opacity: 0.5; color: #454748;" groups="project.group_project_stages">You are receiving this email because your project has been moved to the stage <b t-out="object.stage_id.name or ''">Done</b></span>
|
||||||
</field>
|
</field>
|
||||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
|
||||||
<field name="auto_delete" eval="True"/>
|
<field name="auto_delete" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo noupdate="1">
|
||||||
<!-- Project Stages -->
|
<!-- Project Stages -->
|
||||||
<record id="project_project_stage_0" model="project.project.stage">
|
<record id="project_project_stage_0" model="project.project.stage">
|
||||||
<field name="sequence">10</field>
|
<field name="sequence">10</field>
|
||||||
|
|
@ -19,7 +19,48 @@
|
||||||
|
|
||||||
<record id="project_project_stage_3" model="project.project.stage">
|
<record id="project_project_stage_3" model="project.project.stage">
|
||||||
<field name="sequence">25</field>
|
<field name="sequence">25</field>
|
||||||
<field name="name">Canceled</field>
|
<field name="name">Cancelled</field>
|
||||||
<field name="fold" eval="True"/>
|
<field name="fold" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- Project Task export template -->
|
||||||
|
<record id="project_task_export_template" model="ir.exports">
|
||||||
|
<field name="name">Tasks</field>
|
||||||
|
<field name="resource">project.task</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_export_template_line_id" model="ir.exports.line">
|
||||||
|
<field name="export_id" ref="project_task_export_template"/>
|
||||||
|
<field name="name">id</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_export_template_line_project_id" model="ir.exports.line">
|
||||||
|
<field name="export_id" ref="project_task_export_template"/>
|
||||||
|
<field name="name">project_id</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_export_template_line_name" model="ir.exports.line">
|
||||||
|
<field name="export_id" ref="project_task_export_template"/>
|
||||||
|
<field name="name">name</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_export_template_line_user_ids" model="ir.exports.line">
|
||||||
|
<field name="export_id" ref="project_task_export_template"/>
|
||||||
|
<field name="name">user_ids</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_export_template_line_stage_id" model="ir.exports.line">
|
||||||
|
<field name="export_id" ref="project_task_export_template"/>
|
||||||
|
<field name="name">stage_id</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_export_template_line_state" model="ir.exports.line">
|
||||||
|
<field name="export_id" ref="project_task_export_template"/>
|
||||||
|
<field name="name">state</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_task_export_template_line_tag_ids" model="ir.exports.line">
|
||||||
|
<field name="export_id" ref="project_task_export_template"/>
|
||||||
|
<field name="name">tag_ids</field>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="project_tour" model="web_tour.tour">
|
||||||
|
<field name="name">project_tour</field>
|
||||||
|
<field name="sequence">110</field>
|
||||||
|
<field name="rainbow_man_message">Congratulations, you are now a master of project management.</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +1,23 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import analytic_account
|
from . import account_analytic_account
|
||||||
from . import mail_message
|
from . import mail_message
|
||||||
from . import project_milestone
|
|
||||||
from . import project_project_stage
|
from . import project_project_stage
|
||||||
from . import project_task_recurrence
|
from . import project_task_recurrence
|
||||||
# `project_task_stage_personal` has to be loaded before `project`
|
# `project_task_stage_personal` has to be loaded before `project_project` and `project_milestone`
|
||||||
from . import project_task_stage_personal
|
from . import project_task_stage_personal
|
||||||
from . import project
|
from . import project_milestone
|
||||||
|
from . import project_project
|
||||||
|
from . import project_role
|
||||||
|
from . import project_task
|
||||||
|
from . import project_task_type
|
||||||
|
from . import project_tags
|
||||||
from . import project_collaborator
|
from . import project_collaborator
|
||||||
from . import project_update
|
from . import project_update
|
||||||
from . import company
|
|
||||||
from . import res_config_settings
|
from . import res_config_settings
|
||||||
from . import res_partner
|
from . import res_partner
|
||||||
from . import digest
|
from . import res_users_settings
|
||||||
|
from . import res_users
|
||||||
|
from . import digest_digest
|
||||||
from . import ir_ui_menu
|
from . import ir_ui_menu
|
||||||
|
|
|
||||||
|
|
@ -9,28 +9,24 @@ class AccountAnalyticAccount(models.Model):
|
||||||
_inherit = 'account.analytic.account'
|
_inherit = 'account.analytic.account'
|
||||||
_description = 'Analytic Account'
|
_description = 'Analytic Account'
|
||||||
|
|
||||||
project_ids = fields.One2many('project.project', 'analytic_account_id', string='Projects')
|
project_ids = fields.One2many('project.project', 'account_id', string='Projects', export_string_translation=False)
|
||||||
project_count = fields.Integer("Project Count", compute='_compute_project_count')
|
project_count = fields.Integer("Project Count", compute='_compute_project_count', export_string_translation=False)
|
||||||
|
|
||||||
@api.depends('project_ids')
|
@api.depends('project_ids')
|
||||||
def _compute_project_count(self):
|
def _compute_project_count(self):
|
||||||
project_data = self.env['project.project']._read_group([('analytic_account_id', 'in', self.ids)], ['analytic_account_id'], ['analytic_account_id'])
|
project_data = self.env['project.project']._read_group([('account_id', 'in', self.ids)], ['account_id'], ['__count'])
|
||||||
mapping = {m['analytic_account_id'][0]: m['analytic_account_id_count'] for m in project_data}
|
mapping = {analytic_account.id: count for analytic_account, count in project_data}
|
||||||
for account in self:
|
for account in self:
|
||||||
account.project_count = mapping.get(account.id, 0)
|
account.project_count = mapping.get(account.id, 0)
|
||||||
|
|
||||||
@api.constrains('company_id')
|
|
||||||
def _check_company_id(self):
|
|
||||||
for record in self:
|
|
||||||
if record.company_id and not all(record.company_id == c for c in record.project_ids.mapped('company_id')):
|
|
||||||
raise UserError(_('You cannot change the company of an analytic account if it is related to a project.'))
|
|
||||||
|
|
||||||
@api.ondelete(at_uninstall=False)
|
@api.ondelete(at_uninstall=False)
|
||||||
def _unlink_except_existing_tasks(self):
|
def _unlink_except_existing_tasks(self):
|
||||||
projects = self.env['project.project'].search([('analytic_account_id', 'in', self.ids)])
|
has_tasks = self.env['project.task'].search_count(
|
||||||
has_tasks = self.env['project.task'].search_count([('project_id', 'in', projects.ids)])
|
[('project_id.account_id', 'in', self.ids)],
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
if has_tasks:
|
if has_tasks:
|
||||||
raise UserError(_('Please remove existing tasks in the project linked to the accounts you want to delete.'))
|
raise UserError(_("Before we can bid farewell to these accounts, you need to tidy up the projects linked to them by removing their existing tasks!"))
|
||||||
|
|
||||||
def action_view_projects(self):
|
def action_view_projects(self):
|
||||||
kanban_view_id = self.env.ref('project.view_project_kanban').id
|
kanban_view_id = self.env.ref('project.view_project_kanban').id
|
||||||
|
|
@ -38,7 +34,7 @@ class AccountAnalyticAccount(models.Model):
|
||||||
"type": "ir.actions.act_window",
|
"type": "ir.actions.act_window",
|
||||||
"res_model": "project.project",
|
"res_model": "project.project",
|
||||||
"views": [[kanban_view_id, "kanban"], [False, "form"]],
|
"views": [[kanban_view_id, "kanban"], [False, "form"]],
|
||||||
"domain": [['analytic_account_id', '=', self.id]],
|
"domain": [['account_id', '=', self.id]],
|
||||||
"context": {"create": False},
|
"context": {"create": False},
|
||||||
"name": _("Projects"),
|
"name": _("Projects"),
|
||||||
}
|
}
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from odoo import fields, models
|
|
||||||
|
|
||||||
|
|
||||||
class ResCompany(models.Model):
|
|
||||||
_name = "res.company"
|
|
||||||
_inherit = "res.company"
|
|
||||||
|
|
||||||
analytic_plan_id = fields.Many2one(
|
|
||||||
'account.analytic.plan',
|
|
||||||
string="Default Plan",
|
|
||||||
check_company=True,
|
|
||||||
readonly=False,
|
|
||||||
compute="_compute_analytic_plan_id",
|
|
||||||
help="Default Plan for a new analytic account for projects")
|
|
||||||
|
|
||||||
def _compute_analytic_plan_id(self):
|
|
||||||
for company in self:
|
|
||||||
default_plan = self.env['ir.config_parameter'].with_company(company).sudo().get_param("default_analytic_plan_id_%s" % company.id)
|
|
||||||
company.analytic_plan_id = int(default_plan) if default_plan else False
|
|
||||||
if not company.analytic_plan_id:
|
|
||||||
company.analytic_plan_id = self.env['account.analytic.plan'].with_company(company)._get_default()
|
|
||||||
|
|
||||||
def write(self, values):
|
|
||||||
for company in self:
|
|
||||||
if 'analytic_plan_id' in values:
|
|
||||||
self.env['ir.config_parameter'].sudo().set_param("default_analytic_plan_id_%s" % company.id, values['analytic_plan_id'])
|
|
||||||
return super().write(values)
|
|
||||||
|
|
@ -5,26 +5,23 @@ from odoo import fields, models, _
|
||||||
from odoo.exceptions import AccessError
|
from odoo.exceptions import AccessError
|
||||||
|
|
||||||
|
|
||||||
class Digest(models.Model):
|
class DigestDigest(models.Model):
|
||||||
_inherit = 'digest.digest'
|
_inherit = 'digest.digest'
|
||||||
|
|
||||||
kpi_project_task_opened = fields.Boolean('Open Tasks')
|
kpi_project_task_opened = fields.Boolean('Open Tasks')
|
||||||
kpi_project_task_opened_value = fields.Integer(compute='_compute_project_task_opened_value')
|
kpi_project_task_opened_value = fields.Integer(compute='_compute_project_task_opened_value', export_string_translation=False)
|
||||||
|
|
||||||
def _compute_project_task_opened_value(self):
|
def _compute_project_task_opened_value(self):
|
||||||
if not self.env.user.has_group('project.group_project_user'):
|
if not self.env.user.has_group('project.group_project_user'):
|
||||||
raise AccessError(_("Do not have access, skip this data for user's digest email"))
|
raise AccessError(_("Do not have access, skip this data for user's digest email"))
|
||||||
for record in self:
|
|
||||||
start, end, company = record._get_kpi_compute_parameters()
|
self._calculate_company_based_kpi(
|
||||||
record.kpi_project_task_opened_value = self.env['project.task'].search_count([
|
'project.task',
|
||||||
('stage_id.fold', '=', False),
|
'kpi_project_task_opened_value',
|
||||||
('create_date', '>=', start),
|
additional_domain=[('stage_id.fold', '=', False), ('project_id', '!=', False)],
|
||||||
('create_date', '<', end),
|
)
|
||||||
('company_id', '=', company.id),
|
|
||||||
('display_project_id', '!=', False),
|
|
||||||
])
|
|
||||||
|
|
||||||
def _compute_kpis_actions(self, company, user):
|
def _compute_kpis_actions(self, company, user):
|
||||||
res = super(Digest, self)._compute_kpis_actions(company, user)
|
res = super()._compute_kpis_actions(company, user)
|
||||||
res['kpi_project_task_opened'] = 'project.open_view_project_all&menu_id=%s' % self.env.ref('project.menu_main_pm').id
|
res['kpi_project_task_opened'] = 'project.open_view_project_all?menu_id=%s' % self.env.ref('project.menu_main_pm').id
|
||||||
return res
|
return res
|
||||||
|
|
@ -1,18 +1,9 @@
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import models
|
from odoo import models
|
||||||
from odoo.tools.sql import create_index
|
|
||||||
|
|
||||||
|
|
||||||
class MailMessage(models.Model):
|
class MailMessage(models.Model):
|
||||||
_inherit = 'mail.message'
|
_inherit = 'mail.message'
|
||||||
|
|
||||||
def init(self):
|
_date_res_id_id_for_burndown_chart = models.Index("(date, res_id, id) WHERE model = 'project.task' AND message_type = 'notification'")
|
||||||
super().init()
|
|
||||||
create_index(
|
|
||||||
self._cr,
|
|
||||||
'mail_message_date_res_id_id_for_burndown_chart',
|
|
||||||
self._table,
|
|
||||||
['date', 'res_id', 'id'],
|
|
||||||
where="model='project.task' AND message_type='notification'"
|
|
||||||
)
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models
|
||||||
|
|
@ -8,17 +7,20 @@ class ProjectCollaborator(models.Model):
|
||||||
_name = 'project.collaborator'
|
_name = 'project.collaborator'
|
||||||
_description = 'Collaborators in project shared'
|
_description = 'Collaborators in project shared'
|
||||||
|
|
||||||
project_id = fields.Many2one('project.project', 'Project Shared', domain=[('privacy_visibility', '=', 'portal')], required=True, readonly=True)
|
project_id = fields.Many2one('project.project', 'Project Shared', domain=[('privacy_visibility', '=', 'portal'), ('is_template', '=', False)], required=True, readonly=True, export_string_translation=False)
|
||||||
partner_id = fields.Many2one('res.partner', 'Collaborator', required=True, readonly=True)
|
partner_id = fields.Many2one('res.partner', 'Collaborator', required=True, readonly=True, export_string_translation=False)
|
||||||
partner_email = fields.Char(related='partner_id.email')
|
partner_email = fields.Char(related='partner_id.email', export_string_translation=False)
|
||||||
|
limited_access = fields.Boolean('Limited Access', default=False, export_string_translation=False)
|
||||||
|
|
||||||
_sql_constraints = [
|
_unique_collaborator = models.Constraint(
|
||||||
('unique_collaborator', 'UNIQUE(project_id, partner_id)', 'A collaborator cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.'),
|
'UNIQUE(project_id, partner_id)',
|
||||||
]
|
'A collaborator cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.',
|
||||||
|
)
|
||||||
|
|
||||||
def name_get(self):
|
@api.depends('project_id', 'partner_id')
|
||||||
collaborator_search_read = self.search_read([('id', 'in', self.ids)], ['id', 'project_id', 'partner_id'])
|
def _compute_display_name(self):
|
||||||
return [(collaborator['id'], '%s - %s' % (collaborator['project_id'][1], collaborator['partner_id'][1])) for collaborator in collaborator_search_read]
|
for collaborator in self:
|
||||||
|
collaborator.display_name = f'{collaborator.project_id.display_name} - {collaborator.partner_id.display_name}'
|
||||||
|
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,35 @@
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models
|
||||||
|
from odoo.tools import format_date
|
||||||
|
|
||||||
|
from .project_task import CLOSED_STATES
|
||||||
|
|
||||||
|
|
||||||
class ProjectMilestone(models.Model):
|
class ProjectMilestone(models.Model):
|
||||||
_name = 'project.milestone'
|
_name = 'project.milestone'
|
||||||
_description = "Project Milestone"
|
_description = "Project Milestone"
|
||||||
_inherit = ['mail.thread']
|
_inherit = ['mail.thread']
|
||||||
_order = 'deadline, is_reached desc, name'
|
_order = 'sequence, deadline, is_reached desc, name'
|
||||||
|
|
||||||
def _get_default_project_id(self):
|
def _get_default_project_id(self):
|
||||||
return self.env.context.get('default_project_id') or self.env.context.get('active_id')
|
return self.env.context.get('default_project_id') or self.env.context.get('active_id')
|
||||||
|
|
||||||
name = fields.Char(required=True)
|
name = fields.Char(required=True)
|
||||||
project_id = fields.Many2one('project.project', required=True, default=_get_default_project_id, ondelete='cascade')
|
sequence = fields.Integer('Sequence', default=10)
|
||||||
|
project_id = fields.Many2one('project.project', required=True, default=_get_default_project_id, domain=[('is_template', '=', False)], index=True, ondelete='cascade')
|
||||||
deadline = fields.Date(tracking=True, copy=False)
|
deadline = fields.Date(tracking=True, copy=False)
|
||||||
is_reached = fields.Boolean(string="Reached", default=False, copy=False)
|
is_reached = fields.Boolean(string="Reached", default=False, copy=False)
|
||||||
reached_date = fields.Date(compute='_compute_reached_date', store=True)
|
reached_date = fields.Date(compute='_compute_reached_date', store=True, export_string_translation=False)
|
||||||
task_ids = fields.One2many('project.task', 'milestone_id', 'Tasks')
|
task_ids = fields.One2many('project.task', 'milestone_id', 'Tasks', export_string_translation=False)
|
||||||
|
project_allow_milestones = fields.Boolean(compute='_compute_project_allow_milestones', search='_search_project_allow_milestones', compute_sudo=True, export_string_translation=False)
|
||||||
|
|
||||||
# computed non-stored fields
|
# computed non-stored fields
|
||||||
is_deadline_exceeded = fields.Boolean(compute="_compute_is_deadline_exceeded")
|
is_deadline_exceeded = fields.Boolean(compute="_compute_is_deadline_exceeded", export_string_translation=False)
|
||||||
is_deadline_future = fields.Boolean(compute="_compute_is_deadline_future")
|
is_deadline_future = fields.Boolean(compute="_compute_is_deadline_future", export_string_translation=False)
|
||||||
task_count = fields.Integer('# of Tasks', compute='_compute_task_count', groups='project.group_project_milestone')
|
task_count = fields.Integer('# of Tasks', compute='_compute_task_count', groups='project.group_project_milestone', export_string_translation=False)
|
||||||
can_be_marked_as_done = fields.Boolean(compute='_compute_can_be_marked_as_done', groups='project.group_project_milestone')
|
done_task_count = fields.Integer('# of Done Tasks', compute='_compute_task_count', groups='project.group_project_milestone', export_string_translation=False)
|
||||||
|
can_be_marked_as_done = fields.Boolean(compute='_compute_can_be_marked_as_done', export_string_translation=False)
|
||||||
|
|
||||||
@api.depends('is_reached')
|
@api.depends('is_reached')
|
||||||
def _compute_reached_date(self):
|
def _compute_reached_date(self):
|
||||||
|
|
@ -45,36 +52,51 @@ class ProjectMilestone(models.Model):
|
||||||
|
|
||||||
@api.depends('task_ids.milestone_id')
|
@api.depends('task_ids.milestone_id')
|
||||||
def _compute_task_count(self):
|
def _compute_task_count(self):
|
||||||
task_read_group = self.env['project.task']._read_group([('milestone_id', 'in', self.ids), ('allow_milestones', '=', True)], ['milestone_id'], ['milestone_id'])
|
all_and_done_task_count_per_milestone = {
|
||||||
task_count_per_milestone = {res['milestone_id'][0]: res['milestone_id_count'] for res in task_read_group}
|
milestone.id: (count, sum(state in CLOSED_STATES for state in state_list))
|
||||||
|
for milestone, count, state_list in self.env['project.task']._read_group(
|
||||||
|
[('milestone_id', 'in', self.ids), ('allow_milestones', '=', True)],
|
||||||
|
['milestone_id'], ['__count', 'state:array_agg'],
|
||||||
|
)
|
||||||
|
}
|
||||||
for milestone in self:
|
for milestone in self:
|
||||||
milestone.task_count = task_count_per_milestone.get(milestone.id, 0)
|
milestone.task_count, milestone.done_task_count = all_and_done_task_count_per_milestone.get(milestone.id, (0, 0))
|
||||||
|
|
||||||
def _compute_can_be_marked_as_done(self):
|
def _compute_can_be_marked_as_done(self):
|
||||||
if not any(self._ids):
|
if not any(self._ids):
|
||||||
for milestone in self:
|
for milestone in self:
|
||||||
milestone.can_be_marked_as_done = not milestone.is_reached and all(milestone.task_ids.is_closed)
|
milestone.can_be_marked_as_done = not milestone.is_reached and all(milestone.task_ids.mapped(lambda t: t.is_closed))
|
||||||
return
|
return
|
||||||
|
|
||||||
unreached_milestones = self.filtered(lambda milestone: not milestone.is_reached)
|
unreached_milestones = self.filtered(lambda milestone: not milestone.is_reached)
|
||||||
(self - unreached_milestones).can_be_marked_as_done = False
|
(self - unreached_milestones).can_be_marked_as_done = False
|
||||||
if unreached_milestones:
|
task_read_group = self.env['project.task']._read_group(
|
||||||
task_read_group = self.env['project.task']._read_group(
|
[('milestone_id', 'in', unreached_milestones.ids)],
|
||||||
[('milestone_id', 'in', unreached_milestones.ids)],
|
['milestone_id', 'state'],
|
||||||
['milestone_id', 'is_closed', 'task_count:count(id)'],
|
['__count'],
|
||||||
['milestone_id', 'is_closed'],
|
)
|
||||||
lazy=False,
|
task_count_per_milestones = defaultdict(lambda: (0, 0))
|
||||||
)
|
for milestone, state, count in task_read_group:
|
||||||
task_count_per_milestones = defaultdict(lambda: (0, 0))
|
opened_task_count, closed_task_count = task_count_per_milestones[milestone.id]
|
||||||
for res in task_read_group:
|
if state in CLOSED_STATES:
|
||||||
opened_task_count, closed_task_count = task_count_per_milestones[res['milestone_id'][0]]
|
closed_task_count += count
|
||||||
if res['is_closed']:
|
else:
|
||||||
closed_task_count += res['task_count']
|
opened_task_count += count
|
||||||
else:
|
task_count_per_milestones[milestone.id] = opened_task_count, closed_task_count
|
||||||
opened_task_count += res['task_count']
|
for milestone in unreached_milestones:
|
||||||
task_count_per_milestones[res['milestone_id'][0]] = opened_task_count, closed_task_count
|
opened_task_count, closed_task_count = task_count_per_milestones[milestone.id]
|
||||||
for milestone in unreached_milestones:
|
milestone.can_be_marked_as_done = closed_task_count > 0 and not opened_task_count
|
||||||
opened_task_count, closed_task_count = task_count_per_milestones[milestone.id]
|
|
||||||
milestone.can_be_marked_as_done = closed_task_count > 0 and not opened_task_count
|
@api.depends('project_id.allow_milestones')
|
||||||
|
def _compute_project_allow_milestones(self):
|
||||||
|
for milestone in self:
|
||||||
|
milestone.project_allow_milestones = milestone.project_id.allow_milestones
|
||||||
|
|
||||||
|
def _search_project_allow_milestones(self, operator, value):
|
||||||
|
query = self.env['project.project'].sudo()._search([
|
||||||
|
('allow_milestones', operator, value),
|
||||||
|
])
|
||||||
|
return [('project_id', 'in', query)]
|
||||||
|
|
||||||
def toggle_is_reached(self, is_reached):
|
def toggle_is_reached(self, is_reached):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
@ -94,7 +116,7 @@ class ProjectMilestone(models.Model):
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_fields_to_export(self):
|
def _get_fields_to_export(self):
|
||||||
return ['id', 'name', 'deadline', 'is_reached', 'reached_date', 'is_deadline_exceeded', 'is_deadline_future', 'can_be_marked_as_done']
|
return ['id', 'name', 'deadline', 'is_reached', 'reached_date', 'is_deadline_exceeded', 'is_deadline_future', 'can_be_marked_as_done', 'sequence']
|
||||||
|
|
||||||
def _get_data(self):
|
def _get_data(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
@ -103,12 +125,19 @@ class ProjectMilestone(models.Model):
|
||||||
def _get_data_list(self):
|
def _get_data_list(self):
|
||||||
return [ms._get_data() for ms in self]
|
return [ms._get_data() for ms in self]
|
||||||
|
|
||||||
@api.returns('self', lambda value: value.id)
|
|
||||||
def copy(self, default=None):
|
def copy(self, default=None):
|
||||||
if default is None:
|
default = dict(default or {})
|
||||||
default = {}
|
new_milestones = super().copy(default)
|
||||||
milestone_copy = super(ProjectMilestone, self).copy(default)
|
milestone_mapping = self.env.context.get('milestone_mapping', {})
|
||||||
if self.project_id.allow_milestones:
|
for old_milestone, new_milestone in zip(self, new_milestones):
|
||||||
milestone_mapping = self.env.context.get('milestone_mapping', {})
|
if old_milestone.project_id.allow_milestones:
|
||||||
milestone_mapping[self.id] = milestone_copy.id
|
milestone_mapping[old_milestone.id] = new_milestone.id
|
||||||
return milestone_copy
|
return new_milestones
|
||||||
|
|
||||||
|
def _compute_display_name(self):
|
||||||
|
super()._compute_display_name()
|
||||||
|
if not self.env.context.get('display_milestone_deadline'):
|
||||||
|
return
|
||||||
|
for milestone in self:
|
||||||
|
if milestone.deadline:
|
||||||
|
milestone.display_name = f'{milestone.display_name} - {format_date(self.env, milestone.deadline)}'
|
||||||
|
|
|
||||||
1443
odoo-bringout-oca-ocb-project/project/models/project_project.py
Normal file
1443
odoo-bringout-oca-ocb-project/project/models/project_project.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,17 +1,83 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import fields, models
|
from odoo import fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
class ProjectProjectStage(models.Model):
|
class ProjectProjectStage(models.Model):
|
||||||
_name = 'project.project.stage'
|
_name = 'project.project.stage'
|
||||||
_description = 'Project Stage'
|
_description = 'Project Stage'
|
||||||
_order = 'sequence, id'
|
_order = 'sequence, id'
|
||||||
|
|
||||||
active = fields.Boolean(default=True)
|
active = fields.Boolean(default=True, export_string_translation=False)
|
||||||
sequence = fields.Integer(default=50)
|
sequence = fields.Integer(default=50, export_string_translation=False)
|
||||||
name = fields.Char(required=True, translate=True)
|
name = fields.Char(required=True, translate=True)
|
||||||
mail_template_id = fields.Many2one('mail.template', string='Email Template', domain=[('model', '=', 'project.project')],
|
mail_template_id = fields.Many2one('mail.template', string='Email Template', domain=[('model', '=', 'project.project')],
|
||||||
help="If set, an email will be automatically sent to the customer when the project reaches this stage.")
|
help="If set, an email will be automatically sent to the customer when the project reaches this stage.")
|
||||||
fold = fields.Boolean('Folded in Kanban',
|
fold = fields.Boolean('Folded',
|
||||||
help="If enabled, this stage will be displayed as folded in the Kanban view of your projects. Projects in a folded stage are considered as closed.")
|
help="If enabled, this stage will be displayed as folded in the Kanban and List views of your projects. Projects in a folded stage are considered as closed.")
|
||||||
|
company_id = fields.Many2one('res.company', string="Company")
|
||||||
|
color = fields.Integer(string='Color', export_string_translation=False)
|
||||||
|
|
||||||
|
def copy_data(self, default=None):
|
||||||
|
vals_list = super().copy_data(default=default)
|
||||||
|
return [dict(vals, name=self.env._("%s (copy)", stage.name)) for stage, vals in zip(self, vals_list)]
|
||||||
|
|
||||||
|
def unlink_wizard(self, stage_view=False):
|
||||||
|
wizard = self.with_context(active_test=False).env['project.project.stage.delete.wizard'].create({
|
||||||
|
'stage_ids': self.ids
|
||||||
|
})
|
||||||
|
|
||||||
|
context = dict(self.env.context)
|
||||||
|
context['stage_view'] = stage_view
|
||||||
|
return {
|
||||||
|
'name': _('Delete Project Stage'),
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_model': 'project.project.stage.delete.wizard',
|
||||||
|
'views': [(self.env.ref('project.view_project_project_stage_delete_wizard').id, 'form')],
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_id': wizard.id,
|
||||||
|
'target': 'new',
|
||||||
|
'context': context,
|
||||||
|
}
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if vals.get('company_id'):
|
||||||
|
# Checking if there is a project with a different company_id than the target one. If so raise an error since this is not allowed
|
||||||
|
project = self.env['project.project'].search(['&', ('stage_id', 'in', self.ids), ('company_id', '!=', vals['company_id'])], limit=1)
|
||||||
|
if project:
|
||||||
|
company = self.env['res.company'].browse(vals['company_id'])
|
||||||
|
raise UserError(
|
||||||
|
_("You are not able to switch the company of this stage to %(company_name)s since it currently "
|
||||||
|
"includes projects associated with %(project_company_name)s. Please ensure that this stage exclusively "
|
||||||
|
"consists of projects linked to %(company_name)s.",
|
||||||
|
company_name=company.name,
|
||||||
|
project_company_name=project.company_id.name or "no company"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'active' in vals and not vals['active']:
|
||||||
|
self.env['project.project'].search([('stage_id', 'in', self.ids)]).write({'active': False})
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
|
def action_unarchive(self):
|
||||||
|
res = super().action_unarchive()
|
||||||
|
stage_active = self.filtered(self._active_name)
|
||||||
|
if stage_active and self.env['project.project'].with_context(active_test=False).search_count(
|
||||||
|
[('active', '=', False), ('stage_id', 'in', stage_active.ids)], limit=1
|
||||||
|
):
|
||||||
|
wizard = self.env['project.project.stage.delete.wizard'].create({
|
||||||
|
'stage_ids': stage_active.ids,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'name': _('Unarchive Projects'),
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_model': 'project.project.stage.delete.wizard',
|
||||||
|
'views': [(self.env.ref('project.view_project_project_stage_unarchive_wizard').id, 'form')],
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_id': wizard.id,
|
||||||
|
'target': 'new',
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
|
|
||||||
20
odoo-bringout-oca-ocb-project/project/models/project_role.py
Normal file
20
odoo-bringout-oca-ocb-project/project/models/project_role.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectRole(models.Model):
|
||||||
|
_name = 'project.role'
|
||||||
|
_description = 'Project Role'
|
||||||
|
|
||||||
|
def _get_default_color(self):
|
||||||
|
return randint(1, 11)
|
||||||
|
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
name = fields.Char(required=True, translate=True)
|
||||||
|
color = fields.Integer(default=_get_default_color)
|
||||||
|
sequence = fields.Integer(export_string_translation=False)
|
||||||
|
|
||||||
|
def copy_data(self, default=None):
|
||||||
|
vals_list = super().copy_data(default=default)
|
||||||
|
return [dict(vals, name=self.env._('%s (copy)', role.name)) for role, vals in zip(self, vals_list)]
|
||||||
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