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 @@ + + + +