diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py
index b6d936bc..15ab5615 100644
--- a/.docker_files/main/__manifest__.py
+++ b/.docker_files/main/__manifest__.py
@@ -12,6 +12,8 @@
"summary": "Install all addons required for testing.",
"depends": [
"project_default_task_stage",
+ "project_milestone_enhanced",
+ "project_milestone_spent_hours",
"project_parent_enhanced",
"project_stage_allow_timesheet",
"project_task_date_planned",
diff --git a/Dockerfile b/Dockerfile
index 417ef2bf..0b5e0bf1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,6 +14,8 @@ RUN gitoo install-all --conf_file /gitoo.yml --destination "${THIRD_PARTY_ADDONS
USER odoo
COPY project_default_task_stage /mnt/extra-addons/project_default_task_stage
+COPY project_milestone_enhanced /mnt/extra-addons/project_milestone_enhanced
+COPY project_milestone_spent_hours /mnt/extra-addons/project_milestone_spent_hours
COPY project_parent_enhanced mnt/extra-addons/project_parent_enhanced
COPY project_stage_allow_timesheet mnt/extra-addons/project_stage_allow_timesheet
COPY project_task_date_planned /mnt/extra-addons/project_task_date_planned
diff --git a/project_milestone_enhanced/README.rst b/project_milestone_enhanced/README.rst
new file mode 100644
index 00000000..e7fedae0
--- /dev/null
+++ b/project_milestone_enhanced/README.rst
@@ -0,0 +1,96 @@
+Project Milestone Enhanced
+==========================
+
+.. contents:: Table of Contents
+
+Context
+-------
+Milestones can be activated for a project. First in project > settings, then it is possible to use it or not for each project.
+
+Multiple tasks in the project can be linked to a given milestone.
+
+When a project is copied, its milestones and tasks are copied as well.
+
+The problem is that the copied tasks are linked to milestones
+in the old project instead of the new one.
+
+Technical
+---------
+
+Add possibility to not copy by default milestones of a project using a the key "milestones_no_copy" set to True in context.
+
+Description
+-----------
+In this module :
+
+Copied tasks are linked to the copied milestones when duplicating a project.
+
+Add the field "active" on milestones and a button is displayed on form view
+
+Add the field "active toggle" on milestones which store last value of field "active" using button active in milestone form view.
+If value of field "active toggle" is False, when a project is disabled or field Use milestones is disabled, when they are reactivated,
+milestones stay inactive
+
+When a milestone has his project modified, all his associated tasks not associated to this new project are dissociated.
+
+when a project change field "Milestones", milestones are set to same value if field "active toggle" is set to True.
+
+When a project is (de)activated, milestones too if field "active toggle" is set to True.
+
+In addition, milestone has progress field which is calculated from their tasks in closed state (stage is folded).
+
+.. image:: static/description/project_milestone_progress.png
+
+.. image:: static/description/milestone_progress.png
+
+Overview
+--------
+I open the form of a project with milestones and tasks.
+
+.. image:: static/description/project_form.png
+
+I duplicate the project.
+
+.. image:: static/description/project_form_copy.png
+
+I notice that the milestones where copied and
+that the new tasks are linked to these milestones.
+
+.. image:: static/description/new_project.png
+
+I open the form of a milestone, I see button "Active"
+
+.. image:: static/description/milestone_field_active.png
+
+I open the form of a milestone, with a project and tasks of this project associated
+
+.. image:: static/description/milestone_project_tasks.png
+
+I change the project of the milestone, previous displayed tasks are dissociated
+
+.. image:: static/description/milestone_change_project.png
+
+A project with field "Milestones" set to True, has its milestones active
+
+.. image:: static/description/project_use_milestones.png
+
+Milestone associated to the project is active
+
+.. image:: static/description/milestone_use_milestones.png
+
+I uncheck field "Milestones" on the project
+
+.. image:: static/description/project_not_use_milestones.png
+
+Milestone associated to the project is now inactive
+
+.. image:: static/description/milestone_not_use_milestones.png
+
+If I (de)activate a project, its associated milestones too
+
+.. image:: static/description/project_deactive_milestone.png
+
+
+Contributors
+------------
+* Numigi (tm) and all its contributors (https://bit.ly/numigiens)
diff --git a/project_milestone_enhanced/__init__.py b/project_milestone_enhanced/__init__.py
new file mode 100644
index 00000000..ac4a6864
--- /dev/null
+++ b/project_milestone_enhanced/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2023 - 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/project_milestone_enhanced/__manifest__.py b/project_milestone_enhanced/__manifest__.py
new file mode 100644
index 00000000..36ba567f
--- /dev/null
+++ b/project_milestone_enhanced/__manifest__.py
@@ -0,0 +1,18 @@
+# Copyright 2023 - today 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 Enhanced",
+ "version": "16.0.1.0.0",
+ "author": "Numigi",
+ "maintainer": "Numigi",
+ "website": "https://bit.ly/numigi-com",
+ "license": "LGPL-3",
+ "category": "Project",
+ "summary": "Improve usability of project milestones",
+ "depends": ["project"],
+ "data": [
+ "views/project_milestone.xml",
+ "views/project.xml",
+ ],
+ "installable": True,
+}
diff --git a/project_milestone_enhanced/i18n/fr.po b/project_milestone_enhanced/i18n/fr.po
new file mode 100644
index 00000000..1263aeee
--- /dev/null
+++ b/project_milestone_enhanced/i18n/fr.po
@@ -0,0 +1,283 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * project_milestone_enhanced
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-12-12 04:30+0000\n"
+"PO-Revision-Date: 2024-12-12 04:30+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_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_needaction
+msgid "Action Needed"
+msgstr "Nécessite une action"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__active
+msgid "Active"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__activity_ids
+msgid "Activities"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__activity_exception_decoration
+msgid "Activity Exception Decoration"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__activity_state
+msgid "Activity State"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__activity_type_icon
+msgid "Activity Type Icon"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.ui.menu,name:project_milestone_enhanced.milestone_configuration_menu
+msgid "All Milestones"
+msgstr "Tous les jalons"
+
+#. module: project_milestone_enhanced
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_enhancements_milestone_inherit_search_view
+msgid "Milestone Projects"
+msgstr "Projets du jalon"
+
+#. module: project_milestone_enhanced
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_active_view_form
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_milestone_active_view_form
+msgid "Archive"
+msgstr "Archiver"
+
+#. module: project_milestone_enhanced
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_active_view_form
+msgid ""
+"Archiving a project automatically archives its tasks and milestones. Do you "
+"want to proceed ?"
+msgstr ""
+"Archiver un projet archivera automatiquement ses tâches et jalons. Voulez-vous continuer ?"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_attachment_count
+msgid "Attachment Count"
+msgstr "Nombre de pièces jointes"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_follower_ids
+msgid "Followers"
+msgstr "Abonnés"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_partner_ids
+msgid "Followers (Partners)"
+msgstr "Abonnés (Partenaires)"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__activity_type_icon
+msgid "Font awesome icon e.g. fa-tasks"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_milestone_view_search
+msgid "Group By"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__has_message
+msgid "Has Message"
+msgstr "A un message"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__activity_exception_icon
+msgid "Icon"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__activity_exception_icon
+msgid "Icon to indicate an exception activity."
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__message_needaction
+msgid "If checked, new messages require your attention."
+msgstr "Si coché, de nouveaux messages demandent votre attention."
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__message_has_error
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__message_has_sms_error
+msgid "If checked, some messages have a delivery error."
+msgstr "Si coché, certains messages ont une erreur de livraison."
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_is_follower
+msgid "Is Follower"
+msgstr "Est un abonné"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_main_attachment_id
+msgid "Main Attachment"
+msgstr "Pièce jointe principale"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_has_error
+msgid "Message Delivery error"
+msgstr "Erreur d'envoi du message"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_ids
+msgid "Messages"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_active_view_form
+msgid "Milestones"
+msgstr "Jalons"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__my_activity_date_deadline
+msgid "My Activity Deadline"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__activity_date_deadline
+msgid "Next Activity Deadline"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__activity_summary
+msgid "Next Activity Summary"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__activity_type_id
+msgid "Next Activity Type"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_needaction_counter
+msgid "Number of Actions"
+msgstr "Nombre d'actions"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_has_error_counter
+msgid "Number of errors"
+msgstr "Nombre d'erreurs"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__message_needaction_counter
+msgid "Number of messages requiring action"
+msgstr "Nombre de messages nécessitant une action"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__message_has_error_counter
+msgid "Number of messages with delivery error"
+msgstr "Nombre de messages avec des erreurs d'envoi"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__progress
+msgid "Percentage of Completed Tasks vs Incomplete Tasks."
+msgstr "Pourcentage de tâches achevées par rapport aux tâches incomplètes."
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__progress
+msgid "Progress"
+msgstr "Progression"
+
+#. module: project_milestone_enhanced
+#: model:ir.model,name:project_milestone_enhanced.model_project_project
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_milestone_view_search
+msgid "Project"
+msgstr "Projet"
+
+#. module: project_milestone_enhanced
+#: model:ir.model,name:project_milestone_enhanced.model_project_milestone
+msgid "Project Milestone"
+msgstr "Jalon du projet"
+
+#. module: project_milestone_enhanced
+#: model:ir.actions.act_window,name:project_milestone_enhanced.project_milestone_action
+msgid "Project Milestones"
+msgstr "Jalons du projet"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__activity_user_id
+msgid "Responsible User"
+msgstr ""
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__message_has_sms_error
+msgid "SMS Delivery error"
+msgstr "Erreur d'envoi SMS"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__sequence
+msgid "Sequence"
+msgstr "Séquence"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_project__show_milestones
+msgid "Show milestones"
+msgstr "Afficher les jalons"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__activity_state
+msgid ""
+"Status based on activities\n"
+"Overdue: Due date is already passed\n"
+"Today: Activity date is today\n"
+"Planned: Future activities."
+msgstr "Statut basé sur les activités\n"
+"En retard : la date d'échéance est déjà dépassée\n"
+"Aujourd'hui : la date de l'activité est aujourd'hui\n"
+"Planifié : activités futures."
+
+#. module: project_milestone_enhanced
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_milestone_active_view_form
+msgid "Tasks"
+msgstr "Tâches"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__active_toggle
+msgid "Toggle active"
+msgstr "Toggle actif"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__activity_exception_decoration
+msgid "Type of the exception activity on record."
+msgstr "Type d'activité d'exception enregistrée."
+
+#. module: project_milestone_enhanced
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_active_view_form
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_milestone_active_view_form
+msgid "Unarchive"
+msgstr "Désarchiver"
+
+#. module: project_milestone_enhanced
+#: model_terms:ir.ui.view,arch_db:project_milestone_enhanced.project_active_view_form
+msgid ""
+"Unarchiving a project automatically unarchives its tasks and milestones. Do "
+"you want to proceed ?"
+msgstr ""
+"Désarchiver un projet désarchivera automatiquement ses tâches et jalons. Voulez-vous continuer ?"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,field_description:project_milestone_enhanced.field_project_milestone__website_message_ids
+msgid "Website Messages"
+msgstr "Messages du site web"
+
+#. module: project_milestone_enhanced
+#: model:ir.model.fields,help:project_milestone_enhanced.field_project_milestone__website_message_ids
+msgid "Website communication history"
+msgstr "Historique de communication du site web"
\ No newline at end of file
diff --git a/project_milestone_enhanced/models/__init__.py b/project_milestone_enhanced/models/__init__.py
new file mode 100644
index 00000000..eb62378e
--- /dev/null
+++ b/project_milestone_enhanced/models/__init__.py
@@ -0,0 +1,5 @@
+# Copyright 2023 - 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
+from . import project_milestone
diff --git a/project_milestone_enhanced/models/project.py b/project_milestone_enhanced/models/project.py
new file mode 100644
index 00000000..cd0dd1f3
--- /dev/null
+++ b/project_milestone_enhanced/models/project.py
@@ -0,0 +1,77 @@
+# Copyright 2023 - 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 api, models, fields
+
+
+class Project(models.Model):
+ _inherit = "project.project"
+
+ # `show_milestones` is not restricted by group
+ # like allow_milestones to use it in views
+ show_milestones = fields.Boolean(
+ related="allow_milestones", string="Show milestones", readonly=True
+ )
+
+ @api.returns("self", lambda value: value.id)
+ def copy(self, default=None):
+ default = dict(default or {})
+ default, milestones_no_copy = self._milestones_no_copy(default)
+ project = super(Project, self).copy(default)
+ if not milestones_no_copy:
+ project._link_tasks_to_milestones()
+ else:
+ project.milestone_ids = False
+ return project
+
+ def _milestones_no_copy(self, default):
+ context = dict(self.env.context or {})
+ milestones_no_copy = (
+ "milestones_no_copy" in context and context["milestones_no_copy"]
+ )
+ if milestones_no_copy:
+ default["milestone_ids"] = False
+ return default, milestones_no_copy
+
+ def _link_tasks_to_milestones(self):
+ for task in self.with_context(active_test=False).task_ids.filtered(
+ "milestone_id"
+ ):
+ task.milestone_id = self._find_equivalent_milestone(task.milestone_id)
+
+ def _find_equivalent_milestone(self, milestone):
+ return next(
+ (
+ m
+ for m in self.with_context(active_test=False).milestone_ids
+ if m.name == milestone.name
+ ),
+ None,
+ )
+
+ def write(self, vals):
+ res = super(Project, self).write(vals)
+ if "active" in vals or "allow_milestones" in vals:
+ self._test_active_milestones(vals)
+ return res
+
+ def _test_active_milestones(self, vals):
+ for project in self:
+ project._set_active_milestones(vals)
+
+ def _set_active_milestones(self, vals):
+ active = self._get_active_milestones(vals)
+ self.with_context(active_test=False).milestone_ids.filtered(
+ lambda milestone: milestone.active_toggle
+ ).sudo().write({"active": active})
+
+ def _get_active_milestones(self, vals):
+ if "allow_milestones" in vals:
+ allow_milestones = vals["allow_milestones"]
+ else:
+ allow_milestones = self.allow_milestones
+ if "active" in vals:
+ active = vals["active"]
+ else:
+ active = self.active
+ return allow_milestones and active
diff --git a/project_milestone_enhanced/models/project_milestone.py b/project_milestone_enhanced/models/project_milestone.py
new file mode 100644
index 00000000..8cb29675
--- /dev/null
+++ b/project_milestone_enhanced/models/project_milestone.py
@@ -0,0 +1,62 @@
+# Copyright 2023 - 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 api, models, fields
+
+
+class ProjectMilestone(models.Model):
+ _name = "project.milestone"
+
+ _inherit = ["project.milestone", "mail.thread", "mail.activity.mixin"]
+
+ active = fields.Boolean(string="Active", default=True)
+ active_toggle = fields.Boolean(string="Toggle active", default=True)
+ sequence = fields.Integer()
+ progress = fields.Float(
+ compute="_compute_milestone_progress",
+ store=True,
+ help="Percentage of Completed Tasks vs Incomplete Tasks.",
+ )
+
+ def toggle_active(self):
+ res = super(ProjectMilestone, self).toggle_active()
+ self.toggle_active_change()
+ return res
+
+ def toggle_active_change(self):
+ for milestone in self:
+ milestone.active_toggle = milestone.active
+
+ def write(self, vals):
+ res = super(ProjectMilestone, self).write(vals)
+
+ if "project_id" in vals:
+ self._remove_task_milestones(vals["project_id"])
+
+ if "active" in vals and vals["active"]:
+ self._milestone_not_active()
+ return res
+
+ def _remove_task_milestones(self, project_id):
+ self.with_context(active_test=False).mapped("task_ids").filtered(
+ lambda milestone: not project_id or milestone.project_id.id != project_id
+ ).write({"milestone_id": False})
+
+ def _milestone_not_active(self):
+ self.filtered(lambda milestone: not milestone.active_toggle).write(
+ {"active": False}
+ )
+
+ @api.depends("task_ids.stage_id")
+ def _compute_milestone_progress(self):
+ task_total = 0.0
+ task_closed = 0.0
+ for milestone in self:
+ for task in milestone.task_ids:
+ task_total += 1
+ if task.stage_id.fold:
+ task_closed += 1
+ if task_total > 0:
+ milestone.progress = (task_closed / task_total) * 100
+ else:
+ milestone.progress = 0.0
diff --git a/project_milestone_enhanced/static/description/icon.png b/project_milestone_enhanced/static/description/icon.png
new file mode 100644
index 00000000..92a86b10
Binary files /dev/null and b/project_milestone_enhanced/static/description/icon.png differ
diff --git a/project_milestone_enhanced/static/description/milestone_change_project.png b/project_milestone_enhanced/static/description/milestone_change_project.png
new file mode 100644
index 00000000..e6c4ea9f
Binary files /dev/null and b/project_milestone_enhanced/static/description/milestone_change_project.png differ
diff --git a/project_milestone_enhanced/static/description/milestone_field_active.png b/project_milestone_enhanced/static/description/milestone_field_active.png
new file mode 100644
index 00000000..f3d47680
Binary files /dev/null and b/project_milestone_enhanced/static/description/milestone_field_active.png differ
diff --git a/project_milestone_enhanced/static/description/milestone_not_use_milestones.png b/project_milestone_enhanced/static/description/milestone_not_use_milestones.png
new file mode 100644
index 00000000..09281d65
Binary files /dev/null and b/project_milestone_enhanced/static/description/milestone_not_use_milestones.png differ
diff --git a/project_milestone_enhanced/static/description/milestone_progress.png b/project_milestone_enhanced/static/description/milestone_progress.png
new file mode 100644
index 00000000..18c068b2
Binary files /dev/null and b/project_milestone_enhanced/static/description/milestone_progress.png differ
diff --git a/project_milestone_enhanced/static/description/milestone_project_tasks.png b/project_milestone_enhanced/static/description/milestone_project_tasks.png
new file mode 100644
index 00000000..2172da19
Binary files /dev/null and b/project_milestone_enhanced/static/description/milestone_project_tasks.png differ
diff --git a/project_milestone_enhanced/static/description/milestone_use_milestones.png b/project_milestone_enhanced/static/description/milestone_use_milestones.png
new file mode 100644
index 00000000..5597428d
Binary files /dev/null and b/project_milestone_enhanced/static/description/milestone_use_milestones.png differ
diff --git a/project_milestone_enhanced/static/description/new_project.png b/project_milestone_enhanced/static/description/new_project.png
new file mode 100644
index 00000000..7102ea47
Binary files /dev/null and b/project_milestone_enhanced/static/description/new_project.png differ
diff --git a/project_milestone_enhanced/static/description/project_deactive_milestone.png b/project_milestone_enhanced/static/description/project_deactive_milestone.png
new file mode 100644
index 00000000..28a81cc1
Binary files /dev/null and b/project_milestone_enhanced/static/description/project_deactive_milestone.png differ
diff --git a/project_milestone_enhanced/static/description/project_form.png b/project_milestone_enhanced/static/description/project_form.png
new file mode 100644
index 00000000..67967197
Binary files /dev/null and b/project_milestone_enhanced/static/description/project_form.png differ
diff --git a/project_milestone_enhanced/static/description/project_form_copy.png b/project_milestone_enhanced/static/description/project_form_copy.png
new file mode 100644
index 00000000..d485f87b
Binary files /dev/null and b/project_milestone_enhanced/static/description/project_form_copy.png differ
diff --git a/project_milestone_enhanced/static/description/project_milestone_progress.png b/project_milestone_enhanced/static/description/project_milestone_progress.png
new file mode 100644
index 00000000..f307f3fb
Binary files /dev/null and b/project_milestone_enhanced/static/description/project_milestone_progress.png differ
diff --git a/project_milestone_enhanced/static/description/project_not_use_milestones.png b/project_milestone_enhanced/static/description/project_not_use_milestones.png
new file mode 100644
index 00000000..439b4625
Binary files /dev/null and b/project_milestone_enhanced/static/description/project_not_use_milestones.png differ
diff --git a/project_milestone_enhanced/static/description/project_use_milestones.png b/project_milestone_enhanced/static/description/project_use_milestones.png
new file mode 100644
index 00000000..759b0124
Binary files /dev/null and b/project_milestone_enhanced/static/description/project_use_milestones.png differ
diff --git a/project_milestone_enhanced/tests/__init__.py b/project_milestone_enhanced/tests/__init__.py
new file mode 100644
index 00000000..291e3f64
--- /dev/null
+++ b/project_milestone_enhanced/tests/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2023 - 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
diff --git a/project_milestone_enhanced/tests/test_project.py b/project_milestone_enhanced/tests/test_project.py
new file mode 100644
index 00000000..a76a5af6
--- /dev/null
+++ b/project_milestone_enhanced/tests/test_project.py
@@ -0,0 +1,91 @@
+# Copyright 2023 - 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.tests import common
+
+
+class TestProject(common.TransactionCase):
+ def setUp(self):
+ super().setUp()
+ self.project = self.env["project.project"].create(
+ {"name": "My Project", "allow_milestones": True}
+ )
+
+ self.milestone = self.env["project.milestone"].create(
+ {"name": "My Milestone", "project_id": self.project.id}
+ )
+
+ self.milestone_2 = self.env["project.milestone"].create(
+ {"name": "My Milestone 2", "project_id": self.project.id}
+ )
+
+ self.task = self.env["project.task"].create(
+ {
+ "name": "My Task",
+ "project_id": self.project.id,
+ "milestone_id": self.milestone.id,
+ }
+ )
+
+ self.task_2 = self.env["project.task"].create(
+ {
+ "name": "My Task 1",
+ "project_id": self.project.id,
+ "milestone_id": self.milestone.id,
+ "active": False,
+ }
+ )
+
+ self.task_3 = self.env["project.task"].create(
+ {
+ "name": "My Task 2",
+ "project_id": self.project.id,
+ "milestone_id": self.milestone.id,
+ }
+ )
+
+ self.test_close_stage = self.env["project.task.type"].create(
+ {"name": "TestCloseStage", "fold": True}
+ )
+
+ def test_copy_project(self):
+ project = self.project.copy({})
+ tasks = project.with_context(active_test=False).task_ids
+ milestone = project.milestone_ids.filtered(
+ lambda milestone: "2" not in milestone.name
+ )
+ assert tasks[0].milestone_id == milestone and tasks[1].milestone_id == milestone
+
+ def test_copy_project_not_milestones(self):
+ project = self.project.with_context(milestones_no_copy=True).copy({})
+ assert not project.with_context(active_test=False).milestone_ids
+
+ def test_milestone_change_project(self):
+ new_project = self.project.copy({})
+ self.milestone.project_id = new_project.id
+ assert not self.milestone.task_ids
+
+ def test_project_change_allow_milestones(self):
+ self.milestone_2.toggle_active()
+ self.project.allow_milestones = False
+ assert not self.milestone.active
+ self.project.allow_milestones = True
+ assert self.milestone.active
+ assert not self.milestone_2.active
+
+ def test_project_change_active(self):
+ self.milestone_2.toggle_active()
+ self.project.toggle_active()
+ assert not self.milestone.active
+ self.project.toggle_active()
+ assert self.milestone.active
+ assert not self.milestone_2.active
+
+ def test_milestone_progress(self):
+ milestone1 = self.milestone
+
+ self.task.stage_id = self.test_close_stage.id
+ self.assertEqual(milestone1.progress, 50)
+
+ self.task_3.stage_id = self.test_close_stage.id
+ self.assertEqual(milestone1.progress, 100)
diff --git a/project_milestone_enhanced/views/project.xml b/project_milestone_enhanced/views/project.xml
new file mode 100644
index 00000000..7c6fbed8
--- /dev/null
+++ b/project_milestone_enhanced/views/project.xml
@@ -0,0 +1,68 @@
+
+
+
+
+ Project Active Form
+ project.project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ project.milestone.tree
+ project.project
+
+ tree
+ child_ids
+
+
+
+
+
+
+
+
+
+ project.milestone.filter
+ project.project
+
+
+
+
+
+
+
+
+
+
diff --git a/project_milestone_enhanced/views/project_milestone.xml b/project_milestone_enhanced/views/project_milestone.xml
new file mode 100644
index 00000000..25a2aa7e
--- /dev/null
+++ b/project_milestone_enhanced/views/project_milestone.xml
@@ -0,0 +1,101 @@
+
+
+
+
+ Project Milestone Active Form
+ project.milestone
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Project Milestone Sequence
+ project.milestone
+ 0
+ 1
+
+
+
+
+ Project Milestone List
+ project.milestone
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Project Milestone Search
+ project.milestone
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Project Milestones
+ project.milestone
+ {'group_by': 'project_id'}
+ tree,form
+
+
+
+
+
diff --git a/project_milestone_spent_hours/README.rst b/project_milestone_spent_hours/README.rst
new file mode 100644
index 00000000..9e7dbbac
--- /dev/null
+++ b/project_milestone_spent_hours/README.rst
@@ -0,0 +1,34 @@
+Project Milestone Spent Hours
+=================================
+
+.. contents:: Table of Contents
+
+Context
+-------
+Natively, Odoo allows to define milestones for a project.
+
+Multiple tasks in the project can be linked to a given milestone.
+
+Field total hours is displayed in form and list view of a project milestone and in tab of milestones of a project
+
+Description
+-----------
+Field Total Hours is the sum of timesheets of active tasks associated to the milestone
+
+Overview
+--------
+
+I create timesheets and set duration for 2 tasks associated to the same milestone
+
+.. image:: static/description/task1.png
+
+.. image:: static/description/task2.png
+
+I open the milestone, the field Total hours is set with sum of timesheets spent hours of associated tasks
+
+.. image:: static/description/milestone.png
+
+
+Contributors
+------------
+* Numigi (tm) and all its contributors (https://bit.ly/numigiens)
diff --git a/project_milestone_spent_hours/__init__.py b/project_milestone_spent_hours/__init__.py
new file mode 100644
index 00000000..ac4a6864
--- /dev/null
+++ b/project_milestone_spent_hours/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2023 - 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/project_milestone_spent_hours/__manifest__.py b/project_milestone_spent_hours/__manifest__.py
new file mode 100644
index 00000000..24babe16
--- /dev/null
+++ b/project_milestone_spent_hours/__manifest__.py
@@ -0,0 +1,20 @@
+# Copyright 2023 - today 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 Spent Hours",
+ "version": "16.0.1.0.0",
+ "author": "Numigi",
+ "maintainer": "Numigi",
+ "website": "https://bit.ly/numigi-com",
+ "license": "LGPL-3",
+ "category": "Project",
+ "summary": """Add field Total Hours in milestones which display the sum
+ of all hours of tasks associated to the milestone""",
+ "depends": ["hr_timesheet", "project_milestone_enhanced"],
+ "data": [
+ "views/project_milestone.xml",
+ "views/project.xml",
+ ],
+ "installable": True,
+}
diff --git a/project_milestone_spent_hours/i18n/fr.po b/project_milestone_spent_hours/i18n/fr.po
new file mode 100644
index 00000000..baf9f6bb
--- /dev/null
+++ b/project_milestone_spent_hours/i18n/fr.po
@@ -0,0 +1,36 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * project_milestone_spent_hours
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-12-07 05:29+0000\n"
+"PO-Revision-Date: 2024-12-07 05:29+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_spent_hours
+#: model:ir.model,name:project_milestone_spent_hours.model_account_analytic_line
+msgid "Analytic Line"
+msgstr "Ligne analytique"
+
+#. module: project_milestone_spent_hours
+#: model:ir.model.fields,field_description:project_milestone_spent_hours.field_account_analytic_line__milestone_id
+msgid "Milestone"
+msgstr "Jalon"
+
+#. module: project_milestone_spent_hours
+#: model:ir.model,name:project_milestone_spent_hours.model_project_milestone
+msgid "Project Milestone"
+msgstr "Jalon"
+
+#. module: project_milestone_spent_hours
+#: model:ir.model.fields,field_description:project_milestone_spent_hours.field_project_milestone__total_hours
+msgid "Total Hours"
+msgstr "Heures passées"
\ No newline at end of file
diff --git a/project_milestone_spent_hours/models/__init__.py b/project_milestone_spent_hours/models/__init__.py
new file mode 100644
index 00000000..cff2c1f5
--- /dev/null
+++ b/project_milestone_spent_hours/models/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2023 - 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_milestone, account_analytic_line
diff --git a/project_milestone_spent_hours/models/account_analytic_line.py b/project_milestone_spent_hours/models/account_analytic_line.py
new file mode 100644
index 00000000..266e56a8
--- /dev/null
+++ b/project_milestone_spent_hours/models/account_analytic_line.py
@@ -0,0 +1,17 @@
+# Copyright 2023 - 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 fields, models
+
+
+class AccountAnalytic_line(models.Model):
+ _inherit = "account.analytic.line"
+
+ milestone_id = fields.Many2one(
+ "project.milestone",
+ related="task_id.milestone_id",
+ string="Milestone",
+ index=True,
+ compute_sudo=True,
+ store=True,
+ )
diff --git a/project_milestone_spent_hours/models/project_milestone.py b/project_milestone_spent_hours/models/project_milestone.py
new file mode 100644
index 00000000..54a4ddb0
--- /dev/null
+++ b/project_milestone_spent_hours/models/project_milestone.py
@@ -0,0 +1,34 @@
+# Copyright 2023 - 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 fields, models, api
+
+
+class ProjectMilestone(models.Model):
+ _inherit = "project.milestone"
+
+ total_hours = fields.Float(
+ compute="_compute_total_hours",
+ string="Total Hours",
+ compute_sudo=True,
+ store=True,
+ )
+
+ @api.depends(
+ "task_ids",
+ "task_ids.active",
+ "task_ids.milestone_id",
+ "task_ids.timesheet_ids",
+ "task_ids.timesheet_ids.unit_amount",
+ "active",
+ )
+ def _compute_total_hours(self):
+ for record in self:
+ total_hours = 0.0
+ if record.active:
+ total_hours = sum(
+ record.task_ids.filtered(lambda milestone: milestone.active)
+ .mapped("timesheet_ids")
+ .mapped("unit_amount")
+ )
+ record.total_hours = total_hours
diff --git a/project_milestone_spent_hours/static/description/icon.png b/project_milestone_spent_hours/static/description/icon.png
new file mode 100644
index 00000000..92a86b10
Binary files /dev/null and b/project_milestone_spent_hours/static/description/icon.png differ
diff --git a/project_milestone_spent_hours/static/description/milestone.png b/project_milestone_spent_hours/static/description/milestone.png
new file mode 100644
index 00000000..44d878f5
Binary files /dev/null and b/project_milestone_spent_hours/static/description/milestone.png differ
diff --git a/project_milestone_spent_hours/static/description/task1.png b/project_milestone_spent_hours/static/description/task1.png
new file mode 100644
index 00000000..bb1410b5
Binary files /dev/null and b/project_milestone_spent_hours/static/description/task1.png differ
diff --git a/project_milestone_spent_hours/static/description/task2.png b/project_milestone_spent_hours/static/description/task2.png
new file mode 100644
index 00000000..9a82e25a
Binary files /dev/null and b/project_milestone_spent_hours/static/description/task2.png differ
diff --git a/project_milestone_spent_hours/tests/__init__.py b/project_milestone_spent_hours/tests/__init__.py
new file mode 100644
index 00000000..e43172e4
--- /dev/null
+++ b/project_milestone_spent_hours/tests/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2023 - 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_spent_hours
diff --git a/project_milestone_spent_hours/tests/test_spent_hours.py b/project_milestone_spent_hours/tests/test_spent_hours.py
new file mode 100644
index 00000000..1e9c8f5a
--- /dev/null
+++ b/project_milestone_spent_hours/tests/test_spent_hours.py
@@ -0,0 +1,82 @@
+# Copyright 2023 - 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.tests.common import TransactionCase
+
+
+class TestMilestoneTotalHours(TransactionCase):
+ def setUp(self):
+ super().setUp()
+
+ self.employee_manager = self.env["hr.employee"].create(
+ {
+ "name": "Employee Manager",
+ "hourly_cost": 808,
+ }
+ )
+
+ self.project = self.env["project.project"].create({"name": "My Project"})
+
+ self.milestone_1 = self.env["project.milestone"].create(
+ {"name": "My Milestone 1", "project_id": self.project.id}
+ )
+
+ self.milestone_2 = self.env["project.milestone"].create(
+ {"name": "My Milestone 2", "project_id": self.project.id}
+ )
+
+ self.task = self.env["project.task"].create(
+ {
+ "name": "My Task",
+ "project_id": self.project.id,
+ "milestone_id": self.milestone_1.id,
+ }
+ )
+
+ self.analytic_line_1 = self.env["account.analytic.line"].create(
+ {
+ "name": "My Timesheet 1",
+ "task_id": self.task.id,
+ "unit_amount": 10,
+ "project_id": self.project.id,
+ "employee_id": self.employee_manager.id,
+ }
+ )
+
+ self.analytic_line_2 = self.env["account.analytic.line"].create(
+ {
+ "name": "My Timesheet 2",
+ "task_id": self.task.id,
+ "unit_amount": 20,
+ "project_id": self.project.id,
+ "employee_id": self.employee_manager.id,
+ }
+ )
+
+ def test_propagate_milestone_on_analytic_line(self):
+ assert self.task.milestone_id & self.analytic_line_1.milestone_id
+
+ def test_update_milestone_total_hours_when_creating_analytic_line(self):
+ assert self.milestone_1.total_hours == 30
+
+ def test_update_milestone_total_hours_when_updating_analytic_line(self):
+ self.analytic_line_1.unit_amount = 20
+ assert self.milestone_1.total_hours == 40
+
+ def test_update_milestone_total_hours_when_removing_analytic_line(self):
+ self.analytic_line_1.unlink()
+ assert self.milestone_1.total_hours == 20
+
+ def test_update_milestone_total_hours_when_modifying_milestone_on_task(self):
+ self.task.milestone_id = self.milestone_2
+ assert self.milestone_1.total_hours == 0
+ assert self.milestone_2.total_hours == 30
+
+ def test_update_milestone_total_hours_when_task_inactive(self):
+ self.task.active = 0
+ assert self.milestone_1.total_hours == 0
+ assert self.milestone_2.total_hours == 0
+
+ def test_update_milestone_total_hours_when_remove_project(self):
+ self.milestone_1.project_id = False
+ assert self.milestone_1.total_hours == 0
diff --git a/project_milestone_spent_hours/views/project.xml b/project_milestone_spent_hours/views/project.xml
new file mode 100644
index 00000000..413f9a32
--- /dev/null
+++ b/project_milestone_spent_hours/views/project.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Project Milestone Spent Hours Form
+ project.project
+
+ form
+
+
+
+
+
+
+
diff --git a/project_milestone_spent_hours/views/project_milestone.xml b/project_milestone_spent_hours/views/project_milestone.xml
new file mode 100644
index 00000000..3c81b7ad
--- /dev/null
+++ b/project_milestone_spent_hours/views/project_milestone.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ Project Milestone Spent Hours List
+ project.milestone
+
+
+
+
+
+
+
+
+
+
+ Project Milestone Spent Hours Form
+ project.milestone
+
+
+
+
+
+
+
+
diff --git a/project_task_editable_list_view/static/description/editable_fields.png b/project_task_editable_list_view/static/description/editable_fields.png
index f3b4f98b..c45900c1 100644
Binary files a/project_task_editable_list_view/static/description/editable_fields.png and b/project_task_editable_list_view/static/description/editable_fields.png differ
diff --git a/project_task_editable_list_view/views/project_task_views.xml b/project_task_editable_list_view/views/project_task_views.xml
index 98888e95..87c5c797 100644
--- a/project_task_editable_list_view/views/project_task_views.xml
+++ b/project_task_editable_list_view/views/project_task_views.xml
@@ -9,7 +9,7 @@
-
+