diff --git a/project_workload/models/project_task.py b/project_workload/models/project_task.py index 63234e1e..3a275c0a 100644 --- a/project_workload/models/project_task.py +++ b/project_workload/models/project_task.py @@ -64,25 +64,35 @@ def _get_workload_sync(self): *[(2, workload_id.id) for workload_id in self._get_obsolete_workloads()], ] + def _get_main_workloads(self): + return self.workload_ids + def _get_new_workloads(self): self.ensure_one() - # Handle only one workload in automatic - if not self.workload_ids: + workloads = self._get_main_workloads() + # Do not create load if user_id is not set + if self.user_id and not workloads: + # Handle only one workload in automatic return [self._prepare_workload()] return [] def _get_updated_workloads(self): self.ensure_one() + workloads = self._get_main_workloads() # Remove other workloads and update the first workload values - if self.workload_ids: - return [(self.workload_ids[0], self._prepare_workload())] + if self.user_id and workloads: + return [(workloads[0], self._prepare_workload())] return [] def _get_obsolete_workloads(self): self.ensure_one() + workloads = self._get_main_workloads() + # All workload are removed if user_id is removed + if not self.user_id: + return workloads # Remove other workloads and update the first workload values - if len(self.workload_ids) > 1: - return self.workload_ids[1:] + if len(workloads) > 1: + return workloads[1:] return [] @api.depends("workload_ids.unit_ids") diff --git a/project_workload/models/project_task_workload.py b/project_workload/models/project_task_workload.py index d7ae440b..6aaa6e51 100644 --- a/project_workload/models/project_task_workload.py +++ b/project_workload/models/project_task_workload.py @@ -48,7 +48,7 @@ def _check_end_date(self): _("The end date cannot be earlier than the start date.") ) - @api.depends("date_start", "date_end", "hours") + @api.depends("date_start", "date_end", "hours", "user_id") def _compute_unit_ids(self): for record in self: # We need to have the data to compute the unit (this happens at create) @@ -71,6 +71,13 @@ def _compute_unit_ids(self): 0, 0, { + # We have to set here the value of user_id + # as related field user_id will be not computed + # The "project.workload" are created from the computed + # field "workload_ids" and odoo do not play the compute + # field when there are trigered inside a create + # that come from a compute + "user_id": record.user_id.id, "hours": hours, "week": week, }, diff --git a/project_workload/tests/test_workload.py b/project_workload/tests/test_workload.py index 97618f8b..1024bd55 100644 --- a/project_workload/tests/test_workload.py +++ b/project_workload/tests/test_workload.py @@ -5,7 +5,7 @@ from datetime import timedelta -from .common import TestWorkloadCommon +from odoo.addons.project_workload.tests.common import TestWorkloadCommon class TestWorkload(TestWorkloadCommon): @@ -25,6 +25,12 @@ def test_change_user(self): self.assertEqual(self.task.workload_ids.user_id, self.user_2) self.assertEqual(self.task.workload_ids.unit_ids.user_id, self.user_2) + def test_remove_user(self): + self.task.user_id = self.user_2 + self.assertTrue(self.task.workload_ids) + self.task.user_id = False + self.assertFalse(self.task.workload_ids) + def test_change_date(self): self.task.date_end = self.task.date_start + timedelta(days=13) workload = self.task.workload_ids diff --git a/project_workload_additions/models/project_task.py b/project_workload_additions/models/project_task.py index 35827668..7993e721 100644 --- a/project_workload_additions/models/project_task.py +++ b/project_workload_additions/models/project_task.py @@ -8,71 +8,39 @@ class ProjectTask(models.Model): _inherit = "project.task" + def _get_main_workloads(self): + return ( + super() + ._get_main_workloads() + .filtered(lambda s: not s.additional_workload_id) + ) + def _get_new_workloads(self): rv = super()._get_new_workloads() - # super creates a new workload if there are none - # but here we can have only additional workloads - # so we also need to check if the existing workloads are additional - if not rv and all( - workload.additional_workload_id for workload in self.workload_ids - ): - rv.append(self._prepare_workload()) - - additional_workloads = { - workload.additional_workload_id: workload - for workload in self.workload_ids - if workload.additional_workload_id - } - # Now we need to create a new workload for each additional workload + # We need to create a new workload for each additional workload for additional_workload in self.project_id.additional_workload_ids: - if additional_workload not in additional_workloads: + if additional_workload not in self.workload_ids.additional_workload_id: rv.append(self._prepare_additional_workload(additional_workload)) - return rv def _get_updated_workloads(self): - # We sort the workloads by additional_workload_id to ensure that the - # first workload is the main one - self.workload_ids = self.workload_ids.sorted( - key=lambda w: w.additional_workload_id - ) rv = super()._get_updated_workloads() - if rv and rv[0][0].additional_workload_id: - rv = [] - - additional_workloads = { - workload.additional_workload_id: workload - for workload in self.workload_ids - if workload.additional_workload_id - } - # Now we need to update the existing workload for each additional workload - for additional_workload, workload in additional_workloads.items(): - rv.append( - ( - workload, - self._prepare_additional_workload(additional_workload), + for workload in self.workload_ids: + additional_workload = workload.additional_workload_id + if additional_workload in self.project_id.additional_workload_ids: + rv.append( + (workload, self._prepare_additional_workload(additional_workload)) ) - ) - return rv def _get_obsolete_workloads(self): - self.workload_ids = self.workload_ids.sorted( - key=lambda w: w.additional_workload_id - ) rv = super()._get_obsolete_workloads() - if rv: - # Do not delete additional workloads - rv = [workload for workload in rv if not workload.additional_workload_id] - - # Remove all additional workloads that are not in the project anymore - additional_workloads = { - workload.additional_workload_id: workload - for workload in self.workload_ids - if workload.additional_workload_id - } - for additional_workload in additional_workloads: - if additional_workload not in self.project_id.additional_workload_ids: + for workload in self.workload_ids: + additional_workload = workload.additional_workload_id + if ( + additional_workload + and additional_workload not in self.project_id.additional_workload_ids + ): rv.append(additional_workload) return rv diff --git a/project_workload_additions/models/project_task_workload_addition.py b/project_workload_additions/models/project_task_workload_addition.py index 9f692781..aeefeb56 100644 --- a/project_workload_additions/models/project_task_workload_addition.py +++ b/project_workload_additions/models/project_task_workload_addition.py @@ -10,19 +10,36 @@ class ProjectWorkloadAddition(models.Model): _description = "Project Task Workload Addition" project_id = fields.Many2one("project.project", string="Project", required=True) - type = fields.Many2one( + type_id = fields.Many2one( "project.task.workload.addition.type", string="Addition Type", required=True ) percentage = fields.Float( required=True, string="Added Percentage", + store=True, + compute="_compute_percentage", + readonly=False, ) user_id = fields.Many2one("res.users", string="User", required=True) task_id = fields.Many2one("project.task", string="Task", required=True) - @api.onchange("type") - def _onchange_type(self): - self.percentage = self.type.default_percentage + @api.model_create_multi + def create(self, list_vals): + # TODO remove on next version when computed field are run before the create + # for now we have to do it manually as the field is required + for vals in list_vals: + if not vals.get("percentage") and vals.get("type_id"): + vals["percentage"] = ( + self.env["project.task.workload.addition.type"] + .browse(vals["type_id"]) + .default_percentage + ) + return super().create(list_vals) + + @api.depends("type_id") + def _compute_percentage(self): + for record in self: + record.percentage = record.type_id.default_percentage def _compute_hours_from_task(self, task): self.ensure_one() diff --git a/project_workload_additions/tests/__init__.py b/project_workload_additions/tests/__init__.py new file mode 100644 index 00000000..faed57e5 --- /dev/null +++ b/project_workload_additions/tests/__init__.py @@ -0,0 +1 @@ +from . import test_workload_addition diff --git a/project_workload_additions/tests/test_workload_addition.py b/project_workload_additions/tests/test_workload_addition.py new file mode 100644 index 00000000..e37292b5 --- /dev/null +++ b/project_workload_additions/tests/test_workload_addition.py @@ -0,0 +1,105 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo.addons.project_workload.tests.common import TestWorkloadCommon + + +class TestWorkload(TestWorkloadCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.task_management, cls.task_review = cls.env["project.task"].create( + [ + {"name": "Management", "project_id": cls.project.id}, + {"name": "Review", "project_id": cls.project.id}, + ] + ) + cls.type_management, cls.type_review = cls.env[ + "project.task.workload.addition.type" + ].create( + [ + {"name": "Management", "default_percentage": 15}, + {"name": "Review", "default_percentage": 20}, + ] + ) + cls.add_work_management, cls.add_work_review = cls.env[ + "project.task.workload.addition" + ].create( + [ + { + "project_id": cls.project.id, + "type_id": cls.type_management.id, + "user_id": cls.user_1.id, + "task_id": cls.task_management.id, + }, + { + "project_id": cls.project.id, + "type_id": cls.type_review.id, + "user_id": cls.user_2.id, + "task_id": cls.task_review.id, + }, + ] + ) + + def _create_task(self, user_id=None): + now = datetime.now() + return self.env["project.task"].create( + { + "name": "Task 1", + "project_id": self.project.id, + "user_id": user_id, + "date_start": now, + "date_end": now + timedelta(days=20), + "planned_hours": 21, + } + ) + + def test_task_assign_with_hours(self): + task = self._create_task(self.user_1.id) + workloads = task.workload_ids + + self.assertEqual(len(workloads), 3) + worload, workload_management, workload_review = workloads + + self.assertEqual(workload_management.date_start, task.date_start.date()) + self.assertEqual(workload_management.date_end, task.date_end.date()) + self.assertEqual(workload_management.hours, 3.15) + self.assertEqual(workload_management.user_id, self.user_1) + self.assertEqual( + workload_management.additional_workload_id, self.add_work_management + ) + + self.assertEqual(workload_review.date_start, task.date_start.date()) + self.assertEqual(workload_review.date_end, task.date_end.date()) + self.assertEqual(workload_review.hours, 4.2) + self.assertEqual(workload_review.user_id, self.user_2) + self.assertEqual(workload_review.additional_workload_id, self.add_work_review) + + def _assert_only_additionnal_workload(self, workloads): + self.assertEqual(len(workloads), 2) + workload_management, workload_review = workloads + self.assertEqual( + workload_management.additional_workload_id, self.add_work_management + ) + self.assertEqual(workload_review.additional_workload_id, self.add_work_review) + + def test_create_unasign_task(self): + task = self._create_task(None) + self._assert_only_additionnal_workload(task.workload_ids) + + def test_remove_user(self): + task = self._create_task(self.user_1.id) + task.user_id = False + self._assert_only_additionnal_workload(task.workload_ids) + + def test_update_hours(self): + task = self._create_task(self.user_1.id) + task.planned_hours = 42 + self.assertEqual(len(task.workload_ids), 3) + worload, workload_management, workload_review = task.workload_ids + self.assertEqual(workload_management.hours, 6.30) + self.assertEqual(workload_review.hours, 8.4) diff --git a/project_workload_additions/views/project_project_view.xml b/project_workload_additions/views/project_project_view.xml index b2487ae1..34967962 100644 --- a/project_workload_additions/views/project_project_view.xml +++ b/project_workload_additions/views/project_project_view.xml @@ -12,9 +12,9 @@ attrs="{'invisible': [('use_workload', '=', False)]}" > - + - + # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, models +from odoo import api, fields, models class ProjectTask(models.Model): _inherit = "project.task" - def _prepare_task_dates_vals_from_milestone(self, vals): - # If we create a task with a milestone or affect it after, - # we set the task dates to the milestone dates + date_start = fields.Datetime( + compute="_compute_date_start_end", + store=True, + readonly=False, + ) + date_end = fields.Datetime( + compute="_compute_date_start_end", + store=True, + readonly=False, + ) - if "milestone_id" in vals: - milestone = self.env["project.milestone"].browse(vals["milestone_id"]) - if milestone: - vals["date_end"] = milestone.target_date - vals["date_start"] = milestone.start_date - return vals - - @api.model - def create(self, vals): - return super().create(self._prepare_task_dates_vals_from_milestone(vals)) - - def write(self, vals): - return super().write(self._prepare_task_dates_vals_from_milestone(vals)) + @api.depends("milestone_id.start_date", "milestone_id.target_date") + def _compute_date_start_end(self): + for record in self: + record.date_end = record.milestone_id.target_date + record.date_start = record.milestone_id.start_date