From 4132408f26f1ce2dfa1c96b1ca060e67529293de Mon Sep 17 00:00:00 2001 From: "Thomas Lefebvre (thle)" Date: Mon, 5 Jun 2023 08:25:16 +0000 Subject: [PATCH] [FIX] sale_project: link subtask to sale order line of parent Steps to reproduce: ------------------- - create two product of type "service"; - field "Create on Order" is set to "Project & Task"; - create a Sale Order with these two products; - condirm the Sale Order; Two tasks are created in a project and each task has its own Sale Order Line Item. - create a subtask from both tasks respectively; - select a project for the subtask. Issue: ------ The same Sale Order Line is selected for the two subtasks. It always take the first Sale Order line. Cause: ------ To choose the sale order line, we first look to see if the subtask belongs to a project. If so, we take the sale order line which created the project. Unfortunately, a subtask can belong to a project created with a sale order line without belonging to the same sale order line (in the case where a task is added to a project via another product and therefore another sale order line). Solution: --------- - A project without parent --> use project's sale order line - A parent without project --> no sale order line defined - A parent in the same project --> use parent's sale order line - A parent in an other project --> use parent's sale order line opw-3333936 closes odoo/odoo#124260 X-original-commit: 323d7f631916f5acc5ab34f8531ae59bb0455828 Signed-off-by: Xavier Bol (xbo) Signed-off-by: Lefebvre Thomas (thle) --- addons/sale_project/models/project.py | 8 +- .../sale_project/tests/test_sale_project.py | 98 +++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index c9371d19caa..179d7412c7e 100755 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -738,11 +738,13 @@ class ProjectTask(models.Model): continue if not task.sale_line_id: # if the project_id is set then it means the task is classic task or a subtask with another project than its parent. + # To determine the sale_line_id, we first need to look at the parent before the project to manage the case of subtasks. + # Two sub-tasks in the same project do not necessarily have the same sale_line_id (need to look at the parent task). sale_line = False - if task.project_id.sale_line_id and task.project_id.partner_id.commercial_partner_id == task.partner_id.commercial_partner_id: - sale_line = task.project_id.sale_line_id - elif task.parent_id.sale_line_id and task.parent_id.partner_id.commercial_partner_id == task.partner_id.commercial_partner_id: + if task.parent_id.sale_line_id and task.parent_id.partner_id.commercial_partner_id == task.partner_id.commercial_partner_id: sale_line = task.parent_id.sale_line_id + elif task.project_id.sale_line_id and task.project_id.partner_id.commercial_partner_id == task.partner_id.commercial_partner_id: + sale_line = task.project_id.sale_line_id task.sale_line_id = sale_line or task.milestone_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: diff --git a/addons/sale_project/tests/test_sale_project.py b/addons/sale_project/tests/test_sale_project.py index 44c7c662bfb..8eec56e2172 100644 --- a/addons/sale_project/tests/test_sale_project.py +++ b/addons/sale_project/tests/test_sale_project.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import Command from odoo.tests.common import new_test_user from .common import TestSaleProjectCommon from odoo.tests.common import tagged @@ -236,6 +237,103 @@ class TestSaleProject(TestSaleProjectCommon): sale_order.with_context({'disable_cancel_warning': True}).action_cancel() self.assertFalse(self.project_global.sale_line_id, "The project should not be linked to the SOL anymore") + def test_links_with_sale_order_line(self): + """ + Check that the subtasks are linked to the correct sale order line. + """ + product_A, product_B, product_C = self.env['product.product'].create([ + { + 'name': 'product_A', + 'lst_price': 100.0, + 'detailed_type': 'service', + 'service_tracking': 'task_in_project', + }, + { + 'name': 'product_B', + 'lst_price': 100.0, + 'detailed_type': 'service', + 'service_tracking': 'task_in_project', + }, + { + 'name': 'product_C', + 'lst_price': 100.0, + 'detailed_type': 'service', + 'service_tracking': 'task_in_project', + }, + ]) + sale_order_first, sale_order_second = self.env['sale.order'].create([ + { + 'partner_id': self.partner.id, + 'order_line': [ + Command.create({'product_id': product_A.id}), + Command.create({'product_id': product_B.id}), + ] + }, + { + 'partner_id': self.partner.id, + 'order_line': [ + Command.create({'product_id': product_C.id}), + ] + } + ]) + (sale_order_first + sale_order_second).action_confirm() + + sale_order_line_A = sale_order_first.order_line.filtered(lambda sol: sol.product_id == product_A) + sale_order_line_B = sale_order_first.order_line - sale_order_line_A + sale_order_line_C = sale_order_second.order_line + + project_first = sale_order_first.project_ids + project_second = sale_order_second.project_ids + + task_A = sale_order_first.tasks_ids.filtered(lambda task: task.sale_line_id == sale_order_line_A) + task_B = sale_order_first.tasks_ids - task_A + task_C = sale_order_second.tasks_ids + + # [CASE 1] Parent in the same project --> use parent's sale order line + task_A.write({ + 'child_ids': [ + Command.create({'name': 'Sub A in first project', 'project_id': project_first.id}), + ] + }) + task_B.write({ + 'child_ids': [ + Command.create({'name': 'Sub B in first project', 'project_id': project_first.id}), + ] + }) + task_C.write({ + 'child_ids': [ + Command.create({'name': 'Sub C in second project', 'project_id': project_second.id}), + ] + }) + self.assertEqual(task_A.child_ids.sale_line_id, sale_order_line_A) + self.assertEqual(task_B.child_ids.sale_line_id, sale_order_line_B) + self.assertEqual(task_C.child_ids.sale_line_id, sale_order_line_C) + + # [CASE 2] Parent in an other project --> use parent's sale_order line + task_B.write({ + 'child_ids': [ + Command.create({'name': 'Sub B in second project', 'project_id': project_second.id}), + ] + }) + sub_B_second = task_B.child_ids.filtered(lambda sub: sub.name == 'Sub B in second project') + self.assertEqual(sub_B_second.sale_line_id, sale_order_line_B) + + # [CASE 3] Without project --> no sale order line defined + task_B.write({ + 'child_ids': [ + Command.create({'name': 'Sub B without project'}), + ] + }) + sub_B_without = task_B.child_ids.filtered(lambda sub: sub.name == 'Sub B without project') + self.assertEqual(len(sub_B_without.sale_line_id), 0) + + # [CASE 4] Without parent --> use sale order line of the project + task_D = self.env['project.task'].create({ + 'name': 'Task D', + 'project_id': project_first.id, + }) + self.assertEqual(task_D.sale_line_id, project_first.sale_line_id) + def test_compute_project_and_task_button(self): """ Ensures that when a sale order has at least one sol with a service product whose service-policy is either delivered on milestone or ordered prepaid, then the