diff --git a/addons/hr_work_entry_holidays/models/hr_contract.py b/addons/hr_work_entry_holidays/models/hr_contract.py index 2c040cc8f23..22a01619ecc 100644 --- a/addons/hr_work_entry_holidays/models/hr_contract.py +++ b/addons/hr_work_entry_holidays/models/hr_contract.py @@ -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) diff --git a/addons/hr_work_entry_holidays/models/hr_leave.py b/addons/hr_work_entry_holidays/models/hr_leave.py index 8614901de4a..6ae03fe6ce8 100644 --- a/addons/hr_work_entry_holidays/models/hr_leave.py +++ b/addons/hr_work_entry_holidays/models/hr_leave.py @@ -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')), diff --git a/addons/hr_work_entry_holidays/tests/test_multi_contract.py b/addons/hr_work_entry_holidays/tests/test_multi_contract.py index 8ffd8caa0d2..5061e499e7e 100644 --- a/addons/hr_work_entry_holidays/tests/test_multi_contract.py +++ b/addons/hr_work_entry_holidays/tests/test_multi_contract.py @@ -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) diff --git a/addons/hr_work_entry_holidays/tests/test_performance.py b/addons/hr_work_entry_holidays/tests/test_performance.py index 8d981873051..fa1a7df80f8 100644 --- a/addons/hr_work_entry_holidays/tests/test_performance.py +++ b/addons/hr_work_entry_holidays/tests/test_performance.py @@ -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()