[IMP] hr_holidays: convert dashboard and widget to OWL

Convert the `leave_stats` widget and the leaves dashboard to OWL.

closes odoo/odoo#80397

Taskid: 2675380
Signed-off-by: Kevin Baptiste <kba@odoo.com>
This commit is contained in:
Kevin Baptiste
2021-12-06 16:49:20 +00:00
parent 6739514087
commit 60728a63db
17 changed files with 448 additions and 421 deletions
+6 -5
View File
@@ -67,12 +67,11 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p
],
'web.assets_backend': [
'hr_holidays/static/src/js/time_off_calendar/*.js',
'hr_holidays/static/src/js/radio_image.js',
'hr_holidays/static/src/js/leave_stats_widget.js',
'hr_holidays/static/src/models/*/*.js',
'hr_holidays/static/src/scss/time_off.scss',
'hr_holidays/static/src/components/*/*.scss',
'hr_holidays/static/src/scss/accrual_plan_level.scss',
'hr_holidays/static/src/dashboard/**/*.js',
'hr_holidays/static/src/dashboard/**/*.scss',
'hr_holidays/static/src/leave_stats/**/*.js',
],
'web.tests_assets': [
'hr_holidays/static/tests/helpers/**/*',
@@ -83,7 +82,9 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p
],
'web.assets_qweb': [
'hr_holidays/static/src/components/*/*.xml',
'hr_holidays/static/src/xml/*.xml',
'hr_holidays/static/src/dashboard/**/*.xml',
'hr_holidays/static/src/leave_stats/**/*.xml',
'hr_holidays/static/src/xml/**/*.xml',
],
},
'license': 'LGPL-3',
@@ -0,0 +1,16 @@
/* @odoo-module */
export class TimeOffCardPopover extends owl.Component {}
TimeOffCardPopover.template = 'hr_holidays.TimeOffCardPopover';
TimeOffCardPopover.props = ['allocated', 'approved', 'planned', 'left'];
export class TimeOffCard extends owl.Component {}
TimeOffCard.components = { TimeOffCardPopover };
TimeOffCard.template = 'hr_holidays.TimeOffCard';
TimeOffCard.props = ['name', 'id', 'data', 'requires_allocation'];
export class TimeOffCardMobile extends TimeOffCard {}
TimeOffCardMobile.template = 'hr_holidays.TimeOffCardMobile';
@@ -0,0 +1,73 @@
.o_timeoff_card {
color: $o-enterprise-color;
flex-direction: column;
flex-grow: 1;
display: flex;
text-align: center;
&:not(:last-child) {
border-right: 2px solid $border-color;
}
span {
line-height: 1;
}
.o_timeoff_name {
color: $body-color;
font-size: 15px;
margin-bottom: 3px;
}
.o_timeoff_duration {
font-size: 30px;
font-weight: bold;
img {
height: 30px;
}
}
.o_timeoff_validity {
font-size: 10px;
}
.o_timeoff_info {
display: inline-block;
margin-right: -10px;
span {
cursor: pointer;
padding-right: 10px;
}
}
}
.o_timeoff_card_mobile {
.o_timeoff_purple {
color: $o-enterprise-color;
}
.o_timeoff_green {
color: $o-enterprise-primary-color;
}
}
.o_timeoff_popover {
font-size: 12px;
ul {
list-style-type: none;
margin-bottom: 0;
padding: 0;
}
li {
display: flex;
justify-content: space-between;
span {
padding-left: 3px;
}
}
}
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<div t-name="hr_holidays.TimeOffCard" owl="1" class="o_timeoff_card py-3">
<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="show_popover" t-value="true"/>
<strong class="o_timeoff_name"><t t-esc="props.name"/></strong>
<span class="o_timeoff_duration">
<t t-if="data and data.icon">
<img t-att-src="data.icon" />
</t>
<t t-esc="duration"/>
</span>
<div class="text-uppercase">
<t t-if="data.request_unit == 'hour'">hours</t>
<t t-else="">days</t>
<t t-if="props.requires_allocation" name="duration_type">
available
</t>
<t t-else="">
taken
</t>
<t t-if="show_popover">
<TimeOffCardPopover
allocated="data.max_leaves"
approved="data.leaves_approved"
planned="data.leaves_requested"
left="data.virtual_remaining_leaves" />
</t>
</div>
<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>
valid until <span t-esc="data.closest_allocation_expire"/>)
</t>
<t t-else="">
(valid until <span t-esc="data.closest_allocation_expire"/>)
</t>
</span>
</div>
<t t-name="hr_holidays.TimeOffCardMobile" owl="1">
<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" />
<span class="pull-right o_timeoff_card_mobile">
<t t-if="props.requires_allocation" name="duration_type">
<strong t-esc="duration" class="o_timeoff_green"/> / <span t-esc="data.max_leaves"/> <t t-if="data.request_unit == 'hour'">Hours</t><t t-else="">Days</t> <span class="o_timeoff_green">Available</span>
</t>
<t t-else="">
<strong t-esc="duration"/> <t t-if="data.request_unit == 'hour'">Hours</t><t t-else="">Days</t> <span class="o_timeoff_purple">Taken</span>
</t>
</span>
</t>
<t t-name="hr_holidays.TimeOffCardPopover" owl="1">
<div class="o_timeoff_info">
<Popover position="'right'" popoverClass="'o_timeoff_popover'">
<span class="fa fa-question-circle-o"/>
<t t-set="opened">
<ul>
<li>Allocated: <span t-esc="props.allocated"/></li>
<li>Approved: <span t-esc="props.approved"/></li>
<li style="border-bottom: 1px solid gray;">Planned: <span t-esc="props.planned"/></li>
<li>Left: <span t-esc="props.left"/></li>
</ul>
</t>
</Popover>
</div>
</t>
</templates>
@@ -0,0 +1,9 @@
/* @odoo-module */
import { TimeOffCard } from './time_off_card';
export class TimeOffDashboard extends owl.Component {}
TimeOffDashboard.components = { TimeOffCard };
TimeOffDashboard.template = 'hr_holidays.TimeOffDashboard';
TimeOffDashboard.props = ['holidays'];
@@ -0,0 +1,11 @@
.o_timeoff_dashboard {
display: flex;
justify-content: space-between;
height: auto;
box-shadow: inset 0 -1px 0 $border-color;
position: sticky;
top: 0;
z-index: 100;
background-color: $o-webclient-background-color;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<div t-name="hr_holidays.TimeOffDashboard" owl="1" class="o_timeoff_dashboard">
<t t-foreach="props.holidays" t-as="holiday" t-key="holiday[3]">
<TimeOffCard name="holiday[0]" data="holiday[1]" requires_allocation="holiday[2] == 'yes'" id="holiday[3]"/>
</t>
</div>
</templates>
@@ -1,153 +0,0 @@
odoo.define('hr_holidays.LeaveStatsWidget', function (require) {
"use strict";
var time = require('web.time');
var Widget = require('web.Widget');
var widget_registry = require('web.widget_registry');
var LeaveStatsWidget = Widget.extend({
template: 'hr_holidays.leave_stats',
/**
* @override
* @param {Widget|null} parent
* @param {Object} params
*/
init: function (parent, params) {
this._setState(params);
this._super(parent);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override to fetch data before rendering.
*/
willStart: function () {
return Promise.all([this._super(), this._fetchLeaveTypesData(), this._fetchDepartmentLeaves()]);
},
/**
* Fetch new data if needed (according to updated fields) and re-render the widget.
* Called by the basic renderer when the view changes.
* @param {Object} state
* @returns {Promise}
*/
updateState: function (state) {
var self = this;
var to_await = [];
var updatedFields = this._setState(state);
if (_.intersection(updatedFields, ['employee', 'date']).length) {
to_await.push(this._fetchLeaveTypesData());
}
if (_.intersection(updatedFields, ['department', 'date']).length) {
to_await.push(this._fetchDepartmentLeaves());
}
return Promise.all(to_await).then(function () {
self.renderElement();
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Update the state
* @param {Object} state
* @returns {String[]} list of updated fields
*/
_setState: function (state) {
var updatedFields = [];
if (state.data.employee_id.res_id !== (this.employee && this.employee.res_id)) {
updatedFields.push('employee');
this.employee = state.data.employee_id;
}
if (state.data.department_id.res_id !== (this.department && this.department.res_id)) {
updatedFields.push('department');
this.department = state.data.department_id;
}
if (state.data.date_from !== this.date) {
updatedFields.push('date');
this.date = state.data.date_from;
}
return updatedFields;
},
/**
* Fetch leaves taken by members of ``this.department`` in the
* month of ``this.date``.
* Three fields are fetched for each leave, namely: employee_id, date_from
* and date_to.
* The resulting data is assigned to ``this.departmentLeaves``
* @private
* @returns {Promise}
*/
_fetchDepartmentLeaves: function () {
if (!this.date || !this.department) {
this.departmentLeaves = null;
return Promise.resolve();
}
var self = this;
var month_date_from = this.date.clone().startOf('month');
var month_date_to = this.date.clone().endOf('month');
return this._rpc({
model: 'hr.leave',
method: 'search_read',
args: [
[['department_id', '=', this.department.res_id],
['state', '=', 'validate'],
['holiday_type', '=', 'employee'],
['date_from', '<=', month_date_to],
['date_to', '>=', month_date_from]],
['employee_id', 'date_from', 'date_to', 'number_of_days'],
],
}).then(function (data) {
var dateFormat = time.getLangDateFormat();
self.departmentLeaves = data.map(function (leave) {
// Format datetimes to date (in the user's format)
return _.extend(leave, {
date_from: moment(leave.date_from).format(dateFormat),
date_to: moment(leave.date_to).format(dateFormat),
number_of_days: leave.number_of_days,
});
});
});
},
/**
* Fetch the number of leaves, grouped by leave type, taken by ``this.employee``
* in the year of ``this.date``.
* The resulting data is assigned to ``this.leavesPerType``
* @private
* @returns {Promise}
*/
_fetchLeaveTypesData: function () {
if (!this.date || !this.employee) {
this.leavesPerType = null;
return Promise.resolve();
}
var self = this;
var year_date_from = this.date.clone().startOf('year');
var year_date_to = this.date.clone().endOf('year');
return this._rpc({
model: 'hr.leave',
method: 'read_group',
kwargs: {
domain: [['employee_id', '=', this.employee.res_id], ['state', '=', 'validate'], ['date_from', '<=', year_date_to], ['date_to', '>=', year_date_from]],
fields: ['holiday_status_id', 'number_of_days:sum'],
groupby: ['holiday_status_id'],
},
}).then(function (data) {
self.leavesPerType = data;
});
}
});
widget_registry.add('hr_leave_stats', LeaveStatsWidget);
return LeaveStatsWidget;
});
@@ -1,58 +1,69 @@
/** @odoo-module **/
import { qweb as QWeb } from 'web.core';
import config from 'web.config';
import { ComponentWrapper } from 'web.OwlCompatibility';
import { TimeOffPopoverRenderer } from "./time_off_popover_renderer";
import { TimeOffDashboard } from '@hr_holidays/dashboard/time_off_dashboard';
import { TimeOffCardMobile } from '@hr_holidays/dashboard/time_off_card';
import config from 'web.config';
import session from 'web.session';
export const TimeOffCalendarRenderer = TimeOffPopoverRenderer.extend({
async _render () {
async willStart() {
await this._super(...arguments);
const result = await this._rpc({
model: 'hr.leave.type',
method: 'get_days_all_request',
context: this.state.context,
});
this.$el.parent().find('.o_calendar_mini').hide();
this.$el.parent().find('.o_timeoff_container').remove();
this.holidays = await this.loadDashboardData();
},
// Do not display header if there is no element to display
if (result.length > 0) {
if (config.device.isMobile) {
result.forEach((data) => {
const elem = QWeb.render('hr_holidays.dashboard_calendar_header_mobile', {
timeoff: data,
});
this.$el.find('.o_calendar_filter_item[data-value=' + data[3] + '] .o_cw_filter_title').append(elem);
});
} else {
const elem = QWeb.render('hr_holidays.dashboard_calendar_header', {
timeoffs: result,
});
this.$el.before(elem);
async start() {
await this._super(...arguments);
//add popover to the information tags
[...this.$el.parent().find('.fa-question-circle-o')].forEach((popup) => {
$(popup).popover({
trigger: 'hover',
html: true,
delay: {show: 300, hide: 0},
content() {
const data = {
allocated: popup.dataset.allocated,
approved: popup.dataset.approved,
planned: popup.dataset.planned,
left: popup.dataset.left
};
const elem_popover = QWeb.render('hr_holidays.dashboard_calendar_header_leave_type_popover', {
data: data,
});
return elem_popover
},
});
});
}
this.TimeOffDashboardWrapper = null;
if (!config.device.isMobile) {
this.TimeOffDashboardWrapper = new ComponentWrapper(this, TimeOffDashboard, { holidays: this.holidays });
await this.TimeOffDashboardWrapper.mount(this.el.parentElement, { position: 'first-child' });
}
},
async _render() {
await this._super(...arguments);
if (this.TimeOffDashboardWrapper) {
this.holidays = await this.loadDashboardData();
this.TimeOffDashboardWrapper.update({ holidays: this.holidays });
}
},
async loadDashboardData() {
return await this._rpc({
model: 'hr.leave.type',
method: 'get_days_all_request',
context: session.user_context,
});
},
async _renderFilters() {
await this._super(...arguments);
if (!config.device.isMobile) {
return;
}
this.holidays = await this.loadDashboardData();
for (const holiday of this.holidays) {
const node = this.el.querySelector(`.o_calendar_filter_item[data-value="${holiday[3]}"] .o_cw_filter_title`);
if (node) {
const mobileCard = new ComponentWrapper(this, TimeOffCardMobile, {
name: holiday[0],
data: holiday[1],
requires_allocation: holiday[2] === 'yes',
id: holiday[3]
});
await mobileCard.mount(node);
}
}
}
});
@@ -5,7 +5,9 @@ import config from 'web.config';
import CalendarRenderer from "web.CalendarRenderer";
import { TimeOffCalendarPopover } from "./time_off_calendar_popover";
export const TimeOffPopoverRenderer = CalendarRenderer.extend({
import { WidgetAdapterMixin } from 'web.OwlCompatibility';
export const TimeOffPopoverRenderer = CalendarRenderer.extend(WidgetAdapterMixin, {
template: "TimeOff.CalendarView.extend",
/**
* We're overriding this to display the weeknumbers on the year view
@@ -67,4 +69,22 @@ export const TimeOffPopoverRenderer = CalendarRenderer.extend({
}
});
},
/*
* OWL Compatibility layer
*/
on_attach_callback() {
this._super(...arguments);
WidgetAdapterMixin.on_attach_callback.call(this);
},
on_detach_callback() {
this._super(...arguments);
WidgetAdapterMixin.on_detach_callback.call(this);
},
destroy() {
this._super(...arguments);
WidgetAdapterMixin.destroy.call(this);
},
});
@@ -0,0 +1,120 @@
/* @odoo-module */
import { useService } from '@web/core/utils/hooks';
import { getLangDateFormat } from 'web.time';
import widgetRegistry from 'web.widgetRegistry';
const { Component, useState } = owl;
export class LeaveStatsComponent extends Component {
setup() {
super.setup(...arguments);
this.rpc = useService('rpc');
this.state = useState({
leaves: [],
departmentLeaves: [],
});
}
async willStart() {
await this.loadLeaves(this.date, this.employee);
await this.loadDepartmentLeaves(this.date, this.department, this.employee);
}
async willUpdateProps(nextProps) {
const dateFrom = nextProps.record.data.date_from || moment();
const dateChanged = this.date !== dateFrom;
const employee = nextProps.record.data.employee_id.data;
const department = nextProps.record.data.department_id.data;
if (dateChanged || employee && (this.employee && this.employee.id) !== employee.id) {
await this.loadLeaves(dateFrom, employee);
}
if (dateChanged || department && (this.department && this.department.id) !== department.id) {
await this.loadDepartmentLeaves(dateFrom, department, employee);
}
}
get date() {
return this.props.record.data.date_from || moment();
}
get department() {
return this.props.record.data.department_id.data;
}
get employee() {
return this.props.record.data.employee_id.data;
}
get thisYear() {
return this.date.format('YYYY');
}
get thisMonth() {
return this.date.format('MMMM');
}
async loadDepartmentLeaves(date, department, employee) {
if (!(department && employee && date)) {
this.state.departmentLeaves = [];
return;
}
const dateFrom = date.clone().startOf('month');
const dateTo = date.clone().endOf('month');
const dateFormat = getLangDateFormat();
const departmentLeaves = await this.rpc({
model: 'hr.leave',
method: 'search_read',
args: [
[
['department_id', '=', department.id],
['state', '=', 'validate'],
['holiday_type', '=', 'employee'],
['date_from', '<=', dateTo],
['date_to', '>=', dateFrom],
],
['employee_id', 'date_from', 'date_to', 'number_of_days'],
]
});
this.state.departmentLeaves = departmentLeaves.map((leave) => {
return Object.assign({}, leave, {
dateFrom: moment(leave.date_from).format(dateFormat),
dateTo: moment(leave.date_to).format(dateFormat),
sameEmployee: leave.employee_id[0] === employee.id,
});
});
}
async loadLeaves(date, employee) {
if (!(employee && date)) {
this.state.leaves = [];
return;
}
const dateFrom = date.clone().startOf('year');
const dateTo = date.clone().endOf('year');
this.state.leaves = await this.rpc({
model: 'hr.leave',
method: 'read_group',
kwargs: {
domain: [
['employee_id', '=', employee.id],
['state', '=', 'validate'],
['date_from', '<=', dateTo],
['date_to', '>=', dateFrom]
],
fields: ['holiday_status_id', 'number_of_days:sum'],
groupby: ['holiday_status_id'],
}
});
}
}
LeaveStatsComponent.template = 'hr_holidays.LeaveStatsComponent';
widgetRegistry.add('hr_leave_stats', LeaveStatsComponent);
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<div t-name="hr_holidays.LeaveStatsComponent" owl="1" class="o_leave_stats">
<div t-if="employee" id="o_leave_stats_employee">
<div class="o_horizontal_separator">
<t t-esc="employee.display_name"/> in <t t-esc="thisYear"/>
</div>
<div t-if="state.leaves.length === 0">
None
</div>
<div t-foreach="state.leaves" t-as="leave" class="d-flex flex-row justify-content-between">
<span t-esc="leave.holiday_status_id[1]"/>
<span><t t-esc="leave.number_of_days"/> day(s)</span>
</div>
</div>
<div t-if="department" id="o_leave_stats_department">
<div class="o_horizontal_separator">
<t t-esc="department.display_name"/> in <t t-esc="thisMonth"/>
</div>
<div t-if="state.departmentLeaves.length === 0">
None
</div>
<div t-foreach="state.departmentLeaves" t-as="leave" t-attf-class="d-flex flex-row justify-content-between {{leave.sameEmployee ? 'font-weight-bold': ''}}">
<span><t t-esc="leave.employee_id[1]"/>: <t t-esc="leave.number_of_days"/> day(s)</span>
<span><t t-esc="leave.dateFrom"/> - <t t-esc="leave.dateTo"/></span>
</div>
</div>
</div>
</templates>
@@ -1,21 +1,4 @@
.o_timeoff_calendar .o_content {
.o_timeoff_container {
height: auto;
box-shadow: inset 0 -1px 0 $border-color;
position: sticky;
top: 0;
z-index: 100;
background-color: $o-webclient-background-color;
}
.o_timeoff_card {
border-right: $border-color solid 2px;
text-align: center;
div {
line-height: 1;
}
}
.fc-dayGridYear-view {
.fc-bgevent {
border-radius: 25px;
@@ -38,30 +21,6 @@
}
}
.o_timeoff_card_last {
border-right: 0px;
}
.o_timeoff_normal {
font-size: 10px;
}
.o_timeoff_big {
font-size: 15px;
}
.o_timeoff_huge {
font-size: 30px;
}
.o_timeoff_purple {
color: $o-enterprise-color;
}
.o_timeoff_green {
color: $o-enterprise-primary-color;
}
.o_calendar_filter {
> h5 {
font-weight: bold;
@@ -86,17 +45,3 @@
}
}
}
.o_timeoff_dashboard_popover{
hr{
margin-top: 0px;
margin-bottom: 0px
}
ul{
list-style: none;
padding-left: 0;
margin:0px;
}
}
@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr_holidays.leave_per_type">
<table class="o_group o_inner_group table-striped">
<thead>
<tr>
<td colspan="2">
<div class="o_horizontal_separator"><t t-esc="widget.employee.data.display_name"/> in <t t-esc="widget.date.format('YYYY')"/></div>
</td>
</tr>
</thead>
<tbody>
<t t-if="widget.leavesPerType.length === 0">
<tr>
<td>None</td>
</tr>
</t>
<t t-foreach="widget.leavesPerType" t-as="leave_type">
<tr>
<td><t t-esc="leave_type.holiday_status_id[1]"/></td>
<td class="w-50"><t t-esc="leave_type.number_of_days"/> day(s)</td>
</tr>
</t>
</tbody>
</table>
</t>
<t t-name="hr_holidays.department_leave">
<table class="o_group o_inner_group table-striped">
<thead>
<tr>
<td colspan="2">
<div class="o_horizontal_separator"><t t-esc="widget.department.data.display_name"/> in <t t-esc="widget.date.format('MMMM')"/></div>
</td>
</tr>
</thead>
<tbody>
<t t-if="widget.departmentLeaves.length === 0">
<tr>
<td>None</td>
</tr>
</t>
<t t-foreach="widget.departmentLeaves" t-as="leave">
<tr t-attf-class="{{leave.employee_id[0] === widget.employee.res_id ? 'font-weight-bold' : ''}}">
<td><t t-esc="leave.employee_id[1]"/>: <t t-esc="leave.number_of_days"/> day(s) </td>
<td class="w-50"><t t-esc="leave.date_from"/> - <t t-esc="leave.date_to"/></td>
</tr>
</t>
</tbody>
</table>
</t>
<div t-name="hr_holidays.leave_stats" class="o_leave_stats">
<t t-if="widget.employee">
<t t-if="widget.leavesPerType">
<t t-call="hr_holidays.leave_per_type"/>
</t>
<t t-if="widget.departmentLeaves">
<t t-call="hr_holidays.department_leave"/>
</t>
</t>
</div>
</templates>
@@ -1,72 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr_holidays.dashboard_calendar_header">
<div class="o_timeoff_container d-flex">
<div t-foreach="timeoffs" t-as="timeoff" t-attf-class="o_timeoff_card flex-grow-1 d-flex flex-column py-3 {{ timeoff_last ? 'o_timeoff_card_last' : '' }}">
<t t-set="requires_allocation" t-value="timeoff[2] === 'yes'"/>
<t t-set="has_icon" t-value="timeoff[1]['icon'] !== false"/>
<t t-set="cl" t-value="'text-muted'"/>
<b><span t-esc="timeoff[0]" class="o_timeoff_name o_timeoff_big"/></b>
<div class="mt-1">
<t t-if="requires_allocation">
<t t-if="has_icon"><img height="30px" t-attf-src="{{timeoff[1]['icon']}}"/></t><span t-esc="timeoff[1]['virtual_remaining_leaves']" class="o_timeoff_huge o_timeoff_purple font-weight-bold align-middle"/><br/>
<t class="o_timeoff_big o_timeoff_purple" t-if="timeoff[1]['request_unit'] == 'hour'"><span class="o_timeoff_purple">HOURS</span></t><t class="o_timeoff_big o_timeoff_purple" t-else=""><span class="o_timeoff_purple">DAYS</span></t> <span class="o_timeoff_purple">AVAILABLE </span>
<span class="fa fa-question-circle-o o_timeoff_purple" t-att-data-allocated="timeoff[1]['max_leaves']" t-att-data-approved="timeoff[1]['leaves_approved']" t-att-data-planned="timeoff[1]['leaves_requested']" t-att-data-left="timeoff[1]['virtual_remaining_leaves']"/>
</t>
<t t-else="">
<t t-if="has_icon"><img height="30px" t-attf-src="{{timeoff[1]['icon']}}"/></t><span t-esc="timeoff[1]['virtual_leaves_taken']" class="o_timeoff_huge o_timeoff_purple font-weight-bold align-middle"/><br/>
<t t-if="timeoff[1]['request_unit'] == 'hour'"><span class="o_timeoff_purple">HOURS</span></t><t t-else=""><span class="o_timeoff_purple">DAYS</span></t> <span class="o_timeoff_purple">TAKEN</span>
</t>
</div>
<t t-if="requires_allocation and timeoff[1]['closest_allocation_expire'] != false">
<t t-if="timeoff[1]['closest_allocation_remaining'] == timeoff[1]['virtual_remaining_leaves']">
<span class="o_timeoff_purple o_timeoff_normal">
(VALID UNTIL <span t-esc="timeoff[1]['closest_allocation_expire']"/>)
</span>
</t>
<t t-else="">
<span class="o_timeoff_purple o_timeoff_normal">
(<span t-esc="timeoff[1]['closest_allocation_remaining']"/> <t t-if="timeoff[1]['request_unit'] == 'hour'"><span>HOURS</span></t><t t-else=""><span>DAYS</span></t>
VALID UNTIL <span t-esc="timeoff[1]['closest_allocation_expire']"/>)
</span>
</t>
</t>
</div>
</div>
</t>
<t t-name="hr_holidays.dashboard_calendar_header_leave_type_popover">
<div class="o_timeoff_dashboard_popover">
<ul>
<li>Allocated: <span t-esc="data['allocated']"/></li>
<li>Approved: <span t-esc="data['approved']"/></li>
<li>Planned: <span t-esc="data['planned']"/></li>
<hr/>
<li>Left: <span t-esc="data['left']"/></li>
</ul>
</div>
</t>
<t t-name="hr_holidays.dashboard_calendar_header_mobile">
<t t-set="requires_allocation" t-value="timeoff[2] === 'yes'"/>
<span class="pull-right">
<span>
<t t-if="requires_allocation">
<strong t-esc="timeoff[1]['virtual_remaining_leaves']" class="o_timeoff_green"/> / <span t-esc="timeoff[1]['max_leaves']"/>
<t t-if="timeoff[1]['request_unit'] == 'hour'">Hours</t><t t-else="">Days</t><span class="o_timeoff_green ml-2">Available </span>
</t>
<t t-else="">
<strong t-esc="timeoff[1]['virtual_leaves_taken']"/> <t t-if="timeoff[1]['request_unit'] == 'hour'">Hours</t><t t-else="">Days</t><span class="o_timeoff_purple ml-2">Taken</span>
</t>
</span>
</span>
</t>
<t t-name="hr_holidays.dashboard.calendar.button">
<button class="btn btn-primary btn-time-off" type="button">
<t t-esc="time_off"/>
@@ -106,16 +106,15 @@ odoo.define('hr_holidays.leave_stats_widget_tests', function (require) {
return this._super.apply(this, arguments);
},
});
var $leaveTypeBody = form.$('.o_leave_stats table:first > tbody');
var $leavesDepartmentBody = form.$('.o_leave_stats table:nth-child(2) > tbody');
var $leavesDepartmentHeader = form.$('.o_leave_stats table:nth-child(2) > thead');
const $leaveTypeBody = form.$('.o_leave_stats #o_leave_stats_employee');
const $leavesDepartmentBody = form.$('.o_leave_stats #o_leave_stats_department');
assert.strictEqual($leaveTypeBody.find('td:contains(Legal Leave)').length, 1, "it should have leave type");
assert.strictEqual($leaveTypeBody.find('td:contains(6)').length, 1, "it should have 6 days");
assert.strictEqual($leaveTypeBody.find('span:contains(Legal Leave)').length, 1, "it should have leave type");
assert.strictEqual($leaveTypeBody.find('span:contains(6)').length, 1, "it should have 6 days");
assert.strictEqual($leavesDepartmentBody.find('td:contains(Richard)').length, 2, "it should have 2 leaves for Richard");
assert.strictEqual($leavesDepartmentBody.find('td:contains(Jesus)').length, 1, "it should have 1 leaves for Jesus");
assert.strictEqual($leavesDepartmentHeader.find('td:contains(R&D)').length, 1, "it should have R&D title");
assert.strictEqual($leavesDepartmentBody.find('span:contains(Richard)').length, 2, "it should have 2 leaves for Richard");
assert.strictEqual($leavesDepartmentBody.find('span:contains(Jesus)').length, 1, "it should have 1 leaves for Jesus");
assert.strictEqual($leavesDepartmentBody.find('div.o_horizontal_separator:contains(R&D)').length, 1, "it should have R&D title");
form.destroy();
});
QUnit.test('leave stats reload when employee/department changes', async function (assert) {
@@ -1,34 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-inherit="hr_holidays.dashboard_calendar_header" t-inherit-mode="extension">
<!--inject a first condition-->
<xpath expr="//t[@t-if='requires_allocation']" position="before">
<t t-if="timeoff[1]['overtime_deductible'] == true &amp;&amp; !requires_allocation">
<t t-if="has_icon">
<img height="30px" t-attf-src="{{timeoff[1]['icon']}}"/>
</t>
<span t-esc="timeoff[1]['virtual_remaining_leaves']" class="o_timeoff_huge o_timeoff_purple font-weight-bold align-middle"/><br/>
<span class="o_timeoff_purple">HOURS AVAILABLE</span>
</t>
<t t-name="hr_holidays_attendance.TimeOffCard" t-inherit="hr_holidays.TimeOffCard" t-inherit-mode="extension" owl="1">
<xpath expr="//t[@t-set='duration']" position="replace">
<t t-set="duration" t-value="props.requires_allocation || props.data['overtime_deductible']?props.data['virtual_remaining_leaves']:props.data['virtual_leaves_taken']" />
</xpath>
<!--change t-if to t-elif-->
<xpath expr="//t[@t-if='requires_allocation']" position="attributes">
<attribute name="t-if"/>
<attribute name="t-elif">requires_allocation</attribute>
<xpath expr="//t[@t-set='show_popover']" position="replace">
<t t-set="show_popover" t-value="!props.data['overtime_deductible']"/>
</xpath>
<xpath expr="//t[@name='duration_type']" position="attributes">
<attribute name="t-if">props.requires_allocation || props.data['overtime_deductible']</attribute>
</xpath>
</t>
<t t-inherit="hr_holidays.dashboard_calendar_header_mobile" t-inherit-mode="extension">
<!--inject a first condition-->
<xpath expr="//t[@t-if='requires_allocation']" position="before">
<t t-if="timeoff[1]['overtime_deductible'] == true &amp;&amp; !requires_allocation">
<strong t-esc="timeoff[1]['virtual_remaining_leaves']" class="o_timeoff_green"/> Hours <span class="o_timeoff_green ml-2">Available </span>
<t t-name="hr_holidays_attendance.TimeOffCardMobile" t-inherit="hr_holidays.TimeOffCardMobile" t-inherit-mode="extension" owl="1">
<xpath expr="//t[@t-set='duration']" position="replace">
<t t-set="duration" t-value="props.requires_allocation || props.data['overtime_deductible']?props.data['virtual_remaining_leaves']:props.data['virtual_leaves_taken']" />
</xpath>
<xpath expr="//t[@name='duration_type']" position="before">
<t t-if="props.data['overtime_deductible'] == true &amp;&amp; !props.requires_allocation">
<strong t-esc="duration" class="o_timeoff_green"/> <t t-if="props.data['request_unit'] == 'hour'">Hours</t><t t-else="">Days</t> <span class="o_timeoff_green">Available</span>
</t>
</xpath>
<!--change t-if to t-elif-->
<xpath expr="//t[@t-if='requires_allocation']" position="attributes">
<xpath expr="//t[@name='duration_type']" position="attributes">
<attribute name="t-if"/>
<attribute name="t-elif">requires_allocation</attribute>
<attribute name="t-elif">props.requires_allocation</attribute>
</xpath>
</t>
</templates>