[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:
@@ -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 && !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 && !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 && !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>
|
||||
|
||||
Reference in New Issue
Block a user