[IMP] hr_holidays: migrate calendar views to Owl

task-2991495

closes odoo/odoo#101658

X-original-commit: fb2dfd438c240848c4410b040d2177e4ff6a71b3
Signed-off-by: Kevin Baptiste <kba@odoo.com>
Signed-off-by: William Braeckman (wbr) <wbr@odoo.com>
This commit is contained in:
Kevin Baptiste
2022-09-29 20:51:00 +02:00
parent 9d4f2624df
commit f90e5a5300
40 changed files with 818 additions and 982 deletions
+3 -2
View File
@@ -69,7 +69,9 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p
'hr_holidays/static/src/components/*/*',
],
'web.assets_backend': [
'hr_holidays/static/src/js/**/*.js',
'hr_holidays/static/src/views/**/*.js',
'hr_holidays/static/src/views/**/*.scss',
'hr_holidays/static/src/views/**/*.xml',
'hr_holidays/static/src/components/*/*.scss',
'hr_holidays/static/src/components/*/*.xml',
'hr_holidays/static/src/dashboard/**/*.js',
@@ -78,7 +80,6 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p
'hr_holidays/static/src/leave_stats/**/*.js',
'hr_holidays/static/src/leave_stats/**/*.xml',
'hr_holidays/static/src/scss/*.scss',
'hr_holidays/static/src/xml/**/*.xml',
'hr_holidays/static/src/tours/*.js',
'hr_holidays/static/src/radio_image_field/*.js',
'hr_holidays/static/src/radio_image_field/*.xml',
+9 -4
View File
@@ -80,7 +80,7 @@ class HolidaysRequest(models.Model):
defaults = super(HolidaysRequest, self).default_get(fields_list)
defaults = self._default_get_request_parameters(defaults)
if 'holiday_status_id' in fields_list and not defaults.get('holiday_status_id'):
if self.env.context.get('holiday_status_name_get', True) and 'holiday_status_id' in fields_list and not defaults.get('holiday_status_id'):
lt = self.env['hr.leave.type'].search(['|', ('requires_allocation', '=', 'no'), ('has_valid_allocation', '=', True)], limit=1, order='sequence')
if lt:
@@ -143,7 +143,7 @@ class HolidaysRequest(models.Model):
holiday_status_id = fields.Many2one(
"hr.leave.type", compute='_compute_from_employee_id', store=True, string="Time Off Type", required=True, readonly=False,
states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]},
domain=['|', ('requires_allocation', '=', 'no'), ('has_valid_allocation', '=', True)])
domain="[('company_id', '?=', employee_company_id), '|', ('requires_allocation', '=', 'no'), ('has_valid_allocation', '=', True)]")
holiday_allocation_id = fields.Many2one(
'hr.leave.allocation', compute='_compute_from_holiday_status_id', string="Allocation", store=True, readonly=False)
color = fields.Integer("Color", related='holiday_status_id.color')
@@ -455,7 +455,8 @@ class HolidaysRequest(models.Model):
for holiday in self:
holiday.manager_id = holiday.employee_id.parent_id.id
if holiday.employee_id.user_id != self.env.user and self._origin.employee_id != holiday.employee_id:
holiday.holiday_status_id = False
if holiday.employee_id and not holiday.holiday_status_id.with_context(employee_id=holiday.employee_id.id).has_valid_allocation:
holiday.holiday_status_id = False
@api.depends('employee_id', 'holiday_type')
def _compute_department_id(self):
@@ -753,6 +754,10 @@ class HolidaysRequest(models.Model):
# ORM Overrides methods
####################################################
@api.depends('employee_id', 'holiday_status_id')
def _compute_display_name(self):
super()._compute_display_name()
def onchange(self, values, field_name, field_onchange):
# Try to force the leave_type name_get when creating new records
# This is called right after pressing create and returns the name_get for
@@ -809,7 +814,7 @@ class HolidaysRequest(models.Model):
display_date = fields.Date.to_string(date_from_utc) or ""
if leave.number_of_days > 1 and date_from_utc and date_to_utc:
display_date += ' / %s' % fields.Date.to_string(date_to_utc) or ""
if self.env.context.get('hide_employee_name') and 'employee_id' in self.env.context.get('group_by', []):
if not target or self.env.context.get('hide_employee_name') and 'employee_id' in self.env.context.get('group_by', []):
res.append((
leave.id,
_("%(leave_type)s: %(duration).2f days (%(start)s)",
@@ -4,7 +4,16 @@
<field name="name">hr.leave.report.calendar.view</field>
<field name="model">hr.leave.report.calendar</field>
<field name="arch" type="xml">
<calendar string="Time Off" date_start="start_datetime" date_stop="stop_datetime" mode="month" quick_add="False" color="employee_id" event_open_popup="True" js_class="time_off_calendar_all" show_unusual_days="True">
<calendar
string="Time Off"
date_start="start_datetime"
date_stop="stop_datetime"
mode="month"
quick_add="False"
color="employee_id"
event_open_popup="True"
js_class="time_off_calendar"
show_unusual_days="True">
<field name="name"/>
<field name="employee_id" filters="1" invisible="1"/>
<field name="is_hatched" invisible="1"/>
@@ -1,11 +1,42 @@
/* @odoo-module */
import { TimeOffCard } from './time_off_card';
import { useBus, useService } from "@web/core/utils/hooks";
const { Component } = owl;
const { Component, useState, onWillStart } = owl;
export class TimeOffDashboard extends Component {}
export class TimeOffDashboard extends Component {
setup() {
this.orm = useService("orm");
this.state = useState({
holidays: [],
});
useBus(this.env.timeOffBus, 'update_dashboard', async () => {
await this.loadDashboardData()
});
onWillStart(async () => {
await this.loadDashboardData();
});
}
async loadDashboardData() {
const context = {};
if (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
}
);
}
}
TimeOffDashboard.components = { TimeOffCard };
TimeOffDashboard.template = 'hr_holidays.TimeOffDashboard';
TimeOffDashboard.props = ['holidays'];
TimeOffDashboard.props = ['employeeId'];
@@ -1,7 +1,7 @@
<?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]">
<t t-foreach="state.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>
@@ -1,43 +0,0 @@
/** @odoo-module **/
import viewRegistry from 'web.view_registry';
import FormController from "web.FormController";
import FormView from "web.FormView";
export const AllocationLeaveFormController = FormController.extend({
/**
* @override
*/
async _applyChanges(dataPointID, changes, event) {
const result = await this._super.apply(this, arguments);
const formData = event.target.record.data;
if (formData.holiday_status_id) {
let nameSearchContext = {'holiday_status_name_get': false};
const employee_id = formData.employee_id;
if (employee_id) {
nameSearchContext = {'holiday_status_name_get': true, 'employee_id': employee_id.data.id};
}
const nameSearch = await this._rpc({
model: 'hr.leave.type',
method: 'name_get',
args: [formData.holiday_status_id.data.id],
context: nameSearchContext,
limit: 1
});
if (this.$el.find("div[name='holiday_status_id']").find('input')[0]) {
this.$el.find("div[name='holiday_status_id']").find('input')[0].value = nameSearch[0][1];
}
}
return result;
}
});
export const AllocationLeaveFormView = FormView.extend({
config: Object.assign({}, FormView.prototype.config, {
Controller: AllocationLeaveFormController,
}),
});
viewRegistry.add('allocation_leave_form', AllocationLeaveFormView);
@@ -1,37 +0,0 @@
/** @odoo-module **/
import { FieldRadio } from 'web.relational_fields';
import FieldRegistry from 'web.field_registry';
import Core from 'web.core';
const QWeb = Core.qweb;
const FieldRadioImage = FieldRadio.extend({
_render() {
const self = this;
let currentValue;
if (this.field.type === 'many2one') {
currentValue = this.value && this.value.data.id;
} else {
currentValue = this.value;
}
this.$el.empty();
this.$el.attr('role', 'radiogroup')
.attr('aria-label', this.string);
_.each(this.values, function (value, index) {
if (self.mode === 'edit' || value[0] === currentValue) {
self.$el.append(QWeb.render('FieldRadioIcon.button', {
checked: value[0] === currentValue,
id: self.unique_id + '_' + value[0],
index: index,
name: self.unique_id,
value: value,
disabled: self.hasReadonlyModifier,
mode: self.mode,
}));
}
});
}
});
FieldRegistry.add('hr_holidays_radio_image', FieldRadioImage);
@@ -1,228 +0,0 @@
/** @odoo-module **/
import { _t, qweb as QWeb } from "web.core";
import CalendarController from "web.CalendarController";
import dialogs from "web.view_dialogs";
import Dialog from "web.Dialog";
export const TimeOffCalendarController = CalendarController.extend({
events: Object.assign({}, CalendarController.prototype.events, {
'click .btn-time-off': '_onNewTimeOff',
'click .btn-allocation': '_onNewAllocation',
}),
/**
* @override
*/
start() {
this.$el.addClass('o_timeoff_calendar');
return this._super(...arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Render the buttons and add new button about
* time off and allocations request
*
* @override
*/
renderButtons($node) {
this._super(...arguments);
$(QWeb.render('hr_holidays.dashboard.calendar.button', {
time_off: _t('New Time Off'),
request: this._getAllocationButtonTitle(),
})).appendTo(this.$buttons);
if ($node) {
this.$buttons.appendTo($node);
} else {
this.$('.o_calendar_buttons').replaceWith(this.$buttons);
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_getFormDialogOptions(self, event) {
const options = this._super(...arguments);
const eventId = parseInt(event.data._id)
// Get the time off data from the event
const timeoffData = event.target.state.data.find((timeOff) => timeOff.id === eventId);
// Take the record associated to the timeoffData and read its state to hide or not the Delete button
const timeoffState = timeoffData && timeoffData.record.state;
const canCancel = timeoffData && timeoffData.record.can_cancel;
if(canCancel || timeoffState && !['validate', 'refuse'].includes(timeoffState)) {
options.deletable = true;
options.removeButtonText = _t("Delete");
options.on_remove = () => this._deleteLeave(timeoffData.record);
}
return options;
},
_onDeleteRecord(ev) {
this._deleteLeave(ev.data.event.record);
},
_deleteLeave(timeoff) {
const canCancel = timeoff.can_cancel;
if (canCancel) {
const context = Object.assign({}, this.context, {
default_leave_id: timeoff.id,
});
const cancel_dialog = new dialogs.FormViewDialog(this, {
title: _t('Delete Confirmation'),
res_model: 'hr.holidays.cancel.leave',
context: context,
});
cancel_dialog.on('execute_action', this, (ev) => {
const action_name = ev.data.action_data.name || ev.data.action_data.special;
const event_data = _.clone(ev.data);
if (action_name === 'action_cancel_leave') {
ev.stopPropagation();
this.trigger_up('execute_action', event_data);
setTimeout(() => this.reload(), 300);
cancel_dialog.close();
}
});
cancel_dialog.open();
} else {
Dialog.confirm(this, _t("Are you sure you want to delete this record ?"), {
confirm_callback: () => {
this.model.deleteRecords(timeoff.id, this.modelName).then(() => {
this.reload();
});
}
});
}
},
_update() {
return this._super(...arguments).then(() => {
const request_args = [
this.res_id,
this.model.data.start_date,
this.model.data.end_date,
]
if (this.context.employee_id)
request_args[0] = this.context.employee_id;
this._rpc({
model: 'hr.employee',
method: 'get_stress_days',
args: request_args,
context: this.context,
}).then((stressDays) => {
this.$el.find('td.fc-day').toArray().forEach((td) => {
if (stressDays[td.dataset.date]) {
td.classList.add('hr_stress_day_' + stressDays[td.dataset.date]);
}
});
});
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Action: create a new time off request
*
* @private
*/
async _onNewTimeOff() {
this.timeOffDialog = new dialogs.FormViewDialog(this, {
res_model: "hr.leave",
context: this._getTimeOffContext(),
title: _t("New time off"),
disable_multiple_selection: true,
on_saved: () => this.reload()
});
this.timeOffDialog.open();
},
/**
* Action: create a new allocation request
*
* @private
*/
async _onNewAllocation() {
this.allocationDialog = new dialogs.FormViewDialog(this, {
res_model: "hr.leave.allocation",
context: this._getAllocationContext(),
title: _t("New Allocation"),
disable_multiple_selection: true,
on_saved: () => this.reload()
});
this.allocationDialog.open();
},
_onOpenCreate() {
this._super(...arguments);
if(this.previousOpen) {
this.previousOpen.on('closed', this, (ev) => {
// we reload as record can be created or modified (sent, unpublished, ...)
this.reload();
});
}
},
/**
* @override
*/
_setEventTitle() {
return _t('Time Off Request');
},
_getAllocationButtonTitle() {
return _t('Allocation Request');
},
_getFormViewId() {
return 'hr_holidays.hr_leave_allocation_view_form_dashboard';
},
_getTimeOffContext() {
let date_from = moment();
if (this.mode === "day") {
date_from = this.model.data.start_date;
}
let date_to = moment(date_from);
date_from = date_from.set({
'hour': 0,
'minute': 0,
'second': 0
});
date_from.subtract(this.getSession().getTZOffset(date_from), 'minutes');
date_from = date_from.locale('en').format('YYYY-MM-DD HH:mm:ss');
date_to = date_to.set({
'hour': 23,
'minute': 59,
'second': 59
});
date_to.subtract(this.getSession().getTZOffset(date_to), 'minutes');
date_to = date_to.locale('en').format('YYYY-MM-DD HH:mm:ss');
return {
'default_date_from': date_from,
'default_date_to': date_to,
'lang': this.context.lang,
'form_view_ref': 'hr_holidays.hr_leave_view_form_dashboard_new_time_off'
};
},
_getAllocationContext() {
return {
'default_state': 'confirm',
'lang': this.context.lang,
'form_view_ref': this._getFormViewId()
};
},
});
@@ -1,33 +0,0 @@
/** @odoo-module **/
import { _t } from "web.core";
import { TimeOffCalendarController } from "./time_off_calendar_controller";
export const TimeOffCalendarEmployeeController = TimeOffCalendarController.extend({
_onOpenCreate() {
this.context['default_employee_id'] = this.context.employee_id[0];
this._super(...arguments);
},
_getAllocationButtonTitle() {
return _t('New Allocation Request');
},
_getFormViewId() {
return 'hr_holidays.hr_leave_allocation_view_form_manager_dashboard';
},
_getTimeOffContext() {
const context = this._super(...arguments);
context.default_employee_id = this.context.employee_id[0];
return context;
},
_getAllocationContext() {
const context = this._super(...arguments);
context.default_employee_id = this.context.employee_id[0];
return context;
},
});
@@ -1,15 +0,0 @@
/** @odoo-module **/
import { TimeOffCalendarEmployeeController } from "./time_off_calendar_employee_controller";
import { TimeOffCalendarModel } from "./time_off_calendar_model";
import { TimeOffCalendarView } from "./time_off_calendar_views";
import viewRegistry from 'web.view_registry';
export const TimeOffCalendarEmployeeView = TimeOffCalendarView.extend({
config: Object.assign({}, TimeOffCalendarView.prototype.config, {
Controller: TimeOffCalendarEmployeeController,
Model: TimeOffCalendarModel,
}),
});
viewRegistry.add('time_off_employee_calendar', TimeOffCalendarEmployeeView);
@@ -1,29 +0,0 @@
/** @odoo-module */
import CalendarModel from "web.CalendarModel";
export const TimeOffCalendarModel = CalendarModel.extend({
_getFilterDomain: function() {
const company_domain = [['user_id.company_id', 'in', this.data.context.allowed_company_ids]];
return this._super().concat(company_domain);
},
calendarEventToRecord(event) {
const data = this._super(...arguments);
if (event.allDay) {
let date_from = data.date_from.subtract(7, 'hours');
date_from.add(this.getSession().getTZOffset(date_from), 'minutes');
date_from = date_from.locale('en').format('YYYY-MM-DD HH:mm:ss');
let date_to = data.date_to.add({
'hour': 4,
'minute': 59,
'second': 59
});
date_to.add(this.getSession().getTZOffset(date_to), 'minutes');
date_to = date_to.locale('en').format('YYYY-MM-DD HH:mm:ss');
data.date_from = date_from;
data.date_to = date_to;
}
return data;
},
});
@@ -1,23 +0,0 @@
/** @odoo-module **/
import { _t } from 'web.core';
import CalendarPopover from 'web.CalendarPopover';
export const TimeOffCalendarPopover = CalendarPopover.extend({
template: 'hr_holidays.calendar.popover',
init(parent, eventInfo) {
this._super(...arguments);
const state = this.event.extendedProps.record.state;
this.canDelete = this.event.extendedProps.record.can_cancel || state && !['validate', 'refuse'].includes(state);
this.canEdit = state !== undefined;
this.displayFields = [];
if (this.modelName === "hr.leave.report.calendar") {
const duration = this.event.extendedProps.record.display_name.split(':').slice(-1);
this.display_name = _.str.sprintf(_t("Time Off : %s"), duration);
} else {
this.display_name = this.event.extendedProps.record.display_name;
}
},
});
@@ -1,68 +0,0 @@
/** @odoo-module **/
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';
export const TimeOffCalendarRenderer = TimeOffPopoverRenderer.extend({
async willStart() {
await this._super(...arguments);
this.holidays = await this.loadDashboardData();
},
async start() {
await this._super(...arguments);
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: this.state.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);
}
}
}
});
@@ -1,40 +0,0 @@
/** @odoo-module **/
import CalendarView from "web.CalendarView";
import config from 'web.config';
import { TimeOffCalendarController } from "./time_off_calendar_controller";
import { TimeOffCalendarModel } from "./time_off_calendar_model";
import { TimeOffCalendarRenderer } from "./time_off_calendar_renderer";
import { TimeOffPopoverRenderer } from "./time_off_popover_renderer";
import viewRegistry from 'web.view_registry';
export const TimeOffCalendarView = CalendarView.extend({
config: Object.assign({}, CalendarView.prototype.config, {
Controller: TimeOffCalendarController,
Model: TimeOffCalendarModel,
Renderer: TimeOffCalendarRenderer,
}),
/**
* @override
*/
init: function (viewInfo, params) {
this._super(viewInfo, params);
if(config.device.isMobile) {
this.loadParams.mode = "month";
}
}
});
/**
* Calendar shown in the "Everyone" menu
*/
export const TimeOffCalendarAllView = CalendarView.extend({
config: Object.assign({}, CalendarView.prototype.config, {
Controller: TimeOffCalendarController,
Renderer: TimeOffPopoverRenderer,
}),
});
viewRegistry.add('time_off_calendar', TimeOffCalendarView);
viewRegistry.add('time_off_calendar_all', TimeOffCalendarAllView);
@@ -1,89 +0,0 @@
/** @odoo-module **/
import { qweb as QWeb }from 'web.core';
import CalendarRenderer from "web.CalendarRenderer";
import { TimeOffCalendarPopover } from "./time_off_calendar_popover";
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
*
* @override
* @private
*/
_getFullCalendarOptions() {
const oldOptions = this._super(...arguments);
// Parameters
oldOptions.views.dayGridYear.weekNumbers = true;
oldOptions.views.dayGridYear.weekNumbersWithinDays = false;
return oldOptions;
},
config: Object.assign({}, CalendarRenderer.prototype.config, {
CalendarPopover: TimeOffCalendarPopover,
}),
_getPopoverParams(eventData) {
const params = this._super(...arguments);
let calendarIcon;
const state = eventData.extendedProps.record.state;
if (state === 'validate') {
calendarIcon = 'fa-calendar-check-o';
} else if (state === 'refuse') {
calendarIcon = 'fa-calendar-times-o';
} else if(state) {
calendarIcon = 'fa-calendar-o';
}
params.title = eventData.extendedProps.record.display_name.split(':').slice(0, -1).join(':');
params.template = QWeb.render('hr_holidays.calendar.popover.placeholder', {
color: this.getColor(eventData.color_index),
calendarIcon: calendarIcon,
});
return params;
},
_render() {
return this._super.apply(this, arguments).then(() => {
this.$el.parent().find('.o_calendar_mini').hide();
});
},
/**
* @override
* @private
*/
_renderCalendar() {
this._super(...arguments);
const weekNumbers = this.$el.find('.fc-week-number');
weekNumbers.each( function() {
const weekRow = this.parentNode;
// By default, each month has 6 weeks displayed, hide the week number if there is no days for the week
if (!weekRow.children[1].classList.length &&
!weekRow.children[weekRow.children.length - 1].classList.length) {
this.innerHTML = '';
}
});
},
/*
* 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);
},
});
@@ -1,58 +1,52 @@
/* @odoo-module */
import { useService } from '@web/core/utils/hooks';
import { getLangDateFormat } from 'web.time';
import widgetRegistry from 'web.widgetRegistry';
import { registry } from '@web/core/registry'
import { formatDate, parseDateTime } from "@web/core/l10n/dates";
const { Component, useState, onWillStart, onWillUpdateProps } = owl;
const { DateTime } = luxon;
export class LeaveStatsComponent extends Component {
setup() {
this.rpc = useService('rpc');
this.orm = useService('orm');
this.state = useState({
leaves: [],
departmentLeaves: [],
});
this.date = this.props.record.data.date_from || DateTime.now();
this.department = this.props.record.data.department_id;
this.employee = this.props.record.data.employee_id;
onWillStart(async () => {
await this.loadLeaves(this.date, this.employee);
await this.loadDepartmentLeaves(this.date, this.department, this.employee);
});
onWillUpdateProps(async (nextProps) => {
const dateFrom = nextProps.record.data.date_from || moment();
const dateFrom = nextProps.record.data.date_from || DateTime.now();
const dateChanged = this.date !== dateFrom;
const employee = nextProps.record.data.employee_id.data;
const department = nextProps.record.data.department_id.data;
const employee = nextProps.record.data.employee_id;
const department = nextProps.record.data.department_id;
if (dateChanged || employee && (this.employee && this.employee.id) !== employee.id) {
if (dateChanged || employee && (this.employee && this.employee[0]) !== employee[0]) {
await this.loadLeaves(dateFrom, employee);
}
if (dateChanged || department && (this.department && this.department.id) !== department.id) {
if (dateChanged || department && (this.department && this.department[0]) !== department[0]) {
await this.loadDepartmentLeaves(dateFrom, department, employee);
}
this.date = dateFrom;
this.employee = employee;
this.department = department;
})
}
get date() {
return this.props.record.data.date_from || moment();
}
get department() {
return this.props.record.data.department_id && 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');
return this.date.toFormat('yyyy');
}
async loadDepartmentLeaves(date, department, employee) {
@@ -61,30 +55,26 @@ export class LeaveStatsComponent extends Component {
return;
}
const dateFrom = date.clone().startOf('month');
const dateTo = date.clone().endOf('month');
const dateFormat = getLangDateFormat();
const dateFrom = date.startOf('month');
const dateTo = date.endOf('month');
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'],
]
});
const departmentLeaves = await this.orm.searchRead(
'hr.leave',
[
['department_id', '=', department[0]],
['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,
dateFrom: formatDate(parseDateTime(leave.date_from)),
dateTo: formatDate(parseDateTime(leave.date_to)),
sameEmployee: leave.employee_id[0] === employee[0],
});
});
}
@@ -95,24 +85,21 @@ export class LeaveStatsComponent extends Component {
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'],
}
});
const dateFrom = date.startOf('year');
const dateTo = date.endOf('year');
this.state.leaves = await this.orm.readGroup(
'hr.leave',
[
['employee_id', '=', employee[0]],
['state', '=', 'validate'],
['date_from', '<=', dateTo],
['date_to', '>=', dateFrom]
],
['holiday_status_id', 'number_of_days:sum'],
['holiday_status_id'],
);
}
}
LeaveStatsComponent.template = 'hr_holidays.LeaveStatsComponent';
widgetRegistry.add('hr_leave_stats', LeaveStatsComponent);
registry.category('view_widgets').add('hr_leave_stats', LeaveStatsComponent);
@@ -3,7 +3,7 @@
<div t-name="hr_holidays.LeaveStatsComponent" owl="1" class="o_leave_stats">
<div t-if="employee" id="o_leave_stats_employee">
<div class="o_hr_leave_subtitle">
<t t-esc="employee.display_name"/> in <t t-esc="thisYear"/>
<t t-esc="employee[1]"/> in <t t-esc="thisYear"/>
</div>
<div t-if="state.leaves.length === 0">
None
@@ -16,7 +16,7 @@
<div t-if="department" id="o_leave_stats_department">
<div class="o_horizontal_separator o_hr_leave_subtitle">
<t t-esc="department.display_name"/>
<t t-esc="department[1]"/>
</div>
<div t-if="state.departmentLeaves.length === 0">
None
@@ -1,110 +1,3 @@
.o_timeoff_calendar .o_content {
.fc-dayGridYear-view {
.fc-bgevent {
border-radius: 25px;
}
> .fc-month-container {
> .fc-month {
.fc-dayGridMonth-view {
.fc-has-event {
background-color: unset;
}
}
.fc-content-skeleton {
padding: 0;
tbody {
display: none;
}
}
}
}
}
.o_calendar_filter {
> h5 {
font-weight: bold;
}
}
.fc-week-number {
color: #adb5bd;
}
.o_legacy_calendar_view .o_calendar_widget .fc-dayGridMonth-view {
.fc-week-number {
padding-top: 0.2rem !important;
padding-bottom: 0.3rem !important;
padding-left: 0.3rem !important;
line-height: unset;
span {
font-size: 0.85rem;
}
}
.fc-week-number.fc-widget-header {
font-size: 0.85rem;
}
}
@for $size from 1 through length($o-colors) {
.hr_stress_day_#{$size - 1} {
background-color: lighten(nth($o-colors, $size), 25%) !important;
}
}
}
.o_timeoff_calendar .o_cp_bottom {
@include media-breakpoint-down(md) {
position: relative;
flex-direction: column;
margin-left: -16px;
.o_cp_bottom_right {
position: absolute;
column-gap: 0;
top: 0;
right: 0;
}
.o_cp_bottom_left {
flex: 1;
width: auto;
.o_cp_buttons {
max-width: 100%;
}
.o_calendar_scale_buttons {
display: block;
max-width: 100vw;
}
}
}
@include media-breakpoint-down(sm) {
.o_cp_bottom_left {
button {
padding: 7px 6px;
}
}
}
.o_cp_bottom_left {
flex: 1;
width: auto;
}
.o_cp_bottom_right {
flex: none;
width: auto;
}
}
.o_timeoff_dashboard_popover {
hr {
margin-top: 0px;
margin-bottom: 0px;
}
ul {
list-style: none;
padding-left: 0;
margin: 0px;
}
}
.o_hr_leave_form {
.o_form_sheet {
padding: 0!important;
@@ -1,7 +1,6 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
import { _t } from 'web.core';
import session from 'web.session';
const leaveType = "NotLimitedHR";
const leaveDateFrom = "01/17/2022";
@@ -25,39 +24,30 @@ tour.register('hr_holidays_tour', {
position: 'bottom',
},
{
trigger: '.o_field_widget[name="holiday_status_id"]',
trigger: 'div[name="holiday_status_id"] input',
content: _t("Let's try to create a Sick Time Off, select it in the list"),
position: 'right',
run: function(actions) {
actions.text(leaveType, this.$anchor.find('input'));
}
run: `text ${leaveType}`,
},
{
trigger: ".ui-menu-item > a",
trigger: `.ui-autocomplete .ui-menu-item a:contains("${leaveType}")`,
run: "click",
auto: true,
in_modal: false
in_modal: false,
},
{
trigger: 'input[name="request_date_from"]',
trigger: '.o_field_widget[name="request_date_from"] input',
content: _t("You can select the period you need to take off, from start date to end date"),
position: 'right',
run: function(actions) {
this.$anchor.val(leaveDateFrom);
this.$anchor.trigger("change");
}
run: `text ${leaveDateFrom}`,
},
{
trigger: 'input[name="request_date_to"]',
trigger: '.o_field_widget[name="request_date_to"] input',
content: _t("You can select the period you need to take off, from start date to end date"),
position: 'right',
run: function(actions) {
this.$anchor.val(leaveDateTo);
this.$anchor.trigger("change");
},
auto: true
run: `text ${leaveDateTo}`,
},
{
trigger: 'textarea[name="name"]',
trigger: 'div[name="name"] textarea',
content: _t("Add some description for the people that will validate it"),
run: `text ${description}`,
position: 'right'
@@ -83,15 +73,7 @@ tour.register('hr_holidays_tour', {
position: 'bottom',
run: function(actions) {
const rows = this.$anchor.find('tr.o_data_row');
const createdRow = rows.filter((i, row) => {
const el = $(row);
return el.find(`div[name="all_employee_ids"]:contains("${session.name}")`).length &&
el.find(`td[name="name"]:contains("${description}")`).length &&
el.find(`td[name="holiday_status_id"]:contains("${leaveType}")`).length &&
el.find(`td[name="date_from"]:contains("${leaveDateFrom}")`).length &&
el.find(`td[name="date_to"]:contains("${leaveDateTo}")`).length;
});
actions.click(createdRow);
actions.click(rows[0]);
}
},
{
@@ -0,0 +1,105 @@
/** @odoo-module */
import { CalendarController } from '@web/views/calendar/calendar_controller';
import { FormViewDialog } from '@web/views/view_dialogs/form_view_dialog';
import { serializeDate } from "@web/core/l10n/dates";
import { TimeOffCalendarFilterPanel } from './filter_panel/calendar_filter_panel';
import { TimeOffFormViewDialog } from '../view_dialog/form_view_dialog';
import { useLeaveCancelWizard } from '../hooks';
const { EventBus, useSubEnv } = owl;
export class TimeOffCalendarController extends CalendarController {
setup() {
super.setup();
useSubEnv({
timeOffBus: new EventBus(),
});
this.leaveCancelWizard = useLeaveCancelWizard();
}
get employeeId() {
return this.model.employeeId;
}
newTimeOffRequest() {
const context = {};
if (this.employeeId) {
context['default_employee_id'] = this.employeeId;
}
if (this.model.meta.scale == 'day') {
context['default_date_from'] = serializeDate(
this.model.data.range.start.set({ hours: 7 }), "datetime"
);
context['default_date_to'] = serializeDate(
this.model.data.range.end.set({ hours: 19 }), "datetime"
);
}
this.displayDialog(FormViewDialog, {
resModel: 'hr.leave',
title: this.env._t('New Time Off'),
viewId: this.model.formViewId,
onRecordSaved: () => {
this.model.load();
this.env.timeOffBus.trigger('update_dashboard');
},
context: context,
});
}
newAllocationRequest() {
const context = {
'default_state': 'confirm',
'form_view_ref': 'hr_holidays.hr_leave_allocation_view_form_dashboard',
};
if (this.employeeId) {
context['default_employee_id'] = this.employeeId;
context['form_view_ref'] = 'hr_holidays.hr_leave_allocation_view_form_manager_dashboard';
}
this.displayDialog(FormViewDialog, {
resModel: 'hr.leave.allocation',
title: this.env._t('New Allocation'),
context: context,
});
}
deleteRecord(record) {
if (!record.rawRecord.can_cancel) {
return super.deleteRecord(record);
}
this.leaveCancelWizard(record.id, () => {
this.model.load();
this.env.timeOffBus.trigger('update_dashboard');
});
}
async editRecord(record, context = {}, shouldFetchFormViewId = true) {
return new Promise((resolve) => {
this.displayDialog(
TimeOffFormViewDialog, {
resModel: this.model.resModel,
resId: record.id || false,
context,
title: record.title,
viewId: this.model.formViewId,
onRecordSaved: () => this.model.load(),
onLeaveCancelled: () => {
this.model.load();
this.env.timeOffBus.trigger('update_dashboard');
}
},
{ onClose: () => resolve() }
);
});
}
}
TimeOffCalendarController.template = "hr_holidays.CalendarController";
TimeOffCalendarController.components = {
...TimeOffCalendarController.components,
FilterPanel: TimeOffCalendarFilterPanel,
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.CalendarController" t-inherit="web.CalendarController" t-inherit-mode="primary" owl="1">
<xpath expr="//DatePicker" position="replace"/>
</t>
<t t-name="hr_holidays.CalendarController.controlButtons" t-inherit="web.CalendarController.controlButtons" t-inherit-mode="primary" owl="1">
<xpath expr="//span[hasclass('o_calendar_scale_buttons')]" position="after">
<span class="o_timeoff_buttons">
<button class="btn btn-primary btn-time-off mx-1" t-on-click="newTimeOffRequest" type="button">
New Time Off
</button>
<button class="btn btn-secondary" t-on-click="newAllocationRequest" type="button">
<t t-if="employeeId">New</t> Allocation Request
</button>
<span>
</xpath>
</t>
</templates>
@@ -0,0 +1,53 @@
/** @odoo-module */
import { CalendarModel } from '@web/views/calendar/calendar_model';
import { serializeDate } from "@web/core/l10n/dates";
export class TimeOffCalendarModel extends CalendarModel {
setup(params, services) {
super.setup(params, services);
this.data.stressDays = {};
if (this.env.isSmall) {
this.meta.scale = 'month';
}
}
makeContextDefaults(record) {
const context = super.makeContextDefaults(record);
if (this.employeeId) {
context['default_employee_id'] = this.employeeId;
}
return context;
}
async updateData(data) {
await super.updateData(data);
data.stressDays = await this.fetchStressDays(data);
}
async fetchStressDays(data) {
return this.orm.call("hr.employee", "get_stress_days", [
this.employeeId,
serializeDate(data.range.start, "datetime"),
serializeDate(data.range.end, "datetime"),
]);
}
get stressDays() {
return this.data.stressDays;
}
get employeeId() {
return this.meta.context.employee_id && this.meta.context.employee_id[0] || null;
}
fetchRecords(data) {
const { fieldNames, resModel } = this.meta;
const context = {};
if (!this.employeeId) {
context['short_name'] = 1;
}
return this.orm.searchRead(resModel, this.computeDomain(data), fieldNames, { context });
}
}
@@ -0,0 +1,33 @@
/** @odoo-module */
import { CalendarRenderer } from '@web/views/calendar/calendar_renderer';
import { TimeOffCalendarCommonRenderer } from './common/calendar_common_renderer';
import { TimeOffCalendarYearRenderer } from './year/calendar_year_renderer';
import { TimeOffDashboard } from '../../dashboard/time_off_dashboard';
export class TimeOffCalendarRenderer extends CalendarRenderer {
get employeeId() {
return this.props.model.employeeId;
}
get showDashboard() {
return false;
}
}
TimeOffCalendarRenderer.template = 'hr_holidays.CalendarRenderer';
TimeOffCalendarRenderer.components = {
...TimeOffCalendarRenderer.components,
day: TimeOffCalendarCommonRenderer,
week: TimeOffCalendarCommonRenderer,
month: TimeOffCalendarCommonRenderer,
year: TimeOffCalendarYearRenderer,
TimeOffDashboard,
};
export class TimeOffDashboardCalendarRenderer extends TimeOffCalendarRenderer {
get showDashboard() {
return !this.env.isSmall;
}
}
@@ -0,0 +1,43 @@
.o_timeoff_calendar {
.o_calendar_renderer {
height: unset;
.o_calendar_widget {
@for $size from 1 through length($o-colors) {
.hr_stress_day_#{$size - 1}:not(.fc-disabled-day) {
background-color: lighten(nth($o-colors, $size), 25%) !important;
}
}
}
.fc-bgevent {
border-radius: 25px;
}
@for $i from 1 through length($o-filter_colors) {
$color: nth($o-filter_colors, $i);
.o_calendar_color_#{$i - 1} {
&.o_event_hatched {
background: repeating-linear-gradient(
45deg,
$color,
$color 10px,
rgba($color, 0.4) 10px,
rgba($color, 0.4) 20px
) !important;
.fc-bg {
background: repeating-linear-gradient(
45deg,
$color,
$color 10px,
rgba($color, 0.5) 10px,
rgba($color, 0.5) 20px
);
}
}
}
}
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.CalendarRenderer" owl="1">
<div class="o_timeoff_calendar">
<TimeOffDashboard t-if="showDashboard" employeeId="employeeId"/>
<t t-call="web.CalendarRenderer"/>
</div>
</t>
</templates>
@@ -0,0 +1,25 @@
/** @odoo-module */
import { calendarView } from '@web/views/calendar/calendar_view';
import { TimeOffCalendarController } from './calendar_controller';
import { TimeOffCalendarModel } from './calendar_model';
import { TimeOffCalendarRenderer, TimeOffDashboardCalendarRenderer } from './calendar_renderer';
import { registry } from '@web/core/registry';
const TimeOffCalendarView = {
...calendarView,
Controller: TimeOffCalendarController,
Renderer: TimeOffCalendarRenderer,
Model: TimeOffCalendarModel,
buttonTemplate: "hr_holidays.CalendarController.controlButtons",
}
registry.category('views').add('time_off_calendar', TimeOffCalendarView);
registry.category('views').add('time_off_calendar_dashboard', {
...TimeOffCalendarView,
Renderer: TimeOffDashboardCalendarRenderer,
});
@@ -0,0 +1,25 @@
/** @odoo-module */
import { CalendarCommonPopover } from "@web/views/calendar/calendar_common/calendar_common_popover";
import { useService } from "@web/core/utils/hooks";
export class TimeOffCalendarCommonPopover extends CalendarCommonPopover {
setup() {
super.setup();
this.dialog = useService('dialog');
this.action = useService('action');
}
get isEventDeletable() {
const record = this.props.record.rawRecord;
const state = record.state;
return record.can_cancel || state && !['validate', 'refuse'].includes(state);
}
get isEventEditable() {
const state = this.props.record.rawRecord.state;
return state !== undefined;
}
}
@@ -0,0 +1,23 @@
/** @odoo-module */
import { CalendarCommonRenderer } from '@web/views/calendar/calendar_common/calendar_common_renderer';
import { useStressDays } from '../../hooks';
import { TimeOffCalendarCommonPopover } from './calendar_common_popover';
export class TimeOffCalendarCommonRenderer extends CalendarCommonRenderer {
setup() {
super.setup();
this.stressDays = useStressDays(this.props);
}
onDayRender(info) {
super.onDayRender(info);
this.stressDays(info);
}
}
TimeOffCalendarCommonRenderer.components = {
...TimeOffCalendarCommonRenderer,
Popover: TimeOffCalendarCommonPopover,
}
@@ -0,0 +1,47 @@
/** @odoo-module */
import { CalendarFilterPanel } from "@web/views/calendar/filter_panel/calendar_filter_panel";
import { TimeOffCardMobile } from "../../../dashboard/time_off_card";
import { useService } from "@web/core/utils/hooks";
const { useState, onWillStart } = owl;
export class TimeOffCalendarFilterPanel extends CalendarFilterPanel {
setup() {
super.setup();
this.orm = useService('orm');
this.leaveState = useState({
holidays: [],
});
onWillStart(async () => {
await this.loadFilterData();
});
}
async loadFilterData() {
if(!this.env.isSmall) {
return;
}
const filterData = {};
const data = await this.orm.call(
'hr.leave.type', 'get_days_all_request', [],
);
data.forEach((leave) => {
filterData[leave[3]] = leave;
});
this.leaveState.holidays = filterData;
}
}
TimeOffCalendarFilterPanel.template = 'hr_holidays.CalendarFilterPanel';
TimeOffCalendarFilterPanel.components = {
...TimeOffCalendarFilterPanel.components,
TimeOffCardMobile,
}
TimeOffCalendarFilterPanel.subTemplates = {
filter: "hr_holidays.CalendarFilterPanel.filter",
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.CalendarFilterPanel" t-inherit="web.CalendarFilterPanel" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-foreach='props.model.filterSections']" position="after">
<div class="o_calendar_filter">
<h5>Legend</h5>
<div class="d-flex flex-column">
<span><img src="/hr/static/src/img/icons/plain.svg" width="30px"/> Validated</span>
<span><img src="/hr/static/src/img/icons/hatched.svg" width="30px"/> To Approve</span>
<span><img src="/hr/static/src/img/icons/line.svg" width="30px"/> Refused</span>
</div>
</div>
</xpath>
</t>
<t t-name="hr_holidays.CalendarFilterPanel.filter" t-inherit="web.CalendarFilterPanel.filter" t-inherit-mode="primary" owl="1">
<xpath expr="//span[@t-esc='filter.label']" position="replace">
<span class="o_cw_filter_title text-truncate flex-grow">
<t t-esc="filter.label"/>
<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'" />
</t>
</span>
</xpath>
</t>
</templates>
@@ -0,0 +1,40 @@
/** @odoo-module */
import { CalendarYearRenderer } from '@web/views/calendar/calendar_year/calendar_year_renderer';
import { useStressDays } from '../../hooks';
const { useEffect } = owl;
export class TimeOffCalendarYearRenderer extends CalendarYearRenderer {
setup() {
super.setup();
this.stressDays = useStressDays(this.props);
useEffect((el) => {
for (const week of el) {
const row = week.parentElement;
// Remove the week number if the week is empty.
// FullCalendar always displays 6 weeks even when empty.
if (!row.children[1].classList.length &&
!row.children[row.children.length - 1].classList.length) {
row.remove();
}
}
}, () => [this.rootRef.el && this.rootRef.el.querySelectorAll('.fc-content-skeleton td.fc-week-number')]);
}
get options() {
return Object.assign(super.options, {
weekNumbers: true,
weekNumbersWithinDays: false,
weekLabel: this.env._t('Week'),
});
}
onDayRender(info) {
super.onDayRender(info);
this.stressDays(info);
}
}
@@ -0,0 +1,14 @@
.o_timeoff_calendar {
.o_calendar_widget {
.fc-dayGridYear-view {
.fc-week-number {
color: #adb5bd;
font-size: 0.85rem;
cursor: default;
vertical-align: middle;
line-height: unset;
}
}
}
}
@@ -0,0 +1,35 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
const { useEnv } = owl;
export function useStressDays(props) {
return (info) => {
const date = luxon.DateTime.fromJSDate(info.date).toISODate();
const stressDay = props.model.stressDays[date];
if (stressDay) {
info.el.classList.add('hr_stress_day_' + stressDay);
}
}
}
export function useLeaveCancelWizard() {
const action = useService('action');
const env = useEnv();
return (leaveId, callback) => {
action.doAction({
name: env._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,
});
}
}
@@ -0,0 +1,63 @@
/** @odoo-module */
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
import { registry } from '@web/core/registry';
import { formView } from '@web/views/form/form_view';
import { FormController } from '@web/views/form/form_controller';
import { useLeaveCancelWizard } from '../hooks';
export class TimeOffDialogFormController extends FormController {
setup() {
super.setup();
this.leaveCancelWizard = useLeaveCancelWizard();
}
deleteRecord() {
const leaveId = this.model.root.data.id;
this.props.onCancelLeave();
if (this.model.root.data.can_cancel) {
this.leaveCancelWizard(leaveId, () => {
this.props.onLeaveCancelled();
});
}
}
get canDelete() {
return this.model.root.data.can_cancel || false;
}
}
TimeOffDialogFormController.props = {
...FormController.props,
onCancelLeave: Function,
onLeaveCancelled: Function,
}
registry.category('views').add('timeoff_dialog_form', {
...formView,
Controller: TimeOffDialogFormController,
});
export class TimeOffFormViewDialog extends FormViewDialog {
setup() {
super.setup();
this.viewProps = Object.assign(this.viewProps, {
type: "timeoff_dialog_form",
buttonTemplate: 'hr_holidays.FormViewDialog.buttons',
onCancelLeave: () => {
this.props.close();
},
onLeaveCancelled: this.props.onLeaveCancelled.bind(this),
})
}
}
TimeOffFormViewDialog.props = {
...TimeOffFormViewDialog.props,
onLeaveCancelled: Function,
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.FormViewDialog.buttons" t-inherit="web.FormViewDialog.ToOne.buttons" owl="1">
<xpath expr="//button[contains(@class, 'o_form_button_save')]" position="replace">
<button class="btn btn-primary o_form_button_save" t-on-click="save" data-hotkey="c">Save</button>
</xpath>
<xpath expr="//button[contains(@class, 'o_form_button_cancel')]" position="after">
<button class="btn btn-secondary" t-if="canDelete" t-on-click="deleteRecord" data-hotkey="x">Delete</button>
</xpath>
</t>
</templates>
@@ -1,72 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr_holidays.dashboard.calendar.button">
<button class="btn btn-primary btn-time-off" type="button">
<t t-esc="time_off"/>
</button>
<button class="btn btn-secondary btn-allocation" type="button">
<t t-esc="request"/>
</button>
</t>
<t t-name="hr_holidays.calendar.popover.placeholder">
<div t-attf-class="o_cw_popover popover card shadow #{typeof color === 'number' ? _.str.sprintf('o_calendar_color_%s', color) : ''}" role="tooltip">
<div class="arrow"/>
<div class="card-header d-flex justify-content-between py-2 pe-2">
<h4 class="popover-header border-0 p-0 pt-1"/>
<div class="ms-4">
<i t-if="calendarIcon" t-attf-class="fa {{calendarIcon}}"></i>
<span class="o_cw_popover_close ms-1"><i class="fa fa-close small"/></span>
</div>
</div>
<div class="o_cw_body">
</div>
</div>
</t>
<t t-name="hr_holidays.calendar.popover">
<div class="o_cw_body">
<ul class="list-group list-group-flush">
<li t-if="!widget.hideDate and widget.eventDate.date" class="list-group-item">
<b class="text-capitalize" t-esc="widget.eventDate.date"/> <small t-if="widget.eventDate.duration"><b t-esc="_.str.sprintf('(%s)', widget.eventDate.duration)"/></small>
</li>
<li t-if="!widget.hideTime and widget.eventTime.time" class="list-group-item">
<b t-esc="widget.eventTime.time"/> <small t-if="widget.eventTime.duration"><b t-esc="_.str.sprintf('(%s)', widget.eventTime.duration)"/></small>
</li>
</ul>
<ul class="list-group list-group-flush o_cw_popover_fields_secondary" t-if="widget.display_name">
<li class="list-group-item">
<span class="o_field_char o_field_widget" t-esc="widget.display_name" />
</li>
</ul>
<div class="card-footer border-top" t-if="widget.canEdit or widget.canDelete">
<a t-if="widget.canEdit" href="#" class="btn btn-primary o_cw_popover_edit">Edit</a>
<a t-if="widget.canDelete" href="#" class="btn btn-secondary o_cw_popover_delete ms-2">Delete</a>
</div>
</div>
</t>
<t t-name="FieldRadioIcon.button">
<div t-if="mode === 'edit'" class="form-check o_radio_item" aria-atomic="true">
<input type="radio" class="form-check-input o_radio_input" t-att-checked="checked ? true : undefined" t-att-disabled="disabled ? true : undefined"
t-att-name="name" t-att-data-value="value[0]" t-att-data-index="index" t-att-id="id"/>
<label class="form-check-label o_form_label" t-att-for="id"><img t-attf-src="/hr_holidays/static/src/img/icons/{{value[1]}}" width="48" height="48" /></label>
</div>
<div t-else="">
<img t-attf-src="/hr_holidays/static/src/img/icons/{{value[1]}}" width="48" height="48"/>
</div>
</t>
<t t-name="TimeOff.CalendarView.extend" t-extend="CalendarView">
<t t-jquery='.o_calendar_sidebar_container' t-operation="append">
<div class="o_timeoff_legend">
<h5>Legend</h5>
<img src="/hr/static/src/img/icons/plain.svg" width="30px" class="mb-1"/><span>Validated</span><br/>
<img src="/hr/static/src/img/icons/hatched.svg" width="30px" class="mb-1"/><span>To Approve</span><br/>
<img src="/hr/static/src/img/icons/line.svg" width="30px" class="mb-1"/><span>Refused</span><br/>
</div>
</t>
</t>
</templates>
@@ -1,14 +1,19 @@
odoo.define('hr_holidays.leave_stats_widget_tests', function (require) {
"use strict";
/** @odoo-module */
var FormView = require("web.FormView");
var testUtils = require('web.test_utils');
import { selectDropdownItem, editInput, getFixture } from '@web/../tests/helpers/utils';
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
var createView = testUtils.createView;
QUnit.module('leave_stats_widget', {
beforeEach: function () {
this.data = {
let serverData;
let target;
QUnit.module('leave_stats_widget', (hooks) => {
hooks.beforeEach(() => {
setupViewRegistries();
target = getFixture();
serverData = {
models: {
department: {
fields: {
name: { string: "Name", type: "char" },
@@ -82,74 +87,73 @@ odoo.define('hr_holidays.leave_stats_widget_tests', function (require) {
holiday_type: 'employee',
}]
}
};
}
}, function () {
QUnit.test('leave stats renders correctly', async function (assert) {
assert.expect(5);
var self = this;
var form = await createView({
View: FormView,
model: 'hr.leave',
data: this.data,
arch: '<form string="Leave">' +
'<field name="employee_id"/>' +
'<field name="department_id"/>' +
'<field name="date_from"/>' +
'<widget name="hr_leave_stats"/>' +
'</form>',
res_id: 12,
mockRPC: function (route, args) {
if (args.model === 'hr.leave' && args.method === 'search') {
return Promise.resolve(self.data['hr.leave'].records.map(function (record) { return record.id; }));
}
return this._super.apply(this, arguments);
},
});
const $leaveTypeBody = form.$('.o_leave_stats #o_leave_stats_employee');
const $leavesDepartmentBody = form.$('.o_leave_stats #o_leave_stats_department');
}
};
});
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('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 renders correctly', async (assert) => {
assert.expect(5);
await makeView({
serverData,
type: "form",
resModel: 'hr.leave',
arch: '<form string="Leave">' +
'<field name="employee_id"/>' +
'<field name="department_id"/>' +
'<field name="date_from"/>' +
'<widget name="hr_leave_stats"/>' +
'</form>',
resId: 12,
mockRPC(route, args) {
if (args.model === 'hr.leave' && args.method === 'search') {
return Promise.resolve(this.data['hr.leave'].records.map(function (record) { return record.id; }));
}
},
});
QUnit.test('leave stats reload when employee/department changes', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'hr.leave',
mode: 'edit',
data: this.data,
arch: '<form string="Leave">' +
'<field name="employee_id"/>' +
'<field name="department_id"/>' +
'<field name="date_from"/>' +
'<widget name="hr_leave_stats"/>' +
'</form>',
mockRPC: function (route, args) {
if (args.model === 'hr.leave' && args.method === 'search_read') {
assert.ok(_.some(args.args[0], _.matcher(['department_id', '=', 11])), "It should load department's leaves data");
}
if (args.model === 'hr.leave' && args.method === 'read_group') {
assert.ok(_.some(args.kwargs.domain, _.matcher(['employee_id', '=', 200])), "It should load employee's leaves data");
}
return this._super.apply(this, arguments);
},
});
// Set date => shouldn't load data yet (no employee nor department defined)
await testUtils.fields.editSelect($('input[name="date_from"]'), '2016-10-12 09:00:00');
// Set employee => should load employee's date
await testUtils.fields.many2one.clickOpenDropdown("employee_id");
await testUtils.fields.many2one.clickItem("employee_id", "Jesus");
// Set department => should load department's data
await testUtils.fields.many2one.clickOpenDropdown("department_id");
await testUtils.fields.many2one.clickItem("department_id", "R&D");
form.destroy();
const $leaveTypeBody = target.querySelector('.o_leave_stats #o_leave_stats_employee');
const $leavesDepartmentBody = target.querySelector('.o_leave_stats #o_leave_stats_department');
assert.containsOnce($leaveTypeBody, 'span:contains(Legal Leave)', "it should have leave type");
assert.containsOnce($leaveTypeBody, 'span:contains(6)', "it should have 6 days");
assert.containsN($leavesDepartmentBody, 'span:contains(Richard)', 2, "it should have 2 leaves for Richard");
assert.containsOnce($leavesDepartmentBody, 'span:contains(Jesus)', "it should have 1 leaves for Jesus");
assert.containsOnce($leavesDepartmentBody, 'div.o_horizontal_separator:contains(R&D)', "it should have R&D title");
});
QUnit.test('leave stats reload when employee/department changes', async (assert) => {
assert.expect(3);
await makeView({
serverData,
type: "form",
resModel: 'hr.leave',
mode: 'edit',
arch: '<form string="Leave">' +
'<field name="employee_id"/>' +
'<field name="department_id"/>' +
'<field name="date_from"/>' +
'<widget name="hr_leave_stats"/>' +
'</form>',
mockRPC(route, args) {
if (args.model === 'hr.leave' && args.method === 'search_read') {
assert.ok(_.some(args.kwargs.domain, _.matcher(['department_id', '=', 11])), "It should load department's leaves data");
}
if (args.model === 'hr.leave' && args.method === 'read_group') {
assert.ok(_.some(args.kwargs.domain, _.matcher(['employee_id', '=', 200])), "It should load employee's leaves data");
}
},
});
// Set date => shouldn't load data yet (no employee nor department defined)
await editInput(
target,
"div[name='date_from'] input",
"2016-10-12 09:00:00"
);
// Set employee => should load employee's date
await selectDropdownItem(target, "employee_id", "Jesus");
// Set department => should load department's data
await selectDropdownItem(target, "department_id", "R&D");
});
});
@@ -63,7 +63,7 @@
<field name="model">hr.leave.allocation</field>
<field name="priority">32</field>
<field name="arch" type="xml">
<form string="Allocation Request" js_class="allocation_leave_form">
<form string="Allocation Request">
<field name="can_reset" invisible="1"/>
<field name="can_approve" invisible="1"/>
<field name="holiday_type" invisible="1"/>
@@ -91,7 +91,7 @@
</button>
</div>
<div id="title" class="oe_title">
<h2><field name="name" placeholder="e.g. Time Off type (From validity start to validity end / no limit)" attrs="{'invisible': [('state', 'not in', ('draft', 'confirm'))]}" required="1"/>
<h2><field name="name" class="w-100" placeholder="e.g. Time Off type (From validity start to validity end / no limit)" attrs="{'invisible': [('state', 'not in', ('draft', 'confirm'))]}" required="1"/>
<field name="name_validity" attrs="{'invisible': [('state', 'in', ('draft', 'confirm'))]}"/></h2>
</div>
<group>
@@ -102,7 +102,7 @@
attrs="{'readonly': ['|', ('is_officer', '=', False), ('state', 'not in', ('draft', 'confirm'))]}"/>
<field name="is_officer" invisible="1"/>
<field name="accrual_plan_id" attrs="{'invisible': [('allocation_type', '=', 'regular')], 'readonly': ['|', ('is_officer', '=', False), ('state', 'not in', ('draft', 'confirm'))]}"/>
<div class="o_td_label">
<div class="o_td_label" name="validity_label">
<label for="date_from" string="Validity Period" attrs="{'invisible': ['|', ('allocation_type', '=', 'accrual'), ('state', 'not in', ('draft', 'confirm'))]}"/>
<label for="date_from" string="Start Date" attrs="{'invisible': [('allocation_type', '=', 'regular')]}"/>
</div>
@@ -116,7 +116,7 @@
<field name="number_of_days" invisible="1"/>
<div class="o_td_label">
<label for="number_of_days" string="Duration"
<label for="number_of_days_display" string="Duration"
attrs="{'readonly': [('allocation_type', '=', 'accrual'), ('state', 'not in', ('draft', 'confirm'))]}"/>
</div>
<div name="duration_display">
@@ -224,12 +224,16 @@
<div name="button_box" position="attributes">
<attribute name="invisible">1</attribute>
</div>
<div name="validity_label" position="attributes">
<attribute name="invisible">1</attribute>
</div>
<div name="validity" position="attributes">
<attribute name="invisible">1</attribute>
</div>
<label for="date_from" position="attributes">
<attribute name="invisible">1</attribute>
</label>
<div class="oe_chatter" position="replace"/>
</field>
</record>
@@ -68,7 +68,7 @@
attrs="{
'invisible': [('leave_validation_type', 'in', ['no_validation', 'manager']), '|', ('requires_allocation', '=', 'no'), ('allocation_validation_type', '!=', 'officer')],
'required': ['|',('leave_validation_type', 'in', ['both', 'hr']), ('requires_allocation', '=', 'yes'), ('allocation_validation_type', '=', 'officer')]}"/>
<field name="request_unit" widget="radio-inline"/>
<field name="request_unit"/>
<field name="support_document" string="Allow To Attach Supporting Document" />
<field name="time_type" required="1"/>
<field name="company_id" groups="base.group_multi_company"/>
+30 -16
View File
@@ -220,7 +220,7 @@
<field name="model">hr.leave</field>
<field name="priority">32</field>
<field name="arch" type="xml">
<form string="Time Off Request" class="o_hr_leave_form" js_class="allocation_leave_form">
<form string="Time Off Request" class="o_hr_leave_form">
<field name="active" invisible="1"/>
<field name="can_reset" invisible="1"/>
<field name="can_approve" invisible="1"/>
@@ -256,10 +256,10 @@
<field name="leave_type_request_unit" invisible="1"/>
<div class="o_hr_leave_content row">
<div class="o_hr_leave_column col_left col-md-6 col-12">
<div name="title" class="o_hr_leave_title">
<field name="display_name" invisible="1"/>
<div name="title" class="o_hr_leave_title" invisible="1">
<field name="employee_id" nolabel="1" readonly="1" force_save="1" invisible="1"/>
<field name="employee_ids" invisible="1"/>
<field name="display_name" attrs="{'invisible': [('holiday_status_id', '=', False)]}"/>
</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'}" class="w-100"/>
@@ -397,7 +397,7 @@
<field name="name">hr.leave.view.dashboard</field>
<field name="model">hr.leave</field>
<field name="arch" type="xml">
<calendar js_class="time_off_calendar"
<calendar js_class="time_off_calendar_dashboard"
string="Time Off Request"
form_view_id="%(hr_holidays.hr_leave_view_form_dashboard_new_time_off)d"
event_open_popup="true"
@@ -422,7 +422,17 @@
<field name="name">hr.leave.view.dashboard</field>
<field name="model">hr.leave</field>
<field name="arch" type="xml">
<calendar string="Time Off Request" js_class="time_off_employee_calendar" form_view_id="%(hr_holidays.hr_leave_view_form_dashboard_new_time_off)d" event_open_popup="true" date_start="date_from" date_stop="date_to" mode="year" quick_add="False" show_unusual_days="True" color="color" hide_time="True">
<calendar string="Time Off Request"
js_class="time_off_calendar_dashboard"
form_view_id="%(hr_holidays.hr_leave_view_form_dashboard_new_time_off)d"
event_open_popup="true"
date_start="date_from"
date_stop="date_to"
mode="year"
quick_add="False"
show_unusual_days="True"
color="color"
hide_time="True">
<field name="display_name"/>
<field name="holiday_status_id" filters="1" invisible="1" color="color"/>
<field name="state" invisible="1"/>
@@ -440,6 +450,9 @@
<field name="mode">primary</field>
<field name="priority">16</field>
<field name="arch" type="xml">
<xpath expr="//div[@name='title']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//field[@name='holiday_status_id']" position="before">
<field name="holiday_type" string="Mode"
groups="hr_holidays.group_hr_holidays_user"/>
@@ -455,14 +468,6 @@
'required': [('holiday_type', '=', 'department')],
'invisible': [('holiday_type', '!=', 'department')]
}"/>
</xpath>
<field name="holiday_status_id" position="replace"/>
<div name="title" position="inside">
<h1 class="d-flex flex-row align-items-end justify-content-between o_hr_leave_title">
<field name="holiday_status_id" options="{'no_open': True}" context="{'request_type':'leave', 'from_manager_leave_form': True ,'employee_id': employee_id, 'default_date_from':date_from, 'default_date_to':date_to}"/>
</h1>
</div>
<label id="label_dates" position="before">
<field name="multi_employee" invisible="1" force_save="1"/>
<field name="employee_id" groups="hr_holidays.group_hr_holidays_user" attrs="{
'invisible': ['|', '|', ('holiday_type', '!=', 'employee'), ('state', '!=', 'validate'), ('employee_id', '=', False)]
@@ -471,10 +476,10 @@
'required': [('holiday_type', '=', 'employee'), ('state', 'in', ('draft', 'cancel', 'refuse'))],
'invisible': ['|', ('holiday_type', '!=', 'employee'), '&amp;', ('state', '=', 'validate'), ('employee_id', '!=', False)],
}" widget="many2many_avatar_employee"/>
</label>
</xpath>
<field name="name" position="replace"/>
<field name="user_id" position="before">
<field name="name"/>
<field name="name" widget="text"/>
</field>
<xpath expr="//div[hasclass('col_left')]" position="after">
<div class="o_hr_leave_column col_right col-md-6 col-12">
@@ -495,7 +500,16 @@
<field name="name">hr.leave.view.calendar</field>
<field name="model">hr.leave</field>
<field name="arch" type="xml">
<calendar js_class="time_off_calendar_all" string="Time Off Request" form_view_id="%(hr_holidays.hr_leave_view_form_dashboard)d" event_open_popup="true" date_start="date_from" date_stop="date_to" mode="month" show_unusual_days="True" quick_add="False" color="color">
<calendar js_class="time_off_calendar"
string="Time Off Request"
form_view_id="%(hr_holidays.hr_leave_view_form_dashboard)d"
event_open_popup="true"
date_start="date_from"
date_stop="date_to"
mode="month"
show_unusual_days="True"
quick_add="False"
color="color">
<field name="display_name"/>
<field name="holiday_status_id" color="color" filters="1" invisible="1"/>
<field name="employee_id" filters="1" invisible="1"/>