From 86d6a4b62cdd4b2e68c5c6c3ddd02648e9250224 Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Tue, 30 Jul 2024 15:50:23 +0200 Subject: [PATCH 01/15] [ADD] resource_multi_week_calendar Signed-off-by: Carmen Bianca BAKKER --- resource_multi_week_calendar/README.rst | 99 ++++ resource_multi_week_calendar/__init__.py | 5 + resource_multi_week_calendar/__manifest__.py | 22 + .../models/__init__.py | 5 + .../models/resource_calendar.py | 97 ++++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 14 + .../static/description/index.html | 438 ++++++++++++++++++ .../tests/__init__.py | 5 + .../tests/test_calendar.py | 87 ++++ .../odoo/addons/resource_multi_week_calendar | 1 + setup/resource_multi_week_calendar/setup.py | 6 + 12 files changed, 782 insertions(+) create mode 100644 resource_multi_week_calendar/README.rst create mode 100644 resource_multi_week_calendar/__init__.py create mode 100644 resource_multi_week_calendar/__manifest__.py create mode 100644 resource_multi_week_calendar/models/__init__.py create mode 100644 resource_multi_week_calendar/models/resource_calendar.py create mode 100644 resource_multi_week_calendar/readme/CONTRIBUTORS.rst create mode 100644 resource_multi_week_calendar/readme/DESCRIPTION.rst create mode 100644 resource_multi_week_calendar/static/description/index.html create mode 100644 resource_multi_week_calendar/tests/__init__.py create mode 100644 resource_multi_week_calendar/tests/test_calendar.py create mode 120000 setup/resource_multi_week_calendar/odoo/addons/resource_multi_week_calendar create mode 100644 setup/resource_multi_week_calendar/setup.py diff --git a/resource_multi_week_calendar/README.rst b/resource_multi_week_calendar/README.rst new file mode 100644 index 00000000000..15f17a5c500 --- /dev/null +++ b/resource_multi_week_calendar/README.rst @@ -0,0 +1,99 @@ +==================== +Multi-week calendars +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:71adeb4c733912c857b2051610ef2056cebe834c7ff4857c7f861e8ecd5940ed + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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-OCA%2Fhr-lightgray.png?logo=github + :target: https://github.com/OCA/hr/tree/16.0/resource_multi_week_calendar + :alt: OCA/hr +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-16-0/hr-16-0-resource_multi_week_calendar + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/hr&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allow a calendar to alternate between multiple weeks. + +An implementation of this functionality exists in Odoo's ``resource`` module +since version 13. In Odoo's implementation, you can only alternate between two +weeks. Furthermore, the implementation is more than a little wonky. + +The advantage of this module over the implementation in ``resource`` is that you +can alternate between more than two weeks. The implementation is (hopefully) +better. + +The downside of adopting this module is that all modules which interact with the +week-alternating functionality of ``resource`` must be adapted to be compatible +with this module. At the time of writing (2024-07-29), the only Odoo module +which does this is ``hr_holidays``. + +**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 +~~~~~~~ + +* Coop IT Easy SC + +Contributors +~~~~~~~~~~~~ + +* `Coop IT Easy SC `_: + + * Carmen Bianca BAKKER + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-carmenbianca| image:: https://github.com/carmenbianca.png?size=40px + :target: https://github.com/carmenbianca + :alt: carmenbianca + +Current `maintainer `__: + +|maintainer-carmenbianca| + +This module is part of the `OCA/hr `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/resource_multi_week_calendar/__init__.py b/resource_multi_week_calendar/__init__.py new file mode 100644 index 00000000000..3eb78877c5b --- /dev/null +++ b/resource_multi_week_calendar/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from . import models diff --git a/resource_multi_week_calendar/__manifest__.py b/resource_multi_week_calendar/__manifest__.py new file mode 100644 index 00000000000..eca025ed304 --- /dev/null +++ b/resource_multi_week_calendar/__manifest__.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +{ + "name": "Multi-week calendars", + "summary": """ + Allow a calendar to alternate between multiple weeks.""", + "version": "16.0.1.0.0", + "category": "Hidden", + "website": "https://coopiteasy.be", + "author": "Coop IT Easy SC, Odoo Community Association (OCA)", + "maintainers": ["carmenbianca"], + "license": "AGPL-3", + "application": False, + "depends": [ + "resource", + ], + "data": [], + "demo": [], + "qweb": [], +} diff --git a/resource_multi_week_calendar/models/__init__.py b/resource_multi_week_calendar/models/__init__.py new file mode 100644 index 00000000000..7a85334ad46 --- /dev/null +++ b/resource_multi_week_calendar/models/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from . import resource_calendar diff --git a/resource_multi_week_calendar/models/resource_calendar.py b/resource_multi_week_calendar/models/resource_calendar.py new file mode 100644 index 00000000000..39eee0a71f0 --- /dev/null +++ b/resource_multi_week_calendar/models/resource_calendar.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class ResourceCalendar(models.Model): + _inherit = "resource.calendar" + + parent_calendar_id = fields.Many2one( + comodel_name="resource.calendar", + domain=[("parent_calendar_id", "=", False)], + # TODO: should this cascade instead? + ondelete="set null", + string="Main Working Time", + ) + child_calendar_ids = fields.One2many( + comodel_name="resource.calendar", + inverse_name="parent_calendar_id", + # TODO: this causes a recursion error, but seems correct to me. + # domain=[("child_calendar_ids", "=", [])], + string="Alternating Working Times", + copy=True, + ) + + # Making week_number a computed derivative of week_sequence has the + # advantage of being able to drag calendars around in a table, and not + # having to manually fiddle with every week number (nor make sure that no + # weeks are skipped). + # + # However, week sequences MUST be unique. Unfortunately, creating a + # constraint on (parent_calendar_id, week_sequence) does not work. The + # constraint method is called before all children/siblings are saved, + # meaning that they can conflict with each other in this interim stage. + # + # If this value is not unique, the behaviour is undefined. Fortunately, this + # should not happen in regular Odoo usage. + week_sequence = fields.Integer(default=0) + week_number = fields.Integer( + compute="_compute_week_number", + store=True, + recursive=True, + ) + + @api.depends( + "week_sequence", + "parent_calendar_id", + "parent_calendar_id.child_calendar_ids", + "parent_calendar_id.child_calendar_ids.week_sequence", + ) + def _compute_week_number(self): + for calendar in self: + parent = calendar.parent_calendar_id + if parent: + for week_number, sibling in enumerate( + parent.child_calendar_ids.sorted(lambda item: item.week_sequence), + start=2, + ): + if calendar == sibling: + calendar.week_number = week_number + break + else: + calendar.week_number = 1 + + @api.constrains("parent_calendar_id", "child_calendar_ids") + def _check_child_is_not_parent(self): + err_str = _( + "Working Time '%s' may not be the Main Working Time of another" + " Working Time ('%s') while it has a Main Working Time itself ('%s')" + ) + for calendar in self: + if calendar.parent_calendar_id and calendar.child_calendar_ids: + raise ValidationError( + err_str + % ( + calendar.name, + calendar.child_calendar_ids[0].name, + calendar.parent_calendar_id.name, + ) + ) + # This constraint isn't triggered on calendars which have children + # added to them. Therefore, we also check whether our parent already + # has a parent. + if ( + calendar.parent_calendar_id + and calendar.parent_calendar_id.parent_calendar_id + ): + raise ValidationError( + err_str + % ( + calendar.parent_calendar_id.name, + calendar.name, + calendar.parent_calendar_id.parent_calendar_id.name, + ) + ) diff --git a/resource_multi_week_calendar/readme/CONTRIBUTORS.rst b/resource_multi_week_calendar/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..f1ac675779f --- /dev/null +++ b/resource_multi_week_calendar/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Coop IT Easy SC `_: + + * Carmen Bianca BAKKER diff --git a/resource_multi_week_calendar/readme/DESCRIPTION.rst b/resource_multi_week_calendar/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..471edb3475a --- /dev/null +++ b/resource_multi_week_calendar/readme/DESCRIPTION.rst @@ -0,0 +1,14 @@ +Allow a calendar to alternate between multiple weeks. + +An implementation of this functionality exists in Odoo's ``resource`` module +since version 13. In Odoo's implementation, you can only alternate between two +weeks. Furthermore, the implementation is more than a little wonky. + +The advantage of this module over the implementation in ``resource`` is that you +can alternate between more than two weeks. The implementation is (hopefully) +better. + +The downside of adopting this module is that all modules which interact with the +week-alternating functionality of ``resource`` must be adapted to be compatible +with this module. At the time of writing (2024-07-29), the only Odoo module +which does this is ``hr_holidays``. diff --git a/resource_multi_week_calendar/static/description/index.html b/resource_multi_week_calendar/static/description/index.html new file mode 100644 index 00000000000..68db2fbedcb --- /dev/null +++ b/resource_multi_week_calendar/static/description/index.html @@ -0,0 +1,438 @@ + + + + + +Multi-week calendars + + + +
+

Multi-week calendars

+ + +

Beta License: AGPL-3 OCA/hr Translate me on Weblate Try me on Runboat

+

Allow a calendar to alternate between multiple weeks.

+

An implementation of this functionality exists in Odoo’s resource module +since version 13. In Odoo’s implementation, you can only alternate between two +weeks. Furthermore, the implementation is more than a little wonky.

+

The advantage of this module over the implementation in resource is that you +can alternate between more than two weeks. The implementation is (hopefully) +better.

+

The downside of adopting this module is that all modules which interact with the +week-alternating functionality of resource must be adapted to be compatible +with this module. At the time of writing (2024-07-29), the only Odoo module +which does this is hr_holidays.

+

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

+
    +
  • Coop IT Easy SC
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

carmenbianca

+

This module is part of the OCA/hr project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/resource_multi_week_calendar/tests/__init__.py b/resource_multi_week_calendar/tests/__init__.py new file mode 100644 index 00000000000..7634ab040df --- /dev/null +++ b/resource_multi_week_calendar/tests/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from . import test_calendar diff --git a/resource_multi_week_calendar/tests/test_calendar.py b/resource_multi_week_calendar/tests/test_calendar.py new file mode 100644 index 00000000000..46b3d8ff0cf --- /dev/null +++ b/resource_multi_week_calendar/tests/test_calendar.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError + + +class TestCalendarConstraints(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Calendar = cls.env["resource.calendar"] + cls.parent_calendar = cls.Calendar.create({"name": "Parent"}) + + def test_cant_add_child_to_child(self): + one = self.Calendar.create( + { + "name": "One", + "parent_calendar_id": self.parent_calendar.id, + "week_sequence": 1, + } + ) + with self.assertRaises(ValidationError): + self.Calendar.create( + { + "name": "Two", + "parent_calendar_id": one.id, + "week_sequence": 2, + } + ) + + def test_cant_add_parent_to_parent(self): + self.Calendar.create( + { + "name": "Child", + "parent_calendar_id": self.parent_calendar.id, + "week_sequence": 1, + } + ) + with self.assertRaises(ValidationError): + self.Calendar.create( + { + "name": "Parent of parent", + "child_calendar_ids": self.parent_calendar.ids, + # This value is kind of arbitrary here. + "week_sequence": 2, + } + ) + + +class TestCalendarWeekNumber(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Calendar = cls.env["resource.calendar"] + cls.parent_calendar = cls.Calendar.create({"name": "Parent"}) + + def test_solo(self): + self.assertEqual(self.parent_calendar.week_number, 1) + + def test_children(self): + # The parent's sequence should not matter. + self.parent_calendar.week_sequence = 100 + one = self.Calendar.create( + { + "name": "One", + "parent_calendar_id": self.parent_calendar.id, + "week_sequence": 1, + } + ) + two = self.Calendar.create( + { + "name": "Two", + "parent_calendar_id": self.parent_calendar.id, + # Arbitrarily big number. + "week_sequence": 30, + } + ) + self.assertEqual(self.parent_calendar.week_number, 1) + self.assertEqual(one.week_number, 2) + self.assertEqual(two.week_number, 3) + + # Change the order. + one.week_sequence = 31 + self.assertEqual(one.week_number, 3) + self.assertEqual(two.week_number, 2) diff --git a/setup/resource_multi_week_calendar/odoo/addons/resource_multi_week_calendar b/setup/resource_multi_week_calendar/odoo/addons/resource_multi_week_calendar new file mode 120000 index 00000000000..4b80a72c090 --- /dev/null +++ b/setup/resource_multi_week_calendar/odoo/addons/resource_multi_week_calendar @@ -0,0 +1 @@ +../../../../resource_multi_week_calendar \ No newline at end of file diff --git a/setup/resource_multi_week_calendar/setup.py b/setup/resource_multi_week_calendar/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/resource_multi_week_calendar/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 9559926db30452a98509727aac12eb982228a8ca Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Thu, 1 Aug 2024 17:12:34 +0200 Subject: [PATCH 02/15] [IMP] resource_multi_week_calendar: Implement computation of current calendar This is the bones of the implementation. Signed-off-by: Carmen Bianca BAKKER --- .../models/resource_calendar.py | 136 ++++++++++++++++-- .../tests/test_calendar.py | 129 +++++++++++++++-- 2 files changed, 244 insertions(+), 21 deletions(-) diff --git a/resource_multi_week_calendar/models/resource_calendar.py b/resource_multi_week_calendar/models/resource_calendar.py index 39eee0a71f0..4902233444e 100644 --- a/resource_multi_week_calendar/models/resource_calendar.py +++ b/resource_multi_week_calendar/models/resource_calendar.py @@ -2,7 +2,10 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -from odoo import api, fields, models, _ +import math +from datetime import timedelta + +from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -24,6 +27,12 @@ class ResourceCalendar(models.Model): string="Alternating Working Times", copy=True, ) + family_calendar_ids = fields.One2many( + comodel_name="resource.calendar", + compute="_compute_family_calendar_ids", + recursive=True, + ) + is_multi_week = fields.Boolean(compute="_compute_is_multi_week", store=True) # Making week_number a computed derivative of week_sequence has the # advantage of being able to drag calendars around in a table, and not @@ -43,6 +52,52 @@ class ResourceCalendar(models.Model): store=True, recursive=True, ) + current_week_number = fields.Integer( + compute="_compute_current_week", + recursive=True, + ) + current_calendar_id = fields.Many2one( + comodel_name="resource.calendar", + compute="_compute_current_week", + recursive=True, + ) + + multi_week_epoch_date = fields.Date( + string="Date of First Week", + help="""When using alternating weeks, the week which contains the + specified date becomes the first week, and all subsequent weeks + alternate in order.""", + # required=True, + # default="1970-01-01", + # Compute this on child calendars; write this manually on parent + # calendars. Would use 'related=', but that wouldn't work here. Although + # technically, the value of this field on child calendars isn't super + # pertinent. + compute="_compute_multi_week_epoch_date", + readonly=False, + store=True, + recursive=True, + ) + + @api.depends( + "child_calendar_ids", + "parent_calendar_id", + "parent_calendar_id.child_calendar_ids", + ) + def _compute_family_calendar_ids(self): + for calendar in self: + parent = calendar.parent_calendar_id or calendar + calendar.family_calendar_ids = parent | parent.child_calendar_ids + + @api.depends( + "child_calendar_ids", + "parent_calendar_id", + ) + def _compute_is_multi_week(self): + for calendar in self: + calendar.is_multi_week = bool( + calendar.child_calendar_ids or calendar.parent_calendar_id + ) @api.depends( "week_sequence", @@ -64,21 +119,57 @@ def _compute_week_number(self): else: calendar.week_number = 1 + def _get_first_day_of_epoch_week(self): + self.ensure_one() + return self.multi_week_epoch_date - timedelta( + days=self.multi_week_epoch_date.weekday() + ) + + @api.depends( + "multi_week_epoch_date", + "week_number", + "family_calendar_ids", + # TODO: current date. Port company_today or add a cron. Or don't store. + ) + def _compute_current_week(self): + for calendar in self: + family_size = len(calendar.family_calendar_ids) + weeks_since_epoch = math.floor( + (fields.Date.today() - calendar._get_first_day_of_epoch_week()).days / 7 + ) + current_week_number = (weeks_since_epoch % family_size) + 1 + # TODO: does this work in the negative, too? + calendar.current_week_number = current_week_number + calendar.current_calendar_id = calendar.family_calendar_ids.filtered( + lambda item: item.week_number == current_week_number + ) + + @api.depends("parent_calendar_id.multi_week_epoch_date") + def _compute_multi_week_epoch_date(self): + for calendar in self: + parent = calendar.parent_calendar_id + if parent: + calendar.multi_week_epoch_date = parent.multi_week_epoch_date + else: + # A default value. + calendar.multi_week_epoch_date = "1970-01-01" + @api.constrains("parent_calendar_id", "child_calendar_ids") def _check_child_is_not_parent(self): err_str = _( - "Working Time '%s' may not be the Main Working Time of another" - " Working Time ('%s') while it has a Main Working Time itself ('%s')" + "Working Time '%(name)s' may not be the Main Working Time of" + " another Working Time ('%(child)s') while it has a Main Working" + " Time itself ('%(parent)s')" ) for calendar in self: if calendar.parent_calendar_id and calendar.child_calendar_ids: raise ValidationError( err_str - % ( - calendar.name, - calendar.child_calendar_ids[0].name, - calendar.parent_calendar_id.name, - ) + % { + "name": calendar.name, + "child": calendar.child_calendar_ids[0].name, + "parent": calendar.parent_calendar_id.name, + } ) # This constraint isn't triggered on calendars which have children # added to them. Therefore, we also check whether our parent already @@ -89,9 +180,28 @@ def _check_child_is_not_parent(self): ): raise ValidationError( err_str - % ( - calendar.parent_calendar_id.name, - calendar.name, - calendar.parent_calendar_id.parent_calendar_id.name, - ) + % { + "name": calendar.parent_calendar_id.name, + "child": calendar.name, + "parent": calendar.parent_calendar_id.parent_calendar_id.name, + } ) + + @api.constrains("parent_calendar_id", "multi_week_epoch_date") + def _check_epoch_date_matches_parent(self): + for calendar in self: + if calendar.parent_calendar_id: + if ( + calendar.multi_week_epoch_date + != calendar.parent_calendar_id.multi_week_epoch_date + ): + # Because the epoch date is hidden on the views of children, + # this should not happen. However, for sanity, we do this + # check anyway. + raise ValidationError( + _( + "Working Time '%s' has an epoch date which does not" + " match its Main Working Time's. This should not happen." + ) + % calendar.name + ) diff --git a/resource_multi_week_calendar/tests/test_calendar.py b/resource_multi_week_calendar/tests/test_calendar.py index 46b3d8ff0cf..28fd029e4a6 100644 --- a/resource_multi_week_calendar/tests/test_calendar.py +++ b/resource_multi_week_calendar/tests/test_calendar.py @@ -2,17 +2,34 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -from odoo.tests.common import TransactionCase +import datetime + +from freezegun import freeze_time + from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase -class TestCalendarConstraints(TransactionCase): +class CalendarCase(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.Calendar = cls.env["resource.calendar"] cls.parent_calendar = cls.Calendar.create({"name": "Parent"}) + cls._sequence = 0 + + def create_simple_child(self): + self._sequence += 1 + return self.Calendar.create( + { + "name": "Child {}".format(self._sequence), + "parent_calendar_id": self.parent_calendar.id, + "week_sequence": self._sequence, + } + ) + +class TestCalendarConstraints(CalendarCase): def test_cant_add_child_to_child(self): one = self.Calendar.create( { @@ -48,14 +65,23 @@ def test_cant_add_parent_to_parent(self): } ) + def test_cant_change_epoch_date(self): + child = self.create_simple_child() + with self.assertRaises(ValidationError): + child.multi_week_epoch_date = "2001-01-01" + + +class TestCalendarIsMultiweek(CalendarCase): + def test_solo(self): + self.assertFalse(self.parent_calendar.is_multi_week) + + def test_has_child_or_parent(self): + child = self.create_simple_child() + self.assertTrue(self.parent_calendar.is_multi_week) + self.assertTrue(child.is_multi_week) -class TestCalendarWeekNumber(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.Calendar = cls.env["resource.calendar"] - cls.parent_calendar = cls.Calendar.create({"name": "Parent"}) +class TestCalendarWeekNumber(CalendarCase): def test_solo(self): self.assertEqual(self.parent_calendar.week_number, 1) @@ -85,3 +111,90 @@ def test_children(self): one.week_sequence = 31 self.assertEqual(one.week_number, 3) self.assertEqual(two.week_number, 2) + + +class TestCalendarWeekEpoch(CalendarCase): + def test_set_epoch_on_parent(self): + child = self.create_simple_child() + self.parent_calendar.multi_week_epoch_date = "2001-01-01" + self.assertEqual(child.multi_week_epoch_date, datetime.date(2001, 1, 1)) + + def test_set_epoch_on_parent_prior_to_creation(self): + self.parent_calendar.multi_week_epoch_date = "2001-01-01" + child = self.create_simple_child() + self.assertEqual(child.multi_week_epoch_date, datetime.date(2001, 1, 1)) + + def test_unix_epoch_default(self): + self.assertEqual( + self.parent_calendar.multi_week_epoch_date, datetime.date(1970, 1, 1) + ) + + @freeze_time("1970-01-08") + def test_compute_current_week_no_family(self): + self.assertEqual(self.parent_calendar.current_week_number, 1) + self.assertEqual(self.parent_calendar.current_calendar_id, self.parent_calendar) + + @freeze_time("1970-01-01") + def test_compute_current_week_same_day(self): + child = self.create_simple_child() + self.assertEqual(child.current_week_number, 1) + self.assertEqual(child.current_calendar_id, self.parent_calendar) + + # 1969-12-29 is a Monday. + @freeze_time("1969-12-29") + def test_compute_current_week_first_day_of_week(self): + child = self.create_simple_child() + self.assertEqual(child.current_week_number, 1) + self.assertEqual(child.current_calendar_id, self.parent_calendar) + + # 1969-12-28 is a Sunday. + @freeze_time("1969-12-28") + def test_compute_current_week_one_week_ago(self): + child = self.create_simple_child() + self.assertEqual(child.current_week_number, 2) + self.assertEqual(child.current_calendar_id, child) + # Test against parent, too, which should have the same result. + self.assertEqual(self.parent_calendar.current_week_number, 2) + self.assertEqual(self.parent_calendar.current_calendar_id, child) + + # 1970-01-04 is a Sunday. + @freeze_time("1970-01-04") + def test_compute_current_week_last_day_of_week(self): + child = self.create_simple_child() + self.assertEqual(child.current_week_number, 1) + self.assertEqual(child.current_calendar_id, self.parent_calendar) + + # 1970-01-05 is a Monday. + @freeze_time("1970-01-05") + def test_compute_current_week_next_week(self): + child = self.create_simple_child() + self.assertEqual(child.current_week_number, 2) + self.assertEqual(child.current_calendar_id, child) + + # 1970-01-12 is a Monday. + @freeze_time("1970-01-12") + def test_compute_current_week_in_two_weeks(self): + child = self.create_simple_child() + self.assertEqual(child.current_week_number, 1) + self.assertEqual(child.current_calendar_id, self.parent_calendar) + + # 1970-01-12 is a Monday. + @freeze_time("1970-01-12") + def test_compute_current_week_in_two_weeks_three_calendars(self): + self.create_simple_child() + child_2 = self.create_simple_child() + self.assertEqual(child_2.current_week_number, 3) + self.assertEqual(child_2.current_calendar_id, child_2) + + # 1970-01-04 is a Sunday. + @freeze_time("1970-01-04") + def test_compute_current_week_when_day_changes(self): + child = self.create_simple_child() + self.assertEqual(child.current_week_number, 1) + self.assertEqual(child.current_calendar_id, self.parent_calendar) + with freeze_time("1970-01-05"): + # This re-compute shouldn't technically be needed... Maybe there's a + # cache? + child._compute_current_week() + self.assertEqual(child.current_week_number, 2) + self.assertEqual(child.current_calendar_id, child) From 2e20e8a2ee4d69e89c706836ba15d72f7a109633 Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Tue, 27 Aug 2024 16:57:41 +0200 Subject: [PATCH 03/15] [IMP] resource_multi_week_calendar: Create form view Signed-off-by: Carmen Bianca BAKKER --- resource_multi_week_calendar/__manifest__.py | 6 ++- .../models/resource_calendar.py | 10 +++++ .../static/description/index.html | 11 ++--- .../views/resource_calendar_views.xml | 43 +++++++++++++++++++ 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 resource_multi_week_calendar/views/resource_calendar_views.xml diff --git a/resource_multi_week_calendar/__manifest__.py b/resource_multi_week_calendar/__manifest__.py index eca025ed304..704649e5182 100644 --- a/resource_multi_week_calendar/__manifest__.py +++ b/resource_multi_week_calendar/__manifest__.py @@ -8,7 +8,7 @@ Allow a calendar to alternate between multiple weeks.""", "version": "16.0.1.0.0", "category": "Hidden", - "website": "https://coopiteasy.be", + "website": "https://github.com/OCA/hr", "author": "Coop IT Easy SC, Odoo Community Association (OCA)", "maintainers": ["carmenbianca"], "license": "AGPL-3", @@ -16,7 +16,9 @@ "depends": [ "resource", ], - "data": [], + "data": [ + "views/resource_calendar_views.xml", + ], "demo": [], "qweb": [], } diff --git a/resource_multi_week_calendar/models/resource_calendar.py b/resource_multi_week_calendar/models/resource_calendar.py index 4902233444e..6607aa8812f 100644 --- a/resource_multi_week_calendar/models/resource_calendar.py +++ b/resource_multi_week_calendar/models/resource_calendar.py @@ -79,6 +79,16 @@ class ResourceCalendar(models.Model): recursive=True, ) + def copy(self, default=None): + self.ensure_one() + if default is None: + default = {} + sequences = sorted(self.family_calendar_ids.mapped("week_sequence")) + if sequences: + # Assign highest value sequence. + default["week_sequence"] = sequences[-1] + 1 + return super().copy(default=default) + @api.depends( "child_calendar_ids", "parent_calendar_id", diff --git a/resource_multi_week_calendar/static/description/index.html b/resource_multi_week_calendar/static/description/index.html index 68db2fbedcb..20cedf4bf9a 100644 --- a/resource_multi_week_calendar/static/description/index.html +++ b/resource_multi_week_calendar/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -421,9 +420,7 @@

Contributors

Maintainers

This module is maintained by the OCA.

- -Odoo Community Association - +Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/resource_multi_week_calendar/views/resource_calendar_views.xml b/resource_multi_week_calendar/views/resource_calendar_views.xml new file mode 100644 index 00000000000..37ca1a9a12f --- /dev/null +++ b/resource_multi_week_calendar/views/resource_calendar_views.xml @@ -0,0 +1,43 @@ + + + + resource.calendar.form + resource.calendar + + + + + + + + + + + + + + + + + + + + + + + + From f9cf0b95e33e1ed15ebb9403b0a3f6663770c522 Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Thu, 29 Aug 2024 16:48:06 +0200 Subject: [PATCH 04/15] [IMP] resource_multi_week_calendar: Implement _attendance_intervals_batch This is the real implementation work. With this method implemented, all other methods correctly get the correct week each time. Signed-off-by: Carmen Bianca BAKKER --- resource_multi_week_calendar/__manifest__.py | 2 - .../models/resource_calendar.py | 87 ++++++++++++++++-- .../tests/test_calendar.py | 88 +++++++++++++++++++ 3 files changed, 168 insertions(+), 9 deletions(-) diff --git a/resource_multi_week_calendar/__manifest__.py b/resource_multi_week_calendar/__manifest__.py index 704649e5182..f1f018a28a9 100644 --- a/resource_multi_week_calendar/__manifest__.py +++ b/resource_multi_week_calendar/__manifest__.py @@ -19,6 +19,4 @@ "data": [ "views/resource_calendar_views.xml", ], - "demo": [], - "qweb": [], } diff --git a/resource_multi_week_calendar/models/resource_calendar.py b/resource_multi_week_calendar/models/resource_calendar.py index 6607aa8812f..d57cdf434df 100644 --- a/resource_multi_week_calendar/models/resource_calendar.py +++ b/resource_multi_week_calendar/models/resource_calendar.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import math -from datetime import timedelta +from datetime import datetime, timedelta from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -135,6 +135,18 @@ def _get_first_day_of_epoch_week(self): days=self.multi_week_epoch_date.weekday() ) + def _get_week_number(self, day=None): + self.ensure_one() + if day is None: + day = fields.Date.today() + if isinstance(day, datetime): + day = day.date() + family_size = len(self.family_calendar_ids) + weeks_since_epoch = math.floor( + (day - self._get_first_day_of_epoch_week()).days / 7 + ) + return (weeks_since_epoch % family_size) + 1 + @api.depends( "multi_week_epoch_date", "week_number", @@ -143,12 +155,7 @@ def _get_first_day_of_epoch_week(self): ) def _compute_current_week(self): for calendar in self: - family_size = len(calendar.family_calendar_ids) - weeks_since_epoch = math.floor( - (fields.Date.today() - calendar._get_first_day_of_epoch_week()).days / 7 - ) - current_week_number = (weeks_since_epoch % family_size) + 1 - # TODO: does this work in the negative, too? + current_week_number = calendar._get_week_number() calendar.current_week_number = current_week_number calendar.current_calendar_id = calendar.family_calendar_ids.filtered( lambda item: item.week_number == current_week_number @@ -215,3 +222,69 @@ def _check_epoch_date_matches_parent(self): ) % calendar.name ) + + @api.model + def _split_into_weeks(self, start_dt, end_dt): + # TODO: This method splits weeks on the timezone of start_dt. Maybe it + # should split weeks on the timezone of the calendar. It is not + # immediately clear to me how to implement that. + current_start = start_dt + while current_start < end_dt: + # Calculate the end of the week (Monday 00:00:00, the threshold + # of Sunday-to-Monday.) + days_until_monday = 7 - current_start.weekday() + week_end = current_start + timedelta(days=days_until_monday) + week_end = week_end.replace(hour=0, minute=0, second=0, microsecond=0) + + current_end = min(week_end, end_dt) + yield (current_start, current_end) + + # Move to the next week (start of next Monday) + current_start = current_end + + def _attendance_intervals_batch( + self, start_dt, end_dt, resources=None, domain=None, tz=None + ): + self.ensure_one() + if not self.is_multi_week: + return super()._attendance_intervals_batch( + start_dt, end_dt, resources=resources, domain=domain, tz=tz + ) + + if self.parent_calendar_id: + return self.parent_calendar_id._attendance_intervals_batch( + start_dt, end_dt, resources=resources, domain=domain, tz=tz + ) + calendars_by_week = { + calendar.week_number: calendar + for calendar in self | self.child_calendar_ids + } + results = [] + + # Calculate each week separately, choosing the correct calendar for each + # week. + for week_start, week_end in self._split_into_weeks(start_dt, end_dt): + results.append( + super( + ResourceCalendar, + calendars_by_week[self._get_week_number(week_start)].with_context( + # This context is not used here, but could possibly be + # used by other modules that use this module. I am not + # sure how useful it is. + recursive_multi_week=True + ), + )._attendance_intervals_batch( + week_start, week_end, resources=resources, domain=domain, tz=tz + ) + ) + + # Aggregate the results from each week. + result = {} + for item in results: + for resource, intervals in item.items(): + if resource not in result: + result[resource] = intervals + else: + result[resource] |= intervals + + return result diff --git a/resource_multi_week_calendar/tests/test_calendar.py b/resource_multi_week_calendar/tests/test_calendar.py index 28fd029e4a6..82cecd8a205 100644 --- a/resource_multi_week_calendar/tests/test_calendar.py +++ b/resource_multi_week_calendar/tests/test_calendar.py @@ -7,6 +7,7 @@ from freezegun import freeze_time from odoo.exceptions import ValidationError +from odoo.fields import Command from odoo.tests.common import TransactionCase @@ -198,3 +199,90 @@ def test_compute_current_week_when_day_changes(self): child._compute_current_week() self.assertEqual(child.current_week_number, 2) self.assertEqual(child.current_calendar_id, child) + + +class TestMultiCalendar(CalendarCase): + def setUp(self): + super().setUpClass() + # The parent calendar has attendances by default: Every weekday from 8 + # to 12, and 13 to 17. + self.child_calendar = self.create_simple_child() + # In the child calendar, only work the mornings. + self.child_calendar.attendance_ids = False + self.child_calendar.attendance_ids = [ + Command.create( + { + "name": "Monday Morning", + "dayofweek": "0", + "hour_from": 8, + "hour_to": 12, + "day_period": "morning", + } + ), + Command.create( + { + "name": "Tuesday Morning", + "dayofweek": "1", + "hour_from": 8, + "hour_to": 12, + "day_period": "morning", + } + ), + Command.create( + { + "name": "Wednesday Morning", + "dayofweek": "2", + "hour_from": 8, + "hour_to": 12, + "day_period": "morning", + } + ), + Command.create( + { + "name": "Thursday Morning", + "dayofweek": "3", + "hour_from": 8, + "hour_to": 12, + "day_period": "morning", + } + ), + Command.create( + { + "name": "Friday Morning", + "dayofweek": "4", + "hour_from": 8, + "hour_to": 12, + "day_period": "morning", + } + ), + ] + + def test_count_work_hours_two_weeks(self): + hours = self.parent_calendar.get_work_hours_count( + # 1st of July is a Monday. + datetime.datetime.fromisoformat("2024-07-01T00:00:00+00:00"), + datetime.datetime.fromisoformat("2024-07-14T23:59:59+00:00"), + ) + # 40 from the parent, 20 from the child + self.assertEqual(hours, 60) + + def test_count_work_hours_from_child(self): + # It doesn't matter whether you call the method from the child. + hours = self.child_calendar.get_work_hours_count( + datetime.datetime.fromisoformat("2024-07-01T00:00:00+00:00"), + datetime.datetime.fromisoformat("2024-07-14T23:59:59+00:00"), + ) + self.assertEqual(hours, 60) + + def test_count_work_hours_weeks_separately(self): + self.parent_calendar.multi_week_epoch_date = "2024-07-01" + hours = self.parent_calendar.get_work_hours_count( + datetime.datetime.fromisoformat("2024-07-01T00:00:00+00:00"), + datetime.datetime.fromisoformat("2024-07-07T23:59:59+00:00"), + ) + self.assertEqual(hours, 40) + hours = self.parent_calendar.get_work_hours_count( + datetime.datetime.fromisoformat("2024-07-08T00:00:00+00:00"), + datetime.datetime.fromisoformat("2024-07-14T23:59:59+00:00"), + ) + self.assertEqual(hours, 20) From 551497acb0d1fabe7925c12893ea3034f42be4fd Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Fri, 30 Aug 2024 13:51:08 +0200 Subject: [PATCH 05/15] [REF] resource_multi_week_calendar: Simplify epoch date The epoch date is hidden on the child anyway. Let's just hide it, and always make sure to get the parent's epoch date. This gets rid of the complicated computation stuff that won't backport well to v12. Signed-off-by: Carmen Bianca BAKKER --- .../models/resource_calendar.py | 50 ++++--------------- .../tests/test_calendar.py | 27 +++------- 2 files changed, 16 insertions(+), 61 deletions(-) diff --git a/resource_multi_week_calendar/models/resource_calendar.py b/resource_multi_week_calendar/models/resource_calendar.py index d57cdf434df..0854808e3d5 100644 --- a/resource_multi_week_calendar/models/resource_calendar.py +++ b/resource_multi_week_calendar/models/resource_calendar.py @@ -67,16 +67,8 @@ class ResourceCalendar(models.Model): help="""When using alternating weeks, the week which contains the specified date becomes the first week, and all subsequent weeks alternate in order.""", - # required=True, - # default="1970-01-01", - # Compute this on child calendars; write this manually on parent - # calendars. Would use 'related=', but that wouldn't work here. Although - # technically, the value of this field on child calendars isn't super - # pertinent. - compute="_compute_multi_week_epoch_date", - readonly=False, - store=True, - recursive=True, + required=True, + default="1970-01-01", ) def copy(self, default=None): @@ -131,9 +123,8 @@ def _compute_week_number(self): def _get_first_day_of_epoch_week(self): self.ensure_one() - return self.multi_week_epoch_date - timedelta( - days=self.multi_week_epoch_date.weekday() - ) + epoch_date = self.get_multi_week_epoch_date() + return epoch_date - timedelta(days=epoch_date.weekday()) def _get_week_number(self, day=None): self.ensure_one() @@ -161,16 +152,6 @@ def _compute_current_week(self): lambda item: item.week_number == current_week_number ) - @api.depends("parent_calendar_id.multi_week_epoch_date") - def _compute_multi_week_epoch_date(self): - for calendar in self: - parent = calendar.parent_calendar_id - if parent: - calendar.multi_week_epoch_date = parent.multi_week_epoch_date - else: - # A default value. - calendar.multi_week_epoch_date = "1970-01-01" - @api.constrains("parent_calendar_id", "child_calendar_ids") def _check_child_is_not_parent(self): err_str = _( @@ -204,24 +185,11 @@ def _check_child_is_not_parent(self): } ) - @api.constrains("parent_calendar_id", "multi_week_epoch_date") - def _check_epoch_date_matches_parent(self): - for calendar in self: - if calendar.parent_calendar_id: - if ( - calendar.multi_week_epoch_date - != calendar.parent_calendar_id.multi_week_epoch_date - ): - # Because the epoch date is hidden on the views of children, - # this should not happen. However, for sanity, we do this - # check anyway. - raise ValidationError( - _( - "Working Time '%s' has an epoch date which does not" - " match its Main Working Time's. This should not happen." - ) - % calendar.name - ) + def get_multi_week_epoch_date(self): + self.ensure_one() + if self.parent_calendar_id: + return self.parent_calendar_id.multi_week_epoch_date + return self.multi_week_epoch_date @api.model def _split_into_weeks(self, start_dt, end_dt): diff --git a/resource_multi_week_calendar/tests/test_calendar.py b/resource_multi_week_calendar/tests/test_calendar.py index 82cecd8a205..5f97b721167 100644 --- a/resource_multi_week_calendar/tests/test_calendar.py +++ b/resource_multi_week_calendar/tests/test_calendar.py @@ -66,11 +66,6 @@ def test_cant_add_parent_to_parent(self): } ) - def test_cant_change_epoch_date(self): - child = self.create_simple_child() - with self.assertRaises(ValidationError): - child.multi_week_epoch_date = "2001-01-01" - class TestCalendarIsMultiweek(CalendarCase): def test_solo(self): @@ -115,21 +110,6 @@ def test_children(self): class TestCalendarWeekEpoch(CalendarCase): - def test_set_epoch_on_parent(self): - child = self.create_simple_child() - self.parent_calendar.multi_week_epoch_date = "2001-01-01" - self.assertEqual(child.multi_week_epoch_date, datetime.date(2001, 1, 1)) - - def test_set_epoch_on_parent_prior_to_creation(self): - self.parent_calendar.multi_week_epoch_date = "2001-01-01" - child = self.create_simple_child() - self.assertEqual(child.multi_week_epoch_date, datetime.date(2001, 1, 1)) - - def test_unix_epoch_default(self): - self.assertEqual( - self.parent_calendar.multi_week_epoch_date, datetime.date(1970, 1, 1) - ) - @freeze_time("1970-01-08") def test_compute_current_week_no_family(self): self.assertEqual(self.parent_calendar.current_week_number, 1) @@ -200,6 +180,13 @@ def test_compute_current_week_when_day_changes(self): self.assertEqual(child.current_week_number, 2) self.assertEqual(child.current_calendar_id, child) + # 2024-07-01 is a Monday. + @freeze_time("2024-07-01") + def test_compute_current_week_non_unix(self): + child = self.create_simple_child() + self.parent_calendar.multi_week_epoch_date = "2024-07-08" + self.assertEqual(child.current_week_number, 2) + class TestMultiCalendar(CalendarCase): def setUp(self): From df1a6bae76a0887fa15004424f6b602efb006d52 Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Fri, 30 Aug 2024 17:52:29 +0200 Subject: [PATCH 06/15] [REF] resource_multi_week_calendar: Parent calendar no longer uses its attendances The idea here is that the children contain all the logic/attendances, and the parent is just a holder of children. Signed-off-by: Carmen Bianca BAKKER --- .../models/resource_calendar.py | 19 ++- .../tests/test_calendar.py | 109 +++++++++++------- .../views/resource_calendar_views.xml | 10 +- 3 files changed, 83 insertions(+), 55 deletions(-) diff --git a/resource_multi_week_calendar/models/resource_calendar.py b/resource_multi_week_calendar/models/resource_calendar.py index 0854808e3d5..4edaa993549 100644 --- a/resource_multi_week_calendar/models/resource_calendar.py +++ b/resource_multi_week_calendar/models/resource_calendar.py @@ -27,6 +27,8 @@ class ResourceCalendar(models.Model): string="Alternating Working Times", copy=True, ) + # These are all your siblings (including yourself) if you are a child, or + # all your children if you are a parent. family_calendar_ids = fields.One2many( comodel_name="resource.calendar", compute="_compute_family_calendar_ids", @@ -89,7 +91,7 @@ def copy(self, default=None): def _compute_family_calendar_ids(self): for calendar in self: parent = calendar.parent_calendar_id or calendar - calendar.family_calendar_ids = parent | parent.child_calendar_ids + calendar.family_calendar_ids = parent.child_calendar_ids @api.depends( "child_calendar_ids", @@ -113,13 +115,14 @@ def _compute_week_number(self): if parent: for week_number, sibling in enumerate( parent.child_calendar_ids.sorted(lambda item: item.week_sequence), - start=2, + start=1, ): if calendar == sibling: calendar.week_number = week_number break else: - calendar.week_number = 1 + # Parent calendars have no week number. + calendar.week_number = 0 def _get_first_day_of_epoch_week(self): self.ensure_one() @@ -128,6 +131,8 @@ def _get_first_day_of_epoch_week(self): def _get_week_number(self, day=None): self.ensure_one() + if not self.is_multi_week: + return 0 if day is None: day = fields.Date.today() if isinstance(day, datetime): @@ -142,7 +147,6 @@ def _get_week_number(self, day=None): "multi_week_epoch_date", "week_number", "family_calendar_ids", - # TODO: current date. Port company_today or add a cron. Or don't store. ) def _compute_current_week(self): for calendar in self: @@ -219,13 +223,8 @@ def _attendance_intervals_batch( start_dt, end_dt, resources=resources, domain=domain, tz=tz ) - if self.parent_calendar_id: - return self.parent_calendar_id._attendance_intervals_batch( - start_dt, end_dt, resources=resources, domain=domain, tz=tz - ) calendars_by_week = { - calendar.week_number: calendar - for calendar in self | self.child_calendar_ids + calendar.week_number: calendar for calendar in self.family_calendar_ids } results = [] diff --git a/resource_multi_week_calendar/tests/test_calendar.py b/resource_multi_week_calendar/tests/test_calendar.py index 5f97b721167..8d6e3e6167a 100644 --- a/resource_multi_week_calendar/tests/test_calendar.py +++ b/resource_multi_week_calendar/tests/test_calendar.py @@ -79,7 +79,8 @@ def test_has_child_or_parent(self): class TestCalendarWeekNumber(CalendarCase): def test_solo(self): - self.assertEqual(self.parent_calendar.week_number, 1) + # Parents don't have a week number. + self.assertEqual(self.parent_calendar.week_number, 0) def test_children(self): # The parent's sequence should not matter. @@ -99,104 +100,124 @@ def test_children(self): "week_sequence": 30, } ) - self.assertEqual(self.parent_calendar.week_number, 1) - self.assertEqual(one.week_number, 2) - self.assertEqual(two.week_number, 3) + self.assertEqual(self.parent_calendar.week_number, 0) + self.assertEqual(one.week_number, 1) + self.assertEqual(two.week_number, 2) # Change the order. one.week_sequence = 31 - self.assertEqual(one.week_number, 3) - self.assertEqual(two.week_number, 2) + self.assertEqual(one.week_number, 2) + self.assertEqual(two.week_number, 1) class TestCalendarWeekEpoch(CalendarCase): @freeze_time("1970-01-08") def test_compute_current_week_no_family(self): - self.assertEqual(self.parent_calendar.current_week_number, 1) - self.assertEqual(self.parent_calendar.current_calendar_id, self.parent_calendar) + self.assertEqual(self.parent_calendar.current_week_number, 0) + self.assertFalse(self.parent_calendar.current_calendar_id) + # 1970-01-01 is a Thursday. @freeze_time("1970-01-01") - def test_compute_current_week_same_day(self): + def test_compute_current_week_solo(self): child = self.create_simple_child() self.assertEqual(child.current_week_number, 1) - self.assertEqual(child.current_calendar_id, self.parent_calendar) + self.assertEqual(child.current_calendar_id, child) + + # 1970-01-01 is a Thursday. + @freeze_time("1970-01-01") + def test_compute_current_week_same_day(self): + child_1 = self.create_simple_child() + child_2 = self.create_simple_child() + self.assertEqual(child_1.current_week_number, 1) + self.assertEqual(child_1.current_calendar_id, child_1) + # Test against the others, too, which should have the same result. + self.assertEqual(self.parent_calendar.current_week_number, 1) + self.assertEqual(self.parent_calendar.current_calendar_id, child_1) + self.assertEqual(child_2.current_week_number, 1) + self.assertEqual(child_2.current_calendar_id, child_1) # 1969-12-29 is a Monday. @freeze_time("1969-12-29") def test_compute_current_week_first_day_of_week(self): - child = self.create_simple_child() - self.assertEqual(child.current_week_number, 1) - self.assertEqual(child.current_calendar_id, self.parent_calendar) + child_1 = self.create_simple_child() + self.create_simple_child() + self.assertEqual(child_1.current_week_number, 1) + self.assertEqual(child_1.current_calendar_id, child_1) # 1969-12-28 is a Sunday. @freeze_time("1969-12-28") def test_compute_current_week_one_week_ago(self): - child = self.create_simple_child() - self.assertEqual(child.current_week_number, 2) - self.assertEqual(child.current_calendar_id, child) - # Test against parent, too, which should have the same result. - self.assertEqual(self.parent_calendar.current_week_number, 2) - self.assertEqual(self.parent_calendar.current_calendar_id, child) + child_1 = self.create_simple_child() + child_2 = self.create_simple_child() + self.assertEqual(child_1.current_week_number, 2) + self.assertEqual(child_1.current_calendar_id, child_2) # 1970-01-04 is a Sunday. @freeze_time("1970-01-04") def test_compute_current_week_last_day_of_week(self): - child = self.create_simple_child() - self.assertEqual(child.current_week_number, 1) - self.assertEqual(child.current_calendar_id, self.parent_calendar) + child_1 = self.create_simple_child() + self.create_simple_child() + self.assertEqual(child_1.current_week_number, 1) + self.assertEqual(child_1.current_calendar_id, child_1) # 1970-01-05 is a Monday. @freeze_time("1970-01-05") def test_compute_current_week_next_week(self): - child = self.create_simple_child() - self.assertEqual(child.current_week_number, 2) - self.assertEqual(child.current_calendar_id, child) + child_1 = self.create_simple_child() + child_2 = self.create_simple_child() + self.assertEqual(child_1.current_week_number, 2) + self.assertEqual(child_1.current_calendar_id, child_2) # 1970-01-12 is a Monday. @freeze_time("1970-01-12") def test_compute_current_week_in_two_weeks(self): - child = self.create_simple_child() - self.assertEqual(child.current_week_number, 1) - self.assertEqual(child.current_calendar_id, self.parent_calendar) + child_1 = self.create_simple_child() + self.create_simple_child() + self.assertEqual(child_1.current_week_number, 1) + self.assertEqual(child_1.current_calendar_id, child_1) # 1970-01-12 is a Monday. @freeze_time("1970-01-12") def test_compute_current_week_in_two_weeks_three_calendars(self): self.create_simple_child() - child_2 = self.create_simple_child() - self.assertEqual(child_2.current_week_number, 3) - self.assertEqual(child_2.current_calendar_id, child_2) + self.create_simple_child() + child_3 = self.create_simple_child() + self.assertEqual(child_3.current_week_number, 3) + self.assertEqual(child_3.current_calendar_id, child_3) # 1970-01-04 is a Sunday. @freeze_time("1970-01-04") def test_compute_current_week_when_day_changes(self): - child = self.create_simple_child() - self.assertEqual(child.current_week_number, 1) - self.assertEqual(child.current_calendar_id, self.parent_calendar) + child_1 = self.create_simple_child() + child_2 = self.create_simple_child() + self.assertEqual(child_1.current_week_number, 1) + self.assertEqual(child_1.current_calendar_id, child_1) with freeze_time("1970-01-05"): # This re-compute shouldn't technically be needed... Maybe there's a # cache? - child._compute_current_week() - self.assertEqual(child.current_week_number, 2) - self.assertEqual(child.current_calendar_id, child) + child_1._compute_current_week() + self.assertEqual(child_1.current_week_number, 2) + self.assertEqual(child_1.current_calendar_id, child_2) # 2024-07-01 is a Monday. @freeze_time("2024-07-01") def test_compute_current_week_non_unix(self): - child = self.create_simple_child() + child_1 = self.create_simple_child() + self.create_simple_child() self.parent_calendar.multi_week_epoch_date = "2024-07-08" - self.assertEqual(child.current_week_number, 2) + self.assertEqual(child_1.current_week_number, 2) class TestMultiCalendar(CalendarCase): def setUp(self): super().setUpClass() - # The parent calendar has attendances by default: Every weekday from 8 + # The child_1 calendar has attendances by default: Every weekday from 8 # to 12, and 13 to 17. - self.child_calendar = self.create_simple_child() + self.child_1 = self.create_simple_child() + self.child_2 = self.create_simple_child() # In the child calendar, only work the mornings. - self.child_calendar.attendance_ids = False - self.child_calendar.attendance_ids = [ + self.child_2.attendance_ids = False + self.child_2.attendance_ids = [ Command.create( { "name": "Monday Morning", @@ -255,7 +276,7 @@ def test_count_work_hours_two_weeks(self): def test_count_work_hours_from_child(self): # It doesn't matter whether you call the method from the child. - hours = self.child_calendar.get_work_hours_count( + hours = self.child_2.get_work_hours_count( datetime.datetime.fromisoformat("2024-07-01T00:00:00+00:00"), datetime.datetime.fromisoformat("2024-07-14T23:59:59+00:00"), ) diff --git a/resource_multi_week_calendar/views/resource_calendar_views.xml b/resource_multi_week_calendar/views/resource_calendar_views.xml index 37ca1a9a12f..68960cf5e13 100644 --- a/resource_multi_week_calendar/views/resource_calendar_views.xml +++ b/resource_multi_week_calendar/views/resource_calendar_views.xml @@ -33,11 +33,19 @@ name="multi_week_epoch_date" attrs="{'invisible': [('parent_calendar_id', '!=', False)]}" /> - + + + + {'invisible': [('child_calendar_ids', '!=', [])]} + + From a8c3c888a067134bf03af3c262fac9c6fce24406 Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Mon, 2 Sep 2024 10:53:24 +0200 Subject: [PATCH 07/15] [IMP] resource_multi_week_calendar: Display only parents by default Signed-off-by: Carmen Bianca BAKKER --- .../views/resource_calendar_views.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/resource_multi_week_calendar/views/resource_calendar_views.xml b/resource_multi_week_calendar/views/resource_calendar_views.xml index 68960cf5e13..64ec8c05945 100644 --- a/resource_multi_week_calendar/views/resource_calendar_views.xml +++ b/resource_multi_week_calendar/views/resource_calendar_views.xml @@ -48,4 +48,23 @@ + + + resource.calendar.search + resource.calendar + + + + + + + + + + {'search_default_only_parent_calendars': 1} + From 51269f6df1096e7567a0ed08f6cf5cab3c440b6e Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Mon, 2 Sep 2024 11:12:20 +0200 Subject: [PATCH 08/15] [IMP] resource_multi_week_calendar: Add roadmap Signed-off-by: Carmen Bianca BAKKER --- resource_multi_week_calendar/README.rst | 10 +++++++ .../readme/ROADMAP.rst | 6 ++++ .../static/description/index.html | 29 ++++++++++++------- 3 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 resource_multi_week_calendar/readme/ROADMAP.rst diff --git a/resource_multi_week_calendar/README.rst b/resource_multi_week_calendar/README.rst index 15f17a5c500..40b75e0aa7d 100644 --- a/resource_multi_week_calendar/README.rst +++ b/resource_multi_week_calendar/README.rst @@ -48,6 +48,16 @@ which does this is ``hr_holidays``. .. contents:: :local: +Known issues / Roadmap +====================== + +This module is a template for building on top of. It _will_ need glue modules to +work with various other modules. Most notably, ``hr_holidays`` will not work +without modification. + +The existing base Odoo two-week calendar functionality is hidden rather than +disabled. This may or may not be desirable. + Bug Tracker =========== diff --git a/resource_multi_week_calendar/readme/ROADMAP.rst b/resource_multi_week_calendar/readme/ROADMAP.rst new file mode 100644 index 00000000000..9700b4c3479 --- /dev/null +++ b/resource_multi_week_calendar/readme/ROADMAP.rst @@ -0,0 +1,6 @@ +This module is a template for building on top of. It _will_ need glue modules to +work with various other modules. Most notably, ``hr_holidays`` will not work +without modification. + +The existing base Odoo two-week calendar functionality is hidden rather than +disabled. This may or may not be desirable. diff --git a/resource_multi_week_calendar/static/description/index.html b/resource_multi_week_calendar/static/description/index.html index 20cedf4bf9a..4734fe505a8 100644 --- a/resource_multi_week_calendar/static/description/index.html +++ b/resource_multi_week_calendar/static/description/index.html @@ -383,17 +383,26 @@

Multi-week calendars

Table of contents

+
+

Known issues / Roadmap

+

This module is a template for building on top of. It _will_ need glue modules to +work with various other modules. Most notably, hr_holidays will not work +without modification.

+

The existing base Odoo two-week calendar functionality is hidden rather than +disabled. This may or may not be desirable.

+
-

Bug Tracker

+

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 @@ -401,15 +410,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Coop IT Easy SC
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose From a1a39c1aee034e9d2fb8898f4319a75e4c05b91c Mon Sep 17 00:00:00 2001 From: Carmen Bianca BAKKER Date: Mon, 2 Sep 2024 11:17:53 +0200 Subject: [PATCH 09/15] [FIX] resource_multi_week_calendar: Hide two-week calendar Signed-off-by: Carmen Bianca BAKKER --- .../views/resource_calendar_views.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resource_multi_week_calendar/views/resource_calendar_views.xml b/resource_multi_week_calendar/views/resource_calendar_views.xml index 64ec8c05945..a63a6c36a01 100644 --- a/resource_multi_week_calendar/views/resource_calendar_views.xml +++ b/resource_multi_week_calendar/views/resource_calendar_views.xml @@ -5,6 +5,10 @@ resource.calendar + @@ -49,8 +45,14 @@ - - + + + + + {'invisible': [('child_calendar_ids', '!=', [])]} + + + {'invisible': [('child_calendar_ids', '!=', [])]} @@ -63,7 +65,7 @@ resource.calendar - +