- Choose which attendance sheet ranges should be used by default
-
-
-
-
-
-
-
-
-
-
-
-
-
- Choose the week start day.
-
-
-
-
-
-
-
-
@@ -94,61 +41,6 @@
-
Attendance Rules
-
-
-
-
-
-
-
-
-
- Choose if lunches are auto calculated after a certain duration. If a single attendance surpasses the duration, an auto lunch will be applied. If there are multiple attendances, but the time between them is shorter than the lunch time, then an auto lunch will be applied.
-
-
-
-
- Hours
-
-
-
-
-
- Hours
-
-
-
-
-
diff --git a/hr_attendance_sheet_compensatory/README.rst b/hr_attendance_sheet_compensatory/README.rst
new file mode 100644
index 00000000..72dc61d0
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/README.rst
@@ -0,0 +1,125 @@
+================================
+HR Attendance Sheet Compensatory
+================================
+
+.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! 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/14.0/hr_attendance_sheet_compensatory
+ :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-14-0/hr-attendance-14-0-hr_attendance_sheet_compensatory
+ :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=14.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This is based on `hr_attendance_sheet` which add a validation mechanism
+to review employee attendance with an auto-lunch calculation.
+This allows to generate compensatory hours (allocated leaves) that can
+be used later as day off or to regulate credits leaves as
+this module is compatible with `hr_holidays_credit` module.
+
+This is based on the `hr_attendance_overtime` module which
+mark rows as "overtime" those rows are not due by default
+as it could came from possible mist check-out. So manager can
+decide to add or not those overtime attendance lines or not and
+compute or adjust compensatory/leaves hours to generate.
+
+This module is **incompatible** with hr_attendance_validation
+because it implements the same feature and especially generates
+errors during hr.attendance validation
+
+..note::
+
+ If you are allowing flexible hours - check-in/check-out range
+ are bigger than average hours per day - So you can generate
+ compensatory hours from lines that are not marked as overtime.
+
+Once review is validated attendance lines are locked on that period.
+
+At the end managers can check holidays allocation per year and
+by employee to make sure allowed employee compensatory hours are
+not over.
+
+Employees can:
+- access to validated sheets to review hours taken account
+- see current week hours on check-in view
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Configuration
+=============
+
+* Ensure employee weeks are properly set
+* Set the leave type to use by generating compensatory
+ hours from attendance review (to be done in hr attendance configuration)
+* once all leaves and attendances has been recorded you can generate leave reviews
+ by setting up a cron job running every monday morning to generate the previous week
+ with the following code on `hr.attendance.sheet` model::
+
+ model.generate_reviews()
+
+Usage
+=====
+
+* Once review has been generate by ir cron manager are able to
+ open each one on Attenances > Manager > Attendance validation
+* On each form decide if recorded overtime are due or not
+* change the amount of allocated compensatory hours to generate
+* validate the review to generate the allocation
+
+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
+~~~~~~~
+
+* Odoo S.A.
+
+Contributors
+~~~~~~~~~~~~
+
+* Pierre Verkest
+* Maxime Franco
+
+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_sheet_compensatory/__init__.py b/hr_attendance_sheet_compensatory/__init__.py
new file mode 100644
index 00000000..0650744f
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/hr_attendance_sheet_compensatory/__manifest__.py b/hr_attendance_sheet_compensatory/__manifest__.py
new file mode 100644
index 00000000..737eb974
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/__manifest__.py
@@ -0,0 +1,29 @@
+# Copyright 2021 Pierre Verkest
+# Copyright 2023 ACSONE SA/NV
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+{
+ "name": "HR Attendance Sheet Compensatory",
+ "version": "14.0.1.0.3",
+ "category": "Human Resources",
+ "summary": "Group attendances into attendance sheets.",
+ "website": "https://github.com/OCA/hr-attendance",
+ "author": "Odoo S.A., Odoo Community Association (OCA)",
+ "license": "AGPL-3",
+ "installable": True,
+ "depends": [
+ "hr_attendance",
+ "hr_attendance_sheet",
+ "hr_attendance_overtime",
+ "hr_attendance_reason",
+ "hr_attendance_modification_tracking",
+ "hr_holidays",
+ ],
+ "data": [
+ "security/hr_attendance_sheet_rule.xml",
+ "views/assets.xml",
+ "views/hr_attendance.xml",
+ "views/hr_attendance_sheet.xml",
+ "views/res_config_settings_views.xml",
+ ],
+}
diff --git a/hr_attendance_sheet_compensatory/models/__init__.py b/hr_attendance_sheet_compensatory/models/__init__.py
new file mode 100644
index 00000000..48c62588
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/models/__init__.py
@@ -0,0 +1,7 @@
+from . import hr_attendance
+from . import hr_attendance_sheet
+from . import hr_employee
+from . import hr_leave
+from . import res_company
+from . import res_config
+from . import resource_calendar
diff --git a/hr_attendance_sheet_compensatory/models/hr_attendance.py b/hr_attendance_sheet_compensatory/models/hr_attendance.py
new file mode 100644
index 00000000..7eb58d58
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/models/hr_attendance.py
@@ -0,0 +1,98 @@
+# Copyright 2021 Pierre Verkest
+# Copyright 2023 ACSONE SA/NV
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+from odoo import SUPERUSER_ID, _, api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class HrAttendance(models.Model):
+ _inherit = "hr.attendance"
+
+ is_overtime_due = fields.Boolean(
+ string="Is overtime due",
+ default=False,
+ help="Whether the overtime is due or not. "
+ "By default overtime is not due until a manager validates it.",
+ )
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ attendances = super().create(vals_list)
+ for attendance in attendances:
+ if attendance._is_validated_employee_week():
+ raise ValidationError(
+ _(
+ "Cannot create new attendance for employee %s. "
+ "Attendance for the day of the check in %s "
+ "has already been reviewed and validated."
+ )
+ % (
+ attendance.employee_id.name,
+ attendance.check_in.date(),
+ )
+ )
+ return attendances
+
+ def unlink(self):
+ for record in self:
+ if record.attendance_sheet_id.state == "done":
+ raise ValidationError(
+ _(
+ "Can not remove this attendance (%s, %s) "
+ "which has been already reviewed and validated."
+ )
+ % (
+ record.employee_id.name,
+ record.check_in.date(),
+ )
+ )
+ return super().unlink()
+
+ def write(self, vals):
+ allowed_fields = self.get_allowed_fields()
+ is_allowed_fields = allowed_fields.issuperset(vals.keys())
+ for record in self:
+ if record.attendance_sheet_id.state == "done" and not is_allowed_fields:
+ raise ValidationError(
+ _(
+ "Can not change this attendance (%s, %s) "
+ "which has been already reviewed and validated."
+ )
+ % (
+ record.employee_id.name,
+ record.check_in.date(),
+ )
+ )
+ res = super().write(vals)
+ for record in self:
+ if record._is_validated_employee_week() and not is_allowed_fields:
+ raise ValidationError(
+ _(
+ "Can not change this attendance (%s, %s) "
+ "which would be moved to a validated day."
+ )
+ % (
+ record.employee_id.name,
+ record.check_in.date(),
+ )
+ )
+ return res
+
+ def _is_validated_employee_week(self):
+ validated_week = (
+ self.env["hr.attendance.sheet"]
+ .with_user(SUPERUSER_ID)
+ .search_count(
+ [
+ ("employee_id", "=", self.employee_id.id),
+ ("state", "=", "done"),
+ ("start_date", "<=", self.check_in.date()),
+ ("end_date", ">=", self.check_in.date()),
+ ]
+ )
+ )
+ return validated_week > 0
+
+ def get_allowed_fields(self):
+ return {"auto_lunch"}
diff --git a/hr_attendance_sheet_compensatory/models/hr_attendance_sheet.py b/hr_attendance_sheet_compensatory/models/hr_attendance_sheet.py
new file mode 100644
index 00000000..2ac234b4
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/models/hr_attendance_sheet.py
@@ -0,0 +1,333 @@
+# Copyright 2021 Pierre Verkest
+# Copyright 2023 ACSONE SA/NV
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+from datetime import timedelta
+
+from odoo import _, api, fields, models
+from odoo.osv import expression
+
+
+class HrAttendanceSheet(models.Model):
+ _inherit = "hr.attendance.sheet"
+
+ theoretical_hours = fields.Float(
+ string="Theoretical (hours)",
+ related="working_hours.total_hours",
+ help="Theoretical calendar hours to spend by week.",
+ )
+ attendance_due_ids = fields.One2many(
+ "hr.attendance",
+ compute="_compute_attendance_due_ids",
+ string="Valid attendances",
+ )
+ leave_ids = fields.Many2many("hr.leave", string="Leaves")
+ leave_allocation_id = fields.Many2one(
+ "hr.leave.allocation",
+ string="Leave allocation",
+ help="Automatically generated on validation if compensatory_hour > 0",
+ )
+ leave_id = fields.Many2one(
+ "hr.leave",
+ string="Leave",
+ help="Automatically generated on validation if "
+ "regularization_compensatory_hour_taken > 0",
+ )
+ leave_hours = fields.Float(
+ "Leaves (hours)",
+ compute="_compute_leaves",
+ help="Compute number of leaves in hours",
+ )
+ attendance_full_hours = fields.Float(
+ "Attendance (Full hours)",
+ compute="_compute_attendances_hours",
+ help="Compute number of attendance lines",
+ )
+ attendance_hours = fields.Float(
+ "Attendance without overtime",
+ compute="_compute_attendances_hours",
+ help="Compute number of attendance lines not marked as overtime",
+ )
+ attendance_total_hours = fields.Float(
+ "Total Attendance (without lunch - Not Due)",
+ compute="_compute_attendances_hours",
+ help="Validated attendances. Sum attendance and due overtime lines.",
+ )
+ overtime_due_hours = fields.Float(
+ "Overtime due (hours)",
+ compute="_compute_attendances_hours",
+ help="Compute number of attendance lines marked as overtime which are marked as due",
+ )
+ overtime_not_due_hours = fields.Float(
+ "Overtime not due (hours)",
+ compute="_compute_attendances_hours",
+ help="Compute number of attendance lines marked as overtime which are not due",
+ )
+ compensatory_hour = fields.Float(
+ "Compensatory hour",
+ help="Compensatory hours that will be allocated to the employee.",
+ )
+ regularization_compensatory_hour_taken = fields.Float(
+ "Regularization compensatory hours' taken",
+ help="Compensatory hours that will be counted as leaves for the employee.",
+ )
+ require_regeneration = fields.Boolean(
+ "Require regeneration",
+ default=False,
+ help="Couldn't properly call action retrieve lines in onchange "
+ "instead alert user to click on it when needs.",
+ )
+ start_date = fields.Date(
+ default=lambda self: self._default_start_date(),
+ )
+ end_date = fields.Date(
+ default=lambda self: self._default_end_date(),
+ )
+
+ def _default_start_date(self):
+ """returns the monday before last sunday"""
+ today = fields.Date.today()
+ return today - timedelta(days=today.weekday() + 7)
+
+ def _default_end_date(self):
+ """returns last sunday"""
+ today = fields.Date.today()
+ return today - timedelta(days=today.weekday() + 1)
+
+ @api.depends("leave_ids")
+ def _compute_leaves(self):
+ for record in self:
+ leave_hours = 0
+ for leave in record.leave_ids:
+ if leave.request_unit_half or leave.request_unit_hours:
+ # we assume time off is recorded by hours
+ leave_hours += leave.sudo().number_of_hours_display
+ else:
+ # As far leaves can be record on multiple weeks
+ # intersect calendar attendance and leaves date
+ # to compute theoretical leave time
+ current_date = max(leave.request_date_from, record.start_date)
+ date_to = min(
+ leave.request_date_to or leave.request_date_from,
+ record.end_date,
+ )
+ while current_date <= date_to:
+ leave_hours += sum(
+ record.working_hours.attendance_ids.filtered(
+ lambda att: int(att.dayofweek) == current_date.weekday()
+ ).mapped(lambda att: att.hour_to - att.hour_from)
+ )
+ current_date += timedelta(days=1)
+
+ record.leave_hours = leave_hours
+
+ @api.depends("attendance_ids", "attendance_ids.is_overtime")
+ def _compute_attendances_hours(self):
+ for record in self:
+ record.attendance_full_hours = sum(
+ record.attendance_ids.mapped("worked_hours")
+ )
+ record.attendance_hours = sum(
+ record.attendance_ids.filtered(lambda att: not att.is_overtime).mapped(
+ "duration"
+ )
+ )
+ record.overtime_due_hours = sum(
+ record.attendance_ids.filtered(
+ lambda att: att.is_overtime and att.is_overtime_due
+ ).mapped("duration")
+ )
+ record.overtime_not_due_hours = sum(
+ record.attendance_ids.filtered(
+ lambda att: att.is_overtime and not att.is_overtime_due
+ ).mapped("duration")
+ )
+ record.attendance_total_hours = sum(
+ record.attendance_due_ids.mapped("duration")
+ )
+
+ def _compute_attendance_due_ids(self):
+ for record in self:
+ record.attendance_due_ids = record.attendance_ids.filtered(
+ lambda att: not att.is_overtime or att.is_overtime_due
+ )
+
+ @api.onchange("employee_id", "start_date", "end_date")
+ def _onchange_recompute_lines(self):
+ self.ensure_one()
+ self.require_regeneration = True
+
+ def _retrieve_attendance(self):
+ """Method that link to hr.attendance between date from and date to"""
+ HrAttendance = self.env["hr.attendance"]
+ for record in self:
+ record.attendance_ids = HrAttendance.search(
+ [
+ ("employee_id", "=", record.employee_id.id),
+ ("check_in", ">=", record.start_date),
+ ("check_in", "<=", record.end_date),
+ ],
+ )
+
+ def _retrieve_leave(self):
+ """Method that link to hr.leave between date from and date to"""
+ HrLeave = self.env["hr.leave"]
+ for record in self:
+ domain = expression.AND(
+ [
+ [
+ ("state", "in", ["validate", "validate1"]),
+ ("employee_id", "=", record.employee_id.id),
+ ],
+ expression.OR(
+ [
+ # leaves thats starts in the validation sheet interval
+ expression.AND(
+ [
+ [("request_date_from", ">=", record.start_date)],
+ [("request_date_from", "<=", record.end_date)],
+ ]
+ ),
+ # leaves thats ends in the validation sheet interval
+ expression.AND(
+ [
+ [("request_date_to", ">=", record.start_date)],
+ [("request_date_to", "<=", record.end_date)],
+ ]
+ ),
+ # leaves thats start before and ends after the validation sheet
+ expression.AND(
+ [
+ [("request_date_from", "<", record.start_date)],
+ [("request_date_to", ">", record.end_date)],
+ ]
+ ),
+ ]
+ ),
+ ]
+ )
+ record.leave_ids = HrLeave.search(domain)
+
+ def action_retrieve_attendance_and_leaves(self):
+ """Action to retrieve both attendance and leave lines"""
+ self._retrieve_attendance()
+ self._retrieve_leave()
+ # this method can be called by cron, ensure that properly recompute
+ # default comp hours
+ self._compute_default_compensatory_hour()
+ self.require_regeneration = False
+
+ def action_attendance_sheet_done(self):
+ """Method to validate this sheet and generate leave allocation
+ if necessary
+ """
+ self.generate_leave_allocation()
+ self.generate_leave()
+ res = super().action_attendance_sheet_done()
+ return res
+
+ def action_attendance_sheet_draft(self):
+ self.clear_leaves_and_allocation()
+ return super().action_attendance_sheet_draft()
+
+ def clear_leaves_and_allocation(self):
+ for record in self:
+ if record.leave_allocation_id:
+ record.leave_allocation_id.action_refuse()
+ record.leave_allocation_id.action_draft()
+ if record.leave_id:
+ record.leave_id.action_refuse()
+ record.leave_id.action_draft()
+ self.remove_leaves()
+
+ def remove_leaves(self):
+ for record in self:
+ record.leave_allocation_id = False
+ record.leave_id = False
+
+ def unlink(self):
+ self.clear_leaves_and_allocation()
+ return super().unlink()
+
+ def generate_leave_allocation(self):
+ HrAllocation = self.env["hr.leave.allocation"]
+ for sheet in self:
+ holiday_status_id = (
+ sheet.employee_id.company_id.hr_attendance_compensatory_leave_type_id.id
+ )
+ if sheet.compensatory_hour > 0 and not sheet.leave_allocation_id:
+ sheet.leave_allocation_id = HrAllocation.create(
+ {
+ "employee_id": sheet.employee_id.id,
+ "holiday_status_id": holiday_status_id,
+ "number_of_days": sheet.compensatory_hour
+ / sheet.working_hours.hours_per_day,
+ "holiday_type": "employee",
+ "state": "validate",
+ "private_name": _("Compensatory hours: %s")
+ % sheet.display_name,
+ "notes": _(
+ "Allocation created and validated from attendance "
+ "validation reviews: %s"
+ )
+ % sheet.display_name,
+ }
+ )
+ return HrAllocation
+
+ def generate_leave(self):
+ HrLeave = self.env["hr.leave"]
+ for sheet in self:
+ holiday_status_id = (
+ sheet.employee_id.company_id.hr_attendance_compensatory_leave_type_id.id
+ )
+ if sheet.regularization_compensatory_hour_taken > 0 and not sheet.leave_id:
+ leave = HrLeave.create(
+ {
+ "employee_id": sheet.employee_id.id,
+ "holiday_status_id": holiday_status_id,
+ "number_of_days": sheet.regularization_compensatory_hour_taken
+ / sheet.working_hours.hours_per_day,
+ "name": _("Compensatory hours regularization generated from %s")
+ % sheet.display_name,
+ "request_date_from": sheet.end_date,
+ "request_date_to": sheet.end_date,
+ "date_from": sheet.end_date,
+ "date_to": sheet.end_date,
+ "request_unit_hours": False,
+ }
+ )
+ leave.action_validate()
+ sheet.leave_id = leave
+ return HrLeave
+
+ @api.onchange(
+ "leave_hours",
+ "attendance_hours",
+ "overtime_due_hours",
+ "overtime_not_due_hours",
+ )
+ def _compute_default_compensatory_hour(self):
+ """Re-compute default compensatory hour based on
+ accepted overtime
+ """
+ for record in self:
+ diff = (
+ record.attendance_hours
+ + record.leave_hours
+ + record.overtime_due_hours
+ - record.hours_to_work
+ )
+ record.compensatory_hour = max(0, diff)
+ record.regularization_compensatory_hour_taken = abs(min(0, diff))
+
+ @api.model
+ def _create_sheet_id(self):
+ sheets = super()._create_sheet_id()
+ sheets.action_retrieve_attendance_and_leaves()
+ return sheets
+
+ def action_to_review(self):
+ self.write({"state": "draft"})
+
+ def action_compute_compensatory_hour(self):
+ self._compute_default_compensatory_hour()
diff --git a/hr_attendance_sheet_compensatory/models/hr_employee.py b/hr_attendance_sheet_compensatory/models/hr_employee.py
new file mode 100644
index 00000000..d0952d5c
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/models/hr_employee.py
@@ -0,0 +1,111 @@
+# Copyright 2021 Pierre Verkest
+# Copyright 2023 ACSONE SA/NV
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+from datetime import timedelta
+
+import pytz
+from dateutil.relativedelta import relativedelta
+
+from odoo import fields, models
+
+
+class HrEmployeeBase(models.AbstractModel):
+ _inherit = "hr.employee.base"
+
+ hours_current_week = fields.Float(compute="_compute_hours_current_week")
+
+ def _compute_hours(self, start_naive, end_naive):
+ self.ensure_one()
+ attendances = self.env["hr.attendance"].search(
+ [
+ ("employee_id", "=", self.id),
+ "&",
+ ("check_in", "<=", end_naive),
+ ("check_out", ">=", start_naive),
+ "|",
+ ("is_overtime", "=", False),
+ "&",
+ ("is_overtime", "=", True),
+ ("is_overtime_due", "=", True),
+ ]
+ )
+ hours = 0
+ for attendance in attendances:
+ check_in = max(attendance.check_in, start_naive)
+ check_out = min(attendance.check_out, end_naive)
+ hours += (check_out - check_in).total_seconds() / 3600.0
+ return hours
+
+ def _compute_hours_current_week(self):
+ now = fields.Datetime.now()
+ now_utc = pytz.utc.localize(now)
+ for employee in self:
+ tz = pytz.timezone(employee.tz or "UTC")
+ now_tz = now_utc.astimezone(tz)
+ # return last monday
+ start_tz = (
+ now_tz
+ + timedelta(days=-now_tz.weekday())
+ + relativedelta(hour=0, minute=0, second=0, microsecond=0)
+ )
+ start_naive = start_tz.astimezone(pytz.utc).replace(tzinfo=None)
+ end_naive = now_tz.astimezone(pytz.utc).replace(tzinfo=None)
+ hours = self._compute_hours(start_naive, end_naive)
+ employee.hours_current_week = round(hours, 2)
+
+ def _compute_hours_last_month(self):
+ # Copied from hr_attendance module to overwrite hr.attendance search
+ # and refactor with _compute_hours_current_week
+ now = fields.Datetime.now()
+ now_utc = pytz.utc.localize(now)
+ for employee in self:
+ tz = pytz.timezone(employee.tz or "UTC")
+ now_tz = now_utc.astimezone(tz)
+ start_tz = now_tz + relativedelta(
+ months=-1, day=1, hour=0, minute=0, second=0, microsecond=0
+ )
+ start_naive = start_tz.astimezone(pytz.utc).replace(tzinfo=None)
+ end_tz = now_tz + relativedelta(
+ day=1, hour=0, minute=0, second=0, microsecond=0
+ )
+ end_naive = end_tz.astimezone(pytz.utc).replace(tzinfo=None)
+ hours = self._compute_hours(start_naive, end_naive)
+ employee.hours_last_month = round(hours, 2)
+ employee.hours_last_month_display = "%g" % employee.hours_last_month
+
+ def _compute_hours_today(self):
+ # Copied from hr_attendance module to overwrite hr.attendance search
+ now = fields.Datetime.now()
+ now_utc = pytz.utc.localize(now)
+ for employee in self:
+ # start of day in the employee's timezone might be the previous day in utc
+ tz = pytz.timezone(employee.tz)
+ now_tz = now_utc.astimezone(tz)
+ start_tz = now_tz + relativedelta(
+ hour=0, minute=0
+ ) # day start in the employee's timezone
+ start_naive = start_tz.astimezone(pytz.utc).replace(tzinfo=None)
+
+ attendances = self.env["hr.attendance"].search(
+ [
+ ("employee_id", "=", employee.id),
+ ("check_in", "<=", now),
+ "|",
+ ("check_out", ">=", start_naive),
+ ("check_out", "=", False),
+ "|",
+ ("is_overtime", "=", False),
+ "&",
+ ("is_overtime", "=", True),
+ ("is_overtime_due", "=", True),
+ ]
+ )
+
+ worked_hours = 0
+ for attendance in attendances:
+ delta = (attendance.check_out or now) - max(
+ attendance.check_in, start_naive
+ )
+ worked_hours += delta.total_seconds() / 3600.0
+ employee.hours_today = worked_hours
diff --git a/hr_attendance_sheet_compensatory/models/hr_leave.py b/hr_attendance_sheet_compensatory/models/hr_leave.py
new file mode 100644
index 00000000..30841c5a
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/models/hr_leave.py
@@ -0,0 +1,13 @@
+# Copyright 2021 Pierre Verkest
+# Copyright 2023 ACSONE SA/NV
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+from odoo import fields, models
+
+
+class HrLeave(models.Model):
+ _inherit = "hr.leave"
+
+ validation_sheet_id = fields.Many2one(
+ "hr.attendance.sheet",
+ string="Validation sheet",
+ )
diff --git a/hr_attendance_sheet_compensatory/models/res_company.py b/hr_attendance_sheet_compensatory/models/res_company.py
new file mode 100644
index 00000000..77a5b5da
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/models/res_company.py
@@ -0,0 +1,17 @@
+# Copyright 2021 Pierre Verkest
+# Copyright 2023 ACSONE SA/NV
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+from odoo import fields, models
+
+
+class ResCompany(models.Model):
+ _inherit = "res.company"
+
+ hr_attendance_compensatory_leave_type_id = fields.Many2one(
+ "hr.leave.type",
+ "Overtime compensatory leave type",
+ required=True,
+ default=lambda self: self.env.ref("hr_holidays.holiday_status_comp"),
+ help="Compensatory leave type used while validate weekly attendance sheet.",
+ )
diff --git a/hr_attendance_sheet_compensatory/models/res_config.py b/hr_attendance_sheet_compensatory/models/res_config.py
new file mode 100644
index 00000000..9afea9c4
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/models/res_config.py
@@ -0,0 +1,13 @@
+# Copyright 2021 Pierre Verkest
+# Copyright 2023 ACSONE SA/NV
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = "res.config.settings"
+
+ hr_attendance_compensatory_leave_type_id = fields.Many2one(
+ related="company_id.hr_attendance_compensatory_leave_type_id", readonly=False
+ )
diff --git a/hr_attendance_sheet_compensatory/models/resource_calendar.py b/hr_attendance_sheet_compensatory/models/resource_calendar.py
new file mode 100644
index 00000000..c5ee1ebd
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/models/resource_calendar.py
@@ -0,0 +1,21 @@
+# Copyright 2021 Pierre Verkest
+# Copyright 2023 ACSONE SA/NV
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+from odoo import fields, models
+
+
+class ResourceCalendar(models.Model):
+ _inherit = "resource.calendar"
+
+ total_hours = fields.Float(
+ "Total calendar hours",
+ compute="_compute_total_hours",
+ help="Total theoretical hours used to compute compensatory days",
+ )
+
+ def _compute_total_hours(self):
+ for rec in self:
+ rec.total_hours = sum(
+ rec.attendance_ids.mapped(lambda att: att.hour_to - att.hour_from)
+ )
diff --git a/hr_attendance_sheet_compensatory/readme/CONFIGURE.rst b/hr_attendance_sheet_compensatory/readme/CONFIGURE.rst
new file mode 100644
index 00000000..74a345cf
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/readme/CONFIGURE.rst
@@ -0,0 +1,8 @@
+* Ensure employee weeks are properly set
+* Set the leave type to use by generating compensatory
+ hours from attendance review (to be done in hr attendance configuration)
+* once all leaves and attendances has been recorded you can generate leave reviews
+ by setting up a cron job running every monday morning to generate the previous week
+ with the following code on `hr.attendance.sheet` model::
+
+ model.generate_reviews()
diff --git a/hr_attendance_sheet_compensatory/readme/CONTRIBUTORS.rst b/hr_attendance_sheet_compensatory/readme/CONTRIBUTORS.rst
new file mode 100644
index 00000000..57db78c1
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/readme/CONTRIBUTORS.rst
@@ -0,0 +1,2 @@
+* Pierre Verkest
+* Maxime Franco
diff --git a/hr_attendance_sheet_compensatory/readme/DESCRIPTION.rst b/hr_attendance_sheet_compensatory/readme/DESCRIPTION.rst
new file mode 100644
index 00000000..007b9ede
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/readme/DESCRIPTION.rst
@@ -0,0 +1,31 @@
+This is based on `hr_attendance_sheet` which add a validation mechanism
+to review employee attendance with an auto-lunch calculation.
+This allows to generate compensatory hours (allocated leaves) that can
+be used later as day off or to regulate credits leaves as
+this module is compatible with `hr_holidays_credit` module.
+
+This is based on the `hr_attendance_overtime` module which
+mark rows as "overtime" those rows are not due by default
+as it could came from possible mist check-out. So manager can
+decide to add or not those overtime attendance lines or not and
+compute or adjust compensatory/leaves hours to generate.
+
+This module is **incompatible** with hr_attendance_validation
+because it implements the same feature and especially generates
+errors during hr.attendance validation
+
+..note::
+
+ If you are allowing flexible hours - check-in/check-out range
+ are bigger than average hours per day - So you can generate
+ compensatory hours from lines that are not marked as overtime.
+
+Once review is validated attendance lines are locked on that period.
+
+At the end managers can check holidays allocation per year and
+by employee to make sure allowed employee compensatory hours are
+not over.
+
+Employees can:
+- access to validated sheets to review hours taken account
+- see current week hours on check-in view
diff --git a/hr_attendance_sheet_compensatory/readme/USAGE.rst b/hr_attendance_sheet_compensatory/readme/USAGE.rst
new file mode 100644
index 00000000..b3f57efb
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/readme/USAGE.rst
@@ -0,0 +1,5 @@
+* Once review has been generate by ir cron manager are able to
+ open each one on Attenances > Manager > Attendance validation
+* On each form decide if recorded overtime are due or not
+* change the amount of allocated compensatory hours to generate
+* validate the review to generate the allocation
diff --git a/hr_attendance_sheet_compensatory/security/hr_attendance_sheet_rule.xml b/hr_attendance_sheet_compensatory/security/hr_attendance_sheet_rule.xml
new file mode 100644
index 00000000..021538af
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/security/hr_attendance_sheet_rule.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ user: read own attendance validation sheet only
+
+ [('employee_id.user_id','=',user.id), ('state', '=', 'done')]
+
+
+
+
+
+
+
+ Manager: access to attendances validation sheets
+
+ [(1,'=',1)]
+
+
+
+
+
+
+
diff --git a/hr_attendance_sheet_compensatory/static/description/index.html b/hr_attendance_sheet_compensatory/static/description/index.html
new file mode 100644
index 00000000..058efd9f
--- /dev/null
+++ b/hr_attendance_sheet_compensatory/static/description/index.html
@@ -0,0 +1,474 @@
+
+
+
+
+
+
+HR Attendance Sheet Compensatory
+
+
+
+
+
HR Attendance Sheet Compensatory
+
+
+
+
This is based on hr_attendance_sheet which add a validation mechanism
+to review employee attendance with an auto-lunch calculation.
+This allows to generate compensatory hours (allocated leaves) that can
+be used later as day off or to regulate credits leaves as
+this module is compatible with hr_holidays_credit module.
+
This is based on the hr_attendance_overtime module which
+mark rows as “overtime” those rows are not due by default
+as it could came from possible mist check-out. So manager can
+decide to add or not those overtime attendance lines or not and
+compute or adjust compensatory/leaves hours to generate.
+
This module is incompatible with hr_attendance_validation
+because it implements the same feature and especially generates
+errors during hr.attendance validation
+
..note:
+
+If you are allowing flexible hours - check-in/check-out range
+are bigger than average hours per day - So you can generate
+compensatory hours from lines that are not marked as overtime.
+
+
Once review is validated attendance lines are locked on that period.
+
At the end managers can check holidays allocation per year and
+by employee to make sure allowed employee compensatory hours are
+not over.
+
Employees can:
+- access to validated sheets to review hours taken account
+- see current week hours on check-in view
Set the leave type to use by generating compensatory
+hours from attendance review (to be done in hr attendance configuration)
+
+
once all leaves and attendances has been recorded you can generate leave reviews
+by setting up a cron job running every monday morning to generate the previous week
+with the following code on hr.attendance.sheet model:
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.
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.