diff --git a/addons/hr_holidays/data/hr_holidays_demo.xml b/addons/hr_holidays/data/hr_holidays_demo.xml index fb9b43eead5..f3c7407534a 100644 --- a/addons/hr_holidays/data/hr_holidays_demo.xml +++ b/addons/hr_holidays/data/hr_holidays_demo.xml @@ -2,6 +2,22 @@ + + Overtime Compensation + + blue + + + hour + + + + Parental Leaves + + brown + both + + Legal Leaves for Peter Parker @@ -18,6 +34,14 @@ confirm + + Overtime Compensation + + 160 + 20 + + + Summer Vacation @@ -25,8 +49,8 @@ - - + + Trip with Family diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 7fca30f50f8..f0359744ebb 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -5,14 +5,16 @@ import logging import math -from datetime import timedelta + +from datetime import timedelta, datetime, time +from pytz import timezone, UTC from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError from odoo.tools import float_compare from odoo.tools.translate import _ -from odoo.addons.resource.models.resource import HOURS_PER_DAY +from odoo.addons.resource.models.resource import float_to_time _logger = logging.getLogger(__name__) @@ -85,6 +87,9 @@ class HolidaysRequest(models.Model): states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Number of days of the leave request according to your working schedule.') number_of_days = fields.Float('Number of Days', compute='_compute_number_of_days', store=True, track_visibility='onchange') + number_of_hours = fields.Float( + 'Hours Allocation', copy=False, readonly=True, compute='_compute_number_of_hours', + help='Number of hours of the leave request according to your working schedule.') meeting_id = fields.Many2one('calendar.event', string='Meeting') parent_id = fields.Many2one('hr.leave', string='Parent') @@ -107,19 +112,181 @@ class HolidaysRequest(models.Model): can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve') + # Those fields are mostly for the interface only + + # Interface fields used when not using hour-based computation + request_date_from = fields.Date('Request Start Date') + request_date_to = fields.Date('Request End Date') + + leave_type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True) + + # These fields are only used only when the leave is taken in half days + request_date_from_period = fields.Selection([('am', 'Morning'), + ('pm', 'Afternoon')], string="Date Period Start", default='am') + request_date_to_period = fields.Selection([('am', 'Morning'), + ('pm', 'Afternoon')], string="Date Period End", default='pm') + + request_unit_all = fields.Selection([('half', 'Half-day'), + ('day', '1 Day'), + ('period', 'Period')]) + # Duplicate field because we cannot hide some entries of a selection field + request_unit_day = fields.Selection([('day', '1 Day'), + ('period', 'Period')], default='day') + _sql_constraints = [ ('type_value', "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or (holiday_type='category' AND category_id IS NOT NULL) or (holiday_type='department' AND department_id IS NOT NULL) )", "The employee, department or employee category of this request is missing. Please make sure that your user login is linked to an employee."), ('date_check2', "CHECK ((date_from <= date_to))", "The start date must be anterior to the end date."), - ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."), + ('date_check', "CHECK ( number_of_days_temp >= 0 )", "If you want to change the number of days you should use the 'period' mode"), ] + @api.onchange('request_unit_all', 'request_date_from_period', 'request_date_to_period', + 'holiday_status_id', 'request_date_from', 'request_date_to', 'employee_id') + def _onchange_request_parameters(self): + date_from = False + date_to = False + + if self.request_date_from: + if self.date_from: + date_from = fields.Datetime.to_string(datetime.combine(fields.Date.from_string(self.request_date_from), fields.Datetime.from_string(self.date_from).time())) + else: + date_from = self.request_date_from + + if self.request_date_to: + if self.date_to: + date_to = fields.Datetime.to_string(datetime.combine(fields.Date.from_string(self.request_date_to), fields.Datetime.from_string(self.date_to).time())) + else: + date_to = self.request_date_to + + if not self.request_date_from or not self.request_date_to: + if date_from: + self.date_from = date_from + if date_to: + self.date_to = date_to + self.number_of_days_temp = 0 + return + + domain = [('calendar_id', '=', self.employee_id.resource_calendar_id.id)] + attendances = self.env['resource.calendar.attendance'].search(domain, order='dayofweek, day_period DESC') + + first_day = fields.Date.from_string(date_from) + last_day = fields.Date.from_string(date_to) + + if self.request_unit_all in ['day', 'half']: + last_day = first_day + + # find first attendance coming after first_day + attendance_from = next((att for att in attendances if int(att.dayofweek) >= first_day.weekday()), attendances[0]) + # find last attendance coming before last_day + attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= last_day.weekday()), attendances[-1]) + + if self.request_unit_all == 'day' or (self.request_unit_all == 'period' and self.leave_type_request_unit == 'day'): + hour_from = float_to_time(attendance_from.hour_from) + hour_to = float_to_time(attendance_to.hour_to) + + elif self.request_unit_all == 'half': + hour_from = float_to_time(attendance_from.hour_from if self.request_date_from_period == 'am' else attendance_to.hour_from) + hour_to = float_to_time(attendance_from.hour_to if self.request_date_from_period == 'am' else attendance_to.hour_to) + + elif self.request_unit_all == 'period' and self.leave_type_request_unit == 'half': + hour_from = float_to_time(attendance_from.hour_from if self.request_date_from_period == 'am' else attendance_from.hour_to) + hour_to = float_to_time(attendance_to.hour_from if self.request_date_to_period == 'am' else attendance_to.hour_to) + + if self.leave_type_request_unit == 'hour' and self.request_unit_all == 'period': + date_from = fields.Datetime.from_string(date_from) + date_to = fields.Datetime.from_string(date_to) + else: + date_from = timezone(self.env.user.tz).localize(datetime.combine(first_day, hour_from)).astimezone(UTC) + date_to = timezone(self.env.user.tz).localize(datetime.combine(last_day, hour_to)).astimezone(UTC) + + self.date_from = date_from + self.date_to = date_to + + if not (self.leave_type_request_unit == 'hour' and self.request_unit_all == 'period'): + date_from = date_from + date_to = date_to + + self.number_of_days_temp = self._get_number_of_days(date_from, date_to, self.employee_id.id) + + @api.onchange('request_unit_day') + def _onchange_request_unit_day(self): + self.request_unit_all = self.request_unit_day + + @api.onchange('holiday_type') + def _onchange_type(self): + if self.holiday_type == 'employee' and not self.employee_id: + if self.env.user.employee_ids: + self.employee_id = self.env.user.employee_ids[0] + elif self.holiday_type == 'department': + if self.env.user.employee_ids: + self.department_id = self.department_id or self.env.user.employee_ids[0].department_id + self.employee_id = None + elif self.holiday_type == 'category': + self.employee_id = None + self.department_id = None + + @api.onchange('date_from') + def _onchange_date_from(self): + """ If there are no date set for date_to, automatically set one 8 hours later than + the date_from. Also update the number_of_days. + """ + date_from = fields.Datetime.from_string(self.date_from) + date_to = fields.Datetime.from_string(self.date_to) + + # No date_to set so far: automatically compute one 8 hours later + if date_from and not date_to: + date_to = date_from + timedelta(hours=self.employee_id.resource_calendar_id.hours_per_day) + self.date_to = date_to + + self.request_date_from = date_from + self.request_date_to = date_to + + if (date_from and date_to) and (date_from.day < date_to.day): + self.request_unit_all = 'period' + self.request_unit_day = 'period' + + # Compute and update the number of days + if (date_to and date_from) and (date_from <= date_to): + self.number_of_days_temp = self._get_number_of_days(date_from, date_to, self.employee_id.id) + else: + self.number_of_days_temp = 0 + + @api.onchange('date_to') + def _onchange_date_to(self): + """ Update the number_of_days. """ + date_from = fields.Datetime.from_string(self.date_from) + date_to = fields.Datetime.from_string(self.date_to) + + self.request_date_from = date_from + self.request_date_to = date_to + + if (date_from and date_to) and (date_from.day < date_to.day): + self.request_unit_all = 'period' + self.request_unit_day = 'period' + + # Compute and update the number of days + if (date_to and date_from) and (date_from <= date_to): + self.number_of_days_temp = self._get_number_of_days(date_from, date_to, self.employee_id.id) + else: + self.number_of_days_temp = 0 + + @api.onchange('employee_id') + def _onchange_employee_id(self): + self.manager_id = self.employee_id and self.employee_id.parent_id + self.department_id = self.employee_id.department_id + @api.multi @api.depends('number_of_days_temp') def _compute_number_of_days(self): for holiday in self: holiday.number_of_days = -holiday.number_of_days_temp + @api.multi + @api.depends('number_of_days_temp') + def _compute_number_of_hours(self): + for holiday in self: + holiday.number_of_hours = holiday.number_of_days_temp * self.employee_id.resource_calendar_id.hours_per_day + @api.multi def _compute_can_reset(self): """ User can reset a leave request if it is its own leave request @@ -141,56 +308,6 @@ class HolidaysRequest(models.Model): manager = self.user_has_groups('hr_holidays.group_hr_holidays_manager') holiday.can_approve = (holiday.employee_id.user_id.id != self.env.uid) or manager - @api.onchange('holiday_type') - def _onchange_type(self): - if self.holiday_type == 'employee' and not self.employee_id: - if self.env.user.employee_ids: - self.employee_id = self.env.user.employee_ids[0] - elif self.holiday_type == 'department': - if self.env.user.employee_ids: - self.department_id = self.department_id or self.env.user.employee_ids[0].department_id - self.employee_id = None - elif self.holiday_type == 'category': - self.employee_id = None - self.department_id = None - - @api.onchange('employee_id') - def _onchange_employee_id(self): - self.manager_id = self.employee_id and self.employee_id.parent_id - if self.holiday_type == 'employee': - self.department_id = self.employee_id.department_id - - @api.onchange('date_from') - def _onchange_date_from(self): - """ If there are no date set for date_to, automatically set one 8 hours later than - the date_from. Also update the number_of_days. - """ - date_from = self.date_from - date_to = self.date_to - - # No date_to set so far: automatically compute one 8 hours later - if date_from and not date_to: - date_to_with_delta = fields.Datetime.from_string(date_from) + timedelta(hours=HOURS_PER_DAY) - self.date_to = str(date_to_with_delta) - - # Compute and update the number of days - if (date_to and date_from) and (date_from <= date_to): - self.number_of_days_temp = self._get_number_of_days(date_from, date_to, self.employee_id.id) - else: - self.number_of_days_temp = 0 - - @api.onchange('date_to') - def _onchange_date_to(self): - """ Update the number_of_days. """ - date_from = self.date_from - date_to = self.date_to - - # Compute and update the number of days - if (date_to and date_from) and (date_from <= date_to): - self.number_of_days_temp = self._get_number_of_days(date_from, date_to, self.employee_id.id) - else: - self.number_of_days_temp = 0 - @api.constrains('date_from', 'date_to') def _check_date(self): for holiday in self: @@ -218,14 +335,11 @@ class HolidaysRequest(models.Model): def _get_number_of_days(self, date_from, date_to, employee_id): """ Returns a float equals to the timedelta between two dates given as string.""" - from_dt = fields.Datetime.from_string(date_from) - to_dt = fields.Datetime.from_string(date_to) - if employee_id: employee = self.env['hr.employee'].browse(employee_id) - return employee.get_work_days_data(from_dt, to_dt)['days'] + return employee.get_work_days_data(date_from, date_to)['days'] - time_delta = to_dt - from_dt + time_delta = date_to - date_from return math.ceil(time_delta.days + float(time_delta.seconds) / 86400) #################################################### @@ -302,11 +416,14 @@ class HolidaysRequest(models.Model): def _create_resource_leave(self): """ This method will create entry in resource calendar leave object at the time of holidays validated """ for leave in self: + date_from = fields.Datetime.from_string(leave.date_from) + date_to = fields.Datetime.from_string(leave.date_to) + self.env['resource.calendar.leaves'].create({ 'name': leave.name, - 'date_from': leave.date_from, + 'date_from': fields.Datetime.to_string(date_from), 'holiday_id': leave.id, - 'date_to': leave.date_to, + 'date_to': fields.Datetime.to_string(date_to), 'resource_id': leave.employee_id.resource_id.id, 'calendar_id': leave.employee_id.resource_calendar_id.id, 'time_type': leave.holiday_status_id.time_type, @@ -334,7 +451,7 @@ class HolidaysRequest(models.Model): 'name': self.display_name, 'categ_ids': [(6, 0, [ self.holiday_status_id.categ_id.id])] if self.holiday_status_id.categ_id else [], - 'duration': self.number_of_days_temp * HOURS_PER_DAY, + 'duration': self.number_of_days_temp * self.employee_id.resource_calendar_id.hours_per_day, 'description': self.notes, 'user_id': self.user_id.id, 'start': self.date_from, diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 7131b972be3..e5b63e4215d 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -65,6 +65,7 @@ class HolidaysAllocation(models.Model): states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Number of days of the leave request according to your working schedule.') number_of_days = fields.Float('Number of Days', compute='_compute_number_of_days', store=True, track_visibility='onchange') + number_of_hours = fields.Float('Number of Hours', help="Number of hours of the leave allocation according to your working schedule.") parent_id = fields.Many2one('hr.leave.allocation', string='Parent') linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests') department_id = fields.Many2one('hr.department', string='Department', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) @@ -84,6 +85,7 @@ class HolidaysAllocation(models.Model): validation_type = fields.Selection('Validation Type', related='holiday_status_id.validation_type') can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve') + type_request_unit = fields.Selection(related='holiday_status_id.request_unit') _sql_constraints = [ ('type_value', "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or (holiday_type='category' AND category_id IS NOT NULL) or (holiday_type='department' AND department_id IS NOT NULL) )", @@ -92,10 +94,13 @@ class HolidaysAllocation(models.Model): ] @api.multi - @api.depends('number_of_days_temp') + @api.depends('number_of_days_temp', 'type_request_unit', 'number_of_hours') def _compute_number_of_days(self): for holiday in self: - holiday.number_of_days = holiday.number_of_days_temp + if holiday.type_request_unit == 'hour': + holiday.number_of_days = holiday.number_of_hours / holiday.employee_id.resource_calendar_id.hours_per_day + else: + holiday.number_of_days = holiday.number_of_days_temp @api.multi def _compute_can_reset(self): @@ -136,6 +141,14 @@ class HolidaysAllocation(models.Model): if self.holiday_type == 'employee': self.department_id = self.employee_id.department_id + @api.onchange('number_of_days_temp') + def _onchange_number_of_days_temp(self): + self.number_of_hours = self.number_of_days_temp * self.employee_id.resource_calendar_id.hours_per_day + + @api.onchange('number_of_hours') + def _onchange_number_of_hours(self): + self.number_of_days_temp = self.number_of_hours / self.employee_id.resource_calendar_id.hours_per_day + #################################################### # ORM Overrides methods #################################################### @@ -144,7 +157,10 @@ class HolidaysAllocation(models.Model): def name_get(self): res = [] for leave in self: - res.append((leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp, leave.employee_id.name))) + if leave.type_request_unit == 'hour': + res.append((leave.id, _("Allocation of %s : %.2f hour(s) To %s") % (leave.holiday_status_id.name, leave.number_of_hours, leave.employee_id.name))) + else: + res.append((leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp, leave.employee_id.name))) return res @api.multi diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index 8a7a8a5f534..c31e0355d9f 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -78,6 +78,9 @@ class HolidaysType(models.Model): time_type = fields.Selection([('leave', 'Leave'), ('other', 'Other')], default='leave', string="Kind of Leave", help="Whether this should be computed as a holiday or as work time (eg: formation)") + request_unit = fields.Selection([('day', 'Day'), + ('half', 'Half-day'), + ('hour', 'Hours')], default='day', string='Take Leaves in', required=True) @api.multi @api.constrains('validity_start', 'validity_stop') @@ -100,11 +103,13 @@ class HolidaysType(models.Model): for holiday_type in self: if holiday_type.validity_start and holiday_type.validity_stop: holiday_type.valid = ((dt < holiday_type.validity_stop) and (dt > holiday_type.validity_start)) + elif holiday_type.validity_start and (dt > holiday_type.validity_start): + holiday_type.valid = False else: - holiday_type.valid = not holiday_type.validity_stop + holiday_type.valid = True def _search_valid(self, operator, value): - dt = self._context.get('default_date_from', fields.Date.today()) + dt = self._context.get('default_date_from', fields.Date.today()) or fields.Date.today() signs = ['>=', '<='] if operator == '=' else ['<=', '>='] return ['|', ('validity_stop', operator, False), '&', diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml index efc3b36879e..c39e12c34d7 100644 --- a/addons/hr_holidays/views/hr_leave_allocation_views.xml +++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml @@ -69,12 +69,16 @@ + -