From b6642f1b702fc32f765dfd24e855a652572a83e1 Mon Sep 17 00:00:00 2001
From: RomainLibert <rli@odoo.com>
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         | 211 ++++++++++++++----
 .../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, 291 insertions(+), 99 deletions(-)

diff --git a/addons/hr_holidays/data/hr_holidays_demo.xml b/addons/hr_holidays/data/hr_holidays_demo.xml
index fb9b43eead55..f3c7407534ab 100644
--- a/addons/hr_holidays/data/hr_holidays_demo.xml
+++ b/addons/hr_holidays/data/hr_holidays_demo.xml
@@ -2,6 +2,22 @@
 <odoo>
 <data noupdate="1">
 
+    <record id="hr_holiday_status_hl" model="hr.leave.type">
+      <field name="name">Overtime Compensation</field>
+      <field name="limit" eval="False"/>
+      <field name="color_name">blue</field>
+      <field name="validity_start" eval="time.strftime('%Y-01-01')"/>
+      <field name="validity_stop" eval="time.strftime('%Y-12-31')"/>
+      <field name="request_unit">hour</field>
+    </record>
+
+    <record id="hr_holiday_status_dv" model="hr.leave.type">
+      <field name="name">Parental Leaves</field>
+      <field name="limit" eval="True"/>
+      <field name="color_name">brown</field>
+      <field name="validation_type">both</field>
+    </record>
+
     <record id="hr_holidays_employee1_allocation_cl" model="hr.leave.allocation">
         <field name="name">Legal Leaves for Peter Parker</field>
         <field name="holiday_status_id" ref="holiday_status_cl"/>
@@ -18,6 +34,14 @@
         <field name="state">confirm</field>
     </record>
 
+    <record id="hr_holidays_employee1_allocation_hl" model="hr.leave.allocation">
+        <field name="name">Overtime Compensation</field>
+        <field name="holiday_status_id" ref="hr_holiday_status_hl"/>
+        <field name="number_of_hours">160</field>
+        <field name="number_of_days_temp">20</field>
+        <field name="employee_id" ref="hr.employee_root"/>
+    </record>
+
     <record id="hr_holidays_employee1_vc" model="hr.leave.allocation">
         <field name="name">Summer Vacation</field>
         <field name="holiday_status_id" ref="holiday_status_unpaid"/>
@@ -25,8 +49,8 @@
         <field name="employee_id" ref="hr.employee_root"/>
     </record>
 
-    <!-- approve the first 2 leave allocations -->
-    <function model="hr.leave.allocation" name="action_approve" eval="[[ref('hr_holidays.hr_holidays_employee1_allocation_cl'), ref('hr_holidays.hr_holidays_employee1_int_tour')]]"/>
+    <!-- approve the first 3 leave allocations -->
+    <function model="hr.leave.allocation" name="action_approve" eval="[[ref('hr_holidays.hr_holidays_employee1_allocation_cl'), ref('hr_holidays.hr_holidays_employee1_int_tour'), ref('hr_holidays_employee1_allocation_hl')]]"/>
 
     <record id="hr_holidays_employee1_cl" model="hr.leave">
         <field name="name">Trip with Family</field>
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index 7fca30f50f80..f0359744ebbd 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,39 +112,105 @@ 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.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.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
 
-    @api.multi
-    def _compute_can_reset(self):
-        """ User can reset a leave request if it is its own leave request
-            or if he is an Hr Manager.
-        """
-        user = self.env.user
-        group_hr_manager = self.env.ref('hr_holidays.group_hr_holidays_manager')
-        for holiday in self:
-            if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user:
-                holiday.can_reset = True
+        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
 
-    @api.depends('employee_id', 'department_id')
-    def _compute_can_approve(self):
-        """ User can only approve a leave request if it is not his own
-            Exception : User is holiday manager and has no manager
-        """
-        for holiday in self:
-            # User is holiday manager and has no manager
-            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
+        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):
@@ -154,24 +225,25 @@ class HolidaysRequest(models.Model):
             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
+        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_with_delta = fields.Datetime.from_string(date_from) + timedelta(hours=HOURS_PER_DAY)
-            self.date_to = str(date_to_with_delta)
+            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):
@@ -182,8 +254,15 @@ class HolidaysRequest(models.Model):
     @api.onchange('date_to')
     def _onchange_date_to(self):
         """ Update the number_of_days. """
-        date_from = self.date_from
-        date_to = self.date_to
+        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):
@@ -191,6 +270,44 @@ class HolidaysRequest(models.Model):
         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
+            or if he is an Hr Manager.
+        """
+        user = self.env.user
+        group_hr_manager = self.env.ref('hr_holidays.group_hr_holidays_manager')
+        for holiday in self:
+            if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user:
+                holiday.can_reset = True
+
+    @api.depends('employee_id', 'department_id')
+    def _compute_can_approve(self):
+        """ User can only approve a leave request if it is not his own
+            Exception : User is holiday manager and has no manager
+        """
+        for holiday in self:
+            # User is holiday manager and has no manager
+            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.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 7131b972be3c..e5b63e4215dd 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 8a7a8a5f5340..c31e0355d9fd 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 efc3b36879e7..c39e12c34d7d 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 @@
                     </div>
                     <group>
                         <group>
+                            <field name="type_request_unit" invisible="1"/>
                             <field name="name" attrs="{'readonly':[('state','!=','draft'),('state','!=','confirm')]}"/>
                             <field name="holiday_status_id" context="{'employee_id':employee_id}"/>
-                            <label for="number_of_days_temp" string="Duration"/>
+                            <label string="Duration"/>
                             <div>
-                                <div>
-                                    <field name="number_of_days_temp" class="oe_inline"/> days
+                                <div attrs="{'invisible': [('type_request_unit', '=', 'hour')]}">
+                                    <field name="number_of_days_temp" class="oe_inline" attrs="{'readonly': [('state', '=', 'validate')]}"/> days
+                                </div>
+                                <div attrs="{'invisible': [('type_request_unit', '!=', 'hour')]}">
+                                    <field name="number_of_hours" class="oe_inline" attrs="{'readonly': [('state', '=', 'validate')]}"/> hours
                                 </div>
                             </div>
                         </group>
diff --git a/addons/hr_holidays/views/hr_leave_type_views.xml b/addons/hr_holidays/views/hr_leave_type_views.xml
index 2a0cf8600a6f..d4c23139c75a 100644
--- a/addons/hr_holidays/views/hr_leave_type_views.xml
+++ b/addons/hr_holidays/views/hr_leave_type_views.xml
@@ -33,12 +33,13 @@
                             <field name="time_type" groups="base.group_no_one"/>
                             <field name="limit"/>
                             <field name="sequence" attrs="{'invisible': [('limit', '=', False)]}" groups="base.group_no_one"/>
+                            <field name="request_unit"/>
                         </group>
                         <group name="validation" string="Validation">
                             <field name="validation_type" widget="radio"/>
                         </group>
                         <group name="visibility" string="Visibility">
-                            <field name="employee_applicability" widget="radio" attrs="{'readonly': [('limit', '=', True)]}"/>
+                            <field name="employee_applicability" widget="radio" attrs="{'readonly': [('limit', '=', True)]}" force_save="1"/>
                         </group>
                         <group name="calendar" string="Calendar">
                             <field name="categ_id"/>
@@ -102,7 +103,6 @@
                 <field name="validation_type" />
                 <field name="validity_start" />
                 <field name="validity_stop" />
-                <field name="valid"/>
             </tree>
         </field>
     </record>
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml
index 1b210c22eb4e..30f578d81f67 100644
--- a/addons/hr_holidays/views/hr_leave_views.xml
+++ b/addons/hr_holidays/views/hr_leave_views.xml
@@ -104,13 +104,36 @@
                         <field name="holiday_status_id" context="{'employee_id':employee_id, 'default_date_from':date_from}"/>
                         <label for="number_of_days_temp" string="Duration"/>
                         <div>
+                            <field name="leave_type_request_unit" invisible="1"/>
                             <div>
-                                <field name="date_from" class="oe_inline"/>
-                                <label string="-" class="oe_inline"/>
-                                <field name="date_to" class="oe_inline"/>
+                              <field name="request_unit_all" widget="radio" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': [('leave_type_request_unit', '=', 'day')]}"/>
+                              <field name="request_unit_day" widget="radio" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': [('leave_type_request_unit', '!=', 'day')]}"/>
+
+                              <div>
+                                  <div class="o_row">
+                                      <span attrs="{'invisible': [('request_unit_all', '!=', 'period')]}">From </span>
+                                      <field name="request_date_from" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': [('leave_type_request_unit', '=', 'hour'), ('request_unit_all', '=', 'period')]}"/>
+                                      <field name="date_from" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': ['|', ('leave_type_request_unit', '!=', 'hour'), ('request_unit_all', '!=', 'period')]}"/>
+                                      <span class="ml8" attrs="{'invisible': ['|', '&amp;', ('request_unit_all', '!=', 'half'), ('leave_type_request_unit', '!=', 'half'), '&amp;', ('request_unit_all', '=', '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_all', '=', 'day'), '&amp;', ('leave_type_request_unit', '=', 'hour'), ('request_unit_all', '!=', 'half')]}"/>
+                                  </div>
+                                  <div class="o_row">
+                                      <span  attrs="{'invisible': [('request_unit_all', '!=', 'period')]}">To </span>
+                                      <field name="request_date_to" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': ['|', ('request_unit_all', '!=', 'period'), ('leave_type_request_unit', '=', 'hour')]}"/>
+                                      <field name="date_to" attrs="{'readonly': [('state', '=', 'validate')], 'invisible': ['|', ('leave_type_request_unit', '!=', 'hour'), ('request_unit_all', '!=', 'period')], 'required': ['|', ('request_unit_all', '=', 'period'), ('request_unit_day', '=', 'period')]}"/>
+                                      <span class="ml8" attrs="{'invisible': ['|', ('leave_type_request_unit', '!=', 'half'), ('request_unit_all', '!=', 'period')]}"/>
+                                      <field name="request_date_to_period" options="{'horizontal': True}"
+                                          attrs="{'readonly': [('state', '=', 'validate')], 'invisible': ['|', ('leave_type_request_unit', '!=', 'half'), ('request_unit_all', '!=', 'period')]}"/>
+                                  </div>
+                              </div>
+                          </div>
+
+                            <div attrs="{'invisible': [('leave_type_request_unit', '=', 'hour')]}">
+                                <field name="number_of_days_temp" class="oe_inline"/> day(s)
                             </div>
-                            <div>
-                                <field name="number_of_days_temp" class="oe_inline"/> days
+                            <div attrs="{'invisible': [('leave_type_request_unit', '!=', 'hour')]}">
+                                <field name="number_of_hours" class="oe_inline"/> hour(s)
                             </div>
                         </div>
                     </group>
diff --git a/addons/resource/data/resource_data.xml b/addons/resource/data/resource_data.xml
index 84d0e86f46eb..52bd5f1895e6 100644
--- a/addons/resource/data/resource_data.xml
+++ b/addons/resource/data/resource_data.xml
@@ -27,16 +27,16 @@
         <field name="hours_per_day">7.0</field>
         <field name="attendance_ids"
             eval="[
-                (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16}),
-                (0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16}),
-                (0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16}),
-                (0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16}),
-                (0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16})
+                (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
+                (0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
+                (0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
+                (0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
+                (0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'})
             ]"
         />
     </record>
@@ -47,16 +47,16 @@
         <field name="hours_per_day">7.6</field>
         <field name="attendance_ids"
             eval="[
-                (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16.6}),
-                (0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16.6}),
-                (0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16.6}),
-                (0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16.6}),
-                (0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12}),
-                (0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16.6})
+                (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'}),
+                (0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'}),
+                (0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'}),
+                (0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'}),
+                (0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+                (0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16.6, 'day_period': 'afternoon'})
             ]"
         />
     </record>
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index f25b8131ac26..0ab6306565db 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -142,16 +142,16 @@ class ResourceCalendar(models.Model):
 
     def _get_default_attendance_ids(self):
         return [
-            (0, 0, {'name': _('Monday Morning'), 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12}),
-            (0, 0, {'name': _('Monday Evening'), 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17}),
-            (0, 0, {'name': _('Tuesday Morning'), 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12}),
-            (0, 0, {'name': _('Tuesday Evening'), 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17}),
-            (0, 0, {'name': _('Wednesday Morning'), 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12}),
-            (0, 0, {'name': _('Wednesday Evening'), 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17}),
-            (0, 0, {'name': _('Thursday Morning'), 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12}),
-            (0, 0, {'name': _('Thursday Evening'), 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17}),
-            (0, 0, {'name': _('Friday Morning'), 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12}),
-            (0, 0, {'name': _('Friday Evening'), 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17})
+            (0, 0, {'name': _('Monday Morning'), 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+            (0, 0, {'name': _('Monday Evening'), 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
+            (0, 0, {'name': _('Tuesday Morning'), 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+            (0, 0, {'name': _('Tuesday Evening'), 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
+            (0, 0, {'name': _('Wednesday Morning'), 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+            (0, 0, {'name': _('Wednesday Evening'), 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
+            (0, 0, {'name': _('Thursday Morning'), 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+            (0, 0, {'name': _('Thursday Evening'), 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
+            (0, 0, {'name': _('Friday Morning'), 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
+            (0, 0, {'name': _('Friday Evening'), 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'})
         ]
 
     name = fields.Char(required=True)
@@ -395,6 +395,7 @@ class ResourceCalendarAttendance(models.Model):
     hour_from = fields.Float(string='Work from', required=True, index=True, help="Start and End time of working.")
     hour_to = fields.Float(string='Work to', required=True)
     calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True, ondelete='cascade')
+    day_period = fields.Selection([('morning', 'Morning'), ('afternoon', 'Afternoon')], required=True, default='morning')
 
     @api.onchange('hour_from', 'hour_to')
     def _onchange_hours(self):
diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py
index d5aa1cee51fa..c74db0753954 100644
--- a/addons/resource/models/resource_mixin.py
+++ b/addons/resource/models/resource_mixin.py
@@ -8,8 +8,8 @@ from pytz import utc
 from odoo import api, fields, models
 from odoo.tools import float_utils
 
-# This will generate quarter of days
-ROUNDING_FACTOR = 4
+# This will generate eights of days
+ROUNDING_FACTOR = 8
 
 
 class ResourceMixin(models.AbstractModel):
diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py
index a190878e7302..d790a897ba8d 100644
--- a/addons/resource/tests/test_resource.py
+++ b/addons/resource/tests/test_resource.py
@@ -325,7 +325,7 @@ class TestResMixin(TestResourceCommon):
             datetime_tz(2018, 4, 6, 16, 0, 0, tzinfo=self.john.tz),
         )
         # still showing as 5 days because of rounding, but we see only 39 hours
-        self.assertEqual(data, {'days': 5, 'hours': 39})
+        self.assertEqual(data, {'days': 4.875, 'hours': 39})
 
         # Looking at John's calendar
 
@@ -335,7 +335,7 @@ class TestResMixin(TestResourceCommon):
             datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
             datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
         )
-        self.assertEqual(data, {'days': 1.5, 'hours': 13})
+        self.assertEqual(data, {'days': 1.375, 'hours': 13})
 
         # Viewing it as Patel
         # Views from 2018/04/01 11:00:00 to 2018/04/06 10:00:00
@@ -343,7 +343,7 @@ class TestResMixin(TestResourceCommon):
             datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz),
             datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.patel.tz),
         )
-        self.assertEqual(data, {'days': 1.25, 'hours': 10})
+        self.assertEqual(data, {'days': 1.125, 'hours': 10})
 
         # Viewing it as John
         data = self.john.get_work_days_data(
diff --git a/addons/resource/views/resource_views.xml b/addons/resource/views/resource_views.xml
index 4d39092d801d..6629144f053a 100644
--- a/addons/resource/views/resource_views.xml
+++ b/addons/resource/views/resource_views.xml
@@ -193,6 +193,7 @@
                 <field name="hour_to" widget="float_time"/>
                 <field name="date_from"/>
                 <field name="date_to"/>
+                <field name="day_period"/>
             </tree>
         </field>
     </record>
@@ -212,6 +213,7 @@
                         <field name="hour_from" widget="float_time" class="oe_inline"/> -
                         <field name="hour_to" widget="float_time" class="oe_inline"/>
                     </div>
+                    <field name="day_period"/>
                 </group>
             </form>
         </field>
-- 
GitLab