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 @@ - +