[IMP] hr_holidays: see future allocations + refactoring

This commit adds the feature to select a date in the future
in order to see future allocations, would it be for accrual plans
granting new allocated days, lost days due to expired allocations
or allocations that are not yet available to the employee.
The date selector only appear when the employee has accrual allocation,
in which case the feature is more relevant.

This feature comes with a major refactoring of hr_holidays
methods to handle some edge cases in the management of leaves.
The refactoring also includes the removal of the 'draft' and 'cancel'
state in allocations.

This commit also removes unused imports and implements some linting fixes.

task-2675380

closes odoo/odoo#108148

Related: odoo/upgrade#4238
Related: odoo/enterprise#35157
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Dossogne Bertrand
2023-10-06 08:25:12 +00:00
parent 41d135d860
commit 8f87e102a9
33 changed files with 1040 additions and 877 deletions
+4
View File
@@ -507,6 +507,10 @@ class HrEmployeePrivate(models.Model):
# Returns a dict {employee_id: tz}
return {emp.id: emp._get_tz() for emp in self}
def _get_calendar_attendances(self, date_from, date_to):
self.ensure_one()
return self.resource_calendar_id.get_work_duration_data(date_from, date_to)
# ---------------------------------------------------------
# Business Methods
# ---------------------------------------------------------
+7
View File
@@ -140,6 +140,13 @@ class Employee(models.Model):
))
return unusual_days
def _get_calendar_attendances(self, date_from, date_to):
self.ensure_one()
valid_contracts = self.sudo()._get_contracts(date_from, date_to, states=['open', 'close'])
if not valid_contracts:
return super()._get_calendar_attendances(date_from, date_to)
return valid_contracts.resource_calendar_id.get_work_duration_data(date_from, date_to)
def write(self, vals):
res = super().write(vals)
if vals.get('contract_id'):
+11
View File
@@ -11,4 +11,15 @@
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
</record>
<record id="hr_leave_cron_cancel_invalid" model="ir.cron">
<field name="name">Time Off: Cancel invalid leaves</field>
<field name="model_id" ref="model_hr_leave"/>
<field name="state">code</field>
<field name="code">model._cancel_invalid_leaves()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
</record>
</odoo>
+193 -16
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from datetime import datetime, date, time
from collections import defaultdict
from dateutil.relativedelta import relativedelta
import pytz
@@ -82,7 +83,7 @@ class HrEmployeeBase(models.AbstractModel):
def _compute_allocation_count(self):
# Don't get allocations that are expired
current_date = datetime.date.today()
current_date = date.today()
data = self.env['hr.leave.allocation']._read_group([
('employee_id', 'in', self.ids),
('holiday_status_id.active', '=', True),
@@ -102,15 +103,15 @@ class HrEmployeeBase(models.AbstractModel):
def _compute_allocation_remaining_display(self):
allocations = self.env['hr.leave.allocation'].search([('employee_id', 'in', self.ids)])
leaves_taken = allocations.holiday_status_id._get_employees_days_per_allocation(self.ids)
leaves_taken = self._get_consumed_leaves(allocations.holiday_status_id)[0]
for employee in self:
employee_remaining_leaves = 0
for leave_type in leaves_taken[employee.id]:
for leave_type in leaves_taken[employee]:
if leave_type.requires_allocation == 'no':
continue
for allocation in leaves_taken[employee.id][leave_type]:
for allocation in leaves_taken[employee][leave_type]:
if allocation:
virtual_remaining_leaves = leaves_taken[employee.id][leave_type][allocation]['virtual_remaining_leaves']
virtual_remaining_leaves = leaves_taken[employee][leave_type][allocation]['virtual_remaining_leaves']
employee_remaining_leaves += virtual_remaining_leaves\
if leave_type.request_unit in ['day', 'half_day']\
else virtual_remaining_leaves / (employee.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
@@ -171,7 +172,7 @@ class HrEmployeeBase(models.AbstractModel):
raise UserError(_('Operation not supported'))
# This search is only used for the 'Absent Today' filter however
# this only returns employees that are absent right now.
today_date = datetime.datetime.utcnow().date()
today_date = datetime.utcnow().date()
today_start = fields.Datetime.to_string(today_date)
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
holidays = self.env['hr.leave'].sudo().search([
@@ -281,11 +282,6 @@ class HrEmployee(models.Model):
},
}
def _get_contextual_employee(self):
if self.env.context.get('employee_id'):
return self.browse(self.env.context['employee_id'])
return self.env.user.employee_id
def _is_leave_user(self):
return self == self.env.user.employee_id and self.user_has_groups('hr_holidays.group_hr_holidays_user')
@@ -317,10 +313,10 @@ class HrEmployee(models.Model):
return list(map(lambda bh: {
'id': -bh.id,
'colorIndex': 0,
'end': datetime.datetime.combine(bh.date_to.astimezone(employee_tz), datetime.datetime.max.time()).isoformat(),
'end': datetime.combine(bh.date_to.astimezone(employee_tz), datetime.max.time()).isoformat(),
'endType': "datetime",
'isAllDay': True,
'start': datetime.datetime.combine(bh.date_from.astimezone(employee_tz), datetime.datetime.min.time()).isoformat(),
'start': datetime.combine(bh.date_from.astimezone(employee_tz), datetime.min.time()).isoformat(),
'startType': "datetime",
'title': bh.name,
}, public_holidays))
@@ -350,10 +346,10 @@ class HrEmployee(models.Model):
return list(map(lambda sd: {
'id': -sd.id,
'colorIndex': sd.color,
'end': datetime.datetime.combine(sd.end_date, datetime.datetime.max.time()).isoformat(),
'end': datetime.combine(sd.end_date, datetime.max.time()).isoformat(),
'endType': "datetime",
'isAllDay': True,
'start': datetime.datetime.combine(sd.start_date, datetime.datetime.min.time()).isoformat(),
'start': datetime.combine(sd.start_date, datetime.min.time()).isoformat(),
'startType': "datetime",
'title': sd.name,
}, mandatory_days))
@@ -382,3 +378,184 @@ class HrEmployee(models.Model):
domain += [('department_ids', '=', False)]
return self.env['hr.leave.mandatory.day'].search(domain)
@api.model
def _get_contextual_employee(self):
ctx = self.env.context
return self.browse(ctx.get('employee_id') or ctx.get('default_employee_id')) or self.env.user.employee_id
def _get_consumed_leaves(self, leave_types, target_date=False, ignore_future=False):
employees = self or self._get_contextual_employee()
leaves_domain = [
('holiday_status_id', 'in', leave_types.ids),
('employee_id', 'in', employees.ids),
('state', 'in', ['confirm', 'validate1', 'validate']),
]
if not target_date:
target_date = fields.Date.today()
if ignore_future:
leaves_domain.append(('date_from', '<=', target_date))
leaves = self.env['hr.leave'].search(leaves_domain)
leaves_per_employee_type = defaultdict(lambda: defaultdict(lambda: self.env['hr.leave']))
for leave in leaves:
leaves_per_employee_type[leave.employee_id][leave.holiday_status_id] |= leave
allocations = self.env['hr.leave.allocation'].with_context(active_test=False).search([
('employee_id', 'in', employees.ids),
('holiday_status_id', 'in', leave_types.ids),
('state', '=', 'validate'),
]).filtered(lambda al: al.active or not al.employee_id.active)
allocations_per_employee_type = defaultdict(lambda: defaultdict(lambda: self.env['hr.leave.allocation']))
for allocation in allocations:
allocations_per_employee_type[allocation.employee_id][allocation.holiday_status_id] |= allocation
# allocation_leaves_consumed is a tuple of two dictionnaries.
# 1) The first is a dictionary to map the number of days/hours of leaves taken per allocation
# The structure is the following:
# - KEYS:
# allocation_leaves_consumed
# |--employee_id
# |--holiday_status_id
# |--allocation
# |--virtual_leaves_taken
# |--leaves_taken
# |--virtual_remaining_leaves
# |--remaining_leaves
# |--max_leaves
# |--accrual_bonus
# - VALUES:
# Integer representing the number of (virtual) remaining leaves, (virtual) leaves taken or max leaves for each allocation.
# leaves_taken and remaining_leaves only take into account validated leaves, while the "virtual" equivalent are
# also based on leaves in "confirm" or "validate1" state.
# The unit is in hour or days depending on the leave type request unit
# 2) The second is a dictionary mapping the remaining days per employee and per leave type that are either
# not taken into account by the allocations, mainly because accruals don't take future leaves into account.
# This is used to warn the user if the leaves they takes bring them above their available limit.
# - KEYS:
# allocation_leaves_consumed
# |--employee_id
# |--holiday_status_id
# |--to_recheck_leaves
# |--excess_days
# |--exceeding_duration
# - VALUES:
# "to_recheck_leaves" stores every leave that is not yet taken into account by the "allocation_leaves_consumed" dictionary.
# "excess_days" represents the excess amount that somehow isn't taken into account by the first dictionary.
# "exceeding_duration" sum up the to_recheck_leaves duration and compares it to the maximum allocated for that time period.
allocations_leaves_consumed = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0))))
to_recheck_leaves_per_leave_type = defaultdict(lambda:
defaultdict(lambda: {
'excess_days': 0,
'exceeding_duration': 0,
'to_recheck_leaves': self.env['hr.leave']
})
)
for allocation in allocations:
allocation_data = allocations_leaves_consumed[allocation.employee_id][allocation.holiday_status_id][allocation]
future_leaves = 0
if allocation.allocation_type == 'accrual':
future_leaves = allocation._get_future_leaves_on(target_date)
max_leaves = allocation.number_of_hours_display\
if allocation.type_request_unit in ['hour']\
else allocation.number_of_days_display
max_leaves += future_leaves
allocation_data.update({
'max_leaves': max_leaves,
'accrual_bonus': future_leaves,
'virtual_remaining_leaves': max_leaves,
'remaining_leaves': max_leaves,
'leaves_taken': 0,
'virtual_leaves_taken': 0,
})
for employee in employees:
for leave_type in leave_types:
allocations_with_date_to = self.env['hr.leave.allocation']
allocations_without_date_to = self.env['hr.leave.allocation']
for leave_allocation in allocations_per_employee_type[employee][leave_type]:
if leave_allocation.date_to:
allocations_with_date_to |= leave_allocation
else:
allocations_without_date_to |= leave_allocation
sorted_leave_allocations = allocations_with_date_to.sorted(key='date_to') + allocations_without_date_to
if leave_type.request_unit in ['day', 'half_day']:
leave_duration_field = 'number_of_days'
leave_unit = 'days'
else:
leave_duration_field = 'number_of_hours_display'
leave_unit = 'hours'
leave_type_data = allocations_leaves_consumed[employee][leave_type]
for leave in leaves_per_employee_type[employee][leave_type].sorted('date_from'):
leave_duration = leave[leave_duration_field]
if leave_type.requires_allocation == 'yes':
for allocation in sorted_leave_allocations:
# We don't want to include future leaves linked to accruals into the total count of available leaves.
# However, we'll need to check if those leaves take more than what will be accrued in total of those days
# to give a warning if the total exceeds what will be accrued.
if allocation.allocation_type == 'accrual' and leave.date_from.date() > target_date:
to_recheck_leaves_per_leave_type[employee][leave_type]['to_recheck_leaves'] |= leave
continue
interval_start = max(
leave.date_from,
datetime.combine(allocation.date_from, time.min)
).replace(tzinfo=pytz.UTC)
interval_end = min(
leave.date_to,
datetime.combine(allocation.date_to, time.max)
if allocation.date_to else leave.date_to
).replace(tzinfo=pytz.UTC)
duration_info = employee._get_calendar_attendances(interval_start, interval_end)
max_allowed_duration = min(
duration_info['hours' if leave_unit == 'hours' else 'days'],
leave_type_data[allocation]['virtual_remaining_leaves']
)
if not max_allowed_duration:
continue
allocated_time = min(max_allowed_duration, leave_duration)
leave_type_data[allocation]['virtual_leaves_taken'] += allocated_time
leave_type_data[allocation]['virtual_remaining_leaves'] -= allocated_time
if leave.state == 'validate':
leave_type_data[allocation]['leaves_taken'] += allocated_time
leave_type_data[allocation]['remaining_leaves'] -= allocated_time
leave_duration -= allocated_time
if not leave_duration:
break
if round(leave_duration, 2) > 0:
to_recheck_leaves_per_leave_type[employee][leave_type]['excess_days'] += leave_duration
else:
if leave_unit == 'hour':
allocated_time = leave.number_of_hours_display
else:
allocated_time = leave.number_of_days_display
leave_type_data[False]['virtual_leaves_taken'] += allocated_time
leave_type_data[False]['virtual_remaining_leaves'] = 0
leave_type_data[False]['remaining_leaves'] = 0
if leave.state == 'validate':
leave_type_data[False]['leaves_taken'] += allocated_time
for employee in to_recheck_leaves_per_leave_type:
for leave_type in to_recheck_leaves_per_leave_type[employee]:
content = to_recheck_leaves_per_leave_type[employee][leave_type]
consumed_content = allocations_leaves_consumed[employee][leave_type]
if content['to_recheck_leaves']:
date_to_simulate = max(content['to_recheck_leaves'].mapped('date_from')).date()
latest_accrual_bonus = 0
date_accrual_bonus = 0
virtual_remaining = 0
additional_leaves_duration = 0
for allocation in consumed_content:
latest_accrual_bonus += allocation._get_future_leaves_on(date_to_simulate)
date_accrual_bonus += consumed_content[allocation]['accrual_bonus']
virtual_remaining += consumed_content[allocation]['virtual_remaining_leaves']
for leave in content['to_recheck_leaves']:
additional_leaves_duration += leave.number_of_hours if leave_type.request_unit == 'hours' else leave.number_of_days
latest_remaining = virtual_remaining - date_accrual_bonus + latest_accrual_bonus
content['exceeding_duration'] = round(min(0, latest_remaining - additional_leaves_duration), 2)
return (allocations_leaves_consumed, to_recheck_leaves_per_leave_type)
+67 -42
View File
@@ -17,8 +17,8 @@ from odoo import api, Command, fields, models, tools
from odoo.addons.base.models.res_partner import _tz_get
from odoo.addons.resource.models.utils import float_to_time, HOURS_PER_DAY
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import float_compare, format_date
from odoo.tools.float_utils import float_round
from odoo.tools.float_utils import float_round, float_compare
from odoo.tools.misc import format_date
from odoo.tools.translate import _
from odoo.osv import expression
@@ -116,12 +116,13 @@ class HolidaysRequest(models.Model):
# description
name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False, copy=False)
private_name = fields.Char('Time Off Description', groups='hr_holidays.group_hr_holidays_user')
state = fields.Selection([
('draft', 'To Submit'),
('confirm', 'To Approve'),
('refuse', 'Refused'),
('validate1', 'Second Approval'),
('validate', 'Approved')
state = fields.Selection(
[
('draft', 'To Submit'),
('confirm', 'To Approve'),
('refuse', 'Refused'),
('validate1', 'Second Approval'),
('validate', 'Approved')
], string='Status', compute='_compute_state', store=True, tracking=True, copy=False, readonly=False,
help="The status is set to 'To Submit', when a time off request is created." +
"\nThe status is 'To Approve', when time off request is confirmed by user." +
@@ -132,10 +133,16 @@ class HolidaysRequest(models.Model):
manager_id = fields.Many2one('hr.employee', compute='_compute_from_employee_id', store=True, readonly=False)
# leave type configuration
holiday_status_id = fields.Many2one(
"hr.leave.type", compute='_compute_from_employee_id', store=True, string="Time Off Type", required=True, readonly=False,
domain="[('company_id', '?=', employee_company_id), '|', ('requires_allocation', '=', 'no'), ('has_valid_allocation', '=', True)]", tracking=True)
holiday_allocation_id = fields.Many2one(
'hr.leave.allocation', compute='_compute_from_holiday_status_id', string="Allocation", store=True, readonly=False)
"hr.leave.type", compute='_compute_from_employee_id',
store=True, string="Time Off Type",
required=True, readonly=False,
domain="""[
('company_id', 'in', [employee_company_id, False]),
'|',
('requires_allocation', '=', 'no'),
('has_valid_allocation', '=', True),
]""",
tracking=True)
color = fields.Integer("Color", related='holiday_status_id.color')
validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.leave_validation_type', readonly=False)
# HR data
@@ -691,34 +698,6 @@ class HolidaysRequest(models.Model):
raise ValidationError(_('You can not set two time off that overlap on the same day for the same employees.\nExisting time off:\n%s') %
('\n'.join(conflicting_holidays_strings)))
@api.constrains('state', 'number_of_days', 'holiday_status_id')
def _check_holidays(self):
for holiday in self:
mapped_days_date = holiday.date_from.date() or None
mapped_days = self.holiday_status_id.get_employees_days((holiday.employee_id | holiday.sudo().employee_ids).ids, mapped_days_date)
if holiday.holiday_type != 'employee'\
or not holiday.employee_id and not holiday.sudo().employee_ids\
or holiday.holiday_status_id.requires_allocation == 'no':
continue
if holiday.employee_id:
leave_days = mapped_days[holiday.employee_id.id][holiday.holiday_status_id.id]
if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1\
or float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1:
raise ValidationError(_('The number of remaining time off is not sufficient for this time off type.\n'
'Please also check the time off waiting for validation.'))
else:
unallocated_employees = []
for employee in holiday.sudo().employee_ids:
leave_days = mapped_days[employee.id][holiday.holiday_status_id.id]
if float_compare(leave_days['remaining_leaves'], self.number_of_days, precision_digits=2) == -1\
or float_compare(leave_days['virtual_remaining_leaves'], self.number_of_days, precision_digits=2) == -1:
unallocated_employees.append(employee.name)
if unallocated_employees:
raise ValidationError(_('The number of remaining time off is not sufficient for this time off type.\n'
'Please also check the time off waiting for validation.')
+ _('\nThe employees that lack allocation days are:\n%s',
(', '.join(unallocated_employees))))
@api.constrains('date_from', 'date_to', 'employee_id')
def _check_date_state(self):
if self.env.context.get('leave_skip_state_check'):
@@ -727,6 +706,18 @@ class HolidaysRequest(models.Model):
if holiday.state in ['cancel', 'refuse', 'validate1', 'validate']:
raise ValidationError(_("This modification is not allowed in the current state."))
@api.constrains('date_from', 'date_to')
def _check_validity(self):
for leave in self:
leave_type = leave.holiday_status_id
if leave_type.requires_allocation == 'no':
continue
employee = leave.employee_id
date_from = leave.date_from.date()
leave_data = leave_type.get_allocation_data(employee, date=date_from)
if leave_data[employee][0][1]['excess_days']:
raise ValidationError(_('The allocation configuration does not allow you to take this leave.'))
####################################################
# ORM Overrides methods
####################################################
@@ -767,7 +758,7 @@ class HolidaysRequest(models.Model):
leave.display_name = _("%(leave_type)s: %(duration).2f hours on %(date)s",
leave_type=time_off_type_display,
duration=leave.number_of_hours_display,
date=fields.Date.to_string(date_from_utc) or "",
date=display_date,
)
elif not time_off_type_display:
leave.display_name = _("%(person)s: %(duration).2f hours on %(date)s",
@@ -1110,7 +1101,6 @@ class HolidaysRequest(models.Model):
current_employee = self.env.user.employee_id
self.filtered(lambda hol: hol.validation_type == 'both').write({'state': 'validate1', 'first_approver_id': current_employee.id})
# Post a second message, more verbose than the tracking message
for holiday in self.filtered(lambda holiday: holiday.employee_id.user_id):
user_tz = timezone(holiday.tz)
@@ -1614,3 +1604,38 @@ class HolidaysRequest(models.Model):
attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= request_date_to.weekday()), attendances[-1] if attendances else default_value)
return (attendance_from, attendance_to)
####################################################
# Cron methods
####################################################
@api.model
def _cancel_invalid_leaves(self):
inspected_date = fields.Date.today() + timedelta(days=31)
start_datetime = datetime.combine(fields.Date.today(), datetime.min.time())
end_datetime = datetime.combine(inspected_date, datetime.max.time())
concerned_leaves = self.search([
('date_from', '>=', start_datetime),
('date_from', '<=', end_datetime),
('state', 'in', ['confirm', 'validate1', 'validate']),
], order='date_from desc')
accrual_allocations = self.env['hr.leave.allocation'].search([
('employee_id', 'in', concerned_leaves.employee_id.ids),
('holiday_status_id', 'in', concerned_leaves.holiday_status_id.ids),
('allocation_type', '=', 'accrual'),
('date_from', '<=', end_datetime),
'|',
('date_to', '>=', start_datetime),
('date_to', '=', False),
])
# only take leaves linked to accruals
concerned_leaves = concerned_leaves\
.filtered(lambda leave: leave.holiday_status_id in accrual_allocations.holiday_status_id)\
.sorted('date_from', reverse=True)
reason = _("the accruated amount is insufficient for that duration.")
for leave in concerned_leaves:
to_recheck_leaves_per_leave_type = concerned_leaves.employee_id._get_consumed_leaves(leave.holiday_status_id)[1]
exceeding_duration = to_recheck_leaves_per_leave_type[leave.employee_id][leave.holiday_status_id]['exceeding_duration']
if not exceeding_duration:
continue
leave._force_cancel(reason, 'mail.mt_note')
+102 -124
View File
@@ -37,20 +37,23 @@ class HolidaysAllocation(models.Model):
return [('requires_allocation', '=', 'yes')]
return [('employee_requests', '=', 'yes')]
name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False)
name = fields.Char(
string='Description',
compute='_compute_description',
inverse='_inverse_description',
search='_search_description',
compute_sudo=False)
name_validity = fields.Char('Description with validity', compute='_compute_description_validity')
active = fields.Boolean(default=True)
private_name = fields.Char('Allocation Description', groups='hr_holidays.group_hr_holidays_user')
state = fields.Selection([
('draft', 'To Submit'),
('cancel', 'Cancelled'),
('confirm', 'To Approve'),
('refuse', 'Refused'),
('validate', 'Approved')
], string='Status', readonly=True, tracking=True, copy=False, default='draft',
help="The status is set to 'To Submit', when an allocation request is created." +
"\nThe status is 'To Approve', when an allocation request is confirmed by user." +
"\nThe status is 'Refused', when an allocation request is refused by manager." +
('validate', 'Approved')],
string='Status', readonly=True, tracking=True, copy=False, default='confirm',
help="The status is set to 'To Submit', when an allocation request is created."
"\nThe status is 'To Approve', when an allocation request is confirmed by user."
"\nThe status is 'Refused', when an allocation request is refused by manager."
"\nThe status is 'Approved', when an allocation request is approved by manager.")
date_from = fields.Date('Start Date', index=True, copy=False, default=fields.Date.context_today,
tracking=True, required=True)
@@ -84,7 +87,6 @@ class HolidaysAllocation(models.Model):
'hr.employee', string='First Approval', readonly=True, copy=False,
help='This area is automatically filled by the user who validates the allocation')
validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.allocation_validation_type', readonly=True)
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([
('hour', 'Hours'),
@@ -123,10 +125,11 @@ class HolidaysAllocation(models.Model):
('accrual', 'Accrual Allocation')
], string="Allocation Type", default="regular", required=True, readonly=True)
is_officer = fields.Boolean(compute='_compute_is_officer')
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', compute="_compute_from_holiday_status_id", store=True, readonly=False, domain="['|', ('time_off_type_id', '=', False), ('time_off_type_id', '=', holiday_status_id)]", tracking=True)
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan',
compute="_compute_from_holiday_status_id", store=True, readonly=False, tracking=True,
domain="['|', ('time_off_type_id', '=', False), ('time_off_type_id', '=', holiday_status_id)]")
max_leaves = fields.Float(compute='_compute_leaves')
leaves_taken = fields.Float(compute='_compute_leaves', string='Time off Taken')
taken_leave_ids = fields.One2many('hr.leave', 'holiday_allocation_id', domain="[('state', 'in', ['confirm', 'validate1', 'validate'])]")
has_accrual_plan = fields.Boolean(compute='_compute_has_accrual_plan', string='Accrual Plan Available')
_sql_constraints = [
@@ -205,12 +208,13 @@ class HolidaysAllocation(models.Model):
name_validity = _("%s (from %s to No Limit)", allocation.name, allocation.date_from.strftime("%b %d %Y"))
allocation.name_validity = name_validity
@api.depends('employee_id', 'holiday_status_id', 'taken_leave_ids.number_of_days', 'taken_leave_ids.state')
@api.depends('employee_id', 'holiday_status_id')
def _compute_leaves(self):
employee_days_per_allocation = self.holiday_status_id.with_context(ignore_future=True)._get_employees_days_per_allocation(self.employee_id.ids)
date_from = fields.Date.from_string(self._context['default_date_from']) if 'default_date_from' in self._context else fields.Date.today()
employee_days_per_allocation = self.employee_id._get_consumed_leaves(self.holiday_status_id, date_from, ignore_future=True)[0]
for allocation in self:
allocation.max_leaves = allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days
allocation.leaves_taken = employee_days_per_allocation[allocation.employee_id.id][allocation.holiday_status_id][allocation]['leaves_taken']
allocation.leaves_taken = employee_days_per_allocation[allocation.employee_id][allocation.holiday_status_id][allocation]['leaves_taken']
@api.depends('number_of_days')
def _compute_number_of_days_display(self):
@@ -235,16 +239,6 @@ class HolidaysAllocation(models.Model):
else float_round(allocation.number_of_days_display, precision_digits=2)),
_('hours') if allocation.type_request_unit == 'hour' else _('days'))
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_reset(self):
for allocation in self:
try:
allocation._check_approval_update('draft')
except (AccessError, UserError):
allocation.can_reset = False
else:
allocation.can_reset = True
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_approve(self):
for allocation in self:
@@ -308,14 +302,14 @@ class HolidaysAllocation(models.Model):
@api.depends('accrual_plan_id')
def _compute_holiday_status_id(self):
default_holiday_status_id = None
for holiday in self:
if not holiday.holiday_status_id:
if holiday.accrual_plan_id:
holiday.holiday_status_id = holiday.accrual_plan_id.time_off_type_id
for allocation in self:
if not allocation.holiday_status_id:
if allocation.accrual_plan_id:
allocation.holiday_status_id = allocation.accrual_plan_id.time_off_type_id
else:
if not default_holiday_status_id: # fetch when we need it
default_holiday_status_id = self._default_holiday_status_id()
holiday.holiday_status_id = default_holiday_status_id
allocation.holiday_status_id = default_holiday_status_id
@api.depends('holiday_status_id', 'allocation_type', 'number_of_hours_display', 'number_of_days_display', 'date_to')
def _compute_from_holiday_status_id(self):
@@ -446,14 +440,14 @@ class HolidaysAllocation(models.Model):
period_prorata = min(1, call_days / period_days) if period_days else 1
return added_value * period_prorata
def _process_accrual_plans(self, date_to=False, force_period=False):
def _process_accrual_plans(self, date_to=False, force_period=False, log=True):
"""
This method is part of the cron's process.
The goal of this method is to retroactively apply accrual plan levels and progress from nextcall to date_to or today.
If force_period is set, the accrual will run until date_to in a prorated way (used for end of year accrual actions).
"""
date_to = date_to or fields.Date.today()
first_allocation = _("""This allocation have already ran once, any modification won't be effective to the days allocated to the employee. If you need to change the configuration of the allocation, cancel and create a new one.""")
first_allocation = _("""This allocation have already ran once, any modification won't be effective to the days allocated to the employee. If you need to change the configuration of the allocation, delete and create a new one.""")
for allocation in self:
level_ids = allocation.accrual_plan_id.level_ids.sorted('sequence')
if not level_ids:
@@ -478,7 +472,8 @@ class HolidaysAllocation(models.Model):
if len(level_ids) > 1:
second_level_start_date = allocation.date_from + get_timedelta(level_ids[1].start_count, level_ids[1].start_type)
allocation.nextcall = min(second_level_start_date, allocation.nextcall)
allocation._message_log(body=first_allocation)
if log:
allocation._message_log(body=first_allocation)
(current_level, current_level_idx) = (False, 0)
current_level_maximum_leave = 0.0
# all subsequent runs, at every loop:
@@ -550,6 +545,25 @@ class HolidaysAllocation(models.Model):
'|', ('nextcall', '=', False), ('nextcall', '<=', today)])
allocations._process_accrual_plans()
def _get_future_leaves_on(self, accrual_date):
# As computing future accrual allocation days automatically updates the allocation,
# We need to create a temporary copy of that allocation to return the difference in number of days
# to see how much more days will be allocated from now until that date.
self.ensure_one()
if not accrual_date or accrual_date <= date.today():
return 0
if not (self.accrual_plan_id
and self.state == 'validate'
and self.allocation_type == 'accrual'
and (not self.date_to or self.date_to > accrual_date)
and (not self.nextcall or self.nextcall <= accrual_date)):
return 0
fake_allocation = self.env['hr.leave.allocation'].new(origin=self)
fake_allocation.sudo()._process_accrual_plans(accrual_date, log=False)
return round((fake_allocation.number_of_days - self.number_of_days), 2)
####################################################
# ORM Overrides methods
####################################################
@@ -578,8 +592,13 @@ class HolidaysAllocation(models.Model):
target = allocation.category_id.name
elif allocation.employee_id:
target = allocation.employee_id.name
else:
elif len(allocation.employee_ids) <= 3:
target = ', '.join(allocation.employee_ids.sudo().mapped('name'))
else:
target = _('%(first)s, %(second)s and %(amount)s others',
first=allocation.employee_ids[0].sudo().name,
second=allocation.employee_ids[1].sudo().name,
amount=len(allocation.employee_ids) - 2)
allocation.display_name = _("Allocation of %s: %.2f %s to %s",
allocation.holiday_status_id.sudo().name,
@@ -604,27 +623,24 @@ class HolidaysAllocation(models.Model):
values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id})
if 'lastcall' not in values:
values['lastcall'] = fields.Date.today()
holidays = super(HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
created_from_dashboard = self.env.context.get('created_from_dashboard')
for holiday in holidays:
allocations = super(HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
for allocation in allocations:
partners_to_subscribe = set()
if holiday.employee_id.user_id:
partners_to_subscribe.add(holiday.employee_id.user_id.partner_id.id)
if holiday.validation_type == 'officer':
partners_to_subscribe.add(holiday.employee_id.parent_id.user_id.partner_id.id)
partners_to_subscribe.add(holiday.employee_id.leave_manager_id.partner_id.id)
holiday.message_subscribe(partner_ids=tuple(partners_to_subscribe))
if allocation.employee_id.user_id:
partners_to_subscribe.add(allocation.employee_id.user_id.partner_id.id)
if allocation.validation_type == 'officer':
partners_to_subscribe.add(allocation.employee_id.parent_id.user_id.partner_id.id)
partners_to_subscribe.add(allocation.employee_id.leave_manager_id.partner_id.id)
allocation.message_subscribe(partner_ids=tuple(partners_to_subscribe))
if not self._context.get('import_file'):
holiday.activity_update()
if holiday.validation_type == 'no' and holiday.state == 'draft':
holiday.action_confirm()
if created_from_dashboard:
holiday.action_confirm()
return holidays
allocation.activity_update()
if allocation.validation_type == 'no' and allocation.state == 'confirm':
allocation.action_validate()
return allocations
def write(self, values):
if not self.env.context.get('toggle_active') and not bool(values.get('active', True)):
if any(allocation.state not in ['draft', 'cancel', 'refuse'] for allocation in self):
if any(allocation.state not in ['refuse'] for allocation in self):
raise UserError(_('You cannot archive an allocation which is in confirm or validate state.'))
employee_id = values.get('employee_id', False)
if values.get('state'):
@@ -636,8 +652,8 @@ class HolidaysAllocation(models.Model):
@api.ondelete(at_uninstall=False)
def _unlink_if_correct_states(self):
state_description_values = {elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env)}
for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']):
raise UserError(_('You cannot delete an allocation request which is in %s state.') % (state_description_values.get(holiday.state),))
for allocation in self.filtered(lambda allocation: allocation.state not in ['confirm', 'refuse']):
raise UserError(_('You cannot delete an allocation request which is in %s state.') % (state_description_values.get(allocation.state),))
@api.ondelete(at_uninstall=False)
def _unlink_if_no_leaves(self):
@@ -669,80 +685,49 @@ class HolidaysAllocation(models.Model):
'accrual_plan_id': self.accrual_plan_id.id,
} for employee in employees]
def action_draft(self):
if any(holiday.state not in ['confirm', 'refuse'] for holiday in self):
raise UserError(_('Allocation request state must be "Refused" or "To Approve" in order to be reset to Draft.'))
self.write({
'state': 'draft',
'approver_id': False,
})
linked_requests = self.mapped('linked_request_ids')
if linked_requests:
linked_requests.action_draft()
linked_requests.unlink()
self.activity_update()
return True
def action_confirm(self):
if self.filtered(lambda holiday: holiday.state != 'draft' and holiday.validation_type != 'no'):
raise UserError(_('Allocation request must be in Draft state ("To Submit") in order to confirm it.'))
validated_holidays = self.filtered(lambda holiday: holiday.state == 'validate')
res = (self - validated_holidays).write({'state': 'confirm'})
self.activity_update()
no_employee_requests = [holiday.id for holiday in self.sudo() if holiday.holiday_status_id.employee_requests == 'no']
self.filtered(lambda holiday: (holiday.id in no_employee_requests or holiday.validation_type == 'no') and holiday.state != 'validate').action_validate()
return res
def action_validate(self):
current_employee = self.env.user.employee_id
no_employee_requests = [holiday.id for holiday in self.sudo() if holiday.holiday_status_id.employee_requests == 'no']
if any((holiday.state != 'confirm' and holiday.id not in no_employee_requests and holiday.validation_type != 'no') for holiday in self):
raise UserError(_('Allocation request must be confirmed in order to approve it.'))
self.write({
'state': 'validate',
'approver_id': current_employee.id
'approver_id': self.env.user.employee_id.id
})
for holiday in self:
holiday._action_validate_create_childs()
self._action_validate_create_childs()
self.activity_update()
return True
def _action_validate_create_childs(self):
childs = self.env['hr.leave.allocation']
# In the case we are in holiday_type `employee` and there is only one employee we can keep the same allocation
# Otherwise we do need to create an allocation for all employees to have a behaviour that is in line
# with the other holiday_type
if self.state == 'validate' and (self.holiday_type in ['category', 'department', 'company'] or
(self.holiday_type == 'employee' and len(self.employee_ids) > 1)):
if self.holiday_type == 'employee':
employees = self.employee_ids
elif self.holiday_type == 'category':
employees = self.category_id.employee_ids
elif self.holiday_type == 'department':
employees = self.department_id.member_ids
else:
employees = self.env['hr.employee'].search([('company_id', '=', self.mode_company_id.id)])
allocation_vals = []
for allocation in self:
# In the case we are in holiday_type `employee` and there is only one employee we can keep the same allocation
# Otherwise we do need to create an allocation for all employees to have a behaviour that is in line
# with the other holiday_type
if allocation.state == 'validate' and (allocation.holiday_type in ['category', 'department', 'company'] or
(allocation.holiday_type == 'employee' and len(allocation.employee_ids) > 1)):
if allocation.holiday_type == 'employee':
employees = allocation.employee_ids
elif allocation.holiday_type == 'category':
employees = allocation.category_id.employee_ids
elif allocation.holiday_type == 'department':
employees = allocation.department_id.member_ids
else:
employees = self.env['hr.employee'].search([('company_id', '=', allocation.mode_company_id.id)])
allocation_create_vals = self._prepare_holiday_values(employees)
childs += self.with_context(
allocation_vals += allocation._prepare_holiday_values(employees)
if allocation_vals:
children = self.env['hr.leave.allocation'].with_context(
mail_notify_force_send=False,
mail_activity_automation_skip=True
).create(allocation_create_vals)
if childs:
childs.action_validate()
return childs
).create(allocation_vals)
children.filtered(lambda c: c.validation_type != 'no').action_validate()
def action_refuse(self):
current_employee = self.env.user.employee_id
if any(holiday.state not in ['confirm', 'validate', 'validate1'] for holiday in self):
if any(allocation.state not in ['confirm', 'validate'] for allocation in self):
raise UserError(_('Allocation request must be confirmed or validated in order to refuse it.'))
days_per_allocation = self.holiday_status_id._get_employees_days_per_allocation(self.employee_id.ids)
days_per_allocation = self.employee_id._get_consumed_leaves(self.holiday_status_id)[0]
for allocation in self:
days_taken = days_per_allocation[allocation.employee_id.id][allocation.holiday_status_id][allocation]['virtual_leaves_taken']
days_taken = days_per_allocation[allocation.employee_id][allocation.holiday_status_id][allocation]['virtual_leaves_taken']
if days_taken > 0:
raise UserError(_('You cannot refuse this allocation request since the employee has already taken leaves for it. Please refuse or delete those leaves first.'))
@@ -763,24 +748,19 @@ class HolidaysAllocation(models.Model):
return
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
for holiday in self:
val_type = holiday.holiday_status_id.sudo().allocation_validation_type
for allocation in self:
val_type = allocation.holiday_status_id.sudo().allocation_validation_type
if state == 'confirm':
continue
if state == 'draft':
if holiday.employee_id != current_employee and not is_manager:
raise UserError(_('Only a time off Manager can reset other people allocation.'))
continue
if not is_officer and self.env.user != holiday.employee_id.leave_manager_id and not val_type == 'no':
if not is_officer and self.env.user != allocation.employee_id.leave_manager_id and not val_type == 'no':
raise UserError(_('Only a time off Officer/Responsible or Manager can approve or refuse time off requests.'))
if is_officer or self.env.user == holiday.employee_id.leave_manager_id:
if is_officer or self.env.user == allocation.employee_id.leave_manager_id:
# use ir.rule based first access check: department, members, ... (see security.xml)
holiday.check_access_rule('write')
allocation.check_access_rule('write')
if holiday.employee_id == current_employee and not is_manager and not val_type == 'no':
if allocation.employee_id == current_employee and not is_manager and not val_type == 'no':
raise UserError(_('Only a time off Manager can approve its own requests.'))
@api.onchange('allocation_type')
@@ -830,9 +810,7 @@ class HolidaysAllocation(models.Model):
count=allocation.number_of_days,
allocation_type=allocation.holiday_status_id.name
)
if allocation.state == 'draft':
to_clean |= allocation
elif allocation.state == 'confirm':
if allocation.state == 'confirm':
if allocation.holiday_status_id.responsible_ids:
user_ids = allocation.sudo()._get_responsible_for_approval().ids
for user_id in user_ids:
@@ -880,7 +858,7 @@ class HolidaysAllocation(models.Model):
if self.state == 'confirm':
app_action = self._notify_get_action_link('controller', controller='/allocation/validate', **local_msg_vals)
hr_actions += [{'url': app_action, 'title': _('Approve')}]
if self.state in ['confirm', 'validate', 'validate1']:
if self.state in ['confirm', 'validate']:
ref_action = self._notify_get_action_link('controller', controller='/allocation/refuse', **local_msg_vals)
hr_actions += [{'url': ref_action, 'title': _('Refuse')}]
@@ -899,8 +877,8 @@ class HolidaysAllocation(models.Model):
def message_subscribe(self, partner_ids=None, subtype_ids=None):
# due to record rule can not allow to add follower and mention on validated leave so subscribe through sudo
if any(state in ['validate', 'validate1'] for state in self.mapped('state')):
if any(state in ['validate'] for state in self.mapped('state')):
self.check_access_rights('read')
self.check_access_rule('read')
return super(HolidaysAllocation, self.sudo()).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
return super(HolidaysAllocation, self).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
return super().message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
+176 -327
View File
@@ -3,18 +3,16 @@
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
import datetime
import logging
import pytz
from collections import defaultdict
from datetime import time, timedelta
from datetime import time, datetime
from odoo import api, fields, models
from odoo.osv import expression
from odoo.tools import format_date
from odoo.tools.translate import _
from odoo.tools.float_utils import float_round
from odoo.addons.resource.models.utils import Intervals
_logger = logging.getLogger(__name__)
@@ -28,31 +26,27 @@ class HolidaysType(models.Model):
def _model_sorting_key(self, leave_type):
remaining = leave_type.virtual_remaining_leaves > 0
taken = leave_type.leaves_taken > 0
return -1*leave_type.sequence, leave_type.employee_requests == 'no' and remaining, leave_type.employee_requests == 'yes' and remaining, taken
return -1 * leave_type.sequence, leave_type.employee_requests == 'no' and remaining, leave_type.employee_requests == 'yes' and remaining, taken
name = fields.Char('Time Off Type', required=True, translate=True)
sequence = fields.Integer(default=100,
help='The type with the smallest sequence is the default value in time off request')
help='The type with the smallest sequence is the default value in time off request')
create_calendar_meeting = fields.Boolean(string="Display Time Off in Calendar", default=True)
color = fields.Integer(string='Color', help="The color selected here will be used in every screen with the time off type.")
icon_id = fields.Many2one('ir.attachment', string='Cover Image', domain="[('res_model', '=', 'hr.leave.type'), ('res_field', '=', 'icon_id')]")
active = fields.Boolean('Active', default=True,
help="If the active field is set to false, it will allow you to hide the time off type without removing it.")
# employee specific computed data
max_leaves = fields.Float(compute='_compute_leaves', string='Maximum Allowed', search='_search_max_leaves',
help='This value is given by the sum of all time off requests with a positive value.')
help='This value is given by the sum of all time off requests with a positive value.')
leaves_taken = fields.Float(
compute='_compute_leaves', string='Time off Already Taken',
help='This value is given by the sum of all time off requests with a negative value.')
remaining_leaves = fields.Float(
compute='_compute_leaves', string='Remaining Time Off',
help='Maximum Time Off Allowed - Time Off Already Taken')
virtual_remaining_leaves = fields.Float(
compute='_compute_leaves', search='_search_virtual_remaining_leaves', string='Virtual Remaining Time Off',
help='Maximum Time Off Allowed - Time Off Already Taken - Time Off Waiting Approval')
virtual_leaves_taken = fields.Float(
compute='_compute_leaves', string='Virtual Time Off Already Taken',
help='Sum of validated and non validated time off requests.')
closest_allocation_to_expire = fields.Many2one('hr.leave.allocation', 'Allocation', compute='_compute_leaves')
allocation_count = fields.Integer(
compute='_compute_allocation_count', string='Allocations')
group_days_leave = fields.Float(
@@ -107,59 +101,64 @@ class HolidaysType(models.Model):
or that don't need an allocation
return [('id', domain_operator, [x['id'] for x in res])]
"""
date_to = self._context.get('default_date_from') or fields.Date.today().strftime('%Y-1-1')
date_from = self._context.get('default_date_to') or fields.Date.today().strftime('%Y-12-31')
date_from = self._context.get('default_date_from') or fields.Date.today().strftime('%Y-1-1')
date_to = self._context.get('default_date_to') or fields.Date.today().strftime('%Y-12-31')
employee_id = self._context.get('default_employee_id', self._context.get('employee_id')) or self.env.user.employee_id.id
if not isinstance(value, bool):
raise ValueError('Invalid value: %s' % (value))
if operator not in ['=', '!=']:
raise ValueError('Invalid operator: %s' % (operator))
new_operator = 'in' if operator == '=' else 'not in'
# '!=' True or '=' False
if (operator == '=') ^ value:
new_operator = 'not in'
# '=' True or '!=' False
else:
new_operator = 'in'
query = '''
SELECT
holiday_status_id
FROM
hr_leave_allocation alloc
WHERE
alloc.employee_id = %s AND
alloc.active = True AND alloc.state = 'validate' AND
(alloc.date_to >= %s OR alloc.date_to IS NULL) AND
alloc.date_from <= %s
'''
leave_types = self.env['hr.leave.allocation'].search([
('employee_id', '=', employee_id),
('state', '=', 'validate'),
('date_from', '<=', date_to),
'|',
('date_to', '>=', date_from),
('date_to', '=', False),
]).holiday_status_id
self._cr.execute(query, (employee_id or None, date_to, date_from))
return [('id', new_operator, leave_types.ids)]
return [('id', new_operator, [x['holiday_status_id'] for x in self._cr.dictfetchall()])]
@api.depends('requires_allocation')
@api.depends('requires_allocation', 'max_leaves', 'virtual_remaining_leaves')
def _compute_valid(self):
date_to = self._context.get('default_date_to', fields.Datetime.today())
date_from = self._context.get('default_date_from', fields.Datetime.today())
date_to = self._context.get('default_date_to', fields.Datetime.today())
employee_id = self._context.get('default_employee_id', self._context.get('employee_id', self.env.user.employee_id.id))
for holiday_type in self:
if holiday_type.requires_allocation == 'yes':
allocation = self.env['hr.leave.allocation'].search([
allocations = self.env['hr.leave.allocation'].search([
('holiday_status_id', '=', holiday_type.id),
('allocation_type', '=', 'accrual'),
('employee_id', '=', employee_id),
('date_from', '<=', date_from),
'|',
('date_to', '>=', date_to),
'&',
('date_to', '=', False),
('date_from', '<=', date_from)])
holiday_type.has_valid_allocation = bool(allocation)
])
allocations = allocations.filtered(lambda alloc:
alloc.allocation_type == 'accrual'
or (alloc.max_leaves > 0 and alloc.virtual_remaining_leaves > 0)
)
holiday_type.has_valid_allocation = bool(allocations)
else:
holiday_type.has_valid_allocation = True
def _search_max_leaves(self, operator, value):
value = float(value)
employee_id = self._get_contextual_employee_id()
employee = self.env['hr.employee']._get_contextual_employee()
leaves = defaultdict(int)
if employee_id:
if employee:
allocations = self.env['hr.leave.allocation'].search([
('employee_id', '=', employee_id),
('employee_id', '=', employee.id),
('state', '=', 'validate')
])
for allocation in allocations:
@@ -205,291 +204,21 @@ class HolidaysType(models.Model):
return [('id', 'in', valid_leave_types.ids)]
def _get_employees_days_per_allocation(self, employee_ids, date=None):
if not date:
date = fields.Date.to_date(self.env.context.get('default_date_from')) or fields.Date.context_today(self)
leaves_domain = [
('employee_id', 'in', employee_ids),
('state', 'in', ['confirm', 'validate1', 'validate']),
('holiday_status_id', 'in', self.ids)
]
if self.env.context.get("ignore_future"):
leaves_domain.append(('date_from', '<=', date))
leaves = self.env['hr.leave'].search(leaves_domain)
allocations = self.env['hr.leave.allocation'].with_context(active_test=False).search([
('employee_id', 'in', employee_ids),
('state', 'in', ['validate']),
('holiday_status_id', 'in', self.ids),
])
# The allocation_employees dictionary groups the allocations based on the employee and the holiday type
# The structure is the following:
# - KEYS:
# allocation_employees
# |--employee_id
# |--holiday_status_id
# - VALUES:
# Intervals with the start and end date of each allocation and associated allocations within this interval
allocation_employees = defaultdict(lambda: defaultdict(list))
### Creation of the allocation intervals ###
for holiday_status_id in allocations.holiday_status_id:
for employee_id in employee_ids:
allocation_intervals = Intervals([(
fields.datetime.combine(allocation.date_from, time.min),
fields.datetime.combine(allocation.date_to or datetime.date.max, time.max),
allocation)
for allocation in allocations.filtered(lambda allocation: allocation.employee_id.id == employee_id and allocation.holiday_status_id == holiday_status_id)])
allocation_employees[employee_id][holiday_status_id] = allocation_intervals
# The leave_employees dictionary groups the leavess based on the employee and the holiday type
# The structure is the following:
# - KEYS:
# leave_employees
# |--employee_id
# |--holiday_status_id
# - VALUES:
# Intervals with the start and end date of each leave and associated leave within this interval
leaves_employees = defaultdict(lambda: defaultdict(list))
leave_intervals = []
### Creation of the leave intervals ###
if leaves:
for holiday_status_id in leaves.holiday_status_id:
for employee_id in employee_ids:
leave_intervals = Intervals([(
fields.datetime.combine(leave.date_from, time.min),
fields.datetime.combine(leave.date_to, time.max),
leave)
for leave in leaves.filtered(lambda leave: leave.employee_id.id == employee_id and leave.holiday_status_id == holiday_status_id)])
leaves_employees[employee_id][holiday_status_id] = leave_intervals
# allocation_days_consumed is a dictionary to map the number of days/hours of leaves taken per allocation
# The structure is the following:
# - KEYS:
# allocation_days_consumed
# |--employee_id
# |--holiday_status_id
# |--allocation
# |--virtual_leaves_taken
# |--leaves_taken
# |--virtual_remaining_leaves
# |--remaining_leaves
# |--max_leaves
# |--closest_allocation_to_expire
# - VALUES:
# Integer representing the number of (virtual) remaining leaves, (virtual) leaves taken or max leaves for each allocation.
# The unit is in hour or days depending on the leave type request unit
allocations_days_consumed = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0))))
company_domain = [('company_id', 'in', list(set(self.env.company.ids + self.env.context.get('allowed_company_ids', []))))]
### Existing leaves assigned to allocations ###
if leaves_employees:
for employee_id, leaves_interval_by_status in leaves_employees.items():
for holiday_status_id in leaves_interval_by_status:
days_consumed = allocations_days_consumed[employee_id][holiday_status_id]
if allocation_employees[employee_id][holiday_status_id]:
allocations = allocation_employees[employee_id][holiday_status_id] & leaves_interval_by_status[holiday_status_id]
available_allocations = self.env['hr.leave.allocation']
for allocation_interval in allocations._items:
available_allocations |= allocation_interval[2]
# Consume the allocations that are close to expiration first
sorted_available_allocations = available_allocations.filtered('date_to').sorted(key='date_to')
sorted_available_allocations += available_allocations.filtered(lambda allocation: not allocation.date_to)
leave_intervals = leaves_interval_by_status[holiday_status_id]._items
sorted_allocations_with_remaining_leaves = self.env['hr.leave.allocation']
for leave_interval in leave_intervals:
leaves = leave_interval[2]
for leave in leaves:
if leave.leave_type_request_unit in ['day', 'half_day']:
leave_duration = leave.number_of_days
leave_unit = 'days'
else:
leave_duration = leave.number_of_hours_display
leave_unit = 'hours'
if holiday_status_id.requires_allocation != 'no':
for available_allocation in sorted_available_allocations:
if (available_allocation.date_to and available_allocation.date_to < leave.date_from.date()) \
or (available_allocation.date_from > leave.date_to.date()):
continue
virtual_remaining_leaves = (available_allocation.number_of_days if leave_unit == 'days' else available_allocation.number_of_hours_display) - allocations_days_consumed[employee_id][holiday_status_id][available_allocation]['virtual_leaves_taken']
max_leaves = min(virtual_remaining_leaves, leave_duration)
days_consumed[available_allocation]['virtual_leaves_taken'] += max_leaves
if leave.state == 'validate':
days_consumed[available_allocation]['leaves_taken'] += max_leaves
leave_duration -= max_leaves
# Check valid allocations with still availabe leaves on it
if days_consumed[available_allocation]['virtual_remaining_leaves'] > 0 and available_allocation.date_to and available_allocation.date_to > date:
sorted_allocations_with_remaining_leaves |= available_allocation
if leave_duration > 0:
# There are not enough allocation for the number of leaves
days_consumed[False]['virtual_remaining_leaves'] -= leave_duration
else:
days_consumed[False]['virtual_leaves_taken'] += leave_duration
if leave.state == 'validate':
days_consumed[False]['leaves_taken'] += leave_duration
# no need to sort the allocations again
allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire'] = sorted_allocations_with_remaining_leaves[0] if sorted_allocations_with_remaining_leaves else False
# Future available leaves
future_allocations_date_from = fields.datetime.combine(date, time.min)
future_allocations_date_to = fields.datetime.combine(date, time.max) + timedelta(days=5*365)
for employee_id, allocation_intervals_by_status in allocation_employees.items():
employee = self.env['hr.employee'].browse(employee_id)
for holiday_status_id, intervals in allocation_intervals_by_status.items():
if not intervals:
continue
future_allocation_intervals = intervals & Intervals([(
future_allocations_date_from,
future_allocations_date_to,
self.env['hr.leave'])])
search_date = date
closest_allocations = self.env['hr.leave.allocation']
for interval in intervals._items:
closest_allocations |= interval[2]
allocations_with_remaining_leaves = self.env['hr.leave.allocation']
for interval_from, interval_to, interval_allocations in future_allocation_intervals._items:
if interval_from.date() > search_date:
continue
interval_allocations = interval_allocations.filtered('active')
if not interval_allocations:
continue
# If no end date to the allocation, consider the number of days remaining as infinite
employee_quantity_available = (
employee._get_work_days_data_batch(interval_from, interval_to, compute_leaves=False, domain=company_domain)[employee_id]
if interval_to != future_allocations_date_to
else {'days': float('inf'), 'hours': float('inf')}
)
for allocation in interval_allocations:
if allocation.date_from > search_date:
continue
days_consumed = allocations_days_consumed[employee_id][holiday_status_id][allocation]
if allocation.type_request_unit in ['day', 'half_day']:
quantity_available = employee_quantity_available['days']
remaining_days_allocation = (allocation.number_of_days - days_consumed['virtual_leaves_taken'])
else:
quantity_available = employee_quantity_available['hours']
remaining_days_allocation = (allocation.number_of_hours_display - days_consumed['virtual_leaves_taken'])
if quantity_available <= remaining_days_allocation:
search_date = interval_to.date() + timedelta(days=1)
days_consumed['virtual_remaining_leaves'] += min(quantity_available, remaining_days_allocation)
days_consumed['max_leaves'] = allocation.number_of_days if allocation.type_request_unit in ['day', 'half_day'] else allocation.number_of_hours_display
days_consumed['remaining_leaves'] = days_consumed['max_leaves'] - days_consumed['leaves_taken']
if remaining_days_allocation >= quantity_available:
break
# Check valid allocations with still availabe leaves on it
if days_consumed['virtual_remaining_leaves'] > 0 and allocation.date_to and allocation.date_to > date:
allocations_with_remaining_leaves |= allocation
allocations_sorted = sorted(allocations_with_remaining_leaves, key=lambda a: a.date_to)
allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire'] = allocations_sorted[0] if allocations_sorted else False
return allocations_days_consumed
def get_employees_days(self, employee_ids, date=None):
result = {
employee_id: {
leave_type.id: {
'max_leaves': 0,
'leaves_taken': 0,
'remaining_leaves': 0,
'virtual_remaining_leaves': 0,
'virtual_leaves_taken': 0,
'closest_allocation_to_expire': False,
} for leave_type in self
} for employee_id in employee_ids
}
if not date:
date = fields.Date.to_date(self.env.context.get('default_date_from')) or fields.Date.context_today(self)
allocations_days_consumed = self._get_employees_days_per_allocation(employee_ids, date)
leave_keys = ['max_leaves', 'leaves_taken', 'remaining_leaves', 'virtual_remaining_leaves', 'virtual_leaves_taken']
for employee_id in allocations_days_consumed:
for holiday_status_id in allocations_days_consumed[employee_id]:
for allocation in allocations_days_consumed[employee_id][holiday_status_id]:
if allocation:
if allocation.date_to and (allocation.date_to < date or allocation.date_from > date):
continue
for leave_key in leave_keys:
result[employee_id][holiday_status_id if isinstance(holiday_status_id, int) else holiday_status_id.id][leave_key] += allocations_days_consumed[employee_id][holiday_status_id][allocation][leave_key]
else:
result[employee_id][holiday_status_id if isinstance(holiday_status_id, int) else holiday_status_id.id]['closest_allocation_to_expire'] = allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire']
for leave_key in leave_keys:
if allocations_days_consumed[employee_id][holiday_status_id][False].get(leave_key):
result[employee_id][holiday_status_id if isinstance(holiday_status_id, int) else holiday_status_id.id][leave_key] = allocations_days_consumed[employee_id][holiday_status_id][False][leave_key]
return result
@api.model
def get_days_all_request(self):
leave_types = sorted(self.search([]).filtered(lambda x: ((x.virtual_remaining_leaves > 0 or x.max_leaves))), key=self._model_sorting_key, reverse=True)
return [lt._get_days_request() for lt in leave_types]
def _get_days_request(self):
self.ensure_one()
result = self._get_employees_days_per_allocation(self.closest_allocation_to_expire.employee_id.ids)
closest_allocation_remaining = 0
if self.closest_allocation_to_expire:
# Shows the sum of allocation expiring on the same day as the closest to expire
employee_allocations = result[self.closest_allocation_to_expire.employee_id.id][self].items()
closest_allocation_remaining = sum(
res['virtual_remaining_leaves']
for alloc, res in employee_allocations
if alloc and alloc.date_to == self.closest_allocation_to_expire.date_to
)
return (self.name, {
'remaining_leaves': ('%.2f' % self.remaining_leaves).rstrip('0').rstrip('.'),
'virtual_remaining_leaves': ('%.2f' % self.virtual_remaining_leaves).rstrip('0').rstrip('.'),
'max_leaves': ('%.2f' % self.max_leaves).rstrip('0').rstrip('.'),
'leaves_taken': ('%.2f' % self.leaves_taken).rstrip('0').rstrip('.'),
'virtual_leaves_taken': ('%.2f' % self.virtual_leaves_taken).rstrip('0').rstrip('.'),
'leaves_requested': ('%.2f' % (self.max_leaves - self.virtual_remaining_leaves - self.leaves_taken)).rstrip('0').rstrip('.'),
'leaves_approved': ('%.2f' % self.leaves_taken).rstrip('0').rstrip('.'),
'closest_allocation_remaining': ('%.2f' % closest_allocation_remaining).rstrip('0').rstrip('.'),
'closest_allocation_expire': format_date(self.env, self.closest_allocation_to_expire.date_to) if self.closest_allocation_to_expire.date_to else False,
'request_unit': self.request_unit,
'icon': self.sudo().icon_id.url,
'id': self.id,
}, self.requires_allocation, self.id)
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.user.employee_id.id
return employee_id
@api.depends_context('employee_id', 'default_employee_id')
@api.depends_context('employee_id', 'default_employee_id', 'default_date_from')
def _compute_leaves(self):
data_days = {}
employee_id = self._get_contextual_employee_id()
if employee_id:
data_days = (self.get_employees_days(employee_id)[employee_id[0]] if isinstance(employee_id, list) else
self.get_employees_days([employee_id])[employee_id])
employee = self.env['hr.employee']._get_contextual_employee()
date = self._context['default_date_from'] if 'default_date_from' in self._context else None
data_days = self.get_allocation_data(employee, date)[employee]
for holiday_status in self:
result = data_days.get(holiday_status.id, {})
holiday_status.max_leaves = result.get('max_leaves', 0)
holiday_status.leaves_taken = result.get('leaves_taken', 0)
holiday_status.remaining_leaves = result.get('remaining_leaves', 0)
holiday_status.virtual_remaining_leaves = result.get('virtual_remaining_leaves', 0)
holiday_status.virtual_leaves_taken = result.get('virtual_leaves_taken', 0)
holiday_status.closest_allocation_to_expire = result.get('closest_allocation_to_expire', 0)
result = [item for item in data_days if item[0] == holiday_status.name]
leave_type_tuple = result[0] if result else ('', {})
holiday_status.max_leaves = leave_type_tuple[1].get('max_leaves', 0)
holiday_status.leaves_taken = leave_type_tuple[1].get('leaves_taken', 0)
holiday_status.virtual_remaining_leaves = leave_type_tuple[1].get('virtual_remaining_leaves', 0)
def _compute_allocation_count(self):
min_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
max_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
min_datetime = fields.Datetime.to_string(datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
max_datetime = fields.Datetime.to_string(datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
domain = [
('holiday_status_id', 'in', self.ids),
('date_from', '>=', min_datetime),
@@ -507,8 +236,8 @@ class HolidaysType(models.Model):
allocation.allocation_count = grouped_dict.get(allocation.id, 0)
def _compute_group_days_leave(self):
min_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
max_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
min_datetime = fields.Datetime.to_string(datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
max_datetime = fields.Datetime.to_string(datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
domain = [
('holiday_status_id', 'in', self.ids),
('date_from', '>=', min_datetime),
@@ -570,8 +299,8 @@ 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()
if order == self._order and employee_id:
employee = self.env['hr.employee']._get_contextual_employee()
if order == self._order and employee:
# retrieve all leaves, sort them, then apply offset and limit
leaves = self.browse(super()._search(domain, access_rights_uid=access_rights_uid))
leaves = leaves.sorted(key=self._model_sorting_key, reverse=True)
@@ -582,8 +311,6 @@ class HolidaysType(models.Model):
def action_see_days_allocated(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_allocation_action_all")
date_from = fields.Datetime.to_string(
datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
action['domain'] = [
('holiday_status_id', 'in', self.ids),
]
@@ -616,3 +343,125 @@ class HolidaysType(models.Model):
'default_time_off_type_id': self.id,
}
return action
# ------------------------------------------------------------
# Leave - Allocation link methods
# ------------------------------------------------------------
@api.model
def get_allocation_data_request(self, date=None):
leave_types = self.search([
'|',
('company_id', 'in', self.env.context.get('allowed_company_ids')),
('company_id', '=', False),
])
employee = self.env['hr.employee']._get_contextual_employee()
if employee:
return leave_types.get_allocation_data(employee, date)[employee]
return []
def get_allocation_data(self, employees, date=None):
allocation_data = defaultdict(list)
if date and isinstance(date, str):
date = datetime.fromisoformat(date).date()
elif date and isinstance(date, datetime):
date = date.date()
elif not date:
date = fields.Date.today()
allocations_leaves_consumed, extra_data = employees._get_consumed_leaves(self, date)
leave_type_requires_allocation = self.filtered(lambda lt: lt.requires_allocation == 'yes')
for employee in employees:
for leave_type in leave_type_requires_allocation:
if len(allocations_leaves_consumed[employee][leave_type]) == 0:
continue
lt_info = (
leave_type.name,
{
'remaining_leaves': 0,
'virtual_remaining_leaves': 0,
'max_leaves': 0,
'accrual_bonus': 0,
'leaves_taken': 0,
'virtual_leaves_taken': 0,
'leaves_requested': 0,
'leaves_approved': 0,
'closest_allocation_remaining': 0,
'closest_allocation_expire': False,
'holds_changes': False,
'excess_days': extra_data[employee][leave_type]['excess_days'],
'exceeding_duration': extra_data[employee][leave_type]['exceeding_duration'],
'request_unit': leave_type.request_unit,
'icon': leave_type.sudo().icon_id.url,
'has_accrual_allocation': False,
},
leave_type.requires_allocation,
leave_type.id)
allocations_now = self.env['hr.leave.allocation']
allocations_date = self.env['hr.leave.allocation']
allocations_with_remaining_leaves = self.env['hr.leave.allocation']
for allocation, data in allocations_leaves_consumed[employee][leave_type].items():
# We only need the allocation that are valid at the given date
if allocation:
if allocation.allocation_type == 'accrual':
lt_info[1]['has_accrual_allocation'] = True
today = date.today()
if allocation.date_from <= today and (not allocation.date_to or allocation.date_to >= today):
# we get each allocation available now to indicate visually if
# the future evaluation holds changes compared to now
allocations_now |= allocation
if allocation.date_from <= date and (not allocation.date_to or allocation.date_to >= date):
# we get each allocation available now to indicate visually if
# the future evaluation holds changes compared to now
allocations_date |= allocation
if allocation.date_from > date:
continue
if allocation.date_to and allocation.date_to < date:
continue
lt_info[1]['remaining_leaves'] += data['remaining_leaves']
lt_info[1]['virtual_remaining_leaves'] += data['virtual_remaining_leaves']
lt_info[1]['max_leaves'] += data['max_leaves']
lt_info[1]['accrual_bonus'] += data['accrual_bonus']
lt_info[1]['leaves_taken'] += data['leaves_taken']
lt_info[1]['virtual_leaves_taken'] += data['virtual_leaves_taken']
lt_info[1]['leaves_requested'] += data['virtual_leaves_taken']
lt_info[1]['leaves_approved'] += data['leaves_taken']
if data['virtual_remaining_leaves'] > 0:
allocations_with_remaining_leaves |= allocation
closest_allocation = allocations_with_remaining_leaves[0] if allocations_with_remaining_leaves else self.env['hr.leave.allocation']
closest_allocations = allocations_with_remaining_leaves.filtered(lambda a: a.date_to == closest_allocation.date_to)
closest_allocation_remaining = 0
for closest_allocation in closest_allocations:
closest_allocation_remaining += allocations_leaves_consumed[employee][leave_type][closest_allocation]['virtual_remaining_leaves']
if closest_allocation.date_to:
closest_allocation_expire = format_date(self.env, closest_allocation.date_to)
# closest_allocation_duration corresponds to the time remaining before the allocation expires
closest_allocation_duration =\
employee.resource_calendar_id._attendance_intervals_batch(
datetime.combine(closest_allocation.date_to, time.min).replace(tzinfo=pytz.UTC),
datetime.combine(date, time.max).replace(tzinfo=pytz.UTC))\
if leave_type.request_unit in ['hour']\
else (closest_allocation.date_to - date).days + 1
else:
closest_allocation_expire = False
closest_allocation_duration = False
# the allocations are assumed to be different from today's allocations if there is any
# accrual days granted or if there is any difference between allocations now and on the selected date
holds_changes = (lt_info[1]['accrual_bonus'] > 0
or bool(allocations_date - allocations_now)
or bool(allocations_now - allocations_date))\
and date != fields.Date.today()
lt_info[1].update({
'closest_allocation_remaining': closest_allocation_remaining,
'closest_allocation_expire': closest_allocation_expire,
'closest_allocation_duration': closest_allocation_duration,
'holds_changes': holds_changes,
})
allocation_data[employee].append(lt_info)
for employee in allocation_data:
for leave_type_data in allocation_data[employee]:
for key, value in leave_type_data[1].items():
if isinstance(value, float):
leave_type_data[1][key] = round(value, 2)
return allocation_data
+2 -2
View File
@@ -68,14 +68,14 @@ class CalendarLeaves(models.Model):
for previous_duration, leave, state in zip(previous_durations, leaves, previous_states):
duration_difference = previous_duration - leave.number_of_days
message = False
if duration_difference > 0 and leave['holiday_allocation_id']:
if duration_difference > 0 and leave.holiday_status_id.requires_allocation == 'yes':
message = _("Due to a change in global time offs, you have been granted %s day(s) back.", duration_difference)
if leave.number_of_days > previous_duration\
and leave.holiday_status_id not in sick_time_status:
message = _("Due to a change in global time offs, %s extra day(s) have been taken from your allocation. Please review this leave if you need it to be changed.", -1 * duration_difference)
try:
leave.write({'state': state})
leave._check_holidays()
leave._check_validity()
except ValidationError:
leave.action_refuse()
message = _("Due to a change in global time offs, this leave no longer has the required amount of available allocation and has been set to refused. Please review this leave.")
@@ -1,19 +1,22 @@
/* @odoo-module */
import { usePopover } from "@web/core/popover/popover_hook";
import { useNewAllocationRequest } from "@hr_holidays/views/hooks";
import { Component } from "@odoo/owl";
import { formatNumber, useNewAllocationRequest } from "@hr_holidays/views/hooks";
import { Component, onWillRender } from "@odoo/owl";
export class TimeOffCardPopover extends Component {}
TimeOffCardPopover.template = "hr_holidays.TimeOffCardPopover";
TimeOffCardPopover.props = [
"allocated",
"accrual_bonus",
"approved",
"planned",
"left",
"employeeId",
"holidayStatusId",
"warning",
"closest",
"request_unit",
"exceeding_duration",
"close?",
"onClickNewAllocationRequest?",
];
@@ -21,21 +24,37 @@ TimeOffCardPopover.props = [
export class TimeOffCard extends Component {
setup() {
this.popover = usePopover(TimeOffCardPopover, {
position: "right",
position: "bottom",
popoverClass: "bg-view",
});
this.newAllocationRequest = useNewAllocationRequest();
this.lang = this.env.services.user.lang;
this.formatNumber = formatNumber;
this.updateWarning();
onWillRender(this.updateWarning);
}
updateWarning() {
const { data } = this.props;
const closeExpire =
data.closest_allocation_duration &&
data.closest_allocation_duration < data.virtual_remaining_leaves;
this.warning = data.exceeding_duration || closeExpire;
}
onClickInfo(ev) {
const { data } = this.props;
this.popover.open(ev.target, {
allocated: data.max_leaves,
approved: data.leaves_approved,
planned: data.leaves_requested,
left: data.virtual_remaining_leaves,
employeeId: this.props.employeeId,
holidayStatusId: this.props.holidayStatusId,
allocated: formatNumber(this.lang, data.max_leaves),
accrual_bonus: formatNumber(this.lang, data.accrual_bonus),
approved: formatNumber(this.lang, data.leaves_approved),
planned: formatNumber(this.lang, data.leaves_requested),
left: formatNumber(this.lang, data.virtual_remaining_leaves),
warning: this.warning,
closest: data.closest_allocation_duration,
request_unit: data.request_unit,
exceeding_duration: data.exceeding_duration,
onClickNewAllocationRequest: this.newAllocationRequestFrom.bind(this),
});
}
@@ -1,13 +1,20 @@
.o_timeoff_card {
flex-direction: column;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: start;
flex: 1;
text-align: center;
&:not(:last-child) {
border-right: 2px solid $border-color;
}
&>* {
width: fit-content;
margin-left: auto;
margin-right: auto;
}
span {
line-height: 1;
}
@@ -32,17 +39,23 @@
font-size: 10px;
}
.o_timeoff_details {
width: fit-content;
margin: 0 auto;
cursor: pointer;
}
.o_timeoff_info {
display: inline-block;
margin-right: -10px;
span {
cursor: pointer;
padding-right: 10px;
}
padding-right: 10px;
}
}
.o_time_off_icon_types img {
filter: var(--timeOffCard-icon-filter);
}
.o_time_off_card_popover_warning {
max-width: 300px;
}
@@ -3,6 +3,7 @@
<div t-name="hr_holidays.TimeOffCard" class="o_timeoff_card py-3 text-odoo">
<t t-set="data" t-value="props.data"/>
<t t-set="duration" t-value="props.requires_allocation ? data.virtual_remaining_leaves : data.virtual_leaves_taken"/>
<t t-set="parsed_duration" t-value="formatNumber(this.lang, duration)"/>
<t t-set="show_popover" t-value="true"/>
<strong class="o_timeoff_name"><t t-esc="props.name"/></strong>
@@ -10,9 +11,13 @@
<t t-if="data and data.icon">
<img t-att-src="data.icon" />
</t>
<t t-esc="duration"/>
<span t-att-style="data.holds_changes ? 'color: #B6D369;' : ''">
<t t-esc="parsed_duration"/>
</span>
</span>
<div class="text-uppercase">
<span
t-att-class="'text-uppercase o_timeoff_details p-1' + (warning ? ' alert alert-warning' : '')"
t-on-click.stop="(ev) => (show_popover &amp;&amp; this.onClickInfo(ev))">
<t t-if="data.request_unit == 'hour'" name="duration_unit">hours</t>
<t t-else="">days</t>
<t t-if="props.requires_allocation" name="duration_type">
@@ -21,12 +26,10 @@
<t t-else="">
taken
</t>
<t t-if="show_popover">
<div class="o_timeoff_info">
<span class="fa fa-question-circle-o" t-on-click.stop="onClickInfo"/>
</div>
</t>
</div>
<span t-if="show_popover"
t-att-class="'o_timeoff_info fa' + (warning ? ' fa-exclamation-triangle' : ' fa-question-circle-o')"
/>
</span>
<span t-if="props.requires_allocation and data.closest_allocation_expire !== false" class="text-uppercase o_timeoff_validity">
<t t-if="data.closest_allocation_remaining != data.virtual_remaining_leaves">
(<t t-esc="data.closest_allocation_remaining"/> <t t-if="data.request_unit == 'hour'">hours</t><t t-else="">days</t>
@@ -54,10 +57,30 @@
<t t-name="hr_holidays.TimeOffCardPopover">
<ul class="list-unstyled p-3 mb-0">
<li class="d-flex justify-content-between">Allocated (<span class="btn-link p-0 cursor-pointer" t-on-click="props.onClickNewAllocationRequest">new request</span>): <span class="ps-1" t-esc="props.allocated"/></li>
<li class="d-flex justify-content-between">
<span>Allocated (<span class="btn-link p-0 cursor-pointer" t-on-click="props.onClickNewAllocationRequest">new request</span>):</span>
<span class="ps-1" t-esc="props.allocated"/>
</li>
<li class="d-flex justify-content-between" t-if="props.accrual_bonus">
Accrual (Future): <span class="ps-1" t-esc="props.accrual_bonus"/>
</li>
<li class="d-flex justify-content-between">Approved: <span class="ps-1" t-esc="props.approved"/></li>
<li class="d-flex justify-content-between border-bottom">Planned: <span class="ps-1" t-esc="props.planned"/></li>
<li class="d-flex justify-content-between">Available: <span class="ps-1" t-esc="props.left"/></li>
</ul>
<div t-if="props.warning" class="alert alert-warning mb-0 o_time_off_card_popover_warning">
<span class="m-0 mt-3"
t-if="props.closest &amp;&amp; props.closest &lt; props.left">
<i class="fa fa-warning"/> Only <t t-esc="props.closest"/>
<t t-if="props.request_unit == 'hour'"> hours</t>
<t t-else=""> days</t>
can be used before the allocation expires.
</span>
<span class="m-0 mt-3"
t-else="">
The leaves planned in the future are exceeding the maximum value of the allocation.
It will not be possible to take all of them.
</span>
</div>
</t>
</templates>
@@ -3,6 +3,7 @@
import { TimeOffCard } from "./time_off_card";
import { useNewAllocationRequest } from "@hr_holidays/views/hooks";
import { useBus, useService } from "@web/core/utils/hooks";
import { DateTimeInput } from "@web/core/datetime/datetime_input";
import { Component, useState, onWillStart } from "@odoo/owl";
export class TimeOffDashboard extends Component {
@@ -10,6 +11,8 @@ export class TimeOffDashboard extends Component {
this.orm = useService("orm");
this.newRequest = useNewAllocationRequest();
this.state = useState({
date: luxon.DateTime.now(),
today: luxon.DateTime.now(),
holidays: [],
});
useBus(this.env.timeOffBus, "update_dashboard", async () => {
@@ -21,21 +24,38 @@ export class TimeOffDashboard extends Component {
});
}
async loadDashboardData() {
async loadDashboardData(date = false) {
const context = {};
if (this.props.employeeId !== null) {
if (this.props && this.props.employeeId !== null) {
context["employee_id"] = this.props.employeeId;
}
this.state.holidays = await this.orm.call("hr.leave.type", "get_days_all_request", [], {
context: context,
});
if (date) {
this.state.date = date;
}
this.state.holidays = await this.orm.call(
"hr.leave.type",
"get_allocation_data_request",
[this.state.date],
{
context: context,
}
);
}
async newAllocationRequest() {
await this.newRequest(this.props.employeeId);
}
resetDate() {
this.state.date = luxon.DateTime.now();
this.loadDashboardData();
}
has_accrual_allocation() {
return this.state.holidays.some((leave_type) => leave_type[1]["has_accrual_allocation"]);
}
}
TimeOffDashboard.components = { TimeOffCard };
TimeOffDashboard.components = { TimeOffCard, DateTimeInput };
TimeOffDashboard.template = "hr_holidays.TimeOffDashboard";
TimeOffDashboard.props = ["employeeId"];
@@ -1,6 +1,16 @@
.o_timeoff_today_button {
border: none;
width: fit-content;
background: none;
transition: all 0.2s;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
.o_timeoff_dashboard {
display: flex;
justify-content: space-between;
flex-direction: row;
height: auto;
box-shadow: inset 0 -1px 0 $border-color;
@@ -9,3 +19,11 @@
z-index: 100;
background-color: $o-webclient-background-color;
}
.o_timeoff_dashboard_cards {
display: flex;
flex-direction: row;
& > * {
flex: 1;
}
}
@@ -2,13 +2,32 @@
<templates xml:space="preserve">
<div t-name="hr_holidays.TimeOffDashboard" class="o_timeoff_dashboard">
<t t-foreach="state.holidays" t-as="holiday" t-key="holiday[3]">
<TimeOffCard name="holiday[0]" data="holiday[1]" requires_allocation="holiday[2] == 'yes'" holidayStatusId="holiday[3]" employeeId="props.employeeId"/>
<TimeOffCard
name="holiday[0]"
data="holiday[1]"
requires_allocation="holiday[2] == 'yes'"
holidayStatusId="holiday[3]"
employeeId="props.employeeId"/>
</t>
<div class="o_timeoff_card py-3 text-odoo d-flex flex-grow-0 justify-content-center">
<button class="btn btn-link p-4" t-on-click="newAllocationRequest" type="button">
<t t-if="employeeId">Grant Time</t>
<t t-else="">New Allocation Request</t>
</button>
<div class="o_timeoff_card p-0 d-flex justify-content-around">
<div class="row justify-content-center align-items-center border-bottom h-50 w-100 p-1" t-if="has_accrual_allocation()">
Balance at the
<div class="p-1" style="max-width: 100px!important">
<DateTimeInput
type="'date'"
value="state.date"
onChange="(date) => this.loadDashboardData(date)"
minDate="state.today"
placeholder="'Today'"/>
</div>
<button class="o_timeoff_today_button btn btn-secondary" t-on-click="resetDate">Today</button>
</div>
<div class="row justify-content-center align-items-center h-50 w-100">
<button class="btn btn-secondary m-auto" t-on-click="newAllocationRequest" type="button" style="width: fit-content;">
<t t-if="employeeId">Grant Time</t>
<t t-else="">New Allocation Request</t>
</button>
</div>
</div>
</div>
</templates>
@@ -60,7 +60,7 @@ export class TimeOffCalendarFilterPanel extends CalendarFilterPanel {
}
const filterData = {};
const data = await this.orm.call("hr.leave.type", "get_days_all_request", []);
const data = await this.orm.call("hr.leave.type", "get_allocation_data_request", []);
data.forEach((leave) => {
filterData[leave[3]] = leave;
@@ -45,7 +45,7 @@
<t t-if="env.isSmall">
<t t-set="holiday" t-value="leaveState.holidays[filter.value]"/>
<TimeOffCardMobile t-if="holiday" name="holiday[0]" id="holiday[3]" data="holiday[1]" requires_allocation="holiday[2] === 'yes'" />
<TimeOffCardMobile t-if="holiday" name="holiday[0]" id="holiday[3]" data="holiday[1]" requires_allocation="holiday[2] === 'yes'"/>
</t>
</span>
</xpath>
+53 -40
View File
@@ -2,16 +2,26 @@
import { _t } from "@web/core/l10n/translation";
import { useService, useOwnedDialogs } from "@web/core/utils/hooks";
import { FormViewDialog } from '@web/views/view_dialogs/form_view_dialog';
import { useComponent } from '@odoo/owl';
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
import { useComponent } from "@odoo/owl";
export function formatNumber(lang, number, maxDecimals = 2) {
const userLang = lang.split("_").join("-");
const numberFormat = new Intl.NumberFormat(userLang, { maximumFractionDigits: maxDecimals });
return numberFormat.format(number);
}
export function useMandatoryDays(props) {
return (info) => {
const date = luxon.DateTime.fromJSDate(info.date).toISODate();
const mandatoryDay = props.model.mandatoryDays[date];
if (mandatoryDay) {
const dayNumberElTop = info.view.el.querySelector(`.fc-day-top[data-date="${info.el.dataset.date }"]`)
const dayNumberEl = info.view.el.querySelector(`.fc-day[data-date="${info.el.dataset.date }"]`)
const dayNumberElTop = info.view.el.querySelector(
`.fc-day-top[data-date="${info.el.dataset.date}"]`
);
const dayNumberEl = info.view.el.querySelector(
`.fc-day[data-date="${info.el.dataset.date}"]`
);
if (dayNumberElTop) {
dayNumberElTop.classList.add(`hr_mandatory_day_top_${mandatoryDay}`);
}
@@ -20,52 +30,55 @@ export function useMandatoryDays(props) {
}
}
return props.model.mandatoryDays;
}
};
}
export function useLeaveCancelWizard() {
const action = useService('action');
const action = useService("action");
return (leaveId, callback) => {
action.doAction({
name: _t('Delete Confirmation'),
type: "ir.actions.act_window",
res_model: "hr.holidays.cancel.leave",
target: "new",
views: [[false, "form"]],
context: {
default_leave_id: leaveId,
action.doAction(
{
name: _t("Delete Confirmation"),
type: "ir.actions.act_window",
res_model: "hr.holidays.cancel.leave",
target: "new",
views: [[false, "form"]],
context: {
default_leave_id: leaveId,
},
},
{
onClose: callback,
}
}, {
onClose: callback,
});
}
);
};
}
export function useNewAllocationRequest() {
const addDialog = useOwnedDialogs();
const component = useComponent();
return async (employeeId, holidayStatusId) => {
const context = {
'form_view_ref': 'hr_holidays.hr_leave_allocation_view_form_dashboard',
'is_employee_allocation': true,
'created_from_dashboard': true,
};
if (employeeId) {
context['default_employee_id'] = employeeId;
context['default_employee_ids'] = [employeeId];
context['form_view_ref'] = 'hr_holidays.hr_leave_allocation_view_form_manager_dashboard';
}
if (holidayStatusId) {
context['default_holiday_status_id'] = holidayStatusId;
}
addDialog(FormViewDialog, {
resModel: 'hr.leave.allocation',
title: _t('New Allocation'),
context: context,
onRecordSaved: () => {
component.env.timeOffBus.trigger('update_dashboard');
}
});
}
const context = {
form_view_ref: "hr_holidays.hr_leave_allocation_view_form_dashboard",
is_employee_allocation: true,
};
if (employeeId) {
context["default_employee_id"] = employeeId;
context["default_employee_ids"] = [employeeId];
context["form_view_ref"] =
"hr_holidays.hr_leave_allocation_view_form_manager_dashboard";
}
if (holidayStatusId) {
context["default_holiday_status_id"] = holidayStatusId;
}
addDialog(FormViewDialog, {
resModel: "hr.leave.allocation",
title: _t("New Allocation"),
context: context,
onRecordSaved: () => {
component.env.timeOffBus.trigger("update_dashboard");
},
});
};
}
+1 -1
View File
@@ -23,7 +23,7 @@ class TestHrHolidaysCommon(common.TransactionCase):
cls.user_hrmanager_id = cls.user_hrmanager.id
cls.user_hrmanager.tz = 'Europe/Brussels'
cls.user_employee = mail_new_test_user(cls.env, login='david', groups='base.group_user')
cls.user_employee = mail_new_test_user(cls.env, login='enguerran', password='enguerran', groups='base.group_user')
cls.user_employee_id = cls.user_employee.id
# Hr Data
@@ -63,7 +63,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
@@ -102,7 +101,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
@@ -158,7 +156,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
@@ -198,7 +195,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'date_from': '2021-09-03',
})
with freeze_time(datetime.date.today() + relativedelta(days=2)):
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
@@ -245,7 +241,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'date_from': '2021-09-03',
})
self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00')
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
@@ -289,7 +284,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'date_from': '2021-08-31',
})
self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00')
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
@@ -328,7 +322,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
})
self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00')
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
@@ -372,7 +365,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
})
self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00')
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
@@ -532,7 +524,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
tomorrow = datetime.date.today() + relativedelta(days=2)
@@ -573,7 +564,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
tomorrow = datetime.date.today() + relativedelta(days=2)
@@ -623,7 +613,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
next_date = datetime.date.today() + relativedelta(days=11)
second_level = self.env['hr.leave.accrual.level'].search([('accrual_plan_id', '=', accrual_plan.id), ('start_count', '=', 10)])
@@ -660,7 +649,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
next_date = datetime.date.today() + relativedelta(days=11)
second_level = self.env['hr.leave.accrual.level'].search([('accrual_plan_id', '=', accrual_plan.id), ('start_count', '=', 10)])
@@ -690,7 +678,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 10,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
# Reset the cron's lastcall
@@ -725,7 +712,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 10,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
# Reset the cron's lastcall
@@ -759,7 +745,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
# Reset the cron's lastcall
@@ -796,7 +781,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 10,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
# Reset the cron's lastcall
@@ -830,7 +814,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
# Reset the cron's lastcall
@@ -874,7 +857,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': datetime.date(2020, 8, 16),
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time('2022-1-10'):
allocation._update_accrual()
@@ -923,7 +905,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': datetime.date(2022, 1, 31),
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time('2022-7-20'):
allocation._update_accrual()
@@ -970,7 +951,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': datetime.date(2021, 1, 1),
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time('2022-4-4'):
allocation._update_accrual()
@@ -1003,7 +983,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': datetime.date(2019, 1, 1),
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time('2022-4-1'):
@@ -1035,7 +1014,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
})
with freeze_time("2021-10-3"):
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
@@ -1065,7 +1043,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
})
with freeze_time("2021-10-3"):
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
@@ -1095,7 +1072,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': '2022-01-01',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2022-3-2"):
@@ -1140,7 +1116,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': '2022-01-01',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time(datetime.date(2022, 4, 1)):
@@ -1186,7 +1161,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': '2023-04-24',
})
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
@@ -1204,7 +1178,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': '2023-04-24',
})
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
@@ -1236,7 +1209,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': '2023-04-13',
})
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
@@ -1283,7 +1255,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': '2023-04-26',
})
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 1, "Should accrue 1 day, at the start of the period.")
@@ -1323,7 +1294,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': '2023-04-26',
})
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 0, "Should accrue 0 days, days are added on 27th.")
@@ -1373,7 +1343,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': '2023-04-20',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2024-04-20"):
@@ -1437,7 +1406,6 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'allocation_type': 'accrual',
'date_from': '2023-04-4',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2026-08-01"):
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import tests
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
from odoo.exceptions import AccessError, UserError
import time
@@ -85,11 +84,7 @@ class TestAccessRightsSimpleUser(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_employee.id, values)
self.assertEqual(allocation.state, 'draft')
allocation.action_confirm()
self.assertEqual(allocation.state, 'confirm', "It should be confirmed")
allocation.action_draft()
self.assertEqual(allocation.state, 'draft', "It should have been reset to draft")
self.assertEqual(allocation.state, 'confirm', "The allocation should be in 'confirm' state")
class TestAccessRightsEmployeeManager(TestAllocationRights):
@@ -118,7 +113,6 @@ class TestAccessRightsEmployeeManager(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_employee.id, values)
allocation.action_confirm()
allocation.action_validate()
self.assertEqual(allocation.state, 'validate', "The allocation should be validated")
@@ -129,7 +123,6 @@ class TestAccessRightsEmployeeManager(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_employee.id, values)
allocation.action_confirm()
allocation.action_refuse()
self.assertEqual(allocation.state, 'refuse', "The allocation should be validated")
@@ -162,7 +155,6 @@ class TestAccessRightsHolidayUser(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_hruser.id, values)
allocation.action_confirm()
allocation.action_validate()
self.assertEqual(allocation.state, 'validate', "It should have been validated")
@@ -183,7 +175,6 @@ class TestAccessRightsHolidayUser(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_hruser.id, values)
allocation.action_confirm()
with self.assertRaises(UserError):
allocation.action_validate()
@@ -197,7 +188,6 @@ class TestAccessRightsHolidayManager(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_hrmanager.id, values)
allocation.action_confirm()
allocation.action_validate()
self.assertEqual(allocation.state, 'validate', "It should have been validated")
@@ -208,7 +198,6 @@ class TestAccessRightsHolidayManager(TestAllocationRights):
'holiday_status_id': self.lt_validation_manager.id,
}
allocation = self.request_allocation(self.user_hrmanager.id, values)
allocation.action_confirm()
allocation.action_validate()
self.assertEqual(allocation.state, 'validate', "It should have been validated")
allocation.action_refuse()
@@ -82,7 +82,6 @@ class TestCompanyLeave(TransactionCase):
self.assertEqual(all_leaves[3].number_of_days, 1)
self.assertEqual(all_leaves[3].state, 'validate')
def test_leave_whole_company_02(self):
# TEST CASE 2: Leaves taken in half-days. Take a 3 days leave
# Add a company leave on the second day
@@ -12,21 +12,20 @@ from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tagged('post_install', '-at_install', 'holiday_calendar')
class TestHolidaysCalendar(HttpCase, TestHrHolidaysCommon):
@users('admin')
@users('enguerran')
def test_hours_time_off_request_calendar_view(self):
"""
Testing the flow of clicking on a day, save the leave request directly
and verify that the start/end time are correctly set.
"""
self.env.user.tz = 'UTC'
first_day_of_year = date(date.today().year, 1, 1)
days_to_thursday = (3 - first_day_of_year.weekday()) % 7
first_thursday_of_year = first_day_of_year + timedelta(days=days_to_thursday)
leave = self.env['hr.leave'].new({
'name': 'Reference Holiday',
'employee_id': self.env.user.employee_id.id,
'employee_id': self.employee_emp.id,
'request_date_from': first_thursday_of_year,
'request_date_to': first_thursday_of_year,
})
@@ -35,9 +34,9 @@ class TestHolidaysCalendar(HttpCase, TestHrHolidaysCommon):
expected_leave_end = leave.date_to.hour
# Tour that takes a leave on the first thursday of the year.
self.start_tour('/', 'time_off_request_calendar_view', login='admin')
self.start_tour('/', 'time_off_request_calendar_view', login='enguerran')
last_leave = self.env['hr.leave'].search([('employee_id.id', '=', self.env.user.employee_id.id)]).sorted(lambda leave: leave.create_date)[-1]
last_leave = self.env['hr.leave'].search([('employee_id.id', '=', self.employee_emp.id)]).sorted(lambda leave: leave.create_date)[-1]
self.assertEqual(last_leave.date_from.weekday(), 3, "It should be Thursday")
self.assertEqual(last_leave.date_from.hour, expected_leave_start, "Wrong start of the day")
self.assertEqual(last_leave.date_to.hour, expected_leave_end, "Wrong end of the day")
+13 -12
View File
@@ -8,7 +8,6 @@ from freezegun import freeze_time
from psycopg2 import IntegrityError
from odoo import Command
from odoo.exceptions import AccessError, ValidationError, UserError
from odoo.tools import date_utils, mute_logger, test_reports
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@@ -75,7 +74,6 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
hol12_manager_group.action_approve()
self.assertEqual(hol1_user_group.state, 'validate', 'hr_holidays: validates leave request should be in validate state')
@mute_logger('odoo.addons.base.models.ir_model', 'odoo.models')
def test_01_leave_request_flow_limited(self):
""" Testing leave request flow: limited type of leave request """
@@ -111,14 +109,15 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
}
]).action_validate()
def _check_holidays_status(holiday_status, ml, lt, rl, vrl):
self.assertEqual(holiday_status.max_leaves, ml,
def _check_holidays_status(holiday_status, employee, ml, lt, rl, vrl):
result = holiday_status.get_allocation_data(employee)[employee][0][1]
self.assertEqual(result['max_leaves'], ml,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.leaves_taken, lt,
self.assertEqual(result['leaves_taken'], lt,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.remaining_leaves, rl,
self.assertEqual(result['remaining_leaves'], rl,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.virtual_remaining_leaves, vrl,
self.assertEqual(result['virtual_remaining_leaves'], vrl,
'hr_holidays: wrong type days computation')
# HrManager creates some holiday statuses
@@ -148,12 +147,13 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
'date_from': time.strftime('%Y-%m-01'),
})
# HrUser validates the first step
self.env.flush_all()
# HrManager validates the second step
aloc1_user_group.with_user(self.user_hrmanager_id).action_validate()
# Checks Employee has effectively some days left
hol_status_2_employee_group = self.holidays_status_limited.with_user(self.user_employee_id)
_check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 2.0)
_check_holidays_status(hol_status_2_employee_group, self.employee_emp, 2.0, 0.0, 2.0, 2.0)
# Employee creates a leave request in the limited category, now that he has some days left
hol2 = HolidaysEmployeeGroup.create({
@@ -163,10 +163,11 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
'request_date_from': (date.today() + relativedelta(days=2)),
'request_date_to': (date.today() + relativedelta(days=2)),
})
self.env.flush_all()
hol2_user_group = hol2.with_user(self.user_hruser_id)
# Check left days: - 1 virtual remaining day
hol_status_2_employee_group.invalidate_model()
_check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 1.0)
_check_holidays_status(hol_status_2_employee_group, self.employee_emp, 2.0, 0.0, 2.0, 1.0)
# HrManager validates the second step
hol2_user_group.with_user(self.user_hrmanager_id).action_validate()
@@ -174,7 +175,7 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
'hr_holidays: second validation should lead to validate state')
# Check left days: - 1 day taken
hol_status_2_employee_group.invalidate_model(['max_leaves', 'leaves_taken'])
_check_holidays_status(hol_status_2_employee_group, 2.0, 1.0, 1.0, 1.0)
_check_holidays_status(hol_status_2_employee_group, self.employee_emp, 2.0, 1.0, 1.0, 1.0)
# HrManager finds an error: he refuses the leave request
hol2.with_user(self.user_hrmanager_id).action_refuse()
@@ -183,7 +184,7 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
# Check left days: 2 days left again
hol_status_2_employee_group.invalidate_model(['max_leaves'])
_check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 2.0)
_check_holidays_status(hol_status_2_employee_group, self.employee_emp, 2.0, 0.0, 2.0, 2.0)
self.assertEqual(hol2.state, 'refuse',
'hr_holidays: hr_user should not be able to reset a refused leave request')
@@ -218,7 +219,7 @@ class TestHolidaysFlow(TestHrHolidaysCommon):
hol3.action_validate()
self.assertEqual(hol3.state, 'validate', 'hr_holidays: validation should lead to validate state')
# Check left days for casual leave: 19 days left
_check_holidays_status(hol3_status, 20.0, 1.0, 19.0, 19.0)
_check_holidays_status(hol3_status, self.env['hr.employee'].browse(employee_id), 20.0, 1.0, 19.0, 19.0)
def test_10_leave_summary_reports(self):
# Print the HR Holidays(Summary Employee) Report through the wizard
@@ -32,7 +32,7 @@ class TestHrHolidaysTour(HttpCase):
'leave_validation_type': 'hr',
})
# add allocation
allocation = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': 'Expired Allocation',
'employee_id': admin_employee.id,
'holiday_status_id': holidays_type_1.id,
@@ -41,6 +41,5 @@ class TestHrHolidaysTour(HttpCase):
'date_from': '2022-01-01',
'date_to': '2022-12-31',
})
allocation.action_validate()
self.start_tour('/web', 'hr_holidays_tour', login="admin")
+122 -91
View File
@@ -20,15 +20,20 @@ from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
@tagged('leave_requests')
class TestLeaveRequests(TestHrHolidaysCommon):
def _check_holidays_status(self, holiday_status, ml, lt, rl, vrl):
self.assertEqual(holiday_status.max_leaves, ml,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.leaves_taken, lt,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.remaining_leaves, rl,
'hr_holidays: wrong type days computation')
self.assertEqual(holiday_status.virtual_remaining_leaves, vrl,
'hr_holidays: wrong type days computation')
def _check_holidays_status(self, holiday_status, employee, ml, lt, rl, vrl):
result = holiday_status.get_allocation_data(employee)[employee][0][1]
self.assertEqual(
result['max_leaves'], ml,
'hr_holidays: wrong type days computation')
self.assertEqual(
result['leaves_taken'], lt,
'hr_holidays: wrong type days computation')
self.assertEqual(
result['remaining_leaves'], rl,
'hr_holidays: wrong type days computation')
self.assertEqual(
result['virtual_remaining_leaves'], vrl,
'hr_holidays: wrong type days computation')
@classmethod
def setUpClass(cls):
@@ -70,19 +75,12 @@ class TestLeaveRequests(TestHrHolidaysCommon):
cls.set_employee_create_date(cls.employee_emp_id, '2010-02-03 00:00:00')
cls.set_employee_create_date(cls.employee_hruser_id, '2010-02-03 00:00:00')
def _check_holidays_count(self, holidays_count_result, ml, lt, rl, vrl, vlt, closest_allocation):
self.assertEqual(
holidays_count_result,
{
'closest_allocation_to_expire': closest_allocation,
'max_leaves': ml,
'leaves_taken': lt,
'remaining_leaves': rl,
'virtual_remaining_leaves': vrl,
'virtual_leaves_taken': vlt,
}
)
def _check_holidays_count(self, holidays_count_result, ml, lt, rl, vrl, vlt):
self.assertEqual(holidays_count_result['max_leaves'], ml)
self.assertEqual(holidays_count_result['remaining_leaves'], rl)
self.assertEqual(holidays_count_result['virtual_remaining_leaves'], vrl)
self.assertEqual(holidays_count_result['leaves_taken'], lt)
self.assertEqual(holidays_count_result['virtual_leaves_taken'], vlt)
@classmethod
def set_employee_create_date(cls, _id, newdate):
@@ -117,11 +115,47 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'request_date_to': datetime.today(),
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_limited_type_days_left(self):
""" Employee creates a leave request in a limited category and has enough days left """
def test_limited_type_not_enough_days(self):
with freeze_time('2022-01-05'):
aloc1_user_group = self.env['hr.leave.allocation'].with_user(self.user_hruser_id).create({
self.env['hr.leave.allocation'].with_user(self.user_hruser_id).create({
'name': 'Days for limited category',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'number_of_days': 2,
'state': 'confirm',
'date_from': time.strftime('%Y-1-1'),
'date_to': time.strftime('%Y-12-31'),
})
# Employee cannot take a leave longer than the allocation
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Invalid Hol21',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': time.strftime('2022-02-01'),
'request_date_to': time.strftime('2022-02-04'),
})
# A leave cannot be modified so that it's longer than the allocation
valid_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Valid Hol21',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
'request_date_from': time.strftime('2022-02-02'),
'request_date_to': time.strftime('2022-02-03'),
})
with self.assertRaises(ValidationError):
valid_leave.write({
'request_date_from': time.strftime('2022-02-01'),
'request_date_to': time.strftime('2022-02-05'),
})
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_limited_type_days_left(self):
""" Employee creates a leave request in a limited category and has enough days left """
with freeze_time('2022-01-05'):
self.env['hr.leave.allocation'].with_user(self.user_hruser_id).create({
'name': 'Days for limited category',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
@@ -130,10 +164,9 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': time.strftime('%Y-1-1'),
'date_to': time.strftime('%Y-12-31'),
})
aloc1_user_group.action_validate()
holiday_status = self.holidays_type_2.with_user(self.user_employee_id)
self._check_holidays_status(holiday_status, 2.0, 0.0, 2.0, 2.0)
self._check_holidays_status(holiday_status, self.employee_emp, 2.0, 0.0, 2.0, 2.0)
hol = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Hol11',
@@ -144,25 +177,26 @@ class TestLeaveRequests(TestHrHolidaysCommon):
})
holiday_status.invalidate_model()
self._check_holidays_status(holiday_status, 2.0, 0.0, 2.0, 0.0)
self._check_holidays_status(holiday_status, self.employee_emp, 2.0, 0.0, 2.0, 0.0)
hol.with_user(self.user_hrmanager_id).action_approve()
holiday_status.invalidate_model(['max_leaves'])
self._check_holidays_status(holiday_status, 2.0, 2.0, 0.0, 0.0)
self._check_holidays_status(holiday_status, self.employee_emp, 2.0, 2.0, 0.0, 0.0)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_accrual_validity_time_valid(self):
""" Employee ask leave during a valid validity time """
self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).create({
'name': 'Sick Time Off',
'holiday_status_id': self.holidays_type_2.id,
'employee_id': self.employee_emp.id,
'date_from': fields.Datetime.from_string('2017-01-01 00:00:00'),
'date_to': fields.Datetime.from_string('2017-06-01 00:00:00'),
'number_of_days': 10,
}).action_validate()
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).create({
'name': 'Sick Time Off',
'holiday_status_id': self.holidays_type_2.id,
'employee_id': self.employee_emp.id,
'date_from': fields.Datetime.from_string('2017-01-01 00:00:00'),
'date_to': fields.Datetime.from_string('2017-06-01 00:00:00'),
'number_of_days': 10,
})
allocation.action_validate()
self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Valid time period',
@@ -197,7 +231,7 @@ class TestLeaveRequests(TestHrHolidaysCommon):
allocation_form.date_from = date(2019, 5, 6)
allocation_form.date_to = date(2019, 5, 6)
allocation_form.name = 'New Allocation Request'
allocation = allocation_form.save()
allocation_form.save()
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_employee_is_absent(self):
@@ -327,7 +361,7 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'request_unit': 'hour',
'leave_validation_type': 'both',
})
allocation = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': '20 days allocation',
'holiday_status_id': leave_type.id,
'number_of_days': 20,
@@ -336,7 +370,6 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': time.strftime('2018-1-1'),
'date_to': time.strftime('%Y-1-1'),
})
allocation.action_validate()
leave1 = self.env['hr.leave'].create({
'name': 'Holiday 1 week',
@@ -459,7 +492,7 @@ class TestLeaveRequests(TestHrHolidaysCommon):
self._test_leave_with_tz(tz, local_date_from, local_date_to, 6)
def test_expired_allocation(self):
allocation = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': 'Expired Allocation',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
@@ -468,7 +501,6 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': '2020-01-01',
'date_to': '2020-12-31',
})
allocation.action_validate()
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
@@ -488,7 +520,7 @@ class TestLeaveRequests(TestHrHolidaysCommon):
def test_no_days_expired(self):
# First expired allocation
allocation1 = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': 'Expired Allocation',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
@@ -497,7 +529,7 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': '2020-01-01',
'date_to': '2020-12-31',
})
allocation2 = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': 'Expired Allocation',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
@@ -506,8 +538,6 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': '2021-01-01',
'date_to': '2021-12-31',
})
allocation1.action_validate()
allocation2.action_validate()
# Try creating a request that could be validated if allocation1 was still valid
with self.assertRaises(ValidationError):
self.env['hr.leave'].with_user(self.user_employee_id).create({
@@ -528,7 +558,7 @@ class TestLeaveRequests(TestHrHolidaysCommon):
def test_company_leaves(self):
# First expired allocation
allocation = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': 'Allocation',
'holiday_type': 'company',
'mode_company_id': self.env.company.id,
@@ -537,7 +567,6 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'state': 'confirm',
'date_from': '2021-01-01',
})
allocation.action_validate()
req1_form = Form(self.env['hr.leave'].sudo())
req1_form.employee_ids.add(self.employee_emp)
@@ -588,11 +617,8 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': '2022-01-01',
'date_to': '2022-12-31',
}
allocation1 = self.env['hr.leave.allocation'].create(allocation_vals)
allocation2 = self.env['hr.leave.allocation'].create(allocation_vals)
allocation1.action_validate()
allocation2.action_validate()
self.env['hr.leave.allocation'].create(allocation_vals)
self.env['hr.leave.allocation'].create(allocation_vals)
# Able to create a leave of 10 days with two allocations of 5 days
self.env['hr.leave'].with_user(self.user_employee_id).create({
@@ -628,7 +654,6 @@ class TestLeaveRequests(TestHrHolidaysCommon):
allocation_vals.update({'number_of_days': 1})
allocation_1day = Allocation.create(allocation_vals)
allocations = (allocation_4days + allocation_1day)
allocations.action_validate()
leave_vals.update({
'request_date_from': '2022-01-03',
@@ -649,10 +674,16 @@ class TestLeaveRequests(TestHrHolidaysCommon):
leaves = (leave_4days + leave_1day)
leaves.action_approve()
allocation_days = self.holidays_type_2._get_employees_days_per_allocation([self.employee_emp_id])
allocation_days = self.employee_emp._get_consumed_leaves(self.holidays_type_2)[0]
self.assertEqual(allocation_days[self.employee_emp_id][self.holidays_type_2][allocation_4days]['leaves_taken'], leave_4days['number_of_%ss_display' % unit], 'As 4 days were available in this allocation, they should have been taken')
self.assertEqual(allocation_days[self.employee_emp_id][self.holidays_type_2][allocation_1day]['leaves_taken'], leave_1day['number_of_%ss_display' % unit], 'As no days were available in previous allocation, they should have been taken in this one')
self.assertEqual(
allocation_days[self.employee_emp][self.holidays_type_2][allocation_4days]['leaves_taken'],
leave_4days['number_of_%ss_display' % unit],
'As 4 days were available in this allocation, they should have been taken')
self.assertEqual(
allocation_days[self.employee_emp][self.holidays_type_2][allocation_1day]['leaves_taken'],
leave_1day['number_of_%ss_display' % unit],
'As no days were available in previous allocation, they should have been taken in this one')
leaves.action_refuse()
allocations.action_refuse()
@@ -771,7 +802,6 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': '2020-01-01',
'date_to': '2020-12-31',
})
allocation.action_validate()
leave1 = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
'employee_id': self.employee_emp_id,
@@ -781,8 +811,8 @@ class TestLeaveRequests(TestHrHolidaysCommon):
})
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=5, lt=0, rl=5, vrl=2, vlt=3, closest_allocation=allocation,
self.employee_emp._get_consumed_leaves(self.holidays_type_2)[0][self.employee_emp][self.holidays_type_2][allocation],
ml=5, lt=0, rl=5, vrl=2, vlt=3,
)
self.holidays_type_2.requires_allocation = 'no'
@@ -797,8 +827,8 @@ class TestLeaveRequests(TestHrHolidaysCommon):
# The 5 allocation days are not consumed anymore
# virtual_remaining_leaves reflect the total number of leave days taken
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=5, lt=0, rl=5, vrl=5, vlt=6, closest_allocation=allocation,
self.employee_emp._get_consumed_leaves(self.holidays_type_2)[0][self.employee_emp][self.holidays_type_2][False],
ml=0, lt=0, rl=0, vrl=0, vlt=6,
)
leave1.with_user(self.user_hrmanager_id).action_approve()
@@ -806,8 +836,8 @@ class TestLeaveRequests(TestHrHolidaysCommon):
# leaves_taken and virtual_leaves_taken reflect the total number of leave days taken
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=5, lt=6, rl=5, vrl=5, vlt=6, closest_allocation=allocation,
self.employee_emp._get_consumed_leaves(self.holidays_type_2)[0][self.employee_emp][self.holidays_type_2][False],
ml=0, lt=6, rl=0, vrl=0, vlt=6,
)
def test_archived_allocation(self):
@@ -821,7 +851,7 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': '2021-06-01',
'date_to': '2021-12-31',
})
allocation_2022 = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': 'Annual Time Off 2022',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_2.id,
@@ -830,8 +860,6 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': '2022-01-01',
'date_to': '2022-12-31',
})
allocation_2021.action_validate()
allocation_2022.action_validate()
# Leave taken in 2021
leave_2021 = self.env['hr.leave'].with_user(self.user_employee_id).create({
@@ -846,14 +874,15 @@ class TestLeaveRequests(TestHrHolidaysCommon):
# The holidays count only takes into account the valid allocations at that date
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id], date=date(2021, 12, 1))[self.employee_emp_id][self.holidays_type_2.id],
ml=10, lt=5, rl=5, vrl=5, vlt=5, closest_allocation=allocation_2021,
self.holidays_type_2.get_allocation_data(self.employee_emp, date=date(2021, 12, 1))[self.employee_emp][0][1],
ml=10, lt=5, rl=5, vrl=5, vlt=5,
)
# Virtual remaining leave is equal to 1 because there is only one day remaining in the allocation based on its validity
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id], date=date(2021, 12, 31))[self.employee_emp_id][self.holidays_type_2.id],
ml=10, lt=5, rl=5, vrl=1, vlt=5, closest_allocation=allocation_2022,
# Days remaining before the allocation ends is equal to 1 because there is only one day remaining in the allocation based on its validity
self.assertEqual(
self.holidays_type_2.get_allocation_data(self.employee_emp, date=date(2021, 12, 31))[self.employee_emp][0][1]['closest_allocation_duration'],
1,
"Only one day should remain before the allocation expires"
)
leave_2022 = self.env['hr.leave'].with_user(self.user_employee_id).create({
@@ -868,14 +897,14 @@ class TestLeaveRequests(TestHrHolidaysCommon):
# The holidays count in 2022 is not affected by the first leave taken in 2021
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=20, lt=4, rl=16, vrl=16, vlt=4, closest_allocation=allocation_2022,
self.holidays_type_2.get_allocation_data(self.employee_emp)[self.employee_emp][0][1],
ml=20, lt=4, rl=16, vrl=16, vlt=4,
)
# The holidays count in 2021 is not affected by the leave taken in 2022
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id], date=date(2021, 12, 1))[self.employee_emp_id][self.holidays_type_2.id],
ml=10, lt=5, rl=5, vrl=5, vlt=5, closest_allocation=allocation_2021,
self.holidays_type_2.get_allocation_data(self.employee_emp, date=date(2021, 12, 1))[self.employee_emp][0][1],
ml=10, lt=5, rl=5, vrl=5, vlt=5,
)
with self.assertRaisesRegex(UserError,
@@ -887,37 +916,40 @@ class TestLeaveRequests(TestHrHolidaysCommon):
allocation_2021.active = False
# If the allocation is archived, the leaves taken are still counted on this allocation
# but the max leaves and remaining leaves are not counted anymore
# If there are no virtual_remaining_leaves, then there is no upcoming allocation (closest_allocation_to_expire) to expire
# If the allocation is archived, the leaves taken are not taken into account anymore
# As there is no valid allocation, then there is no upcoming one (closest_allocation_to_expire)
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id], date=date(2021, 12, 1))[self.employee_emp_id][self.holidays_type_2.id],
ml=0, lt=5, rl=0, vrl=0, vlt=5, closest_allocation=False,
self.holidays_type_2.get_allocation_data(self.employee_emp, date=date(2021, 12, 1))[self.employee_emp][0][1],
ml=0, lt=0, rl=0, vrl=0, vlt=0,
)
self.assertEqual(
self.holidays_type_2.get_allocation_data(self.employee_emp, date=date(2021, 12, 1))[self.employee_emp][0][1]['closest_allocation_expire'],
False,
)
# The holidays count in 2022 is not affected by the archived allocation in 2021
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=20, lt=4, rl=16, vrl=16, vlt=4, closest_allocation=allocation_2022,
self.holidays_type_2.get_allocation_data(self.employee_emp)[self.employee_emp][0][1],
ml=20, lt=4, rl=16, vrl=16, vlt=4,
)
allocation_2021.active = True
# The holidays count in 2021 is back to what it was when the allocation was active
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id], date=date(2021, 12, 1))[self.employee_emp_id][self.holidays_type_2.id],
ml=10, lt=5, rl=5, vrl=5, vlt=5, closest_allocation=allocation_2021,
self.holidays_type_2.get_allocation_data(self.employee_emp, date=date(2021, 12, 1))[self.employee_emp][0][1],
ml=10, lt=5, rl=5, vrl=5, vlt=5,
)
# The holidays count in 2022 is still not affected by the allocation in 2021
self._check_holidays_count(
self.holidays_type_2.get_employees_days([self.employee_emp_id])[self.employee_emp_id][self.holidays_type_2.id],
ml=20, lt=4, rl=16, vrl=16, vlt=4, closest_allocation=allocation_2022,
self.holidays_type_2.get_allocation_data(self.employee_emp)[self.employee_emp][0][1],
ml=20, lt=4, rl=16, vrl=16, vlt=4,
)
def test_cancel_leave(self):
with freeze_time('2020-09-15'):
allocation = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': 'Annual Time Off',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_4.id,
@@ -926,7 +958,6 @@ class TestLeaveRequests(TestHrHolidaysCommon):
'date_from': '2020-01-01',
'date_to': '2020-12-31',
})
allocation.action_validate()
leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
'name': 'Holiday Request',
@@ -19,7 +19,7 @@
('date_to', '=', False),
('date_to', '&gt;=', context_today().strftime('%Y-%m-%d')),
]"
help="Active Allocations"/>
help="Active Allocations"/>
<separator/>
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]" groups="mail.group_mail_notification_type_inbox"/>
<separator/>
@@ -66,63 +66,75 @@
<field name="priority">32</field>
<field name="arch" type="xml">
<form string="Allocation Request">
<field name="can_reset" invisible="1"/>
<field name="can_approve" invisible="1"/>
<field name="holiday_type" invisible="1" readonly="state not in ['confirm', 'draft']"/>
<field name="holiday_type" invisible="1" readonly="state not in ['confirm']"/>
<header>
<button string="Confirm" name="action_confirm" invisible="state != 'draft' and 'allocation_type' != 'accrual' and not employee_id" type="object" class="oe_highlight"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,validate"/>
<field name="state" widget="statusbar" statusbar_visible="confirm,validate"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
</div>
<div class="oe_button_box" name="button_box"/>
<div id="title" class="oe_title">
<h2><field name="name" class="w-100" placeholder="e.g. Time Off type (From validity start to validity end / no limit)" invisible="state not in ('draft', 'confirm')" readonly="True"/>
<field name="name_validity" invisible="state in ('draft', 'confirm')"/></h2>
<h2><field name="name" class="w-100"
placeholder="e.g. Time Off type (From validity start to validity end / no limit)"
invisible="state != 'confirm'" readonly="True"/>
<field name="name_validity" invisible="state == 'confirm'"/></h2>
</div>
<group>
<group>
<field name="type_request_unit" invisible="1"/>
<field name="has_accrual_plan" invisible="1"/>
<field name="holiday_status_id" context="{'employee_id':employee_id, 'default_date_from':current_date, 'request_type':'allocation'}" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']"/>
<field name="allocation_type" widget="radio"
<field name="holiday_status_id"
context="{'employee_id':employee_id, 'default_date_from':current_date, 'request_type':'allocation'}"
readonly="state == 'validate'"/>
<field name="allocation_type"
widget="radio"
invisible="1"
readonly="not is_officer or state not in ('draft', 'confirm')"/>
readonly="not is_officer or state == 'validate'"/>
<field name="is_officer" invisible="1"/>
<field name="accrual_plan_id" invisible="allocation_type == 'regular'" readonly="not is_officer or state not in ('draft', 'confirm')"/>
<field name="accrual_plan_id"
invisible="allocation_type == 'regular'"
required="allocation_type == 'accrual'"
readonly="not is_officer or state == 'validate'"/>
<div class="o_td_label" name="validity_label">
<label for="date_from" string="Validity Period" invisible="allocation_type == 'accrual' or state not in ('draft', 'confirm')"/>
<label for="date_from" string="Validity Period"
invisible="allocation_type == 'accrual' or state != 'confirm'"/>
<label for="date_from" string="Start Date" invisible="allocation_type == 'regular'"/>
</div>
<div class="o_row" name="validity">
<field name="date_from" widget="date" nolabel="1" readonly="1" invisible="allocation_type == 'regular' and state not in ('draft', 'confirm')"/>
<i class="fa fa-long-arrow-right mx-2" aria-label="Arrow icon" title="Arrow" invisible="allocation_type == 'accrual' or state not in ('draft', 'confirm')"/>
<label class="mx-2" for="date_to" string="Run until" invisible="allocation_type == 'regular'"/>
<field name="date_to" widget="date" nolabel="1" placeholder="No Limit" readonly="1" invisible="allocation_type == 'regular' and state not in ('draft', 'confirm')"/>
<div id="no_limit_label" class="oe_read_only" invisible="not id or date_to or state not in ('draft', 'confirm')">No limit</div>
<field name="date_from" widget="date" nolabel="1" readonly="1"
invisible="allocation_type == 'regular' and state != 'confirm'"/>
<i class="fa fa-long-arrow-right mx-2" aria-label="Arrow icon" title="Arrow"
invisible="allocation_type == 'accrual' or state != 'confirm'"/>
<label class="mx-2" for="date_to" string="Run until"
invisible="allocation_type == 'regular'"/>
<field name="date_to" widget="date" nolabel="1"
placeholder="No Limit" readonly="1"
invisible="allocation_type == 'regular' and state != 'confirm'"/>
<div id="no_limit_label" class="oe_read_only"
invisible="not id or date_to or state != 'confirm'">No limit</div>
</div>
<field name="number_of_days" invisible="1"/>
<div class="o_td_label">
<label for="number_of_days_display" string="Allocation"
readonly="allocation_type == 'accrual' and state not in ('draft', 'confirm')"/>
readonly="allocation_type == 'accrual' and state == 'validate'"/>
</div>
<div name="duration_display">
<field name="number_of_days_display" nolabel="1" style="width: 5rem;"
invisible="type_request_unit == 'hour'"
readonly="state not in ('draft', 'confirm')"/>
readonly="state == 'validate'"/>
<field name="number_of_hours_display" nolabel="1" style="width: 5rem;"
invisible="type_request_unit != 'hour'"
readonly="state not in ('draft', 'confirm')"/>
readonly="state == 'validate'"/>
<span class="ml8" invisible="type_request_unit == 'hour'">Days</span>
<span class="ml8" invisible="type_request_unit != 'hour'">Hours</span>
</div>
</group>
<group name="alloc_right_col">
<field name="employee_id" invisible="1" readonly="state in ['cancel', 'refuse', 'validate']"/>
<field name="department_id" invisible="1" readonly="state not in ['confirm', 'draft']"/>
<field name="employee_id" invisible="1" readonly="state in ['refuse', 'validate']"/>
<field name="department_id" invisible="1" readonly="state not in ['confirm']"/>
</group>
</group>
<field name="notes" nolabel="1" placeholder="Add a reason..." readonly="state not in ['confirm', 'draft']"/>
<field name="notes" nolabel="1" placeholder="Add a reason..." readonly="state not in ['confirm']"/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
@@ -140,74 +152,73 @@
<field name="mode">primary</field>
<field name="priority">16</field>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_confirm']" position="after">
<button string="Validate" name="action_validate" invisible="state != 'confirm'" type="object" class="oe_highlight"/>
<field name="state" position="before">
<button string="Validate" name="action_validate"
invisible="state == 'validate'" type="object" class="oe_highlight"/>
<button string="Refuse" name="action_refuse" type="object"
invisible="not can_approve or state not in ('confirm', 'validate')"/>
<button string="Mark as Draft" name="action_draft" type="object"
invisible="not can_reset or state not in ['confirm', 'refuse']"/>
</xpath>
<xpath expr="//div[@id='title']" position="replace">
</field>
<div id="title" position="replace">
<div class="oe_title">
<h2><field name="name" placeholder="e.g. Time Off type (From validity start to validity end / no limit)" required="1"/></h2>
</div>
</xpath>
<xpath expr="//field[@name='employee_id']" position="before">
<field name="holiday_type" string="Mode" groups="hr_holidays.group_hr_holidays_user" context="{'employee_id':employee_id}" readonly="state not in ['confirm', 'draft']"/>
</xpath>
<xpath expr="//field[@name='employee_id']" position="replace">
</div>
<field name="employee_id" position="before">
<field name="holiday_type" string="Mode" groups="hr_holidays.group_hr_holidays_user" context="{'employee_id':employee_id}" readonly="state not in ['confirm']"/>
</field>
<field name="employee_id" position="replace">
<!-- :TestLeaveRequests.test_allocation_request -->
<field name="employee_id" invisible="1" readonly="state in ['cancel', 'refuse', 'validate']"/>
<field name="employee_id" invisible="1" readonly="state in ['refuse', 'validate']"/>
<field name="multi_employee" invisible="1" force_save="1"/>
<!-- Employee id is only visible if the allocation is created specifically for that user in `_action_validate_create_childs` -->
<field name="employee_id" groups="hr_holidays.group_hr_holidays_user"
invisible="holiday_type != 'employee' or not employee_id or state in ('draft', 'cancel', 'confirm')"
readonly="state in ['cancel', 'refuse', 'validate']"
invisible="holiday_type != 'employee' or not employee_id or state == 'confirm'"
readonly="state in ['refuse', 'validate']"
widget="many2one_avatar_employee"
options="{'relation': 'hr.employee.public'}"/>
<field name="employee_ids" widget="many2many_avatar_employee"
groups="hr_holidays.group_hr_holidays_user"
invisible="holiday_type != 'employee' or (state not in ('draft', 'cancel', 'confirm') and employee_id)"
readonly="state in ['cancel', 'refuse', 'validate']"
required="holiday_type == 'employee' and state in ('draft', 'cancel', 'confirm')"
invisible="holiday_type != 'employee' or state != 'confirm' and employee_id"
readonly="state in ['refuse', 'validate']"
required="holiday_type == 'employee' and state == 'confirm'"
options="{'relation': 'hr.employee.public'}"/>
</xpath>
<xpath expr="//field[@name='employee_id']" position="after">
</field>
<field name="employee_id" position="after">
<field name="category_id"
invisible="holiday_type != 'category'"
readonly="state in ['cancel', 'refuse', 'validate']"
readonly="state in ['refuse', 'validate']"
required="holiday_type == 'category'"/>
</xpath>
<xpath expr="//field[@name='department_id']" position="replace">
</field>
<field name="department_id" position="replace">
<field name="department_id" groups="hr_holidays.group_hr_holidays_user"
invisible="holiday_type != 'department'"
readonly="state not in ['confirm', 'draft']"
readonly="state not in ['confirm']"
required="holiday_type == 'department'"/>
</xpath>
<xpath expr="//field[@name='department_id']" position="after">
</field>
<field name="department_id" position="after">
<field name="mode_company_id" string="Company" groups="hr_holidays.group_hr_holidays_user"
invisible="holiday_type != 'company'"
readonly="state in ['cancel', 'refuse', 'validate']"
readonly="state in ['refuse', 'validate']"
required="holiday_type == 'company'"/>
</xpath>
<xpath expr="//field[@name='allocation_type']" position="attributes">
</field>
<field name="allocation_type" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//label[@for='date_from']" position="replace">
</field>
<label for="date_from" position="replace">
<label for="date_from" string="Validity Period" invisible="allocation_type == 'accrual'"/>
</xpath>
<xpath expr="//field[@name='date_from']" position="replace">
<field name="date_from" widget="date" nolabel="1" readonly="allocation_type == 'accrual' and state not in ('draft', 'confirm')"/>
</xpath>
</label>
<field name="date_from" position="replace">
<field name="date_from" widget="date" nolabel="1" readonly="allocation_type == 'accrual' and state != 'confirm'"/>
</field>
<xpath expr="//i[hasclass('fa-long-arrow-right')]" position="replace">
<i class="fa fa-long-arrow-right mx-2" aria-label="Arrow icon" title="Arrow" invisible="allocation_type == 'accrual'"/>
</xpath>
<xpath expr="//field[@name='date_to']" position="replace">
<field name="date_to" widget="date" nolabel="1" placeholder="No Limit" readonly="allocation_type == 'accrual' and state not in ('draft', 'confirm')"/>
</xpath>
<xpath expr="//div[@id='no_limit_label']" position="replace">
<field name="date_to" position="replace">
<field name="date_to" widget="date" nolabel="1" placeholder="No Limit" readonly="allocation_type == 'accrual' and state != 'confirm'"/>
</field>
<div id="no_limit_label" position="replace">
<div id="no_limit_label" class="oe_read_only" invisible="not id or date_to">No limit</div>
</xpath>
</div>
</field>
</record>
@@ -237,7 +248,7 @@
</field>
</record>
<record id="hr_leave_allocation_view_form_manager_dashboard" model="ir.ui.view">
<record id="hr_leave_allocation_view_form_manager_dashboard" model="ir.ui.view">
<field name="name">hr.leave.allocation.view.form.manager.dashboard</field>
<field name="model">hr.leave.allocation</field>
<field name="inherit_id" ref="hr_holidays.hr_leave_allocation_view_form_manager"/>
@@ -258,18 +269,18 @@
<field name="model">hr.leave.allocation</field>
<field name="priority">16</field>
<field name="arch" type="xml">
<tree string="Allocation Requests" sample="1" decoration-info="state == 'draft'">
<field name="employee_id" decoration-muted="not active_employee" widget="many2one_avatar_user" readonly="state in ['cancel', 'refuse', 'validate']"/>
<field name="department_id" optional="hide" readonly="state not in ['confirm', 'draft']"/>
<field name="holiday_status_id" class="fw-bold" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']"/>
<tree string="Allocation Requests" sample="1">
<field name="employee_id" decoration-muted="not active_employee" widget="many2one_avatar_user" readonly="state in ['refuse', 'validate']"/>
<field name="department_id" optional="hide" readonly="state not in ['confirm']"/>
<field name="holiday_status_id" class="fw-bold" readonly="state in ['refuse', 'validate', 'validate1']"/>
<field name="name"/>
<field name="duration_display" string="Amount"/>
<field name="date_from" string="Validity Start" optional="hide"/>
<field name="date_to" string="Validity Stop" optional="hide" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']"/>
<field name="allocation_type" readonly="state not in ['confirm', 'draft']"/>
<field name="date_to" string="Validity Stop" optional="hide" readonly="state in ['refuse', 'validate', 'validate1']"/>
<field name="allocation_type" readonly="state not in ['confirm']"/>
<field name="message_needaction" column_invisible="True"/>
<field name="active_employee" column_invisible="True"/>
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-warning="state == 'confirm'" decoration-success="state == 'validate'"/>
<field name="state" widget="badge" decoration-warning="state == 'confirm'" decoration-success="state == 'validate'"/>
<button string="Validate" name="action_validate" type="object"
icon="fa-check"
invisible="state != 'confirm'"
@@ -325,7 +336,7 @@
<xpath expr="//filter[@name='my_leaves']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//filter[@name='active_employee']" position="attributes">
<xpath expr="//filter[@name='active_employee']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//filter[@name='active_types']" position="attributes">
@@ -410,7 +421,7 @@
</templates>
</kanban>
</field>
</record>
</record>
<record id="hr_leave_allocation_view_activity" model="ir.ui.view">
<field name="name">hr.leave.allocation.view.activity</field>
@@ -500,7 +511,7 @@
<field name="binding_model_id" ref="hr_holidays.model_hr_leave_allocation"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">
<field name="code">
if records:
records.action_validate()
</field>
+16 -5
View File
@@ -212,7 +212,6 @@
<field name="can_reset" invisible="1"/>
<field name="can_approve" invisible="1"/>
<field name="can_cancel" invisible="1"/>
<field name="holiday_allocation_id" invisible="1" force_save="1"/>
<field name="has_mandatory_day" invisible="1"/>
<header>
<button string="Confirm" name="action_confirm" type="object" class="oe_highlight" invisible="state != 'draft' or not active"/>
@@ -249,9 +248,22 @@
<field name="display_name" invisible="not holiday_status_id"/>
</div>
<group name="col_left">
<field name="holiday_status_id" force_save="1" domain="['|', ('requires_allocation', '=', 'no'), '&amp;', ('has_valid_allocation', '=', True), '&amp;', ('virtual_remaining_leaves', '&gt;', 0), ('max_leaves', '>', '0')]" context="{'employee_id':employee_id, 'default_date_from':date_from, 'default_date_to':date_to}" options="{'no_create': True, 'no_open': True, 'request_type':'leave'}" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']"/>
<field name="date_from" invisible="1" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']" />
<field name="date_to" invisible="1" readonly="state in ['cancel', 'refuse', 'validate', 'validate1']" />
<field name="employee_company_id" invisible="1"/>
<field name="holiday_status_id" force_save="1"
domain="[
'|',
('requires_allocation', '=', 'no'),
'&amp;',
('has_valid_allocation', '=', True),
'&amp;',
('virtual_remaining_leaves', '&gt;', 0),
('max_leaves', '>', '0')
]"
context="{'employee_id': employee_id, 'default_date_from': date_from, 'default_date_to': date_to}"
options="{'no_create': True, 'no_open': True, 'request_type': 'leave'}"
readonly="state in ['cancel', 'refuse', 'validate', 'validate1']"/>
<field name="date_from" invisible="1"/>
<field name="date_to" invisible="1"/>
<field name="request_date_from" invisible="1"/>
<field name="request_date_to" invisible="1"/>
<!-- half day or custom hours: only show one date -->
@@ -690,7 +702,6 @@
'hide_employee_name': 1,
'holiday_status_display_name': False}
</field>
<field name="domain">[('holiday_allocation_id', '=', active_id)]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Meet the time off dashboard.
@@ -5,7 +5,6 @@ from datetime import datetime
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import float_round
from odoo.osv import expression
@@ -64,23 +63,6 @@ class HolidaysAllocation(models.Model):
allocation.overtime_id.sudo().duration = -1 * duration
return res
def action_draft(self):
overtime_allocations = self.filtered('overtime_deductible')
if any([a.employee_overtime < float_round(a.number_of_hours_display, 2) for a in overtime_allocations]):
raise ValidationError(_('The employee does not have enough extra hours to request this allocation.'))
res = super().action_draft()
overtime_allocations.overtime_id.sudo().unlink()
for allocation in overtime_allocations:
overtime = self.env['hr.attendance.overtime'].sudo().create({
'employee_id': allocation.employee_id.id,
'date': allocation.date_from,
'adjustment': True,
'duration': -1 * allocation.number_of_hours_display
})
allocation.sudo().overtime_id = overtime.id
return res
def action_refuse(self):
res = super().action_refuse()
self.overtime_id.sudo().unlink()
@@ -33,22 +33,23 @@ class HRLeaveType(models.Model):
}
super(HRLeaveType, self - overtime_leaves)._compute_display_name()
def get_employees_days(self, employee_ids, date=None):
res = super().get_employees_days(employee_ids, date)
deductible_time_off_type_ids = self.env['hr.leave.type'].search([
def get_allocation_data(self, employees, date=None):
res = super().get_allocation_data(employees, date)
deductible_time_off_types = self.env['hr.leave.type'].search([
('overtime_deductible', '=', True),
('requires_allocation', '=', 'no')]).ids
for employee_id, allocations in res.items():
for allocation_id in allocations:
if allocation_id in deductible_time_off_type_ids:
res[employee_id][allocation_id]['virtual_remaining_leaves'] = self.env['hr.employee'].sudo().browse(employee_id).total_overtime
res[employee_id][allocation_id]['overtime_deductible'] = True
('requires_allocation', '=', 'no')])
leave_type_names = deductible_time_off_types.mapped('name')
for employee in res:
for leave_data in res[employee]:
if leave_data[0] in leave_type_names:
leave_data[1]['virtual_remaining_leaves'] = employee.sudo().total_overtime
leave_data[1]['overtime_deductible'] = True
else:
res[employee_id][allocation_id]['overtime_deductible'] = False
leave_data[1]['overtime_deductible'] = False
return res
def _get_days_request(self):
res = super()._get_days_request()
def _get_days_request(self, date=None):
res = super()._get_days_request(date)
res[1]['overtime_deductible'] = self.overtime_deductible
return res
@@ -47,7 +47,6 @@ class TestAccrualAllocationsAttendance(TestHrHolidaysCommon):
'number_of_days': 0,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
@@ -162,7 +162,7 @@ class TestHolidaysOvertime(TransactionCase):
'holiday_status_id': self.leave_type_employee_allocation.id,
'employee_id': self.employee.id,
'number_of_days': 1,
'state': 'draft',
'state': 'confirm',
'date_from': time.strftime('%Y-1-1'),
'date_to': time.strftime('%Y-12-31'),
})
@@ -170,16 +170,15 @@ class TestHolidaysOvertime(TransactionCase):
self.new_attendance(check_in=datetime(2021, 1, 2, 8), check_out=datetime(2021, 1, 2, 16))
self.assertAlmostEqual(self.employee.total_overtime, 8, 'Should have 8 hours of overtime')
allocation = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].sudo().create({
'name': 'test allocation',
'holiday_status_id': self.leave_type_employee_allocation.id,
'employee_id': self.employee.id,
'number_of_days': 1,
'state': 'draft',
'state': 'confirm',
'date_from': time.strftime('%Y-1-1'),
'date_to': time.strftime('%Y-12-31'),
})
allocation.action_confirm()
self.assertEqual(self.employee.total_overtime, 0)
leave_type = self.env['hr.leave.type'].sudo().create({
@@ -192,16 +191,15 @@ class TestHolidaysOvertime(TransactionCase):
})
# User can request another allocation even without overtime
allocation2 = self.env['hr.leave.allocation'].create({
self.env['hr.leave.allocation'].create({
'name': 'test allocation',
'holiday_status_id': leave_type.id,
'employee_id': self.employee.id,
'number_of_days': 1,
'state': 'draft',
'state': 'confirm',
'date_from': time.strftime('%Y-1-1'),
'date_to': time.strftime('%Y-12-31'),
})
allocation2.action_confirm()
def test_allocation_check_overtime_write(self):
self.new_attendance(check_in=datetime(2021, 1, 2, 8), check_out=datetime(2021, 1, 2, 16))
@@ -213,7 +211,7 @@ class TestHolidaysOvertime(TransactionCase):
'holiday_status_id': self.leave_type_employee_allocation.id,
'employee_id': self.employee.id,
'number_of_days': 1,
'state': 'draft',
'state': 'confirm',
'date_from': time.strftime('%Y-1-1'),
'date_to': time.strftime('%Y-12-31'),
})
@@ -229,7 +229,7 @@ class TestWorkEntryHolidaysMultiContract(TestWorkEntryHolidaysBase):
'leave_validation_type': 'hr',
'request_unit': 'day',
})
self.env['hr.leave.allocation'].create({
allocation = self.env['hr.leave.allocation'].create({
'name': 'Allocation',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
@@ -237,19 +237,20 @@ class TestWorkEntryHolidaysMultiContract(TestWorkEntryHolidaysBase):
'state': 'confirm',
'date_from': datetime.strptime('2023-01-01', '%Y-%m-%d').date(),
'date_to': datetime.strptime('2023-12-31', '%Y-%m-%d').date(),
}).action_validate()
})
allocation.action_validate()
leave_during_full_time, leave_during_partial_time = self.env['hr.leave'].create([
{
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': '2023-1-3', # Tuesday
'request_date_to': '2023-1-5', # Thursday
'request_date_from': '2023-1-3', # Tuesday
'request_date_to': '2023-1-5', # Thursday
},
{
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': '2023-12-5', # Tuesday
'request_date_to': '2023-12-7', # Thursday
'request_date_from': '2023-12-5', # Tuesday
'request_date_to': '2023-12-7', # Thursday
},
])
self.assertEqual(leave_during_full_time.number_of_days_display, 3)
@@ -80,7 +80,6 @@ class TestTimesheetHolidays(TestCommonTimesheet):
'date_from': time.strftime('%Y-01-01'),
'date_to': time.strftime('%Y-12-31'),
})
self.hr_leave_allocation_with_ts.action_validate()
self.hr_leave_allocation_no_ts = self.Allocations.sudo().create({
'name': 'Days for limited category without timesheet',
'employee_id': self.empl_employee.id,
@@ -90,7 +89,6 @@ class TestTimesheetHolidays(TestCommonTimesheet):
'date_from': time.strftime('%Y-01-01'),
'date_to': time.strftime('%Y-12-31'),
})
self.hr_leave_allocation_no_ts.action_validate()
def test_validate_with_timesheet(self):
# employee creates a leave request