[IMP] project: Use stored computed fields for parent to children task

Purpose
=======
When you create a task independently and link it to a Parent task later on,
several changes are applied: the person assigned on the sub-task switches
to the one set on the parent task, same for the customer, and so on.
This creates frustration because the user most likely set specific data on the
sub task for a reason. In addition, the changes are made implicitely, so the
user might not notice it or get confused as to what changed from the original
task.

Specification
=============
Values for fields `partner_id`, `email_from`, `project_id`, `sale_line_id` should be
transfered from parent task to children task, only if the value is not already set
on the child.

This was already partially implemented by 749810a and 45396f7.
But to achieve it, similar code was duplicated in several methods:
default_get, onchanges, write, create.

Since the new ORM, the same behavior can be achieved with only computed fields
with store=True and readonly=False. This commit changes the previous implementation
to take advantage of this which greatly improves code readability and maintainability.

Tests by Maximilen Larue
Business code by Lucas Lefèvre

closes odoo/odoo#39369

Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Co-authored-by: MaxLarue <mla@odoo.com>
This commit is contained in:
Lucas Lefèvre
2019-10-25 14:22:36 +00:00
co-authored by MaxLarue
parent 32f14b9118
commit 2dccf4d6cd
6 changed files with 257 additions and 101 deletions
+21 -53
View File
@@ -436,24 +436,6 @@ class Task(models.Model):
_order = "priority desc, sequence, id desc"
_check_company_auto = True
@api.model
def default_get(self, fields_list):
result = super(Task, self).default_get(fields_list)
# find default value from parent for the not given ones
parent_task_id = result.get('parent_id') or self._context.get('default_parent_id')
if parent_task_id:
parent_values = self._subtask_values_from_parent(parent_task_id)
for fname, value in parent_values.items():
if fname not in result:
result[fname] = value
return result
@api.model
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'])
return default_project_id.exists().partner_id
def _get_default_stage_id(self):
""" Gives default stage_id """
project_id = self.env.context.get('default_project_id')
@@ -504,7 +486,8 @@ class Task(models.Model):
index=True,
copy=False,
readonly=True)
project_id = fields.Many2one('project.project', string='Project', default=lambda self: self.env.context.get('default_project_id'),
project_id = fields.Many2one('project.project', string='Project',
compute='_compute_project_id', store=True, readonly=False,
index=True, tracking=True, check_company=True, change_default=True)
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.',tracking=True)
subtask_planned_hours = fields.Float("Subtasks", compute='_compute_subtask_planned_hours', help="Computed using sum of hours planned of all subtasks created from main task. Usually these hours are less or equal to the Planned Hours (of main task).")
@@ -514,7 +497,7 @@ class Task(models.Model):
index=True, tracking=True)
partner_id = fields.Many2one('res.partner',
string='Customer',
default=lambda self: self._get_default_partner(),
compute='_compute_partner_id', store=True, readonly=False,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
partner_city = fields.Char(related='partner_id.city', readonly=False)
manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True, related_sudo=False)
@@ -532,7 +515,8 @@ class Task(models.Model):
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks", context={'active_test': False})
subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True)
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_from = fields.Char(string='Email', help="These people will receive email.", index=True,
compute='_compute_email_from', store="True", readonly=False)
# Computed field about working time elapsed between record creation and assignation/closing.
working_hours_open = fields.Float(compute='_compute_elapsed', string='Working hours to assign', store=True, group_operator="avg")
working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg")
@@ -610,17 +594,6 @@ class Task(models.Model):
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, value in self._subtask_values_from_parent(self.parent_id.id).items():
if not self[field_name]:
self[field_name] = value
@api.onchange('project_id')
def _onchange_project(self):
if self.project_id:
@@ -713,11 +686,6 @@ class Task(models.Model):
if vals.get('stage_id'):
vals.update(self.update_date_end(vals['stage_id']))
vals['date_last_stage_update'] = fields.Datetime.now()
# substask default values
if vals.get('parent_id'):
for fname, value in self._subtask_values_from_parent(vals['parent_id']).items():
if fname not in vals:
vals[fname] = value
task = super(Task, self.with_context(context)).create(vals)
return task
@@ -750,19 +718,22 @@ class Task(models.Model):
# Subtasks
# ---------------------------------------------------
def _subtask_default_fields(self):
""" Return the list of field name for default value when creating a subtask """
return ['partner_id', 'email_from']
@api.depends('parent_id.partner_id', 'project_id.partner_id')
def _compute_partner_id(self):
for task in self:
if not task.partner_id:
task.partner_id = task.parent_id.partner_id or task.project_id.partner_id
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_default_fields():
result[field_name] = parent_task[field_name]
# special case for the subtask default project
result['project_id'] = parent_task.project_id.subtask_project_id
return self._convert_to_write(result)
@api.depends('partner_id.email', 'parent_id.email_from')
def _compute_email_from(self):
for task in self:
task.email_from = task.partner_id.email or task.email_from or task.parent_id.email_from
@api.depends('parent_id.project_id.subtask_project_id')
def _compute_project_id(self):
for task in self:
if not task.project_id:
task.project_id = task.parent_id.project_id.subtask_project_id
# ---------------------------------------------------
# Mail gateway
@@ -935,10 +906,7 @@ class Task(models.Model):
'default_company_id': default_project.company_id.id if default_project else self.env.company.id,
'search_default_parent_id': self.id,
})
parent_values = self._subtask_values_from_parent(self.id)
for fname, value in parent_values.items():
if 'default_' + fname not in ctx:
ctx['default_' + fname] = value
action['context'] = ctx
return action
+17 -47
View File
@@ -121,32 +121,12 @@ class Project(models.Model):
class ProjectTask(models.Model):
_inherit = "project.task"
@api.model
def _get_default_partner(self):
partner = False
if 'default_project_id' in self.env.context: # partner from SO line is prior on one from project
project_sudo = self.env['project.project'].browse(self.env.context['default_project_id']).sudo()
partner = project_sudo.sale_line_id.order_partner_id
if not partner:
partner = super(ProjectTask, self)._get_default_partner()
return partner
@api.model
def _default_sale_line_id(self):
sale_line_id = False
if self._context.get('default_parent_id'):
parent_task = self.env['project.task'].browse(self._context['default_parent_id'])
sale_line_id = parent_task.sale_line_id.id
if not sale_line_id and self._context.get('default_project_id'):
project = self.env['project.project'].browse(self.env.context['default_project_id'])
if project.billable_type != 'no':
sale_line_id = project.sale_line_id.id
return sale_line_id
sale_line_id = fields.Many2one('sale.order.line', 'Sales Order Item', default=_default_sale_line_id, domain="[('is_service', '=', True), ('order_partner_id', '=', partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]",
sale_line_id = fields.Many2one('sale.order.line', 'Sales Order Item', domain="[('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]",
compute='_compute_sale_line', store=True, readonly=False,
help="Sales order item to which the task is linked. If an employee timesheets on a this task, "
"and if this employee is not in the 'Employee/Sales Order Item Mapping' of the project, the "
"timesheet entry will be linked to this sales order item.", copy=False)
commercial_partner_id = fields.Many2one(related='partner_id.commercial_partner_id')
sale_order_id = fields.Many2one('sale.order', 'Sales Order', compute='_compute_sale_order_id', store=True, readonly=False, help="Sales order to which the task is linked.")
billable_type = fields.Selection([
('task_rate', 'At Task Rate'),
@@ -200,22 +180,21 @@ class ProjectTask(models.Model):
result.setdefault('domain', {})['sale_line_id'] = domain
return result
@api.onchange('partner_id')
def _onchange_partner_id(self):
result = super(ProjectTask, self)._onchange_partner_id()
result = result or {}
if self.sale_line_id.order_partner_id.commercial_partner_id != self.partner_id.commercial_partner_id:
self.sale_line_id = False
if self.partner_id:
result.setdefault('domain', {})['sale_line_id'] = [('is_service', '=', True), ('is_expense', '=', False), ('order_partner_id', 'child_of', self.partner_id.commercial_partner_id.id), ('state', 'in', ['sale', 'done'])]
return result
@api.depends('project_id.sale_line_id.order_partner_id')
def _compute_partner_id(self):
for task in self:
if not task.partner_id:
task.partner_id = task.project_id.sale_line_id.order_partner_id
super()._compute_partner_id()
@api.onchange('parent_id')
def _onchange_parent_id(self):
super(ProjectTask, self)._onchange_parent_id()
# check sale_line_id and customer are coherent
if self.sale_line_id and self.partner_id != self.sale_line_id.order_partner_id:
self.sale_line_id = False
@api.depends('partner_id.commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id')
def _compute_sale_line(self):
for task in self:
if not task.sale_line_id:
task.sale_line_id = task.parent_id.sale_line_id or task.project_id.sale_line_id
# check sale_line_id and customer are coherent
if task.sale_line_id.order_partner_id.commercial_partner_id != task.partner_id.commercial_partner_id:
task.sale_line_id = False
@api.constrains('sale_line_id')
def _check_sale_line_type(self):
@@ -236,15 +215,6 @@ class ProjectTask(models.Model):
raise ValidationError(_('You have to unlink the task from the sale order item in order to delete it.'))
return super(ProjectTask, self).unlink()
# ---------------------------------------------------
# Subtasks
# ---------------------------------------------------
@api.model
def _subtask_default_fields(self):
result = super(ProjectTask, self)._subtask_default_fields()
return result + ['sale_line_id']
# ---------------------------------------------------
# Actions
# ---------------------------------------------------
+1
View File
@@ -7,3 +7,4 @@ from . import test_sale_service
from . import test_project_billing
from . import test_reinvoice
from . import test_reporting
from . import test_child_tasks
@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
from odoo.tests.common import SavepointCase, new_test_user
class TestNestedTaskUpdate(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env['res.partner'].create({'name': "Mur en béton"})
sale_order = cls.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': cls.partner.id,
'partner_invoice_id': cls.partner.id,
'partner_shipping_id': cls.partner.id,
})
product = cls.env['product.product'].create({
'name': "Prepaid Consulting",
'type': 'service',
})
cls.order_line = cls.env['sale.order.line'].create({
'name': "Order line",
'product_id': product.id,
'order_id': sale_order.id,
})
cls.user = new_test_user(cls.env, login='mla')
#----------------------------------
#
# When creating tasks that have a parent_id, they pick some values from their parent
#
#----------------------------------
def test_creating_subtask_user_id_on_parent_dont_go_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_id': self.user.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'user_id': False})
self.assertFalse(child.user_id)
def test_creating_subtask_partner_id_on_parent_goes_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
self.assertEqual(child.partner_id, self.user.partner_id)
def test_creating_subtask_email_from_on_parent_goes_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'email_from': 'a@c.be'})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
self.assertEqual(child.email_from, 'a@c.be')
def test_creating_subtask_sale_line_id_on_parent_goes_on_child_if_same_partner_in_values(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.partner.id, 'parent_id': parent.id})
self.assertEqual(child.sale_line_id, parent.sale_line_id)
parent.write({'sale_line_id': False})
self.assertEqual(child.sale_line_id, self.order_line)
def test_creating_subtask_sale_line_id_on_parent_goes_on_child_with_partner_if_not_in_values(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
self.assertEqual(child.partner_id, parent.partner_id)
self.assertEqual(child.sale_line_id, parent.sale_line_id)
def test_creating_subtask_sale_line_id_on_parent_dont_go_on_child_if_other_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id, 'parent_id': parent.id})
self.assertFalse(child.sale_line_id)
self.assertNotEqual(child.partner_id, parent.partner_id)
def test_creating_subtask_sale_line_id_on_parent_go_on_child_if_same_commercial_partner(self):
commercial_partner = self.env['res.partner'].create({'name': "Jémémy"})
self.partner.parent_id = commercial_partner
self.user.partner_id.parent_id = commercial_partner
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id, 'parent_id': parent.id})
self.assertEqual(child.sale_line_id, self.order_line, "Sale order line on parent should be transfered to child")
self.assertNotEqual(child.partner_id, parent.partner_id)
#----------------------------------------
#
# When writing on a parent task, some values adapt on their children
#
#----------------------------------------
def test_write_user_id_on_parent_dont_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_id': False})
child = self.env['project.task'].create({'name': 'child', 'user_id': False, 'parent_id': parent.id})
self.assertFalse(child.user_id)
parent.write({'user_id': self.user.id})
self.assertFalse(child.user_id)
parent.write({'user_id': False})
self.assertFalse(child.user_id)
def test_write_partner_id_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': False})
child = self.env['project.task'].create({'name': 'child', 'partner_id': False, 'parent_id': parent.id})
self.assertFalse(child.partner_id)
parent.write({'partner_id': self.user.partner_id.id})
self.assertEqual(child.partner_id, parent.partner_id)
parent.write({'partner_id': False})
self.assertEqual(child.partner_id, self.user.partner_id)
def test_write_email_from_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent'})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
self.assertFalse(child.email_from)
parent.write({'email_from': 'a@c.be'})
self.assertEqual(child.email_from, parent.email_from)
parent.write({'email_from': ''})
self.assertEqual(child.email_from, 'a@c.be')
def test_write_sale_line_id_on_parent_write_on_child_if_same_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.partner.id})
self.assertFalse(child.sale_line_id)
parent.write({'sale_line_id': self.order_line.id})
self.assertEqual(child.sale_line_id, parent.sale_line_id)
parent.write({'sale_line_id': False})
self.assertEqual(child.sale_line_id, self.order_line)
def test_write_sale_line_id_on_parent_write_on_child_with_partner_if_not_set(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
self.assertFalse(child.sale_line_id)
parent.write({'sale_line_id': self.order_line.id})
self.assertEqual(child.sale_line_id, parent.sale_line_id)
self.assertEqual(child.partner_id, self.partner)
parent.write({'sale_line_id': False})
self.assertEqual(child.sale_line_id, self.order_line)
def test_write_sale_line_id_on_parent_dont_write_on_child_if_other_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.user.partner_id.id})
self.assertFalse(child.sale_line_id)
parent.write({'sale_line_id': self.order_line.id})
self.assertFalse(child.sale_line_id)
#----------------------------------
#
# When linking two existent task, some values go on the child
#
#----------------------------------
def test_linking_user_id_on_parent_dont_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_id': self.user.id})
child = self.env['project.task'].create({'name': 'child', 'user_id': False})
self.assertFalse(child.user_id)
child.write({'parent_id': parent.id})
self.assertFalse(child.user_id)
def test_linking_partner_id_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': False})
self.assertFalse(child.partner_id)
child.write({'parent_id': parent.id})
self.assertEqual(child.partner_id, self.user.partner_id)
def test_linking_email_from_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'email_from': 'a@c.be'})
child = self.env['project.task'].create({'name': 'child', 'email_from': False})
self.assertFalse(child.email_from)
child.write({'parent_id': parent.id})
self.assertEqual(child.email_from, 'a@c.be')
def test_linking_sale_line_id_on_parent_write_on_child_if_same_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.partner.id})
self.assertFalse(child.sale_line_id)
child.write({'parent_id': parent.id})
self.assertEqual(child.sale_line_id, parent.sale_line_id)
parent.write({'sale_line_id': False})
self.assertEqual(child.sale_line_id, self.order_line)
def test_linking_sale_line_id_on_parent_write_on_child_with_partner_if_not_set(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': False})
self.assertFalse(child.sale_line_id)
self.assertFalse(child.partner_id)
child.write({'parent_id': parent.id})
self.assertEqual(child.partner_id, parent.partner_id)
self.assertEqual(child.sale_line_id, parent.sale_line_id)
def test_linking_sale_line_id_on_parent_write_dont_child_if_other_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id})
self.assertFalse(child.sale_line_id)
self.assertNotEqual(child.partner_id, parent.partner_id)
child.write({'parent_id': parent.id})
self.assertFalse(child.sale_line_id)
def test_writing_on_parent_with_multiple_tasks(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_id': False, 'partner_id': self.partner.id})
children_values = [{'name': 'child%s' % i, 'user_id': False, 'parent_id': parent.id} for i in range(5)]
children = self.env['project.task'].create(children_values)
# test writing sale_line_id
for child in children:
self.assertFalse(child.sale_line_id)
parent.write({'sale_line_id': self.order_line.id})
for child in children:
self.assertEqual(child.sale_line_id, self.order_line)
def test_linking_on_parent_with_multiple_tasks(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'user_id': self.user.id})
children_values = [{'name': 'child%s' % i, 'user_id': False} for i in range(5)]
children = self.env['project.task'].create(children_values)
# test writing user_id and sale_line_id
for child in children:
self.assertFalse(child.user_id)
self.assertFalse(child.sale_line_id)
children.write({'parent_id': parent.id})
for child in children:
self.assertEqual(child.sale_line_id, self.order_line)
self.assertFalse(child.user_id)
@@ -598,4 +598,4 @@ class TestSaleService(TestCommonSaleTimesheetNoChart):
# copy the task
task_copy = task.copy()
self.assertEqual(task.sale_line_id, task_copy.sale_line_id, "Duplicatinga task should keep its Sale line")
self.assertFalse(task_copy.sale_line_id, "Duplicatinga task should not keep its Sale line")
@@ -53,6 +53,7 @@
<field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False), '&amp;', ('sale_order_id', '!=', False), ('sale_line_id', '=', False)]}" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}"/>
<field name="billable_type" invisible="1"/>
<field name="sale_order_id" invisible="1" />
<field name="commercial_partner_id" invisible="1" />
</field>
</field>
</record>