From d8a0912bbfd58ffc8653107315269f64d0c0d38b Mon Sep 17 00:00:00 2001 From: William Braeckman Date: Mon, 19 Jul 2021 12:04:45 +0000 Subject: [PATCH] [IMP] project: track dependencies in tasks Tasks that are blocked by other tasks don't get any other information from those tasks. In order to get updates on the state of the blocking tasks we now track some fields (determined by the `task_dependency_tracking` attribute) in the parent's chatter. Task ID: 2601448 closes odoo/odoo#73965 Signed-off-by: Yannick Tivisse (yti) --- addons/project/__manifest__.py | 1 + addons/project/data/mail_data.xml | 15 ++++++ addons/project/models/project.py | 50 +++++++++++++++++-- addons/project/models/res_config_settings.py | 7 +++ .../project/tests/test_task_dependencies.py | 42 ++++++++++++++++ .../project/views/project_task_templates.xml | 10 ++++ 6 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 addons/project/views/project_task_templates.xml diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 75872e5f323..6df8d13fcc9 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -36,6 +36,7 @@ 'views/res_config_settings_views.xml', 'views/mail_activity_views.xml', 'views/project_portal_templates.xml', + 'views/project_task_templates.xml', 'data/ir_cron_data.xml', 'data/mail_data.xml', 'data/mail_template_data.xml', diff --git a/addons/project/data/mail_data.xml b/addons/project/data/mail_data.xml index 25310bbd0e9..e6dc9a9218a 100644 --- a/addons/project/data/mail_data.xml +++ b/addons/project/data/mail_data.xml @@ -31,6 +31,12 @@ project.task + + Task Dependency Changes + project.task + + + Task Created @@ -72,4 +78,13 @@ project_id + + Task Dependency Changes + 15 + project.project + + + project_id + + diff --git a/addons/project/models/project.py b/addons/project/models/project.py index c7902132f9e..16512b2dd7d 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -8,7 +8,7 @@ from datetime import timedelta, datetime from random import randint from odoo import api, fields, models, tools, SUPERUSER_ID, _ -from odoo.exceptions import UserError, AccessError, ValidationError, RedirectWarning +from odoo.exceptions import UserError, ValidationError from odoo.osv.expression import OR from .project_task_recurrence import DAYS, WEEKS @@ -696,19 +696,19 @@ class Task(models.Model): stage_id = fields.Many2one('project.task.type', string='Stage', compute='_compute_stage_id', store=True, readonly=False, ondelete='restrict', tracking=True, index=True, default=_get_default_stage_id, group_expand='_read_group_stage_ids', - domain="[('project_ids', '=', project_id)]", copy=False) + domain="[('project_ids', '=', project_id)]", copy=False, task_dependency_tracking=True) tag_ids = fields.Many2many('project.tags', string='Tags') kanban_state = fields.Selection([ ('normal', 'In Progress'), ('done', 'Ready'), ('blocked', 'Blocked')], string='Status', copy=False, default='normal', required=True) - kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', tracking=True) + kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', tracking=True, task_dependency_tracking=True) create_date = fields.Datetime("Created On", readonly=True, index=True) write_date = fields.Datetime("Last Updated On", readonly=True, index=True) date_end = fields.Datetime(string='Ending Date', index=True, copy=False) date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True) - date_deadline = fields.Date(string='Deadline', index=True, copy=False, tracking=True) + date_deadline = fields.Date(string='Deadline', index=True, copy=False, tracking=True, task_dependency_tracking=True) date_last_stage_update = fields.Datetime(string='Last Stage Update', index=True, copy=False, @@ -1103,6 +1103,19 @@ class Task(models.Model): ) return super(Task, self).get_empty_list_help(help) + def _valid_field_parameter(self, field, name): + # If the field has `task_dependency_tracking` on we track the changes made in the dependent task on the parent task + return name == 'task_dependency_tracking' or super()._valid_field_parameter(field, name) + + @tools.ormcache('self.env.uid', 'self.env.su') + def _get_depends_tracked_fields(self): + """ Returns the set of tracked field names for the current model. + Those fields are the ones tracked in the parent task when using task dependencies. + + See :meth:`mail.models.MailThread._get_tracked_fields`""" + fields = {name for name, field in self._fields.items() if getattr(field, 'task_dependency_tracking', None)} + return fields and set(self.fields_get(fields)) + # ---------------------------------------- # Case management # ---------------------------------------- @@ -1305,6 +1318,35 @@ class Task(models.Model): # Mail gateway # --------------------------------------------------- + def _mail_track(self, tracked_fields, initial_values): + result = super()._mail_track(tracked_fields, initial_values) + changes, tracking_value_ids = result + # Track changes on depending tasks + depends_tracked_fields = self._get_depends_tracked_fields() + depends_changes = changes & depends_tracked_fields + if depends_changes and self.user_has_groups('project.group_project_task_dependencies') and self.allow_task_dependencies: + parent_ids = self.env['project.task'].search([('depend_on_ids', 'in', self.ids)]) + if parent_ids: + fields_to_ids = self.env['ir.model.fields']._get_ids('project.task') + field_ids = [fields_to_ids.get(name) for name in depends_changes] + depends_tracking_value_ids = [ + tracking_values for tracking_values in tracking_value_ids + if tracking_values[2]['field'] in field_ids + ] + subtype = self.env.ref('project.mt_task_dependency_change') + # We want to include the original subtype message coming from the child task + # for example when the stage changes the message in the chatter starts with 'Stage Changed' + child_subtype = self._track_subtype(dict((col_name, initial_values[col_name]) for col_name in changes)) + child_subtype_info = child_subtype.description or child_subtype.name if child_subtype else False + # NOTE: the subtype does not have a description on purpose, otherwise the description would be put + # at the end of the message instead of at the top, we use the name here + body = self.env['ir.qweb']._render('project.task_track_depending_tasks', { + 'child': self, + 'child_subtype': child_subtype_info, + }) + parent_ids.message_post(body=body, subtype_id=subtype.id, tracking_value_ids=depends_tracking_value_ids) + return result + def _track_template(self, changes): res = super(Task, self)._track_template(changes) test_task = self[0] diff --git a/addons/project/models/res_config_settings.py b/addons/project/models/res_config_settings.py index 13d4837002a..1db31a61037 100644 --- a/addons/project/models/res_config_settings.py +++ b/addons/project/models/res_config_settings.py @@ -44,4 +44,11 @@ class ResConfigSettings(models.TransientModel): # update for basic projects update_projects(projects.filtered_domain(self._get_basic_project_domain()), basic_project_features) + # Hide the task dependency changes subtype when the dependency setting is disabled + task_dep_change_subtype_id = self.env.ref('project.mt_task_dependency_change') + project_task_dep_change_subtype_id = self.env.ref('project.mt_project_task_dependency_change') + if task_dep_change_subtype_id.hidden != (not self['group_project_task_dependencies']): + task_dep_change_subtype_id.hidden = not self['group_project_task_dependencies'] + project_task_dep_change_subtype_id.hidden = not self['group_project_task_dependencies'] + super(ResConfigSettings, self).set_values() diff --git a/addons/project/tests/test_task_dependencies.py b/addons/project/tests/test_task_dependencies.py index 42eda7fecb2..5ddc6e640c3 100644 --- a/addons/project/tests/test_task_dependencies.py +++ b/addons/project/tests/test_task_dependencies.py @@ -6,6 +6,7 @@ from odoo.tests import tagged from odoo.addons.project.tests.test_project_base import TestProjectCommon +from datetime import date @tagged('-at_install', 'post_install', 'task_dependencies') class TestTaskDependencies(TestProjectCommon): @@ -23,6 +24,11 @@ class TestTaskDependencies(TestProjectCommon): 'project_id': cls.project_pigs.id, }) + def flush_tracking(self): + """ Force the creation of tracking values. """ + self.env['base'].flush() + self.cr.precommit.run() + def test_task_dependencies(self): """ Test the task dependencies feature @@ -85,3 +91,39 @@ class TestTaskDependencies(TestProjectCommon): 'depend_on_ids': [Command.link(self.task_1.id)], }) self.assertEqual(len(self.task_2.depend_on_ids), 1, "The number of dependencies should no change in the task 2 because of a cyclic dependency.") + + def test_tracking_dependencies(self): + # Enable the company setting + self.env['res.config.settings'].create({ + 'group_project_task_dependencies': True + }).execute() + self.task_1.write({ + 'depend_on_ids': [Command.link(self.task_2.id)] + }) + self.cr.precommit.clear() + # Check that changing a dependency tracked field in task_2 logs a message in task_1. + self.task_2.write({'date_deadline': date(1983, 3, 1)}) # + 1 message in task_1 and task_2 + self.flush_tracking() + self.assertEqual(len(self.task_1.message_ids), 1, + 'Changing the deadline on task 2 should have logged a message in task 1.') + + # Check that changing a dependency tracked field in task_1 does not log a message in task_2. + self.task_1.date_deadline = date(2020, 1, 2) # + 1 message in task_1 + self.flush_tracking() + self.assertEqual(len(self.task_2.message_ids), 1, + 'Changing the deadline on task 1 should not have logged a message in task 2.') + + # Check that changing a field that is not tracked at all on task 2 does not impact task 1. + self.task_2.color = 100 # no new message + self.flush_tracking() + self.assertEqual(len(self.task_1.message_ids), 2, + 'Changing the color on task 2 should not have logged a message in task 1 since it is not tracked.') + + # Check that changing multiple fields does not log more than one message. + self.task_2.write({ + 'date_deadline': date(2020, 1, 1), + 'kanban_state': 'blocked', + }) # + 1 message in task_1 and task_2 + self.flush_tracking() + self.assertEqual(len(self.task_1.message_ids), 3, + 'Changing multiple fields on task 2 should only log one message in task 1.') diff --git a/addons/project/views/project_task_templates.xml b/addons/project/views/project_task_templates.xml new file mode 100644 index 00000000000..6bfef7c0e09 --- /dev/null +++ b/addons/project/views/project_task_templates.xml @@ -0,0 +1,10 @@ + + + +