From 69bc951b3e66a30b38b11da1dca4852d6c3b43c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phan=20Sainl=C3=A9ger?= Date: Tue, 5 May 2026 17:24:48 +0200 Subject: [PATCH] [MIG] hr_effective_attendance_period: migrate to 18.0 --- hr_effective_attendance_period/README.rst | 67 +++++++++++++ hr_effective_attendance_period/__init__.py | 1 + .../__manifest__.py | 24 +++++ hr_effective_attendance_period/i18n/README | 1 + hr_effective_attendance_period/i18n/fr.po | 49 ++++++++++ .../i18n/hr_effective_attendance_period.pot | 49 ++++++++++ .../models/__init__.py | 1 + .../models/resource.py | 97 +++++++++++++++++++ .../views/resource_views.xml | 36 +++++++ 9 files changed, 325 insertions(+) create mode 100644 hr_effective_attendance_period/README.rst create mode 100644 hr_effective_attendance_period/__init__.py create mode 100644 hr_effective_attendance_period/__manifest__.py create mode 100644 hr_effective_attendance_period/i18n/README create mode 100644 hr_effective_attendance_period/i18n/fr.po create mode 100644 hr_effective_attendance_period/i18n/hr_effective_attendance_period.pot create mode 100644 hr_effective_attendance_period/models/__init__.py create mode 100644 hr_effective_attendance_period/models/resource.py create mode 100644 hr_effective_attendance_period/views/resource_views.xml diff --git a/hr_effective_attendance_period/README.rst b/hr_effective_attendance_period/README.rst new file mode 100644 index 0000000..dcec42f --- /dev/null +++ b/hr_effective_attendance_period/README.rst @@ -0,0 +1,67 @@ +============================== +hr_effective_attendance_period +============================== + +Provide indication that attendance periods are effectively worked or not. + +Description +=========== + +This module adds a boolean field on working time attendance lines to mark +which periods are effectively worked by employees. It provides utility methods +on the resource calendar to determine: + +- Whether a day is a working day +- Whether a day is a full working day (both morning and afternoon) +- Whether all attendances of a day are effectively worked (no leaves) + +Installation +============ + +Use Odoo normal module installation procedure to install +``hr_effective_attendance_period``. + +This module depends on: + +* ``base`` (Odoo core) +* ``resource`` (Odoo core) + +Configuration +============= + +Go to ``Configuration > Technical > Resource > Working Times`` and create +Working Times for your company. For each attendance line, check +``Effective Attendance Period`` to indicate which periods are effectively +attended by employees. + +Known issues / Roadmap +====================== + +None yet. + +Bug Tracker +=========== + +Bugs are tracked on `our issues website `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Stéphan Sainléger + +Funders +------- + +The development of this module has been financially supported by: + +* Elabore (https://elabore.coop) + +Maintainer +---------- + +This module is maintained by Elabore. diff --git a/hr_effective_attendance_period/__init__.py b/hr_effective_attendance_period/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/hr_effective_attendance_period/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_effective_attendance_period/__manifest__.py b/hr_effective_attendance_period/__manifest__.py new file mode 100644 index 0000000..c20c36a --- /dev/null +++ b/hr_effective_attendance_period/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2023 Stéphan Sainléger (Elabore) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "hr_effective_attendance_period", + "version": "18.0.1.0.0", + "author": "Elabore", + "website": "https://git.elabore.coop/elabore/hr-tools", + "maintainer": "Stéphan Sainléger", + "license": "AGPL-3", + "category": "Tools", + "summary": "Indicate whether attendance periods are effectively worked", + # any module necessary for this one to work correctly + "depends": [ + "base", + "resource", + ], + "data": [ + "views/resource_views.xml", + ], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/hr_effective_attendance_period/i18n/README b/hr_effective_attendance_period/i18n/README new file mode 100644 index 0000000..62197a1 --- /dev/null +++ b/hr_effective_attendance_period/i18n/README @@ -0,0 +1 @@ +This directory should contain the *.po for Odoo translation. diff --git a/hr_effective_attendance_period/i18n/fr.po b/hr_effective_attendance_period/i18n/fr.po new file mode 100644 index 0000000..099512e --- /dev/null +++ b/hr_effective_attendance_period/i18n/fr.po @@ -0,0 +1,49 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_effective_attendance_period +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-24 21:50+0000\n" +"PO-Revision-Date: 2023-01-24 21:50+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_effective_attendance_period +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar__display_name +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar_attendance__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: hr_effective_attendance_period +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar_attendance__effective_attendance_period +msgid "Effective Attendance Period" +msgstr "Présence effective" + +#. module: hr_effective_attendance_period +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar__id +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar_attendance__id +msgid "ID" +msgstr "" + +#. module: hr_effective_attendance_period +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar____last_update +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar_attendance____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: hr_effective_attendance_period +#: model:ir.model,name:hr_effective_attendance_period.model_resource_calendar +msgid "Resource Working Time" +msgstr "Temps de travail de la ressource" + +#. module: hr_effective_attendance_period +#: model:ir.model,name:hr_effective_attendance_period.model_resource_calendar_attendance +msgid "Work Detail" +msgstr "Détail du travail" diff --git a/hr_effective_attendance_period/i18n/hr_effective_attendance_period.pot b/hr_effective_attendance_period/i18n/hr_effective_attendance_period.pot new file mode 100644 index 0000000..dd84576 --- /dev/null +++ b/hr_effective_attendance_period/i18n/hr_effective_attendance_period.pot @@ -0,0 +1,49 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_effective_attendance_period +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-24 21:49+0000\n" +"PO-Revision-Date: 2023-01-24 21:49+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_effective_attendance_period +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar__display_name +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar_attendance__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_effective_attendance_period +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar_attendance__effective_attendance_period +msgid "Effective Attendance Period" +msgstr "" + +#. module: hr_effective_attendance_period +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar__id +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar_attendance__id +msgid "ID" +msgstr "" + +#. module: hr_effective_attendance_period +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar____last_update +#: model:ir.model.fields,field_description:hr_effective_attendance_period.field_resource_calendar_attendance____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_effective_attendance_period +#: model:ir.model,name:hr_effective_attendance_period.model_resource_calendar +msgid "Resource Working Time" +msgstr "" + +#. module: hr_effective_attendance_period +#: model:ir.model,name:hr_effective_attendance_period.model_resource_calendar_attendance +msgid "Work Detail" +msgstr "" diff --git a/hr_effective_attendance_period/models/__init__.py b/hr_effective_attendance_period/models/__init__.py new file mode 100644 index 0000000..364a06e --- /dev/null +++ b/hr_effective_attendance_period/models/__init__.py @@ -0,0 +1 @@ +from . import resource diff --git a/hr_effective_attendance_period/models/resource.py b/hr_effective_attendance_period/models/resource.py new file mode 100644 index 0000000..e845613 --- /dev/null +++ b/hr_effective_attendance_period/models/resource.py @@ -0,0 +1,97 @@ +import math +from datetime import timedelta + +import pytz + +from odoo import fields, models + + +class ResourceCalendarAttendance(models.Model): + _inherit = "resource.calendar.attendance" + + effective_attendance_period = fields.Boolean(store=True) + + +class ResourceCalendar(models.Model): + _inherit = "resource.calendar" + + def _retrieve_day_matching_attendances(self, day): + domain = [ + ("calendar_id", "=", self.id), + ("dayofweek", "=", day.weekday()), + ("effective_attendance_period", "=", True), + ] + if self.two_weeks_calendar: + # Employee has Even/Odd weekly calendar + week_type = 1 if int(math.floor((day.toordinal() - 1) / 7) % 2) else 0 + domain.append(("week_type", "=", week_type)) + result = self.env["resource.calendar.attendance"].search(domain) + return result + + def is_working_day(self, day): + day_attendances = self._retrieve_day_matching_attendances(day) + if len(day_attendances) == 0: + # This day of the week is not supposed to be a working day + return False + else: + # This day of the week is supposed to be a working day + return True + + def is_full_working_day(self, day): + day_attendances = self._retrieve_day_matching_attendances(day) + morning_worked = ( + len(day_attendances.filtered(lambda x: x.day_period == "morning")) > 0 + ) + afternoon_worked = ( + len(day_attendances.filtered(lambda x: x.day_period == "afternoon")) > 0 + ) + return morning_worked and afternoon_worked + + def _compute_datetime_in_utc_tz(self, date): + user_tz = pytz.timezone(self.env.context.get("tz", "utc") or "utc") + return user_tz.localize(date).astimezone(pytz.utc) + + def _is_worked_attendance(self, resource, day, attendance): + attendance_start = fields.Datetime.to_datetime(day.date()) + timedelta( + hours=attendance.hour_from + ) + attendance_end = fields.Datetime.to_datetime(day.date()) + timedelta( + hours=attendance.hour_to + ) + utc_start = self._compute_datetime_in_utc_tz(attendance_start) + utc_end = self._compute_datetime_in_utc_tz(attendance_end) + resource_leaves = ( + self.env["resource.calendar.leaves"] + .sudo() + .search( + [ + ("company_id", "=", resource.company_id.id), + ("date_from", "<=", utc_start), + ("date_to", ">=", utc_end), + "|", + ("resource_id", "=", resource.id), + ("resource_id", "=", None), + ] + ) + ) + if resource_leaves: + return False + else: + # a part or the whole attendance is worked + return True + + def is_worked_day(self, resource, day): + day_attendances = self._retrieve_day_matching_attendances(day) + # If at least one attendance is worked, return True + for attendance in day_attendances: + if self._is_worked_attendance(resource, day, attendance): + return True + return False + + def all_attendances_worked(self, resource, day): + day_attendances = self._retrieve_day_matching_attendances(day) + # If at least one attendance is not worked, return False + for attendance in day_attendances: + if not self._is_worked_attendance(resource, day, attendance): + return False + return True diff --git a/hr_effective_attendance_period/views/resource_views.xml b/hr_effective_attendance_period/views/resource_views.xml new file mode 100644 index 0000000..f274709 --- /dev/null +++ b/hr_effective_attendance_period/views/resource_views.xml @@ -0,0 +1,36 @@ + + + + resource.calendar.attendance.view.list.inherit + resource.calendar.attendance + + + + + + + + + + resource.calendar.attendance.view.list.inherit + resource.calendar.attendance + + + + + + + +