diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index d7d10055c7f..3581a360ce6 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -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
diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py
index ebe002ba822..adb07482c37 100644
--- a/addons/sale_timesheet/models/project.py
+++ b/addons/sale_timesheet/models/project.py
@@ -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
# ---------------------------------------------------
diff --git a/addons/sale_timesheet/tests/__init__.py b/addons/sale_timesheet/tests/__init__.py
index 689ea8ed4eb..139d07e5871 100644
--- a/addons/sale_timesheet/tests/__init__.py
+++ b/addons/sale_timesheet/tests/__init__.py
@@ -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
diff --git a/addons/sale_timesheet/tests/test_child_tasks.py b/addons/sale_timesheet/tests/test_child_tasks.py
new file mode 100644
index 00000000000..776a2d77ccf
--- /dev/null
+++ b/addons/sale_timesheet/tests/test_child_tasks.py
@@ -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)
diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py
index 725298877d0..13413f7c3fa 100644
--- a/addons/sale_timesheet/tests/test_sale_service.py
+++ b/addons/sale_timesheet/tests/test_sale_service.py
@@ -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")
diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml
index 7beb53bfbdb..38a85e057d2 100644
--- a/addons/sale_timesheet/views/project_task_views.xml
+++ b/addons/sale_timesheet/views/project_task_views.xml
@@ -53,6 +53,7 @@
+