[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:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user