diff --git a/resource_multi_week_calendar/README.rst b/resource_multi_week_calendar/README.rst new file mode 100644 index 00000000000..40b75e0aa7d --- /dev/null +++ b/resource_multi_week_calendar/README.rst @@ -0,0 +1,109 @@ +==================== +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: + +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 +=========== + +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..0b3a4368d21 --- /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": "12.0.1.0.0", + "category": "Hidden", + "website": "https://github.com/OCA/hr", + "author": "Coop IT Easy SC, Odoo Community Association (OCA)", + "maintainers": ["carmenbianca"], + "license": "AGPL-3", + "application": False, + "depends": [ + "resource", + ], + "data": [ + "views/resource_calendar_views.xml", + ], +} 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..640310fe552 --- /dev/null +++ b/resource_multi_week_calendar/models/resource_calendar.py @@ -0,0 +1,248 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +import math +from datetime import datetime, timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.addons.resource.models.resource import Intervals + + +class ResourceCalendar(models.Model): + _inherit = "resource.calendar" + + parent_calendar_id = fields.Many2one( + comodel_name="resource.calendar", + domain=[("parent_calendar_id", "=", False)], + ondelete="cascade", + string="Main Working Time", + ) + child_calendar_ids = fields.One2many( + comodel_name="resource.calendar", + inverse_name="parent_calendar_id", + 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. This is not a sorted set. + multi_week_calendar_ids = fields.One2many( + comodel_name="resource.calendar", + compute="_compute_multi_week_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 + # 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 order is preserved between the identical + # elements. The elements of child_calendar_ids are always sorted by _order, + # which is id by default. The value may not be unique when new calendars are + # added. + week_sequence = fields.Integer(default=0) + week_number = fields.Integer( + compute="_compute_week_number", + store=True, + recursive=True, + ) + current_week_number = fields.Integer( + compute="_compute_current_week", + recursive=True, + ) + current_multi_week_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", + ) + + def copy(self, default=None): + self.ensure_one() + if default is None: + default = {} + sequences = sorted(self.multi_week_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", + "parent_calendar_id.child_calendar_ids", + ) + def _compute_multi_week_calendar_ids(self): + for calendar in self: + parent = calendar.parent_calendar_id or calendar + calendar.multi_week_calendar_ids = 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", + "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=1, + ): + if calendar == sibling: + calendar.week_number = week_number + break + else: + # Parent calendars have no week number. + calendar.week_number = 0 + + def _get_first_day_of_epoch_week(self): + self.ensure_one() + 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() + if not self.is_multi_week: + return 0 + if day is None: + day = fields.Date.today() + if isinstance(day, datetime): + day = day.date() + calendar_count = len(self.multi_week_calendar_ids) + weeks_since_epoch = math.floor( + (day - self._get_first_day_of_epoch_week()).days / 7 + ) + return (weeks_since_epoch % calendar_count) + 1 + + def _get_multi_week_calendar(self, day=None): + self.ensure_one() + if not self.is_multi_week: + return self + week_number = self._get_week_number(day=day) + # Should return a 1-item recordset. If it does not, we've hit a bug. + return self.multi_week_calendar_ids.filtered( + lambda item: item.week_number == week_number + ) + + @api.depends( + "multi_week_epoch_date", + "week_number", + "multi_week_calendar_ids", + ) + def _compute_current_week(self): + for calendar in self: + current_calendar = calendar._get_multi_week_calendar() + calendar.current_multi_week_calendar_id = current_calendar + calendar.current_week_number = current_calendar.week_number + + @api.constrains("parent_calendar_id", "child_calendar_ids") + def _check_child_is_not_parent(self): + err_str = _( + "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 + % { + "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 + # has a parent. + if ( + calendar.parent_calendar_id + and calendar.parent_calendar_id.parent_calendar_id + ): + raise ValidationError( + err_str + % { + "name": calendar.parent_calendar_id.name, + "child": calendar.name, + "parent": calendar.parent_calendar_id.parent_calendar_id.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): + # 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(self, start_dt, end_dt, resource=None): + self.ensure_one() + if not self.is_multi_week: + return super()._attendance_intervals(start_dt, end_dt, resource=resource) + + calendars_by_week = { + calendar.week_number: calendar for calendar in self.multi_week_calendar_ids + } + result = Intervals() + + # 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): + result |= 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(week_start, week_end, resource=resource) + + return result 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/readme/ROADMAP.rst b/resource_multi_week_calendar/readme/ROADMAP.rst new file mode 100644 index 00000000000..31b06e98e14 --- /dev/null +++ b/resource_multi_week_calendar/readme/ROADMAP.rst @@ -0,0 +1,14 @@ +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 module may need improvements for timezone handling; this is currently +untested. ``_split_into_weeks`` splits weeks on the timezone of the datetime +objects passed to it instead of on the timezone of the calendar. The calculation +of the current week number uses ``fields.Date.today()`` instead of the +environment's or calendar's timezone. Finally, child calendars may have a +different timezone compared to their parent, which is probably not a desired +feature. + +This module assumes that a week always starts on a Monday. Upstream Odoo appears +to do the same, but this may not be desired by certain audiences. 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..4734fe505a8 --- /dev/null +++ b/resource_multi_week_calendar/static/description/index.html @@ -0,0 +1,444 @@ + + + + + +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

+ +
+

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

+

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..a1b89b54d1b --- /dev/null +++ b/resource_multi_week_calendar/tests/test_calendar.py @@ -0,0 +1,307 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +import datetime + +from freezegun import freeze_time + +from odoo.exceptions import ValidationError +from odoo.tests.common import SavepointCase + + +class CalendarCase(SavepointCase): + @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( + { + "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, + } + ) + parent_of_parent = self.Calendar.create( + { + "name": "Parent of parent", + # This value is kind of arbitrary here. + "week_sequence": 2, + } + ) + with self.assertRaises(ValidationError): + parent_of_parent.child_calendar_ids = self.parent_calendar + + +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(CalendarCase): + def test_solo(self): + # 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. + 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, 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, 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, 0) + self.assertEqual( + self.parent_calendar.current_multi_week_calendar_id, self.parent_calendar + ) + + # 1970-01-01 is a Thursday. + @freeze_time("1970-01-01") + def test_compute_current_week_solo(self): + child = self.create_simple_child() + self.assertEqual(child.current_week_number, 1) + self.assertEqual(child.current_multi_week_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_multi_week_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_multi_week_calendar_id, child_1) + self.assertEqual(child_2.current_week_number, 1) + self.assertEqual(child_2.current_multi_week_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_1 = self.create_simple_child() + self.create_simple_child() + self.assertEqual(child_1.current_week_number, 1) + self.assertEqual(child_1.current_multi_week_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_1 = self.create_simple_child() + child_2 = self.create_simple_child() + self.assertEqual(child_1.current_week_number, 2) + self.assertEqual(child_1.current_multi_week_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_1 = self.create_simple_child() + self.create_simple_child() + self.assertEqual(child_1.current_week_number, 1) + self.assertEqual(child_1.current_multi_week_calendar_id, child_1) + + # 1970-01-05 is a Monday. + @freeze_time("1970-01-05") + def test_compute_current_week_next_week(self): + 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_multi_week_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_1 = self.create_simple_child() + self.create_simple_child() + self.assertEqual(child_1.current_week_number, 1) + self.assertEqual(child_1.current_multi_week_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() + self.create_simple_child() + child_3 = self.create_simple_child() + self.assertEqual(child_3.current_week_number, 3) + self.assertEqual(child_3.current_multi_week_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_1 = self.create_simple_child() + child_2 = self.create_simple_child() + self.assertEqual(child_1.current_week_number, 1) + self.assertEqual(child_1.current_multi_week_calendar_id, child_1) + with freeze_time("1970-01-05"): + # This re-compute shouldn't technically be needed... Maybe there's a + # cache? + child_1._compute_current_week() + self.assertEqual(child_1.current_week_number, 2) + self.assertEqual(child_1.current_multi_week_calendar_id, child_2) + + # 2024-07-01 is a Monday. + @freeze_time("2024-07-01") + def test_compute_current_week_non_unix(self): + child_1 = self.create_simple_child() + self.create_simple_child() + self.parent_calendar.multi_week_epoch_date = "2024-07-08" + self.assertEqual(child_1.current_week_number, 2) + + +class TestMultiCalendar(CalendarCase): + def setUp(self): + super().setUpClass() + # The child_1 calendar has attendances by default: Every weekday from 8 + # to 12, and 13 to 17. + self.child_1 = self.create_simple_child() + self.child_2 = self.create_simple_child() + # In the child calendar, only work the mornings. + self.child_2.attendance_ids = False + self.child_2.attendance_ids = [ + ( + 0, + False, + { + "name": "Monday Morning", + "dayofweek": "0", + "hour_from": 8, + "hour_to": 12, + "day_period": "morning", + }, + ), + ( + 0, + False, + { + "name": "Tuesday Morning", + "dayofweek": "1", + "hour_from": 8, + "hour_to": 12, + "day_period": "morning", + }, + ), + ( + 0, + False, + { + "name": "Wednesday Morning", + "dayofweek": "2", + "hour_from": 8, + "hour_to": 12, + "day_period": "morning", + }, + ), + ( + 0, + False, + { + "name": "Thursday Morning", + "dayofweek": "3", + "hour_from": 8, + "hour_to": 12, + "day_period": "morning", + }, + ), + ( + 0, + False, + { + "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_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"), + ) + 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) 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..b927868b326 --- /dev/null +++ b/resource_multi_week_calendar/views/resource_calendar_views.xml @@ -0,0 +1,81 @@ + + + + resource.calendar.form + resource.calendar + + + + {'invisible': [('child_calendar_ids', '!=', [])]} + + + + + + + + + + + + + + + + + + + + + + + + {'invisible': [('child_calendar_ids', '!=', [])]} + + + + + {'invisible': [('child_calendar_ids', '!=', [])]} + + + + + + + resource.calendar.search + resource.calendar + + + + + + + + + + {'search_default_only_parent_calendars': 1} + + 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, +)