From acb013b52e9d3e6acda5d7aa62e5477d5115d4e4 Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Thu, 10 Aug 2023 11:50:27 +0200 Subject: [PATCH 1/7] [ADD] hr_attendance_missing_days: Generation of attendance for working days without one --- hr_attendance_missing_days/README.rst | 79 ++++ hr_attendance_missing_days/__init__.py | 4 + hr_attendance_missing_days/__manifest__.py | 21 + .../data/hr_attendance_reason.xml | 7 + hr_attendance_missing_days/data/ir_cron.xml | 20 + hr_attendance_missing_days/models/__init__.py | 4 + .../models/hr_employee.py | 72 +++ .../models/res_company.py | 16 + .../models/res_config_settings.py | 13 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 7 + .../static/description/index.html | 426 ++++++++++++++++++ hr_attendance_missing_days/tests/__init__.py | 4 + .../tests/test_attendance.py | 43 ++ .../views/res_config_settings_views.xml | 44 ++ .../odoo/addons/hr_attendance_missing_days | 1 + setup/hr_attendance_missing_days/setup.py | 6 + 17 files changed, 768 insertions(+) create mode 100644 hr_attendance_missing_days/README.rst create mode 100644 hr_attendance_missing_days/__init__.py create mode 100644 hr_attendance_missing_days/__manifest__.py create mode 100644 hr_attendance_missing_days/data/hr_attendance_reason.xml create mode 100644 hr_attendance_missing_days/data/ir_cron.xml create mode 100644 hr_attendance_missing_days/models/__init__.py create mode 100644 hr_attendance_missing_days/models/hr_employee.py create mode 100644 hr_attendance_missing_days/models/res_company.py create mode 100644 hr_attendance_missing_days/models/res_config_settings.py create mode 100644 hr_attendance_missing_days/readme/CONTRIBUTORS.rst create mode 100644 hr_attendance_missing_days/readme/DESCRIPTION.rst create mode 100644 hr_attendance_missing_days/static/description/index.html create mode 100644 hr_attendance_missing_days/tests/__init__.py create mode 100644 hr_attendance_missing_days/tests/test_attendance.py create mode 100644 hr_attendance_missing_days/views/res_config_settings_views.xml create mode 120000 setup/hr_attendance_missing_days/odoo/addons/hr_attendance_missing_days create mode 100644 setup/hr_attendance_missing_days/setup.py diff --git a/hr_attendance_missing_days/README.rst b/hr_attendance_missing_days/README.rst new file mode 100644 index 00000000..45ed4cf4 --- /dev/null +++ b/hr_attendance_missing_days/README.rst @@ -0,0 +1,79 @@ +====================================== +Attendance generation for missing days +====================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_attendance-lightgray.png?logo=github + :target: https://github.com/OCA/hr_attendance/tree/15.0/hr_attendance_missing_days + :alt: OCA/hr_attendance +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr_attendance-15-0/hr_attendance-15-0-hr_attendance_missing_days + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/hr_attendance&target_branch=15.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This modules generates attendances for employee with 0 minutes for working days with missing +attendances. The configured reason is set to make it easier for filtering. This can be used +to generate pseudo attendances for working days otherwise Odoo wouldn't reduce the overtime +of the employee. + +#. Go to *Attendances > Configuration > Missing Days* +#. Select a reason to set for the created attendances + +**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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* initOS GmbH + +Contributors +~~~~~~~~~~~~ + +* initOS GmbH (initOS.com) + +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. + +This module is part of the `OCA/hr_attendance `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_attendance_missing_days/__init__.py b/hr_attendance_missing_days/__init__.py new file mode 100644 index 00000000..0f55bfa9 --- /dev/null +++ b/hr_attendance_missing_days/__init__.py @@ -0,0 +1,4 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/hr_attendance_missing_days/__manifest__.py b/hr_attendance_missing_days/__manifest__.py new file mode 100644 index 00000000..3daf6be0 --- /dev/null +++ b/hr_attendance_missing_days/__manifest__.py @@ -0,0 +1,21 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Attendance generation for missing days", + "version": "15.0.1.0.0", + "category": "Hidden", + "author": "initOS GmbH, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/hr-attendance", + "license": "AGPL-3", + "summary": "This modules generates attendances for working days without attendance", + "depends": [ + "hr_attendance_reason", + ], + "data": [ + "data/hr_attendance_reason.xml", + "data/ir_cron.xml", + "views/res_config_settings_views.xml", + ], + "installable": True, +} diff --git a/hr_attendance_missing_days/data/hr_attendance_reason.xml b/hr_attendance_missing_days/data/hr_attendance_reason.xml new file mode 100644 index 00000000..74ac587a --- /dev/null +++ b/hr_attendance_missing_days/data/hr_attendance_reason.xml @@ -0,0 +1,7 @@ + + + + System generated attendances for missing days + S-GMD + + diff --git a/hr_attendance_missing_days/data/ir_cron.xml b/hr_attendance_missing_days/data/ir_cron.xml new file mode 100644 index 00000000..ff304a4e --- /dev/null +++ b/hr_attendance_missing_days/data/ir_cron.xml @@ -0,0 +1,20 @@ + + + + Missing Attendance + 1 + days + -1 + + + + + code + + model.create_missing_attendances(datetime.datetime.now() - datetime.timedelta(days=31)) + + + diff --git a/hr_attendance_missing_days/models/__init__.py b/hr_attendance_missing_days/models/__init__.py new file mode 100644 index 00000000..e5a43d85 --- /dev/null +++ b/hr_attendance_missing_days/models/__init__.py @@ -0,0 +1,4 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import hr_employee, res_company, res_config_settings diff --git a/hr_attendance_missing_days/models/hr_employee.py b/hr_attendance_missing_days/models/hr_employee.py new file mode 100644 index 00000000..893ce2a7 --- /dev/null +++ b/hr_attendance_missing_days/models/hr_employee.py @@ -0,0 +1,72 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import datetime, time + +import pytz + +from odoo import models + +_logger = logging.getLogger(__name__) + + +def ensure_tz(dt, tz=None): + if not dt.tzinfo: + dt = pytz.utc.localize(dt) + return dt.astimezone(tz) if tz else dt + + +class Employee(models.Model): + _inherit = "hr.employee" + + def create_missing_attendances(self, date_from=None, date_to=None): + for emp in self.search([]): + emp._create_missing_attendances(date_from, date_to) + + def _create_missing_attendances(self, date_from=None, date_to=None): + self.ensure_one() + + reason = self.env.company.sudo().attendance_missing_days_reason + if not reason: + return + + if not date_from: + date_from = datetime.combine( + self.env.company.sudo().overtime_start_date, time.min + ) + + if not date_to: + date_to = datetime.now() + + date_from, date_to = map(ensure_tz, (date_from, date_to)) + + intervals = self.resource_calendar_id._work_intervals_batch(date_from, date_to) + work_dates = {} + for start, _stop, _attendance in sorted(intervals[False]): + start_date = start.date() + if start_date not in work_dates: + work_dates[start_date] = ensure_tz(start, pytz.utc).replace(tzinfo=None) + + domain = [ + ("check_in", ">=", date_from.replace(tzinfo=None)), + ("check_in", "<=", date_to.replace(tzinfo=None)), + ] + tz = pytz.timezone(self.tz) + attendances = { + ensure_tz(attendance.check_in, tz).date() + for attendance in self.attendance_ids.filtered_domain(domain) + } + + vals = [] + for missing in set(work_dates) - attendances: + vals.append( + { + "employee_id": self.id, + "check_in": work_dates[missing], + "check_out": work_dates[missing], + "attendance_reason_ids": [(4, reason.id)], + } + ) + + self.env["hr.attendance"].create(vals) diff --git a/hr_attendance_missing_days/models/res_company.py b/hr_attendance_missing_days/models/res_company.py new file mode 100644 index 00000000..8adc05d5 --- /dev/null +++ b/hr_attendance_missing_days/models/res_company.py @@ -0,0 +1,16 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + attendance_missing_days_reason = fields.Many2one( + "hr.attendance.reason", + default=lambda self: self.env.ref( + "hr_attendance_missing_days.attendance_reason_missing_days", + raise_if_not_found=False, + ), + ) diff --git a/hr_attendance_missing_days/models/res_config_settings.py b/hr_attendance_missing_days/models/res_config_settings.py new file mode 100644 index 00000000..968403a2 --- /dev/null +++ b/hr_attendance_missing_days/models/res_config_settings.py @@ -0,0 +1,13 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + attendance_missing_days_reason = fields.Many2one( + related="company_id.attendance_missing_days_reason", + readonly=False, + ) diff --git a/hr_attendance_missing_days/readme/CONTRIBUTORS.rst b/hr_attendance_missing_days/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..db386db6 --- /dev/null +++ b/hr_attendance_missing_days/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* initOS GmbH (initOS.com) diff --git a/hr_attendance_missing_days/readme/DESCRIPTION.rst b/hr_attendance_missing_days/readme/DESCRIPTION.rst new file mode 100644 index 00000000..2b6da7e0 --- /dev/null +++ b/hr_attendance_missing_days/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +This modules generates attendances for employee with 0 minutes for working days with missing +attendances. The configured reason is set to make it easier for filtering. This can be used +to generate pseudo attendances for working days otherwise Odoo wouldn't reduce the overtime +of the employee. + +#. Go to *Attendances > Configuration > Missing Days* +#. Select a reason to set for the created attendances diff --git a/hr_attendance_missing_days/static/description/index.html b/hr_attendance_missing_days/static/description/index.html new file mode 100644 index 00000000..293ff594 --- /dev/null +++ b/hr_attendance_missing_days/static/description/index.html @@ -0,0 +1,426 @@ + + + + + + +Attendance generation for missing days + + + +
+

Attendance generation for missing days

+ + +

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

+

This modules generates attendances for employee with 0 minutes for working days with missing +attendances. The configured reason is set to make it easier for filtering. This can be used +to generate pseudo attendances for working days otherwise Odoo wouldn’t reduce the overtime +of the employee.

+
    +
  1. Go to Attendances > Configuration > Missing Days
  2. +
  3. Select a reason to set for the created attendances
  4. +
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • initOS GmbH
  • +
+
+
+

Contributors

+
    +
  • initOS GmbH (initOS.com)
  • +
+
+
+

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.

+

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

+

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

+
+
+
+ + diff --git a/hr_attendance_missing_days/tests/__init__.py b/hr_attendance_missing_days/tests/__init__.py new file mode 100644 index 00000000..59d8989a --- /dev/null +++ b/hr_attendance_missing_days/tests/__init__.py @@ -0,0 +1,4 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_attendance diff --git a/hr_attendance_missing_days/tests/test_attendance.py b/hr_attendance_missing_days/tests/test_attendance.py new file mode 100644 index 00000000..e81e6e7e --- /dev/null +++ b/hr_attendance_missing_days/tests/test_attendance.py @@ -0,0 +1,43 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import TransactionCase + + +class TestAttendance(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.employee = cls.env["hr.employee"].create( + { + "name": "Test Employee", + "user_id": cls.env.user.id, + "company_id": cls.env.company.id, + "tz": "UTC", + } + ) + cls.reason = cls.env["hr.attendance.reason"].create( + { + "name": "Missing Attendance", + "code": "MA", + } + ) + + def test_attendance_creation_no_reason(self): + self.env.company.attendance_missing_days_reason = False + attendances_before = self.employee.attendance_ids + self.employee._create_missing_attendances() + attendances_after = self.employee.attendance_ids + + self.assertEqual(attendances_before, attendances_after) + + def test_attendance_creation_with_reason(self): + self.env.company.attendance_missing_days_reason = self.reason + attendances_before = self.employee.attendance_ids + self.employee._create_missing_attendances() + attendances_after = self.employee.attendance_ids + + attendances_new = attendances_after - attendances_before + self.assertTrue(attendances_new) + self.assertFalse(any(attendances_new.mapped("worked_hours"))) diff --git a/hr_attendance_missing_days/views/res_config_settings_views.xml b/hr_attendance_missing_days/views/res_config_settings_views.xml new file mode 100644 index 00000000..dc7253c7 --- /dev/null +++ b/hr_attendance_missing_days/views/res_config_settings_views.xml @@ -0,0 +1,44 @@ + + + + res.config.settings + + + +

Missing Attendance

+
+
+
+
+
+
+
+ + + + diff --git a/setup/hr_attendance_missing_days/odoo/addons/hr_attendance_missing_days b/setup/hr_attendance_missing_days/odoo/addons/hr_attendance_missing_days new file mode 120000 index 00000000..e1d52213 --- /dev/null +++ b/setup/hr_attendance_missing_days/odoo/addons/hr_attendance_missing_days @@ -0,0 +1 @@ +../../../../hr_attendance_missing_days \ No newline at end of file diff --git a/setup/hr_attendance_missing_days/setup.py b/setup/hr_attendance_missing_days/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/hr_attendance_missing_days/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From c52ca3b4fd0f7314de460c87546a9f9dacdb0113 Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Mon, 14 Aug 2023 12:28:08 +0200 Subject: [PATCH 2/7] [IMP] hr_attendance_missing_days: Add additional unittest for edge cases --- .../tests/test_attendance.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/hr_attendance_missing_days/tests/test_attendance.py b/hr_attendance_missing_days/tests/test_attendance.py index e81e6e7e..017b937a 100644 --- a/hr_attendance_missing_days/tests/test_attendance.py +++ b/hr_attendance_missing_days/tests/test_attendance.py @@ -1,9 +1,22 @@ # © 2023 initOS GmbH # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import date, datetime, timedelta + +import pytz + from odoo.tests import TransactionCase +def convert_tz(dt, *, from_tz=None, to_tz=None): + return ( + pytz.timezone(from_tz or "UTC") + .localize(dt) + .astimezone(pytz.timezone(to_tz or "UTC")) + .replace(tzinfo=None) + ) + + class TestAttendance(TransactionCase): @classmethod def setUpClass(cls): @@ -41,3 +54,41 @@ def test_attendance_creation_with_reason(self): attendances_new = attendances_after - attendances_before self.assertTrue(attendances_new) self.assertFalse(any(attendances_new.mapped("worked_hours"))) + + def test_attendance_creation(self): + self.env.company.attendance_missing_days_reason = self.reason + + attended = {date(2023, 7, 3 + offset) for offset in range(4)} + for tz in ["Europe/Amsterdam", "Pacific/Auckland", "America/New_York"]: + employee = self.employee.copy({"tz": tz, "name": f"Employee {tz}"}) + for offset, times in enumerate(((0, 30), (23, 30), (11, 30), (12, 30))): + # Convert the times from the employee TZ zo UTC. 3rd is monday + start = convert_tz( + datetime(2023, 7, 3 + offset, *times), + from_tz=tz, + to_tz="UTC", + ) + + # Generate a 30min attendance blocking the date + self.env["hr.attendance"].create( + { + "employee_id": employee.id, + "check_in": start, + "check_out": start + timedelta(minutes=30), + } + ) + + # Cover a huge time span + employee._create_missing_attendances( + datetime(2023, 6, 1, 0, 0), datetime(2023, 8, 1, 0, 0) + ) + + domain = [ + ("employee_id", "=", employee.id), + ("attendance_reason_ids", "=", self.reason.id), + ] + attendances = self.env["hr.attendance"].search(domain) + self.assertTrue(attendances) + for attendance in attendances: + checkin = convert_tz(attendance.check_in, to_tz=tz) + self.assertNotIn(checkin.date(), attended) From 4e780600f0652d9055b823c124eaaa4dff44f509 Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Mon, 14 Aug 2023 13:29:32 +0200 Subject: [PATCH 3/7] [FIX] hr_attendance_missing_days: Prevent a generation of an empty attendance mid working day --- .../models/hr_employee.py | 9 ++++++++ .../tests/test_attendance.py | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/hr_attendance_missing_days/models/hr_employee.py b/hr_attendance_missing_days/models/hr_employee.py index 893ce2a7..658b6bad 100644 --- a/hr_attendance_missing_days/models/hr_employee.py +++ b/hr_attendance_missing_days/models/hr_employee.py @@ -45,6 +45,15 @@ def _create_missing_attendances(self, date_from=None, date_to=None): work_dates = {} for start, _stop, _attendance in sorted(intervals[False]): start_date = start.date() + + # Check that the end of the day for the employee is before date_to to + # avoid a run mid working day + tz = pytz.timezone(self.tz or "UTC") + end_of_day = datetime.combine(start_date, time.max) + end_of_day = tz.localize(end_of_day).astimezone(pytz.utc) + if end_of_day >= date_to: + continue + if start_date not in work_dates: work_dates[start_date] = ensure_tz(start, pytz.utc).replace(tzinfo=None) diff --git a/hr_attendance_missing_days/tests/test_attendance.py b/hr_attendance_missing_days/tests/test_attendance.py index 017b937a..169691c5 100644 --- a/hr_attendance_missing_days/tests/test_attendance.py +++ b/hr_attendance_missing_days/tests/test_attendance.py @@ -92,3 +92,25 @@ def test_attendance_creation(self): for attendance in attendances: checkin = convert_tz(attendance.check_in, to_tz=tz) self.assertNotIn(checkin.date(), attended) + + def test_attendance_creation_during_day(self): + self.env.company.attendance_missing_days_reason = self.reason + + start = datetime(2023, 7, 3, 12, 30) + self.env["hr.attendance"].create( + { + "employee_id": self.employee.id, + "check_in": start, + "check_out": start + timedelta(minutes=30), + } + ) + + attendances_before = self.employee.attendance_ids + self.employee._create_missing_attendances( + start - timedelta(hours=12), + start - timedelta(minutes=30), + ) + attendances_after = self.employee.attendance_ids + + attendances_new = attendances_after - attendances_before + self.assertFalse(attendances_new) From eefb2a8ff660c3caeed5cbd1f87cde2a44e23f9a Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Tue, 15 Aug 2023 08:40:11 +0200 Subject: [PATCH 4/7] [IMP] hr_attendance_missing_days: Rework to use dates instead of datetimes --- hr_attendance_missing_days/data/ir_cron.xml | 2 +- .../models/hr_employee.py | 55 ++++++++++--------- .../tests/test_attendance.py | 15 ++--- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/hr_attendance_missing_days/data/ir_cron.xml b/hr_attendance_missing_days/data/ir_cron.xml index ff304a4e..c616cf97 100644 --- a/hr_attendance_missing_days/data/ir_cron.xml +++ b/hr_attendance_missing_days/data/ir_cron.xml @@ -14,7 +14,7 @@ code - model.create_missing_attendances(datetime.datetime.now() - datetime.timedelta(days=31)) + model.create_missing_attendances(datetime.date.today() - datetime.timedelta(days=31)) diff --git a/hr_attendance_missing_days/models/hr_employee.py b/hr_attendance_missing_days/models/hr_employee.py index 658b6bad..4a91fad5 100644 --- a/hr_attendance_missing_days/models/hr_employee.py +++ b/hr_attendance_missing_days/models/hr_employee.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from datetime import datetime, time +from datetime import date, datetime, time, timedelta import pytz @@ -20,6 +20,15 @@ def ensure_tz(dt, tz=None): class Employee(models.Model): _inherit = "hr.employee" + def _prepare_missing_attendance_values(self, dt, reasons): + self.ensure_one() + return { + "employee_id": self.id, + "check_in": dt, + "check_out": dt, + "attendance_reason_ids": [(6, 0, reasons.ids)], + } + def create_missing_attendances(self, date_from=None, date_to=None): for emp in self.search([]): emp._create_missing_attendances(date_from, date_to) @@ -32,36 +41,37 @@ def _create_missing_attendances(self, date_from=None, date_to=None): return if not date_from: - date_from = datetime.combine( - self.env.company.sudo().overtime_start_date, time.min - ) + date_from = self.env.company.sudo().overtime_start_date if not date_to: - date_to = datetime.now() + date_to = date.today() + + # Determine the start and end of the day and convert to UTC + dt_from = datetime.combine(date_from, time.min) + dt_to = datetime.combine(date_to, time.max) - date_from, date_to = map(ensure_tz, (date_from, date_to)) + tz = pytz.timezone(self.tz or "UTC") + dt_from, dt_to = map(tz.localize, (dt_from, dt_to)) + dt_from, dt_to = ensure_tz(dt_from, pytz.utc), ensure_tz(dt_to, pytz.utc) - intervals = self.resource_calendar_id._work_intervals_batch(date_from, date_to) + # Skip the active day + if dt_to.replace(tzinfo=None) > datetime.now(): + dt_to -= timedelta(days=1) + + if dt_from > dt_to: + return + + intervals = self.resource_calendar_id._work_intervals_batch(dt_from, dt_to) work_dates = {} for start, _stop, _attendance in sorted(intervals[False]): start_date = start.date() - - # Check that the end of the day for the employee is before date_to to - # avoid a run mid working day - tz = pytz.timezone(self.tz or "UTC") - end_of_day = datetime.combine(start_date, time.max) - end_of_day = tz.localize(end_of_day).astimezone(pytz.utc) - if end_of_day >= date_to: - continue - if start_date not in work_dates: work_dates[start_date] = ensure_tz(start, pytz.utc).replace(tzinfo=None) domain = [ - ("check_in", ">=", date_from.replace(tzinfo=None)), - ("check_in", "<=", date_to.replace(tzinfo=None)), + ("check_in", ">=", dt_from.replace(tzinfo=None)), + ("check_in", "<=", dt_to.replace(tzinfo=None)), ] - tz = pytz.timezone(self.tz) attendances = { ensure_tz(attendance.check_in, tz).date() for attendance in self.attendance_ids.filtered_domain(domain) @@ -70,12 +80,7 @@ def _create_missing_attendances(self, date_from=None, date_to=None): vals = [] for missing in set(work_dates) - attendances: vals.append( - { - "employee_id": self.id, - "check_in": work_dates[missing], - "check_out": work_dates[missing], - "attendance_reason_ids": [(4, reason.id)], - } + self._prepare_missing_attendance_values(work_dates[missing], reason) ) self.env["hr.attendance"].create(vals) diff --git a/hr_attendance_missing_days/tests/test_attendance.py b/hr_attendance_missing_days/tests/test_attendance.py index 169691c5..2d1ae91d 100644 --- a/hr_attendance_missing_days/tests/test_attendance.py +++ b/hr_attendance_missing_days/tests/test_attendance.py @@ -79,9 +79,7 @@ def test_attendance_creation(self): ) # Cover a huge time span - employee._create_missing_attendances( - datetime(2023, 6, 1, 0, 0), datetime(2023, 8, 1, 0, 0) - ) + employee._create_missing_attendances(date(2023, 6, 1), date(2023, 8, 1)) domain = [ ("employee_id", "=", employee.id), @@ -96,20 +94,17 @@ def test_attendance_creation(self): def test_attendance_creation_during_day(self): self.env.company.attendance_missing_days_reason = self.reason - start = datetime(2023, 7, 3, 12, 30) + now = datetime.now() self.env["hr.attendance"].create( { "employee_id": self.employee.id, - "check_in": start, - "check_out": start + timedelta(minutes=30), + "check_in": now - timedelta(minutes=30), + "check_out": now + timedelta(minutes=30), } ) attendances_before = self.employee.attendance_ids - self.employee._create_missing_attendances( - start - timedelta(hours=12), - start - timedelta(minutes=30), - ) + self.employee._create_missing_attendances(now, now) attendances_after = self.employee.attendance_ids attendances_new = attendances_after - attendances_before From 6c3204b310d5298cf9885b657f236b23988311e7 Mon Sep 17 00:00:00 2001 From: fkantelberg Date: Thu, 31 Aug 2023 09:50:05 +0200 Subject: [PATCH 5/7] [IMP] hr_attendance_missing_days: Use hr.contract for the generation of the missing days --- .../README.rst | 74 +++ .../__init__.py | 4 + .../__manifest__.py | 19 + .../models/__init__.py | 4 + .../models/hr_employee.py | 32 ++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 2 + .../static/description/index.html | 420 ++++++++++++++++++ .../models/hr_employee.py | 8 +- .../views/res_config_settings_views.xml | 2 +- .../hr_attendance_contract_missing_days | 1 + .../setup.py | 6 + 12 files changed, 570 insertions(+), 3 deletions(-) create mode 100644 hr_attendance_contract_missing_days/README.rst create mode 100644 hr_attendance_contract_missing_days/__init__.py create mode 100644 hr_attendance_contract_missing_days/__manifest__.py create mode 100644 hr_attendance_contract_missing_days/models/__init__.py create mode 100644 hr_attendance_contract_missing_days/models/hr_employee.py create mode 100644 hr_attendance_contract_missing_days/readme/CONTRIBUTORS.rst create mode 100644 hr_attendance_contract_missing_days/readme/DESCRIPTION.rst create mode 100644 hr_attendance_contract_missing_days/static/description/index.html create mode 120000 setup/hr_attendance_contract_missing_days/odoo/addons/hr_attendance_contract_missing_days create mode 100644 setup/hr_attendance_contract_missing_days/setup.py diff --git a/hr_attendance_contract_missing_days/README.rst b/hr_attendance_contract_missing_days/README.rst new file mode 100644 index 00000000..33bd33c4 --- /dev/null +++ b/hr_attendance_contract_missing_days/README.rst @@ -0,0 +1,74 @@ +============================================================== +Attendance generation for missing days with installed contract +============================================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_attendance-lightgray.png?logo=github + :target: https://github.com/OCA/hr_attendance/tree/15.0/hr_attendance_contract_missing_days + :alt: OCA/hr_attendance +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr_attendance-15-0/hr_attendance-15-0-hr_attendance_contract_missing_days + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/hr_attendance&target_branch=15.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This modules changes the generation of the attendances for the missing days to use the +HR contracts accordingly. + +**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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* initOS GmbH + +Contributors +~~~~~~~~~~~~ + +* initOS GmbH (initOS.com) + +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. + +This module is part of the `OCA/hr_attendance `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_attendance_contract_missing_days/__init__.py b/hr_attendance_contract_missing_days/__init__.py new file mode 100644 index 00000000..0f55bfa9 --- /dev/null +++ b/hr_attendance_contract_missing_days/__init__.py @@ -0,0 +1,4 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/hr_attendance_contract_missing_days/__manifest__.py b/hr_attendance_contract_missing_days/__manifest__.py new file mode 100644 index 00000000..d0ce0e8e --- /dev/null +++ b/hr_attendance_contract_missing_days/__manifest__.py @@ -0,0 +1,19 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Attendance generation for missing days with installed contract", + "version": "15.0.1.0.0", + "category": "Hidden", + "author": "initOS GmbH, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/hr-attendance", + "license": "AGPL-3", + "summary": "This modules combines the generation of attendances for working " + "days without attendance with HR contracts", + "depends": [ + "hr_contract", + "hr_attendance_missing_days", + ], + "auto_install": True, + "installable": True, +} diff --git a/hr_attendance_contract_missing_days/models/__init__.py b/hr_attendance_contract_missing_days/models/__init__.py new file mode 100644 index 00000000..868f64ba --- /dev/null +++ b/hr_attendance_contract_missing_days/models/__init__.py @@ -0,0 +1,4 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import hr_employee diff --git a/hr_attendance_contract_missing_days/models/hr_employee.py b/hr_attendance_contract_missing_days/models/hr_employee.py new file mode 100644 index 00000000..3e9b4882 --- /dev/null +++ b/hr_attendance_contract_missing_days/models/hr_employee.py @@ -0,0 +1,32 @@ +# © 2023 initOS GmbH +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, time + +import pytz + +from odoo import models + + +class Employee(models.Model): + _inherit = "hr.employee" + + def _get_work_intervals_batch(self, dt_from, dt_to): + intervals = [] + + tz = pytz.timezone(self.tz or "UTC") + for contract in self._get_contracts(dt_from, dt_to, states=["open", "close"]): + start = datetime.combine(contract.date_start, time.min) + start = max(dt_from, tz.localize(start).astimezone(pytz.UTC)) + + if contract.date_end: + end = datetime.combine(contract.date_end, time.max) + end = min(dt_to, tz.localize(end).astimezone(pytz.UTC)) + else: + end = dt_to + + intervals.extend( + contract.resource_calendar_id._work_intervals_batch(start, end)[False] + ) + + return intervals diff --git a/hr_attendance_contract_missing_days/readme/CONTRIBUTORS.rst b/hr_attendance_contract_missing_days/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..db386db6 --- /dev/null +++ b/hr_attendance_contract_missing_days/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* initOS GmbH (initOS.com) diff --git a/hr_attendance_contract_missing_days/readme/DESCRIPTION.rst b/hr_attendance_contract_missing_days/readme/DESCRIPTION.rst new file mode 100644 index 00000000..cee63803 --- /dev/null +++ b/hr_attendance_contract_missing_days/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This modules changes the generation of the attendances for the missing days to use the +HR contracts accordingly. diff --git a/hr_attendance_contract_missing_days/static/description/index.html b/hr_attendance_contract_missing_days/static/description/index.html new file mode 100644 index 00000000..fe3c368b --- /dev/null +++ b/hr_attendance_contract_missing_days/static/description/index.html @@ -0,0 +1,420 @@ + + + + + + +Attendance generation for missing days with installed contract + + + +
+

Attendance generation for missing days with installed contract

+ + +

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

+

This modules changes the generation of the attendances for the missing days to use the +HR contracts accordingly.

+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • initOS GmbH
  • +
+
+
+

Contributors

+
    +
  • initOS GmbH (initOS.com)
  • +
+
+
+

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.

+

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

+

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

+
+
+
+ + diff --git a/hr_attendance_missing_days/models/hr_employee.py b/hr_attendance_missing_days/models/hr_employee.py index 4a91fad5..ee5f3db7 100644 --- a/hr_attendance_missing_days/models/hr_employee.py +++ b/hr_attendance_missing_days/models/hr_employee.py @@ -29,6 +29,10 @@ def _prepare_missing_attendance_values(self, dt, reasons): "attendance_reason_ids": [(6, 0, reasons.ids)], } + def _get_work_intervals_batch(self, dt_from, dt_to): + self.ensure_one() + return self.resource_calendar_id._work_intervals_batch(dt_from, dt_to)[False] + def create_missing_attendances(self, date_from=None, date_to=None): for emp in self.search([]): emp._create_missing_attendances(date_from, date_to) @@ -61,9 +65,9 @@ def _create_missing_attendances(self, date_from=None, date_to=None): if dt_from > dt_to: return - intervals = self.resource_calendar_id._work_intervals_batch(dt_from, dt_to) + intervals = self._get_work_intervals_batch(dt_from, dt_to) work_dates = {} - for start, _stop, _attendance in sorted(intervals[False]): + for start, _stop, _attendance in sorted(intervals): start_date = start.date() if start_date not in work_dates: work_dates[start_date] = ensure_tz(start, pytz.utc).replace(tzinfo=None) diff --git a/hr_attendance_missing_days/views/res_config_settings_views.xml b/hr_attendance_missing_days/views/res_config_settings_views.xml index dc7253c7..bd5d28a7 100644 --- a/hr_attendance_missing_days/views/res_config_settings_views.xml +++ b/hr_attendance_missing_days/views/res_config_settings_views.xml @@ -14,7 +14,7 @@