From 18806e357f86c07564bbd0139d23f2048dbe4e07 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 7 Feb 2024 12:34:28 +0100 Subject: [PATCH 01/28] [ADD] project_workload --- project_workload/TODO | 4 + project_workload/__init__.py | 1 + project_workload/__manifest__.py | 24 +++ project_workload/models/__init__.py | 4 + project_workload/models/project_project.py | 11 ++ project_workload/models/project_task.py | 56 +++++++ .../models/project_task_workload.py | 77 ++++++++++ .../models/project_workload_unit.py | 20 +++ project_workload/readme/CONTRIBUTORS.rst | 3 + project_workload/readme/DESCRIPTION.rst | 2 + project_workload/readme/ROADMAP.rst | 2 + project_workload/security/ir.model.access.csv | 3 + project_workload/tests/__init__.py | 1 + project_workload/tests/common.py | 43 ++++++ project_workload/tests/test_workload.py | 37 +++++ project_workload/views/menu_view.xml | 11 ++ .../views/project_project_view.xml | 17 +++ project_workload/views/project_task_view.xml | 36 +++++ .../views/project_task_workload_view.xml | 25 ++++ project_workload_additions/__init__.py | 1 + project_workload_additions/__manifest__.py | 19 +++ project_workload_additions/models/__init__.py | 0 project_workload_capacity/__init__.py | 2 + project_workload_capacity/__manifest__.py | 26 ++++ project_workload_capacity/models/__init__.py | 3 + .../models/project_capacity_unit.py | 66 +++++++++ .../models/project_user_capacity.py | 51 +++++++ .../models/project_user_capacity_line.py | 17 +++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 2 + project_workload_capacity/readme/ROADMAP.rst | 2 + project_workload_capacity/reports/__init__.py | 1 + .../reports/project_load_capacity_report.py | 138 ++++++++++++++++++ .../security/ir.model.access.csv | 6 + project_workload_capacity/tests/__init__.py | 1 + .../tests/test_workload.py | 66 +++++++++ project_workload_capacity/views/menu_view.xml | 18 +++ .../views/project_capacity_unit_view.xml | 35 +++++ ...project_load_capacity_report_line_view.xml | 69 +++++++++ .../project_load_capacity_report_view.xml | 33 +++++ .../views/project_user_capacity_view.xml | 77 ++++++++++ project_workload_timesheet/__init__.py | 1 + project_workload_timesheet/__manifest__.py | 20 +++ project_workload_timesheet/models/__init__.py | 0 .../odoo/addons/project_workload | 1 + setup/project_workload/setup.py | 6 + .../odoo/addons/project_workload_additions | 1 + setup/project_workload_additions/setup.py | 6 + .../odoo/addons/project_workload_capacity | 1 + setup/project_workload_capacity/setup.py | 6 + .../odoo/addons/project_workload_timesheet | 1 + setup/project_workload_timesheet/setup.py | 6 + 52 files changed, 1060 insertions(+) create mode 100644 project_workload/TODO create mode 100644 project_workload/__init__.py create mode 100644 project_workload/__manifest__.py create mode 100644 project_workload/models/__init__.py create mode 100644 project_workload/models/project_project.py create mode 100644 project_workload/models/project_task.py create mode 100644 project_workload/models/project_task_workload.py create mode 100644 project_workload/models/project_workload_unit.py create mode 100644 project_workload/readme/CONTRIBUTORS.rst create mode 100644 project_workload/readme/DESCRIPTION.rst create mode 100644 project_workload/readme/ROADMAP.rst create mode 100644 project_workload/security/ir.model.access.csv create mode 100644 project_workload/tests/__init__.py create mode 100644 project_workload/tests/common.py create mode 100644 project_workload/tests/test_workload.py create mode 100644 project_workload/views/menu_view.xml create mode 100644 project_workload/views/project_project_view.xml create mode 100644 project_workload/views/project_task_view.xml create mode 100644 project_workload/views/project_task_workload_view.xml create mode 100644 project_workload_additions/__init__.py create mode 100644 project_workload_additions/__manifest__.py create mode 100644 project_workload_additions/models/__init__.py create mode 100644 project_workload_capacity/__init__.py create mode 100644 project_workload_capacity/__manifest__.py create mode 100644 project_workload_capacity/models/__init__.py create mode 100644 project_workload_capacity/models/project_capacity_unit.py create mode 100644 project_workload_capacity/models/project_user_capacity.py create mode 100644 project_workload_capacity/models/project_user_capacity_line.py create mode 100644 project_workload_capacity/readme/CONTRIBUTORS.rst create mode 100644 project_workload_capacity/readme/DESCRIPTION.rst create mode 100644 project_workload_capacity/readme/ROADMAP.rst create mode 100644 project_workload_capacity/reports/__init__.py create mode 100644 project_workload_capacity/reports/project_load_capacity_report.py create mode 100644 project_workload_capacity/security/ir.model.access.csv create mode 100644 project_workload_capacity/tests/__init__.py create mode 100644 project_workload_capacity/tests/test_workload.py create mode 100644 project_workload_capacity/views/menu_view.xml create mode 100644 project_workload_capacity/views/project_capacity_unit_view.xml create mode 100644 project_workload_capacity/views/project_load_capacity_report_line_view.xml create mode 100644 project_workload_capacity/views/project_load_capacity_report_view.xml create mode 100644 project_workload_capacity/views/project_user_capacity_view.xml create mode 100644 project_workload_timesheet/__init__.py create mode 100644 project_workload_timesheet/__manifest__.py create mode 100644 project_workload_timesheet/models/__init__.py create mode 120000 setup/project_workload/odoo/addons/project_workload create mode 100644 setup/project_workload/setup.py create mode 120000 setup/project_workload_additions/odoo/addons/project_workload_additions create mode 100644 setup/project_workload_additions/setup.py create mode 120000 setup/project_workload_capacity/odoo/addons/project_workload_capacity create mode 100644 setup/project_workload_capacity/setup.py create mode 120000 setup/project_workload_timesheet/odoo/addons/project_workload_timesheet create mode 100644 setup/project_workload_timesheet/setup.py diff --git a/project_workload/TODO b/project_workload/TODO new file mode 100644 index 000000000..47a6496d0 --- /dev/null +++ b/project_workload/TODO @@ -0,0 +1,4 @@ +- propager les dates des workload depuis la tache +- avoir une vision du lien "planifié" vs "fait" +- avoir une vision d'un bilan d'un sprint +- avoir une vision de sa charge de travail diff --git a/project_workload/__init__.py b/project_workload/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/project_workload/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_workload/__manifest__.py b/project_workload/__manifest__.py new file mode 100644 index 000000000..b677685b7 --- /dev/null +++ b/project_workload/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Project Workload", + "summary": "Ressource Workload Management", + "version": "14.0.1.0.0", + "development_status": "Alpha", + "category": "Uncategorized", + "website": "https://github.com/akretion/ak-odoo-incubator", + "author": " Akretion", + "license": "AGPL-3", + "depends": [ + "project_timeline", + ], + "data": [ + "security/ir.model.access.csv", + "views/project_task_workload_view.xml", + "views/project_task_view.xml", + "views/project_project_view.xml", + "views/menu_view.xml", + ], +} diff --git a/project_workload/models/__init__.py b/project_workload/models/__init__.py new file mode 100644 index 000000000..c84c8cf64 --- /dev/null +++ b/project_workload/models/__init__.py @@ -0,0 +1,4 @@ +from . import project_task_workload +from . import project_workload_unit +from . import project_project +from . import project_task diff --git a/project_workload/models/project_project.py b/project_workload/models/project_project.py new file mode 100644 index 000000000..94c1fb980 --- /dev/null +++ b/project_workload/models/project_project.py @@ -0,0 +1,11 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProjectProject(models.Model): + _inherit = "project.project" + + use_workload = fields.Boolean() diff --git a/project_workload/models/project_task.py b/project_workload/models/project_task.py new file mode 100644 index 000000000..942d36c6c --- /dev/null +++ b/project_workload/models/project_task.py @@ -0,0 +1,56 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + +WORKLOAD_FIELDS = [ + "date_start", + "date_end", + "user_id", + "planned_hours", + "use_workload", + "config_workload_manually", +] + + +class ProjectTask(models.Model): + _inherit = "project.task" + + workload_ids = fields.One2many("project.task.workload", "task_id", "Task") + use_workload = fields.Boolean(related="project_id.use_workload") + config_workload_manually = fields.Boolean() + + def _prepare_workload(self): + return { + "task_id": self.id, + "date_start": self.date_start, + "date_end": self.date_end, + "hours": self.planned_hours, + "user_id": self.user_id.id, + } + + def _sync_workload(self): + for record in self: + if record.use_workload and not record.config_workload_manually: + if not (record.date_start and record.date_end and record.planned_hours): + # Handle only planned task + continue + vals = self._prepare_workload() + if record.workload_ids: + record.workload_ids[1:].unlink() + record.workload_ids.write(vals) + else: + self.env["project.task.workload"].create([vals]) + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + records._sync_workload() + return records + + def write(self, vals): + res = super().write(vals) + if set(vals.keys()).intersection(WORKLOAD_FIELDS): + self._sync_workload() + return res diff --git a/project_workload/models/project_task_workload.py b/project_workload/models/project_task_workload.py new file mode 100644 index 000000000..2fd58b17d --- /dev/null +++ b/project_workload/models/project_task_workload.py @@ -0,0 +1,77 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from datetime import timedelta + +from odoo import fields, models + +WEEK_FORMAT = "%Y-%W" + + +def week_name(value): + if value: + return value.strftime(WEEK_FORMAT) + return None + + +class ProjectTaskWorkload(models.Model): + _name = "project.task.workload" + _description = "Project Task Workload" + + project_id = fields.Many2one( + "project.project", "Project", related="task_id.project_id", store=True + ) + task_id = fields.Many2one("project.task", "Task", required=True) + date_start = fields.Date(required=True) + date_end = fields.Date(required=True) + user_id = fields.Many2one("res.users", "User", required=True) + hours = fields.Float(required=True) + unit_ids = fields.One2many("project.workload.unit", "workload_id", "Units") + + def _get_hours_per_week(self): + weeks = set() + date = self.date_start + while True: + weeks.add(week_name(date)) + date += timedelta(days=7) + if date > self.date_end: + break + # For now a simple stupid split is done by week + # we do not care of the exact start / stop date + hours = self.hours / len(weeks) + return {week: hours for week in weeks} + + def _sync_workload_unit(self): + vals_list = [] + for record in self: + hours_per_week = self._get_hours_per_week() + unit_per_week = {wl.week: wl for wl in self.unit_ids} + for week, hours in hours_per_week.items(): + unit = unit_per_week.get(week) + if unit: + if unit.hours != hours: + unit.hours = hours + else: + vals_list.append( + { + "workload_id": record.id, + "hours": hours, + "week": week, + } + ) + for week, unit in unit_per_week.items(): + if week not in hours_per_week: + unit.unlink() + self.env["project.workload.unit"].create(vals_list) + + def create(self, vals_list): + records = super().create(vals_list) + records._sync_workload_unit() + return records + + def write(self, vals): + res = super().write(vals) + self._sync_workload_unit() + return res diff --git a/project_workload/models/project_workload_unit.py b/project_workload/models/project_workload_unit.py new file mode 100644 index 000000000..c1f498e01 --- /dev/null +++ b/project_workload/models/project_workload_unit.py @@ -0,0 +1,20 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProjectWorkloadUnit(models.Model): + _name = "project.workload.unit" + _description = "Project Workload Unit" + + week = fields.Char() + workload_id = fields.Many2one("project.task.workload", "Workload") + hours = fields.Float() + user_id = fields.Many2one( + "res.users", "User", related="workload_id.user_id", store=True + ) + project_id = fields.Many2one( + "project.project", "Project", related="workload_id.project_id", store=True + ) diff --git a/project_workload/readme/CONTRIBUTORS.rst b/project_workload/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..292f457ef --- /dev/null +++ b/project_workload/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier diff --git a/project_workload/readme/DESCRIPTION.rst b/project_workload/readme/DESCRIPTION.rst new file mode 100644 index 000000000..ffb228768 --- /dev/null +++ b/project_workload/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allow to manage load and capacity by project and cross project +Load is managed by week diff --git a/project_workload/readme/ROADMAP.rst b/project_workload/readme/ROADMAP.rst new file mode 100644 index 000000000..dd00217e5 --- /dev/null +++ b/project_workload/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +TODO +- in case of manual workload assignation add a check on date diff --git a/project_workload/security/ir.model.access.csv b/project_workload/security/ir.model.access.csv new file mode 100644 index 000000000..c4407e5ba --- /dev/null +++ b/project_workload/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 +access_edit_project_task_workload,edit project task workload,model_project_task_workload,project.group_project_user,1,1,1,1 +access_edit_project_workload_unit,edit project workload unit,model_project_workload_unit,project.group_project_user,1,1,1,1 diff --git a/project_workload/tests/__init__.py b/project_workload/tests/__init__.py new file mode 100644 index 000000000..5c9b38db3 --- /dev/null +++ b/project_workload/tests/__init__.py @@ -0,0 +1 @@ +from . import test_workload diff --git a/project_workload/tests/common.py b/project_workload/tests/common.py new file mode 100644 index 000000000..b0bf317ca --- /dev/null +++ b/project_workload/tests/common.py @@ -0,0 +1,43 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from freezegun import freeze_time + +from odoo.tests import SavepointCase + + +@freeze_time("2023-07-24") +class TestWorkloadCommon(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.project = cls.env["project.project"].create( + { + "name": "Project 1", + "use_workload": True, + } + ) + cls.user_1 = cls.env.ref("base.demo_user0") + cls.user_2 = cls.env.ref("base.user_demo") + cls.project_filter = cls.env["ir.filters"].create( + { + "name": "Project Filter 1", + "domain": [("id", "=", cls.project.id)], + "model_id": "project.project", + } + ) + now = datetime.now() + cls.task = cls.env["project.task"].create( + { + "name": "Task 1", + "project_id": cls.project.id, + "user_id": cls.user_1.id, + "date_start": now, + "date_end": now + timedelta(days=20), + "planned_hours": 21, + } + ) diff --git a/project_workload/tests/test_workload.py b/project_workload/tests/test_workload.py new file mode 100644 index 000000000..6fa5da6b3 --- /dev/null +++ b/project_workload/tests/test_workload.py @@ -0,0 +1,37 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import timedelta + +from .common import TestWorkloadCommon + + +class TestWorkload(TestWorkloadCommon): + def test_task_assign_with_hours(self): + workload = self.task.workload_ids + self.assertEqual(len(workload), 1) + self.assertEqual(workload.date_start, self.task.date_start.date()) + self.assertEqual(workload.date_end, self.task.date_end.date()) + self.assertEqual(workload.hours, 21) + load_unit = workload.unit_ids + self.assertEqual(len(load_unit), 3) + self.assertEqual(load_unit.user_id, self.user_1) + self.assertEqual(set(load_unit.mapped("hours")), {7}) + + def test_change_user(self): + self.task.user_id = self.user_2 + self.assertEqual(self.task.workload_ids.user_id, self.user_2) + self.assertEqual(self.task.workload_ids.unit_ids.user_id, self.user_2) + + def test_change_date(self): + self.task.date_end = self.task.date_start + timedelta(days=13) + workload = self.task.workload_ids + self.assertEqual(len(workload), 1) + self.assertEqual(workload.date_start, self.task.date_start.date()) + self.assertEqual(workload.date_end, self.task.date_end.date()) + self.assertEqual(workload.hours, 21) + load_unit = workload.unit_ids + self.assertEqual(len(load_unit), 2) + self.assertEqual(load_unit.user_id, self.user_1) + self.assertEqual(set(load_unit.mapped("hours")), {10.5}) diff --git a/project_workload/views/menu_view.xml b/project_workload/views/menu_view.xml new file mode 100644 index 000000000..e96244b04 --- /dev/null +++ b/project_workload/views/menu_view.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/project_workload/views/project_project_view.xml b/project_workload/views/project_project_view.xml new file mode 100644 index 000000000..54d8f286b --- /dev/null +++ b/project_workload/views/project_project_view.xml @@ -0,0 +1,17 @@ + + + + + project.project + + +
+
+ +
+
+
+
+ +
diff --git a/project_workload/views/project_task_view.xml b/project_workload/views/project_task_view.xml new file mode 100644 index 000000000..3a8fe580d --- /dev/null +++ b/project_workload/views/project_task_view.xml @@ -0,0 +1,36 @@ + + + + + project.task + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project_workload/views/project_task_workload_view.xml b/project_workload/views/project_task_workload_view.xml new file mode 100644 index 000000000..702fa962a --- /dev/null +++ b/project_workload/views/project_task_workload_view.xml @@ -0,0 +1,25 @@ + + + + + project.task.workload + +
+ + + + + + + + + + + + + +
+
+
+ +
diff --git a/project_workload_additions/__init__.py b/project_workload_additions/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/project_workload_additions/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_workload_additions/__manifest__.py b/project_workload_additions/__manifest__.py new file mode 100644 index 000000000..b3d4ee94b --- /dev/null +++ b/project_workload_additions/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Project Workload Additions", + "summary": "Automatically add extra load to tasks.", + "version": "14.0.1.0.0", + "development_status": "Alpha", + "category": "Uncategorized", + "website": "https://github.com/akretion/ak-odoo-incubator", + "author": " Akretion", + "license": "AGPL-3", + "depends": [ + "project_workload", + ], + "data": [], +} diff --git a/project_workload_additions/models/__init__.py b/project_workload_additions/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/project_workload_capacity/__init__.py b/project_workload_capacity/__init__.py new file mode 100644 index 000000000..55ec7fc9a --- /dev/null +++ b/project_workload_capacity/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import reports diff --git a/project_workload_capacity/__manifest__.py b/project_workload_capacity/__manifest__.py new file mode 100644 index 000000000..2cf75ca32 --- /dev/null +++ b/project_workload_capacity/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Project Workload Capacity", + "summary": "Ressource Workload Capacity Management", + "version": "14.0.1.0.0", + "development_status": "Alpha", + "category": "Uncategorized", + "website": "https://github.com/akretion/ak-odoo-incubator", + "author": " Akretion", + "license": "AGPL-3", + "depends": [ + "project_workload", + ], + "data": [ + "security/ir.model.access.csv", + "views/project_capacity_unit_view.xml", + "views/project_user_capacity_view.xml", + "views/project_load_capacity_report_line_view.xml", + "views/project_load_capacity_report_view.xml", + "views/menu_view.xml", + ], +} diff --git a/project_workload_capacity/models/__init__.py b/project_workload_capacity/models/__init__.py new file mode 100644 index 000000000..a63b7b2c3 --- /dev/null +++ b/project_workload_capacity/models/__init__.py @@ -0,0 +1,3 @@ +from . import project_capacity_unit +from . import project_user_capacity +from . import project_user_capacity_line diff --git a/project_workload_capacity/models/project_capacity_unit.py b/project_workload_capacity/models/project_capacity_unit.py new file mode 100644 index 000000000..7f7eddfd8 --- /dev/null +++ b/project_workload_capacity/models/project_capacity_unit.py @@ -0,0 +1,66 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from datetime import datetime + +from odoo import api, fields, models + +from odoo.addons.project_workload.models.project_task_workload import ( + WEEK_FORMAT, + week_name, +) + + +class ProjectCapacityUnit(models.Model): + _name = "project.capacity.unit" + _description = "Project Capacity Unit" + _rec_name = "week" + _order = "week asc" + + week = fields.Char() + capacity_id = fields.Many2one("project.user.capacity", "Capacity") + hours = fields.Float(compute="_compute_capacity", store=True) + user_id = fields.Many2one( + "res.users", "User", related="capacity_id.user_id", store=True + ) + _sql_constraints = [ + ( + "week_capacity_uniq", + "unique(week, capacity_id)", + "The week must be uniq per capacity", + ), + ] + + @api.depends( + "capacity_id.line_ids.hours", + "capacity_id.line_ids.date_start", + "capacity_id.line_ids.date_end", + "capacity_id.line_ids.modulo", + ) + def _compute_capacity(self): + for record in self: + hours = 0 + for line in record.capacity_id.line_ids: + start = week_name(line.date_start) + stop = week_name(line.date_end) + if record.week >= start and (not stop or record.week <= stop): + if line.modulo > 1: + nbr_week = round( + ( + datetime.strptime( + record.week + "-1", WEEK_FORMAT + "-%w" + ).date() + - datetime.strptime( + start + "-1", WEEK_FORMAT + "-%w" + ).date() + ).days + / 7 + ) + if nbr_week % line.modulo: + # week is not a multi skip it + continue + hours = line.hours + break + record.hours = hours diff --git a/project_workload_capacity/models/project_user_capacity.py b/project_workload_capacity/models/project_user_capacity.py new file mode 100644 index 000000000..594a8aa1f --- /dev/null +++ b/project_workload_capacity/models/project_user_capacity.py @@ -0,0 +1,51 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from collections import defaultdict +from datetime import datetime, timedelta + +from odoo import api, fields, models + +from odoo.addons.project_workload.models.project_task_workload import week_name + + +class ProjectUserCapacity(models.Model): + _name = "project.user.capacity" + _description = "Project User Capacity" + + name = fields.Char(required=True) + user_id = fields.Many2one("res.users", "User", required=True) + filter_id = fields.Many2one("ir.filters", "Domain") + line_ids = fields.One2many("project.user.capacity.line", "capacity_id", "Line") + unit_ids = fields.One2many("project.capacity.unit", "capacity_id", "Capacity Unit") + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + records._generate_capacity_unit() + return records + + def _cron_generate_week_report(self, nbr_week=52): + self.search([])._generate_capacity_unit(nbr_week=nbr_week) + + def _generate_capacity_unit(self, nbr_week=52): + now = datetime.now() + items = self.env["project.capacity.unit"].search( + [("week", ">=", week_name(now))] + ) + weeks_per_capacity = defaultdict(set) + for item in items: + weeks_per_capacity[item.capacity_id.id].add(item.week) + weeks = {week_name(now + timedelta(days=7 * x)) for x in range(nbr_week)} + vals_list = [] + for capacity in self: + missing_weeks = weeks - weeks_per_capacity[capacity.id] + vals_list += [ + { + "week": week, + "capacity_id": capacity.id, + } + for week in missing_weeks + ] + return self.env["project.capacity.unit"].create(vals_list) diff --git a/project_workload_capacity/models/project_user_capacity_line.py b/project_workload_capacity/models/project_user_capacity_line.py new file mode 100644 index 000000000..03a06bf3f --- /dev/null +++ b/project_workload_capacity/models/project_user_capacity_line.py @@ -0,0 +1,17 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ProjectUserCapacityLine(models.Model): + _name = "project.user.capacity.line" + _description = "Project User Capacity Line" + + capacity_id = fields.Many2one("project.user.capacity") + date_start = fields.Date(required=True, default=fields.Date.today()) + date_end = fields.Date() + hours = fields.Float() + modulo = fields.Integer(default=1, string="Repeat", help="Repeat every X week") diff --git a/project_workload_capacity/readme/CONTRIBUTORS.rst b/project_workload_capacity/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..bfd14efba --- /dev/null +++ b/project_workload_capacity/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* BEAU Sébastien diff --git a/project_workload_capacity/readme/DESCRIPTION.rst b/project_workload_capacity/readme/DESCRIPTION.rst new file mode 100644 index 000000000..ffb228768 --- /dev/null +++ b/project_workload_capacity/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allow to manage load and capacity by project and cross project +Load is managed by week diff --git a/project_workload_capacity/readme/ROADMAP.rst b/project_workload_capacity/readme/ROADMAP.rst new file mode 100644 index 000000000..dd00217e5 --- /dev/null +++ b/project_workload_capacity/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +TODO +- in case of manual workload assignation add a check on date diff --git a/project_workload_capacity/reports/__init__.py b/project_workload_capacity/reports/__init__.py new file mode 100644 index 000000000..0474dae23 --- /dev/null +++ b/project_workload_capacity/reports/__init__.py @@ -0,0 +1 @@ +from . import project_load_capacity_report diff --git a/project_workload_capacity/reports/project_load_capacity_report.py b/project_workload_capacity/reports/project_load_capacity_report.py new file mode 100644 index 000000000..45e79450c --- /dev/null +++ b/project_workload_capacity/reports/project_load_capacity_report.py @@ -0,0 +1,138 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import ast + +from odoo import fields, models +from odoo.osv import expression + + +class ProjectLoadCapacityReport(models.TransientModel): + _name = "project.load.capacity.report" + _description = "Project Load Capacity Report" + + project_ids = fields.Many2many(comodel_name="project.project", string="Project") + line_ids = fields.One2many( + "project.load.capacity.report.line", "report_id", "Lines" + ) + + def _generate_lines(self): + capacities = self.env["project.user.capacity"].search([]) + match_capacities = self.env["project.user.capacity"] + if self.project_ids: + projects = self.project_ids + for project in self.project_ids: + for capacity in capacities: + if project.filtered_domain(capacity.filter_id.domain): + match_capacities |= capacities + else: + match_capacities = capacities + projects = self.env["project.project"].search([]) + + # TODO fix domain (it's a string, it's expect a list) + domain = [] + for capacity in match_capacities: + if capacity.filter_id.domain: + capacity_domain = ast.literal_eval(capacity.filter_id.domain) + else: + capacity_domain = [] + domain = expression.OR([domain, capacity_domain]) + match_projects = self.env["project.project"].search(domain) + + # TODO add timesheet to know what have been done this week + self.env.cr.execute( + """ + INSERT INTO project_load_capacity_report_line ( + report_id, + total_capacity_hours, + total_planned_hours, + project_capacity_hours, + project_planned_hours, + project_available_hours, + week, + user_id + ) + SELECT + %s AS report_id, + total_capacity.hours as total_capacity_hours, + total_planned.hours as total_planned_hours, + project_capacity.hours as project_capacity_hours, + project_planned.hours as project_planned_hours, + project_capacity_hours - + match_project_planned_hours as project_available_hours, + total_capacity.week as week, + total_capacity.user_id + FROM ( + SELECT week, user_id, sum(hours) as hours + FROM project_capacity_unit + GROUP BY week, user_id + ) AS total_capacity + FULL JOIN ( + SELECT week, user_id, sum(hours) as hours + FROM project_workload_unit + GROUP BY week, user_id + ) AS total_planned + ON total_capacity.week = total_planned.week + AND total_capacity.user_id = total_planned.user_id + FULL JOIN ( + SELECT week, user_id, sum(hours) as hours + FROM project_capacity_unit + WHERE capacity_id in %s + GROUP BY week, user_id + ) AS project_capacity + ON total_capacity.week = project_capacity.week + AND total_capacity.user_id = project_capacity.user_id + FULL JOIN ( + SELECT week, user_id, sum(hours) as hours + FROM project_workload_unit + WHERE project_id in %s + GROUP BY week, user_id + ) AS match_project_planned + ON total_capacity.week = match_project_planned.week + AND total_capacity.user_id = match_project_planned.user_id + FULL JOIN ( + SELECT week, user_id, sum(hours) as hours + FROM project_workload_unit + WHERE project_id in %s + GROUP BY week, user_id + ) AS project_planned + ON total_capacity.week = project_planned.week + AND total_capacity.user_id = project_planned.user_id + """, + ( + self.id, + tuple(match_capacities.ids), + tuple(match_projects.ids), + tuple(projects.ids), + ), + ) + self.env.clear() + + def refresh_lines(self): + self.line_ids.unlink() + # self._generate_lines() + return True + + def open_report(self): + self.refresh_lines() + action = ( + self.env.ref("project_workload.project_load_capacity_report_line_action") + .sudo() + .read()[0] + ) + action["domain"] = [("report_id", "=", self.id)] + return action + + +class ProjectLoadReportLine(models.TransientModel): + _name = "project.load.capacity.report.line" + _description = "Project Load Report Line" + + week = fields.Char() + user_id = fields.Many2one("res.users", "User") + total_planned_hours = fields.Float() + total_capacity_hours = fields.Float() + project_planned_hours = fields.Float() + project_capacity_hours = fields.Float() + project_available_hours = fields.Float() + report_id = fields.Many2one("project.load.capacity.report", "Report") diff --git a/project_workload_capacity/security/ir.model.access.csv b/project_workload_capacity/security/ir.model.access.csv new file mode 100644 index 000000000..6d5590bbd --- /dev/null +++ b/project_workload_capacity/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_edit_project_user_capacity,edit project user capacity,model_project_user_capacity,project.group_project_user,1,1,1,1 +access_edit_project_user_capacity_line,edit project user capacity line,model_project_user_capacity_line,project.group_project_user,1,1,1,1 +access_edit_project_capacity_unit,edit project capacity unit,model_project_capacity_unit,project.group_project_user,1,1,1,1 +access_edit_project_load_capacity_report,edit project load report,model_project_load_capacity_report,project.group_project_user,1,1,1,1 +access_edit_project_load_capacity_report_line,edit project load report line,model_project_load_capacity_report_line,project.group_project_user,1,1,1,1 diff --git a/project_workload_capacity/tests/__init__.py b/project_workload_capacity/tests/__init__.py new file mode 100644 index 000000000..5c9b38db3 --- /dev/null +++ b/project_workload_capacity/tests/__init__.py @@ -0,0 +1 @@ +from . import test_workload diff --git a/project_workload_capacity/tests/test_workload.py b/project_workload_capacity/tests/test_workload.py new file mode 100644 index 000000000..e6dadfc6c --- /dev/null +++ b/project_workload_capacity/tests/test_workload.py @@ -0,0 +1,66 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo.addons.project_workload.tests.common import TestWorkloadCommon + + +class TestWorkloadCapacity(TestWorkloadCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.capacity = cls.env["project.user.capacity"].create( + { + "name": "My Capacity for project 1", + "user_id": cls.user_1.id, + "filter_id": cls.project_filter.id, + "line_ids": [ + (0, 0, {"hours": 21}), + ], + } + ) + + def test_create_capacity(self): + self.assertEqual(len(self.capacity.unit_ids), 52) + self.assertEqual(set(self.capacity.unit_ids.mapped("hours")), {21}) + + def test_cron_create_capacity(self): + self.env["project.user.capacity"]._cron_generate_week_report(60) + self.assertEqual(len(self.capacity.unit_ids), 60) + self.assertEqual(set(self.capacity.unit_ids.mapped("hours")), {21}) + + def test_change_capacity_hours(self): + self.capacity.line_ids.hours = 28 + self.assertEqual(set(self.capacity.unit_ids.mapped("hours")), {28}) + + def test_capacity_modulo(self): + self.capacity.line_ids.modulo = 3 + for idx, unit in enumerate(self.capacity.unit_ids.sorted()): + if idx % 3: + self.assertEqual(unit.hours, 0) + else: + self.assertEqual(unit.hours, 21) + + def test_capacity_date_start_end(self): + self.capacity.line_ids.date_end = datetime.now() + timedelta(days=70) + for idx, unit in enumerate(self.capacity.unit_ids.sorted()): + if idx <= 10: + self.assertEqual(unit.hours, 21) + else: + self.assertEqual(unit.hours, 0) + + def test_generate_report(self): + report = self.env["project.load.capacity.report"].create({}) + report._generate_lines() + for line in report.line_ids: + self.assertEqual(line.user_id, self.user_1) + if line.week in ["2023-30", "2023-31", "2023-32"]: + self.assertEqual(line.total_planned_hours, 7) + self.assertEqual(line.project_planned_hours, 7) + else: + self.assertEqual(line.total_planned_hours, 0) + self.assertEqual(line.project_planned_hours, 0) + self.assertEqual(line.total_capacity_hours, 21) + self.assertEqual(line.project_capacity_hours, 21) diff --git a/project_workload_capacity/views/menu_view.xml b/project_workload_capacity/views/menu_view.xml new file mode 100644 index 000000000..5c64e6589 --- /dev/null +++ b/project_workload_capacity/views/menu_view.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/project_workload_capacity/views/project_capacity_unit_view.xml b/project_workload_capacity/views/project_capacity_unit_view.xml new file mode 100644 index 000000000..965664668 --- /dev/null +++ b/project_workload_capacity/views/project_capacity_unit_view.xml @@ -0,0 +1,35 @@ + + + + + project.capacity.unit + + + + + + + + + + + project.capacity.unit + + + + + + + + + + Capacity Unit + ir.actions.act_window + project.capacity.unit + tree + + [] + {} + + + diff --git a/project_workload_capacity/views/project_load_capacity_report_line_view.xml b/project_workload_capacity/views/project_load_capacity_report_line_view.xml new file mode 100644 index 000000000..3c4f29704 --- /dev/null +++ b/project_workload_capacity/views/project_load_capacity_report_line_view.xml @@ -0,0 +1,69 @@ + + + + + project.load.capacity.report.line + + + + + + + + + + + + + + project.load.capacity.report.line + + + + + + + + + + + + + + project.load.capacity.report.line + + + + + + + + + + + + + + Load Report Line + ir.actions.act_window + project.load.capacity.report.line + tree,form,pivot + + [] + {} + + + diff --git a/project_workload_capacity/views/project_load_capacity_report_view.xml b/project_workload_capacity/views/project_load_capacity_report_view.xml new file mode 100644 index 000000000..3104187b3 --- /dev/null +++ b/project_workload_capacity/views/project_load_capacity_report_view.xml @@ -0,0 +1,33 @@ + + + + + project.load.capacity.report + +
+ Filter the project to analyse (if empty all project will be used) + +
+
+ +
+
+ + + Load Report + ir.actions.act_window + project.load.capacity.report + form + new + [] + {} + + +
diff --git a/project_workload_capacity/views/project_user_capacity_view.xml b/project_workload_capacity/views/project_user_capacity_view.xml new file mode 100644 index 000000000..87a79af77 --- /dev/null +++ b/project_workload_capacity/views/project_user_capacity_view.xml @@ -0,0 +1,77 @@ + + + + + project.user.capacity + + + + + + + + + + + project.user.capacity + +
+
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + +
+
+
+ + + project.user.capacity + + + + + + + + + + Capacity + ir.actions.act_window + project.user.capacity + tree,form + + [] + {} + + +
diff --git a/project_workload_timesheet/__init__.py b/project_workload_timesheet/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/project_workload_timesheet/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_workload_timesheet/__manifest__.py b/project_workload_timesheet/__manifest__.py new file mode 100644 index 000000000..abef4b8b3 --- /dev/null +++ b/project_workload_timesheet/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Project Workload Timesheet", + "summary": "Add Workload To Timesheets", + "version": "14.0.1.0.0", + "development_status": "Alpha", + "category": "Uncategorized", + "website": "https://github.com/akretion/ak-odoo-incubator", + "author": " Akretion", + "license": "AGPL-3", + "depends": [ + "project_workload", + "hr_timesheet", + ], + "data": [], +} diff --git a/project_workload_timesheet/models/__init__.py b/project_workload_timesheet/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/setup/project_workload/odoo/addons/project_workload b/setup/project_workload/odoo/addons/project_workload new file mode 120000 index 000000000..4b3d3182f --- /dev/null +++ b/setup/project_workload/odoo/addons/project_workload @@ -0,0 +1 @@ +../../../../project_workload \ No newline at end of file diff --git a/setup/project_workload/setup.py b/setup/project_workload/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/project_workload/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/project_workload_additions/odoo/addons/project_workload_additions b/setup/project_workload_additions/odoo/addons/project_workload_additions new file mode 120000 index 000000000..793ba60ac --- /dev/null +++ b/setup/project_workload_additions/odoo/addons/project_workload_additions @@ -0,0 +1 @@ +../../../../project_workload_additions \ No newline at end of file diff --git a/setup/project_workload_additions/setup.py b/setup/project_workload_additions/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/project_workload_additions/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/project_workload_capacity/odoo/addons/project_workload_capacity b/setup/project_workload_capacity/odoo/addons/project_workload_capacity new file mode 120000 index 000000000..b1a940739 --- /dev/null +++ b/setup/project_workload_capacity/odoo/addons/project_workload_capacity @@ -0,0 +1 @@ +../../../../project_workload_capacity \ No newline at end of file diff --git a/setup/project_workload_capacity/setup.py b/setup/project_workload_capacity/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/project_workload_capacity/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/project_workload_timesheet/odoo/addons/project_workload_timesheet b/setup/project_workload_timesheet/odoo/addons/project_workload_timesheet new file mode 120000 index 000000000..396e282f2 --- /dev/null +++ b/setup/project_workload_timesheet/odoo/addons/project_workload_timesheet @@ -0,0 +1 @@ +../../../../project_workload_timesheet \ No newline at end of file diff --git a/setup/project_workload_timesheet/setup.py b/setup/project_workload_timesheet/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/project_workload_timesheet/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 181c5e2bb6b904bf61cddb6d34fd05e1fae79f37 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 7 Feb 2024 15:50:05 +0100 Subject: [PATCH 02/28] =?UTF-8?q?[IMP]=20project=5Fworkload:=C2=A0Use=20co?= =?UTF-8?q?mpute=20to=20sync=20tasks,=20workloads=20and=20units.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visibility based on group. Show units in debug mode. --- project_workload/__manifest__.py | 4 +- project_workload/models/project_project.py | 3 +- project_workload/models/project_task.py | 87 ++++++++------ .../models/project_task_workload.py | 108 ++++++++++++------ .../models/project_workload_unit.py | 3 +- .../security/project_workload_security.xml | 23 ++++ project_workload/tests/test_workload.py | 3 +- project_workload/views/menu_view.xml | 1 + .../views/project_project_view.xml | 10 +- project_workload/views/project_task_view.xml | 9 ++ .../views/project_task_workload_view.xml | 36 +++--- 11 files changed, 189 insertions(+), 98 deletions(-) create mode 100644 project_workload/security/project_workload_security.xml diff --git a/project_workload/__manifest__.py b/project_workload/__manifest__.py index b677685b7..8b4d7f559 100644 --- a/project_workload/__manifest__.py +++ b/project_workload/__manifest__.py @@ -1,5 +1,6 @@ -# Copyright 2023 Akretion (https://www.akretion.com). +# Copyright 2024 Akretion (https://www.akretion.com). # @author Sébastien BEAU +# @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { @@ -16,6 +17,7 @@ ], "data": [ "security/ir.model.access.csv", + "security/project_workload_security.xml", "views/project_task_workload_view.xml", "views/project_task_view.xml", "views/project_project_view.xml", diff --git a/project_workload/models/project_project.py b/project_workload/models/project_project.py index 94c1fb980..811667e03 100644 --- a/project_workload/models/project_project.py +++ b/project_workload/models/project_project.py @@ -1,5 +1,6 @@ -# Copyright 2023 Akretion (https://www.akretion.com). +# Copyright 2024 Akretion (https://www.akretion.com). # @author Sébastien BEAU +# @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import fields, models diff --git a/project_workload/models/project_task.py b/project_workload/models/project_task.py index 942d36c6c..317fccf66 100644 --- a/project_workload/models/project_task.py +++ b/project_workload/models/project_task.py @@ -1,56 +1,73 @@ -# Copyright 2023 Akretion (https://www.akretion.com). +# Copyright 2024 Akretion (https://www.akretion.com). # @author Sébastien BEAU +# @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import api, fields, models -WORKLOAD_FIELDS = [ - "date_start", - "date_end", - "user_id", - "planned_hours", - "use_workload", - "config_workload_manually", -] - class ProjectTask(models.Model): _inherit = "project.task" - workload_ids = fields.One2many("project.task.workload", "task_id", "Task") + workload_ids = fields.One2many( + "project.task.workload", + "task_id", + "Task", + compute="_compute_workload_ids", + store=True, + ) + workload_unit_ids = fields.One2many( + "project.workload.unit", + compute="_compute_workload_unit_ids", + string="Workload Units", + ) use_workload = fields.Boolean(related="project_id.use_workload") config_workload_manually = fields.Boolean() + @api.depends( + "date_start", + "date_end", + "planned_hours", + "user_id", + "config_workload_manually", + "use_workload", + ) + def _compute_workload_ids(self): + for record in self: + record.workload_ids = self.env["project.task.workload"].search( + [("task_id", "=", record.id)] + ) + if not record.use_workload: + continue + + # Handle only automatic config in planned task + if record.config_workload_manually or not ( + record.date_start and record.date_end and record.planned_hours + ): + continue + + vals = record._prepare_workload() + # Handle only one workload in automatic + if record.workload_ids: + # Remove other workloads and update the first workload values + record.workload_ids = [ + (1, record.workload_ids[0].id, vals), + *[(2, workload_id.id) for workload_id in record.workload_ids[1:]], + ] + else: + # Create the workload + record.workload_ids = [(0, 0, vals)] + def _prepare_workload(self): return { - "task_id": self.id, "date_start": self.date_start, "date_end": self.date_end, "hours": self.planned_hours, "user_id": self.user_id.id, } - def _sync_workload(self): + @api.depends("workload_ids.unit_ids") + def _compute_workload_unit_ids(self): + # related doesn't retrieve all the data so we need to compute it for record in self: - if record.use_workload and not record.config_workload_manually: - if not (record.date_start and record.date_end and record.planned_hours): - # Handle only planned task - continue - vals = self._prepare_workload() - if record.workload_ids: - record.workload_ids[1:].unlink() - record.workload_ids.write(vals) - else: - self.env["project.task.workload"].create([vals]) - - @api.model_create_multi - def create(self, vals_list): - records = super().create(vals_list) - records._sync_workload() - return records - - def write(self, vals): - res = super().write(vals) - if set(vals.keys()).intersection(WORKLOAD_FIELDS): - self._sync_workload() - return res + record.workload_unit_ids = record.workload_ids.unit_ids diff --git a/project_workload/models/project_task_workload.py b/project_workload/models/project_task_workload.py index 2fd58b17d..04c878c81 100644 --- a/project_workload/models/project_task_workload.py +++ b/project_workload/models/project_task_workload.py @@ -1,11 +1,12 @@ -# Copyright 2023 Akretion (https://www.akretion.com). +# Copyright 2024 Akretion (https://www.akretion.com). # @author Sébastien BEAU +# @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - from datetime import timedelta -from odoo import fields, models +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError WEEK_FORMAT = "%Y-%W" @@ -28,7 +29,61 @@ class ProjectTaskWorkload(models.Model): date_end = fields.Date(required=True) user_id = fields.Many2one("res.users", "User", required=True) hours = fields.Float(required=True) - unit_ids = fields.One2many("project.workload.unit", "workload_id", "Units") + unit_ids = fields.One2many( + "project.workload.unit", + "workload_id", + "Units", + compute="_compute_unit_ids", + store=True, + ) + + @api.constrains("date_start", "date_end") + def _check_end_date(self): + for task in self: + if task.date_end < task.date_start: + raise ValidationError( + _("The end date cannot be earlier than the start date.") + ) + + @api.depends("date_start", "date_end", "hours") + def _compute_unit_ids(self): + for record in self: + record.unit_ids = self.env["project.workload.unit"].search( + [ + ("workload_id", "=", record.id), + ] + ) + # We need to have the data to compute the unit (this happens at create) + if not record.date_start or not record.date_end or not record.hours: + continue + record._check_end_date() + hours_per_week = record._get_hours_per_week() + unit_per_week = {wl.week: wl for wl in record.unit_ids} + commands = [] + for week, hours in hours_per_week.items(): + unit = unit_per_week.get(week) + if unit: + if unit.hours != hours: + # Update unit + commands.append((1, unit.id, {"hours": hours})) + else: + # Create unit + commands.append( + ( + 0, + 0, + { + "hours": hours, + "week": week, + }, + ) + ) + for week, unit in unit_per_week.items(): + if week not in hours_per_week: + # Remove not in week anymore unit + commands.append((2, unit.id)) + + record.unit_ids = commands def _get_hours_per_week(self): weeks = set() @@ -43,35 +98,16 @@ def _get_hours_per_week(self): hours = self.hours / len(weeks) return {week: hours for week in weeks} - def _sync_workload_unit(self): - vals_list = [] - for record in self: - hours_per_week = self._get_hours_per_week() - unit_per_week = {wl.week: wl for wl in self.unit_ids} - for week, hours in hours_per_week.items(): - unit = unit_per_week.get(week) - if unit: - if unit.hours != hours: - unit.hours = hours - else: - vals_list.append( - { - "workload_id": record.id, - "hours": hours, - "week": week, - } - ) - for week, unit in unit_per_week.items(): - if week not in hours_per_week: - unit.unlink() - self.env["project.workload.unit"].create(vals_list) - - def create(self, vals_list): - records = super().create(vals_list) - records._sync_workload_unit() - return records - - def write(self, vals): - res = super().write(vals) - self._sync_workload_unit() - return res + def name_get(self): + result = [] + for task in self: + if not task.date_start or not task.date_end: + continue + week_start = f"Load {week_name(task.date_start)}" + week_end = week_name(task.date_end - timedelta(days=7)) + name = f"{week_start}" + if week_end > week_start: + name += f" - {week_end}" + name += f": {task.hours}h" + result.append((task.id, name)) + return result diff --git a/project_workload/models/project_workload_unit.py b/project_workload/models/project_workload_unit.py index c1f498e01..702e00903 100644 --- a/project_workload/models/project_workload_unit.py +++ b/project_workload/models/project_workload_unit.py @@ -1,5 +1,6 @@ -# Copyright 2023 Akretion (https://www.akretion.com). +# Copyright 2024 Akretion (https://www.akretion.com). # @author Sébastien BEAU +# @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import fields, models diff --git a/project_workload/security/project_workload_security.xml b/project_workload/security/project_workload_security.xml new file mode 100644 index 000000000..b26828e1a --- /dev/null +++ b/project_workload/security/project_workload_security.xml @@ -0,0 +1,23 @@ + + + + + Show Project Workload + + + + + + + + + + + + diff --git a/project_workload/tests/test_workload.py b/project_workload/tests/test_workload.py index 6fa5da6b3..97618f8b7 100644 --- a/project_workload/tests/test_workload.py +++ b/project_workload/tests/test_workload.py @@ -1,5 +1,6 @@ -# Copyright 2023 Akretion (https://www.akretion.com). +# Copyright 2024 Akretion (https://www.akretion.com). # @author Sébastien BEAU +# @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from datetime import timedelta diff --git a/project_workload/views/menu_view.xml b/project_workload/views/menu_view.xml index e96244b04..7bf00df1b 100644 --- a/project_workload/views/menu_view.xml +++ b/project_workload/views/menu_view.xml @@ -6,6 +6,7 @@ parent="project.menu_main_pm" sequence="20" name="Workload" + groups="project_workload.group_project_workload" /> diff --git a/project_workload/views/project_project_view.xml b/project_workload/views/project_project_view.xml index 54d8f286b..d7937d9bc 100644 --- a/project_workload/views/project_project_view.xml +++ b/project_workload/views/project_project_view.xml @@ -6,11 +6,11 @@
-
- -
-
+
+ +
+
diff --git a/project_workload/views/project_task_view.xml b/project_workload/views/project_task_view.xml index 3a8fe580d..b13818f32 100644 --- a/project_workload/views/project_task_view.xml +++ b/project_workload/views/project_task_view.xml @@ -9,6 +9,7 @@ @@ -28,6 +29,14 @@ + + + + + + + +
diff --git a/project_workload/views/project_task_workload_view.xml b/project_workload/views/project_task_workload_view.xml index 702fa962a..3b4f32029 100644 --- a/project_workload/views/project_task_workload_view.xml +++ b/project_workload/views/project_task_workload_view.xml @@ -1,25 +1,25 @@ - - project.task.workload - -
- + + project.task.workload + + - - - + + + + + - - - - - - - -
-
-
+ + + + + + + + +
From 1246e40d1f55535e1a2a36024850c229007cae9f Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 7 Feb 2024 17:35:41 +0100 Subject: [PATCH 03/28] =?UTF-8?q?[IMP]=20project=5Fworkload:=C2=A0Improve?= =?UTF-8?q?=20workload=20sync=20inheritance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project_workload/models/project_task.py | 47 ++++++++++++++----- .../models/project_task_workload.py | 4 +- project_workload/views/project_task_view.xml | 38 +++++++-------- 3 files changed, 56 insertions(+), 33 deletions(-) diff --git a/project_workload/models/project_task.py b/project_workload/models/project_task.py index 317fccf66..42fca877d 100644 --- a/project_workload/models/project_task.py +++ b/project_workload/models/project_task.py @@ -46,26 +46,49 @@ def _compute_workload_ids(self): ): continue - vals = record._prepare_workload() - # Handle only one workload in automatic - if record.workload_ids: - # Remove other workloads and update the first workload values - record.workload_ids = [ - (1, record.workload_ids[0].id, vals), - *[(2, workload_id.id) for workload_id in record.workload_ids[1:]], - ] - else: - # Create the workload - record.workload_ids = [(0, 0, vals)] + record.workload_ids = record._get_workload_sync() - def _prepare_workload(self): + def _prepare_workload(self, **extra): return { "date_start": self.date_start, "date_end": self.date_end, "hours": self.planned_hours, "user_id": self.user_id.id, + **extra, } + def _get_workload_sync(self): + self.ensure_one() + return [ + *[(0, 0, vals) for vals in self._get_new_workloads()], + *[ + (1, workload_id.id, vals) + for workload_id, vals in self._get_updated_workloads() + ], + *[(2, workload_id.id) for workload_id in self._get_obsolete_workloads()], + ] + + def _get_new_workloads(self): + self.ensure_one() + # Handle only one workload in automatic + if not self.workload_ids: + return [self._prepare_workload()] + return [] + + def _get_updated_workloads(self): + self.ensure_one() + # Remove other workloads and update the first workload values + if self.workload_ids: + return [(self.workload_ids[0], self._prepare_workload())] + return [] + + def _get_obsolete_workloads(self): + self.ensure_one() + # Remove other workloads and update the first workload values + if len(self.workload_ids) > 1: + return self.workload_ids[1:] + return [] + @api.depends("workload_ids.unit_ids") def _compute_workload_unit_ids(self): # related doesn't retrieve all the data so we need to compute it diff --git a/project_workload/models/project_task_workload.py b/project_workload/models/project_task_workload.py index 04c878c81..7b5cee41b 100644 --- a/project_workload/models/project_task_workload.py +++ b/project_workload/models/project_task_workload.py @@ -103,9 +103,9 @@ def name_get(self): for task in self: if not task.date_start or not task.date_end: continue - week_start = f"Load {week_name(task.date_start)}" + week_start = week_name(task.date_start) week_end = week_name(task.date_end - timedelta(days=7)) - name = f"{week_start}" + name = f"{_('Load')} {week_start}" if week_end > week_start: name += f" - {week_end}" name += f": {task.hours}h" diff --git a/project_workload/views/project_task_view.xml b/project_workload/views/project_task_view.xml index b13818f32..b8339feec 100644 --- a/project_workload/views/project_task_view.xml +++ b/project_workload/views/project_task_view.xml @@ -17,26 +17,26 @@ + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - From ea6eb377850c1b5b8e9ad939d80ce3e75f442fa7 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 7 Feb 2024 17:35:54 +0100 Subject: [PATCH 04/28] [ADD] project_workload_additions --- project_workload_additions/__manifest__.py | 8 +- project_workload_additions/models/__init__.py | 5 ++ .../models/project_project.py | 14 ++++ .../models/project_task.py | 84 +++++++++++++++++++ .../models/project_task_workload.py | 14 ++++ .../models/project_task_workload_addition.py | 43 ++++++++++ .../project_task_workload_addition_type.py | 16 ++++ .../security/ir.model.access.csv | 3 + .../views/menu_view.xml | 13 +++ .../views/project_project_view.xml | 33 ++++++++ .../views/project_task_view.xml | 14 ++++ ...ject_task_workload_addition_type_views.xml | 60 +++++++++++++ 12 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 project_workload_additions/models/project_project.py create mode 100644 project_workload_additions/models/project_task.py create mode 100644 project_workload_additions/models/project_task_workload.py create mode 100644 project_workload_additions/models/project_task_workload_addition.py create mode 100644 project_workload_additions/models/project_task_workload_addition_type.py create mode 100644 project_workload_additions/security/ir.model.access.csv create mode 100644 project_workload_additions/views/menu_view.xml create mode 100644 project_workload_additions/views/project_project_view.xml create mode 100644 project_workload_additions/views/project_task_view.xml create mode 100644 project_workload_additions/views/project_task_workload_addition_type_views.xml diff --git a/project_workload_additions/__manifest__.py b/project_workload_additions/__manifest__.py index b3d4ee94b..67ecc9986 100644 --- a/project_workload_additions/__manifest__.py +++ b/project_workload_additions/__manifest__.py @@ -15,5 +15,11 @@ "depends": [ "project_workload", ], - "data": [], + "data": [ + "security/ir.model.access.csv", + "views/project_task_view.xml", + "views/project_task_workload_addition_type_views.xml", + "views/project_project_view.xml", + "views/menu_view.xml", + ], } diff --git a/project_workload_additions/models/__init__.py b/project_workload_additions/models/__init__.py index e69de29bb..24ba8ae4d 100644 --- a/project_workload_additions/models/__init__.py +++ b/project_workload_additions/models/__init__.py @@ -0,0 +1,5 @@ +from . import project_project +from . import project_task_workload +from . import project_task +from . import project_task_workload_addition_type +from . import project_task_workload_addition diff --git a/project_workload_additions/models/project_project.py b/project_workload_additions/models/project_project.py new file mode 100644 index 000000000..dbe054e0c --- /dev/null +++ b/project_workload_additions/models/project_project.py @@ -0,0 +1,14 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProjectProject(models.Model): + _inherit = "project.project" + + additional_workload_ids = fields.One2many( + "project.task.workload.addition", "project_id", "Additional Workload" + ) diff --git a/project_workload_additions/models/project_task.py b/project_workload_additions/models/project_task.py new file mode 100644 index 000000000..9017f98ef --- /dev/null +++ b/project_workload_additions/models/project_task.py @@ -0,0 +1,84 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ProjectTask(models.Model): + _inherit = "project.task" + + def _get_new_workloads(self): + rv = super()._get_new_workloads() + # super creates a new workload if there are none + # but here we can have only additional workloads + # so we also need to check if the existing workloads are additional + if not rv and all( + workload.additional_workload_id for workload in self.workload_ids + ): + rv.append(self._prepare_workload()) + + additional_workloads = { + workload.additional_workload_id: workload + for workload in self.workload_ids + if workload.additional_workload_id + } + # Now we need to create a new workload for each additional workload + for additional_workload in self.project_id.additional_workload_ids: + if additional_workload not in additional_workloads: + rv.append(self._prepare_additional_workload(additional_workload)) + + return rv + + def _get_updated_workloads(self): + # We sort the workloads by additional_workload_id to ensure that the first workload is the main one + self.workload_ids = self.workload_ids.sorted( + key=lambda w: w.additional_workload_id + ) + rv = super()._get_updated_workloads() + if rv and rv[0][0].additional_workload_id: + rv = [] + + additional_workloads = { + workload.additional_workload_id: workload + for workload in self.workload_ids + if workload.additional_workload_id + } + # Now we need to update the existing workload for each additional workload + for additional_workload, workload in additional_workloads.items(): + rv.append( + ( + workload, + self._prepare_additional_workload(additional_workload), + ) + ) + + return rv + + def _get_obsolete_workloads(self): + self.workload_ids = self.workload_ids.sorted( + key=lambda w: w.additional_workload_id + ) + rv = super()._get_obsolete_workloads() + if rv: + # Do not delete additional workloads + rv = [workload for workload in rv if not workload.additional_workload_id] + + # Remove all additional workloads that are not in the project anymore + additional_workloads = { + workload.additional_workload_id: workload + for workload in self.workload_ids + if workload.additional_workload_id + } + for additional_workload in additional_workloads: + if additional_workload not in self.project_id.additional_workload_ids: + rv.append(additional_workload) + return rv + + def _prepare_additional_workload(self, additional_workload, **extra): + return self._prepare_workload( + additional_workload_id=additional_workload.id, + hours=additional_workload._compute_hours_from_task(self), + user_id=additional_workload.user_id.id, + **extra, + ) diff --git a/project_workload_additions/models/project_task_workload.py b/project_workload_additions/models/project_task_workload.py new file mode 100644 index 000000000..3b4034d63 --- /dev/null +++ b/project_workload_additions/models/project_task_workload.py @@ -0,0 +1,14 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ProjectTaskWorkload(models.Model): + _inherit = "project.task.workload" + + additional_workload_id = fields.Many2one( + "project.task.workload.addition", string="Additional Workload Reference" + ) diff --git a/project_workload_additions/models/project_task_workload_addition.py b/project_workload_additions/models/project_task_workload_addition.py new file mode 100644 index 000000000..9f6927810 --- /dev/null +++ b/project_workload_additions/models/project_task_workload_addition.py @@ -0,0 +1,43 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class ProjectWorkloadAddition(models.Model): + _name = "project.task.workload.addition" + _description = "Project Task Workload Addition" + + project_id = fields.Many2one("project.project", string="Project", required=True) + type = fields.Many2one( + "project.task.workload.addition.type", string="Addition Type", required=True + ) + percentage = fields.Float( + required=True, + string="Added Percentage", + ) + user_id = fields.Many2one("res.users", string="User", required=True) + task_id = fields.Many2one("project.task", string="Task", required=True) + + @api.onchange("type") + def _onchange_type(self): + self.percentage = self.type.default_percentage + + def _compute_hours_from_task(self, task): + self.ensure_one() + return task.planned_hours * (self.percentage / 100) + + def name_get(self): + result = [] + for record in self: + result.append( + ( + record.id, + _( + "%s additional workload (%d%%)" + % (record.task_id.name, record.percentage) + ), + ) + ) + return result diff --git a/project_workload_additions/models/project_task_workload_addition_type.py b/project_workload_additions/models/project_task_workload_addition_type.py new file mode 100644 index 000000000..674a82b47 --- /dev/null +++ b/project_workload_additions/models/project_task_workload_addition_type.py @@ -0,0 +1,16 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ProjectWorkloadAdditionType(models.Model): + _name = "project.task.workload.addition.type" + _description = "Project Task Workload Addition Type" + + name = fields.Char(required=True) + description = fields.Text() + default_percentage = fields.Float(required=True, default=10) + active = fields.Boolean(default=True) diff --git a/project_workload_additions/security/ir.model.access.csv b/project_workload_additions/security/ir.model.access.csv new file mode 100644 index 000000000..23f0d2274 --- /dev/null +++ b/project_workload_additions/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 +access_edit_project_task_workload_addition,edit project task workload addition,model_project_task_workload_addition,project.group_project_user,1,1,1,1 +access_edit_project_task_workload_addition_type,edit project task workload addition type,model_project_task_workload_addition_type,project.group_project_user,1,1,1,1 diff --git a/project_workload_additions/views/menu_view.xml b/project_workload_additions/views/menu_view.xml new file mode 100644 index 000000000..7dd4bd7ca --- /dev/null +++ b/project_workload_additions/views/menu_view.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/project_workload_additions/views/project_project_view.xml b/project_workload_additions/views/project_project_view.xml new file mode 100644 index 000000000..b2487ae14 --- /dev/null +++ b/project_workload_additions/views/project_project_view.xml @@ -0,0 +1,33 @@ + + + + + project.project + + + + + + + + + + + + + + + + + + + + diff --git a/project_workload_additions/views/project_task_view.xml b/project_workload_additions/views/project_task_view.xml new file mode 100644 index 000000000..efa32de69 --- /dev/null +++ b/project_workload_additions/views/project_task_view.xml @@ -0,0 +1,14 @@ + + + + + project.task + + + + + + + + + diff --git a/project_workload_additions/views/project_task_workload_addition_type_views.xml b/project_workload_additions/views/project_task_workload_addition_type_views.xml new file mode 100644 index 000000000..d4193b688 --- /dev/null +++ b/project_workload_additions/views/project_task_workload_addition_type_views.xml @@ -0,0 +1,60 @@ + + + + + project.task.workload.addition.type + + + + + + + + + + + project.task.workload.addition.type + +
+ +
+

+ +

+
+ + + + + + + +
+
+
+
+ + + project.task.workload.addition.type + + + + + + + + + + Workload Addition Types + ir.actions.act_window + project.task.workload.addition.type + tree,form + + [] + {} + + +
From 562a5cc1b6e39fb5927f1b1681a3693ca1da918f Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 12 Feb 2024 16:35:01 +0100 Subject: [PATCH 05/28] [FIX] project_workload: Fix compute being emptied and fix ui --- project_workload/models/project_task.py | 4 ---- project_workload/models/project_task_workload.py | 12 +++++------- project_workload/models/project_workload_unit.py | 4 +--- .../views/project_task_workload_view.xml | 13 +++++++------ 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/project_workload/models/project_task.py b/project_workload/models/project_task.py index 42fca877d..63234e1e3 100644 --- a/project_workload/models/project_task.py +++ b/project_workload/models/project_task.py @@ -34,9 +34,6 @@ class ProjectTask(models.Model): ) def _compute_workload_ids(self): for record in self: - record.workload_ids = self.env["project.task.workload"].search( - [("task_id", "=", record.id)] - ) if not record.use_workload: continue @@ -45,7 +42,6 @@ def _compute_workload_ids(self): record.date_start and record.date_end and record.planned_hours ): continue - record.workload_ids = record._get_workload_sync() def _prepare_workload(self, **extra): diff --git a/project_workload/models/project_task_workload.py b/project_workload/models/project_task_workload.py index 7b5cee41b..856c61945 100644 --- a/project_workload/models/project_task_workload.py +++ b/project_workload/models/project_task_workload.py @@ -32,7 +32,7 @@ class ProjectTaskWorkload(models.Model): unit_ids = fields.One2many( "project.workload.unit", "workload_id", - "Units", + string="Units", compute="_compute_unit_ids", store=True, ) @@ -48,11 +48,6 @@ def _check_end_date(self): @api.depends("date_start", "date_end", "hours") def _compute_unit_ids(self): for record in self: - record.unit_ids = self.env["project.workload.unit"].search( - [ - ("workload_id", "=", record.id), - ] - ) # We need to have the data to compute the unit (this happens at create) if not record.date_start or not record.date_end or not record.hours: continue @@ -104,7 +99,10 @@ def name_get(self): if not task.date_start or not task.date_end: continue week_start = week_name(task.date_start) - week_end = week_name(task.date_end - timedelta(days=7)) + end = task.date_end + if task.date_start.weekday() > task.date_end.weekday(): + end -= timedelta(days=7) + week_end = week_name(end) name = f"{_('Load')} {week_start}" if week_end > week_start: name += f" - {week_end}" diff --git a/project_workload/models/project_workload_unit.py b/project_workload/models/project_workload_unit.py index 702e00903..acad0993e 100644 --- a/project_workload/models/project_workload_unit.py +++ b/project_workload/models/project_workload_unit.py @@ -16,6 +16,4 @@ class ProjectWorkloadUnit(models.Model): user_id = fields.Many2one( "res.users", "User", related="workload_id.user_id", store=True ) - project_id = fields.Many2one( - "project.project", "Project", related="workload_id.project_id", store=True - ) + task_id = fields.Many2one("project.task", "Task", related="workload_id.task_id") diff --git a/project_workload/views/project_task_workload_view.xml b/project_workload/views/project_task_workload_view.xml index 3b4f32029..c8d591b65 100644 --- a/project_workload/views/project_task_workload_view.xml +++ b/project_workload/views/project_task_workload_view.xml @@ -3,6 +3,7 @@ project.task.workload + project.task.workload.form
@@ -11,13 +12,13 @@ + + + + + + - - - - - -
From e0408f33d5a13ad30acd4d1bc23698ed55c7ab0b Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 12 Feb 2024 16:36:20 +0100 Subject: [PATCH 06/28] [FIX] project_workload_additions: Add additional workload in forms and the related task --- project_workload_additions/__manifest__.py | 3 ++- .../models/project_task_workload.py | 4 ++++ .../views/project_task_view.xml | 1 + .../views/project_task_workload_view.xml | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 project_workload_additions/views/project_task_workload_view.xml diff --git a/project_workload_additions/__manifest__.py b/project_workload_additions/__manifest__.py index 67ecc9986..4b594f3d3 100644 --- a/project_workload_additions/__manifest__.py +++ b/project_workload_additions/__manifest__.py @@ -17,9 +17,10 @@ ], "data": [ "security/ir.model.access.csv", + "views/project_project_view.xml", "views/project_task_view.xml", "views/project_task_workload_addition_type_views.xml", - "views/project_project_view.xml", + "views/project_task_workload_view.xml", "views/menu_view.xml", ], } diff --git a/project_workload_additions/models/project_task_workload.py b/project_workload_additions/models/project_task_workload.py index 3b4034d63..cf59742e8 100644 --- a/project_workload_additions/models/project_task_workload.py +++ b/project_workload_additions/models/project_task_workload.py @@ -12,3 +12,7 @@ class ProjectTaskWorkload(models.Model): additional_workload_id = fields.Many2one( "project.task.workload.addition", string="Additional Workload Reference" ) + + additional_workload_task_id = fields.Many2one( + related="additional_workload_id.task_id", string="Additional Workload Task" + ) diff --git a/project_workload_additions/views/project_task_view.xml b/project_workload_additions/views/project_task_view.xml index efa32de69..611f11c73 100644 --- a/project_workload_additions/views/project_task_view.xml +++ b/project_workload_additions/views/project_task_view.xml @@ -7,6 +7,7 @@ + diff --git a/project_workload_additions/views/project_task_workload_view.xml b/project_workload_additions/views/project_task_workload_view.xml new file mode 100644 index 000000000..e3727b55f --- /dev/null +++ b/project_workload_additions/views/project_task_workload_view.xml @@ -0,0 +1,17 @@ + + + + + project.task.workload + + + + + + + + + + + + From c71a1176fc7d0b073d53682574d564e4cca58df8 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 12 Feb 2024 16:37:47 +0100 Subject: [PATCH 07/28] [IMP] project_workload_timesheet: Add workload units in timesheet sheet and smart button --- project_workload_timesheet/models/__init__.py | 2 + .../models/hr_timesheet_sheet.py | 124 ++++++++++++++++++ .../models/project_workload_unit.py | 23 ++++ .../views/hr_timesheet_sheet_views.xml | 33 +++++ 4 files changed, 182 insertions(+) create mode 100644 project_workload_timesheet/models/hr_timesheet_sheet.py create mode 100644 project_workload_timesheet/models/project_workload_unit.py create mode 100644 project_workload_timesheet/views/hr_timesheet_sheet_views.xml diff --git a/project_workload_timesheet/models/__init__.py b/project_workload_timesheet/models/__init__.py index e69de29bb..ef599bd60 100644 --- a/project_workload_timesheet/models/__init__.py +++ b/project_workload_timesheet/models/__init__.py @@ -0,0 +1,2 @@ +from . import hr_timesheet_sheet +from . import project_workload_unit diff --git a/project_workload_timesheet/models/hr_timesheet_sheet.py b/project_workload_timesheet/models/hr_timesheet_sheet.py new file mode 100644 index 000000000..284cebd3d --- /dev/null +++ b/project_workload_timesheet/models/hr_timesheet_sheet.py @@ -0,0 +1,124 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import timedelta + +from odoo import api, fields, models + +from odoo.addons.project_workload.models.project_task_workload import week_name + + +class Sheet(models.Model): + _inherit = "hr_timesheet.sheet" + + workload_unit_ids = fields.One2many( + "project.workload.unit", + "sheet_id", + string="Workload Units", + compute="_compute_workload_unit_ids", + ) + + next_week_load = fields.Float( + "Next Week Load", + compute="_compute_next_week_load", + help="The workload of the next week", + ) + + @api.depends("date_start", "date_end", "user_id") + def _compute_workload_unit_ids(self): + for record in self: + week = week_name(self.date_start) # Use only start date for now + record.workload_unit_ids = ( + self.env["project.workload.unit"] + .search( + [ + ("week", "=", week), + ("user_id", "=", record.user_id.id), + ], + ) + .sorted(lambda p: -int(p.priority or 0)) # Hum + ) + + @api.depends("date_start", "date_end", "user_id") + def _compute_next_week_load(self): + for record in self: + next_week = week_name(self.date_start + timedelta(days=7)) + next_week_units = self.env["project.workload.unit"].search( + [ + ("week", "=", next_week), + ("user_id", "=", record.user_id.id), + ], + ) + + record.next_week_load = sum(next_week_units.mapped("hours")) + + def button_open_next_week(self): + self.ensure_one() + next_week = self.date_start + timedelta(days=7) + date_start = self._get_period_start(self.env.user.company_id, next_week) + date_end = self._get_period_end(self.env.user.company_id, next_week) + next_week_sheet = self.env["hr_timesheet.sheet"].search( + [ + ("date_start", "=", date_start), + ("date_end", "=", date_end), + ("user_id", "=", self.user_id.id), + ], + limit=1, + ) + view = { + "name": "Next Week", + "type": "ir.actions.act_window", + "res_model": "hr_timesheet.sheet", + "view_id": self.env.ref( + "project_workload_timesheet.hr_timesheet_sheet_form_my" + ).id, + "view_mode": "form", + "context": { + "default_date_start": date_start, + "default_date_end": date_end, + "default_user_id": self.user_id.id, + }, + "target": "current", + } + if next_week_sheet: + view["res_id"] = next_week_sheet.id + + return view + + def _add_line_from_unit(self, unit): + if self.state not in ["new", "draft"]: + return + values = self._prepare_empty_analytic_line() + new_line_unique_id = self._get_new_line_unique_id() + existing_unique_ids = list( + {frozenset(line.get_unique_id().items()) for line in self.line_ids} + ) + if existing_unique_ids: + self.delete_empty_lines(False) + if frozenset(new_line_unique_id.items()) not in existing_unique_ids: + # TODO MAKE this configurable + DAILY_HOURS = 8 + task = unit._get_timesheeting_task() + + if task.date_start and task.date_end: + task_start = task.date_start.date() + task_end = task.date_end.date() + + today = fields.Date.today() + # If this is the current week + if self.date_start <= today <= self.date_end: + # Take the closest day to today + values["date"] = max(min(today, task_end), task_start) + else: + # If start date is in week, take it + if self.date_start <= task_start <= self.date_end: + values["date"] = task_start + + values["unit_amount"] = min(DAILY_HOURS, unit.hours) + values["project_id"] = task.project_id.id + values["task_id"] = task.id + self.timesheet_ids |= self.env["account.analytic.line"]._sheet_create( + values + ) + return True diff --git a/project_workload_timesheet/models/project_workload_unit.py b/project_workload_timesheet/models/project_workload_unit.py new file mode 100644 index 000000000..ef093c306 --- /dev/null +++ b/project_workload_timesheet/models/project_workload_unit.py @@ -0,0 +1,23 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProjectWorkloadUnit(models.Model): + _inherit = "project.workload.unit" + + sheet_id = fields.Many2one("hr_timesheet.sheet") + priority = fields.Selection(related="task_id.priority") + + def action_add(self): + sheet_id = self.env.context.get("current_sheet_id") + if not sheet_id: + return + sheet = self.env["hr_timesheet.sheet"].browse(sheet_id) + return sheet._add_line_from_unit(self) + + def _get_timesheeting_task(self): + # For overrides + return self.task_id diff --git a/project_workload_timesheet/views/hr_timesheet_sheet_views.xml b/project_workload_timesheet/views/hr_timesheet_sheet_views.xml new file mode 100644 index 000000000..c19b3b84f --- /dev/null +++ b/project_workload_timesheet/views/hr_timesheet_sheet_views.xml @@ -0,0 +1,33 @@ + + + + hr_timesheet.sheet + + +
+
+ +
+
+ +
+

Todo

+ + + + + + +
+
+
+
+
From 63e90245fc652baf65ae643518400b60f6800869 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 12 Feb 2024 16:38:39 +0100 Subject: [PATCH 08/28] [ADD] projectt_workload_timesheet_additions --- .../__init__.py | 1 + .../__manifest__.py | 17 +++++++++++++ .../models/__init__.py | 2 ++ .../models/hr_timesheet_sheet.py | 8 ++++++ .../models/project_workload_unit.py | 25 +++++++++++++++++++ .../views/hr_timesheet_sheet_views.xml | 12 +++++++++ 6 files changed, 65 insertions(+) create mode 100644 project_workload_timesheet_additions/__init__.py create mode 100644 project_workload_timesheet_additions/__manifest__.py create mode 100644 project_workload_timesheet_additions/models/__init__.py create mode 100644 project_workload_timesheet_additions/models/hr_timesheet_sheet.py create mode 100644 project_workload_timesheet_additions/models/project_workload_unit.py create mode 100644 project_workload_timesheet_additions/views/hr_timesheet_sheet_views.xml diff --git a/project_workload_timesheet_additions/__init__.py b/project_workload_timesheet_additions/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/project_workload_timesheet_additions/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_workload_timesheet_additions/__manifest__.py b/project_workload_timesheet_additions/__manifest__.py new file mode 100644 index 000000000..2682ddf42 --- /dev/null +++ b/project_workload_timesheet_additions/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Project Workload Timesheet Additions", + "summary": "Add Workload Additions To Timesheets", + "version": "14.0.1.0.0", + "development_status": "Alpha", + "category": "Uncategorized", + "website": "https://github.com/akretion/ak-odoo-incubator", + "author": " Akretion", + "license": "AGPL-3", + "depends": ["project_workload_additions", "project_workload_timesheet"], + "data": ["views/hr_timesheet_sheet_views.xml"], + "auto_install": True, +} diff --git a/project_workload_timesheet_additions/models/__init__.py b/project_workload_timesheet_additions/models/__init__.py new file mode 100644 index 000000000..ef599bd60 --- /dev/null +++ b/project_workload_timesheet_additions/models/__init__.py @@ -0,0 +1,2 @@ +from . import hr_timesheet_sheet +from . import project_workload_unit diff --git a/project_workload_timesheet_additions/models/hr_timesheet_sheet.py b/project_workload_timesheet_additions/models/hr_timesheet_sheet.py new file mode 100644 index 000000000..b9bcd8b40 --- /dev/null +++ b/project_workload_timesheet_additions/models/hr_timesheet_sheet.py @@ -0,0 +1,8 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class Sheet(models.Model): + _inherit = "hr_timesheet.sheet" diff --git a/project_workload_timesheet_additions/models/project_workload_unit.py b/project_workload_timesheet_additions/models/project_workload_unit.py new file mode 100644 index 000000000..c793b2b8b --- /dev/null +++ b/project_workload_timesheet_additions/models/project_workload_unit.py @@ -0,0 +1,25 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProjectWorkloadUnit(models.Model): + _inherit = "project.workload.unit" + additional_workload_id = fields.Many2one( + "project.task.workload.addition", + "Additional Task Workload", + related="workload_id.additional_workload_id", + ) + additional_task_id = fields.Many2one( + "project.task", + "Additional Task", + related="workload_id.additional_workload_task_id", + ) + + def _get_timesheeting_task(self): + # Timesheet in additional workload task + if self.additional_workload_id: + return self.additional_task_id + return super()._get_timesheeting_task() diff --git a/project_workload_timesheet_additions/views/hr_timesheet_sheet_views.xml b/project_workload_timesheet_additions/views/hr_timesheet_sheet_views.xml new file mode 100644 index 000000000..09c7d84b9 --- /dev/null +++ b/project_workload_timesheet_additions/views/hr_timesheet_sheet_views.xml @@ -0,0 +1,12 @@ + + + + hr_timesheet.sheet + + + + + + + + From cb683907d99e897d7ea154aeba99dd039943092a Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 15 Feb 2024 17:08:53 +0100 Subject: [PATCH 09/28] [IMP] project_workload: Improve naming --- .../models/project_task_workload.py | 21 +++++++++++-------- .../models/project_workload_unit.py | 14 +++++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/project_workload/models/project_task_workload.py b/project_workload/models/project_task_workload.py index 856c61945..1d6d43c17 100644 --- a/project_workload/models/project_task_workload.py +++ b/project_workload/models/project_task_workload.py @@ -2,7 +2,7 @@ # @author Sébastien BEAU # @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - +import re from datetime import timedelta from odoo import _, api, fields, models @@ -17,6 +17,9 @@ def week_name(value): return None +week_merge_re = re.compile((r"(\d{4})-(\d{2}) - (\1)-(\d{2})")) + + class ProjectTaskWorkload(models.Model): _name = "project.task.workload" _description = "Project Task Workload" @@ -95,17 +98,17 @@ def _get_hours_per_week(self): def name_get(self): result = [] - for task in self: - if not task.date_start or not task.date_end: + for load in self: + if not load.date_start or not load.date_end: continue - week_start = week_name(task.date_start) - end = task.date_end - if task.date_start.weekday() > task.date_end.weekday(): + week_start = week_name(load.date_start) + end = load.date_end + if load.date_start.weekday() > load.date_end.weekday(): end -= timedelta(days=7) week_end = week_name(end) - name = f"{_('Load')} {week_start}" + name = f"{load.task_id.name}: {week_start}" if week_end > week_start: name += f" - {week_end}" - name += f": {task.hours}h" - result.append((task.id, name)) + name = week_merge_re.sub(r"\1-\2->\4", name) + result.append((load.id, name)) return result diff --git a/project_workload/models/project_workload_unit.py b/project_workload/models/project_workload_unit.py index acad0993e..b5ed96445 100644 --- a/project_workload/models/project_workload_unit.py +++ b/project_workload/models/project_workload_unit.py @@ -17,3 +17,17 @@ class ProjectWorkloadUnit(models.Model): "res.users", "User", related="workload_id.user_id", store=True ) task_id = fields.Many2one("project.task", "Task", related="workload_id.task_id") + project_id = fields.Many2one( + "project.project", "Project", related="workload_id.project_id" + ) + + def name_get(self): + result = [] + for unit in self: + result.append( + ( + unit.id, + "%s: %s" % (unit.task_id.name, unit.week), + ) + ) + return result From 961abcf1afb726c454e8130f1ac9718a23d2e2b2 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 15 Feb 2024 17:09:29 +0100 Subject: [PATCH 10/28] [IMP] project_workload_additions: Improve naming --- project_workload_additions/models/__init__.py | 1 + .../models/project_workload_unit.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 project_workload_additions/models/project_workload_unit.py diff --git a/project_workload_additions/models/__init__.py b/project_workload_additions/models/__init__.py index 24ba8ae4d..e90110a3d 100644 --- a/project_workload_additions/models/__init__.py +++ b/project_workload_additions/models/__init__.py @@ -1,5 +1,6 @@ from . import project_project from . import project_task_workload +from . import project_workload_unit from . import project_task from . import project_task_workload_addition_type from . import project_task_workload_addition diff --git a/project_workload_additions/models/project_workload_unit.py b/project_workload_additions/models/project_workload_unit.py new file mode 100644 index 000000000..105fb25d4 --- /dev/null +++ b/project_workload_additions/models/project_workload_unit.py @@ -0,0 +1,21 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models + + +class ProjectWorkloadUnit(models.Model): + _inherit = "project.workload.unit" + + def name_get(self): + result = super().name_get() + units_names = dict(result) + for unit_id, name in result: + unit = self.browse(unit_id) + if unit.workload_id.additional_workload_id: + name = f"{unit.workload_id.additional_workload_id.task_id.name} {_('of')} {name}" + units_names[unit_id] = name + + return list(units_names.items()) From 3a8cec009be720b5a9efbe4ae2f7d4df96d8e27e Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 15 Feb 2024 17:12:44 +0100 Subject: [PATCH 11/28] [IMP] project_workload_timesheet: Link timesheet lines with workload units Compute remaining time for unit Add quick timesheet buttons Handle unit done status Report unfinished unit at sheet validation --- project_workload_timesheet/__manifest__.py | 6 +- project_workload_timesheet/models/__init__.py | 1 + .../models/account_analytic_line.py | 65 +++++++++ .../models/hr_timesheet_sheet.py | 115 ++++++++++++--- .../models/project_workload_unit.py | 73 +++++++++- .../views/hr_timesheet_sheet_views.xml | 136 ++++++++++++++++-- 6 files changed, 356 insertions(+), 40 deletions(-) create mode 100644 project_workload_timesheet/models/account_analytic_line.py diff --git a/project_workload_timesheet/__manifest__.py b/project_workload_timesheet/__manifest__.py index abef4b8b3..4b90aa6a8 100644 --- a/project_workload_timesheet/__manifest__.py +++ b/project_workload_timesheet/__manifest__.py @@ -14,7 +14,9 @@ "license": "AGPL-3", "depends": [ "project_workload", - "hr_timesheet", + "hr_timesheet_sheet", + ], + "data": [ + "views/hr_timesheet_sheet_views.xml", ], - "data": [], } diff --git a/project_workload_timesheet/models/__init__.py b/project_workload_timesheet/models/__init__.py index ef599bd60..bd28cf786 100644 --- a/project_workload_timesheet/models/__init__.py +++ b/project_workload_timesheet/models/__init__.py @@ -1,2 +1,3 @@ +from . import account_analytic_line from . import hr_timesheet_sheet from . import project_workload_unit diff --git a/project_workload_timesheet/models/account_analytic_line.py b/project_workload_timesheet/models/account_analytic_line.py new file mode 100644 index 000000000..c378f66fd --- /dev/null +++ b/project_workload_timesheet/models/account_analytic_line.py @@ -0,0 +1,65 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + + +from odoo.addons.project_workload.models.project_task_workload import week_name + + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + workload_unit_id = fields.Many2one( + comodel_name="project.workload.unit", + string="Workload Unit", + readonly=False, + compute="_compute_workload_unit_id", + store=True, + domain="[" + "('week', '=', week), " + "('user_id', '=', user_id), " + "('project_id', '=', project_id), " + "('task_id', '=', task_id)" + "]", + ) + + week = fields.Char( + string="Week", + compute="_compute_week", + help="Week number of the year", + ) + + @api.depends("date") + def _compute_week(self): + for record in self: + record.week = week_name(record.date) + + @api.depends("task_id", "date", "user_id") + def _compute_workload_unit_id(self): + for record in self: + if not record.project_id or not record.task_id: + record.workload_unit_id = False + continue + available_workload_units = self.env["project.workload.unit"].search( + record._get_available_workload_units_domain() + ) + if ( + not record.workload_unit_id + or record.workload_unit_id not in available_workload_units + ): + record.workload_unit_id = ( + available_workload_units[0] + if len(available_workload_units) > 0 + else False + ) + + def _get_available_workload_units_domain(self): + return [ + ("project_id", "=", self.project_id.id), + ("task_id", "=", self.task_id.id), + ("week", "=", self.week), + ("user_id", "=", self.user_id.id), + ] diff --git a/project_workload_timesheet/models/hr_timesheet_sheet.py b/project_workload_timesheet/models/hr_timesheet_sheet.py index 284cebd3d..5848cdf02 100644 --- a/project_workload_timesheet/models/hr_timesheet_sheet.py +++ b/project_workload_timesheet/models/hr_timesheet_sheet.py @@ -14,9 +14,9 @@ class Sheet(models.Model): workload_unit_ids = fields.One2many( "project.workload.unit", - "sheet_id", string="Workload Units", compute="_compute_workload_unit_ids", + readonly=False, ) next_week_load = fields.Float( @@ -24,6 +24,17 @@ class Sheet(models.Model): compute="_compute_next_week_load", help="The workload of the next week", ) + next_week_units_count = fields.Integer( + "Next Week Units Count", + compute="_compute_next_week_load", + help="The number of workload units of the next week", + ) + + current = fields.Boolean( + "Current", + compute="_compute_current", + help="Is this the current timesheet", + ) @api.depends("date_start", "date_end", "user_id") def _compute_workload_unit_ids(self): @@ -37,7 +48,14 @@ def _compute_workload_unit_ids(self): ("user_id", "=", record.user_id.id), ], ) - .sorted(lambda p: -int(p.priority or 0)) # Hum + .sorted( + lambda p: ( + p.done, # Put done tasks at the end + -int(p.priority or 0), # Sort by priority + p.remaining_hours, # Sort by remaining hours + -p.id, # Stabilize sort + ) + ) ) @api.depends("date_start", "date_end", "user_id") @@ -50,9 +68,14 @@ def _compute_next_week_load(self): ("user_id", "=", record.user_id.id), ], ) - + record.next_week_units_count = len(next_week_units) record.next_week_load = sum(next_week_units.mapped("hours")) + @api.depends("date_start", "date_end") + def _compute_current(self): + for record in self: + record.current = record.date_start <= fields.Date.today() <= record.date_end + def button_open_next_week(self): self.ensure_one() next_week = self.date_start + timedelta(days=7) @@ -97,28 +120,74 @@ def _add_line_from_unit(self, unit): if existing_unique_ids: self.delete_empty_lines(False) if frozenset(new_line_unique_id.items()) not in existing_unique_ids: - # TODO MAKE this configurable - DAILY_HOURS = 8 task = unit._get_timesheeting_task() + if self.current: + values["date"] = fields.Date.today() - if task.date_start and task.date_end: - task_start = task.date_start.date() - task_end = task.date_end.date() - - today = fields.Date.today() - # If this is the current week - if self.date_start <= today <= self.date_end: - # Take the closest day to today - values["date"] = max(min(today, task_end), task_start) - else: - # If start date is in week, take it - if self.date_start <= task_start <= self.date_end: - values["date"] = task_start - - values["unit_amount"] = min(DAILY_HOURS, unit.hours) + values["unit_amount"] = 0 values["project_id"] = task.project_id.id values["task_id"] = task.id - self.timesheet_ids |= self.env["account.analytic.line"]._sheet_create( - values + values["workload_unit_id"] = unit.id + return self.env["account.analytic.line"]._sheet_create(values) + + @api.model + def _prepare_new_line(self, line): + # We need to check if the new line is similar to a worload unit + # If it is, we need to link it to the workload unit + vals = super()._prepare_new_line(line) + # Yeah, using the same function for 2 different things leads to this :/ + if line._name != "hr_timesheet.sheet.new.analytic.line": + return vals + timesheets = line.sheet_id.timesheet_ids + similar_timesheets = timesheets.filtered( + lambda t: t.project_id == line.project_id and t.task_id == line.task_id + ) + if not similar_timesheets: + return vals + + similar_workload_units = similar_timesheets.mapped("workload_unit_id") + vals["workload_unit_id"] = ( + similar_workload_units.ids[0] if similar_workload_units else False + ) + return vals + + def action_timesheet_done(self): + self.ensure_one() + super().action_timesheet_done() + + next_week = week_name(self.date_start + timedelta(days=7)) + next_week_units = self.env["project.workload.unit"].search( + [ + ("week", "=", next_week), + ("user_id", "=", self.user_id.id), + ], + ) + + unfinished_units = self.workload_unit_ids.filtered(lambda u: not u.done) + for unit in unfinished_units: + next_week_unit = next_week_units.filtered( + lambda u: u.project_id == unit.project_id + and u.task_id == unit.task_id + and u.workload_id == unit.workload_id ) - return True + + # But we report the remaining hours to the next week + # We also report if the remaining hours are negative + # It will decrease the next week workload + # But we don't create a unit in this case + if not next_week_unit and unit.remaining_hours > 0: + # And we create a new unit if it does not exist + # Even if the unit could be after end_date for now + next_week_unit = self.env["project.workload.unit"].create( + { + "task_id": unit.task_id.id, + "workload_id": unit.workload_id.id, + "week": next_week, + "hours": unit.remaining_hours, + } + ) + else: + next_week_unit.hours += unit.remaining_hours + + # The unit are now done so the unit hours are the timesheeted hours + unit.hours = unit.timesheeted_hours diff --git a/project_workload_timesheet/models/project_workload_unit.py b/project_workload_timesheet/models/project_workload_unit.py index ef093c306..020bf7020 100644 --- a/project_workload_timesheet/models/project_workload_unit.py +++ b/project_workload_timesheet/models/project_workload_unit.py @@ -2,22 +2,89 @@ # @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import api, fields, models class ProjectWorkloadUnit(models.Model): _inherit = "project.workload.unit" - sheet_id = fields.Many2one("hr_timesheet.sheet") + timesheet_ids = fields.One2many( + "account.analytic.line", + "workload_unit_id", + "Timesheets", + help="The timesheets (normally one) in which the workload is timesheeted", + ) priority = fields.Selection(related="task_id.priority") - def action_add(self): + timesheeted_hours = fields.Float( + "Timesheeted Hours", + compute="_compute_timesheeted_hours", + help="The hours timesheeted on this workload", + ) + remaining_hours = fields.Float( + "Remaining Hours", + compute="_compute_remaining_hours", + help="The remaining hours to timesheet on this workload (can be negative)", + ) + progress = fields.Float( + "Progress", + compute="_compute_progress", + help="The progress of the task", + ) + done = fields.Boolean( + "Done", + ) + task_stage_id = fields.Many2one( + related="task_id.stage_id", string="Task Stage", readonly=False + ) + + @api.depends("timesheet_ids.unit_amount") + def _compute_timesheeted_hours(self): + for record in self: + record.timesheeted_hours = sum(record.timesheet_ids.mapped("unit_amount")) + + @api.depends("hours", "timesheeted_hours") + def _compute_progress(self): + for record in self: + if record.hours: + record.progress = 100 * record.timesheeted_hours / record.hours + else: + record.progress = 0 + + @api.depends("hours", "timesheeted_hours", "done") + def _compute_remaining_hours(self): + for record in self: + if record.done: + record.remaining_hours = 0 + else: + record.remaining_hours = record.hours - record.timesheeted_hours + + def action_add_to_timesheet(self): sheet_id = self.env.context.get("current_sheet_id") if not sheet_id: return sheet = self.env["hr_timesheet.sheet"].browse(sheet_id) return sheet._add_line_from_unit(self) + def action_timesheet_time(self): + sheet_id = self.env.context.get("current_sheet_id") + if not sheet_id: + return + sheet = self.env["hr_timesheet.sheet"].browse(sheet_id) + if not sheet or not sheet.current: + return + time = self.env.context.get("time", 0) / 60 + + timesheet = self.timesheet_ids.filtered( + lambda t: t.date == fields.Date.today() + ) or sheet._add_line_from_unit(self) + timesheet.unit_amount += time + return True + + def action_timesheet_done(self): + self.done = True + pass + def _get_timesheeting_task(self): # For overrides return self.task_id diff --git a/project_workload_timesheet/views/hr_timesheet_sheet_views.xml b/project_workload_timesheet/views/hr_timesheet_sheet_views.xml index c19b3b84f..1dbfa2f13 100644 --- a/project_workload_timesheet/views/hr_timesheet_sheet_views.xml +++ b/project_workload_timesheet/views/hr_timesheet_sheet_views.xml @@ -5,25 +5,137 @@
-
-
+ + + + + + +

Todo

- - - - - -
From ff9dd406bdab5820be4b766a638f6d3f15b7e542 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 15 Feb 2024 17:13:27 +0100 Subject: [PATCH 12/28] [IMP] project_workload_timesheet_additions: Adapt unit sheet lines relation for additional loads --- .../models/__init__.py | 1 + .../models/account_analytic_line.py | 38 +++++++++++++++++++ .../models/project_workload_unit.py | 2 + .../views/hr_timesheet_sheet_views.xml | 10 ++++- 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 project_workload_timesheet_additions/models/account_analytic_line.py diff --git a/project_workload_timesheet_additions/models/__init__.py b/project_workload_timesheet_additions/models/__init__.py index ef599bd60..bd28cf786 100644 --- a/project_workload_timesheet_additions/models/__init__.py +++ b/project_workload_timesheet_additions/models/__init__.py @@ -1,2 +1,3 @@ +from . import account_analytic_line from . import hr_timesheet_sheet from . import project_workload_unit diff --git a/project_workload_timesheet_additions/models/account_analytic_line.py b/project_workload_timesheet_additions/models/account_analytic_line.py new file mode 100644 index 000000000..e67dbabe9 --- /dev/null +++ b/project_workload_timesheet_additions/models/account_analytic_line.py @@ -0,0 +1,38 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + workload_unit_id = fields.Many2one( + comodel_name="project.workload.unit", + string="Workload Unit", + readonly=False, + compute="_compute_workload_unit_id", + store=True, + domain="[" + "('week', '=', week), " + "('user_id', '=', user_id), " + "('project_id', '=', project_id), " + "'|', " + "'&', ('additional_task_id', '=', False), ('task_id', '=', task_id), " + "('additional_task_id', '=', task_id)" + "]", + ) + + def _get_available_workload_units_domain(self): + return [ + ("week", "=", self.week), + ("user_id", "=", self.user_id.id), + ("project_id", "=", self.project_id.id), + "|", + "&", + ("additional_task_id", "=", False), + ("task_id", "=", self.task_id.id), + ("additional_task_id", "=", self.task_id.id), + ] diff --git a/project_workload_timesheet_additions/models/project_workload_unit.py b/project_workload_timesheet_additions/models/project_workload_unit.py index c793b2b8b..584c25096 100644 --- a/project_workload_timesheet_additions/models/project_workload_unit.py +++ b/project_workload_timesheet_additions/models/project_workload_unit.py @@ -11,11 +11,13 @@ class ProjectWorkloadUnit(models.Model): "project.task.workload.addition", "Additional Task Workload", related="workload_id.additional_workload_id", + store=True, ) additional_task_id = fields.Many2one( "project.task", "Additional Task", related="workload_id.additional_workload_task_id", + store=True, ) def _get_timesheeting_task(self): diff --git a/project_workload_timesheet_additions/views/hr_timesheet_sheet_views.xml b/project_workload_timesheet_additions/views/hr_timesheet_sheet_views.xml index 09c7d84b9..a7cc82bcc 100644 --- a/project_workload_timesheet_additions/views/hr_timesheet_sheet_views.xml +++ b/project_workload_timesheet_additions/views/hr_timesheet_sheet_views.xml @@ -2,9 +2,15 @@ hr_timesheet.sheet - + - + From 341f1c8870f7c4261cfaeb6131b5c8f6c0bd6e46 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 5 Mar 2024 12:08:37 +0100 Subject: [PATCH 13/28] [ADD] project_workload_milestone --- project_workload_milestone/__init__.py | 1 + project_workload_milestone/__manifest__.py | 22 +++++++++ project_workload_milestone/models/__init__.py | 2 + .../models/project_milestone.py | 49 +++++++++++++++++++ .../models/project_task.py | 27 ++++++++++ .../views/project_milestone.xml | 21 ++++++++ .../odoo/addons/project_workload_milestone | 1 + setup/project_workload_milestone/setup.py | 6 +++ 8 files changed, 129 insertions(+) create mode 100644 project_workload_milestone/__init__.py create mode 100644 project_workload_milestone/__manifest__.py create mode 100644 project_workload_milestone/models/__init__.py create mode 100644 project_workload_milestone/models/project_milestone.py create mode 100644 project_workload_milestone/models/project_task.py create mode 100644 project_workload_milestone/views/project_milestone.xml create mode 120000 setup/project_workload_milestone/odoo/addons/project_workload_milestone create mode 100644 setup/project_workload_milestone/setup.py diff --git a/project_workload_milestone/__init__.py b/project_workload_milestone/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/project_workload_milestone/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_workload_milestone/__manifest__.py b/project_workload_milestone/__manifest__.py new file mode 100644 index 000000000..f662e3cdc --- /dev/null +++ b/project_workload_milestone/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Project Workload Milestone", + "summary": "Add Milestone Support to Project Workload", + "version": "14.0.1.0.0", + "development_status": "Alpha", + "category": "Uncategorized", + "website": "https://github.com/akretion/ak-odoo-incubator", + "author": " Akretion", + "license": "AGPL-3", + "depends": [ + "project_workload", + "project_milestone", + ], + "data": [ + "views/project_milestone.xml", + ], + "auto_install": True, +} diff --git a/project_workload_milestone/models/__init__.py b/project_workload_milestone/models/__init__.py new file mode 100644 index 000000000..fed98971a --- /dev/null +++ b/project_workload_milestone/models/__init__.py @@ -0,0 +1,2 @@ +from . import project_milestone +from . import project_task diff --git a/project_workload_milestone/models/project_milestone.py b/project_workload_milestone/models/project_milestone.py new file mode 100644 index 000000000..c1e3617a1 --- /dev/null +++ b/project_workload_milestone/models/project_milestone.py @@ -0,0 +1,49 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ProjectMilestone(models.Model): + _inherit = "project.milestone" + + start_date = fields.Date( + string="Start Date", + help="The date when the Milestone should start.", + compute="_compute_milestone_start_date", + store=True, + readonly=False, + ) + + @api.constrains("start_date", "target_date") + def _check_start_date(self): + for milestone in self: + if milestone.target_date < milestone.start_date: + raise ValidationError( + _("The end date cannot be earlier than the start date.") + ) + + @api.depends("target_date", "project_id") + def _compute_milestone_start_date(self): + for record in self: + if record.start_date: + continue + if not record.target_date: + record.start_date = False + continue + + # Use a simple algorithm to find the start date + # Find previous milestone + previous_milestones = self.search( + [ + ("project_id", "=", record.project_id.id), + ("target_date", "<", record.target_date), + ], + order="target_date desc", + limit=1, + ) + # The start date will be the end date of the previous milestone + record.start_date = previous_milestones.target_date + timedelta(days=1) diff --git a/project_workload_milestone/models/project_task.py b/project_workload_milestone/models/project_task.py new file mode 100644 index 000000000..b88475cd1 --- /dev/null +++ b/project_workload_milestone/models/project_task.py @@ -0,0 +1,27 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class ProjectTask(models.Model): + _inherit = "project.task" + + def _prepare_task_dates_vals_from_milestone(self, vals): + # If we create a task with a milestone or affect it after, + # we set the task dates to the milestone dates + + if "milestone_id" in vals: + milestone = self.env["project.milestone"].browse(vals["milestone_id"]) + if milestone: + vals["date_end"] = milestone.target_date + vals["date_start"] = milestone.start_date + return vals + + @api.model + def create(self, vals): + return super().create(self._prepare_task_dates_vals_from_milestone(vals)) + + def write(self, vals): + return super().write(self._prepare_task_dates_vals_from_milestone(vals)) diff --git a/project_workload_milestone/views/project_milestone.xml b/project_workload_milestone/views/project_milestone.xml new file mode 100644 index 000000000..a0fb1614b --- /dev/null +++ b/project_workload_milestone/views/project_milestone.xml @@ -0,0 +1,21 @@ + + + + + + project.milestone + + + + + + + + + + + + + diff --git a/setup/project_workload_milestone/odoo/addons/project_workload_milestone b/setup/project_workload_milestone/odoo/addons/project_workload_milestone new file mode 120000 index 000000000..e908b1d04 --- /dev/null +++ b/setup/project_workload_milestone/odoo/addons/project_workload_milestone @@ -0,0 +1 @@ +../../../../project_workload_milestone \ No newline at end of file diff --git a/setup/project_workload_milestone/setup.py b/setup/project_workload_milestone/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/project_workload_milestone/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 552b59aa0d7e010f861af8fea0465d1617759bef Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 5 Mar 2024 16:29:33 +0100 Subject: [PATCH 14/28] [LINT] --- project_workload/models/project_task_workload.py | 2 +- project_workload_additions/models/project_task.py | 3 ++- project_workload_additions/models/project_workload_unit.py | 3 ++- .../views/project_task_workload_view.xml | 5 ++++- project_workload_capacity/__manifest__.py | 1 + project_workload_milestone/models/project_milestone.py | 3 ++- project_workload_milestone/views/project_milestone.xml | 1 + project_workload_timesheet/models/account_analytic_line.py | 4 +--- project_workload_timesheet/models/project_workload_unit.py | 1 - .../views/hr_timesheet_sheet_views.xml | 2 +- .../models/account_analytic_line.py | 3 +-- .../models/hr_timesheet_sheet.py | 2 +- .../odoo/addons/project_workload_timesheet_additions | 1 + setup/project_workload_timesheet_additions/setup.py | 6 ++++++ 14 files changed, 24 insertions(+), 13 deletions(-) create mode 120000 setup/project_workload_timesheet_additions/odoo/addons/project_workload_timesheet_additions create mode 100644 setup/project_workload_timesheet_additions/setup.py diff --git a/project_workload/models/project_task_workload.py b/project_workload/models/project_task_workload.py index 1d6d43c17..d7ae440bf 100644 --- a/project_workload/models/project_task_workload.py +++ b/project_workload/models/project_task_workload.py @@ -17,7 +17,7 @@ def week_name(value): return None -week_merge_re = re.compile((r"(\d{4})-(\d{2}) - (\1)-(\d{2})")) +week_merge_re = re.compile(r"(\d{4})-(\d{2}) - (\1)-(\d{2})") class ProjectTaskWorkload(models.Model): diff --git a/project_workload_additions/models/project_task.py b/project_workload_additions/models/project_task.py index 9017f98ef..35827668a 100644 --- a/project_workload_additions/models/project_task.py +++ b/project_workload_additions/models/project_task.py @@ -31,7 +31,8 @@ def _get_new_workloads(self): return rv def _get_updated_workloads(self): - # We sort the workloads by additional_workload_id to ensure that the first workload is the main one + # We sort the workloads by additional_workload_id to ensure that the + # first workload is the main one self.workload_ids = self.workload_ids.sorted( key=lambda w: w.additional_workload_id ) diff --git a/project_workload_additions/models/project_workload_unit.py b/project_workload_additions/models/project_workload_unit.py index 105fb25d4..34e21f896 100644 --- a/project_workload_additions/models/project_workload_unit.py +++ b/project_workload_additions/models/project_workload_unit.py @@ -15,7 +15,8 @@ def name_get(self): for unit_id, name in result: unit = self.browse(unit_id) if unit.workload_id.additional_workload_id: - name = f"{unit.workload_id.additional_workload_id.task_id.name} {_('of')} {name}" + name = f"{unit.workload_id.additional_workload_id.task_id.name} " + f"{_('of')} {name}" units_names[unit_id] = name return list(units_names.items()) diff --git a/project_workload_additions/views/project_task_workload_view.xml b/project_workload_additions/views/project_task_workload_view.xml index e3727b55f..d16b68992 100644 --- a/project_workload_additions/views/project_task_workload_view.xml +++ b/project_workload_additions/views/project_task_workload_view.xml @@ -3,7 +3,10 @@ project.task.workload - + diff --git a/project_workload_capacity/__manifest__.py b/project_workload_capacity/__manifest__.py index 2cf75ca32..a35466481 100644 --- a/project_workload_capacity/__manifest__.py +++ b/project_workload_capacity/__manifest__.py @@ -23,4 +23,5 @@ "views/project_load_capacity_report_view.xml", "views/menu_view.xml", ], + "installable": False, } diff --git a/project_workload_milestone/models/project_milestone.py b/project_workload_milestone/models/project_milestone.py index c1e3617a1..a1346e5ea 100644 --- a/project_workload_milestone/models/project_milestone.py +++ b/project_workload_milestone/models/project_milestone.py @@ -46,4 +46,5 @@ def _compute_milestone_start_date(self): limit=1, ) # The start date will be the end date of the previous milestone - record.start_date = previous_milestones.target_date + timedelta(days=1) + if previous_milestones.target_date: + record.start_date = previous_milestones.target_date + timedelta(days=1) diff --git a/project_workload_milestone/views/project_milestone.xml b/project_workload_milestone/views/project_milestone.xml index a0fb1614b..c3e7220c5 100644 --- a/project_workload_milestone/views/project_milestone.xml +++ b/project_workload_milestone/views/project_milestone.xml @@ -7,6 +7,7 @@ project.milestone + diff --git a/project_workload_timesheet/models/account_analytic_line.py b/project_workload_timesheet/models/account_analytic_line.py index c378f66fd..11fbd1826 100644 --- a/project_workload_timesheet/models/account_analytic_line.py +++ b/project_workload_timesheet/models/account_analytic_line.py @@ -2,9 +2,7 @@ # @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import _, api, fields, models -from odoo.exceptions import UserError, ValidationError - +from odoo import api, fields, models from odoo.addons.project_workload.models.project_task_workload import week_name diff --git a/project_workload_timesheet/models/project_workload_unit.py b/project_workload_timesheet/models/project_workload_unit.py index 020bf7020..3a8f39b51 100644 --- a/project_workload_timesheet/models/project_workload_unit.py +++ b/project_workload_timesheet/models/project_workload_unit.py @@ -83,7 +83,6 @@ def action_timesheet_time(self): def action_timesheet_done(self): self.done = True - pass def _get_timesheeting_task(self): # For overrides diff --git a/project_workload_timesheet/views/hr_timesheet_sheet_views.xml b/project_workload_timesheet/views/hr_timesheet_sheet_views.xml index 1dbfa2f13..5a95f632e 100644 --- a/project_workload_timesheet/views/hr_timesheet_sheet_views.xml +++ b/project_workload_timesheet/views/hr_timesheet_sheet_views.xml @@ -74,7 +74,7 @@ widget="progressbar" optional="show" /> - + # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import _, api, fields, models -from odoo.exceptions import UserError, ValidationError +from odoo import fields, models class AccountAnalyticLine(models.Model): diff --git a/project_workload_timesheet_additions/models/hr_timesheet_sheet.py b/project_workload_timesheet_additions/models/hr_timesheet_sheet.py index b9bcd8b40..34aa71be9 100644 --- a/project_workload_timesheet_additions/models/hr_timesheet_sheet.py +++ b/project_workload_timesheet_additions/models/hr_timesheet_sheet.py @@ -1,7 +1,7 @@ # Copyright 2024 Akretion (https://www.akretion.com). # @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import models class Sheet(models.Model): diff --git a/setup/project_workload_timesheet_additions/odoo/addons/project_workload_timesheet_additions b/setup/project_workload_timesheet_additions/odoo/addons/project_workload_timesheet_additions new file mode 120000 index 000000000..b6e9a6a5c --- /dev/null +++ b/setup/project_workload_timesheet_additions/odoo/addons/project_workload_timesheet_additions @@ -0,0 +1 @@ +../../../../project_workload_timesheet_additions \ No newline at end of file diff --git a/setup/project_workload_timesheet_additions/setup.py b/setup/project_workload_timesheet_additions/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/project_workload_timesheet_additions/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From e3077dbff5a08e166e295d5878ea01cbe611082a Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 30 Sep 2024 14:19:59 +0200 Subject: [PATCH 15/28] [MIG] project_workload: Migration to 16.0 --- project_workload/README.rst | 74 +++ project_workload/__manifest__.py | 2 +- project_workload/models/project_task.py | 32 +- .../static/description/index.html | 431 ++++++++++++++++++ project_workload/tests/common.py | 16 +- project_workload/tests/test_workload.py | 31 +- .../views/project_project_view.xml | 14 +- project_workload_additions/__manifest__.py | 2 +- .../models/project_task.py | 25 +- .../views/project_project_view.xml | 37 +- project_workload_capacity/__manifest__.py | 2 +- project_workload_milestone/__manifest__.py | 3 +- .../models/project_milestone.py | 20 +- .../models/project_task.py | 2 +- .../views/project_milestone.xml | 8 +- project_workload_timesheet/__manifest__.py | 2 +- .../models/project_workload_unit.py | 9 +- .../__manifest__.py | 2 +- .../odoo/addons/project_workload_capacity | 1 - setup/project_workload_capacity/setup.py | 6 - 20 files changed, 624 insertions(+), 95 deletions(-) create mode 100644 project_workload/README.rst create mode 100644 project_workload/static/description/index.html delete mode 120000 setup/project_workload_capacity/odoo/addons/project_workload_capacity delete mode 100644 setup/project_workload_capacity/setup.py diff --git a/project_workload/README.rst b/project_workload/README.rst new file mode 100644 index 000000000..edc4e6725 --- /dev/null +++ b/project_workload/README.rst @@ -0,0 +1,74 @@ +================ +Project Workload +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4a176ece35168ce42257a5e9a7415205d47c52fdf0e5c5ebcd5986afc7b180c7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-akretion%2Fak--odoo--incubator-lightgray.png?logo=github + :target: https://github.com/akretion/ak-odoo-incubator/tree/16.0/project_workload + :alt: akretion/ak-odoo-incubator + +|badge1| |badge2| |badge3| + +This module allow to manage load and capacity by project and cross project +Load is managed by week + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Known issues / Roadmap +====================== + +TODO +- in case of manual workload assignation add a check on date + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier + +Maintainers +~~~~~~~~~~~ + +This module is part of the `akretion/ak-odoo-incubator `_ project on GitHub. + +You are welcome to contribute. diff --git a/project_workload/__manifest__.py b/project_workload/__manifest__.py index 8b4d7f559..219226cdd 100644 --- a/project_workload/__manifest__.py +++ b/project_workload/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Project Workload", "summary": "Ressource Workload Management", - "version": "14.0.1.0.0", + "version": "16.0.1.0.0", "development_status": "Alpha", "category": "Uncategorized", "website": "https://github.com/akretion/ak-odoo-incubator", diff --git a/project_workload/models/project_task.py b/project_workload/models/project_task.py index 63234e1e3..2056fbfd3 100644 --- a/project_workload/models/project_task.py +++ b/project_workload/models/project_task.py @@ -28,7 +28,7 @@ class ProjectTask(models.Model): "date_start", "date_end", "planned_hours", - "user_id", + "user_ids", "config_workload_manually", "use_workload", ) @@ -44,12 +44,12 @@ def _compute_workload_ids(self): continue record.workload_ids = record._get_workload_sync() - def _prepare_workload(self, **extra): + def _prepare_workload(self, user, **extra): return { "date_start": self.date_start, "date_end": self.date_end, "hours": self.planned_hours, - "user_id": self.user_id.id, + "user_id": user.id, **extra, } @@ -66,24 +66,26 @@ def _get_workload_sync(self): def _get_new_workloads(self): self.ensure_one() - # Handle only one workload in automatic - if not self.workload_ids: - return [self._prepare_workload()] - return [] + # Handle only one workload per user in automatic + new_vals = [] + for user in self.user_ids: + user_workload = self.workload_ids.filtered(lambda w: w.user_id == user) + if not user_workload: + new_vals.append(self._prepare_workload(user)) + + return new_vals def _get_updated_workloads(self): self.ensure_one() - # Remove other workloads and update the first workload values - if self.workload_ids: - return [(self.workload_ids[0], self._prepare_workload())] - return [] + # Update the users workload values + for workload in self.workload_ids: + if workload.user_id in self.user_ids: + yield workload, self._prepare_workload(workload.user_id) def _get_obsolete_workloads(self): self.ensure_one() - # Remove other workloads and update the first workload values - if len(self.workload_ids) > 1: - return self.workload_ids[1:] - return [] + # Remove other workloads + return self.workload_ids.filtered(lambda w: w.user_id not in self.user_ids) @api.depends("workload_ids.unit_ids") def _compute_workload_unit_ids(self): diff --git a/project_workload/static/description/index.html b/project_workload/static/description/index.html new file mode 100644 index 000000000..32f90189b --- /dev/null +++ b/project_workload/static/description/index.html @@ -0,0 +1,431 @@ + + + + + +Project Workload + + + +
+

Project Workload

+ + +

Alpha License: AGPL-3 akretion/ak-odoo-incubator

+

This module allow to manage load and capacity by project and cross project +Load is managed by week

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Known issues / Roadmap

+

TODO +- in case of manual workload assignation add a check on date

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the akretion/ak-odoo-incubator project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/project_workload/tests/common.py b/project_workload/tests/common.py index b0bf317ca..b96bf3c79 100644 --- a/project_workload/tests/common.py +++ b/project_workload/tests/common.py @@ -3,15 +3,14 @@ # @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from datetime import datetime, timedelta from freezegun import freeze_time -from odoo.tests import SavepointCase +from odoo.tests import TransactionCase @freeze_time("2023-07-24") -class TestWorkloadCommon(SavepointCase): +class TestWorkloadCommon(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -30,14 +29,3 @@ def setUpClass(cls): "model_id": "project.project", } ) - now = datetime.now() - cls.task = cls.env["project.task"].create( - { - "name": "Task 1", - "project_id": cls.project.id, - "user_id": cls.user_1.id, - "date_start": now, - "date_end": now + timedelta(days=20), - "planned_hours": 21, - } - ) diff --git a/project_workload/tests/test_workload.py b/project_workload/tests/test_workload.py index 97618f8b7..409582814 100644 --- a/project_workload/tests/test_workload.py +++ b/project_workload/tests/test_workload.py @@ -3,13 +3,27 @@ # @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from datetime import timedelta +from datetime import datetime, timedelta from .common import TestWorkloadCommon class TestWorkload(TestWorkloadCommon): + def _create_task(self): + now = datetime.now() + self.task = self.env["project.task"].create( + { + "name": "Task 1", + "project_id": self.project.id, + "user_ids": [(4, self.user_1.id)], + "date_start": now, + "date_end": now + timedelta(days=20), + "planned_hours": 21, + } + ) + def test_task_assign_with_hours(self): + self._create_task() workload = self.task.workload_ids self.assertEqual(len(workload), 1) self.assertEqual(workload.date_start, self.task.date_start.date()) @@ -21,11 +35,24 @@ def test_task_assign_with_hours(self): self.assertEqual(set(load_unit.mapped("hours")), {7}) def test_change_user(self): - self.task.user_id = self.user_2 + self._create_task() + self.task.user_ids = [(6, 0, [self.user_2.id])] self.assertEqual(self.task.workload_ids.user_id, self.user_2) self.assertEqual(self.task.workload_ids.unit_ids.user_id, self.user_2) + def test_add_user(self): + self._create_task() + self.task.user_ids = [(4, self.user_2.id)] + self.assertEqual(len(self.task.workload_ids), 2) + self.assertEqual( + self.task.workload_ids.mapped("user_id"), self.user_1 + self.user_2 + ) + self.assertEqual( + self.task.workload_ids.unit_ids.mapped("user_id"), self.user_1 + self.user_2 + ) + def test_change_date(self): + self._create_task() self.task.date_end = self.task.date_start + timedelta(days=13) workload = self.task.workload_ids self.assertEqual(len(workload), 1) diff --git a/project_workload/views/project_project_view.xml b/project_workload/views/project_project_view.xml index d7937d9bc..6d4e1104a 100644 --- a/project_workload/views/project_project_view.xml +++ b/project_workload/views/project_project_view.xml @@ -5,12 +5,14 @@ project.project -
-
- -
-
+ + + + +
diff --git a/project_workload_additions/__manifest__.py b/project_workload_additions/__manifest__.py index 4b594f3d3..31c5ecb0b 100644 --- a/project_workload_additions/__manifest__.py +++ b/project_workload_additions/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Project Workload Additions", "summary": "Automatically add extra load to tasks.", - "version": "14.0.1.0.0", + "version": "16.0.1.0.0", "development_status": "Alpha", "category": "Uncategorized", "website": "https://github.com/akretion/ak-odoo-incubator", diff --git a/project_workload_additions/models/project_task.py b/project_workload_additions/models/project_task.py index 35827668a..ed73ea37c 100644 --- a/project_workload_additions/models/project_task.py +++ b/project_workload_additions/models/project_task.py @@ -13,10 +13,12 @@ def _get_new_workloads(self): # super creates a new workload if there are none # but here we can have only additional workloads # so we also need to check if the existing workloads are additional - if not rv and all( - workload.additional_workload_id for workload in self.workload_ids - ): - rv.append(self._prepare_workload()) + for user in self.user_ids: + if not [vals for vals in rv if vals["user_id"] == user.id] and all( + workload.additional_workload_id + for workload in self.workload_ids.filtered(lambda w: w.user_id == user) + ): + rv.append(self._prepare_workload(user)) additional_workloads = { workload.additional_workload_id: workload @@ -37,8 +39,12 @@ def _get_updated_workloads(self): key=lambda w: w.additional_workload_id ) rv = super()._get_updated_workloads() - if rv and rv[0][0].additional_workload_id: - rv = [] + # Remove additional workloads from the list + rv = [ + (workload, vals) + for workload, vals in rv + if not workload.additional_workload_id + ] additional_workloads = { workload.additional_workload_id: workload @@ -72,14 +78,17 @@ def _get_obsolete_workloads(self): if workload.additional_workload_id } for additional_workload in additional_workloads: - if additional_workload not in self.project_id.additional_workload_ids: + if ( + additional_workload not in self.project_id.additional_workload_ids + or additional_workload.user_id not in self.user_ids + ): rv.append(additional_workload) return rv def _prepare_additional_workload(self, additional_workload, **extra): return self._prepare_workload( + additional_workload.user_id, additional_workload_id=additional_workload.id, hours=additional_workload._compute_hours_from_task(self), - user_id=additional_workload.user_id.id, **extra, ) diff --git a/project_workload_additions/views/project_project_view.xml b/project_workload_additions/views/project_project_view.xml index b2487ae14..1e1f3c344 100644 --- a/project_workload_additions/views/project_project_view.xml +++ b/project_workload_additions/views/project_project_view.xml @@ -5,28 +5,27 @@ project.project - - + 2 +
+ + - - - - - - - - - - - - + + + + + + + +
diff --git a/project_workload_capacity/__manifest__.py b/project_workload_capacity/__manifest__.py index a35466481..1b3d1bf18 100644 --- a/project_workload_capacity/__manifest__.py +++ b/project_workload_capacity/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Project Workload Capacity", "summary": "Ressource Workload Capacity Management", - "version": "14.0.1.0.0", + "version": "16.0.1.0.0", "development_status": "Alpha", "category": "Uncategorized", "website": "https://github.com/akretion/ak-odoo-incubator", diff --git a/project_workload_milestone/__manifest__.py b/project_workload_milestone/__manifest__.py index f662e3cdc..e037be2a2 100644 --- a/project_workload_milestone/__manifest__.py +++ b/project_workload_milestone/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Project Workload Milestone", "summary": "Add Milestone Support to Project Workload", - "version": "14.0.1.0.0", + "version": "16.0.1.0.0", "development_status": "Alpha", "category": "Uncategorized", "website": "https://github.com/akretion/ak-odoo-incubator", @@ -13,7 +13,6 @@ "license": "AGPL-3", "depends": [ "project_workload", - "project_milestone", ], "data": [ "views/project_milestone.xml", diff --git a/project_workload_milestone/models/project_milestone.py b/project_workload_milestone/models/project_milestone.py index a1346e5ea..ac880d87b 100644 --- a/project_workload_milestone/models/project_milestone.py +++ b/project_workload_milestone/models/project_milestone.py @@ -18,20 +18,24 @@ class ProjectMilestone(models.Model): readonly=False, ) - @api.constrains("start_date", "target_date") + @api.constrains("start_date", "deadline") def _check_start_date(self): for milestone in self: - if milestone.target_date < milestone.start_date: + if ( + milestone.deadline + and milestone.start_date + and milestone.deadline < milestone.start_date + ): raise ValidationError( _("The end date cannot be earlier than the start date.") ) - @api.depends("target_date", "project_id") + @api.depends("deadline", "project_id") def _compute_milestone_start_date(self): for record in self: if record.start_date: continue - if not record.target_date: + if not record.deadline: record.start_date = False continue @@ -40,11 +44,11 @@ def _compute_milestone_start_date(self): previous_milestones = self.search( [ ("project_id", "=", record.project_id.id), - ("target_date", "<", record.target_date), + ("deadline", "<", record.deadline), ], - order="target_date desc", + order="deadline desc", limit=1, ) # The start date will be the end date of the previous milestone - if previous_milestones.target_date: - record.start_date = previous_milestones.target_date + timedelta(days=1) + if previous_milestones.deadline: + record.start_date = previous_milestones.deadline + timedelta(days=1) diff --git a/project_workload_milestone/models/project_task.py b/project_workload_milestone/models/project_task.py index b88475cd1..ab1549ac6 100644 --- a/project_workload_milestone/models/project_task.py +++ b/project_workload_milestone/models/project_task.py @@ -15,7 +15,7 @@ def _prepare_task_dates_vals_from_milestone(self, vals): if "milestone_id" in vals: milestone = self.env["project.milestone"].browse(vals["milestone_id"]) if milestone: - vals["date_end"] = milestone.target_date + vals["date_end"] = milestone.deadline vals["date_start"] = milestone.start_date return vals diff --git a/project_workload_milestone/views/project_milestone.xml b/project_workload_milestone/views/project_milestone.xml index c3e7220c5..b159bfc39 100644 --- a/project_workload_milestone/views/project_milestone.xml +++ b/project_workload_milestone/views/project_milestone.xml @@ -6,14 +6,10 @@ project.milestone - + - - - - - + diff --git a/project_workload_timesheet/__manifest__.py b/project_workload_timesheet/__manifest__.py index 4b90aa6a8..4aabe81b5 100644 --- a/project_workload_timesheet/__manifest__.py +++ b/project_workload_timesheet/__manifest__.py @@ -6,7 +6,7 @@ { "name": "Project Workload Timesheet", "summary": "Add Workload To Timesheets", - "version": "14.0.1.0.0", + "version": "16.0.1.0.0", "development_status": "Alpha", "category": "Uncategorized", "website": "https://github.com/akretion/ak-odoo-incubator", diff --git a/project_workload_timesheet/models/project_workload_unit.py b/project_workload_timesheet/models/project_workload_unit.py index 3a8f39b51..ce414d2fc 100644 --- a/project_workload_timesheet/models/project_workload_unit.py +++ b/project_workload_timesheet/models/project_workload_unit.py @@ -75,9 +75,14 @@ def action_timesheet_time(self): return time = self.env.context.get("time", 0) / 60 + # Add only on lines without names timesheet = self.timesheet_ids.filtered( - lambda t: t.date == fields.Date.today() - ) or sheet._add_line_from_unit(self) + lambda t: t.date == fields.Date.today() and t.name == "/" + ) + if len(timesheet) > 1: + timesheet = timesheet[0] + if not timesheet: + timesheet = sheet._add_line_from_unit(self) timesheet.unit_amount += time return True diff --git a/project_workload_timesheet_additions/__manifest__.py b/project_workload_timesheet_additions/__manifest__.py index 2682ddf42..2456ac5fb 100644 --- a/project_workload_timesheet_additions/__manifest__.py +++ b/project_workload_timesheet_additions/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Project Workload Timesheet Additions", "summary": "Add Workload Additions To Timesheets", - "version": "14.0.1.0.0", + "version": "16.0.1.0.0", "development_status": "Alpha", "category": "Uncategorized", "website": "https://github.com/akretion/ak-odoo-incubator", diff --git a/setup/project_workload_capacity/odoo/addons/project_workload_capacity b/setup/project_workload_capacity/odoo/addons/project_workload_capacity deleted file mode 120000 index b1a940739..000000000 --- a/setup/project_workload_capacity/odoo/addons/project_workload_capacity +++ /dev/null @@ -1 +0,0 @@ -../../../../project_workload_capacity \ No newline at end of file diff --git a/setup/project_workload_capacity/setup.py b/setup/project_workload_capacity/setup.py deleted file mode 100644 index 28c57bb64..000000000 --- a/setup/project_workload_capacity/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -import setuptools - -setuptools.setup( - setup_requires=['setuptools-odoo'], - odoo_addon=True, -) From 987442baee0341bb6f81d09517f91391c843dc56 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 13:15:54 +0200 Subject: [PATCH 16/28] [FIX] project_workload: project.task date_start, date_end renames See: https://github.com/OCA/project/commit/55460f8ff563d436fe22c27f62e186e09088e78d --- project_workload/models/project_task.py | 12 +++++++----- project_workload/models/project_task_workload.py | 4 ++-- project_workload/tests/test_workload.py | 14 +++++++------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/project_workload/models/project_task.py b/project_workload/models/project_task.py index 2056fbfd3..ffc47481c 100644 --- a/project_workload/models/project_task.py +++ b/project_workload/models/project_task.py @@ -25,8 +25,8 @@ class ProjectTask(models.Model): config_workload_manually = fields.Boolean() @api.depends( - "date_start", - "date_end", + "planned_date_start", + "planned_date_end", "planned_hours", "user_ids", "config_workload_manually", @@ -39,15 +39,17 @@ def _compute_workload_ids(self): # Handle only automatic config in planned task if record.config_workload_manually or not ( - record.date_start and record.date_end and record.planned_hours + record.planned_date_start + and record.planned_date_end + and record.planned_hours ): continue record.workload_ids = record._get_workload_sync() def _prepare_workload(self, user, **extra): return { - "date_start": self.date_start, - "date_end": self.date_end, + "date_start": self.planned_date_start, + "date_end": self.planned_date_end, "hours": self.planned_hours, "user_id": user.id, **extra, diff --git a/project_workload/models/project_task_workload.py b/project_workload/models/project_task_workload.py index d7ae440bf..71dc72e80 100644 --- a/project_workload/models/project_task_workload.py +++ b/project_workload/models/project_task_workload.py @@ -42,8 +42,8 @@ class ProjectTaskWorkload(models.Model): @api.constrains("date_start", "date_end") def _check_end_date(self): - for task in self: - if task.date_end < task.date_start: + for load in self: + if load.date_end < load.date_start: raise ValidationError( _("The end date cannot be earlier than the start date.") ) diff --git a/project_workload/tests/test_workload.py b/project_workload/tests/test_workload.py index 409582814..7eaae9f5c 100644 --- a/project_workload/tests/test_workload.py +++ b/project_workload/tests/test_workload.py @@ -16,8 +16,8 @@ def _create_task(self): "name": "Task 1", "project_id": self.project.id, "user_ids": [(4, self.user_1.id)], - "date_start": now, - "date_end": now + timedelta(days=20), + "planned_date_start": now, + "planned_date_end": now + timedelta(days=20), "planned_hours": 21, } ) @@ -26,8 +26,8 @@ def test_task_assign_with_hours(self): self._create_task() workload = self.task.workload_ids self.assertEqual(len(workload), 1) - self.assertEqual(workload.date_start, self.task.date_start.date()) - self.assertEqual(workload.date_end, self.task.date_end.date()) + self.assertEqual(workload.date_start, self.task.planned_date_start.date()) + self.assertEqual(workload.date_end, self.task.planned_date_end.date()) self.assertEqual(workload.hours, 21) load_unit = workload.unit_ids self.assertEqual(len(load_unit), 3) @@ -53,11 +53,11 @@ def test_add_user(self): def test_change_date(self): self._create_task() - self.task.date_end = self.task.date_start + timedelta(days=13) + self.task.planned_date_end = self.task.planned_date_start + timedelta(days=13) workload = self.task.workload_ids self.assertEqual(len(workload), 1) - self.assertEqual(workload.date_start, self.task.date_start.date()) - self.assertEqual(workload.date_end, self.task.date_end.date()) + self.assertEqual(workload.date_start, self.task.planned_date_start.date()) + self.assertEqual(workload.date_end, self.task.planned_date_end.date()) self.assertEqual(workload.hours, 21) load_unit = workload.unit_ids self.assertEqual(len(load_unit), 2) From f28badf02cb3fbe1f0779f55a488ef6e4de33518 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 13:16:02 +0200 Subject: [PATCH 17/28] [FIX] project_workload_milestone: project.task date_start, date_end renames See: https://github.com/OCA/project/commit/55460f8ff563d436fe22c27f62e186e09088e78d --- project_workload_milestone/models/project_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project_workload_milestone/models/project_task.py b/project_workload_milestone/models/project_task.py index ab1549ac6..efc86d327 100644 --- a/project_workload_milestone/models/project_task.py +++ b/project_workload_milestone/models/project_task.py @@ -15,8 +15,8 @@ def _prepare_task_dates_vals_from_milestone(self, vals): if "milestone_id" in vals: milestone = self.env["project.milestone"].browse(vals["milestone_id"]) if milestone: - vals["date_end"] = milestone.deadline - vals["date_start"] = milestone.start_date + vals["planned_date_end"] = milestone.deadline + vals["planned_date_start"] = milestone.start_date return vals @api.model From 910d18daba34d0c20b1ec0e4efaa6180ad2e1768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Tue, 9 Jul 2024 12:07:53 +0200 Subject: [PATCH 18/28] project_workload: several fix and simplify code - fix removing user - fix creating a task without user - simplify code by introducing a hook "_get_main_workload" - improve test coverage --- project_workload/models/project_task.py | 24 ++-- .../models/project_task_workload.py | 9 +- project_workload/tests/test_workload.py | 7 +- .../models/project_task.py | 75 +++---------- .../models/project_task_workload_addition.py | 25 ++++- project_workload_additions/tests/__init__.py | 1 + .../tests/test_workload_addition.py | 105 ++++++++++++++++++ .../views/project_project_view.xml | 2 +- .../models/project_task.py | 33 +++--- 9 files changed, 191 insertions(+), 90 deletions(-) create mode 100644 project_workload_additions/tests/__init__.py create mode 100644 project_workload_additions/tests/test_workload_addition.py diff --git a/project_workload/models/project_task.py b/project_workload/models/project_task.py index ffc47481c..80f8ac165 100644 --- a/project_workload/models/project_task.py +++ b/project_workload/models/project_task.py @@ -60,18 +60,22 @@ def _get_workload_sync(self): return [ *[(0, 0, vals) for vals in self._get_new_workloads()], *[ - (1, workload_id.id, vals) - for workload_id, vals in self._get_updated_workloads() + (1, workload.id, vals) + for workload, vals in self._get_updated_workloads() ], - *[(2, workload_id.id) for workload_id in self._get_obsolete_workloads()], + *[(2, workload.id) for workload in self._get_obsolete_workloads()], ] + def _get_main_workloads(self): + return self.workload_ids + def _get_new_workloads(self): self.ensure_one() + workloads = self._get_main_workloads() # Handle only one workload per user in automatic new_vals = [] for user in self.user_ids: - user_workload = self.workload_ids.filtered(lambda w: w.user_id == user) + user_workload = workloads.filtered(lambda w: w.user_id == user) if not user_workload: new_vals.append(self._prepare_workload(user)) @@ -79,15 +83,19 @@ def _get_new_workloads(self): def _get_updated_workloads(self): self.ensure_one() + workloads = self._get_main_workloads() + vals = [] # Update the users workload values - for workload in self.workload_ids: + for workload in workloads: if workload.user_id in self.user_ids: - yield workload, self._prepare_workload(workload.user_id) + vals.append((workload, self._prepare_workload(workload.user_id))) + return vals def _get_obsolete_workloads(self): self.ensure_one() - # Remove other workloads - return self.workload_ids.filtered(lambda w: w.user_id not in self.user_ids) + workloads = self._get_main_workloads() + # Remove other workloads, dedup for users? + return workloads.filtered(lambda w: w.user_id not in self.user_ids) @api.depends("workload_ids.unit_ids") def _compute_workload_unit_ids(self): diff --git a/project_workload/models/project_task_workload.py b/project_workload/models/project_task_workload.py index 71dc72e80..3256dac09 100644 --- a/project_workload/models/project_task_workload.py +++ b/project_workload/models/project_task_workload.py @@ -48,7 +48,7 @@ def _check_end_date(self): _("The end date cannot be earlier than the start date.") ) - @api.depends("date_start", "date_end", "hours") + @api.depends("date_start", "date_end", "hours", "user_id") def _compute_unit_ids(self): for record in self: # We need to have the data to compute the unit (this happens at create) @@ -71,6 +71,13 @@ def _compute_unit_ids(self): 0, 0, { + # We have to set here the value of user_id + # as related field user_id will be not computed + # The "project.workload" are created from the computed + # field "workload_ids" and odoo do not play the compute + # field when there are trigered inside a create + # that come from a compute + "user_id": record.user_id.id, "hours": hours, "week": week, }, diff --git a/project_workload/tests/test_workload.py b/project_workload/tests/test_workload.py index 7eaae9f5c..dd4afe4d2 100644 --- a/project_workload/tests/test_workload.py +++ b/project_workload/tests/test_workload.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta -from .common import TestWorkloadCommon +from odoo.addons.project_workload.tests.common import TestWorkloadCommon class TestWorkload(TestWorkloadCommon): @@ -51,6 +51,11 @@ def test_add_user(self): self.task.workload_ids.unit_ids.mapped("user_id"), self.user_1 + self.user_2 ) + def test_remove_user(self): + self._create_task() + self.task.user_ids = [(5, 0, 0)] + self.assertFalse(self.task.workload_ids) + def test_change_date(self): self._create_task() self.task.planned_date_end = self.task.planned_date_start + timedelta(days=13) diff --git a/project_workload_additions/models/project_task.py b/project_workload_additions/models/project_task.py index ed73ea37c..3f143684e 100644 --- a/project_workload_additions/models/project_task.py +++ b/project_workload_additions/models/project_task.py @@ -8,77 +8,36 @@ class ProjectTask(models.Model): _inherit = "project.task" + def _get_main_workloads(self): + return ( + super() + ._get_main_workloads() + .filtered(lambda s: not s.additional_workload_id) + ) + def _get_new_workloads(self): rv = super()._get_new_workloads() - # super creates a new workload if there are none - # but here we can have only additional workloads - # so we also need to check if the existing workloads are additional - for user in self.user_ids: - if not [vals for vals in rv if vals["user_id"] == user.id] and all( - workload.additional_workload_id - for workload in self.workload_ids.filtered(lambda w: w.user_id == user) - ): - rv.append(self._prepare_workload(user)) - - additional_workloads = { - workload.additional_workload_id: workload - for workload in self.workload_ids - if workload.additional_workload_id - } - # Now we need to create a new workload for each additional workload + # We need to create a new workload for each additional workload for additional_workload in self.project_id.additional_workload_ids: - if additional_workload not in additional_workloads: + if additional_workload not in self.workload_ids.additional_workload_id: rv.append(self._prepare_additional_workload(additional_workload)) - return rv def _get_updated_workloads(self): - # We sort the workloads by additional_workload_id to ensure that the - # first workload is the main one - self.workload_ids = self.workload_ids.sorted( - key=lambda w: w.additional_workload_id - ) rv = super()._get_updated_workloads() - # Remove additional workloads from the list - rv = [ - (workload, vals) - for workload, vals in rv - if not workload.additional_workload_id - ] - - additional_workloads = { - workload.additional_workload_id: workload - for workload in self.workload_ids - if workload.additional_workload_id - } - # Now we need to update the existing workload for each additional workload - for additional_workload, workload in additional_workloads.items(): - rv.append( - ( - workload, - self._prepare_additional_workload(additional_workload), + for workload in self.workload_ids: + additional_workload = workload.additional_workload_id + if additional_workload in self.project_id.additional_workload_ids: + rv.append( + (workload, self._prepare_additional_workload(additional_workload)) ) - ) - return rv def _get_obsolete_workloads(self): - self.workload_ids = self.workload_ids.sorted( - key=lambda w: w.additional_workload_id - ) rv = super()._get_obsolete_workloads() - if rv: - # Do not delete additional workloads - rv = [workload for workload in rv if not workload.additional_workload_id] - - # Remove all additional workloads that are not in the project anymore - additional_workloads = { - workload.additional_workload_id: workload - for workload in self.workload_ids - if workload.additional_workload_id - } - for additional_workload in additional_workloads: - if ( + for workload in self.workload_ids: + additional_workload = workload.additional_workload_id + if additional_workload and ( additional_workload not in self.project_id.additional_workload_ids or additional_workload.user_id not in self.user_ids ): diff --git a/project_workload_additions/models/project_task_workload_addition.py b/project_workload_additions/models/project_task_workload_addition.py index 9f6927810..aeefeb566 100644 --- a/project_workload_additions/models/project_task_workload_addition.py +++ b/project_workload_additions/models/project_task_workload_addition.py @@ -10,19 +10,36 @@ class ProjectWorkloadAddition(models.Model): _description = "Project Task Workload Addition" project_id = fields.Many2one("project.project", string="Project", required=True) - type = fields.Many2one( + type_id = fields.Many2one( "project.task.workload.addition.type", string="Addition Type", required=True ) percentage = fields.Float( required=True, string="Added Percentage", + store=True, + compute="_compute_percentage", + readonly=False, ) user_id = fields.Many2one("res.users", string="User", required=True) task_id = fields.Many2one("project.task", string="Task", required=True) - @api.onchange("type") - def _onchange_type(self): - self.percentage = self.type.default_percentage + @api.model_create_multi + def create(self, list_vals): + # TODO remove on next version when computed field are run before the create + # for now we have to do it manually as the field is required + for vals in list_vals: + if not vals.get("percentage") and vals.get("type_id"): + vals["percentage"] = ( + self.env["project.task.workload.addition.type"] + .browse(vals["type_id"]) + .default_percentage + ) + return super().create(list_vals) + + @api.depends("type_id") + def _compute_percentage(self): + for record in self: + record.percentage = record.type_id.default_percentage def _compute_hours_from_task(self, task): self.ensure_one() diff --git a/project_workload_additions/tests/__init__.py b/project_workload_additions/tests/__init__.py new file mode 100644 index 000000000..faed57e51 --- /dev/null +++ b/project_workload_additions/tests/__init__.py @@ -0,0 +1 @@ +from . import test_workload_addition diff --git a/project_workload_additions/tests/test_workload_addition.py b/project_workload_additions/tests/test_workload_addition.py new file mode 100644 index 000000000..e37292b5a --- /dev/null +++ b/project_workload_additions/tests/test_workload_addition.py @@ -0,0 +1,105 @@ +# Copyright 2024 Akretion (https://www.akretion.com). +# @author Sébastien BEAU +# @author Florian Mounier +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo.addons.project_workload.tests.common import TestWorkloadCommon + + +class TestWorkload(TestWorkloadCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.task_management, cls.task_review = cls.env["project.task"].create( + [ + {"name": "Management", "project_id": cls.project.id}, + {"name": "Review", "project_id": cls.project.id}, + ] + ) + cls.type_management, cls.type_review = cls.env[ + "project.task.workload.addition.type" + ].create( + [ + {"name": "Management", "default_percentage": 15}, + {"name": "Review", "default_percentage": 20}, + ] + ) + cls.add_work_management, cls.add_work_review = cls.env[ + "project.task.workload.addition" + ].create( + [ + { + "project_id": cls.project.id, + "type_id": cls.type_management.id, + "user_id": cls.user_1.id, + "task_id": cls.task_management.id, + }, + { + "project_id": cls.project.id, + "type_id": cls.type_review.id, + "user_id": cls.user_2.id, + "task_id": cls.task_review.id, + }, + ] + ) + + def _create_task(self, user_id=None): + now = datetime.now() + return self.env["project.task"].create( + { + "name": "Task 1", + "project_id": self.project.id, + "user_id": user_id, + "date_start": now, + "date_end": now + timedelta(days=20), + "planned_hours": 21, + } + ) + + def test_task_assign_with_hours(self): + task = self._create_task(self.user_1.id) + workloads = task.workload_ids + + self.assertEqual(len(workloads), 3) + worload, workload_management, workload_review = workloads + + self.assertEqual(workload_management.date_start, task.date_start.date()) + self.assertEqual(workload_management.date_end, task.date_end.date()) + self.assertEqual(workload_management.hours, 3.15) + self.assertEqual(workload_management.user_id, self.user_1) + self.assertEqual( + workload_management.additional_workload_id, self.add_work_management + ) + + self.assertEqual(workload_review.date_start, task.date_start.date()) + self.assertEqual(workload_review.date_end, task.date_end.date()) + self.assertEqual(workload_review.hours, 4.2) + self.assertEqual(workload_review.user_id, self.user_2) + self.assertEqual(workload_review.additional_workload_id, self.add_work_review) + + def _assert_only_additionnal_workload(self, workloads): + self.assertEqual(len(workloads), 2) + workload_management, workload_review = workloads + self.assertEqual( + workload_management.additional_workload_id, self.add_work_management + ) + self.assertEqual(workload_review.additional_workload_id, self.add_work_review) + + def test_create_unasign_task(self): + task = self._create_task(None) + self._assert_only_additionnal_workload(task.workload_ids) + + def test_remove_user(self): + task = self._create_task(self.user_1.id) + task.user_id = False + self._assert_only_additionnal_workload(task.workload_ids) + + def test_update_hours(self): + task = self._create_task(self.user_1.id) + task.planned_hours = 42 + self.assertEqual(len(task.workload_ids), 3) + worload, workload_management, workload_review = task.workload_ids + self.assertEqual(workload_management.hours, 6.30) + self.assertEqual(workload_review.hours, 8.4) diff --git a/project_workload_additions/views/project_project_view.xml b/project_workload_additions/views/project_project_view.xml index 1e1f3c344..b84080a2d 100644 --- a/project_workload_additions/views/project_project_view.xml +++ b/project_workload_additions/views/project_project_view.xml @@ -15,7 +15,7 @@ attrs="{'invisible': [('use_workload', '=', False)]}" > - + # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, models +from odoo import api, fields, models class ProjectTask(models.Model): _inherit = "project.task" - def _prepare_task_dates_vals_from_milestone(self, vals): - # If we create a task with a milestone or affect it after, - # we set the task dates to the milestone dates + planned_date_start = fields.Datetime( + compute="_compute_planned_date_start_end", + store=True, + readonly=False, + ) + planned_date_end = fields.Datetime( + compute="_compute_planned_date_start_end", + store=True, + readonly=False, + ) - if "milestone_id" in vals: - milestone = self.env["project.milestone"].browse(vals["milestone_id"]) - if milestone: - vals["planned_date_end"] = milestone.deadline - vals["planned_date_start"] = milestone.start_date - return vals - - @api.model - def create(self, vals): - return super().create(self._prepare_task_dates_vals_from_milestone(vals)) - - def write(self, vals): - return super().write(self._prepare_task_dates_vals_from_milestone(vals)) + @api.depends("milestone_id.start_date", "milestone_id.target_date") + def _compute_planned_date_start_end(self): + for record in self: + record.planned_date_end = record.milestone_id.target_date + record.planned_date_start = record.milestone_id.start_date From 5b5623e2f205933d67a50518018c04390687ed42 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 17:20:56 +0200 Subject: [PATCH 19/28] [IMP] project_workload: Make tests coherent with additions tests --- project_workload/tests/test_workload.py | 46 ++++++++++++------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/project_workload/tests/test_workload.py b/project_workload/tests/test_workload.py index dd4afe4d2..f3a0bbbff 100644 --- a/project_workload/tests/test_workload.py +++ b/project_workload/tests/test_workload.py @@ -11,7 +11,7 @@ class TestWorkload(TestWorkloadCommon): def _create_task(self): now = datetime.now() - self.task = self.env["project.task"].create( + return self.env["project.task"].create( { "name": "Task 1", "project_id": self.project.id, @@ -23,11 +23,11 @@ def _create_task(self): ) def test_task_assign_with_hours(self): - self._create_task() - workload = self.task.workload_ids + task = self._create_task() + workload = task.workload_ids self.assertEqual(len(workload), 1) - self.assertEqual(workload.date_start, self.task.planned_date_start.date()) - self.assertEqual(workload.date_end, self.task.planned_date_end.date()) + self.assertEqual(workload.date_start, task.planned_date_start.date()) + self.assertEqual(workload.date_end, task.planned_date_end.date()) self.assertEqual(workload.hours, 21) load_unit = workload.unit_ids self.assertEqual(len(load_unit), 3) @@ -35,34 +35,32 @@ def test_task_assign_with_hours(self): self.assertEqual(set(load_unit.mapped("hours")), {7}) def test_change_user(self): - self._create_task() - self.task.user_ids = [(6, 0, [self.user_2.id])] - self.assertEqual(self.task.workload_ids.user_id, self.user_2) - self.assertEqual(self.task.workload_ids.unit_ids.user_id, self.user_2) + task = self._create_task() + task.user_ids = [(6, 0, [self.user_2.id])] + self.assertEqual(task.workload_ids.user_id, self.user_2) + self.assertEqual(task.workload_ids.unit_ids.user_id, self.user_2) def test_add_user(self): - self._create_task() - self.task.user_ids = [(4, self.user_2.id)] - self.assertEqual(len(self.task.workload_ids), 2) + task = self._create_task() + task.user_ids = [(4, self.user_2.id)] + self.assertEqual(len(task.workload_ids), 2) + self.assertEqual(task.workload_ids.mapped("user_id"), self.user_1 + self.user_2) self.assertEqual( - self.task.workload_ids.mapped("user_id"), self.user_1 + self.user_2 - ) - self.assertEqual( - self.task.workload_ids.unit_ids.mapped("user_id"), self.user_1 + self.user_2 + task.workload_ids.unit_ids.mapped("user_id"), self.user_1 + self.user_2 ) def test_remove_user(self): - self._create_task() - self.task.user_ids = [(5, 0, 0)] - self.assertFalse(self.task.workload_ids) + task = self._create_task() + task.user_ids = [(5, 0, 0)] + self.assertFalse(task.workload_ids) def test_change_date(self): - self._create_task() - self.task.planned_date_end = self.task.planned_date_start + timedelta(days=13) - workload = self.task.workload_ids + task = self._create_task() + task.planned_date_end = task.planned_date_start + timedelta(days=13) + workload = task.workload_ids self.assertEqual(len(workload), 1) - self.assertEqual(workload.date_start, self.task.planned_date_start.date()) - self.assertEqual(workload.date_end, self.task.planned_date_end.date()) + self.assertEqual(workload.date_start, task.planned_date_start.date()) + self.assertEqual(workload.date_end, task.planned_date_end.date()) self.assertEqual(workload.hours, 21) load_unit = workload.unit_ids self.assertEqual(len(load_unit), 2) From 38b7412b41d44f93dcedcdf1583987090415e914 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 17:21:45 +0200 Subject: [PATCH 20/28] [IMP] project_workload_additions: Add multi users tests --- .../tests/test_workload_addition.py | 79 +++++++++++++++---- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/project_workload_additions/tests/test_workload_addition.py b/project_workload_additions/tests/test_workload_addition.py index e37292b5a..6e4e92bad 100644 --- a/project_workload_additions/tests/test_workload_addition.py +++ b/project_workload_additions/tests/test_workload_addition.py @@ -45,36 +45,41 @@ def setUpClass(cls): ] ) - def _create_task(self, user_id=None): + def _create_task(self, user_ids=None): now = datetime.now() return self.env["project.task"].create( { "name": "Task 1", "project_id": self.project.id, - "user_id": user_id, - "date_start": now, - "date_end": now + timedelta(days=20), + "user_ids": [(6, 0, user_ids.ids)] if user_ids else [], + "planned_date_start": now, + "planned_date_end": now + timedelta(days=20), "planned_hours": 21, } ) def test_task_assign_with_hours(self): - task = self._create_task(self.user_1.id) + task = self._create_task(self.user_1) workloads = task.workload_ids self.assertEqual(len(workloads), 3) - worload, workload_management, workload_review = workloads + workload, workload_management, workload_review = workloads - self.assertEqual(workload_management.date_start, task.date_start.date()) - self.assertEqual(workload_management.date_end, task.date_end.date()) + self.assertEqual(workload.date_start, task.planned_date_start.date()) + self.assertEqual(workload.date_end, task.planned_date_end.date()) + self.assertEqual(workload.user_id, self.user_1) + self.assertFalse(workload.additional_workload_id) + + self.assertEqual(workload_management.date_start, task.planned_date_start.date()) + self.assertEqual(workload_management.date_end, task.planned_date_end.date()) self.assertEqual(workload_management.hours, 3.15) self.assertEqual(workload_management.user_id, self.user_1) self.assertEqual( workload_management.additional_workload_id, self.add_work_management ) - self.assertEqual(workload_review.date_start, task.date_start.date()) - self.assertEqual(workload_review.date_end, task.date_end.date()) + self.assertEqual(workload_review.date_start, task.planned_date_start.date()) + self.assertEqual(workload_review.date_end, task.planned_date_end.date()) self.assertEqual(workload_review.hours, 4.2) self.assertEqual(workload_review.user_id, self.user_2) self.assertEqual(workload_review.additional_workload_id, self.add_work_review) @@ -88,18 +93,62 @@ def _assert_only_additionnal_workload(self, workloads): self.assertEqual(workload_review.additional_workload_id, self.add_work_review) def test_create_unasign_task(self): - task = self._create_task(None) + task = self._create_task() self._assert_only_additionnal_workload(task.workload_ids) def test_remove_user(self): - task = self._create_task(self.user_1.id) - task.user_id = False + task = self._create_task(self.user_1) + task.user_ids = [(5, 0, 0)] self._assert_only_additionnal_workload(task.workload_ids) def test_update_hours(self): - task = self._create_task(self.user_1.id) + task = self._create_task(self.user_1) task.planned_hours = 42 self.assertEqual(len(task.workload_ids), 3) - worload, workload_management, workload_review = task.workload_ids + workload, workload_management, workload_review = task.workload_ids self.assertEqual(workload_management.hours, 6.30) self.assertEqual(workload_review.hours, 8.4) + + def test_task_assign_with_hours_and_multiple_users(self): + task = self._create_task(self.user_1 | self.user_2) + workloads = task.workload_ids + + self.assertEqual(len(workloads), 4) + ( + workload_user_1, + workload_user_2, + workload_management_user_1, + workload_review_user_2, + ) = workloads + + self.assertEqual(workload_user_1.date_start, task.planned_date_start.date()) + self.assertEqual(workload_user_1.date_end, task.planned_date_end.date()) + self.assertEqual(workload_user_1.user_id, self.user_1) + self.assertFalse(workload_user_1.additional_workload_id) + + self.assertEqual(workload_user_2.date_start, task.planned_date_start.date()) + self.assertEqual(workload_user_2.date_end, task.planned_date_end.date()) + self.assertEqual(workload_user_2.user_id, self.user_2) + self.assertFalse(workload_user_2.additional_workload_id) + + self.assertEqual( + workload_management_user_1.date_start, task.planned_date_start.date() + ) + self.assertEqual( + workload_management_user_1.date_end, task.planned_date_end.date() + ) + self.assertEqual(workload_management_user_1.hours, 3.15) + self.assertEqual(workload_management_user_1.user_id, self.user_1) + self.assertEqual( + workload_management_user_1.additional_workload_id, self.add_work_management + ) + + self.assertEqual( + workload_review_user_2.date_start, task.planned_date_start.date() + ) + self.assertEqual(workload_review_user_2.date_end, task.planned_date_end.date()) + self.assertEqual(workload_review_user_2.hours, 4.2) + self.assertEqual(workload_review_user_2.user_id, self.user_2) + self.assertEqual( + workload_review_user_2.additional_workload_id, self.add_work_review + ) From ad2abecb8ee13f0e1b8680ef9a3692ead34e4be4 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 17:22:06 +0200 Subject: [PATCH 21/28] [FIX] project_workload_milestone: project.task date_start, date_end renames again --- project_workload_milestone/models/project_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project_workload_milestone/models/project_task.py b/project_workload_milestone/models/project_task.py index 2d0db41a9..4d09d0fb3 100644 --- a/project_workload_milestone/models/project_task.py +++ b/project_workload_milestone/models/project_task.py @@ -19,8 +19,8 @@ class ProjectTask(models.Model): readonly=False, ) - @api.depends("milestone_id.start_date", "milestone_id.target_date") + @api.depends("milestone_id.start_date", "milestone_id.deadline") def _compute_planned_date_start_end(self): for record in self: - record.planned_date_end = record.milestone_id.target_date + record.planned_date_end = record.milestone_id.deadline record.planned_date_start = record.milestone_id.start_date From beba60e3672bf4a1c9f5714fd3b111f644375e0c Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 17:22:21 +0200 Subject: [PATCH 22/28] [LINT] --- .pre-commit-config.yaml | 1 + project_workload_timesheet/models/hr_timesheet_sheet.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28713b266..879c2d07a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,7 @@ exclude: | (?x) # NOT INSTALLABLE ADDONS + ^project_workload_capacity/| # END NOT INSTALLABLE ADDONS # Files and folders generated by bots, to avoid loops ^setup/|/static/description/index\.html$| diff --git a/project_workload_timesheet/models/hr_timesheet_sheet.py b/project_workload_timesheet/models/hr_timesheet_sheet.py index 5848cdf02..ddde32e17 100644 --- a/project_workload_timesheet/models/hr_timesheet_sheet.py +++ b/project_workload_timesheet/models/hr_timesheet_sheet.py @@ -132,7 +132,7 @@ def _add_line_from_unit(self, unit): @api.model def _prepare_new_line(self, line): - # We need to check if the new line is similar to a worload unit + # We need to check if the new line is similar to a workload unit # If it is, we need to link it to the workload unit vals = super()._prepare_new_line(line) # Yeah, using the same function for 2 different things leads to this :/ From f07fc3beb2e4f01899042f4b826b1da15f877d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Mon, 23 Sep 2024 16:32:02 +0200 Subject: [PATCH 23/28] project_workload: add view to see load, automatically set done load when the task is closed --- project_workload/__manifest__.py | 1 + .../models/project_workload_unit.py | 17 ++++- project_workload/views/menu_view.xml | 8 +++ .../views/project_task_workload_unit_view.xml | 66 +++++++++++++++++++ .../models/project_workload_unit.py | 10 ++- 5 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 project_workload/views/project_task_workload_unit_view.xml diff --git a/project_workload/__manifest__.py b/project_workload/__manifest__.py index 219226cdd..01f39379f 100644 --- a/project_workload/__manifest__.py +++ b/project_workload/__manifest__.py @@ -21,6 +21,7 @@ "views/project_task_workload_view.xml", "views/project_task_view.xml", "views/project_project_view.xml", + "views/project_task_workload_unit_view.xml", "views/menu_view.xml", ], } diff --git a/project_workload/models/project_workload_unit.py b/project_workload/models/project_workload_unit.py index b5ed96445..d75a89919 100644 --- a/project_workload/models/project_workload_unit.py +++ b/project_workload/models/project_workload_unit.py @@ -3,7 +3,7 @@ # @author Florian Mounier # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import api, fields, models class ProjectWorkloadUnit(models.Model): @@ -18,8 +18,21 @@ class ProjectWorkloadUnit(models.Model): ) task_id = fields.Many2one("project.task", "Task", related="workload_id.task_id") project_id = fields.Many2one( - "project.project", "Project", related="workload_id.project_id" + "project.project", + "Project", + related="workload_id.project_id", + store=True, ) + done = fields.Boolean(compute="_compute_done", store=True) + + def is_done(self): + self.ensure_one() + return self.task_id.stage_id.is_closed + + @api.depends("task_id.stage_id.is_closed") + def _compute_done(self): + for record in self: + record.done = record.is_done() def name_get(self): result = [] diff --git a/project_workload/views/menu_view.xml b/project_workload/views/menu_view.xml index 7bf00df1b..cd58d3c83 100644 --- a/project_workload/views/menu_view.xml +++ b/project_workload/views/menu_view.xml @@ -9,4 +9,12 @@ groups="project_workload.group_project_workload" /> + +
diff --git a/project_workload/views/project_task_workload_unit_view.xml b/project_workload/views/project_task_workload_unit_view.xml new file mode 100644 index 000000000..f5628bda4 --- /dev/null +++ b/project_workload/views/project_task_workload_unit_view.xml @@ -0,0 +1,66 @@ + + + + + project.workload.unit + + + + + + + + + + + + + + project.workload.unit + + + + + + + + + + + + + + + + + Workload + ir.actions.act_window + project.workload.unit + tree + + [] + {'search_default_my_load': 1, 'search_default_groupby_week': 1} + + + diff --git a/project_workload_timesheet/models/project_workload_unit.py b/project_workload_timesheet/models/project_workload_unit.py index ce414d2fc..8147b3255 100644 --- a/project_workload_timesheet/models/project_workload_unit.py +++ b/project_workload_timesheet/models/project_workload_unit.py @@ -31,13 +31,17 @@ class ProjectWorkloadUnit(models.Model): compute="_compute_progress", help="The progress of the task", ) - done = fields.Boolean( - "Done", + force_done = fields.Boolean( + "Force Done", ) task_stage_id = fields.Many2one( related="task_id.stage_id", string="Task Stage", readonly=False ) + @api.depends("force_done") + def is_done(self): + return super().is_done() or self.force_done + @api.depends("timesheet_ids.unit_amount") def _compute_timesheeted_hours(self): for record in self: @@ -87,7 +91,7 @@ def action_timesheet_time(self): return True def action_timesheet_done(self): - self.done = True + self.force_done = True def _get_timesheeting_task(self): # For overrides From dd4e9dfa70527d4eefa33905d21682ec4f72f855 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 17:26:34 +0200 Subject: [PATCH 24/28] [FIX] project_workrload:Use project.task.is_closed --- project_workload/models/project_workload_unit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project_workload/models/project_workload_unit.py b/project_workload/models/project_workload_unit.py index d75a89919..85ee05c06 100644 --- a/project_workload/models/project_workload_unit.py +++ b/project_workload/models/project_workload_unit.py @@ -27,9 +27,9 @@ class ProjectWorkloadUnit(models.Model): def is_done(self): self.ensure_one() - return self.task_id.stage_id.is_closed + return self.task_id.is_closed - @api.depends("task_id.stage_id.is_closed") + @api.depends("task_id.is_closed") def _compute_done(self): for record in self: record.done = record.is_done() From e495ce017211ebfb4183a222975f9e774c517b9d Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 17:34:08 +0200 Subject: [PATCH 25/28] [FIX] project_workrload_additions: Fix obsolete additional workloads --- project_workload_additions/models/project_task.py | 4 +--- project_workload_additions/tests/test_workload_addition.py | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/project_workload_additions/models/project_task.py b/project_workload_additions/models/project_task.py index 3f143684e..319447d20 100644 --- a/project_workload_additions/models/project_task.py +++ b/project_workload_additions/models/project_task.py @@ -17,7 +17,6 @@ def _get_main_workloads(self): def _get_new_workloads(self): rv = super()._get_new_workloads() - # We need to create a new workload for each additional workload for additional_workload in self.project_id.additional_workload_ids: if additional_workload not in self.workload_ids.additional_workload_id: rv.append(self._prepare_additional_workload(additional_workload)) @@ -39,9 +38,8 @@ def _get_obsolete_workloads(self): additional_workload = workload.additional_workload_id if additional_workload and ( additional_workload not in self.project_id.additional_workload_ids - or additional_workload.user_id not in self.user_ids ): - rv.append(additional_workload) + rv |= workload return rv def _prepare_additional_workload(self, additional_workload, **extra): diff --git a/project_workload_additions/tests/test_workload_addition.py b/project_workload_additions/tests/test_workload_addition.py index 6e4e92bad..f3f3ef69c 100644 --- a/project_workload_additions/tests/test_workload_addition.py +++ b/project_workload_additions/tests/test_workload_addition.py @@ -101,6 +101,11 @@ def test_remove_user(self): task.user_ids = [(5, 0, 0)] self._assert_only_additionnal_workload(task.workload_ids) + def test_remove_users(self): + task = self._create_task(self.user_1 | self.user_2) + task.user_ids = [(5, 0, 0)] + self._assert_only_additionnal_workload(task.workload_ids) + def test_update_hours(self): task = self._create_task(self.user_1) task.planned_hours = 42 From 3b3ccca29f971436b799ab2fc7f029e0aa2650c6 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 17:36:53 +0200 Subject: [PATCH 26/28] [FIX] project_workrload_additions: Use precompute on percentage --- .../views/project_task_workload_unit_view.xml | 2 +- .../models/project_task_workload_addition.py | 14 +------------- .../project_task_workload_addition_type_views.xml | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/project_workload/views/project_task_workload_unit_view.xml b/project_workload/views/project_task_workload_unit_view.xml index f5628bda4..932aebe48 100644 --- a/project_workload/views/project_task_workload_unit_view.xml +++ b/project_workload/views/project_task_workload_unit_view.xml @@ -4,7 +4,7 @@ project.workload.unit - + diff --git a/project_workload_additions/models/project_task_workload_addition.py b/project_workload_additions/models/project_task_workload_addition.py index aeefeb566..fc50ab53e 100644 --- a/project_workload_additions/models/project_task_workload_addition.py +++ b/project_workload_additions/models/project_task_workload_addition.py @@ -19,23 +19,11 @@ class ProjectWorkloadAddition(models.Model): store=True, compute="_compute_percentage", readonly=False, + precompute=True, ) user_id = fields.Many2one("res.users", string="User", required=True) task_id = fields.Many2one("project.task", string="Task", required=True) - @api.model_create_multi - def create(self, list_vals): - # TODO remove on next version when computed field are run before the create - # for now we have to do it manually as the field is required - for vals in list_vals: - if not vals.get("percentage") and vals.get("type_id"): - vals["percentage"] = ( - self.env["project.task.workload.addition.type"] - .browse(vals["type_id"]) - .default_percentage - ) - return super().create(list_vals) - @api.depends("type_id") def _compute_percentage(self): for record in self: diff --git a/project_workload_additions/views/project_task_workload_addition_type_views.xml b/project_workload_additions/views/project_task_workload_addition_type_views.xml index d4193b688..b98a464fa 100644 --- a/project_workload_additions/views/project_task_workload_addition_type_views.xml +++ b/project_workload_additions/views/project_task_workload_addition_type_views.xml @@ -4,7 +4,7 @@ project.task.workload.addition.type - + From 48982c2304701d647a6461483632491b0049aec4 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 7 Oct 2024 17:48:35 +0200 Subject: [PATCH 27/28] [LINT] and documentation --- project_workload_additions/README.rst | 67 +++ .../models/project_task_workload_addition.py | 9 +- .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 424 ++++++++++++++++++ project_workload_milestone/README.rst | 67 +++ .../models/project_milestone.py | 1 - .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 424 ++++++++++++++++++ project_workload_timesheet/README.rst | 67 +++ .../models/account_analytic_line.py | 1 - .../models/hr_timesheet_sheet.py | 7 +- .../models/project_workload_unit.py | 7 +- .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 424 ++++++++++++++++++ .../README.rst | 67 +++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 424 ++++++++++++++++++ 21 files changed, 1989 insertions(+), 16 deletions(-) create mode 100644 project_workload_additions/README.rst create mode 100644 project_workload_additions/readme/CONTRIBUTORS.rst create mode 100644 project_workload_additions/readme/DESCRIPTION.rst create mode 100644 project_workload_additions/static/description/index.html create mode 100644 project_workload_milestone/README.rst create mode 100644 project_workload_milestone/readme/CONTRIBUTORS.rst create mode 100644 project_workload_milestone/readme/DESCRIPTION.rst create mode 100644 project_workload_milestone/static/description/index.html create mode 100644 project_workload_timesheet/README.rst create mode 100644 project_workload_timesheet/readme/CONTRIBUTORS.rst create mode 100644 project_workload_timesheet/readme/DESCRIPTION.rst create mode 100644 project_workload_timesheet/static/description/index.html create mode 100644 project_workload_timesheet_additions/README.rst create mode 100644 project_workload_timesheet_additions/readme/CONTRIBUTORS.rst create mode 100644 project_workload_timesheet_additions/readme/DESCRIPTION.rst create mode 100644 project_workload_timesheet_additions/static/description/index.html diff --git a/project_workload_additions/README.rst b/project_workload_additions/README.rst new file mode 100644 index 000000000..0c0898cef --- /dev/null +++ b/project_workload_additions/README.rst @@ -0,0 +1,67 @@ +========================== +Project Workload Additions +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f160d5c351f92880cfcdf26467249bc9d536b0509cb5f7b8a93c574eeac84591 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-akretion%2Fak--odoo--incubator-lightgray.png?logo=github + :target: https://github.com/akretion/ak-odoo-incubator/tree/16.0/project_workload_additions + :alt: akretion/ak-odoo-incubator + +|badge1| |badge2| |badge3| + +This module adds additional workloads in tasks. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier + +Maintainers +~~~~~~~~~~~ + +This module is part of the `akretion/ak-odoo-incubator `_ project on GitHub. + +You are welcome to contribute. diff --git a/project_workload_additions/models/project_task_workload_addition.py b/project_workload_additions/models/project_task_workload_addition.py index fc50ab53e..75652c0cc 100644 --- a/project_workload_additions/models/project_task_workload_addition.py +++ b/project_workload_additions/models/project_task_workload_addition.py @@ -39,10 +39,11 @@ def name_get(self): result.append( ( record.id, - _( - "%s additional workload (%d%%)" - % (record.task_id.name, record.percentage) - ), + _("%(task)s additional workload (%(percentage)d%%)") + % { + "task": record.task_id.name, + "percentage": record.percentage, + }, ) ) return result diff --git a/project_workload_additions/readme/CONTRIBUTORS.rst b/project_workload_additions/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..292f457ef --- /dev/null +++ b/project_workload_additions/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier diff --git a/project_workload_additions/readme/DESCRIPTION.rst b/project_workload_additions/readme/DESCRIPTION.rst new file mode 100644 index 000000000..fb3e4465e --- /dev/null +++ b/project_workload_additions/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds additional workloads in tasks. diff --git a/project_workload_additions/static/description/index.html b/project_workload_additions/static/description/index.html new file mode 100644 index 000000000..531b235ae --- /dev/null +++ b/project_workload_additions/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Project Workload Additions + + + +
+

Project Workload Additions

+ + +

Alpha License: AGPL-3 akretion/ak-odoo-incubator

+

This module adds additional workloads in tasks.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the akretion/ak-odoo-incubator project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/project_workload_milestone/README.rst b/project_workload_milestone/README.rst new file mode 100644 index 000000000..159682569 --- /dev/null +++ b/project_workload_milestone/README.rst @@ -0,0 +1,67 @@ +========================== +Project Workload Milestone +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:963931779fa5135c8ec9245b45c21251fe06d687dc681bec0bea93a13ff80f36 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-akretion%2Fak--odoo--incubator-lightgray.png?logo=github + :target: https://github.com/akretion/ak-odoo-incubator/tree/16.0/project_workload_milestone + :alt: akretion/ak-odoo-incubator + +|badge1| |badge2| |badge3| + +This module makes tasks inherit milestones start and end dates. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier + +Maintainers +~~~~~~~~~~~ + +This module is part of the `akretion/ak-odoo-incubator `_ project on GitHub. + +You are welcome to contribute. diff --git a/project_workload_milestone/models/project_milestone.py b/project_workload_milestone/models/project_milestone.py index ac880d87b..2013baf88 100644 --- a/project_workload_milestone/models/project_milestone.py +++ b/project_workload_milestone/models/project_milestone.py @@ -11,7 +11,6 @@ class ProjectMilestone(models.Model): _inherit = "project.milestone" start_date = fields.Date( - string="Start Date", help="The date when the Milestone should start.", compute="_compute_milestone_start_date", store=True, diff --git a/project_workload_milestone/readme/CONTRIBUTORS.rst b/project_workload_milestone/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..292f457ef --- /dev/null +++ b/project_workload_milestone/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier diff --git a/project_workload_milestone/readme/DESCRIPTION.rst b/project_workload_milestone/readme/DESCRIPTION.rst new file mode 100644 index 000000000..9aaf02904 --- /dev/null +++ b/project_workload_milestone/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module makes tasks inherit milestones start and end dates. diff --git a/project_workload_milestone/static/description/index.html b/project_workload_milestone/static/description/index.html new file mode 100644 index 000000000..8ab79a4bc --- /dev/null +++ b/project_workload_milestone/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Project Workload Milestone + + + +
+

Project Workload Milestone

+ + +

Alpha License: AGPL-3 akretion/ak-odoo-incubator

+

This module makes tasks inherit milestones start and end dates.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the akretion/ak-odoo-incubator project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/project_workload_timesheet/README.rst b/project_workload_timesheet/README.rst new file mode 100644 index 000000000..34c36503f --- /dev/null +++ b/project_workload_timesheet/README.rst @@ -0,0 +1,67 @@ +========================== +Project Workload Timesheet +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b5ef5651db27343b28f51a36a2f4861a7bb7140f293ac5b3700c16853bf1354a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-akretion%2Fak--odoo--incubator-lightgray.png?logo=github + :target: https://github.com/akretion/ak-odoo-incubator/tree/16.0/project_workload_timesheet + :alt: akretion/ak-odoo-incubator + +|badge1| |badge2| |badge3| + +This module adds workloads directly in timesheets. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier + +Maintainers +~~~~~~~~~~~ + +This module is part of the `akretion/ak-odoo-incubator `_ project on GitHub. + +You are welcome to contribute. diff --git a/project_workload_timesheet/models/account_analytic_line.py b/project_workload_timesheet/models/account_analytic_line.py index 11fbd1826..9fe1cc639 100644 --- a/project_workload_timesheet/models/account_analytic_line.py +++ b/project_workload_timesheet/models/account_analytic_line.py @@ -25,7 +25,6 @@ class AccountAnalyticLine(models.Model): ) week = fields.Char( - string="Week", compute="_compute_week", help="Week number of the year", ) diff --git a/project_workload_timesheet/models/hr_timesheet_sheet.py b/project_workload_timesheet/models/hr_timesheet_sheet.py index ddde32e17..cc812ef55 100644 --- a/project_workload_timesheet/models/hr_timesheet_sheet.py +++ b/project_workload_timesheet/models/hr_timesheet_sheet.py @@ -20,18 +20,15 @@ class Sheet(models.Model): ) next_week_load = fields.Float( - "Next Week Load", compute="_compute_next_week_load", help="The workload of the next week", ) next_week_units_count = fields.Integer( - "Next Week Units Count", compute="_compute_next_week_load", help="The number of workload units of the next week", ) current = fields.Boolean( - "Current", compute="_compute_current", help="Is this the current timesheet", ) @@ -153,7 +150,7 @@ def _prepare_new_line(self, line): def action_timesheet_done(self): self.ensure_one() - super().action_timesheet_done() + rv = super().action_timesheet_done() next_week = week_name(self.date_start + timedelta(days=7)) next_week_units = self.env["project.workload.unit"].search( @@ -191,3 +188,5 @@ def action_timesheet_done(self): # The unit are now done so the unit hours are the timesheeted hours unit.hours = unit.timesheeted_hours + + return rv diff --git a/project_workload_timesheet/models/project_workload_unit.py b/project_workload_timesheet/models/project_workload_unit.py index 8147b3255..6d568d798 100644 --- a/project_workload_timesheet/models/project_workload_unit.py +++ b/project_workload_timesheet/models/project_workload_unit.py @@ -17,23 +17,18 @@ class ProjectWorkloadUnit(models.Model): priority = fields.Selection(related="task_id.priority") timesheeted_hours = fields.Float( - "Timesheeted Hours", compute="_compute_timesheeted_hours", help="The hours timesheeted on this workload", ) remaining_hours = fields.Float( - "Remaining Hours", compute="_compute_remaining_hours", help="The remaining hours to timesheet on this workload (can be negative)", ) progress = fields.Float( - "Progress", compute="_compute_progress", help="The progress of the task", ) - force_done = fields.Boolean( - "Force Done", - ) + force_done = fields.Boolean() task_stage_id = fields.Many2one( related="task_id.stage_id", string="Task Stage", readonly=False ) diff --git a/project_workload_timesheet/readme/CONTRIBUTORS.rst b/project_workload_timesheet/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..292f457ef --- /dev/null +++ b/project_workload_timesheet/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier diff --git a/project_workload_timesheet/readme/DESCRIPTION.rst b/project_workload_timesheet/readme/DESCRIPTION.rst new file mode 100644 index 000000000..6245425c8 --- /dev/null +++ b/project_workload_timesheet/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds workloads directly in timesheets. diff --git a/project_workload_timesheet/static/description/index.html b/project_workload_timesheet/static/description/index.html new file mode 100644 index 000000000..8d017e2c2 --- /dev/null +++ b/project_workload_timesheet/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Project Workload Timesheet + + + +
+

Project Workload Timesheet

+ + +

Alpha License: AGPL-3 akretion/ak-odoo-incubator

+

This module adds workloads directly in timesheets.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the akretion/ak-odoo-incubator project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/project_workload_timesheet_additions/README.rst b/project_workload_timesheet_additions/README.rst new file mode 100644 index 000000000..7ec95bb6b --- /dev/null +++ b/project_workload_timesheet_additions/README.rst @@ -0,0 +1,67 @@ +==================================== +Project Workload Timesheet Additions +==================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:97cfdb2d70cb1fe62e8ab5f97743010162073b756b494792ce81a0ed541583b5 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-akretion%2Fak--odoo--incubator-lightgray.png?logo=github + :target: https://github.com/akretion/ak-odoo-incubator/tree/16.0/project_workload_timesheet_additions + :alt: akretion/ak-odoo-incubator + +|badge1| |badge2| |badge3| + +This module adds additional workloads support in timesheets. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier + +Maintainers +~~~~~~~~~~~ + +This module is part of the `akretion/ak-odoo-incubator `_ project on GitHub. + +You are welcome to contribute. diff --git a/project_workload_timesheet_additions/readme/CONTRIBUTORS.rst b/project_workload_timesheet_additions/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..292f457ef --- /dev/null +++ b/project_workload_timesheet_additions/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Akretion `_: + * BEAU Sébastien + * Florian Mounier diff --git a/project_workload_timesheet_additions/readme/DESCRIPTION.rst b/project_workload_timesheet_additions/readme/DESCRIPTION.rst new file mode 100644 index 000000000..b8dc49f02 --- /dev/null +++ b/project_workload_timesheet_additions/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds additional workloads support in timesheets. diff --git a/project_workload_timesheet_additions/static/description/index.html b/project_workload_timesheet_additions/static/description/index.html new file mode 100644 index 000000000..8b96008e8 --- /dev/null +++ b/project_workload_timesheet_additions/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Project Workload Timesheet Additions + + + +
+

Project Workload Timesheet Additions

+ + +

Alpha License: AGPL-3 akretion/ak-odoo-incubator

+

This module adds additional workloads support in timesheets.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the akretion/ak-odoo-incubator project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + From 4ef865e0018e8fd2bd901310bd383b1cd57621a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Mon, 23 Sep 2024 16:32:02 +0200 Subject: [PATCH 28/28] project_workload: add view to see load, automatically set done load when the task is closed --- project_workload_timesheet/models/project_workload_unit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/project_workload_timesheet/models/project_workload_unit.py b/project_workload_timesheet/models/project_workload_unit.py index 6d568d798..8fd603c9b 100644 --- a/project_workload_timesheet/models/project_workload_unit.py +++ b/project_workload_timesheet/models/project_workload_unit.py @@ -34,6 +34,9 @@ class ProjectWorkloadUnit(models.Model): ) @api.depends("force_done") + def _compute_done(self): + return super()._compute_done() + def is_done(self): return super().is_done() or self.force_done