[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.
This commit is contained in:
Thibault Delavallée
2018-09-10 13:09:00 +02:00
parent 14491e8f9b
commit 744de4ee1c
4 changed files with 221 additions and 128 deletions
+142 -100
View File
@@ -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,
@@ -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([
+12 -5
View File
@@ -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
+62 -21
View File
@@ -89,17 +89,13 @@
<button string="Refuse" name="action_refuse" type="object" attrs="{'invisible': ['|', ('can_approve', '=', False), ('state', 'not in', ('confirm','validate','validate1'))]}"/>
<button string="Reset to Draft" name="action_draft" type="object"
attrs="{'invisible': ['|', ('can_reset', '=', False), ('state', 'not in', ['confirm', 'refuse'])]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,validate"/>
<field name="state" widget="statusbar" statusbar_visible="confirm,validate"/>
</header>
<sheet>
<div class="oe_title">
<h1><field name="display_name"/></h1>
</div>
<field name="leave_type_request_unit" invisible="1"/>
<group>
<group>
<field name="name" attrs="{'readonly':[('state','!=','draft'),('state','!=','confirm')]}"/>
<field name="holiday_status_id" context="{'employee_id':employee_id, 'default_date_from':date_from}"/>
<field name="holiday_status_id" context="{'employee_id':employee_id, 'default_date_from':date_from}" options="{'no_create': True, 'no_open': True}"/>
<field name="number_of_days" invisible="1"/>
<label for="number_of_days_display" string="Duration"/>
@@ -112,25 +108,70 @@
<field name="number_of_hours_display" nolabel="1" class="oe_inline"/>
<span class="ml8">Hours</span>
</div>
<field name="request_unit" widget="radio" attrs="{'readonly': [('state', '=', 'validate')]}"/>
</div>
<div class="o_row">
<span attrs="{'invisible': [('request_unit', '!=', 'period')]}">From </span>
<field name="request_date_from" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': [('leave_type_request_unit', '=', 'hour'), ('request_unit', '=', 'period')]}"/>
<field name="date_from" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': ['|', ('leave_type_request_unit', '!=', 'hour'), ('request_unit', '!=', 'period')]}"/>
<span class="ml8" attrs="{'invisible': ['|', '&amp;', ('request_unit', '!=', 'half'), ('leave_type_request_unit', '!=', 'half'), '&amp;', ('request_unit', '=', 'day'), ('leave_type_request_unit', '=', 'half')]}"/>
<field name="request_date_from_period" options="{'horizontal': True}"
attrs="{'readonly': [('state', '=', 'validate')], 'invisible': ['|', '|', ('leave_type_request_unit', '=', 'day'), '&amp;', ('leave_type_request_unit', '=', 'half'), ('request_unit', '=', 'day'), '&amp;', ('leave_type_request_unit', '=', 'hour'), ('request_unit', '!=', 'half')]}"/>
<label for="request_date_from" string="From"/>
<div>
<field name="date_from" invisible="1"/>
<field name="date_to" invisible="1"/>
<div class="o_row o_row_readonly">
<field name="request_date_from" class="oe_inline"
attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
<span class="oe_inline"
attrs="{'invisible': ['|', ('request_unit_half', '=', True), ('request_unit_hours', '=', True)]}">
To
</span>
<field name="request_date_to" class="oe_inline"
attrs="{
'readonly': [('state', 'not in', ('draft', 'confirm'))],
'invisible': ['|', ('request_unit_half', '=', True), ('request_unit_hours', '=', True)]
}"/>
<field name="request_date_from_period" class="oe_inline"
string="In"
options="{'horizontal': True}"
attrs="{
'readonly': [('state', 'not in', ('draft', 'confirm'))],
'required': [('request_unit_half', '=', True)],
'invisible': [('request_unit_half', '=', False)]}"/>
</div>
<div class="o_row">
<span attrs="{'invisible': [('request_unit', '!=', 'period')]}">To </span>
<field name="request_date_to" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': ['|', ('request_unit', '!=', 'period'), ('leave_type_request_unit', '=', 'hour')]}"/>
<field name="date_to" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': ['|', ('leave_type_request_unit', '!=', 'hour'), ('request_unit', '!=', 'period')], 'required': [('request_unit', '=', 'period')]}"/>
<span class="ml8" attrs="{'invisible': ['|', ('leave_type_request_unit', '!=', 'half'), ('request_unit', '!=', 'period')]}"/>
<field name="request_date_to_period" options="{'horizontal': True}"
attrs="{'readonly': [('state', '=', 'validate')], 'invisible': ['|', ('leave_type_request_unit', '!=', 'half'), ('request_unit', '!=', 'period')]}"/>
<div class="o_row o_row_readonly">
<label for="request_unit_half"
attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
<field name="request_unit_half"
attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
<label for="request_unit_hours" attrs="{
'readonly': [('state', 'not in', ('draft', 'confirm'))],
'invisible': [('leave_type_request_unit', '!=', 'hour')]
}"/>
<field name="request_unit_hours" attrs="{
'readonly': [('state', 'not in', ('draft', 'confirm'))],
'invisible': [('leave_type_request_unit', '!=', 'hour')]
}"/>
<label for="request_unit_custom" invisible="1" attrs="{
'readonly': [('state', 'not in', ('draft', 'confirm'))],
}"/>
<field name="request_unit_custom" invisible="1" attrs="{
'readonly': [('state', 'not in', ('draft', 'confirm'))],
}"/>
</div>
<div class="o_row o_row_readonly">
<label for="request_hour_from" string="From"
attrs="{'invisible': [('request_unit_hours', '=', False)]}"/>
<field name="request_hour_from"
attrs="{
'readonly': [('state', '=', 'validate')],
'required': [('request_unit_hours', '=', True)],
'invisible': [('request_unit_hours', '=', False)]}"/>
<label for="request_hour_to" string="To"
attrs="{'invisible': [('request_unit_hours', '=', False)]}"/>
<field name="request_hour_to"
attrs="{
'readonly': [('state', '=', 'validate')],
'required': [('request_unit_hours', '=', True)],
'invisible': [('request_unit_hours', '=', False)]}"/>
</div>
</div>
<field name="name" attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
</group>
<group name="col_right">
<field name="employee_id" invisible="1"/>