diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28713b26..879c2d07 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/README.rst b/project_workload/README.rst new file mode 100644 index 00000000..edc4e672 --- /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/TODO b/project_workload/TODO new file mode 100644 index 00000000..47a6496d --- /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 00000000..0650744f --- /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 00000000..01f39379 --- /dev/null +++ b/project_workload/__manifest__.py @@ -0,0 +1,27 @@ +# 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", + "summary": "Ressource Workload Management", + "version": "16.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", + "security/project_workload_security.xml", + "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/__init__.py b/project_workload/models/__init__.py new file mode 100644 index 00000000..c84c8cf6 --- /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 00000000..811667e0 --- /dev/null +++ b/project_workload/models/project_project.py @@ -0,0 +1,12 @@ +# 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" + + 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 00000000..80f8ac16 --- /dev/null +++ b/project_workload/models/project_task.py @@ -0,0 +1,104 @@ +# 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 + + +class ProjectTask(models.Model): + _inherit = "project.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( + "planned_date_start", + "planned_date_end", + "planned_hours", + "user_ids", + "config_workload_manually", + "use_workload", + ) + def _compute_workload_ids(self): + for record in self: + if not record.use_workload: + continue + + # Handle only automatic config in planned task + if record.config_workload_manually or not ( + 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.planned_date_start, + "date_end": self.planned_date_end, + "hours": self.planned_hours, + "user_id": user.id, + **extra, + } + + def _get_workload_sync(self): + self.ensure_one() + return [ + *[(0, 0, vals) for vals in self._get_new_workloads()], + *[ + (1, workload.id, vals) + for workload, vals in self._get_updated_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 = workloads.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() + workloads = self._get_main_workloads() + vals = [] + # Update the users workload values + for workload in workloads: + if workload.user_id in self.user_ids: + vals.append((workload, self._prepare_workload(workload.user_id))) + return vals + + def _get_obsolete_workloads(self): + self.ensure_one() + 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): + # related doesn't retrieve all the data so we need to compute it + for record in self: + 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 new file mode 100644 index 00000000..3256dac0 --- /dev/null +++ b/project_workload/models/project_task_workload.py @@ -0,0 +1,121 @@ +# 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). +import re +from datetime import timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +WEEK_FORMAT = "%Y-%W" + + +def week_name(value): + if value: + return value.strftime(WEEK_FORMAT) + 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" + + 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", + string="Units", + compute="_compute_unit_ids", + store=True, + ) + + @api.constrains("date_start", "date_end") + def _check_end_date(self): + for load in self: + if load.date_end < load.date_start: + raise ValidationError( + _("The end date cannot be earlier than the start date.") + ) + + @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) + 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, + { + # 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, + }, + ) + ) + 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() + 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 name_get(self): + result = [] + for load in self: + if not load.date_start or not load.date_end: + continue + 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.task_id.name}: {week_start}" + if week_end > week_start: + name += f" - {week_end}" + 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 new file mode 100644 index 00000000..85ee05c0 --- /dev/null +++ b/project_workload/models/project_workload_unit.py @@ -0,0 +1,46 @@ +# 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 + + +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 + ) + task_id = fields.Many2one("project.task", "Task", related="workload_id.task_id") + project_id = fields.Many2one( + "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.is_closed + + @api.depends("task_id.is_closed") + def _compute_done(self): + for record in self: + record.done = record.is_done() + + def name_get(self): + result = [] + for unit in self: + result.append( + ( + unit.id, + "%s: %s" % (unit.task_id.name, unit.week), + ) + ) + return result diff --git a/project_workload/readme/CONTRIBUTORS.rst b/project_workload/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..292f457e --- /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 00000000..ffb22876 --- /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 00000000..dd00217e --- /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 00000000..c4407e5b --- /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/security/project_workload_security.xml b/project_workload/security/project_workload_security.xml new file mode 100644 index 00000000..b26828e1 --- /dev/null +++ b/project_workload/security/project_workload_security.xml @@ -0,0 +1,23 @@ + + + + + Show Project Workload + + + + + + + + + + + + diff --git a/project_workload/static/description/index.html b/project_workload/static/description/index.html new file mode 100644 index 00000000..32f90189 --- /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/__init__.py b/project_workload/tests/__init__.py new file mode 100644 index 00000000..5c9b38db --- /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 00000000..b96bf3c7 --- /dev/null +++ b/project_workload/tests/common.py @@ -0,0 +1,31 @@ +# 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 freezegun import freeze_time + +from odoo.tests import TransactionCase + + +@freeze_time("2023-07-24") +class TestWorkloadCommon(TransactionCase): + @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", + } + ) diff --git a/project_workload/tests/test_workload.py b/project_workload/tests/test_workload.py new file mode 100644 index 00000000..f3a0bbbf --- /dev/null +++ b/project_workload/tests/test_workload.py @@ -0,0 +1,68 @@ +# 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): + def _create_task(self): + now = datetime.now() + return self.env["project.task"].create( + { + "name": "Task 1", + "project_id": self.project.id, + "user_ids": [(4, self.user_1.id)], + "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() + workload = task.workload_ids + self.assertEqual(len(workload), 1) + 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) + self.assertEqual(load_unit.user_id, self.user_1) + self.assertEqual(set(load_unit.mapped("hours")), {7}) + + def test_change_user(self): + 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): + 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( + task.workload_ids.unit_ids.mapped("user_id"), self.user_1 + self.user_2 + ) + + def test_remove_user(self): + task = self._create_task() + task.user_ids = [(5, 0, 0)] + self.assertFalse(task.workload_ids) + + def test_change_date(self): + 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, 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) + 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 00000000..cd58d3c8 --- /dev/null +++ b/project_workload/views/menu_view.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/project_workload/views/project_project_view.xml b/project_workload/views/project_project_view.xml new file mode 100644 index 00000000..6d4e1104 --- /dev/null +++ b/project_workload/views/project_project_view.xml @@ -0,0 +1,19 @@ + + + + + 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 00000000..b8339fee --- /dev/null +++ b/project_workload/views/project_task_view.xml @@ -0,0 +1,45 @@ + + + + + project.task + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 00000000..932aebe4 --- /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/views/project_task_workload_view.xml b/project_workload/views/project_task_workload_view.xml new file mode 100644 index 00000000..c8d591b6 --- /dev/null +++ b/project_workload/views/project_task_workload_view.xml @@ -0,0 +1,26 @@ + + + + + project.task.workload + project.task.workload.form + +
+ + + + + + + + + + + + + +
+
+
+ +
diff --git a/project_workload_additions/README.rst b/project_workload_additions/README.rst new file mode 100644 index 00000000..0c0898ce --- /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/__init__.py b/project_workload_additions/__init__.py new file mode 100644 index 00000000..0650744f --- /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 00000000..31c5ecb0 --- /dev/null +++ b/project_workload_additions/__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 Additions", + "summary": "Automatically add extra load to tasks.", + "version": "16.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_project_view.xml", + "views/project_task_view.xml", + "views/project_task_workload_addition_type_views.xml", + "views/project_task_workload_view.xml", + "views/menu_view.xml", + ], +} diff --git a/project_workload_additions/models/__init__.py b/project_workload_additions/models/__init__.py new file mode 100644 index 00000000..e90110a3 --- /dev/null +++ b/project_workload_additions/models/__init__.py @@ -0,0 +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_project.py b/project_workload_additions/models/project_project.py new file mode 100644 index 00000000..dbe054e0 --- /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 00000000..319447d2 --- /dev/null +++ b/project_workload_additions/models/project_task.py @@ -0,0 +1,51 @@ +# 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_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() + 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)) + return rv + + def _get_updated_workloads(self): + rv = super()._get_updated_workloads() + 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): + rv = super()._get_obsolete_workloads() + 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 + ): + rv |= 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), + **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 00000000..cf59742e --- /dev/null +++ b/project_workload_additions/models/project_task_workload.py @@ -0,0 +1,18 @@ +# 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" + ) + + additional_workload_task_id = fields.Many2one( + related="additional_workload_id.task_id", string="Additional Workload Task" + ) 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 00000000..75652c0c --- /dev/null +++ b/project_workload_additions/models/project_task_workload_addition.py @@ -0,0 +1,49 @@ +# 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_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, + precompute=True, + ) + user_id = fields.Many2one("res.users", string="User", required=True) + task_id = fields.Many2one("project.task", string="Task", required=True) + + @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() + return task.planned_hours * (self.percentage / 100) + + def name_get(self): + result = [] + for record in self: + result.append( + ( + record.id, + _("%(task)s additional workload (%(percentage)d%%)") + % { + "task": record.task_id.name, + "percentage": 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 00000000..674a82b4 --- /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/models/project_workload_unit.py b/project_workload_additions/models/project_workload_unit.py new file mode 100644 index 00000000..34e21f89 --- /dev/null +++ b/project_workload_additions/models/project_workload_unit.py @@ -0,0 +1,22 @@ +# 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} " + f"{_('of')} {name}" + units_names[unit_id] = name + + return list(units_names.items()) diff --git a/project_workload_additions/readme/CONTRIBUTORS.rst b/project_workload_additions/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..292f457e --- /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 00000000..fb3e4465 --- /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/security/ir.model.access.csv b/project_workload_additions/security/ir.model.access.csv new file mode 100644 index 00000000..23f0d227 --- /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/static/description/index.html b/project_workload_additions/static/description/index.html new file mode 100644 index 00000000..531b235a --- /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_additions/tests/__init__.py b/project_workload_additions/tests/__init__.py new file mode 100644 index 00000000..faed57e5 --- /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 00000000..f3f3ef69 --- /dev/null +++ b/project_workload_additions/tests/test_workload_addition.py @@ -0,0 +1,159 @@ +# 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_ids=None): + now = datetime.now() + return self.env["project.task"].create( + { + "name": "Task 1", + "project_id": self.project.id, + "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) + workloads = task.workload_ids + + self.assertEqual(len(workloads), 3) + workload, workload_management, workload_review = workloads + + 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.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) + + 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() + self._assert_only_additionnal_workload(task.workload_ids) + + def test_remove_user(self): + task = self._create_task(self.user_1) + 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 + self.assertEqual(len(task.workload_ids), 3) + 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 + ) diff --git a/project_workload_additions/views/menu_view.xml b/project_workload_additions/views/menu_view.xml new file mode 100644 index 00000000..7dd4bd7c --- /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 00000000..b84080a2 --- /dev/null +++ b/project_workload_additions/views/project_project_view.xml @@ -0,0 +1,32 @@ + + + + + project.project + + + + 2 + + + + + + + + + + + + + + + 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 00000000..611f11c7 --- /dev/null +++ b/project_workload_additions/views/project_task_view.xml @@ -0,0 +1,15 @@ + + + + + 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 00000000..b98a464f --- /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 + + [] + {} + + +
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 00000000..d16b6899 --- /dev/null +++ b/project_workload_additions/views/project_task_workload_view.xml @@ -0,0 +1,20 @@ + + + + + project.task.workload + + + + + + + + + + + + diff --git a/project_workload_capacity/__init__.py b/project_workload_capacity/__init__.py new file mode 100644 index 00000000..55ec7fc9 --- /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 00000000..1b3d1bf1 --- /dev/null +++ b/project_workload_capacity/__manifest__.py @@ -0,0 +1,27 @@ +# 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": "16.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", + ], + "installable": False, +} diff --git a/project_workload_capacity/models/__init__.py b/project_workload_capacity/models/__init__.py new file mode 100644 index 00000000..a63b7b2c --- /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 00000000..7f7eddfd --- /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 00000000..594a8aa1 --- /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 00000000..03a06bf3 --- /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 00000000..bfd14efb --- /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 00000000..ffb22876 --- /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 00000000..dd00217e --- /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 00000000..0474dae2 --- /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 00000000..45e79450 --- /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 00000000..6d5590bb --- /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 00000000..5c9b38db --- /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 00000000..e6dadfc6 --- /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 00000000..5c64e658 --- /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 00000000..96566466 --- /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 00000000..3c4f2970 --- /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 00000000..3104187b --- /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 00000000..87a79af7 --- /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_milestone/README.rst b/project_workload_milestone/README.rst new file mode 100644 index 00000000..15968256 --- /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/__init__.py b/project_workload_milestone/__init__.py new file mode 100644 index 00000000..0650744f --- /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 00000000..e037be2a --- /dev/null +++ b/project_workload_milestone/__manifest__.py @@ -0,0 +1,21 @@ +# 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": "16.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": [ + "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 00000000..fed98971 --- /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 00000000..2013baf8 --- /dev/null +++ b/project_workload_milestone/models/project_milestone.py @@ -0,0 +1,53 @@ +# 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( + help="The date when the Milestone should start.", + compute="_compute_milestone_start_date", + store=True, + readonly=False, + ) + + @api.constrains("start_date", "deadline") + def _check_start_date(self): + for milestone in self: + 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("deadline", "project_id") + def _compute_milestone_start_date(self): + for record in self: + if record.start_date: + continue + if not record.deadline: + 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), + ("deadline", "<", record.deadline), + ], + order="deadline desc", + limit=1, + ) + # The start date will be the end date of the previous milestone + 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 new file mode 100644 index 00000000..4d09d0fb --- /dev/null +++ b/project_workload_milestone/models/project_task.py @@ -0,0 +1,26 @@ +# 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, fields, models + + +class ProjectTask(models.Model): + _inherit = "project.task" + + 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, + ) + + @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.deadline + record.planned_date_start = record.milestone_id.start_date diff --git a/project_workload_milestone/readme/CONTRIBUTORS.rst b/project_workload_milestone/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..292f457e --- /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 00000000..9aaf0290 --- /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 00000000..8ab79a4b --- /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_milestone/views/project_milestone.xml b/project_workload_milestone/views/project_milestone.xml new file mode 100644 index 00000000..b159bfc3 --- /dev/null +++ b/project_workload_milestone/views/project_milestone.xml @@ -0,0 +1,18 @@ + + + + + + project.milestone + + + + + + + + + + diff --git a/project_workload_timesheet/README.rst b/project_workload_timesheet/README.rst new file mode 100644 index 00000000..34c36503 --- /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/__init__.py b/project_workload_timesheet/__init__.py new file mode 100644 index 00000000..0650744f --- /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 00000000..4aabe81b --- /dev/null +++ b/project_workload_timesheet/__manifest__.py @@ -0,0 +1,22 @@ +# 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": "16.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_sheet", + ], + "data": [ + "views/hr_timesheet_sheet_views.xml", + ], +} diff --git a/project_workload_timesheet/models/__init__.py b/project_workload_timesheet/models/__init__.py new file mode 100644 index 00000000..bd28cf78 --- /dev/null +++ b/project_workload_timesheet/models/__init__.py @@ -0,0 +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 00000000..9fe1cc63 --- /dev/null +++ b/project_workload_timesheet/models/account_analytic_line.py @@ -0,0 +1,62 @@ +# 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.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( + 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 new file mode 100644 index 00000000..cc812ef5 --- /dev/null +++ b/project_workload_timesheet/models/hr_timesheet_sheet.py @@ -0,0 +1,192 @@ +# 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", + string="Workload Units", + compute="_compute_workload_unit_ids", + readonly=False, + ) + + next_week_load = fields.Float( + compute="_compute_next_week_load", + help="The workload of the next week", + ) + next_week_units_count = fields.Integer( + compute="_compute_next_week_load", + help="The number of workload units of the next week", + ) + + current = fields.Boolean( + compute="_compute_current", + help="Is this the current timesheet", + ) + + @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: ( + 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") + 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_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) + 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: + task = unit._get_timesheeting_task() + if self.current: + values["date"] = fields.Date.today() + + values["unit_amount"] = 0 + values["project_id"] = task.project_id.id + values["task_id"] = task.id + 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 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 :/ + 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() + rv = 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 + ) + + # 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 + + return rv 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 00000000..8fd603c9 --- /dev/null +++ b/project_workload_timesheet/models/project_workload_unit.py @@ -0,0 +1,96 @@ +# 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 ProjectWorkloadUnit(models.Model): + _inherit = "project.workload.unit" + + 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") + + timesheeted_hours = fields.Float( + compute="_compute_timesheeted_hours", + help="The hours timesheeted on this workload", + ) + remaining_hours = fields.Float( + compute="_compute_remaining_hours", + help="The remaining hours to timesheet on this workload (can be negative)", + ) + progress = fields.Float( + compute="_compute_progress", + help="The progress of the task", + ) + force_done = fields.Boolean() + task_stage_id = fields.Many2one( + related="task_id.stage_id", string="Task Stage", readonly=False + ) + + @api.depends("force_done") + def _compute_done(self): + return super()._compute_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: + 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 + + # Add only on lines without names + timesheet = self.timesheet_ids.filtered( + 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 + + def action_timesheet_done(self): + self.force_done = True + + def _get_timesheeting_task(self): + # For overrides + return self.task_id diff --git a/project_workload_timesheet/readme/CONTRIBUTORS.rst b/project_workload_timesheet/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..292f457e --- /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 00000000..6245425c --- /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 00000000..8d017e2c --- /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/views/hr_timesheet_sheet_views.xml b/project_workload_timesheet/views/hr_timesheet_sheet_views.xml new file mode 100644 index 00000000..5a95f632 --- /dev/null +++ b/project_workload_timesheet/views/hr_timesheet_sheet_views.xml @@ -0,0 +1,145 @@ + + + + hr_timesheet.sheet + + +
+
+ +
+
+ + + + + + + + +
+

Todo

+ + + + +
+
+
+
+
diff --git a/project_workload_timesheet_additions/README.rst b/project_workload_timesheet_additions/README.rst new file mode 100644 index 00000000..7ec95bb6 --- /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/__init__.py b/project_workload_timesheet_additions/__init__.py new file mode 100644 index 00000000..0650744f --- /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 00000000..2456ac5f --- /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": "16.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 00000000..bd28cf78 --- /dev/null +++ b/project_workload_timesheet_additions/models/__init__.py @@ -0,0 +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 00000000..a645f5d4 --- /dev/null +++ b/project_workload_timesheet_additions/models/account_analytic_line.py @@ -0,0 +1,37 @@ +# 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 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/hr_timesheet_sheet.py b/project_workload_timesheet_additions/models/hr_timesheet_sheet.py new file mode 100644 index 00000000..34aa71be --- /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 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 00000000..584c2509 --- /dev/null +++ b/project_workload_timesheet_additions/models/project_workload_unit.py @@ -0,0 +1,27 @@ +# 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", + 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): + # 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/readme/CONTRIBUTORS.rst b/project_workload_timesheet_additions/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..292f457e --- /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 00000000..b8dc49f0 --- /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 00000000..8b96008e --- /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.

+
+
+
+ + 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 00000000..a7cc82bc --- /dev/null +++ b/project_workload_timesheet_additions/views/hr_timesheet_sheet_views.xml @@ -0,0 +1,18 @@ + + + + hr_timesheet.sheet + + + + + + + + diff --git a/setup/project_workload/odoo/addons/project_workload b/setup/project_workload/odoo/addons/project_workload new file mode 120000 index 00000000..4b3d3182 --- /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 00000000..28c57bb6 --- /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 00000000..793ba60a --- /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 00000000..28c57bb6 --- /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_milestone/odoo/addons/project_workload_milestone b/setup/project_workload_milestone/odoo/addons/project_workload_milestone new file mode 120000 index 00000000..e908b1d0 --- /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 00000000..28c57bb6 --- /dev/null +++ b/setup/project_workload_milestone/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 00000000..396e282f --- /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 00000000..28c57bb6 --- /dev/null +++ b/setup/project_workload_timesheet/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) 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 00000000..b6e9a6a5 --- /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 00000000..28c57bb6 --- /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, +)