Skip to content

Commit

Permalink
Merge PR #323 into 12.0
Browse files Browse the repository at this point in the history
Signed-off-by huguesdk
  • Loading branch information
github-grap-bot committed Oct 9, 2024
2 parents 93a2dd2 + 7338dbd commit 387a23d
Show file tree
Hide file tree
Showing 19 changed files with 1,023 additions and 223 deletions.
66 changes: 47 additions & 19 deletions resource_work_time_from_contracts/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Resource Work Time From Contracts
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:932880843bef5ec49d99664f85891a875b13ed1c68206e95dc5a2255907b88f9
!! source digest: sha256:7776625e183b593987ca71b9d7b76f7a3c602e6cbf8393803c571444d7bfbe9b
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
Expand All @@ -27,34 +27,62 @@ day.

When this module is installed, the number of hours an employee is supposed to
work is only computed from their contracts. Without contracts, the work time
per day is 0, instead of using the default company’s working hours.
per day is 0, instead of using the default work schedule of the company.

The start and end dates of contracts are taken into account, but the status
(state) of contracts are ignored.

For this module to work properly, the company’s working hours should encompass
all possible work days (including weekend days if there are contracts with
weekend days), and each day should have working hours that correspond to the
working hours used in all contracts. This is because the company’s working
hours are used to compute leaves, and the number of hours per day is computed
from it.

For example, if the company working hours define 8 hours per day, from 8 to 12
and 13 to 17, all contracts’ working hours should be set from 8 to 12 and/or
from 13 to 17 for the corresponding days. Half days are thus supported.

If there are contracts with working hours that don’t match the company’s
working hours, the number of days for leaves will be computed incorrectly.

This module also makes the working hours (resource calendar) of an employee
always equal to the company’s working hours, and hides its field on the
employee form view.
This module also makes the work schedule (``resource.calendar``) of an
employee always equal to the work schedule of the company, and hides its field
on the employee form view. This is because this work schedule is only used to
store all the leaves (``resource.calendar.leaves``) of all employees and is
otherwise ignored.

**Table of contents**

.. contents::
:local:

Usage
=====

The number of work hours in a day is taken from the ``hours_per_day`` field of
the work schedule (``resource.calendar``). This field is automatically
computed when work intervals are edited, but its value can also be set
manually.

It is useful to set it manually in case of work schedules with irregular days.
For example, a work schedule of 8 hours per day for 4 days and 4 hours for 1
day should have its ``hours_per_day`` set to 8. This way, the total work time
for a week (used to compute leaves, for example) is exactly 4.5 days.

It is possible to control the rounding of the computed number of days. This
can be useful for work schedules with irregular days, where some days have
durations that don't match common divisions of a full day.

The rounding mode is controlled by the
``resource_work_time_from_contracts.day_rounding_mode`` system parameter
(``ir.config_parameter``). Here are its supported values:

* ``none``: No rounding is done. This is the default.
* ``round``: Round to the nearest unit.
* ``ceil``: Round to the next unit above the value.

For the ``round`` and ``ceil`` modes, the
``resource_work_time_from_contracts.day_rounding_granularity`` system
parameter defines the unit to which to round values. The default value is
``1``, which means to round to full days. Setting it to ``0.5``, for example,
will round to half days.

There is currently no custom view to configure these settings. They must be
set manually using the System Parameters view from the Technical Settings menu
(accessible by enabling the developer mode).

Known issues / Roadmap
======================

* Taking leaves in hours is not yet correctly supported.

Bug Tracker
===========

Expand Down
1 change: 1 addition & 0 deletions resource_work_time_from_contracts/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import models
from .hooks import post_load_hook
3 changes: 2 additions & 1 deletion resource_work_time_from_contracts/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
"category": "Human Resources",
"depends": [
"hr_contract",
"hr_holidays",
],
"data": [
"views/hr_employee.xml",
"views/resource_resource.xml",
],
"demo": [],
"post_load": "post_load_hook",
}
101 changes: 101 additions & 0 deletions resource_work_time_from_contracts/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# SPDX-FileCopyrightText: 2024 Coop IT Easy SC
#
# SPDX-License-Identifier: AGPL-3.0-or-later

from datetime import datetime

from pytz import UTC, timezone

from odoo import api

from odoo.addons.hr_holidays.models.hr_leave import HolidaysRequest
from odoo.addons.resource.models.resource import float_to_time


def patch_hr_leave_onchange_request_parameters():
if not hasattr(HolidaysRequest, "_onchange_request_parameters_original"):
HolidaysRequest._onchange_request_parameters_original = (
HolidaysRequest._onchange_request_parameters
)
# we need to remove the _onchange property of the method, otherwise it
# will still be called.
HolidaysRequest._onchange_request_parameters_original._onchange_original = (
HolidaysRequest._onchange_request_parameters_original._onchange
)
del HolidaysRequest._onchange_request_parameters_original._onchange

# in hr_holidays, this method reads the resource.calendar.attendance
# records directly from the employee's or company resource.calendar to
# find the first one and the last one corresponding to the leave date
# range. below is a copy of the method from hr_holidays, with that part
# replaced by a call to _get_request_attendances() to allow to change that
# behavior.
@api.onchange(
"request_date_from_period",
"request_hour_from",
"request_hour_to",
"request_date_from",
"request_date_to",
"employee_id",
)
def __new_onchange_request_parameters(self):
if not self.request_date_from:
self.date_from = False
return

if self.request_unit_half or self.request_unit_hours:
self.request_date_to = self.request_date_from

if not self.request_date_to:
self.date_to = False
return

# begin change
attendance_from, attendance_to = self._get_request_attendances()
# end change

if self.request_unit_half:
if self.request_date_from_period == "am":
hour_from = float_to_time(attendance_from.hour_from)
hour_to = float_to_time(attendance_from.hour_to)
else:
hour_from = float_to_time(attendance_to.hour_from)
hour_to = float_to_time(attendance_to.hour_to)
elif self.request_unit_hours:
# This hack is related to the definition of the field, basically we convert
# the negative integer into .5 floats
hour_from = float_to_time(
abs(self.request_hour_from) - 0.5
if self.request_hour_from < 0
else self.request_hour_from
)
hour_to = float_to_time(
abs(self.request_hour_to) - 0.5
if self.request_hour_to < 0
else self.request_hour_to
)
elif self.request_unit_custom:
hour_from = self.date_from.time()
hour_to = self.date_to.time()
else:
hour_from = float_to_time(attendance_from.hour_from)
hour_to = float_to_time(attendance_to.hour_to)
self.date_from = (
timezone(self.tz)
.localize(datetime.combine(self.request_date_from, hour_from))
.astimezone(UTC)
.replace(tzinfo=None)
)
self.date_to = (
timezone(self.tz)
.localize(datetime.combine(self.request_date_to, hour_to))
.astimezone(UTC)
.replace(tzinfo=None)
)
self._onchange_leave_dates()

HolidaysRequest._onchange_request_parameters = __new_onchange_request_parameters


def post_load_hook():
patch_hr_leave_onchange_request_parameters()
2 changes: 2 additions & 0 deletions resource_work_time_from_contracts/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from . import hr_leave
from . import resource_calendar
from . import resource_calendar_leaves
from . import resource_mixin
from . import resource_resource
26 changes: 26 additions & 0 deletions resource_work_time_from_contracts/models/hr_leave.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2024 Coop IT Easy SC
#
# SPDX-License-Identifier: AGPL-3.0-or-later

from odoo import models

from odoo.addons.hr_holidays.models.hr_leave import DummyAttendance


class HrLeave(models.Model):
_inherit = "hr.leave"

def _get_request_attendances(self):
"""
Get the first and last resource.calendar.attendance corresponding to
the range defined by request_date_from and request_date_to.
"""
attendance_from, attendance_to = self.employee_id.get_attendances_of_date_range(
self.request_date_from, self.request_date_to
)
default_value = DummyAttendance(0, 0, 0, "morning")
if attendance_from is None:
attendance_from = default_value
if attendance_to is None:
attendance_to = default_value
return attendance_from, attendance_to
63 changes: 63 additions & 0 deletions resource_work_time_from_contracts/models/resource_calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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):
# we should get the first attendance from a list of all the
# attendances that starts with the weekday of date_from then wraps
# around and ends with the weekday before date_from. the list should
# be no longer (in terms of weekdays) than date_to - date_from + 1.
all_attendances = self.attendance_ids
if not all_attendances:
return None
weekday_from = date_from.weekday()
all_attendances = all_attendances.filtered(
lambda r: int(r.dayofweek) >= weekday_from
) + all_attendances.filtered(lambda r: int(r.dayofweek) < weekday_from)
first_attendance = all_attendances[0]
weekday_diff = int(first_attendance.dayofweek) - weekday_from
if weekday_diff < 0:
weekday_diff += 7
first_attendance_date = date_from + datetime.timedelta(days=weekday_diff)
return (first_attendance_date, first_attendance)

def _get_last_attendance(self, date_to):
# do the same as _get_first_attendance(), but in the other direction.
all_attendances = self.attendance_ids
if not all_attendances:
return None
weekday_to = date_to.weekday()
all_attendances = all_attendances.filtered(
lambda r: int(r.dayofweek) > weekday_to
) + all_attendances.filtered(lambda r: int(r.dayofweek) <= weekday_to)
last_attendance = all_attendances[-1]
weekday_diff = int(last_attendance.dayofweek) - weekday_to
if weekday_diff > 0:
weekday_diff -= 7
last_attendance_date = date_to + datetime.timedelta(days=weekday_diff)
return (last_attendance_date, last_attendance)

def get_first_last_attendance(self, date_from, date_to):
"""
Get the first and last attendances between (and including) date_from
and date_to.
Return 2 tuples of the form (date, attendance). Each of the tuples can
be None if there is no match.
"""
first_attendance_tuple = self._get_first_attendance(date_from)
last_attendance_tuple = self._get_last_attendance(date_to)
# some small checks.
if first_attendance_tuple and first_attendance_tuple[0] > date_to:
first_attendance_tuple = None
if last_attendance_tuple and last_attendance_tuple[0] < date_from:
last_attendance_tuple = None
return (first_attendance_tuple, last_attendance_tuple)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@


class ResourceCalendarLeaves(models.Model):

_inherit = "resource.calendar.leaves"

# force this field to be equal to the resource_calendar_id of the resource
Expand Down
Loading

0 comments on commit 387a23d

Please sign in to comment.