271 lines
13 KiB
Python
271 lines
13 KiB
Python
# -*- coding:utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
from datetime import datetime, date
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.osv.expression import AND
|
|
from odoo.tools import format_date
|
|
|
|
|
|
class HrLeaveType(models.Model):
|
|
_inherit = 'hr.leave.type'
|
|
|
|
work_entry_type_id = fields.Many2one('hr.work.entry.type', string='Work Entry Type')
|
|
|
|
|
|
class HrLeave(models.Model):
|
|
_inherit = 'hr.leave'
|
|
|
|
def _create_resource_leave(self):
|
|
"""
|
|
Add a resource leave in calendars of contracts running at the same period.
|
|
This is needed in order to compute the correct number of hours/days of the leave
|
|
according to the contract's calender.
|
|
"""
|
|
resource_leaves = super(HrLeave, self)._create_resource_leave()
|
|
for resource_leave in resource_leaves:
|
|
resource_leave.work_entry_type_id = resource_leave.holiday_id.holiday_status_id.work_entry_type_id.id
|
|
|
|
resource_leave_values = []
|
|
|
|
for leave in self.filtered(lambda l: l.employee_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'].sudo().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):
|
|
"""
|
|
A leave cannot be set across multiple contracts.
|
|
Note: a leave can be across multiple contracts despite this constraint.
|
|
It happens if a leave is correctly created (not across multiple contracts) but
|
|
contracts are later modifed/created in the middle of the leave.
|
|
"""
|
|
for holiday in self.filtered('employee_id'):
|
|
contracts = holiday._get_overlapping_contracts()
|
|
if len(contracts.resource_calendar_id) > 1:
|
|
state_labels = {e[0]: e[1] for e in contracts._fields['state']._description_selection(self.env)}
|
|
raise ValidationError(
|
|
_("""A leave cannot be set across multiple contracts with different working schedules.
|
|
|
|
Please create one time off for each contract.
|
|
|
|
Time off:
|
|
%s
|
|
|
|
Contracts:
|
|
%s""",
|
|
holiday.display_name,
|
|
'\n'.join(_(
|
|
"Contract %s from %s to %s, status: %s",
|
|
contract.name,
|
|
format_date(self.env, contract.date_start),
|
|
format_date(self.env, contract.date_start) if contract.date_end else _("undefined"),
|
|
state_labels[contract.state]
|
|
) for contract in contracts)))
|
|
|
|
def _cancel_work_entry_conflict(self):
|
|
"""
|
|
Creates a leave work entry for each hr.leave in self.
|
|
Check overlapping work entries with self.
|
|
Work entries completely included in a leave are archived.
|
|
e.g.:
|
|
|----- work entry ----|---- work entry ----|
|
|
|------------------- hr.leave ---------------|
|
|
||
|
|
vv
|
|
|----* work entry ****|
|
|
|************ work entry leave --------------|
|
|
"""
|
|
if not self:
|
|
return
|
|
|
|
# 1. Create a work entry for each leave
|
|
work_entries_vals_list = []
|
|
for leave in self:
|
|
contracts = leave.employee_id.sudo()._get_contracts(leave.date_from, leave.date_to, states=['open', 'close'])
|
|
for contract in contracts:
|
|
# Generate only if it has aleady been generated
|
|
if leave.date_to >= contract.date_generated_from and leave.date_from <= contract.date_generated_to:
|
|
work_entries_vals_list += contracts._get_work_entries_values(leave.date_from, leave.date_to)
|
|
|
|
new_leave_work_entries = self.env['hr.work.entry'].create(work_entries_vals_list)
|
|
|
|
if new_leave_work_entries:
|
|
# 2. Fetch overlapping work entries, grouped by employees
|
|
start = min(self.mapped('date_from'), default=False)
|
|
stop = max(self.mapped('date_to'), default=False)
|
|
work_entry_groups = self.env['hr.work.entry']._read_group([
|
|
('date_start', '<', stop),
|
|
('date_stop', '>', start),
|
|
('employee_id', 'in', self.employee_id.ids),
|
|
], ['work_entry_ids:array_agg(id)', 'employee_id'], ['employee_id', 'date_start', 'date_stop'], lazy=False)
|
|
work_entries_by_employee = defaultdict(lambda: self.env['hr.work.entry'])
|
|
for group in work_entry_groups:
|
|
employee_id = group.get('employee_id')[0]
|
|
work_entries_by_employee[employee_id] |= self.env['hr.work.entry'].browse(group.get('work_entry_ids'))
|
|
|
|
# 3. Archive work entries included in leaves
|
|
included = self.env['hr.work.entry']
|
|
overlappping = self.env['hr.work.entry']
|
|
for work_entries in work_entries_by_employee.values():
|
|
# Work entries for this employee
|
|
new_employee_work_entries = work_entries & new_leave_work_entries
|
|
previous_employee_work_entries = work_entries - new_leave_work_entries
|
|
|
|
# Build intervals from work entries
|
|
leave_intervals = new_employee_work_entries._to_intervals()
|
|
conflicts_intervals = previous_employee_work_entries._to_intervals()
|
|
|
|
# Compute intervals completely outside any leave
|
|
# Intervals are outside, but associated records are overlapping.
|
|
outside_intervals = conflicts_intervals - leave_intervals
|
|
|
|
overlappping |= self.env['hr.work.entry']._from_intervals(outside_intervals)
|
|
included |= previous_employee_work_entries - overlappping
|
|
overlappping.write({'leave_id': False})
|
|
included.write({'active': False})
|
|
|
|
def write(self, vals):
|
|
if not self:
|
|
return True
|
|
skip_check = not bool({'employee_id', 'state', 'date_from', 'date_to'} & vals.keys())
|
|
|
|
start = min(self.mapped('date_from') + [fields.Datetime.from_string(vals.get('date_from', False)) or datetime.max])
|
|
stop = max(self.mapped('date_to') + [fields.Datetime.from_string(vals.get('date_to', False)) or datetime.min])
|
|
employee_ids = self.employee_id.ids
|
|
if 'employee_id' in vals and vals['employee_id']:
|
|
employee_ids += [vals['employee_id']]
|
|
with self.env['hr.work.entry']._error_checking(start=start, stop=stop, skip=skip_check, employee_ids=employee_ids):
|
|
return super().write(vals)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
start_dates = [v.get('date_from') for v in vals_list if v.get('date_from')]
|
|
stop_dates = [v.get('date_to') for v in vals_list if v.get('date_to')]
|
|
if any(vals.get('holiday_type', 'employee') == 'employee' and not vals.get('multi_employee', False) and not vals.get('employee_id', False) for vals in vals_list):
|
|
raise ValidationError(_("There is no employee set on the time off. Please make sure you're logged in the correct company."))
|
|
employee_ids = {v['employee_id'] for v in vals_list if v.get('employee_id')}
|
|
with self.env['hr.work.entry']._error_checking(start=min(start_dates, default=False), stop=max(stop_dates, default=False), employee_ids=employee_ids):
|
|
return super().create(vals_list)
|
|
|
|
def action_confirm(self):
|
|
start = min(self.mapped('date_from'), default=False)
|
|
stop = max(self.mapped('date_to'), default=False)
|
|
with self.env['hr.work.entry']._error_checking(start=start, stop=stop, employee_ids=self.employee_id.ids):
|
|
return super().action_confirm()
|
|
|
|
def _get_leaves_on_public_holiday(self):
|
|
return super()._get_leaves_on_public_holiday().filtered(
|
|
lambda l: l.holiday_status_id.work_entry_type_id.code not in ['LEAVE110', 'LEAVE280'])
|
|
|
|
def _validate_leave_request(self):
|
|
super(HrLeave, self)._validate_leave_request()
|
|
self.sudo()._cancel_work_entry_conflict() # delete preexisting conflicting work_entries
|
|
return True
|
|
|
|
def action_refuse(self):
|
|
"""
|
|
Override to archive linked work entries and recreate attendance work entries
|
|
where the refused leave was.
|
|
"""
|
|
res = super(HrLeave, self).action_refuse()
|
|
self._regen_work_entries()
|
|
return res
|
|
|
|
def _action_user_cancel(self, reason):
|
|
res = super()._action_user_cancel(reason)
|
|
self.sudo()._regen_work_entries()
|
|
return res
|
|
|
|
def _regen_work_entries(self):
|
|
"""
|
|
Called when the leave is refused or cancelled to regenerate the work entries properly for that period.
|
|
"""
|
|
work_entries = self.env['hr.work.entry'].sudo().search([('leave_id', 'in', self.ids)])
|
|
|
|
work_entries.write({'active': False})
|
|
# Re-create attendance work entries
|
|
vals_list = []
|
|
for work_entry in work_entries:
|
|
vals_list += work_entry.contract_id._get_work_entries_values(work_entry.date_start, work_entry.date_stop)
|
|
self.env['hr.work.entry'].create(vals_list)
|
|
|
|
def _get_number_of_days(self, date_from, date_to, employee_id):
|
|
""" If an employee is currently working full time but asks for time off next month
|
|
where he has a new contract working only 3 days/week. This should be taken into
|
|
account when computing the number of days for the leave (2 weeks leave = 6 days).
|
|
Override this method to get number of days according to the contract's calendar
|
|
at the time of the leave.
|
|
"""
|
|
days = super(HrLeave, self)._get_number_of_days(date_from, date_to, employee_id)
|
|
if employee_id:
|
|
employee = self.env['hr.employee'].browse(employee_id)
|
|
# Use sudo otherwise base users can't compute number of days
|
|
contracts = employee.sudo()._get_contracts(date_from, date_to, states=['open', 'close'])
|
|
contracts |= employee.sudo()._get_incoming_contracts(date_from, date_to)
|
|
calendar = contracts[:1].resource_calendar_id if contracts else None # Note: if len(contracts)>1, the leave creation will crash because of unicity constaint
|
|
# We force the company in the domain as we are more than likely in a compute_sudo
|
|
domain = [('company_id', 'in', self.env.company.ids + self.env.context.get('allowed_company_ids', []))]
|
|
result = employee._get_work_days_data_batch(date_from, date_to, calendar=calendar, domain=domain)[employee.id]
|
|
if self.request_unit_half and result['hours'] > 0:
|
|
result['days'] = 0.5
|
|
return result
|
|
|
|
return days
|
|
|
|
def _get_calendar(self):
|
|
self.ensure_one()
|
|
if self.date_from and self.date_to:
|
|
contracts = self.employee_id.sudo()._get_contracts(self.date_from, self.date_to, states=['open', 'close'])
|
|
contracts |= self.employee_id.sudo()._get_incoming_contracts(self.date_from, self.date_to)
|
|
contract_calendar = contracts[:1].resource_calendar_id if contracts else None
|
|
return contract_calendar or self.employee_id.resource_calendar_id or self.env.company.resource_calendar_id
|
|
return super()._get_calendar()
|
|
|
|
def _compute_can_cancel(self):
|
|
super()._compute_can_cancel()
|
|
|
|
cancellable_leaves = self.filtered('can_cancel')
|
|
work_entries = self.env['hr.work.entry'].sudo().search([('state', '=', 'validated'), ('leave_id', 'in', cancellable_leaves.ids)])
|
|
leave_ids = work_entries.mapped('leave_id').ids
|
|
|
|
for leave in cancellable_leaves:
|
|
leave.can_cancel = leave.id not in leave_ids
|