diff --git a/company_today/tests/test_today.py b/company_today/tests/test_today.py index a97654934..33b97d507 100644 --- a/company_today/tests/test_today.py +++ b/company_today/tests/test_today.py @@ -12,7 +12,7 @@ class TestToday(SavepointCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.company = cls.ref("base.main_company") + cls.company = cls.env.ref("base.main_company") def test_today(self): self.company.cron_update_today() diff --git a/oca_dependencies.txt b/oca_dependencies.txt index 4575bb7b2..735a74f52 100644 --- a/oca_dependencies.txt +++ b/oca_dependencies.txt @@ -1,6 +1,7 @@ # List the OCA project dependencies, one per line # Add a repository url and branch if you need a forked version +hr https://github.com/coopiteasy/hr partner-contact timesheet obeesdoo https://github.com/beescoop/Obeesdoo diff --git a/resource_multi_week_work_time_from_contracts/__init__.py b/resource_multi_week_work_time_from_contracts/__init__.py new file mode 100644 index 000000000..3eb78877c --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/__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_work_time_from_contracts/__manifest__.py b/resource_multi_week_work_time_from_contracts/__manifest__.py new file mode 100644 index 000000000..0597db756 --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/__manifest__.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +{ + "name": "Multi-week calendars with work time from contracts", + "summary": """ + A compatibility module.""", + "version": "12.0.1.0.0", + "category": "Hidden", + "website": "https://coopiteasy.be", + "author": "Coop IT Easy SC", + "maintainers": ["carmenbianca"], + "license": "AGPL-3", + "application": False, + "depends": [ + "resource_multi_week_calendar", + "resource_work_time_from_contracts", + ], + "auto_install": True, +} diff --git a/resource_multi_week_work_time_from_contracts/models/__init__.py b/resource_multi_week_work_time_from_contracts/models/__init__.py new file mode 100644 index 000000000..54f6e2dad --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/models/__init__.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from . import resource_calendar +from . import resource_mixin diff --git a/resource_multi_week_work_time_from_contracts/models/hr_contract.py b/resource_multi_week_work_time_from_contracts/models/hr_contract.py new file mode 100644 index 000000000..c7252be83 --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/models/hr_contract.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class HrContract(models.Model): + _inherit = "hr.contract" + + # Add a domain. + resource_calendar_id = fields.Many2one( + "resource.calendar", + domain="[('parent_calendar_id', '=', False)]", + ) diff --git a/resource_multi_week_work_time_from_contracts/models/resource_calendar.py b/resource_multi_week_work_time_from_contracts/models/resource_calendar.py new file mode 100644 index 000000000..b72f72a0b --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/models/resource_calendar.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +import datetime + +from odoo import models + + +class ResourceCalendar(models.Model): + _inherit = "resource.calendar" + + def _get_first_attendance(self, date_from): + if not self.is_multi_week: + return super()._get_first_attendance(date_from) + candidate = super( + ResourceCalendar, self._get_multi_week_calendar(date_from) + )._get_first_attendance(date_from) + # Week numbers not equal. This means we are AFTER any of the attendances + # in the target week. So get the first attendance of the succeeding + # week. + if candidate and candidate[0].isocalendar()[1] != date_from.isocalendar()[1]: + candidate = None + days_to_monday = (7 - date_from.weekday()) % 7 or 7 + new_date_from = date_from + # Keep searching the Mondays of succeeding weeks until a match is + # found. Loop as many times as there are calendars. + for _ in self.multi_week_calendar_ids: + new_date_from = new_date_from + datetime.timedelta(days=days_to_monday) + candidate = super( + ResourceCalendar, self._get_multi_week_calendar(new_date_from) + )._get_first_attendance(new_date_from) + if candidate: + break + days_to_monday = 7 + return candidate + + def _get_last_attendance(self, date_to): + if not self.is_multi_week: + return super()._get_last_attendance(date_to) + candidate = super( + ResourceCalendar, self._get_multi_week_calendar(date_to) + )._get_last_attendance(date_to) + # Week numbers not equal. This means we are BEFORE any of the + # attendances in the target week. So get the last attendance of the + # preceding week. + if candidate and candidate[0].isocalendar()[1] != date_to.isocalendar()[1]: + candidate = None + days_since_sunday = date_to.weekday() + 1 + new_date_to = date_to + # Keep searching the Sundays of preceding weeks until a match is + # found. Loop as many times as there are calendars. + for _ in self.multi_week_calendar_ids: + new_date_to = new_date_to - datetime.timedelta(days=days_since_sunday) + candidate = super( + ResourceCalendar, self._get_multi_week_calendar(new_date_to) + )._get_last_attendance(new_date_to) + if candidate: + break + days_since_sunday = 7 + return candidate diff --git a/resource_multi_week_work_time_from_contracts/models/resource_mixin.py b/resource_multi_week_work_time_from_contracts/models/resource_mixin.py new file mode 100644 index 000000000..b9c22acc6 --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/models/resource_mixin.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import models + + +class ResourceMixing(models.AbstractModel): + _inherit = "resource.mixin" + + def get_calendar_for_date(self, date): + calendar = super().get_calendar_for_date(date) + return calendar._get_multi_week_calendar(day=date) diff --git a/resource_multi_week_work_time_from_contracts/readme/CONTRIBUTORS.rst b/resource_multi_week_work_time_from_contracts/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..f1ac67577 --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Coop IT Easy SC `_: + + * Carmen Bianca BAKKER diff --git a/resource_multi_week_work_time_from_contracts/readme/DESCRIPTION.rst b/resource_multi_week_work_time_from_contracts/readme/DESCRIPTION.rst new file mode 100644 index 000000000..ef5a6edbb --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +A compatibility module. diff --git a/resource_multi_week_work_time_from_contracts/tests/__init__.py b/resource_multi_week_work_time_from_contracts/tests/__init__.py new file mode 100644 index 000000000..c17b58251 --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/tests/__init__.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from . import common +from . import test_calendar +from . import test_work_time diff --git a/resource_multi_week_work_time_from_contracts/tests/common.py b/resource_multi_week_work_time_from_contracts/tests/common.py new file mode 100644 index 000000000..6ece7bd0f --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/tests/common.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo.tests.common import SavepointCase + + +class TestCalendarCommon(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Calendar = cls.env["resource.calendar"] + cls.parent_calendar = cls.Calendar.create( + { + "name": "Parent", + # This date is a Monday. + "multi_week_epoch_date": "2024-07-08", + } + ) + cls.child_1 = cls.Calendar.create( + { + "name": "Child 1", + "parent_calendar_id": cls.parent_calendar.id, + "week_sequence": 10, + "attendance_ids": [ + ( + 0, + False, + { + "name": "Wednesday morning", + "dayofweek": "2", + "hour_from": 8, + "hour_to": 12, + }, + ), + ( + 0, + False, + { + "name": "Thursday morning", + "dayofweek": "3", + "hour_from": 8, + "hour_to": 12, + }, + ), + ], + } + ) + cls.child_2 = cls.Calendar.create( + { + "name": "Child 2", + "parent_calendar_id": cls.parent_calendar.id, + "week_sequence": 20, + "attendance_ids": [ + ( + 0, + False, + { + "name": "Monday morning", + "dayofweek": "0", + "hour_from": 8, + "hour_to": 12, + }, + ), + ( + 0, + False, + { + "name": "Friday morning", + "dayofweek": "4", + "hour_from": 8, + "hour_to": 12, + }, + ), + ], + } + ) diff --git a/resource_multi_week_work_time_from_contracts/tests/test_calendar.py b/resource_multi_week_work_time_from_contracts/tests/test_calendar.py new file mode 100644 index 000000000..7e2ef55f1 --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/tests/test_calendar.py @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +import datetime + +from .common import TestCalendarCommon + + +class TestCalendar(TestCalendarCommon): + def test_first_last_attendance_both_weeks(self): + result = self.parent_calendar.get_first_last_attendance( + datetime.date(2024, 7, 8), + datetime.date(2024, 7, 21), + ) + # First date/attendance is the Wednesday. + self.assertEqual(result[0][0], datetime.date(2024, 7, 10)) + self.assertEqual(result[0][1], self.child_1.attendance_ids[0]) + # Last date/attendance is the Friday. + self.assertEqual(result[1][0], datetime.date(2024, 7, 19)) + self.assertEqual(result[1][1], self.child_2.attendance_ids[-1]) + + def test_first_last_attendance_middle_to_middle(self): + result = self.parent_calendar.get_first_last_attendance( + datetime.date(2024, 7, 11), + datetime.date(2024, 7, 16), + ) + # First date/attendance is the Thursday. + self.assertEqual(result[0][0], datetime.date(2024, 7, 11)) + self.assertEqual(result[0][1], self.child_1.attendance_ids[1]) + # Last date/attendance is the Monday. + self.assertEqual(result[1][0], datetime.date(2024, 7, 15)) + self.assertEqual(result[1][1], self.child_2.attendance_ids[0]) + + def test_first_last_attendance_middle_to_before(self): + result = self.parent_calendar.get_first_last_attendance( + datetime.date(2024, 7, 16), + datetime.date(2024, 7, 22), + ) + # First date/attendance is the Friday. + self.assertEqual(result[0][0], datetime.date(2024, 7, 19)) + self.assertEqual(result[0][1], self.child_2.attendance_ids[1]) + # Last date/attendance is the Friday of the week preceding the target date. + self.assertEqual(result[1][0], datetime.date(2024, 7, 19)) + self.assertEqual(result[1][1], self.child_2.attendance_ids[1]) + + def test_first_last_attendance_middle_to_before_skip_a_week(self): + self.Calendar.create( + { + "name": "Empty child 3", + "parent_calendar_id": self.parent_calendar.id, + "week_sequence": 30, # After week 2. + "attendance_ids": [], + } + ) + result = self.parent_calendar.get_first_last_attendance( + # Multi-week 2 + datetime.date(2024, 7, 16), + # Multi-week 1 + datetime.date(2024, 7, 30), + ) + # First date/attendance is the Friday. + self.assertEqual(result[0][0], datetime.date(2024, 7, 19)) + self.assertEqual(result[0][1], self.child_2.attendance_ids[1]) + # Last date/attendance is the Friday of the second week preceding the + # target date. + self.assertEqual(result[1][0], datetime.date(2024, 7, 19)) + self.assertEqual(result[1][1], self.child_2.attendance_ids[1]) + + def test_first_last_attendance_after_to_middle(self): + result = self.parent_calendar.get_first_last_attendance( + datetime.date(2024, 7, 12), + datetime.date(2024, 7, 17), + ) + # First date/attendance is the Monday of the week succeeding the target date. + self.assertEqual(result[0][0], datetime.date(2024, 7, 15)) + self.assertEqual(result[0][1], self.child_2.attendance_ids[0]) + # Last date/attendance is the Monday. + self.assertEqual(result[1][0], datetime.date(2024, 7, 15)) + self.assertEqual(result[1][1], self.child_2.attendance_ids[0]) + + def test_first_last_attendance_after_to_middle_skip_a_week(self): + self.Calendar.create( + { + "name": "Empty child 3", + "parent_calendar_id": self.parent_calendar.id, + # In between week 1 and 2, meaning child 2 is now week 3. + "week_sequence": 15, + "attendance_ids": [], + } + ) + result = self.parent_calendar.get_first_last_attendance( + # Multi-week 1. + datetime.date(2024, 7, 12), + # Multi-week 3. + datetime.date(2024, 7, 24), + ) + # First date/attendance is the Monday of the second week succeeding the + # target date. + self.assertEqual(result[0][0], datetime.date(2024, 7, 22)) + self.assertEqual(result[0][1], self.child_2.attendance_ids[0]) + # Last date/attendance is the Monday. + self.assertEqual(result[1][0], datetime.date(2024, 7, 22)) + self.assertEqual(result[1][1], self.child_2.attendance_ids[0]) diff --git a/resource_multi_week_work_time_from_contracts/tests/test_work_time.py b/resource_multi_week_work_time_from_contracts/tests/test_work_time.py new file mode 100644 index 000000000..62cb313f1 --- /dev/null +++ b/resource_multi_week_work_time_from_contracts/tests/test_work_time.py @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: 2024 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +import datetime + +import pytz + +from .common import TestCalendarCommon + + +class TestWorkTime(TestCalendarCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.timezone = pytz.timezone(cls.env.user.tz) + + cls.user1 = cls.env["res.users"].create( + { + "name": "User 1", + "login": "user1", + "password": "user1", + "groups_id": [(6, 0, cls.env.ref("base.group_user").ids)], + } + ) + + cls.employee1 = cls.env["hr.employee"].create( + { + "name": "Employee 1", + "user_id": cls.user1.id, + "address_id": cls.user1.partner_id.id, + } + ) + + def local_datetime(self, year, month, day, *args, **kwargs): + """ + Create a datetime with the local timezone from local time values + """ + return self.timezone.localize( + datetime.datetime(year, month, day, *args, **kwargs) + ) + + def test_single_contract(self): + """A very rudimentary test that checks whether the glue module works. It + does not test any corner cases. + """ + self.env["hr.contract"].create( + { + "name": "Contract 1", + "employee_id": self.employee1.id, + "wage": 0.0, + "resource_calendar_id": self.parent_calendar.id, + "date_start": "2020-10-18", + } + ) + self.assertEqual( + self.employee1.list_work_time_per_day( + self.local_datetime(2024, 7, 8), self.local_datetime(2024, 7, 21) + ), + [ + (datetime.date(2024, 7, 10), 4), + (datetime.date(2024, 7, 11), 4), + (datetime.date(2024, 7, 15), 4), + (datetime.date(2024, 7, 19), 4), + ], + ) diff --git a/setup/resource_multi_week_work_time_from_contracts/odoo/addons/resource_multi_week_work_time_from_contracts b/setup/resource_multi_week_work_time_from_contracts/odoo/addons/resource_multi_week_work_time_from_contracts new file mode 120000 index 000000000..16b845310 --- /dev/null +++ b/setup/resource_multi_week_work_time_from_contracts/odoo/addons/resource_multi_week_work_time_from_contracts @@ -0,0 +1 @@ +../../../../resource_multi_week_work_time_from_contracts \ No newline at end of file diff --git a/setup/resource_multi_week_work_time_from_contracts/setup.py b/setup/resource_multi_week_work_time_from_contracts/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/resource_multi_week_work_time_from_contracts/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/test-requirements.txt b/test-requirements.txt index 85c977787..33ebd56d8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ +git+https://github.com/coopiteasy/hr@12.0-mig-resource_multi_week_calendar#subdirectory=setup/resource_multi_week_calendar git+https://github.com/beescoop/obeesdoo@12.0#subdirectory=setup/beesdoo_base git+https://github.com/beescoop/obeesdoo@12.0#subdirectory=setup/beesdoo_product git+https://github.com/beescoop/obeesdoo@12.0#subdirectory=setup/beesdoo_product_label @@ -16,3 +17,4 @@ git+https://github.com/beescoop/obeesdoo@12.0#subdirectory=setup/product_main_su git+https://github.com/beescoop/obeesdoo@12.0#subdirectory=setup/sale_adapt_price_wizard git+https://github.com/beescoop/obeesdoo@12.0#subdirectory=setup/sale_suggested_price git+https://github.com/beescoop/obeesdoo@12.0#subdirectory=setup/purchase_order_responsible +freezegun