Skip to content

Commit

Permalink
[IMP] project: drop project substask constraint
Browse files Browse the repository at this point in the history
This commit improves subtask mecanism, on several points:
1) Drop the project constraint: before this commit, creating
subtask in non parent project was not possible. Now you can move
subtask to any project.
The default project of a subtask is the one from the parent.
2) When setting a parent task, we want to force some field to have
the same value as its parent, like `partner_id` or the `sale_line_id`.
The idea is here, we work for the same client; a subtask count for the
same goal (SO line or customer). Those fields should be readonly on the
view.
3) Refactor some methods

Task: 38498
  • Loading branch information
abh-odoo authored and jem-odoo committed Jan 17, 2018
1 parent 7b6056a commit 66a0e5a
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 44 deletions.
10 changes: 0 additions & 10 deletions addons/hr_timesheet/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,3 @@ def _hours_get(self):
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')

_constraints = [(models.BaseModel._check_recursion, 'Circular references are not permitted between tasks and sub-tasks', ['parent_id'])]


@api.model
def create(self, vals):
context = dict(self.env.context)
# Remove default_parent_id to avoid a confusion in get_record_data
if context.get('default_parent_id', False):
vals['parent_id'] = context.pop('default_parent_id', None)
task = super(Task, self.with_context(context)).create(vals)
return task
89 changes: 68 additions & 21 deletions addons/project/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,6 @@ def _get_default_favorite_user_ids(self):
doc_count = fields.Integer(compute='_compute_attached_docs_count', string="Number of documents attached")
date_start = fields.Date(string='Start Date')
date = fields.Date(string='Expiration Date', index=True, track_visibility='onchange')
subtask_project_id = fields.Many2one('project.project', string='Sub-task Project', ondelete="restrict",
help="Choosing a sub-tasks project will both enable sub-tasks and set their default project (possibly the project itself)")
# rating fields
percentage_satisfaction_task = fields.Integer(
compute='_compute_percentage_satisfaction_task', string="Happy % on Task", store=True, default=-1)
Expand Down Expand Up @@ -300,8 +298,6 @@ def create(self, vals):
# Prevent double project creation
self = self.with_context(mail_create_nosubscribe=True)
project = super(Project, self).create(vals)
if not vals.get('subtask_project_id'):
project.subtask_project_id = project.id
if project.privacy_visibility == 'portal' and project.partner_id:
project.message_subscribe(project.partner_id.ids)
return project
Expand Down Expand Up @@ -443,6 +439,14 @@ class Task(models.Model):
_mail_post_access = 'read'
_order = "priority desc, sequence, date_start, name, id"

@api.model
def default_get(self, fields_list):
result = super(Task, self).default_get(fields_list)
# force some parent values, if needed
if 'parent_id' in result and result['parent_id']:
result.update(self._subtask_values_from_parent(result['parent_id']))
return result

def _get_default_partner(self):
if 'default_project_id' in self.env.context:
default_project_id = self.env['project.project'].browse(self.env.context['default_project_id'])
Expand Down Expand Up @@ -503,7 +507,7 @@ def _read_group_stage_ids(self, stages, domain, order):
track_visibility='onchange',
change_default=True)
notes = fields.Text(string='Notes')
planned_hours = fields.Float(string='Initially Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.')
planned_hours = fields.Float("Planned Hours", help='It is the time planned to achieve the task. If this document has sub-tasks, it means the time needed to achieve this tasks and its childs.')
remaining_hours = fields.Float(string='Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
user_id = fields.Many2one('res.users',
string='Assigned to',
Expand All @@ -527,8 +531,7 @@ def _read_group_stage_ids(self, stages, domain, order):
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True)
parent_id = fields.Many2one('project.task', string='Parent Task')
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True)
subtask_count = fields.Integer(compute='_compute_subtask_count', type='integer', string="Sub-task count")
subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count')
email_from = fields.Char(string='Email', help="These people will receive email.", index=True)
email_cc = fields.Char(string='Watchers Emails', help="""These email addresses will be added to the CC field of all inbound
and outbound emails for this record before being sent. Separate multiple email addresses with a comma""")
Expand Down Expand Up @@ -583,20 +586,36 @@ def _compute_portal_url(self):
for task in self:
task.portal_url = '/my/task/%s' % task.id

@api.depends('child_ids')
def _compute_subtask_count(self):
""" Note: since we accept only one level subtask, we can use a read_group here """
task_data = self.env['project.task'].read_group([('parent_id', 'in', self.ids)], ['parent_id'], ['parent_id'])
mapping = dict((data['parent_id'][0], data['parent_id_count']) for data in task_data)
for task in self:
task.subtask_count = mapping.get(task.id, 0)

@api.onchange('partner_id')
def _onchange_partner_id(self):
self.email_from = self.partner_id.email

@api.onchange('parent_id')
def _onchange_parent_id(self):
if self.parent_id:
for field_name in self._subtask_implied_fields():
self[field_name] = self.parent_id[field_name]

@api.onchange('project_id')
def _onchange_project(self):
default_partner_id = self.env.context.get('default_partner_id')
default_partner = self.env['res.partner'].browse(default_partner_id) if default_partner_id else self.env['res.partner']
if self.project_id:
self.partner_id = self.project_id.partner_id or default_partner
if not self.parent_id and not self.partner_id:
self.partner_id = self.project_id.partner_id or default_partner
if self.project_id not in self.stage_id.project_ids:
self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)])
else:
self.partner_id = default_partner
if not self.parent_id:
self.partner_id = default_partner
self.stage_id = False

@api.onchange('user_id')
Expand All @@ -614,17 +633,6 @@ def copy(self, default=None):
default['remaining_hours'] = self.planned_hours
return super(Task, self).copy(default)

@api.multi
def _compute_subtask_count(self):
for task in self:
task.subtask_count = self.search_count([('id', 'child_of', task.id), ('id', '!=', task.id)])

@api.constrains('parent_id')
def _check_subtask_project(self):
for task in self:
if task.parent_id.project_id and task.project_id != task.parent_id.project_id.subtask_project_id:
raise UserError(_("You can't define a parent task if its project is not correctly configured. The sub-task's project of the parent task's project should be this task's project"))

# Override view according to the company definition
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
Expand Down Expand Up @@ -708,7 +716,9 @@ def stage_find(self, section_id, domain=[], order='sequence'):
def create(self, vals):
# context: no_log, because subtype already handle this
context = dict(self.env.context, mail_create_nolog=True)

# force some parent values, if needed
if 'parent_id' in vals and vals['parent_id']:
vals.update(self._subtask_values_from_parent(vals['parent_id']))
# for default stage
if vals.get('project_id') and not context.get('default_project_id'):
context['default_project_id'] = vals.get('project_id')
Expand All @@ -724,6 +734,9 @@ def create(self, vals):
@api.multi
def write(self, vals):
now = fields.Datetime.now()
# subtask: force some parent values, if needed
if 'parent_id' in vals and vals['parent_id']:
vals.update(self._subtask_values_from_parent(vals['parent_id']))
# stage change: update date_last_stage_update
if 'stage_id' in vals:
vals.update(self.update_date_end(vals['stage_id']))
Expand All @@ -739,6 +752,12 @@ def write(self, vals):
# rating on stage
if 'stage_id' in vals and vals.get('stage_id'):
self.filtered(lambda x: x.project_id.rating_status == 'stage')._send_task_rating_mail(force_send=True)
# subtask: update subtask according to parent values
subtask_values_to_write = self._subtask_write_values(vals)
if subtask_values_to_write:
subtasks = self.filtered(lambda task: not task.parent_id).mapped('child_ids')
if subtasks:
subtasks.write(subtask_values_to_write)
return result

def update_date_end(self, stage_id):
Expand Down Expand Up @@ -771,6 +790,34 @@ def get_access_action(self, access_uid=None):
}
return super(Task, self).get_access_action(access_uid)

# ---------------------------------------------------
# Subtasks
# ---------------------------------------------------

@api.model
def _subtask_implied_fields(self):
""" Return the list of field name to apply on subtask when changing parent_id or when updating parent task. """
return ['partner_id', 'email_from']

@api.multi
def _subtask_write_values(self, values):
""" Return the values to write on subtask when `values` is written on parent tasks
:param values: dict of values to write on parent
"""
result = {}
for field_name in self._subtask_implied_fields():
if field_name in values:
result[field_name] = values[field_name]
return result

def _subtask_values_from_parent(self, parent_id):
""" Get values for substask implied field of the given"""
result = {}
parent_task = self.env['project.task'].browse(parent_id)
for field_name in self._subtask_implied_fields():
result[field_name] = parent_task[field_name]
return self._convert_to_write(result)

# ---------------------------------------------------
# Mail gateway
# ---------------------------------------------------
Expand Down
28 changes: 28 additions & 0 deletions addons/project/tests/test_project_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,31 @@ def test_task_process_with_stages(self):
self.assertEqual(task.name, 'Cats', 'project_task: name should be the email subject')
self.assertEqual(task.project_id.id, self.project_goats.id, 'project_task: incorrect project')
self.assertEqual(task.stage_id.sequence, 1, "project_task: should have a stage with sequence=1")

def test_subtask_process(self):
""" Check subtask mecanism and change it from project. """
Task = self.env['project.task'].with_context({'tracking_disable': True})
parent_task = Task.create({
'name': 'Mother Task',
'user_id': self.user_projectuser.id,
'project_id': self.project_pigs.id,
'partner_id': self.partner_2.id,
'planned_hours': 12,
})
child_task = Task.create({
'name': 'Task Child',
'parent_id': parent_task.id,
'project_id': self.project_pigs.id,
'planned_hours': 3,
})

self.assertEqual(parent_task.partner_id, child_task.partner_id, "Subtask should have the same partner than its parent")
self.assertEqual(parent_task.subtask_count, 1, "Parent task should have 1 child")
self.assertEqual(parent_task.subtask_planned_hours, 3, "Planned hours of subtask should impact parent task")

# change project
child_task.write({
'project_id': self.project_goats.id # customer is partner_1
})

self.assertEqual(parent_task.partner_id, child_task.partner_id, "Subtask partner should not change when changing project")
14 changes: 5 additions & 9 deletions addons/project/views/project_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@
'default_parent_id' : active_id,
'default_project_id' : project_id,
'default_name' : name + ':',
'default_partner_id' : partner_id,
'search_default_project_id': [project_id],
}
</field>
<field name="search_view_id" ref="project.view_task_search_form"/>
Expand Down Expand Up @@ -139,7 +137,6 @@
<group>
<field name="user_id" string="Project Manager"
attrs="{'readonly':[('active','=',False)]}"/>
<field name="subtask_project_id" groups="project.group_subtask_project"/>
<field name="privacy_visibility" widget="radio"/>
<field name="partner_id" string="Customer"/>
<label for="rating_status" groups="project.group_project_rating"/>
Expand Down Expand Up @@ -426,9 +423,9 @@
<sheet string="Task">
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" icon="fa-tasks" type="object" name="action_open_parent_task" string="Parent Task" attrs="{'invisible' : [('parent_id', '=', False)]}" groups="project.group_subtask_project"/>
<button name="%(project_task_action_sub_task)d" type="action" class="oe_stat_button" icon="fa-tasks"
<button id="subtask_stat_button" name="%(project_task_action_sub_task)d" type="action" class="oe_stat_button" icon="fa-tasks"
attrs="{'invisible' : [('parent_id', '!=', False)]}"
context="{'project_id': subtask_project_id, 'name': name, 'partner_id': partner_id}" groups="project.group_subtask_project">
context="{'project_id': project_id, 'name': name, 'parent_id': id}" groups="project.group_subtask_project">
<field string="Sub-tasks" name="subtask_count" widget="statinfo"/>
</button>
<button name="%(rating_rating_action_task)d" type="action" attrs="{'invisible': [('rating_count', '=', 0)]}" class="oe_stat_button" icon="fa-smile-o" groups="project.group_project_rating">
Expand Down Expand Up @@ -471,12 +468,11 @@
<group>
<group>
<field name="sequence" groups="base.group_no_one"/>
<field name="partner_id"/>
<field name="email_from"/>
<field name="partner_id" attrs="{'readonly': [('parent_id', '!=', False)]}"/>
<field name="email_from" attrs="{'readonly': [('parent_id', '!=', False)]}"/>
<field name="email_cc" groups="base.group_no_one"/>
<field name="parent_id" groups="project.group_subtask_project"/>
<field name="parent_id" attrs="{'invisible' : [('subtask_count', '>', 0)]}" groups="project.group_subtask_project"/>
<field name="child_ids" invisible="1" />
<field name="subtask_project_id" invisible="1" />
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="displayed_image_id" groups="base.group_no_one"/>
</group>
Expand Down
10 changes: 6 additions & 4 deletions addons/sale_timesheet/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ def unlink(self):
raise ValidationError(_('You cannot delete a task related to a Sales Order. You can only archive this task.'))
return super(ProjectTask, self).unlink()

@api.multi
def _subtask_implied_fields(self):
fields_list = super(ProjectTask, self)._subtask_implied_fields()
fields_list += ['sale_line_id']
return fields_list

@api.multi
def action_view_so(self):
self.ensure_one()
Expand All @@ -96,10 +102,6 @@ def action_view_so(self):
"context": {"create": False, "show_sale": True},
}

@api.onchange('parent_id')
def onchange_parent_id(self):
self.sale_line_id = self.parent_id.sale_line_id.id

def rating_get_partner_id(self):
partner = self.partner_id or self.sale_line_id.order_id.partner_id
if partner:
Expand Down

0 comments on commit 66a0e5a

Please sign in to comment.