[FIX] hr_work_entry_holidays: Split existing leave when opening a contract

Purpose
=======

Currenlty an error is raise saying that a leave cannot be set accross
overlapping contracts.

But in this special case we could split the time off and allow this simple
action.

Taskid: 2836901
X-original-commit: 1012a7ea8033675b6ebab96922e124b04e5ed884
Part-of: odoo/odoo#94885
This commit is contained in:
Yannick Tivisse
2022-07-01 16:35:34 +02:00
parent 3f5e3b0513
commit 4cf496a190
4 changed files with 131 additions and 31 deletions
@@ -1,9 +1,10 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import pytz
from datetime import date
from datetime import date, datetime
from odoo import api, models
@@ -17,6 +18,7 @@ class HrContract(models.Model):
def _get_leaves(self):
return self.env['hr.leave'].search([
('state', '!=', 'refuse'),
('employee_id', 'in', self.mapped('employee_id.id')),
('date_from', '<=', max([end or date.max for end in self.mapped('date_end')])),
('date_to', '>=', min(self.mapped('date_start'))),
@@ -79,3 +81,62 @@ class HrContract(models.Model):
('date_to', '>=', start_dt),
('company_id', 'in', [False, self.company_id.id]),
]
def write(self, vals):
# Special case when setting a contract as running:
# If there is already a validated time off over another contract
# with a different schedule, split the time off, before the
# _check_contracts raises an issue.
if 'state' not in vals or vals['state'] != 'open':
return super().write(vals)
specific_contracts = self.env['hr.contract']
all_new_leave_origin = []
all_new_leave_vals = []
leaves_state = {}
for contract in self:
leaves = contract._get_leaves()
for leave in leaves:
overlapping_contracts = leave._get_overlapping_contracts(contract_states=[('state', '!=', 'cancel')])
if len(overlapping_contracts.resource_calendar_id) <= 1:
continue
if leave.id not in leaves_state:
leaves_state[leave.id] = leave.state
if leave.state != 'refuse':
leave.action_refuse()
super(HrContract, contract).write(vals)
specific_contracts += contract
for overlapping_contract in overlapping_contracts:
# Exclude other draft contracts that are not set to running on this
# transaction
if overlapping_contract.state == 'draft' and overlapping_contract not in self:
continue
new_date_from = max(leave.date_from, datetime.combine(overlapping_contract.date_start, datetime.min.time()))
new_date_to = min(leave.date_to, datetime.combine(overlapping_contract.date_end or date.max, datetime.max.time()))
new_leave_vals = leave.copy_data({
'date_from': new_date_from,
'date_to': new_date_to,
'state': leaves_state[leave.id],
})[0]
new_leave = self.env['hr.leave'].new(new_leave_vals)
new_leave._compute_date_from_to()
new_leave._compute_number_of_days()
# Could happen for part-time contract, that time off is not necessary
# anymore.
if new_leave.date_from < new_leave.date_to:
all_new_leave_origin.append(leave)
all_new_leave_vals.append(new_leave._convert_to_write(new_leave._cache))
if all_new_leave_vals:
new_leaves = self.env['hr.leave'].with_context(
tracking_disable=True,
mail_activity_automation_skip=True,
leave_fast_create=True,
leave_skip_state_check=True
).create(all_new_leave_vals)
new_leaves.filtered(lambda l: l.state in 'validate')._validate_leave_request()
for index, new_leave in enumerate(new_leaves):
subtype_note = self.env.ref('mail.mt_note')
new_leave.message_post_with_view(
'mail.message_origin_link',
values={'self': new_leave, 'origin': all_new_leave_origin[index]},
subtype_id=subtype_note.id)
return super(HrContract, self - specific_contracts).write(vals)
@@ -6,6 +6,7 @@ from datetime import datetime, date
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.osv.expression import AND
class HrLeaveType(models.Model):
@@ -30,22 +31,43 @@ class HrLeave(models.Model):
resource_leave_values = []
for leave in self.filtered(lambda l: l.employee_id):
contract = leave.employee_id.sudo()._get_contracts(leave.date_from, leave.date_to, states=['open'])
if contract and contract.resource_calendar_id != leave.employee_id.resource_calendar_id:
resource_leave_values += [{
'name': _("%s: Time Off", leave.employee_id.name),
'holiday_id': leave.id,
'resource_id': leave.employee_id.resource_id.id,
'work_entry_type_id': leave.holiday_status_id.work_entry_type_id.id,
'time_type': leave.holiday_status_id.time_type,
'date_from': max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time())),
'date_to': min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time())),
'calendar_id': contract.resource_calendar_id.id,
}]
contracts = leave.employee_id.sudo()._get_contracts(leave.date_from, leave.date_to, states=['open'])
for contract in contracts:
if contract and contract.resource_calendar_id != leave.employee_id.resource_calendar_id:
resource_leave_values += [{
'name': _("%s: Time Off", leave.employee_id.name),
'holiday_id': leave.id,
'resource_id': leave.employee_id.resource_id.id,
'work_entry_type_id': leave.holiday_status_id.work_entry_type_id.id,
'time_type': leave.holiday_status_id.time_type,
'date_from': max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time())),
'date_to': min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time())),
'calendar_id': contract.resource_calendar_id.id,
}]
return resource_leaves | self.env['resource.calendar.leaves'].create(resource_leave_values)
def _get_overlapping_contracts(self, contract_states=None):
self.ensure_one()
if contract_states is None:
contract_states = [
'|',
('state', 'not in', ['draft', 'cancel']),
'&',
('state', '=', 'draft'),
('kanban_state', '=', 'done')
]
domain = AND([contract_states, [
('employee_id', '=', self.employee_id.id),
('date_start', '<=', self.date_to),
'|',
('date_end', '>=', self.date_from),
'&',
('date_end', '=', False),
('state', '!=', 'close')
]])
return self.env['hr.contract'].sudo().search(domain)
@api.constrains('date_from', 'date_to')
def _check_contracts(self):
"""
@@ -55,23 +77,8 @@ class HrLeave(models.Model):
contracts are later modifed/created in the middle of the leave.
"""
for holiday in self.filtered('employee_id'):
domain = [
('employee_id', '=', holiday.employee_id.id),
('date_start', '<=', holiday.date_to),
'|',
('state', 'not in', ['draft', 'cancel']),
'&',
('state', '=', 'draft'),
('kanban_state', '=', 'done'),
'|',
('date_end', '>=', holiday.date_from),
'&',
('date_end', '=', False),
('state', '!=', 'close')
]
contracts = self.env['hr.contract'].sudo().search(domain)
contracts = holiday._get_overlapping_contracts()
if len(contracts.resource_calendar_id) > 1:
contracts = self.env['hr.contract'].sudo().search(domain)
raise ValidationError(
_('A leave cannot be set across multiple contracts with different working schedules.') + '\n%s\n%s' % (
', '.join(contracts.mapped('name')),
@@ -138,3 +138,35 @@ class TestWorkEntryHolidaysMultiContract(TestWorkEntryHolidaysBase):
'kanban_state': 'normal',
})
new_contract_cdi.state = 'open'
def test_leave_multi_contracts_split(self):
# Check that setting a contract as running correctly
# splits the existing time off for this employee that
# are ovelapping with another contract with another
# working schedule
leave = self.create_leave(datetime(2022, 6, 1, 5, 0, 0), datetime(2022, 6, 30, 20, 0, 0))
leave.action_approve()
self.assertEqual(leave.number_of_days, 22)
self.assertEqual(leave.state, 'validate')
self.contract_cdi.date_end = date(2022, 6, 15)
new_contract_cdi = self.env['hr.contract'].create({
'date_start': date(2022, 6, 16),
'name': 'New Contract for Jules',
'resource_calendar_id': self.calendar_40h.id,
'wage': 5000.0,
'employee_id': self.jules_emp.id,
'state': 'draft',
'kanban_state': 'normal',
})
new_contract_cdi.state = 'open'
leaves = self.env['hr.leave'].search([('employee_id', '=', self.jules_emp.id)])
self.assertEqual(len(leaves), 3)
self.assertEqual(leave.state, 'refuse')
first_leave = leaves.filtered(lambda l: l.date_from.day == 1 and l.date_to.day == 15)
self.assertEqual(first_leave.state, 'validate')
self.assertEqual(first_leave.number_of_days, 11)
second_leave = leaves.filtered(lambda l: l.date_from.day == 16 and l.date_to.day == 30)
self.assertEqual(second_leave.state, 'validate')
self.assertEqual(second_leave.number_of_days, 11)
@@ -47,7 +47,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
@users('__system__', 'admin')
@warmup
def test_performance_leave_create(self):
with self.assertQueryCount(__system__=26, admin=27): # 25/26 com
with self.assertQueryCount(__system__=27, admin=28): # 25/26 com
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
leave.action_refuse()