diff --git a/project_milestone_time_kpi/README.rst b/project_milestone_time_kpi/README.rst new file mode 100644 index 0000000000..86a21dda4b --- /dev/null +++ b/project_milestone_time_kpi/README.rst @@ -0,0 +1,68 @@ +Project Milestone time KPI +========================== + +.. contents:: Table of Contents + +Context +------- +The module has just enriched the milestones tab on the project, in order to offer 3 new fields: + +* Total Estimated Hours. +* Total Hours Spent. +* Total Remaining Work. + +This option is advantageous because it allows you to compare the total hours spent on the project with the estimated hours of each milestone in order to be able to weight an overrun on a batch phase, and to see the time remaining on the batch by also having visual status of milestones. + +These fields are added to the list view of projects, in order to facilitate monitoring. + + +Overview +-------- +The module inherits two modules that have been added to allow to define an estimated time on the milestone +`(project_milestone_estimated_hours)`, and to calculate the times spent on a milestone `(project_milestone_spent_hours)` + +The module add 4 new fields to project: + +* **Total Estimated Hours**: Field calculated by taking into account the total of the 'Estimated hours' fields of each milestone associated with the project. +* **Total Spent Hours**: Field calculated by taking into account the total of the 'Hours spent' fields of each milestone associated with the project. +* **Remaining Estimated Hours**: Calculated field based on the following calculation: Total Estimated Hours - Total Hours Spent. +* **Total Remaining Work**: Field calculated based on the total of the ‘Remaining hours’ fields of the tasks associated with a project. + +**Total Estimated Hours**, **Total Spent Hours** and **Total Remaining Work** are visible under the milestone tab +in form view and visible in tree view after milestones column. + +Usage +----- +As a user, from a project whose **Use milestones** box is checked, go to the tab of a project. + +1. Add 2 milestones: + +* Analysis (estimated 10h). + + .. image:: project_milestone_time_kpi/static/description/milestone_1.png + :width: 100% + :align: center + :height: 600px + :alt: Milestone_1 + +* Realization (estimated 20h). + + .. image:: project_milestone_time_kpi/static/description/milestone_2.png + :width: 100% + :align: center + :height: 600px + :alt: Milestone_2 + +2. From the project tasks, assigned to the respective milestones, add timelines. + + .. image:: project_milestone_time_kpi/static/description/milestone_spent_hours.png + :width: 100% + :align: center + :height: 600px + :alt: Milestone_spent_hours + +You can see that the estimated times of the milestones, and the times spent are well up in the list of milestones. + +Contributors +------------ +* Numigi (tm) and all its contributors (https://bit.ly/numigiens) diff --git a/project_milestone_time_kpi/__init__.py b/project_milestone_time_kpi/__init__.py new file mode 100644 index 0000000000..fd8d229625 --- /dev/null +++ b/project_milestone_time_kpi/__init__.py @@ -0,0 +1,4 @@ +# © 2023 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/project_milestone_time_kpi/__manifest__.py b/project_milestone_time_kpi/__manifest__.py new file mode 100644 index 0000000000..33a99f3490 --- /dev/null +++ b/project_milestone_time_kpi/__manifest__.py @@ -0,0 +1,17 @@ +# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Project Milestone Time KPI", + "version": "1.0.0", + "author": "Numigi", + "maintainer": "Numigi", + "website": "https://bit.ly/numigi-com", + "license": "LGPL-3", + "category": "Project", + "summary": "Track budgeted hours on projects using milestone", + "depends": ["project_milestone_estimated_hours", + "project_milestone_spent_hours"], + "data": ["views/project_project.xml"], + "installable": True, +} diff --git a/project_milestone_time_kpi/i18n/fr.po b/project_milestone_time_kpi/i18n/fr.po new file mode 100644 index 0000000000..227420dccc --- /dev/null +++ b/project_milestone_time_kpi/i18n/fr.po @@ -0,0 +1,42 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_milestone_time_kpi +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-06-25 00:11+0000\n" +"PO-Revision-Date: 2022-06-25 00:11+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: project_milestone_time_kpi +#: model:ir.model,name:project_milestone_time_kpi.model_project_project +msgid "Project" +msgstr "Projet" + +#. module: project_milestone_time_kpi +#: model:ir.model.fields,field_description:project_milestone_time_kpi.field_project_project__remaining_estimated_hours +msgid "Remaining Estimated Hours" +msgstr "Heures estimées restantes" + +#. module: project_milestone_time_kpi +#: model:ir.model.fields,field_description:project_milestone_time_kpi.field_project_project__total_estimated_hours +msgid "Total Estimated Hours" +msgstr "Total heures estimés" + +#. module: project_milestone_time_kpi +#: model:ir.model.fields,field_description:project_milestone_time_kpi.field_project_project__total_remaining_hours +msgid "Total Remaining Work" +msgstr "Total restant à faire" + +#. module: project_milestone_time_kpi +#: model:ir.model.fields,field_description:project_milestone_time_kpi.field_project_project__total_spent_hours +msgid "Total Spent Hours" +msgstr "Total heures passées" + diff --git a/project_milestone_time_kpi/models/__init__.py b/project_milestone_time_kpi/models/__init__.py new file mode 100644 index 0000000000..f7635957f8 --- /dev/null +++ b/project_milestone_time_kpi/models/__init__.py @@ -0,0 +1,4 @@ +# © 2023 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_project diff --git a/project_milestone_time_kpi/models/project_project.py b/project_milestone_time_kpi/models/project_project.py new file mode 100644 index 0000000000..f521d384dd --- /dev/null +++ b/project_milestone_time_kpi/models/project_project.py @@ -0,0 +1,43 @@ +# © 2023 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 fields, models, api + + +class Project(models.Model): + _inherit = "project.project" + + total_estimated_hours = fields.Float( + "Total Estimated Hours", compute="_compute_total_hours", store=True + ) + total_spent_hours = fields.Float( + "Total Spent Hours", compute="_compute_total_hours", store=True + ) + remaining_estimated_hours = fields.Float( + "Remaining Estimated Hours", + compute="_compute_remaining_estimated_hours", store=True + ) + total_remaining_hours = fields.Float( + "Total Remaining Work", compute="_compute_remaining_hours", store=True + ) + + @api.depends("milestone_ids", "milestone_ids.estimated_hours", + "milestone_ids.total_hours") + def _compute_total_hours(self): + for project in self: + project.total_estimated_hours = sum( + project.milestone_ids.mapped("estimated_hours")) + project.total_spent_hours = sum( + project.milestone_ids.mapped("total_hours")) + + @api.depends("total_estimated_hours", "total_spent_hours") + def _compute_remaining_estimated_hours(self): + for project in self: + project.remaining_estimated_hours = \ + project.total_estimated_hours - project.total_spent_hours + + @api.depends("task_ids", "task_ids.remaining_hours") + def _compute_remaining_hours(self): + for project in self: + project.total_remaining_hours = sum( + project.task_ids.mapped("remaining_hours")) diff --git a/project_milestone_time_kpi/static/description/icon.png b/project_milestone_time_kpi/static/description/icon.png new file mode 100644 index 0000000000..92a86b10ed Binary files /dev/null and b/project_milestone_time_kpi/static/description/icon.png differ diff --git a/project_milestone_time_kpi/static/description/milestone_1.png b/project_milestone_time_kpi/static/description/milestone_1.png new file mode 100644 index 0000000000..1e4c3a9a90 Binary files /dev/null and b/project_milestone_time_kpi/static/description/milestone_1.png differ diff --git a/project_milestone_time_kpi/static/description/milestone_2.png b/project_milestone_time_kpi/static/description/milestone_2.png new file mode 100644 index 0000000000..b9fa371f3b Binary files /dev/null and b/project_milestone_time_kpi/static/description/milestone_2.png differ diff --git a/project_milestone_time_kpi/static/description/milestone_spent_hours.png b/project_milestone_time_kpi/static/description/milestone_spent_hours.png new file mode 100644 index 0000000000..ef760c5ce7 Binary files /dev/null and b/project_milestone_time_kpi/static/description/milestone_spent_hours.png differ diff --git a/project_milestone_time_kpi/tests/__init__.py b/project_milestone_time_kpi/tests/__init__.py new file mode 100644 index 0000000000..02f8880d29 --- /dev/null +++ b/project_milestone_time_kpi/tests/__init__.py @@ -0,0 +1,4 @@ +# © 2023 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_milestone_time_kpi diff --git a/project_milestone_time_kpi/tests/test_project_milestone_time_kpi.py b/project_milestone_time_kpi/tests/test_project_milestone_time_kpi.py new file mode 100644 index 0000000000..a91bf50576 --- /dev/null +++ b/project_milestone_time_kpi/tests/test_project_milestone_time_kpi.py @@ -0,0 +1,98 @@ +# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo.tests.common import SavepointCase + + +class TestProjectMilestoneTimeKPI(SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestProjectMilestoneTimeKPI, cls).setUpClass() + + # create a generic Project with 2 milestones and 2 tasks associated to milestones + cls.stage = cls.env["project.task.type"].create({ + "name": "New" + }) + + cls.project_a = cls.env["project.project"].create({ + "name": "Project A", + "use_milestones": True, + }) + + cls.milestone_a = cls.env['project.milestone'].create({ + "name": "Analysis", + "estimated_hours": 8, + "project_id": cls.project_a.id, + }) + cls.milestone_b = cls.env['project.milestone'].create({ + "name": "Realization", + "estimated_hours": 20, + "project_id": cls.project_a.id, + }) + + cls.task_a = cls.env["project.task"].create({ + "name": "Task A", + "project_id": cls.project_a.id, + "milestone_id": cls.milestone_a.id, + "planned_hours": 4, + "stage_id": cls.stage.id, + }) + + cls.task_b = cls.env["project.task"].create({ + "name": "Task B", + "project_id": cls.project_a.id, + "milestone_id": cls.milestone_b.id, + "planned_hours": 8, + "stage_id": cls.stage.id, + }) + + # Add timelines to created tasks + cls.timesheet_a = cls.env["account.analytic.line"].create({ + "name": "Analyse", + "project_id": cls.project_a.id, + "task_id": cls.task_a.id, + "unit_amount": 3, + "employee_id": 1, + "date": "2022-06-25", + }) + + cls.timesheet_b = cls.env["account.analytic.line"].create({ + "name": "Conception", + "project_id": cls.project_a.id, + "task_id": cls.task_b.id, + "unit_amount": 4, + "employee_id": 1, + "date": "2022-06-30", + }) + + def test_timesheet_line_created(self): + """ Test total_remaining_hours calculation after adding a new timeline to task """ + self.env["account.analytic.line"].create({ + "name": "Development", + "project_id": self.project_a.id, + "task_id": self.task_b.id, + "unit_amount": 3, + "employee_id": 1, + "date": "2022-06-30", + }) + assert self.project_a.total_estimated_hours == 28 + assert self.project_a.total_spent_hours == 10 + assert self.project_a.total_remaining_hours == 2 + + def test_add_milestone(self): + self.env['project.milestone'].create({ + "name": "Test", + "estimated_hours": 8, + "project_id": self.project_a.id, + }) + assert self.project_a.total_estimated_hours == 36 + + def test_unlink_milestone(self): + self.milestone_b.unlink() + assert self.project_a.total_estimated_hours == 8 + assert self.project_a.total_spent_hours == 3 + + def test_update_milestone(self): + self.milestone_a.write({"estimated_hours": 10}) + assert self.project_a.total_estimated_hours == 30 diff --git a/project_milestone_time_kpi/views/project_project.xml b/project_milestone_time_kpi/views/project_project.xml new file mode 100644 index 0000000000..0d4ecf71ba --- /dev/null +++ b/project_milestone_time_kpi/views/project_project.xml @@ -0,0 +1,31 @@ + + + + Project: add milestone time kpi + project.project + + + + + + + + + + + + + + + Project: add milestone time kpi + project.project + + + + + + + + + +