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