From 744de4ee1cec5e43aa77ac3eccc7a9b1224f8fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 17 Aug 2018 11:25:11 +0200 Subject: [PATCH] [REF] hr_holidays: improve leave form view and simplify duration encoding Purpose of this commit is to simplify leave request encoding. We want less options and more straightforward encoding of leave requests. Specifications * taking half-day leaves is limited to one half day. It is not possible anymore to take leaves in half days that span on several days. WHen choosing half day the user choose whether he takes the morning or the afternoon of the given day; * choosing half-days leaves spanning on several days can be done taking several leaves if necessary. It is considered as a corner case that should not bloat the interface; * taking hours-based leaves is limited to a given day. It is not possible anymore to take leaves given some start and end hours that span on several days. When choosing hours the user choose the start and end hours using a selection for every half-hour for the given day; * choosing hours-based leaves spanning on several days can be done taking several leaves if necessary. It is considered as a corner case that should not bloat the interface; * taking leaves spanning on several days is necessarily done with full length days; Various misc specifications about form view cleaning * hide "To submit" stage in the stage bar; * move description below duration information; * remove display name; Duration computation, various fields and form view are updated accordingly to this specification. This commit is linked to task ID 1876795 and PR #26571. --- addons/hr_holidays/models/hr_leave.py | 242 ++++++++++-------- .../hr_holidays/models/hr_leave_allocation.py | 7 +- addons/hr_holidays/models/hr_leave_type.py | 17 +- addons/hr_holidays/views/hr_leave_views.xml | 83 ++++-- 4 files changed, 221 insertions(+), 128 deletions(-) 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 @@