diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py index 24bba5c..6a404dd 100644 --- a/.docker_files/main/__manifest__.py +++ b/.docker_files/main/__manifest__.py @@ -13,6 +13,7 @@ "depends": [ "hr_timesheet_project_parent_enhanced", "project_timesheet_time_control_sheet", + "timesheet_task_project_no_change", ], "installable": True, } diff --git a/Dockerfile b/Dockerfile index caef0b0..13dc86d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ USER odoo COPY hr_timesheet_project_parent_enhanced /mnt/extra-addons/hr_timesheet_project_parent_enhanced COPY project_timesheet_time_control_sheet /mnt/extra-addons/project_timesheet_time_control_sheet +COPY timesheet_task_project_no_change /mnt/extra-addons/timesheet_task_project_no_change COPY .docker_files/main /mnt/extra-addons/main COPY .docker_files/odoo.conf /etc/odoo diff --git a/timesheet_task_project_no_change/README.rst b/timesheet_task_project_no_change/README.rst new file mode 100644 index 0000000..7569ffe --- /dev/null +++ b/timesheet_task_project_no_change/README.rst @@ -0,0 +1,23 @@ +Timesheet Task Project No Change +================================ + +Usage +----- + +As a user of the **Project** module, when attempting to change the project of a task: + +1. If timesheets have already been recorded on the task, a blocking message will appear: + + .. image:: static/description/task_timesheet_error.png + :alt: Blocking error when task has timesheets + +2. If timesheets have been recorded on a subtask, a similar blocking message will appear: + + .. image:: static/description/subtask_timesheet_error.png + :alt: Blocking error when subtask has timesheets + +3. In both cases, the task's project cannot be changed unless no timesheets exist for the task or its subtasks. + +Contributors +------------ +* Numigi (tm) and all its contributors (https://bit.ly/numigiens) diff --git a/timesheet_task_project_no_change/__init__.py b/timesheet_task_project_no_change/__init__.py new file mode 100644 index 0000000..efb1fef --- /dev/null +++ b/timesheet_task_project_no_change/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/timesheet_task_project_no_change/__manifest__.py b/timesheet_task_project_no_change/__manifest__.py new file mode 100644 index 0000000..2109075 --- /dev/null +++ b/timesheet_task_project_no_change/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2024 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Timesheet Task Project No Change", + "version": "16.0.1.0.0", + "author": "Numigi", + "maintainer": "Numigi", + "website": "https://bit.ly/numigi-com", + "license": "LGPL-3", + "category": "Project", + "summary": "Prevent changing the project on a task with timesheets.", + "depends": ["hr_timesheet"], + "installable": True, +} diff --git a/timesheet_task_project_no_change/i18n/fr.po b/timesheet_task_project_no_change/i18n/fr.po new file mode 100644 index 0000000..25f0541 --- /dev/null +++ b/timesheet_task_project_no_change/i18n/fr.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * timesheet_task_project_no_change +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-12-03 11:49+0000\n" +"PO-Revision-Date: 2024-12-03 11:49+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: timesheet_task_project_no_change +#: model:ir.model,name:timesheet_task_project_no_change.model_project_task +msgid "Task" +msgstr "Tâche" + +#. module: timesheet_task_project_no_change +#. odoo-python +#: code:addons/timesheet_task_project_no_change/models/project_task.py:0 +#, python-format +msgid "" +"Timesheets have already been entered on a sub-task ({subtask}). In order to " +"modify the project of the parent task ({task}), there must be no time on the" +" parent task, nor on its child tasks." +msgstr "" +"Du temps a été saisi une des tâches enfant ({subtask}). Pour modifier le " +"projet de la tâche parente ({task}), il ne doit pas y avoir de temps saisi " +"sur celle-ci ou sur ses tâches enfants." + +#. module: timesheet_task_project_no_change +#. odoo-python +#: code:addons/timesheet_task_project_no_change/models/project_task.py:0 +#, python-format +msgid "" +"Timesheets have already been entered on this task ({task}). In order to " +"modify the project of this task, you may close the task and create another " +"in the target project." +msgstr "" +"Du temps a déjà été saisi sur la tâche ({task}). Pour modifier le projet de " +"la tâche, veuillez fermer celle-ci et en créer une nouvelle sur le projet " +"cible." diff --git a/timesheet_task_project_no_change/models/__init__.py b/timesheet_task_project_no_change/models/__init__.py new file mode 100644 index 0000000..022d5ba --- /dev/null +++ b/timesheet_task_project_no_change/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import project_task diff --git a/timesheet_task_project_no_change/models/project_task.py b/timesheet_task_project_no_change/models/project_task.py new file mode 100644 index 0000000..4a0ce64 --- /dev/null +++ b/timesheet_task_project_no_change/models/project_task.py @@ -0,0 +1,50 @@ +# Copyright 2024 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import models, _ +from odoo.exceptions import ValidationError + + +class ProjectTask(models.Model): + _inherit = "project.task" + + def write(self, vals): + if "project_id" in vals: + project = self.env["project.project"].browse(vals["project_id"]) + tasks_with_different_project = self.filtered( + lambda t: t.project_id != project + ) + + for task in tasks_with_different_project: + task._validate_no_timesheets() + task._validate_no_subtask_timesheets() + + return super().write(vals) + + def _validate_no_timesheets(self): + if self.sudo().timesheet_ids: + raise ValidationError( + _( + "Timesheets have already been entered on this task ({task}). " + "In order to modify the project of this task, you may " + "close the task and create another in the target project." + ).format(task=self.display_name) + ) + + def _validate_no_subtask_timesheets(self): + timesheets = ( + self.env["account.analytic.line"] + .sudo() + .search([("task_id.parent_id", "=", self.id)]) + ) + + if timesheets: + raise ValidationError( + _( + "Timesheets have already been entered on a sub-task ({subtask}). " + "In order to modify the project of the parent task ({task}), there must " + "be no time on the parent task, nor on its child tasks." + ).format( + task=self.display_name, subtask=timesheets[0].task_id.display_name + ) + ) diff --git a/timesheet_task_project_no_change/static/description/icon.png b/timesheet_task_project_no_change/static/description/icon.png new file mode 100644 index 0000000..92a86b1 Binary files /dev/null and b/timesheet_task_project_no_change/static/description/icon.png differ diff --git a/timesheet_task_project_no_change/static/description/subtask_timesheet_error.png b/timesheet_task_project_no_change/static/description/subtask_timesheet_error.png new file mode 100644 index 0000000..acf3855 Binary files /dev/null and b/timesheet_task_project_no_change/static/description/subtask_timesheet_error.png differ diff --git a/timesheet_task_project_no_change/static/description/task_timesheet_error.png b/timesheet_task_project_no_change/static/description/task_timesheet_error.png new file mode 100644 index 0000000..4427027 Binary files /dev/null and b/timesheet_task_project_no_change/static/description/task_timesheet_error.png differ diff --git a/timesheet_task_project_no_change/tests/__init__.py b/timesheet_task_project_no_change/tests/__init__.py new file mode 100644 index 0000000..ad0d160 --- /dev/null +++ b/timesheet_task_project_no_change/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import test_project_task diff --git a/timesheet_task_project_no_change/tests/test_project_task.py b/timesheet_task_project_no_change/tests/test_project_task.py new file mode 100644 index 0000000..80952d0 --- /dev/null +++ b/timesheet_task_project_no_change/tests/test_project_task.py @@ -0,0 +1,51 @@ +# Copyright 2024 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import pytest +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestProjectTask(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.project_a = cls.env["project.project"].create({"name": "projectA"}) + cls.project_b = cls.env["project.project"].create({"name": "projectB"}) + cls.task = cls.env["project.task"].create( + {"name": "Parent Task", "project_id": cls.project_a.id} + ) + cls.subtask = cls.env["project.task"].create( + { + "name": "Child Task", + "project_id": cls.project_a.id, + "parent_id": cls.task.id, + } + ) + + def test_if_no_timesheet__project_can_be_changed(self): + self.task.project_id = self.project_b + + def test_if_project_is_the_same__constraint_not_raised(self): + self._make_timesheet_line(self.task) + self.task.project_id = self.project_a + + def test_if_task_has_timesheet__project_can_not_be_changed(self): + self._make_timesheet_line(self.task) + with pytest.raises(ValidationError): + self.task.project_id = self.project_b + + def test_if_subtask_has_timesheet__project_can_not_be_changed(self): + self._make_timesheet_line(self.subtask) + with pytest.raises(ValidationError): + self.task.project_id = self.project_b + + def _make_timesheet_line(self, task): + return self.env["account.analytic.line"].create( + { + "task_id": task.id, + "project_id": task.project_id.id, + "name": "/", + "employee_id": self.ref("hr.employee_admin"), + } + )