From b6642f1b702fc32f765dfd24e855a652572a83e1 Mon Sep 17 00:00:00 2001 From: RomainLibert Date: Thu, 8 Mar 2018 15:20:15 +0100 Subject: [PATCH] [IMP] hr_holidays: add support for taking leaves differently We would like to be able to take leaves using different units, currently we only support taking leaves by hours because we use datetimes. We need to support three units: * days * half days * hours These units will be defined on the leave type. Task #40995 Closes #21760 --- addons/hr_holidays/data/hr_holidays_demo.xml | 28 +- addons/hr_holidays/models/hr_leave.py | 239 +++++++++++++----- .../hr_holidays/models/hr_leave_allocation.py | 22 +- addons/hr_holidays/models/hr_leave_type.py | 9 +- .../views/hr_leave_allocation_views.xml | 10 +- .../hr_holidays/views/hr_leave_type_views.xml | 4 +- addons/hr_holidays/views/hr_leave_views.xml | 33 ++- addons/resource/data/resource_data.xml | 40 +-- addons/resource/models/resource.py | 21 +- addons/resource/models/resource_mixin.py | 4 +- addons/resource/tests/test_resource.py | 6 +- addons/resource/views/resource_views.xml | 2 + 12 files changed, 305 insertions(+), 113 deletions(-) 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 @@ + -