diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py
index 65f50dec..0a32405c 100644
--- a/.docker_files/main/__manifest__.py
+++ b/.docker_files/main/__manifest__.py
@@ -19,6 +19,7 @@
"project_milestone_spent_hours",
"project_no_quick_create",
"project_parent_enhanced",
+ "project_progress_variance",
"project_projected_hours",
"project_remaining_hours_update",
"project_stage_allow_timesheet",
diff --git a/Dockerfile b/Dockerfile
index 9b472bbd..adcdd3d9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,6 +20,7 @@ COPY project_milestone_estimated_hours /mnt/extra-addons/project_milestone_estim
COPY project_milestone_spent_hours /mnt/extra-addons/project_milestone_spent_hours
COPY project_no_quick_create /mnt/extra-addons/project_no_quick_create
COPY project_parent_enhanced mnt/extra-addons/project_parent_enhanced
+COPY project_progress_variance /mnt/extra-addons/project_progress_variance
COPY project_projected_hours mnt/extra-addons/project_projected_hours
COPY project_remaining_hours_update /mnt/extra-addons/project_remaining_hours_update
COPY project_stage_allow_timesheet mnt/extra-addons/project_stage_allow_timesheet
diff --git a/project_progress_variance/README.rst b/project_progress_variance/README.rst
new file mode 100644
index 00000000..c0188a3f
--- /dev/null
+++ b/project_progress_variance/README.rst
@@ -0,0 +1,28 @@
+Project Progress Variance
+=========================
+This module allows to:
+
+* Add the ”Variance on progress” field in the form view of a task
+Computed as : task real progress + task theorical progress.
+
+.. image:: static/description/task_progress_variance.png
+
+* Add the “Variance on progress” in the analysis list view “Progress of tasks”
+
+.. image:: static/description/task_progress_report.png
+
+* Add the ”Progress” tab in the form view of a project
+ * The “Total theoretical progress” field
+ Computed as : total of effective hours of project tasks / total of planned hours of project tasks.
+
+ * The "Total real progress" field
+ Computed as : total of effective hours of project tasks / total of projected hours of project tasks.
+
+ * The “Total variance on progress” field
+ Computed as : total of real progress - Total theoretical progress.
+
+.. image:: static/description/project_progress_tab.png
+
+Contributors
+------------
+* Numigi (tm) and all its contributors (https://bit.ly/numigiens)
diff --git a/project_progress_variance/__init__.py b/project_progress_variance/__init__.py
new file mode 100644
index 00000000..01e87973
--- /dev/null
+++ b/project_progress_variance/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import models
diff --git a/project_progress_variance/__manifest__.py b/project_progress_variance/__manifest__.py
new file mode 100644
index 00000000..af9aa145
--- /dev/null
+++ b/project_progress_variance/__manifest__.py
@@ -0,0 +1,20 @@
+# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Project Progress Variance",
+ "version": "16.0.1.0.0",
+ "author": "Numigi",
+ "maintainer": "Numigi",
+ "website": "https://bit.ly/numigi-com",
+ "license": "AGPL-3",
+ "category": "Project",
+ "depends": ["project_projected_hours"],
+ "summary": "Add some progress variance time on task and project",
+ "data": [
+ "views/project_project_views.xml",
+ "views/project_task_views.xml",
+ "views/project_task_progress_report_views.xml",
+ ],
+ "installable": True,
+}
diff --git a/project_progress_variance/i18n/fr.po b/project_progress_variance/i18n/fr.po
new file mode 100644
index 00000000..45eff3c2
--- /dev/null
+++ b/project_progress_variance/i18n/fr.po
@@ -0,0 +1,51 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * project_progress_variance
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-12-06 06:10+0000\n"
+"PO-Revision-Date: 2024-12-06 06:10+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_progress_variance
+#: model_terms:ir.ui.view,arch_db:project_progress_variance.edit_project
+msgid "Progress"
+msgstr "Avancement"
+
+#. module: project_progress_variance
+#: model:ir.model.fields,field_description:project_progress_variance.field_project_task__progress_variance
+msgid "Progress Variance"
+msgstr "Écart sur avancement"
+
+#. module: project_progress_variance
+#: model:ir.model,name:project_progress_variance.model_project_project
+msgid "Project"
+msgstr "Projet"
+
+#. module: project_progress_variance
+#: model:ir.model,name:project_progress_variance.model_project_task
+msgid "Task"
+msgstr "Tâche"
+
+#. module: project_progress_variance
+#: model:ir.model.fields,field_description:project_progress_variance.field_project_project__total_progress
+msgid "Total Progress"
+msgstr "Total avancement théorique"
+
+#. module: project_progress_variance
+#: model:ir.model.fields,field_description:project_progress_variance.field_project_project__total_progress_variance
+msgid "Total Progress Variance"
+msgstr "Total écart sur avancement"
+
+#. module: project_progress_variance
+#: model:ir.model.fields,field_description:project_progress_variance.field_project_project__total_real_progress
+msgid "Total Real Progress"
+msgstr "Total avancement réel"
diff --git a/project_progress_variance/models/__init__.py b/project_progress_variance/models/__init__.py
new file mode 100644
index 00000000..3ef88a01
--- /dev/null
+++ b/project_progress_variance/models/__init__.py
@@ -0,0 +1,5 @@
+# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import project_project
+from . import project_task
diff --git a/project_progress_variance/models/project_project.py b/project_progress_variance/models/project_project.py
new file mode 100644
index 00000000..a548f678
--- /dev/null
+++ b/project_progress_variance/models/project_project.py
@@ -0,0 +1,57 @@
+# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import api, fields, models
+
+
+class ProjectProject(models.Model):
+ _inherit = "project.project"
+
+ total_progress = fields.Float(
+ "Total Progress",
+ digits=(16, 2),
+ compute="_compute_total_progress",
+ readonly=True,
+ store=True,
+ )
+
+ total_real_progress = fields.Float(
+ "Total Real Progress",
+ digits=(16, 2),
+ compute="_compute_total_real_progress",
+ readonly=True,
+ store=True,
+ )
+
+ total_progress_variance = fields.Float(
+ "Total Progress Variance",
+ digits=(16, 2),
+ compute="_compute_total_progress_variance",
+ readonly=True,
+ store=True,
+ )
+
+ @api.depends("tasks", "tasks.planned_hours", "tasks.effective_hours")
+ def _compute_total_progress(self):
+ for project in self:
+ planned_hours = sum(project.tasks.mapped("planned_hours"))
+ effective_hours = sum(project.tasks.mapped("effective_hours"))
+ project.total_progress = (
+ 100.0 * (effective_hours / planned_hours) if planned_hours else 0.0
+ )
+
+ @api.depends("tasks", "tasks.projected_hours", "tasks.effective_hours")
+ def _compute_total_real_progress(self):
+ for project in self:
+ projected_hours = sum(project.tasks.mapped("projected_hours"))
+ effective_hours = sum(project.tasks.mapped("effective_hours"))
+ project.total_real_progress = (
+ 100.0 * (effective_hours / projected_hours) if projected_hours else 0.0
+ )
+
+ @api.depends("total_progress", "total_real_progress")
+ def _compute_total_progress_variance(self):
+ for project in self:
+ project.total_progress_variance = (
+ project.total_real_progress - project.total_progress
+ )
diff --git a/project_progress_variance/models/project_task.py b/project_progress_variance/models/project_task.py
new file mode 100644
index 00000000..b29afc8e
--- /dev/null
+++ b/project_progress_variance/models/project_task.py
@@ -0,0 +1,21 @@
+# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import api, fields, models
+
+
+class ProjectTask(models.Model):
+ _inherit = "project.task"
+
+ progress_variance = fields.Float(
+ "Progress Variance",
+ digits=(16, 2),
+ compute="_compute_progress_variance",
+ readonly=True,
+ store=True,
+ )
+
+ @api.depends("progress", "real_progress")
+ def _compute_progress_variance(self):
+ for task in self:
+ task.progress_variance = task.real_progress - task.progress
diff --git a/project_progress_variance/static/description/icon.png b/project_progress_variance/static/description/icon.png
new file mode 100644
index 00000000..92a86b10
Binary files /dev/null and b/project_progress_variance/static/description/icon.png differ
diff --git a/project_progress_variance/static/description/project_progress_tab.png b/project_progress_variance/static/description/project_progress_tab.png
new file mode 100644
index 00000000..25e1a468
Binary files /dev/null and b/project_progress_variance/static/description/project_progress_tab.png differ
diff --git a/project_progress_variance/static/description/task_progress_report.png b/project_progress_variance/static/description/task_progress_report.png
new file mode 100644
index 00000000..f2ad9bbb
Binary files /dev/null and b/project_progress_variance/static/description/task_progress_report.png differ
diff --git a/project_progress_variance/static/description/task_progress_variance.png b/project_progress_variance/static/description/task_progress_variance.png
new file mode 100644
index 00000000..3998c00c
Binary files /dev/null and b/project_progress_variance/static/description/task_progress_variance.png differ
diff --git a/project_progress_variance/tests/__init__.py b/project_progress_variance/tests/__init__.py
new file mode 100644
index 00000000..5d0f06d4
--- /dev/null
+++ b/project_progress_variance/tests/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import test_progress_variance
diff --git a/project_progress_variance/tests/test_progress_variance.py b/project_progress_variance/tests/test_progress_variance.py
new file mode 100644
index 00000000..74472022
--- /dev/null
+++ b/project_progress_variance/tests/test_progress_variance.py
@@ -0,0 +1,88 @@
+# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from datetime import datetime
+
+from odoo.tests.common import TransactionCase
+
+
+class TestCustomerReference(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.project = cls.env["project.project"].create(
+ {"name": "test_project", "allow_timesheets": True}
+ )
+ cls.task_1 = cls.env["project.task"].create(
+ {"name": "test_task_1", "project_id": cls.project.id}
+ )
+
+ cls.task_2 = cls.env["project.task"].create(
+ {"name": "test_task_2", "project_id": cls.project.id}
+ )
+
+ def test_total_progress(self):
+ self.assertEqual(self.project.total_progress, 0)
+ self._load_analytic_line_remaining_hours_not_updated()
+ self.assertEqual(round(self.project.total_progress, 2), 47.83)
+
+ def test_total_progress_remaining_hours_updated(self):
+ self.assertEqual(self.project.total_progress, 0)
+ self._load_analytic_line_with_remaining_hours_updated()
+ self.assertEqual(round(self.project.total_progress, 2), 32.5)
+
+ def test_total_real_progress(self):
+ self.assertEqual(self.project.total_real_progress, 0)
+ self._load_analytic_line_remaining_hours_not_updated()
+ self.assertEqual(round(self.project.total_real_progress, 2), 47.83)
+
+ def test_total_real_progress_remaining_hours_updated(self):
+ self.assertEqual(self.project.total_real_progress, 0)
+ self._load_analytic_line_with_remaining_hours_updated()
+ self.assertEqual(round(self.project.total_real_progress, 2), 27.08)
+
+ def test_total_progress_variance(self):
+ self.assertEqual(self.project.total_progress_variance, 0)
+ self._load_analytic_line_remaining_hours_not_updated()
+ self.assertEqual(self.project.total_progress_variance, 0)
+
+ def test_total_progress_variance_remaining_hours_updated(self):
+ self.assertEqual(self.project.total_progress_variance, 0)
+ self._load_analytic_line_with_remaining_hours_updated()
+ self.assertEqual(round(self.project.total_real_progress, 2), 27.08)
+ self.assertEqual(round(self.project.total_progress, 2), 32.5)
+ self.assertEqual(round(self.project.total_progress_variance, 2), -5.42)
+
+ def _load_analytic_line_remaining_hours_not_updated(self):
+ self.task_1.planned_hours = 10
+ self.task_2.planned_hours = 13
+ self._create_analytic_line(
+ datetime(2023, 3, 24, 3), tz="EST", task_id=self.task_1, unit_amount=7
+ )
+ self._create_analytic_line(
+ datetime(2023, 3, 24, 3), tz="EST", task_id=self.task_2, unit_amount=4
+ )
+
+ def _load_analytic_line_with_remaining_hours_updated(self):
+ self.task_1.planned_hours = 30
+ self.task_2.planned_hours = 10
+ self._create_analytic_line(
+ datetime(2023, 3, 24, 3), tz="EST", task_id=self.task_1, unit_amount=5
+ )
+ self._create_analytic_line(
+ datetime(2023, 3, 24, 3), tz="EST", task_id=self.task_2, unit_amount=8
+ )
+ self.task_1.remaining_hours = 30
+ self.task_2.remaining_hours = 5
+
+ def _create_analytic_line(self, datetime_, tz=None, task_id=False, unit_amount=0):
+ self.env["account.analytic.line"].with_context(tz=tz).create(
+ {
+ "date": datetime_,
+ "project_id": self.project.id,
+ "task_id": task_id.id,
+ "name": "Test line",
+ "unit_amount": unit_amount,
+ "employee_id": self.ref("hr.employee_admin"),
+ }
+ )
diff --git a/project_progress_variance/views/project_project_views.xml b/project_progress_variance/views/project_project_views.xml
new file mode 100644
index 00000000..5bc7044d
--- /dev/null
+++ b/project_progress_variance/views/project_project_views.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ project.project.form.inherit
+ project.project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/project_progress_variance/views/project_task_progress_report_views.xml b/project_progress_variance/views/project_task_progress_report_views.xml
new file mode 100644
index 00000000..95ce08b7
--- /dev/null
+++ b/project_progress_variance/views/project_task_progress_report_views.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ report.project.task.progress.tree.inherited
+ project.task
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/project_progress_variance/views/project_task_views.xml b/project_progress_variance/views/project_task_views.xml
new file mode 100644
index 00000000..7eef04be
--- /dev/null
+++ b/project_progress_variance/views/project_task_views.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ project.task.form.inherited
+ project.task
+
+
+
+
+
+
+
+
+