[MIG]l10n_fr_hr_holidays:backport from v17 to 16

This commit is contained in:
2025-09-09 10:33:17 +02:00
committed by laetitiadacosta
parent f206606a96
commit dc5fd2897a
13 changed files with 777 additions and 0 deletions

2
l10n_fr_hr_holidays/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.*~
*pyc

View File

@@ -0,0 +1,41 @@
===================
l10n_fr_hr_holidays
===================
Manages French specificities for leaves and holidays, specialy for part-time employees.
Installation
============
Use Odoo normal module installation procedure to install
``l10n_fr_hr_holidays``.
Configuration
=============
In settings, select the leaves type on wich you want to manage the french specificities.
Known issues / Roadmap
======================
None yet.
Credits
=======
Contributors
------------
* `Elabore <mailto:contacnt@elabore.coop>`
Funders
-------
The development of this module has been financially supported by:
* Elabore (https://elabore.coop)
Maintainer
----------
This module is maintained by Elabore.

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
"name": "France - Time Off",
"version": "16.0.1.0.0",
"category": "Human Resources/Time Off",
"countries": ["fr"],
"summary": "Management of leaves for part-time workers in France",
"depends": ["hr_holidays", "l10n_fr","resource"],
"auto_install": True,
"license": "LGPL-3",
"data": [
"views/res_config_settings_views.xml",
],
"demo": [
"data/l10n_fr_hr_holidays_demo.xml",
],
}

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="l10n_fr_part_time_calendar" model="resource.calendar">
<field name="name">Part time</field>
<field name="company_id" eval="False"/>
<field name="hours_per_day">9</field>
<field name="attendance_ids"
eval="[(5, 0, 0),
(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': 18.0, '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': 18.0, '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': 18.0, 'day_period': 'afternoon'}),
]"
/>
</record>
<record id="l10n_fr_part_time_employee" model="hr.employee">
<field name="company_id" ref="l10n_fr.demo_company_fr"/>
<field name="active" eval="1"/>
<field name="name">Mitchell Admin</field>
<field name="user_id" ref="base.user_admin"/>
<field name="resource_calendar_id" ref="l10n_fr_part_time_calendar"/>
<field name="image_1920" eval="obj(ref('base.partner_admin')).image_1920" model="res.partner"/>
</record>
<record id="l10n_fr_holiday_status_cl" model="hr.leave.type">
<field name="name">Paid Time Off</field>
<field name="company_id" ref="l10n_fr.demo_company_fr"/>
<field name="requires_allocation">yes</field>
<field name="employee_requests">no</field>
<field name="leave_validation_type">both</field>
<field name="allocation_validation_type">officer</field>
<field name="responsible_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="icon_id" ref="hr_holidays.icon_14"/>
<field name="color">2</field>
<field name="has_valid_allocation">True</field>
</record>
<record id="l10n_fr.demo_company_fr" model="res.company">
<field name="l10n_fr_reference_leave_type" ref="l10n_fr_holiday_status_cl"/>
</record>
<record id="l10n_fr_hr_holidays_allocation" model="hr.leave.allocation">
<field name="name">Paid Time Off allocation</field>
<field name="state">confirm</field>
<field name="holiday_status_id" ref="l10n_fr_holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="date_from" eval="time.strftime('%Y-01-01')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
<field name="employee_id" ref="l10n_fr_part_time_employee"/>
<field name="employee_ids" eval="[(4, ref('l10n_fr_part_time_employee'))]"/>
</record>
<function model="hr.leave.allocation" name="action_validate">
<value eval="[ref('l10n_fr_hr_holidays_allocation')]"/>
</function>
</odoo>

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_leave
from . import res_company
from . import resource_calendar
from . import res_config_settings

View File

@@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import fields, models, _
from odoo.exceptions import UserError
class HrLeave(models.Model):
_inherit = 'hr.leave'
resource_calendar_id = fields.Many2one('resource.calendar', compute='_compute_resource_calendar_id', store=True, readonly=False, copy=False)
l10n_fr_date_to_changed = fields.Boolean()
def _compute_resource_calendar_id(self):
for leave in self:
calendar = False
if leave.holiday_type == 'employee':
calendar = leave.employee_id.resource_calendar_id
# YTI: Crappy hack: Move this to a new dedicated hr_holidays_contract module
# We use the request dates to find the contracts, because date_from
# and date_to are not set yet at this point. Since these dates are
# used to get the contracts for which these leaves apply and
# contract start- and end-dates are just dates (and not datetimes)
# these dates are comparable.
if 'hr.contract' in self.env and leave.employee_id:
contracts = self.env['hr.contract'].search([
'|', ('state', 'in', ['open', 'close']),
'&', ('state', '=', 'draft'),
('kanban_state', '=', 'done'),
('employee_id', '=', leave.employee_id.id),
('date_start', '<=', leave.request_date_to),
'|', ('date_end', '=', False),
('date_end', '>=', leave.request_date_from),
])
if contracts:
# If there are more than one contract they should all have the
# same calendar, otherwise a constraint is violated.
calendar = contracts[:1].resource_calendar_id
elif leave.holiday_type == 'department':
calendar = leave.department_id.company_id.resource_calendar_id
elif leave.holiday_type == 'company':
calendar = leave.mode_company_id.resource_calendar_id
leave.resource_calendar_id = calendar or self.env.company.resource_calendar_id
def _l10n_fr_leave_applies(self):
# The french l10n is meant to be computed only in very specific cases:
# - there is only one employee affected by the leave
# - the company is french
# - the leave_type is the reference leave_type of that company
self.ensure_one()
return self.employee_id and \
self.employee_company_id.country_id.code == 'FR' and \
self.resource_calendar_id != self.employee_company_id.resource_calendar_id and \
self.holiday_status_id == self.employee_company_id._get_fr_reference_leave_type()
def _get_fr_date_from_to(self, date_from, date_to):
self.ensure_one()
# What we need to compute is how much we will need to push date_to in order to account for the lost days
# This gets even more complicated in two_weeks_calendars
# The following computation doesn't work for resource calendars in
# which the employee works zero hours.
if not (self.resource_calendar_id.attendance_ids):
raise UserError(_("An employee cannot take a paid time off in a period they work no hours."))
if self.request_unit_half and self.request_date_from_period == 'am':
# In normal workflows request_unit_half implies that date_from and date_to are the same
# request_unit_half allows us to choose between `am` and `pm`
# In a case where we work from mon-wed and request a half day in the morning
# we do not want to push date_to since the next work attendance is actually in the afternoon
date_from_weektype = str(self.env['resource.calendar.attendance'].get_week_type(date_from))
date_from_dayofweek = str(date_from.weekday())
# Get morning and afternoon attendances for that day
attendances_am = self.resource_calendar_id.attendance_ids.filtered(lambda a:
a.dayofweek == date_from_dayofweek
and a.day_period == 'morning'
and (not self.resource_calendar_id.two_weeks_calendar or a.week_type == date_from_weektype))
attendances_pm = self.resource_calendar_id.attendance_ids.filtered(lambda a:
a.dayofweek == date_from_dayofweek
and a.day_period == 'afternoon'
and (not self.resource_calendar_id.two_weeks_calendar or a.week_type == date_from_weektype))
if attendances_am and not attendances_pm:
# If the employee does not work in the afternoon, postpone date_to to the next working day
next_date = date_from + relativedelta(days=1)
while not self.resource_calendar_id._works_on_date(next_date):
next_date += relativedelta(days=1)
return (date_from, next_date)
elif attendances_am and attendances_pm:
# The employee also works in the afternoon, no postponement
return (date_from, date_to)
# Special handling for two-weeks calendars
if self.resource_calendar_id.two_weeks_calendar:
# Count the number of days actually worked by the employee between date_from and date_to
current_date = date_from
days_count = 0
while current_date <= date_to:
if self.resource_calendar_id._works_on_date(current_date):
days_count += 1
current_date += relativedelta(days=1)
# Adjust date_to so it matches the expected number of days
# If the expected number of days is less than the period, reduce date_to
if days_count > 0:
# Find the date_to that gives the right number of worked days
current_date = date_from
counted = 0
while counted < days_count:
if self.resource_calendar_id._works_on_date(current_date):
counted += 1
if counted == days_count:
break
current_date += relativedelta(days=1)
return (date_from, current_date)
# Check calendars for working days until we find the right target, start at date_to + 1 day
# Postpone date_target until the next working day
date_start = date_from
date_target = date_to
# It is necessary to move the start date up to the first work day of
# the employee calendar as otherwise days worked on by the company
# calendar before the actual start of the leave would be taken into
# account.
while not self.resource_calendar_id._works_on_date(date_start):
date_start += relativedelta(days=1)
while not self.resource_calendar_id._works_on_date(date_target + relativedelta(days=1)):
date_target += relativedelta(days=1)
# Undo the last day increment
return (date_start, date_target)
def _compute_date_from_to(self):
super()._compute_date_from_to()
for leave in self:
if leave._l10n_fr_leave_applies():
new_date_from, new_date_to = leave._get_fr_date_from_to(leave.date_from, leave.date_to)
if new_date_from != leave.date_from:
leave.date_from = new_date_from
if new_date_to != leave.date_to:
leave.date_to = new_date_to
leave.l10n_fr_date_to_changed = True
else:
leave.l10n_fr_date_to_changed = False
#@overwrite
def _get_calendar(self):
"""
In France, paid time off for part-time employees is counted on the company's working days (not the employee's own schedule).
The company's calendar must be used for the legal leave day count.
"""
self.ensure_one()
if self._l10n_fr_leave_applies():
return self.employee_company_id.resource_calendar_id or self.env.company.resource_calendar_id
return super()._get_calendar()
#@overwrite
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,
based on the company's calendar. In France, part-time employees accrue and take leave on company working days,
not only on their own working days. Handles half-day requests and rounds according to French rules.
"""
employee = self.env['hr.employee'].browse(employee_ids)
# Force the company in the domain, as we are likely in a compute_sudo context
domain = [
('time_type', '=', 'leave'),
('company_id', 'in', self.env.company.ids + self.env.context.get('allowed_company_ids', []))
]
calendar = self._get_calendar()
result = employee._get_work_days_data_batch(date_from, date_to, calendar=calendar, domain=domain)
for employee_id in result:
# For non-French context: a half-day leave always counts as 0.5 day
if self.request_unit_half and result[employee_id]['hours'] > 0 and not self._l10n_fr_leave_applies():
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
def _round_to_nearest_half(self, x):
"""Round a float to the nearest 0.5."""
return round(x * 2) / 2

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.exceptions import ValidationError
class ResCompany(models.Model):
_inherit = 'res.company'
l10n_fr_reference_leave_type = fields.Many2one(
'hr.leave.type',
string='Company Paid Time Off Type')
def _get_fr_reference_leave_type(self):
self.ensure_one()
if not self.l10n_fr_reference_leave_type:
raise ValidationError(_("You must first define a reference time off type for the company."))
return self.l10n_fr_reference_leave_type

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
l10n_fr_reference_leave_type = fields.Many2one(
'hr.leave.type',
related='company_id.l10n_fr_reference_leave_type',
readonly=False)
# backport from V170
company_country_code = fields.Char(related="company_id.country_id.code", string="Company Country Code", readonly=True)

View File

@@ -0,0 +1,26 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from collections import defaultdict
class ResourceCalendar(models.Model):
_inherit = 'resource.calendar'
def _works_on_date(self, date):
self.ensure_one()
working_days = self._get_working_hours()
dayofweek = str(date.weekday())
if self.two_weeks_calendar:
weektype = str(self.env['resource.calendar.attendance'].get_week_type(date))
return working_days[weektype][dayofweek]
return working_days[False][dayofweek]
def _get_working_hours(self):
self.ensure_one()
working_days = defaultdict(lambda: defaultdict(lambda: False))
for attendance in self.attendance_ids:
working_days[attendance.week_type][attendance.dayofweek] = True
return working_days

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_french_leaves

View File

@@ -0,0 +1,359 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase, tagged
@tagged('post_install_l10n', 'post_install', '-at_install', 'french_leaves')
class TestFrenchLeaves(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
country_fr = cls.env.ref('base.fr')
cls.company = cls.env['res.company'].create({
'name': 'French Company',
'country_id': country_fr.id,
})
cls.employee = cls.env['hr.employee'].create({
'name': 'Camille',
'gender': 'other',
'birthday': '1973-03-29',
'country_id': country_fr.id,
'company_id': cls.company.id,
})
cls.time_off_type = cls.env['hr.leave.type'].create({
'name': 'Time Off',
'requires_allocation': 'no',
'request_unit': 'half_day',
})
cls.company.write({
'l10n_fr_reference_leave_type': cls.time_off_type.id,
})
cls.base_calendar = cls.env['resource.calendar'].create({
'name': 'default calendar',
})
def test_no_differences(self):
# Base case that should not have a different behaviour
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = self.base_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'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, 5, 'The number of days should be equal to 5.')
leave.unlink()
def test_end_of_week(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'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': 17, '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': 17, 'day_period': 'afternoon'}),
(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'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06', #monday
'request_date_to': '2021-09-08', #wednesday
'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, 'The number of days should be equal to 5.')
leave.unlink()
def test_start_of_week(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(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': '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': 17, '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': 17, 'day_period': 'afternoon'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-08',
'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, 5, 'The number of days should be equal to 5.')
leave.unlink()
def test_last_day_half(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(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': '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': 17, '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': 17, 'day_period': 'afternoon'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-10', #friday
'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()
# Since the employee works on the afternoon, the date_to is not post-poned
self.assertEqual(leave.number_of_days, 0.5, 'The number of days should be equal to 0.5.')
leave.request_date_from_period = 'pm'
# This however should push the date_to
self.assertEqual(leave.number_of_days, 2.5, 'The number of days should be equal to 2.5.')
leave.unlink()
def test_full_time_am_day_half(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'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': 17, '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': 17, 'day_period': 'afternoon'}),
(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': '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': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-10', #friday
'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()
# Since the employee works doesnt work the afternoon, the date_to is post-poned
self.assertEqual(leave.number_of_days, 1, 'The number of days should be equal to 1.')
leave.unlink()
def test_am_day_half(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'attendance_ids': [
(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': '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': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-24', #friday
'request_date_to': '2021-09-24',
'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()
# Since the employee works doesnt work the afternoon, the date_to is post-poned
self.assertEqual(leave.number_of_days, 3, 'The number of days should be equal to 3.')
leave.unlink()
def test_calendar_with_holes(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'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': 17, 'day_period': 'afternoon'}),
(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': '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': 17, 'day_period': 'afternoon'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'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, 5, 'The number of days should be equal to 5.')
leave.unlink()
def test_calendar_end_week_hole(self):
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'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': 17, 'day_period': 'afternoon'}),
(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'}),
],
})
self.company.resource_calendar_id = self.base_calendar
self.employee.resource_calendar_id = employee_calendar
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-08',
'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, 'The number of days should be equal to 5.')
leave.unlink()
def test_2_weeks_calendar(self):
company_calendar = self.env['resource.calendar'].create({
'name': 'Company Calendar',
'two_weeks_calendar': True,
'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 Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '0', 'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '0', 'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '0', 'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '0', 'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '0', 'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '0', 'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '0', 'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '0', 'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '1', 'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '1', 'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '1', 'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '1', 'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'week_type': '1', 'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'week_type': '1', 'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
employee_calendar = self.env['resource.calendar'].create({
'name': 'Employee Calendar',
'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': 17, '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': 17, 'day_period': 'afternoon'}),
(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'}),
],
})
self.company.resource_calendar_id = company_calendar
self.employee.resource_calendar_id = employee_calendar
# Week type 0
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-08',
'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, 'The number of days should be equal to 5.')
leave.unlink()
# Week type 1
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-13',
'request_date_to': '2021-09-15',
'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, 'The number of days should be equal to 3.')
leave.unlink()
# Both ending with week type 1
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-06',
'request_date_to': '2021-09-15',
'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, 8, 'The number of days should be equal to 3.')
leave.unlink()
leave = self.env['hr.leave'].create({
'name': 'Test',
'holiday_status_id': self.time_off_type.id,
'employee_id': self.employee.id,
'request_date_from': '2021-09-13',
'request_date_to': '2021-09-22',
'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, 8, 'The number of days should be equal to 3.')
leave.unlink()

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.hr</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="70"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div name="work_organization_setting_container" position="after">
<field name="company_country_code" invisible="1"/>
<h2>French Time Off Localization</h2>
<div class="row mt16 o_settings_container" name="work_organization_setting_container">
<div class="col-12 col-lg-6 o_setting_box" id="default_company_work_organization_setting">
<div class="o_setting_right_pane">
<label for="resource_calendar_id"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." role="img" aria-label="Values set here are company-specific." groups="base.group_multi_company"/>
<div class="row">
<div class="text-muted col-lg-8">
Set the time off type used as the company Paid Time Off to compute part-timers leave duration
</div>
</div>
<div class="content-group">
<div class="mt16">
<field name="l10n_fr_reference_leave_type" required="1"
class="o_light_label"
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]"
context="{'default_company_id': company_id}"/>
</div>
</div>
</div>
</div>
</div>
</div>
</field>
</record>
<menuitem id="hr_holidays_menu_configuration"
name="Settings"
parent="hr_holidays.menu_hr_holidays_configuration"
sequence="10"
action="hr.hr_config_settings_action"
groups="base.group_system"/>
</odoo>