[FIX]l10n_fr_hr_holidays:fix bug in part-time employee duleaves duration calculation
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m37s
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m37s
This commit is contained in:
@@ -26,7 +26,7 @@ Credits
|
|||||||
Contributors
|
Contributors
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* `Elabore <mailto:contacnt@elabore.coop>`
|
* `Elabore <mailto:contact@elabore.coop>`
|
||||||
|
|
||||||
Funders
|
Funders
|
||||||
-------
|
-------
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"name": "France - Time Off",
|
"name": "France - Time Off",
|
||||||
"version": "16.0.1.0.1",
|
"version": "16.0.1.1.0",
|
||||||
"category": "Human Resources/Time Off",
|
"category": "Human Resources/Time Off",
|
||||||
"countries": ["fr"],
|
"countries": ["fr"],
|
||||||
"summary": "Management of leaves for part-time workers in France",
|
"summary": "Management of leaves for part-time workers in France",
|
||||||
|
|||||||
@@ -175,9 +175,14 @@ class HrLeave(models.Model):
|
|||||||
#@overwrite
|
#@overwrite
|
||||||
def _get_number_of_days_batch(self, date_from, date_to, employee_ids):
|
def _get_number_of_days_batch(self, date_from, date_to, employee_ids):
|
||||||
"""
|
"""
|
||||||
Returns a dict with the number of legal leave days for each employee,
|
Returns a dict with the number of legal leave days for each employee.
|
||||||
based on the company's calendar. In France, part-time employees accrue and take leave on company working days,
|
In France, part-time employees' leave is counted in company working days
|
||||||
not only on their own working days. Handles half-day requests and rounds according to French rules.
|
(jours ouvrables) from the first day of absence to the return day
|
||||||
|
(exclusive). The absence period is determined by the employee's calendar,
|
||||||
|
but the day count uses the company's calendar. Public holidays and other
|
||||||
|
global leaves registered on the company calendar are excluded.
|
||||||
|
For half-day requests, the first day counts as 0.5 if the employee
|
||||||
|
works the other half of that day (returns same day), or 1 otherwise.
|
||||||
"""
|
"""
|
||||||
employee = self.env['hr.employee'].browse(employee_ids)
|
employee = self.env['hr.employee'].browse(employee_ids)
|
||||||
# Force the company in the domain, as we are likely in a compute_sudo context
|
# Force the company in the domain, as we are likely in a compute_sudo context
|
||||||
@@ -188,12 +193,55 @@ class HrLeave(models.Model):
|
|||||||
calendar = self._get_calendar()
|
calendar = self._get_calendar()
|
||||||
result = employee._get_work_days_data_batch(date_from, date_to, calendar=calendar, domain=domain)
|
result = employee._get_work_days_data_batch(date_from, date_to, calendar=calendar, domain=domain)
|
||||||
for employee_id in result:
|
for employee_id in result:
|
||||||
# For non-French context: a half-day leave always counts as 0.5 day
|
if self._l10n_fr_leave_applies():
|
||||||
if self.request_unit_half and result[employee_id]['hours'] > 0 and not self._l10n_fr_leave_applies():
|
company_calendar = calendar
|
||||||
|
employee_calendar = self.resource_calendar_id
|
||||||
|
first_day = date_from.date()
|
||||||
|
last_date = date_to.date()
|
||||||
|
|
||||||
|
# _get_fr_date_from_to returns date_to with different semantics:
|
||||||
|
# - AM half-day, employee doesn't work PM: date_to = return day
|
||||||
|
# (must be excluded from the count)
|
||||||
|
# - AM half-day, employee works PM: date_to = same day (leave day)
|
||||||
|
# - General case (full-day, PM half-day): date_to = last leave day
|
||||||
|
# (day before the return, must be included)
|
||||||
|
# Only the first case has date_to as the return day. It is
|
||||||
|
# detected by: AM half-day, date_to strictly after date_from.
|
||||||
|
if (self.request_unit_half and self.request_date_from_period == 'am'
|
||||||
|
and last_date > first_day):
|
||||||
|
last_date -= relativedelta(days=1)
|
||||||
|
|
||||||
|
# Get the set of dates where the company actually works between
|
||||||
|
# first_day and last_date, excluding public holidays (global leaves).
|
||||||
|
# Don't pass the company_id-filtered domain here: the leave is
|
||||||
|
# already matched by calendar_id in _leave_intervals_batch, and
|
||||||
|
# the company_id filter would exclude holidays created on the
|
||||||
|
# french company when self.env.company is the main company.
|
||||||
|
working_dates = company_calendar._get_working_dates(first_day, last_date)
|
||||||
|
|
||||||
|
if self.request_unit_half:
|
||||||
|
# Check if the employee works the other half of the first day
|
||||||
|
dayofweek = str(first_day.weekday())
|
||||||
|
other_period = 'afternoon' if self.request_date_from_period == 'am' else 'morning'
|
||||||
|
other_half = employee_calendar.attendance_ids.filtered(
|
||||||
|
lambda a: a.dayofweek == dayofweek and a.day_period == other_period
|
||||||
|
)
|
||||||
|
first_day_count = 0.5 if (other_half and first_day in working_dates) else (1.0 if first_day in working_dates else 0.0)
|
||||||
|
days_count = first_day_count
|
||||||
|
current_date = first_day + relativedelta(days=1)
|
||||||
|
else:
|
||||||
|
days_count = 0
|
||||||
|
current_date = first_day
|
||||||
|
|
||||||
|
while current_date <= last_date:
|
||||||
|
if current_date in working_dates:
|
||||||
|
days_count += 1
|
||||||
|
current_date += relativedelta(days=1)
|
||||||
|
|
||||||
|
result[employee_id]['days'] = float(days_count)
|
||||||
|
elif self.request_unit_half and result[employee_id]['hours'] > 0:
|
||||||
|
# Non-French context: a half-day leave always counts as 0.5 day
|
||||||
result[employee_id]['days'] = 0.5
|
result[employee_id]['days'] = 0.5
|
||||||
# For French context: round the number of days to the nearest half-day (legal rule)
|
|
||||||
elif self.request_unit_half and result[employee_id]['hours'] > 0 and self._l10n_fr_leave_applies():
|
|
||||||
result[employee_id]['days'] = self._round_to_nearest_half(result[employee_id]['days'])
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _round_to_nearest_half(self, x):
|
def _round_to_nearest_half(self, x):
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
# -*- coding:utf-8 -*-
|
# -*- coding:utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import models
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
class ResourceCalendar(models.Model):
|
class ResourceCalendar(models.Model):
|
||||||
_inherit = 'resource.calendar'
|
_inherit = 'resource.calendar'
|
||||||
@@ -24,3 +27,16 @@ class ResourceCalendar(models.Model):
|
|||||||
for attendance in self.attendance_ids:
|
for attendance in self.attendance_ids:
|
||||||
working_days[attendance.week_type][attendance.dayofweek] = True
|
working_days[attendance.week_type][attendance.dayofweek] = True
|
||||||
return working_days
|
return working_days
|
||||||
|
|
||||||
|
def _get_working_dates(self, from_date, to_date, domain=None):
|
||||||
|
"""Return a set of dates where the company actually works between
|
||||||
|
from_date and to_date (inclusive), excluding public holidays
|
||||||
|
and other global leaves registered on the calendar.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
from pytz import timezone
|
||||||
|
tz = timezone(self.tz or 'UTC')
|
||||||
|
from_dt = tz.localize(datetime.combine(from_date, time.min))
|
||||||
|
to_dt = tz.localize(datetime.combine(to_date, time(23, 59, 59)))
|
||||||
|
intervals = self._work_intervals_batch(from_dt, to_dt, domain=domain)[False]
|
||||||
|
return {start.astimezone(tz).date() for start, stop, meta in intervals}
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
'country_id': country_fr.id,
|
'country_id': country_fr.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
cls.employee = cls.env['hr.employee'].create({
|
cls.base_calendar = cls.env['resource.calendar'].create({
|
||||||
|
'name': 'default calendar',
|
||||||
|
'company_id': cls.company.id,
|
||||||
|
})
|
||||||
|
cls.company.resource_calendar_id = cls.base_calendar
|
||||||
|
|
||||||
|
cls.employee = cls.env['hr.employee'].with_company(cls.company).create({
|
||||||
'name': 'Camille',
|
'name': 'Camille',
|
||||||
'gender': 'other',
|
'gender': 'other',
|
||||||
'birthday': '1973-03-29',
|
'birthday': '1973-03-29',
|
||||||
@@ -32,14 +38,29 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
'l10n_fr_reference_leave_type': cls.time_off_type.id,
|
'l10n_fr_reference_leave_type': cls.time_off_type.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
cls.base_calendar = cls.env['resource.calendar'].create({
|
def _set_employee_calendar(self, calendar):
|
||||||
'name': 'default calendar',
|
"""Set employee resource calendar and update hr.employee.calendar
|
||||||
})
|
planning record if hr_employee_calendar_planning is installed.
|
||||||
|
The planning module overrides resource_calendar_id on hr.leave via
|
||||||
|
_compute_resource_calendar_id, so we must keep the planning record
|
||||||
|
in sync with the test's intended employee calendar.
|
||||||
|
"""
|
||||||
|
self.employee.resource_calendar_id = calendar
|
||||||
|
if 'hr.employee.calendar' in self.env:
|
||||||
|
planning = self.env['hr.employee.calendar'].search(
|
||||||
|
[('employee_id', '=', self.employee.id)], limit=1)
|
||||||
|
if planning:
|
||||||
|
planning.calendar_id = calendar
|
||||||
|
else:
|
||||||
|
self.env['hr.employee.calendar'].create({
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'calendar_id': calendar.id,
|
||||||
|
})
|
||||||
|
|
||||||
def test_no_differences(self):
|
def test_no_differences(self):
|
||||||
# Base case that should not have a different behaviour
|
# Base case that should not have a different behaviour
|
||||||
self.company.resource_calendar_id = self.base_calendar
|
self.company.resource_calendar_id = self.base_calendar
|
||||||
self.employee.resource_calendar_id = self.base_calendar
|
self._set_employee_calendar(self.base_calendar)
|
||||||
|
|
||||||
leave = self.env['hr.leave'].create({
|
leave = self.env['hr.leave'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
@@ -57,6 +78,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
def test_end_of_week(self):
|
def test_end_of_week(self):
|
||||||
employee_calendar = self.env['resource.calendar'].create({
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
'name': 'Employee Calendar',
|
'name': 'Employee Calendar',
|
||||||
|
'company_id': self.company.id,
|
||||||
'attendance_ids': [
|
'attendance_ids': [
|
||||||
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
@@ -67,7 +89,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
self.company.resource_calendar_id = self.base_calendar
|
self.company.resource_calendar_id = self.base_calendar
|
||||||
self.employee.resource_calendar_id = employee_calendar
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
|
||||||
leave = self.env['hr.leave'].create({
|
leave = self.env['hr.leave'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
@@ -85,6 +107,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
def test_start_of_week(self):
|
def test_start_of_week(self):
|
||||||
employee_calendar = self.env['resource.calendar'].create({
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
'name': 'Employee Calendar',
|
'name': 'Employee Calendar',
|
||||||
|
'company_id': self.company.id,
|
||||||
'attendance_ids': [
|
'attendance_ids': [
|
||||||
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
@@ -95,7 +118,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
self.company.resource_calendar_id = self.base_calendar
|
self.company.resource_calendar_id = self.base_calendar
|
||||||
self.employee.resource_calendar_id = employee_calendar
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
|
||||||
leave = self.env['hr.leave'].create({
|
leave = self.env['hr.leave'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
@@ -114,6 +137,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
def test_last_day_half(self):
|
def test_last_day_half(self):
|
||||||
employee_calendar = self.env['resource.calendar'].create({
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
'name': 'Employee Calendar',
|
'name': 'Employee Calendar',
|
||||||
|
'company_id': self.company.id,
|
||||||
'attendance_ids': [
|
'attendance_ids': [
|
||||||
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
@@ -124,7 +148,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
self.company.resource_calendar_id = self.base_calendar
|
self.company.resource_calendar_id = self.base_calendar
|
||||||
self.employee.resource_calendar_id = employee_calendar
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
|
||||||
leave = self.env['hr.leave'].create({
|
leave = self.env['hr.leave'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
@@ -150,6 +174,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
def test_full_time_am_day_half(self):
|
def test_full_time_am_day_half(self):
|
||||||
employee_calendar = self.env['resource.calendar'].create({
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
'name': 'Employee Calendar',
|
'name': 'Employee Calendar',
|
||||||
|
'company_id': self.company.id,
|
||||||
'attendance_ids': [
|
'attendance_ids': [
|
||||||
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
@@ -163,7 +188,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
self.company.resource_calendar_id = self.base_calendar
|
self.company.resource_calendar_id = self.base_calendar
|
||||||
self.employee.resource_calendar_id = employee_calendar
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
|
||||||
leave = self.env['hr.leave'].create({
|
leave = self.env['hr.leave'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
@@ -184,6 +209,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
def test_am_day_half(self):
|
def test_am_day_half(self):
|
||||||
employee_calendar = self.env['resource.calendar'].create({
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
'name': 'Employee Calendar',
|
'name': 'Employee Calendar',
|
||||||
|
'company_id': self.company.id,
|
||||||
'attendance_ids': [
|
'attendance_ids': [
|
||||||
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
@@ -193,7 +219,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
self.company.resource_calendar_id = self.base_calendar
|
self.company.resource_calendar_id = self.base_calendar
|
||||||
self.employee.resource_calendar_id = employee_calendar
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
|
||||||
leave = self.env['hr.leave'].create({
|
leave = self.env['hr.leave'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
@@ -214,6 +240,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
def test_calendar_with_holes(self):
|
def test_calendar_with_holes(self):
|
||||||
employee_calendar = self.env['resource.calendar'].create({
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
'name': 'Employee Calendar',
|
'name': 'Employee Calendar',
|
||||||
|
'company_id': self.company.id,
|
||||||
'attendance_ids': [
|
'attendance_ids': [
|
||||||
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
@@ -224,7 +251,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
self.company.resource_calendar_id = self.base_calendar
|
self.company.resource_calendar_id = self.base_calendar
|
||||||
self.employee.resource_calendar_id = employee_calendar
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
|
||||||
leave = self.env['hr.leave'].create({
|
leave = self.env['hr.leave'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
@@ -242,6 +269,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
def test_calendar_end_week_hole(self):
|
def test_calendar_end_week_hole(self):
|
||||||
employee_calendar = self.env['resource.calendar'].create({
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
'name': 'Employee Calendar',
|
'name': 'Employee Calendar',
|
||||||
|
'company_id': self.company.id,
|
||||||
'attendance_ids': [
|
'attendance_ids': [
|
||||||
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
@@ -250,7 +278,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
self.company.resource_calendar_id = self.base_calendar
|
self.company.resource_calendar_id = self.base_calendar
|
||||||
self.employee.resource_calendar_id = employee_calendar
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
|
||||||
leave = self.env['hr.leave'].create({
|
leave = self.env['hr.leave'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
@@ -268,6 +296,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
def test_2_weeks_calendar(self):
|
def test_2_weeks_calendar(self):
|
||||||
company_calendar = self.env['resource.calendar'].create({
|
company_calendar = self.env['resource.calendar'].create({
|
||||||
'name': 'Company Calendar',
|
'name': 'Company Calendar',
|
||||||
|
'company_id': self.company.id,
|
||||||
'two_weeks_calendar': True,
|
'two_weeks_calendar': True,
|
||||||
'attendance_ids': [
|
'attendance_ids': [
|
||||||
(0, 0, {'week_type': '0', 'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
(0, 0, {'week_type': '0', 'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
@@ -291,6 +320,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
})
|
})
|
||||||
employee_calendar = self.env['resource.calendar'].create({
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
'name': 'Employee Calendar',
|
'name': 'Employee Calendar',
|
||||||
|
'company_id': self.company.id,
|
||||||
'attendance_ids': [
|
'attendance_ids': [
|
||||||
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
@@ -301,7 +331,7 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
self.company.resource_calendar_id = company_calendar
|
self.company.resource_calendar_id = company_calendar
|
||||||
self.employee.resource_calendar_id = employee_calendar
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
|
||||||
# Week type 0
|
# Week type 0
|
||||||
leave = self.env['hr.leave'].create({
|
leave = self.env['hr.leave'].create({
|
||||||
@@ -351,9 +381,241 @@ class TestFrenchLeaves(TransactionCase):
|
|||||||
'employee_id': self.employee.id,
|
'employee_id': self.employee.id,
|
||||||
'request_date_from': '2021-09-13',
|
'request_date_from': '2021-09-13',
|
||||||
'request_date_to': '2021-09-22',
|
'request_date_to': '2021-09-22',
|
||||||
'company_id': self.company.id,
|
'employee_company_id': self.company.id,
|
||||||
'resource_calendar_id': self.employee.resource_calendar_id.id,
|
'resource_calendar_id': self.employee.resource_calendar_id.id,
|
||||||
})
|
})
|
||||||
leave._compute_date_from_to()
|
leave._compute_date_from_to()
|
||||||
self.assertEqual(leave.number_of_days, 8, 'The number of days should be equal to 3.')
|
self.assertEqual(leave.number_of_days, 8, 'The number of days should be equal to 3.')
|
||||||
leave.unlink()
|
leave.unlink()
|
||||||
|
|
||||||
|
def test_part_time_different_hours(self):
|
||||||
|
# Regression test: when the employee's working hours differ from the
|
||||||
|
# company's on the boundary day, the hourly-ratio computation used to
|
||||||
|
# return a fractional number of days (e.g. 12.86 instead of 13).
|
||||||
|
# French legal rule: paid leaves are counted in company working days
|
||||||
|
# (jours ouvrables) as full days.
|
||||||
|
# Company works 6 days/week (Mon-Sat), 9-12 + 13-17 = 7h/day.
|
||||||
|
company_calendar = self.env['resource.calendar'].create({
|
||||||
|
'name': 'Company Calendar 6d/week',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'attendance_ids': [
|
||||||
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Saturday Morning', 'dayofweek': '5', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Saturday Afternoon', 'dayofweek': '5', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
# Employee works Mon, Tue, Thu (part-time), 8-12 + 13-16 = 7h/day but
|
||||||
|
# with shifted hours vs the company, so the boundary day is fractional
|
||||||
|
# under the old hourly-ratio computation.
|
||||||
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
|
'name': 'Employee Calendar Part Time',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'attendance_ids': [
|
||||||
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
self.company.resource_calendar_id = company_calendar
|
||||||
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
self.employee.tz = 'Europe/Paris'
|
||||||
|
|
||||||
|
# Leave from 12/01/2026 (Monday) to 26/01/2026 (Monday) inclusive.
|
||||||
|
# Period: 15 calendar days, 2 Sundays -> 13 company working days.
|
||||||
|
# Dates must be in the past: default_get sets date_to to today if
|
||||||
|
# request_date_to is not in defaults, and the SQL constraint
|
||||||
|
# CHECK(date_from <= date_to) fails if date_from > today during
|
||||||
|
# the intermediate flush in _compute_date_from_to.
|
||||||
|
leave = self.env['hr.leave'].create({
|
||||||
|
'name': 'Test part-time bug',
|
||||||
|
'holiday_status_id': self.time_off_type.id,
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'request_date_from': '2026-01-12',
|
||||||
|
'request_date_to': '2026-01-26',
|
||||||
|
'employee_company_id': self.company.id,
|
||||||
|
'resource_calendar_id': self.employee.resource_calendar_id.id,
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
self.assertEqual(leave.number_of_days, 13.0,
|
||||||
|
'The number of days should be 13 (jours ouvrables), not 12.86.')
|
||||||
|
leave.unlink()
|
||||||
|
|
||||||
|
def test_part_time_half_day_different_hours(self):
|
||||||
|
# Regression test for French part-time half-day leave counting.
|
||||||
|
# Employee works: Mon, Tue, Wed(AM only), Thu, Fri(AM only).
|
||||||
|
# Company works 6 days/week (Mon-Sat), 9-17h.
|
||||||
|
# French rule: count company working days (jours ouvrables) from
|
||||||
|
# first day of absence to return day (exclusive).
|
||||||
|
company_calendar = self.env['resource.calendar'].create({
|
||||||
|
'name': 'Company Calendar 6d/week',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'attendance_ids': [
|
||||||
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Saturday Morning', 'dayofweek': '5', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Saturday Afternoon', 'dayofweek': '5', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
|
'name': 'Employee Calendar Part Time',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'attendance_ids': [
|
||||||
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
self.company.resource_calendar_id = company_calendar
|
||||||
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
self.employee.tz = 'Europe/Paris'
|
||||||
|
|
||||||
|
# Case 1: Wednesday AM half-day.
|
||||||
|
# Employee doesn't work Wed PM -> return = Thu.
|
||||||
|
# Company working days from Wed to Thu (exclusive) = Wed = 1 day.
|
||||||
|
leave = self.env['hr.leave'].create({
|
||||||
|
'name': 'Test Wed AM',
|
||||||
|
'holiday_status_id': self.time_off_type.id,
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'request_date_from': '2021-09-08',
|
||||||
|
'request_date_to': '2021-09-08',
|
||||||
|
'request_unit_half': True,
|
||||||
|
'request_date_from_period': 'am',
|
||||||
|
'employee_company_id': self.company.id,
|
||||||
|
'resource_calendar_id': self.employee.resource_calendar_id.id,
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
self.assertEqual(leave.number_of_days, 1.0,
|
||||||
|
'Wed AM half-day should count as 1 jour ouvrable.')
|
||||||
|
leave.unlink()
|
||||||
|
|
||||||
|
# Case 2: Thursday + Friday full days.
|
||||||
|
# Return = Monday. Company working days: Thu, Fri, Sat = 3 days.
|
||||||
|
leave = self.env['hr.leave'].create({
|
||||||
|
'name': 'Test Thu+Fri',
|
||||||
|
'holiday_status_id': self.time_off_type.id,
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'request_date_from': '2021-09-09',
|
||||||
|
'request_date_to': '2021-09-10',
|
||||||
|
'employee_company_id': self.company.id,
|
||||||
|
'resource_calendar_id': self.employee.resource_calendar_id.id,
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
self.assertEqual(leave.number_of_days, 3.0,
|
||||||
|
'Thu+Fri full days should count as 3 jours ouvrables (Thu, Fri, Sat).')
|
||||||
|
leave.unlink()
|
||||||
|
|
||||||
|
# Case 3: Friday AM half-day.
|
||||||
|
# Employee doesn't work Fri PM -> return = Monday.
|
||||||
|
# Company working days from Fri to Mon (exclusive) = Fri, Sat = 2 days.
|
||||||
|
leave = self.env['hr.leave'].create({
|
||||||
|
'name': 'Test Fri AM',
|
||||||
|
'holiday_status_id': self.time_off_type.id,
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'request_date_from': '2021-09-10',
|
||||||
|
'request_date_to': '2021-09-10',
|
||||||
|
'request_unit_half': True,
|
||||||
|
'request_date_from_period': 'am',
|
||||||
|
'employee_company_id': self.company.id,
|
||||||
|
'resource_calendar_id': self.employee.resource_calendar_id.id,
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
self.assertEqual(leave.number_of_days, 2.0,
|
||||||
|
'Fri AM half-day should count as 2 jours ouvrables (Fri, Sat).')
|
||||||
|
leave.unlink()
|
||||||
|
|
||||||
|
def test_public_holiday_exclusion(self):
|
||||||
|
# Regression test: public holidays (global leaves on the company
|
||||||
|
# calendar) must be excluded from the jours ouvrables count.
|
||||||
|
# Company works 6 days/week (Mon-Sat). Public holiday on 14/07/2020.
|
||||||
|
# Leave 13/07/2020 (Mon) -> 19/07/2020 (Sun).
|
||||||
|
# Company working days without holiday: 13, 14, 15, 16, 17, 18 = 6
|
||||||
|
# With 14/07 being a public holiday: 13, 15, 16, 17, 18 = 5
|
||||||
|
company_calendar = self.env['resource.calendar'].create({
|
||||||
|
'name': 'Company Calendar 6d/week',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'tz': 'Europe/Paris',
|
||||||
|
'attendance_ids': [
|
||||||
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Saturday Morning', 'dayofweek': '5', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Saturday Afternoon', 'dayofweek': '5', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
employee_calendar = self.env['resource.calendar'].create({
|
||||||
|
'name': 'Employee Calendar Part Time',
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'tz': 'Europe/Paris',
|
||||||
|
'attendance_ids': [
|
||||||
|
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||||
|
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
self.company.resource_calendar_id = company_calendar
|
||||||
|
self._set_employee_calendar(employee_calendar)
|
||||||
|
self.employee.tz = 'Europe/Paris'
|
||||||
|
|
||||||
|
# Create a public holiday on 14/07/2020 (Tuesday) on the company calendar
|
||||||
|
self.env['resource.calendar.leaves'].create({
|
||||||
|
'name': 'Bastille Day',
|
||||||
|
'calendar_id': company_calendar.id,
|
||||||
|
'date_from': '2020-07-14 00:00:00',
|
||||||
|
'date_to': '2020-07-14 23:59:59',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Leave from 13/07/2020 (Monday) to 19/07/2020 (Sunday).
|
||||||
|
# Employee works Mon, Tue, Thu, Fri. Leave covers Mon-Sun.
|
||||||
|
# Return = Monday 20/07. date_to extended to Sunday 19/07 (non-working).
|
||||||
|
# Company working days (Mon-Sat) excluding 14/07 holiday:
|
||||||
|
# 13(Mon), 15(Wed), 16(Thu), 17(Fri), 18(Sat) = 5 days
|
||||||
|
leave = self.env['hr.leave'].create({
|
||||||
|
'name': 'Test public holiday',
|
||||||
|
'holiday_status_id': self.time_off_type.id,
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'request_date_from': '2020-07-13',
|
||||||
|
'request_date_to': '2020-07-19',
|
||||||
|
'employee_company_id': self.company.id,
|
||||||
|
'resource_calendar_id': self.employee.resource_calendar_id.id,
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
self.assertEqual(leave.number_of_days, 5.0,
|
||||||
|
'The number of days should be 5 (14/07 excluded as public holiday).')
|
||||||
|
leave.unlink()
|
||||||
|
|||||||
Reference in New Issue
Block a user