diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index a51ab643a18..20018adb968 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -6,11 +6,11 @@ import logging import math -from datetime import timedelta, datetime, time +from datetime import datetime from pytz import timezone, UTC from odoo import api, fields, models -from odoo.addons.resource.models.resource import float_to_time +from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools import float_compare from odoo.tools.translate import _ @@ -62,6 +62,7 @@ class HolidaysRequest(models.Model): @api.model def default_get(self, fields_list): defaults = super(HolidaysRequest, self).default_get(fields_list) + defaults = self._default_get_request_parameters(defaults) LeaveType = self.env['hr.leave.type'].with_context(employee_id=defaults.get('employee_id'), default_date_from=defaults.get('date_from', fields.Datetime.now())) lt = LeaveType.search([('valid', '=', True)]) @@ -72,6 +73,26 @@ class HolidaysRequest(models.Model): def _default_employee(self): return self.env.context.get('default_employee_id') or self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + def _default_get_request_parameters(self, values): + new_values = dict(values) + global_from, global_to = False, False + # TDE FIXME: consider a mapping on several days that is not the standard + # calendar widget 7-19 in user's TZ is some custom input + if values.get('date_from'): + user_tz = self.env.user.tz or 'UTC' + localized_dt = timezone('UTC').localize(values['date_from']).astimezone(timezone(user_tz)) + global_from = localized_dt.time().hour == 7 and localized_dt.time().minute == 0 + new_values['request_date_from'] = values['date_from'].date() + if values.get('date_to'): + user_tz = self.env.user.tz or 'UTC' + localized_dt = timezone('UTC').localize(values['date_to']).astimezone(timezone(user_tz)) + global_to = localized_dt.time().hour == 19 and localized_dt.time().minute == 0 + new_values['request_date_to'] = values['date_to'].date() + if global_from and global_to: + new_values['request_unit_custom'] = True + return new_values + + # description name = fields.Char('Description') state = fields.Selection([ ('draft', 'To Submit'), @@ -107,9 +128,11 @@ class HolidaysRequest(models.Model): # duration date_from = fields.Datetime( 'Start Date', readonly=True, index=True, copy=False, required=True, + default=fields.Datetime.now, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange') date_to = fields.Datetime( 'End Date', readonly=True, copy=False, required=True, + default=fields.Datetime.now, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange') number_of_days = fields.Float( 'Duration (Days)', copy=False, readonly=True, track_visibility='onchange', @@ -143,20 +166,72 @@ class HolidaysRequest(models.Model): help='This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)') can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve') - # UX FIELDS - # used when not using hour-based computation + + # UX fields + leave_type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True) + # 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') + # Interface fields used when using hour-based computation + request_hour_from = fields.Selection([ + (0, '12:00 PM'), (0.5, '0:30 AM'), + (1, '1:00 AM'), (1.5, '1:30 AM'), + (2, '2:00 AM'), (2.5, '2:30 AM'), + (3, '3:00 AM'), (3.5, '3:30 AM'), + (4, '4:00 AM'), (4.5, '4:30 AM'), + (5, '5:00 AM'), (5.5, '5:30 AM'), + (6, '6:00 AM'), (6.5, '6:30 AM'), + (7, '7:00 AM'), (7.5, '7:30 AM'), + (8, '8:00 AM'), (8.5, '8:30 AM'), + (9, '9:00 AM'), (9.5, '9:30 AM'), + (10, '10:00 AM'), (10.5, '10:30 AM'), + (11, '11:00 AM'), (11.5, '11:30 AM'), + (12, '12:00 AM'), (12.5, '0:30 PM'), + (13, '1:00 PM'), (13.5, '1:30 PM'), + (14, '2:00 PM'), (14.5, '2:30 PM'), + (15, '3:00 PM'), (15.5, '3:30 PM'), + (16, '4:00 PM'), (16.5, '4:30 PM'), + (17, '5:00 PM'), (17.5, '5:30 PM'), + (18, '6:00 PM'), (18.5, '6:30 PM'), + (19, '7:00 PM'), (19.5, '7:30 PM'), + (20, '8:00 PM'), (20.5, '8:30 PM'), + (21, '9:00 PM'), (21.5, '9:30 PM'), + (22, '10:00 PM'), (22.5, '10:30 PM'), + (23, '11:00 PM'), (23.5, '11:30 PM')], string='Hour from') + request_hour_to = fields.Selection([ + (0, '12:00 PM'), (0.5, '0:30 AM'), + (1, '1:00 AM'), (1.5, '1:30 AM'), + (2, '2:00 AM'), (2.5, '2:30 AM'), + (3, '3:00 AM'), (3.5, '3:30 AM'), + (4, '4:00 AM'), (4.5, '4:30 AM'), + (5, '5:00 AM'), (5.5, '5:30 AM'), + (6, '6:00 AM'), (6.5, '6:30 AM'), + (7, '7:00 AM'), (7.5, '7:30 AM'), + (8, '8:00 AM'), (8.5, '8:30 AM'), + (9, '9:00 AM'), (9.5, '9:30 AM'), + (10, '10:00 AM'), (10.5, '10:30 AM'), + (11, '11:00 AM'), (11.5, '11:30 AM'), + (12, '12:00 AM'), (12.5, '0:30 PM'), + (13, '1:00 PM'), (13.5, '1:30 PM'), + (14, '2:00 PM'), (14.5, '2:30 PM'), + (15, '3:00 PM'), (15.5, '3:30 PM'), + (16, '4:00 PM'), (16.5, '4:30 PM'), + (17, '5:00 PM'), (17.5, '5:30 PM'), + (18, '6:00 PM'), (18.5, '6:30 PM'), + (19, '7:00 PM'), (19.5, '7:30 PM'), + (20, '8:00 PM'), (20.5, '8:30 PM'), + (21, '9:00 PM'), (21.5, '9:30 PM'), + (22, '10:00 PM'), (22.5, '10:30 PM'), + (23, '11:00 PM'), (23.5, '11:30 PM')], string='Hour to') # 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 type - request_unit = fields.Selection([('day', '1 Day'), - ('period', 'Period')], default='day') + request_unit_half = fields.Boolean('Half Day') + request_unit_hours = fields.Boolean('Custom Hours') + request_unit_custom = fields.Boolean('Days-long custom hours') + _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."), @@ -164,69 +239,70 @@ class HolidaysRequest(models.Model): ('duration_check', "CHECK ( number_of_days >= 0 )", "If you want to change the number of days you should use the 'period' mode"), ] - @api.onchange('request_unit', 'request_date_from_period', 'request_date_to_period', - 'holiday_status_id', 'request_date_from', 'request_date_to', 'employee_id') + @api.onchange('request_date_from_period', 'request_hour_from', 'request_hour_to', + 'request_date_from', 'request_date_to', + 'employee_id') def _onchange_request_parameters(self): - date_from = False - date_to = False - - if not self.employee_id: + if not self.request_date_from: + self.date_from = False return - 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 = datetime.combine(self.request_date_from, time.min) + if self.request_unit_half or self.request_unit_hours: + self.request_date_to = 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 = datetime.combine(self.request_date_to, time.max) - - 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 = 0 + if not self.request_date_to: + self.date_to = False return - domain = [('calendar_id', '=', self.employee_id.resource_calendar_id.id)] + domain = [('calendar_id', '=', self.employee_id.resource_calendar_id.id or self.env.user.company_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 in ['day']: - 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]) + attendance_from = next((att for att in attendances if int(att.dayofweek) >= self.request_date_from.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]) + attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= self.request_date_to.weekday()), attendances[-1]) - if self.request_unit == 'day' or (self.request_unit == 'period' and self.leave_type_request_unit == 'day'): + if self.request_unit_half: + if self.request_date_from_period == 'am': + hour_from = float_to_time(attendance_from.hour_from) + hour_to = float_to_time(attendance_from.hour_to) + else: + hour_from = float_to_time(attendance_to.hour_from) + hour_to = float_to_time(attendance_to.hour_to) + elif self.request_unit_hours: + hour_from = float_to_time(self.request_hour_from) + hour_to = float_to_time(self.request_hour_to) + elif self.request_unit_custom: + hour_from = self.date_from.time() + hour_to = self.date_to.time() + else: hour_from = float_to_time(attendance_from.hour_from) hour_to = float_to_time(attendance_to.hour_to) - if self.request_unit == 'period' and self.leave_type_request_unit == 'hour': - date_from = fields.Datetime.from_string(date_from) - date_to = fields.Datetime.from_string(date_to) - else: - tz = self.env.user.tz or 'UTC' - date_from = timezone(tz).localize(datetime.combine(first_day, hour_from)).astimezone(UTC).replace(tzinfo=None) - date_to = timezone(tz).localize(datetime.combine(last_day, hour_to)).astimezone(UTC).replace(tzinfo=None) + tz = self.env.user.tz if self.env.user.tz and not self.request_unit_custom else 'UTC' # custom -> already in UTC + self.date_from = timezone(tz).localize(datetime.combine(self.request_date_from, hour_from)).astimezone(UTC).replace(tzinfo=None) + self.date_to = timezone(tz).localize(datetime.combine(self.request_date_to, hour_to)).astimezone(UTC).replace(tzinfo=None) - self.date_from = date_from - self.date_to = date_to + @api.onchange('request_unit_half') + def _onchange_request_unit_half(self): + if self.request_unit_half: + self.request_unit_hours = False + self.request_unit_custom = False + self._onchange_request_parameters() - if not (self.request_unit == 'period' and self.leave_type_request_unit == 'hour'): - date_from = date_from - date_to = date_to + @api.onchange('request_unit_hours') + def _onchange_request_unit_hours(self): + if self.request_unit_hours: + self.request_unit_half = False + self.request_unit_custom = False + self._onchange_request_parameters() - self.number_of_days = self._get_number_of_days(date_from, date_to, self.employee_id.id) + @api.onchange('request_unit_custom') + def _onchange_request_unit_custom(self): + if self.request_unit_custom: + self.request_unit_half = False + self.request_unit_hours = False + self._onchange_request_parameters() @api.onchange('holiday_type') def _onchange_type(self): @@ -241,54 +317,18 @@ class HolidaysRequest(models.Model): 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.date() if date_from else False - self.request_date_to = date_to.date() if date_to else False - - if (date_from and date_to) and (date_from.day < date_to.day): - self.request_unit = 'period' - - # Compute and update the number of days - if (date_to and date_from) and (date_from <= date_to): - self.number_of_days = self._get_number_of_days(date_from, date_to, self.employee_id.id) - else: - self.number_of_days = 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.date() if date_from else False - self.request_date_to = date_to.date() if date_to else False - - if (date_from and date_to) and (date_from.day < date_to.day): - self.request_unit = 'period' - - # Compute and update the number of days - if (date_to and date_from) and (date_from <= date_to): - self.number_of_days = self._get_number_of_days(date_from, date_to, self.employee_id.id) - else: - self.number_of_days = 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.onchange('date_from', 'date_to') + def _onchange_leave_dates(self): + if self.date_from and self.date_to: + self.number_of_days = self._get_number_of_days(self.date_from, self.date_to, self.employee_id.id) + else: + self.number_of_days = 0 + @api.multi @api.depends('number_of_days') def _compute_number_of_days_display(self): @@ -299,7 +339,8 @@ class HolidaysRequest(models.Model): @api.depends('number_of_days') def _compute_number_of_hours_display(self): for holiday in self: - holiday.number_of_hours_display = holiday.number_of_days * self.employee_id.resource_calendar_id.hours_per_day + calendar = self.employee_id.resource_calendar_id or self.env.user.company_id.resource_calendar_id + holiday.number_of_hours_display = holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY) @api.multi @api.depends('state', 'employee_id', 'department_id') @@ -485,11 +526,12 @@ class HolidaysRequest(models.Model): @api.multi def _prepare_holidays_meeting_values(self): self.ensure_one() + calendar = self.employee_id.resource_calendar_id or self.env.user.company_id.resource_calendar_id meeting_values = { '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 * self.employee_id.resource_calendar_id.hours_per_day, + 'duration': self.number_of_days * (calendar.hours_per_day or 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 16c35241dc2..6a011c5b03e 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -27,8 +27,11 @@ class HolidaysAllocation(models.Model): return self.env.context.get('default_employee_id') or self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) def _default_holiday_status_id(self): - lt = self.env['hr.leave.type'].with_context(employee_id=self._default_employee().id).search([('valid', '=', True)], limit=1) - return lt[:1] + if self.user_has_groups('hr_holidays.group_hr_holidays_user'): + domain = [('valid', '=', True)] + else: + domain = [('valid', '=', True), ('allocation_type', 'in', ('no', 'fixed_allocation'))] + return self.env['hr.leave.type'].search(domain, limit=1) name = fields.Char('Description') state = fields.Selection([ diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index a5101ace28c..828df4a5586 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -150,13 +150,19 @@ class HolidaysType(models.Model): return result + def _get_contextual_employee_id(self): + if 'employee_id' in self._context: + employee_id = self._context['employee_id'] + elif 'default_employee_id' in self._context: + employee_id = self._context['default_employee_id'] + else: + employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.user.id)], limit=1).id + return employee_id + @api.multi def _compute_leaves(self): data_days = {} - if 'employee_id' in self._context: - employee_id = self._context['employee_id'] - else: - employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.user.id)], limit=1).id + employee_id = self._get_contextual_employee_id() if employee_id: data_days = self.get_days(employee_id) @@ -221,8 +227,9 @@ class HolidaysType(models.Model): is an employee_id in context and that no other order has been given to the method. """ + employee_id = self._get_contextual_employee_id() leave_ids = super(HolidaysType, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) - if not count and not order and self._context.get('employee_id'): + if not count and not order and employee_id: leaves = self.browse(leave_ids) sort_key = lambda l: (l.allocation_type == 'fixed', l.allocation_type == 'fixed_allocation', l.virtual_remaining_leaves) return leaves.sorted(key=sort_key, reverse=True).ids diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index 895bf975e5b..fffb8724478 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -89,17 +89,13 @@