diff --git a/project_task_report/README.rst b/project_task_report/README.rst
new file mode 100644
index 00000000..7ba71679
--- /dev/null
+++ b/project_task_report/README.rst
@@ -0,0 +1,17 @@
+===================
+Project Task Report
+===================
+
+Task wizard to sum up the progress of some projects tasks between two dates
+
+Usage
+=====
+
+To use this module, you need to:
+
+#. Go to Project > Reporting > Tasks Report
+#. Select the related Projects and the Start and End dates of the report
+#. The resulting report is a list of the Tasks with new timesheets and/or with stage change between the two selected dates
+
+
+
diff --git a/project_task_report/__init__.py b/project_task_report/__init__.py
new file mode 100644
index 00000000..5cb1c491
--- /dev/null
+++ b/project_task_report/__init__.py
@@ -0,0 +1 @@
+from . import wizards
diff --git a/project_task_report/__manifest__.py b/project_task_report/__manifest__.py
new file mode 100644
index 00000000..3e90704b
--- /dev/null
+++ b/project_task_report/__manifest__.py
@@ -0,0 +1,17 @@
+# Copyright 2024 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Project Task Report",
+ "summary": """Task wizard to sum up the progress between two dates""",
+ "version": "16.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "Akretion",
+ "website": "http://akretion.com",
+ "depends": ["project", "hr_timesheet"],
+ "data": [
+ "security/ir.model.access.csv",
+ "wizards/project_task_report.xml",
+ ],
+ "demo": ["data/project_task_report_demo.xml"],
+}
diff --git a/project_task_report/data/project_task_report_demo.xml b/project_task_report/data/project_task_report_demo.xml
new file mode 100644
index 00000000..2e001090
--- /dev/null
+++ b/project_task_report/data/project_task_report_demo.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+ Task 1
+
+
+
+ Task 2
+
+
+
+
+
+ Timesheet 1.1
+
+
+
+ 1.00
+
+
+ Timesheet 2.1
+
+
+
+
+
+ Timesheet 2.2
+
+
+
+ 10.00
+
+
+ Timesheet 2.3
+
+
+
+ 2.00
+
+
+
+
+ project.task
+
+ notification
+
+
+
+
+
+
+ Stage
+ New
+ In Progress
+ many2one
+ 1
+ 2
+
+
+
+
+ project.task
+
+ notification
+
+
+
+
+
+
+ Stage
+ In Progress
+ Done
+ many2one
+ 2
+ 3
+
+
+
+
+ project.task
+
+ notification
+
+
+
+
+
+
+ Stage
+ Done
+ Canceled
+ many2one
+ 3
+ 4
+
+
+
+
+
+
diff --git a/project_task_report/security/ir.model.access.csv b/project_task_report/security/ir.model.access.csv
new file mode 100644
index 00000000..f7e0a741
--- /dev/null
+++ b/project_task_report/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+project_task_report.access_project_task_report,access_project_task_report,project_task_report.model_project_task_report,base.group_user,1,1,1,1
+project_task_report.access_project_task_report_line,access_project_task_report_line,project_task_report.model_project_task_report_line,base.group_user,1,1,1,1
diff --git a/project_task_report/tests/__init__.py b/project_task_report/tests/__init__.py
new file mode 100644
index 00000000..7768b833
--- /dev/null
+++ b/project_task_report/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_project_task_report
diff --git a/project_task_report/tests/test_project_task_report.py b/project_task_report/tests/test_project_task_report.py
new file mode 100644
index 00000000..040976b0
--- /dev/null
+++ b/project_task_report/tests/test_project_task_report.py
@@ -0,0 +1,38 @@
+# Copyright 2024 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from datetime import datetime
+from odoo.tests.common import TransactionCase
+
+
+class TestProjectTaskReport(TransactionCase):
+
+ def setUp(self):
+ super().setUp()
+ self.task_report_id = self.env["project.task.report"].create(
+ {
+ "start_date": datetime(2024, 2, 2),
+ "end_date": datetime(2024, 6, 1),
+ "project_ids": self.env.ref("project.project_project_1").ids,
+ }
+ )
+ line_ids = self.task_report_id._create_line_ids()
+ self.line_ids = line_ids.sorted(lambda l: l.task_id.name)
+
+ self.stage_new = self.env.ref("project.project_stage_0")
+ self.stage_in_progress = self.env.ref("project.project_stage_1")
+ self.stage_canceled = self.env.ref("project.project_stage_3")
+
+ def test_task_report_no_stage_change(self):
+ task_line_id = self.line_ids[0]
+ self.assertEqual(task_line_id.task_id.name, "Task 1")
+ self.assertEqual(task_line_id.hours_spent, 1)
+ self.assertEqual(task_line_id.start_stage_id, self.stage_new)
+ self.assertEqual(task_line_id.end_stage_id, self.stage_new)
+
+ def test_task_report_with_stage_change(self):
+ task_line_id = self.line_ids[1]
+ self.assertEqual(task_line_id.task_id.name, "Task 2")
+ self.assertEqual(task_line_id.hours_spent, 12)
+ self.assertEqual(task_line_id.start_stage_id, self.stage_in_progress)
+ self.assertEqual(task_line_id.end_stage_id, self.stage_canceled)
diff --git a/project_task_report/wizards/__init__.py b/project_task_report/wizards/__init__.py
new file mode 100644
index 00000000..b8aa2ac1
--- /dev/null
+++ b/project_task_report/wizards/__init__.py
@@ -0,0 +1 @@
+from . import project_task_report
diff --git a/project_task_report/wizards/project_task_report.py b/project_task_report/wizards/project_task_report.py
new file mode 100644
index 00000000..a57401a5
--- /dev/null
+++ b/project_task_report/wizards/project_task_report.py
@@ -0,0 +1,84 @@
+# Copyright 2024 Akretion
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import _, api, fields, models
+
+
+class ProjectTaskReport(models.TransientModel):
+ _name = "project.task.report"
+ _description = "Task Report"
+
+ start_date = fields.Date()
+ end_date = fields.Date()
+ project_ids = fields.Many2many("project.project", string="Projects")
+
+ def _create_line_ids(self):
+ line_vals = []
+ task_ids = self.env["project.task"].search(
+ [("project_id", "in", self.project_ids.ids)]
+ )
+ field_stage_id = self.env["ir.model.fields"].search(
+ [("model", "=", "project.task"), ("name", "=", "stage_id")]
+ )
+
+ for task_id in task_ids:
+ line_val = {"task_id": task_id.id, "timesheet_ids": []}
+
+ # Catch the timesheets between the start and end dates
+ for timesheet_id in task_id.timesheet_ids:
+ if self.start_date < timesheet_id.date < self.end_date:
+ line_val["timesheet_ids"].append(timesheet_id.id)
+
+ # Catch the historic stage changes between the start and end dates
+ track_ids = task_id.message_ids.tracking_value_ids.filtered(
+ lambda t: t.field == field_stage_id
+ and self.start_date < t.mail_message_id.date.date() < self.end_date
+ ).sorted(lambda t: t.mail_message_id.date)
+
+ if track_ids:
+ line_val.update(
+ {
+ "start_stage_id": track_ids[0].old_value_integer,
+ "end_stage_id": track_ids[-1].new_value_integer,
+ }
+ )
+
+ if line_val["timesheet_ids"] or line_val.get("start_stage_id"):
+ # Fill stage fields in case there have been timesheets without stage change
+ if not line_val.get("start_stage_id"):
+ line_val.update(
+ {
+ "start_stage_id": task_id.stage_id.id,
+ "end_stage_id": task_id.stage_id.id,
+ }
+ )
+ line_vals.append(line_val)
+
+ return self.env["project.task.report.line"].create(line_vals)
+
+ def action_view_task_report(self):
+ self.ensure_one()
+
+ line_ids = self._create_line_ids()
+
+ action_xml_id = "project_task_report.project_task_report_line_act_window"
+ action = self.env["ir.actions.act_window"]._for_xml_id(action_xml_id)
+ action.update({"domain": [("id", "in", line_ids.ids)]})
+ return action
+
+
+class ProjectTaskReportLine(models.TransientModel):
+ _name = "project.task.report.line"
+ _description = "Task Report Line"
+
+ task_id = fields.Many2one("project.task", readonly=True)
+ start_stage_id = fields.Many2one("project.task.type", readonly=True)
+ end_stage_id = fields.Many2one("project.task.type", readonly=True)
+
+ timesheet_ids = fields.Many2many("account.analytic.line", readonly=True)
+ hours_spent = fields.Float(compute="_compute_hours_spent", store=True)
+
+ @api.depends("timesheet_ids.unit_amount")
+ def _compute_hours_spent(self):
+ for rec in self:
+ rec.hours_spent = sum(rec.timesheet_ids.mapped("unit_amount"))
diff --git a/project_task_report/wizards/project_task_report.xml b/project_task_report/wizards/project_task_report.xml
new file mode 100644
index 00000000..55d76c6e
--- /dev/null
+++ b/project_task_report/wizards/project_task_report.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+ project.task.report
+
+
+
+
+
+
+ Tasks Report
+ project.task.report
+ form
+ new
+
+
+
+ project.task.report.line.tree
+ project.task.report.line
+
+
+
+
+
+
+
+
+
+
+
+ project.task.report.line
+
+
+
+
+
+
+ Tasks Report
+ project.task.report.line
+ tree,form
+
+
+
+