summaryrefslogtreecommitdiff
path: root/addons/hr_holidays/static/src
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/hr_holidays/static/src
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/hr_holidays/static/src')
-rw-r--r--addons/hr_holidays/static/src/bugfix/bugfix.js88
-rw-r--r--addons/hr_holidays/static/src/bugfix/bugfix.scss6
-rw-r--r--addons/hr_holidays/static/src/bugfix/bugfix.xml11
-rw-r--r--addons/hr_holidays/static/src/bugfix/bugfix_tests.js110
-rw-r--r--addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss12
-rw-r--r--addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml13
-rw-r--r--addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss11
-rw-r--r--addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml13
-rw-r--r--addons/hr_holidays/static/src/components/thread_view/thread_view.js28
-rw-r--r--addons/hr_holidays/static/src/components/thread_view/thread_view.scss8
-rw-r--r--addons/hr_holidays/static/src/components/thread_view/thread_view.xml10
-rw-r--r--addons/hr_holidays/static/src/js/leave_stats_widget.js153
-rw-r--r--addons/hr_holidays/static/src/js/time_off_calendar.js182
-rw-r--r--addons/hr_holidays/static/src/scss/time_off.scss44
-rw-r--r--addons/hr_holidays/static/src/xml/leave_stats_templates.xml65
-rw-r--r--addons/hr_holidays/static/src/xml/time_off_calendar.xml78
16 files changed, 832 insertions, 0 deletions
diff --git a/addons/hr_holidays/static/src/bugfix/bugfix.js b/addons/hr_holidays/static/src/bugfix/bugfix.js
new file mode 100644
index 00000000..41d3e558
--- /dev/null
+++ b/addons/hr_holidays/static/src/bugfix/bugfix.js
@@ -0,0 +1,88 @@
+/**
+ * This file allows introducing new JS modules without contaminating other files.
+ * This is useful when bug fixing requires adding such JS modules in stable
+ * versions of Odoo. Any module that is defined in this file should be isolated
+ * in its own file in master.
+ */
+odoo.define('hr_holidays/static/src/bugfix/bugfix.js', function (require) {
+'use strict';
+
+});
+
+// FIXME move me in hr_holidays/static/src/models/partner/partner.js
+odoo.define('hr_holidays/static/src/models/partner/partner.js', function (require) {
+'use strict';
+
+const {
+ registerClassPatchModel,
+ registerFieldPatchModel,
+ registerInstancePatchModel,
+} = require('mail/static/src/model/model_core.js');
+const { attr, one2one } = require('mail/static/src/model/model_field.js');
+const { clear } = require('mail/static/src/model/model_field_command.js');
+
+const { str_to_datetime } = require('web.time');
+
+registerClassPatchModel('mail.partner', 'hr_holidays/static/src/models/partner/partner.js', {
+ /**
+ * @override
+ */
+ convertData(data) {
+ const data2 = this._super(data);
+ if ('out_of_office_date_end' in data) {
+ data2.outOfOfficeDateEnd = data.out_of_office_date_end ? data.out_of_office_date_end : clear();
+ }
+ return data2;
+ },
+});
+
+registerInstancePatchModel('mail.partner', 'hr_holidays/static/src/models/partner/partner.js', {
+ /**
+ * @private
+ */
+ _computeOutOfOfficeText() {
+ if (!this.outOfOfficeDateEnd) {
+ return clear();
+ }
+ if (!this.env.messaging.locale.language) {
+ return clear();
+ }
+ const currentDate = new Date();
+ const date = str_to_datetime(this.outOfOfficeDateEnd);
+ const options = { day: 'numeric', month: 'short' };
+ if (currentDate.getFullYear() !== date.getFullYear()) {
+ options.year = 'numeric';
+ }
+ const localeCode = this.env.messaging.locale.language.replace(/_/g,'-');
+ const formattedDate = date.toLocaleDateString(localeCode, options);
+ return _.str.sprintf(this.env._t("Out of office until %s"), formattedDate);
+ },
+
+});
+
+registerFieldPatchModel('mail.partner', 'hr/static/src/models/partner/partner.js', {
+ /**
+ * Serves as compute dependency.
+ */
+ locale: one2one('mail.locale', {
+ related: 'messaging.locale',
+ }),
+ /**
+ * Date of end of the out of office period of the partner as string.
+ * String is expected to use Odoo's datetime string format
+ * (examples: '2011-12-01 15:12:35.832' or '2011-12-01 15:12:35').
+ */
+ outOfOfficeDateEnd: attr(),
+ /**
+ * Text shown when partner is out of office.
+ */
+ outOfOfficeText: attr({
+ compute: '_computeOutOfOfficeText',
+ dependencies: [
+ 'locale',
+ 'outOfOfficeDateEnd',
+ ],
+ }),
+});
+
+});
diff --git a/addons/hr_holidays/static/src/bugfix/bugfix.scss b/addons/hr_holidays/static/src/bugfix/bugfix.scss
new file mode 100644
index 00000000..c4272e52
--- /dev/null
+++ b/addons/hr_holidays/static/src/bugfix/bugfix.scss
@@ -0,0 +1,6 @@
+/**
+* This file allows introducing new styles without contaminating other files.
+* This is useful when bug fixing requires adding new components for instance in
+* stable versions of Odoo. Any style that is defined in this file should be isolated
+* in its own file in master.
+*/
diff --git a/addons/hr_holidays/static/src/bugfix/bugfix.xml b/addons/hr_holidays/static/src/bugfix/bugfix.xml
new file mode 100644
index 00000000..c17906f7
--- /dev/null
+++ b/addons/hr_holidays/static/src/bugfix/bugfix.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+<!--
+ This file allows introducing new static templates without contaminating other files.
+ This is useful when bug fixing requires adding new components for instance in stable
+ versions of Odoo. Any template that is defined in this file should be isolated
+ in its own file in master.
+-->
+
+</templates>
diff --git a/addons/hr_holidays/static/src/bugfix/bugfix_tests.js b/addons/hr_holidays/static/src/bugfix/bugfix_tests.js
new file mode 100644
index 00000000..6f655603
--- /dev/null
+++ b/addons/hr_holidays/static/src/bugfix/bugfix_tests.js
@@ -0,0 +1,110 @@
+odoo.define('hr_holidays/static/src/bugfix/bugfix_tests.js', function (require) {
+'use strict';
+
+/**
+ * This file allows introducing new QUnit test modules without contaminating
+ * other test files. This is useful when bug fixing requires adding new
+ * components for instance in stable versions of Odoo. Any test that is defined
+ * in this file should be isolated in its own file in master.
+ */
+QUnit.module('hr_holidays', {}, function () {
+QUnit.module('bugfix', {}, function () {
+QUnit.module('bugfix_tests.js', {
+
+});
+});
+});
+
+});
+
+// FIXME move me in hr_holidays/static/src/components/thread_view/thread_view_tests.js
+odoo.define('hr_holidays/static/src/components/thread_view/thread_view_tests.js', function (require) {
+'use strict';
+
+const components = {
+ ThreadView: require('mail/static/src/components/thread_view/thread_view.js'),
+};
+const {
+ afterEach,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('hr_holidays', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('thread_view', {}, function () {
+QUnit.module('thread_view_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ /**
+ * @param {mail.thread_view} threadView
+ * @param {Object} [otherProps={}]
+ */
+ this.createThreadViewComponent = async (threadView, otherProps = {}) => {
+ const target = this.widget.el;
+ const props = Object.assign({ threadViewLocalId: threadView.localId }, otherProps);
+ await createRootComponent(this, components.ThreadView, { props, target });
+ };
+
+ this.start = async params => {
+ const { afterEvent, env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.afterEvent = afterEvent;
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('out of office message on direct chat with out of office partner', async function (assert) {
+ assert.expect(2);
+
+ // Returning date of the out of office partner, simulates he'll be back in a month
+ const returningDate = moment.utc().add(1, 'month');
+ // Needed partner & user to allow simulation of message reception
+ this.data['res.partner'].records.push({
+ id: 11,
+ name: "Foreigner partner",
+ out_of_office_date_end: returningDate.format("YYYY-MM-DD HH:mm:ss"),
+ });
+ this.data['mail.channel'].records = [{
+ channel_type: 'chat',
+ id: 20,
+ members: [this.data.currentPartnerId, 11],
+ }];
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel'
+ });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
+ await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
+ assert.containsOnce(
+ document.body,
+ '.o_ThreadView_outOfOffice',
+ "should have an out of office alert on thread view"
+ );
+ const formattedDate = returningDate.toDate().toLocaleDateString(
+ this.env.messaging.locale.language.replace(/_/g,'-'),
+ { day: 'numeric', month: 'short' }
+ );
+ assert.ok(
+ document.querySelector('.o_ThreadView_outOfOffice').textContent.includes(formattedDate),
+ "out of office message should mention the returning date"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss
new file mode 100644
index 00000000..e981e30f
--- /dev/null
+++ b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss
@@ -0,0 +1,12 @@
+// -----------------------------------------------------------------------------
+// Style
+// -----------------------------------------------------------------------------
+
+.o_PartnerImStatusIcon_icon {
+ &.o-leave-online {
+ color: $o-enterprise-primary-color;
+ }
+ &.o-leave-offline {
+ color: theme-color('warning');
+ }
+}
diff --git a/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml
new file mode 100644
index 00000000..c07377d3
--- /dev/null
+++ b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-inherit="mail.PartnerImStatusIcon" t-inherit-mode="extension">
+ <xpath expr="//*[@name='rootCondition']" position="inside">
+ <t t-if="partner.im_status === 'leave_online'">
+ <i class="o_PartnerImStatusIcon_icon o-leave-online fa fa-plane fa-stack-1x" title="Online" role="img" aria-label="User is online"/>
+ </t>
+ <t t-if="partner.im_status === 'leave_offline'">
+ <i class="o_PartnerImStatusIcon_icon o-leave-offline fa fa-plane fa-stack-1x" title="Out of office" role="img" aria-label="User is out of office"/>
+ </t>
+ </xpath>
+ </t>
+</templates>
diff --git a/addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss
new file mode 100644
index 00000000..4caabb3b
--- /dev/null
+++ b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss
@@ -0,0 +1,11 @@
+// ------------------------------------------------------------------
+// Style
+// ------------------------------------------------------------------
+
+.o_ThreadIcon_leaveOnline {
+ color: $o-enterprise-primary-color;
+}
+
+.o_ThreadIcon_leaveOffline {
+ color: theme-color('warning');
+}
diff --git a/addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml
new file mode 100644
index 00000000..f8a728a9
--- /dev/null
+++ b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-inherit="mail.ThreadIcon" t-inherit-mode="extension">
+ <xpath expr="//*[@name='noImStatusCondition']" position="before">
+ <t t-elif="thread.correspondent.im_status === 'leave_online'">
+ <div class="o_ThreadIcon_leaveOnline fa fa-plane" title="Online"/>
+ </t>
+ <t t-elif="thread.correspondent.im_status === 'leave_offline'">
+ <div class="o_ThreadIcon_leaveOffline fa fa-plane" title="Out of office"/>
+ </t>
+ </xpath>
+ </t>
+</templates>
diff --git a/addons/hr_holidays/static/src/components/thread_view/thread_view.js b/addons/hr_holidays/static/src/components/thread_view/thread_view.js
new file mode 100644
index 00000000..73ca28d4
--- /dev/null
+++ b/addons/hr_holidays/static/src/components/thread_view/thread_view.js
@@ -0,0 +1,28 @@
+odoo.define('hr_holidays/static/src/components/thread_view/thread_view.js', function (require) {
+'use strict';
+
+const components = {
+ ThreadView: require('mail/static/src/components/thread_view/thread_view.js'),
+};
+
+const { patch } = require('web.utils');
+
+patch(components.ThreadView, 'hr_holidays/static/src/components/thread_view/thread_view.js', {
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _useStoreSelector(props) {
+ const res = this._super(...arguments);
+ const thread = res.thread;
+ const correspondent = thread && thread.correspondent;
+ return Object.assign({}, res, {
+ correspondentOutOfOfficeText: correspondent && correspondent.outOfOfficeText,
+ });
+ },
+});
+
+});
diff --git a/addons/hr_holidays/static/src/components/thread_view/thread_view.scss b/addons/hr_holidays/static/src/components/thread_view/thread_view.scss
new file mode 100644
index 00000000..9cecd24e
--- /dev/null
+++ b/addons/hr_holidays/static/src/components/thread_view/thread_view.scss
@@ -0,0 +1,8 @@
+// -----------------------------------------------------------------------------
+// Layout
+// -----------------------------------------------------------------------------
+
+.o_ThreadView_outOfOffice {
+ margin-top: 0;
+ margin-bottom: 0;
+}
diff --git a/addons/hr_holidays/static/src/components/thread_view/thread_view.xml b/addons/hr_holidays/static/src/components/thread_view/thread_view.xml
new file mode 100644
index 00000000..796dcfba
--- /dev/null
+++ b/addons/hr_holidays/static/src/components/thread_view/thread_view.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-inherit="mail.ThreadView" t-inherit-mode="extension">
+ <xpath expr="//*[@name='loadingCondition']" position="before">
+ <t t-if="threadView.thread.correspondent and threadView.thread.correspondent.outOfOfficeText">
+ <div class="o_ThreadView_outOfOffice alert alert-primary" t-esc="threadView.thread.correspondent.outOfOfficeText" role="alert"/>
+ </t>
+ </xpath>
+ </t>
+</templates>
diff --git a/addons/hr_holidays/static/src/js/leave_stats_widget.js b/addons/hr_holidays/static/src/js/leave_stats_widget.js
new file mode 100644
index 00000000..2d7f5d64
--- /dev/null
+++ b/addons/hr_holidays/static/src/js/leave_stats_widget.js
@@ -0,0 +1,153 @@
+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;
+});
diff --git a/addons/hr_holidays/static/src/js/time_off_calendar.js b/addons/hr_holidays/static/src/js/time_off_calendar.js
new file mode 100644
index 00000000..ef9f262d
--- /dev/null
+++ b/addons/hr_holidays/static/src/js/time_off_calendar.js
@@ -0,0 +1,182 @@
+odoo.define('hr_holidays.dashboard.view_custo', function(require) {
+ 'use strict';
+
+ var core = require('web.core');
+ var CalendarPopover = require('web.CalendarPopover');
+ var CalendarController = require("web.CalendarController");
+ var CalendarRenderer = require("web.CalendarRenderer");
+ var CalendarView = require("web.CalendarView");
+ var viewRegistry = require('web.view_registry');
+
+ var _t = core._t;
+ var QWeb = core.qweb;
+
+ var TimeOffCalendarPopover = CalendarPopover.extend({
+ template: 'hr_holidays.calendar.popover',
+
+ init: function (parent, eventInfo) {
+ this._super.apply(this, arguments);
+ const state = this.event.extendedProps.record.state;
+ this.canDelete = state && ['validate', 'refuse'].indexOf(state) === -1;
+ 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;
+ }
+ },
+ });
+
+ var TimeOffCalendarController = CalendarController.extend({
+ events: _.extend({}, CalendarController.prototype.events, {
+ 'click .btn-time-off': '_onNewTimeOff',
+ 'click .btn-allocation': '_onNewAllocation',
+ }),
+
+ /**
+ * @override
+ */
+ start: function () {
+ 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: function ($node) {
+ this._super.apply(this, arguments);
+
+ $(QWeb.render('hr_holidays.dashboard.calendar.button', {
+ time_off: _t('New Time Off'),
+ request: _t('New Allocation'),
+ })).appendTo(this.$buttons);
+
+ if ($node) {
+ this.$buttons.appendTo($node);
+ } else {
+ this.$('.o_calendar_buttons').replaceWith(this.$buttons);
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Action: create a new time off request
+ *
+ * @private
+ */
+ _onNewTimeOff: function () {
+ var self = this;
+
+ this.do_action('hr_holidays.hr_leave_action_my_request', {
+ on_close: function () {
+ self.reload();
+ }
+ });
+ },
+
+ /**
+ * Action: create a new allocation request
+ *
+ * @private
+ */
+ _onNewAllocation: function () {
+ var self = this;
+ this.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'hr.leave.allocation',
+ name: 'New Allocation Request',
+ views: [[false,'form']],
+ context: {'form_view_ref': 'hr_holidays.hr_leave_allocation_view_form_dashboard'},
+ target: 'new',
+ }, {
+ on_close: function () {
+ self.reload();
+ }
+ });
+ },
+ });
+
+ var TimeOffPopoverRenderer = CalendarRenderer.extend({
+ config: _.extend({}, CalendarRenderer.prototype.config, {
+ CalendarPopover: TimeOffCalendarPopover,
+ }),
+
+ _getPopoverParams: function (eventData) {
+ let params = this._super.apply(this, arguments);
+ let calendarIcon;
+ let 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: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ self.$el.parent().find('.o_calendar_mini').hide();
+ });
+ },
+ });
+
+ var TimeOffCalendarRenderer = TimeOffPopoverRenderer.extend({
+ _render: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ return self._rpc({
+ model: 'hr.leave.type',
+ method: 'get_days_all_request',
+ context: self.context,
+ });
+ }).then(function (result) {
+ self.$el.parent().find('.o_calendar_mini').hide();
+ self.$el.parent().find('.o_timeoff_container').remove();
+ var elem = QWeb.render('hr_holidays.dashboard_calendar_header', {
+ timeoffs: result,
+ });
+ self.$el.before(elem);
+ });
+ },
+ });
+ var TimeOffCalendarView = CalendarView.extend({
+ config: _.extend({}, CalendarView.prototype.config, {
+ Controller: TimeOffCalendarController,
+ Renderer: TimeOffCalendarRenderer,
+ }),
+ });
+
+ /**
+ * Calendar shown in the "Everyone" menu
+ */
+ var TimeOffCalendarAllView = CalendarView.extend({
+ config: _.extend({}, CalendarView.prototype.config, {
+ Renderer: TimeOffPopoverRenderer,
+ }),
+ });
+
+ viewRegistry.add('time_off_calendar', TimeOffCalendarView);
+ viewRegistry.add('time_off_calendar_all', TimeOffCalendarAllView);
+});
diff --git a/addons/hr_holidays/static/src/scss/time_off.scss b/addons/hr_holidays/static/src/scss/time_off.scss
new file mode 100644
index 00000000..f37b89ca
--- /dev/null
+++ b/addons/hr_holidays/static/src/scss/time_off.scss
@@ -0,0 +1,44 @@
+.o_timeoff_calendar .o_content {
+ .o_timeoff_container {
+ height: 6rem;
+ }
+
+ .o_calendar_container {
+ height: calc(100% - 6rem);
+ }
+
+ @include media-breakpoint-down(sm) {
+ .o_timeoff_container {
+ height: 15%;
+ }
+
+ .o_calendar_container {
+ height: 85%;
+ }
+ }
+
+ .o_timeoff_card {
+ border-right: #adb5bd solid 2px;
+ text-align: center;
+ margin-top: 4px;
+ div {
+ line-height: 1;
+ }
+ }
+
+ .o_timeoff_card_last {
+ border-right: 0px;
+ }
+
+ .o_timeoff_big {
+ font-size: 20px;
+ }
+
+ .o_timeoff_purple {
+ color: $o-enterprise-color;
+ }
+
+ .o_timeoff_green {
+ color: $o-enterprise-primary-color;
+ }
+}
diff --git a/addons/hr_holidays/static/src/xml/leave_stats_templates.xml b/addons/hr_holidays/static/src/xml/leave_stats_templates.xml
new file mode 100644
index 00000000..237ffba9
--- /dev/null
+++ b/addons/hr_holidays/static/src/xml/leave_stats_templates.xml
@@ -0,0 +1,65 @@
+<?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>
diff --git a/addons/hr_holidays/static/src/xml/time_off_calendar.xml b/addons/hr_holidays/static/src/xml/time_off_calendar.xml
new file mode 100644
index 00000000..b041222d
--- /dev/null
+++ b/addons/hr_holidays/static/src/xml/time_off_calendar.xml
@@ -0,0 +1,78 @@
+<?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 {{ timeoff_last ? 'o_timeoff_card_last' : '' }}">
+ <t t-set="need_allocation" t-value="timeoff[2] !== 'no'"/>
+ <t t-set="cl" t-value="'text-muted'"/>
+
+ <t t-if="need_allocation &amp;&amp; timeoff[1]['virtual_remaining_leaves'] &gt; 0">
+ <t t-set="cl" t-value="'o_timeoff_green'"/>
+ </t>
+
+ <div class="mt-2">
+ <t t-if="need_allocation">
+ <span t-esc="timeoff[1]['virtual_remaining_leaves']" class="o_timeoff_big o_timeoff_purple"/> / <span t-esc="timeoff[1]['max_leaves']"/> <t t-if="timeoff[1]['request_unit'] == 'hour'">Hours</t><t t-else="">Days</t>
+ </t>
+ <t t-else="">
+ <span t-esc="timeoff[1]['virtual_leaves_taken']" class="o_timeoff_big o_timeoff_purple"/> <t t-if="timeoff[1]['request_unit'] == 'hour'">Hours</t><t t-else="">Days</t>
+ </t>
+ </div>
+
+ <b><span t-esc="timeoff[0]" class="o_timeoff_name"/></b>
+
+ <span class="mb-4" t-if="need_allocation">
+ <span t-attf-class="mr-1 font-weight-bold {{ cl }}" t-esc="timeoff[1]['virtual_leaves_taken']"/><span>taken</span>
+ <t t-if="timeoff[3]"> (Expire on <span t-esc="moment(timeoff[3]).format('L')"/>)</t>
+ </span>
+ </div>
+ </div>
+ </t>
+
+ <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 pr-2">
+ <h4 class="popover-header border-0 p-0 pt-1"/>
+ <div class="ml-4">
+ <i t-if="calendarIcon" t-attf-class="fa {{calendarIcon}}"></i>
+ <span class="o_cw_popover_close ml-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 ml-2">Delete</a>
+ </div>
+ </div>
+ </t>
+</templates>