summaryrefslogtreecommitdiff
path: root/addons/hr_holidays/static
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
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/hr_holidays/static')
-rw-r--r--addons/hr_holidays/static/description/icon.pngbin0 -> 9961 bytes
-rw-r--r--addons/hr_holidays/static/description/icon.svg24
-rw-r--r--addons/hr_holidays/static/scss/hr_leave_mobile.scss6
-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
-rw-r--r--addons/hr_holidays/static/tests/helpers/mock_models.js28
-rw-r--r--addons/hr_holidays/static/tests/helpers/mock_server.js29
-rw-r--r--addons/hr_holidays/static/tests/test_leave_stats_widget.js156
22 files changed, 1075 insertions, 0 deletions
diff --git a/addons/hr_holidays/static/description/icon.png b/addons/hr_holidays/static/description/icon.png
new file mode 100644
index 00000000..e6337c26
--- /dev/null
+++ b/addons/hr_holidays/static/description/icon.png
Binary files differ
diff --git a/addons/hr_holidays/static/description/icon.svg b/addons/hr_holidays/static/description/icon.svg
new file mode 100644
index 00000000..0395af2a
--- /dev/null
+++ b/addons/hr_holidays/static/description/icon.svg
@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
+ <defs>
+ <path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
+ <linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
+ <stop offset="0%" stop-color="#CDC484"/>
+ <stop offset="100%" stop-color="#B5AA59"/>
+ </linearGradient>
+ <path id="icon-d" d="M27.2777778,16.2348485 C32.4714536,16.2348485 36.681713,20.506348 36.681713,25.7755682 C36.681713,31.0447884 32.4714536,35.3162879 27.2777778,35.3162879 C22.0841019,35.3162879 17.8738426,31.0447884 17.8738426,25.7755682 C17.8738426,20.506348 22.0841019,16.2348485 27.2777778,16.2348485 Z M32,36 C32,35.5868456 24.1175854,37.5736818 20.6967864,35.0773525 L16.5053937,36.1404272 C13.9936026,36.7775087 12.2314815,39.0671622 12.2314815,41.6939608 L12.2314815,43.9029356 C12.2314815,45.4837136 13.4945475,46.7651515 15.052662,46.7651515 C25.363233,46.7651515 30.5185185,46.7651515 30.5185185,46.7651515 C28.6666667,40.4242424 32,36.4131544 32,36 Z M48.5116455,29.4292211 L49.8821338,32.7804044 C50.108418,33.3336802 50.7775488,33.5511019 51.2858154,33.2365172 L54.3643115,31.3309087 C55.1962939,30.8159288 56.2338818,31.569761 56.0011523,32.5201752 L55.140166,36.0368953 C54.9979932,36.61751 55.4115674,37.186738 56.0077051,37.230942 L59.6183691,37.4987982 C60.5941357,37.5712005 60.9904688,38.790921 60.2436182,39.4230444 L57.4800293,41.7619923 C57.0237549,42.1481736 57.0237549,42.8517324 57.4800293,43.2379137 L60.2436719,45.5769153 C60.9905762,46.2090387 60.5942432,47.4287592 59.6184229,47.5011615 L56.0077588,47.7690177 C55.4116211,47.8132217 54.9980469,48.3824497 55.1402197,48.9630644 L56.0012061,52.4797846 C56.2338818,53.4301987 55.1962939,54.1840309 54.3643652,53.669051 L51.2858691,51.7634425 C50.7776025,51.4488041 50.108418,51.6662258 49.8821875,52.2195553 L48.5116992,55.5707386 C48.1413086,56.4764115 46.8587988,56.4764115 46.4884082,55.5707386 L45.1179199,52.2195553 C44.8916357,51.6662795 44.2225049,51.4488578 43.7142383,51.7634425 L40.6356885,53.669051 C39.8037061,54.1840309 38.7661182,53.4301987 38.9988477,52.4797846 L39.859834,48.9630644 C40.0020068,48.3824497 39.5884326,47.8132217 38.9922949,47.7690177 L35.3816309,47.5011615 C34.4058643,47.4287592 34.0095313,46.2090387 34.7563818,45.5769153 L37.5199707,43.2379674 C37.9762451,42.8517862 37.9762451,42.1482273 37.5199707,41.762046 L34.7563281,39.4230444 C34.0094238,38.790921 34.4057568,37.5712005 35.3815771,37.4987982 L38.9922412,37.230942 C39.5883789,37.186738 40.0019531,36.61751 39.8597803,36.0368953 L38.9987939,32.5201752 C38.7661182,31.569761 39.8037061,30.8159288 40.6356348,31.3309087 L43.7141846,33.2365172 C44.2224512,33.5511556 44.891582,33.3337339 45.1178662,32.7804044 L46.4883545,29.4292211 C46.8587451,28.5236019 48.1412549,28.5236019 48.5116455,29.4292211 Z M54.8046875,42.4999799 C54.8046875,38.4721469 51.5277832,35.1952995 47.5,35.1952995 C43.4721631,35.1952995 40.1953125,38.4721469 40.1953125,42.4999799 C40.1953125,46.5278128 43.4721631,49.8046602 47.5,49.8046602 C51.5277832,49.8046602 54.8046875,46.5278128 54.8046875,42.4999799 Z M53.0859375,42.4999799 C53.0859375,45.5800843 50.5801074,48.0859119 47.5,48.0859119 C44.4198926,48.0859119 41.9140625,45.5800843 41.9140625,42.4999799 C41.9140625,39.4198754 44.4198926,36.9140478 47.5,36.9140478 C50.5801074,36.9140478 53.0859375,39.4198754 53.0859375,42.4999799 Z"/>
+ <path id="icon-e" d="M27.2777778,14.2348485 C32.4714536,14.2348485 36.681713,18.506348 36.681713,23.7755682 C36.681713,29.0447884 32.4714536,33.3162879 27.2777778,33.3162879 C22.0841019,33.3162879 17.8738426,29.0447884 17.8738426,23.7755682 C17.8738426,18.506348 22.0841019,14.2348485 27.2777778,14.2348485 Z M32,34 C32,33.5868456 24.1175854,35.5736818 20.6967864,33.0773525 L16.5053937,34.1404272 C13.9936026,34.7775087 12.2314815,37.0671622 12.2314815,39.6939608 L12.2314815,41.9029356 C12.2314815,43.4837136 13.4945475,44.7651515 15.052662,44.7651515 C25.363233,44.7651515 30.5185185,44.7651515 30.5185185,44.7651515 C28.6666667,38.4242424 32,34.4131544 32,34 Z M48.5116455,27.4292211 L49.8821338,30.7804044 C50.108418,31.3336802 50.7775488,31.5511019 51.2858154,31.2365172 L54.3643115,29.3309087 C55.1962939,28.8159288 56.2338818,29.569761 56.0011523,30.5201752 L55.140166,34.0368953 C54.9979932,34.61751 55.4115674,35.186738 56.0077051,35.230942 L59.6183691,35.4987982 C60.5941357,35.5712005 60.9904688,36.790921 60.2436182,37.4230444 L57.4800293,39.7619923 C57.0237549,40.1481736 57.0237549,40.8517324 57.4800293,41.2379137 L60.2436719,43.5769153 C60.9905762,44.2090387 60.5942432,45.4287592 59.6184229,45.5011615 L56.0077588,45.7690177 C55.4116211,45.8132217 54.9980469,46.3824497 55.1402197,46.9630644 L56.0012061,50.4797846 C56.2338818,51.4301987 55.1962939,52.1840309 54.3643652,51.669051 L51.2858691,49.7634425 C50.7776025,49.4488041 50.108418,49.6662258 49.8821875,50.2195553 L48.5116992,53.5707386 C48.1413086,54.4764115 46.8587988,54.4764115 46.4884082,53.5707386 L45.1179199,50.2195553 C44.8916357,49.6662795 44.2225049,49.4488578 43.7142383,49.7634425 L40.6356885,51.669051 C39.8037061,52.1840309 38.7661182,51.4301987 38.9988477,50.4797846 L39.859834,46.9630644 C40.0020068,46.3824497 39.5884326,45.8132217 38.9922949,45.7690177 L35.3816309,45.5011615 C34.4058643,45.4287592 34.0095313,44.2090387 34.7563818,43.5769153 L37.5199707,41.2379674 C37.9762451,40.8517862 37.9762451,40.1482273 37.5199707,39.762046 L34.7563281,37.4230444 C34.0094238,36.790921 34.4057568,35.5712005 35.3815771,35.4987982 L38.9922412,35.230942 C39.5883789,35.186738 40.0019531,34.61751 39.8597803,34.0368953 L38.9987939,30.5201752 C38.7661182,29.569761 39.8037061,28.8159288 40.6356348,29.3309087 L43.7141846,31.2365172 C44.2224512,31.5511556 44.891582,31.3337339 45.1178662,30.7804044 L46.4883545,27.4292211 C46.8587451,26.5236019 48.1412549,26.5236019 48.5116455,27.4292211 Z M54.8046875,40.4999799 C54.8046875,36.4721469 51.5277832,33.1952995 47.5,33.1952995 C43.4721631,33.1952995 40.1953125,36.4721469 40.1953125,40.4999799 C40.1953125,44.5278128 43.4721631,47.8046602 47.5,47.8046602 C51.5277832,47.8046602 54.8046875,44.5278128 54.8046875,40.4999799 Z M53.0859375,40.4999799 C53.0859375,43.5800843 50.5801074,46.0859119 47.5,46.0859119 C44.4198926,46.0859119 41.9140625,43.5800843 41.9140625,40.4999799 C41.9140625,37.4198754 44.4198926,34.9140478 47.5,34.9140478 C50.5801074,34.9140478 53.0859375,37.4198754 53.0859375,40.4999799 Z"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <mask id="icon-b" fill="#fff">
+ <use xlink:href="#icon-a"/>
+ </mask>
+ <g mask="url(#icon-b)">
+ <rect width="70" height="70" fill="url(#icon-c)"/>
+ <path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
+ <path fill="#393939" d="M43.6444444,52 L4,52 C2,52 -7.10542736e-15,51.8514286 0,47.84 L2.21121142e-16,23.3197384 L20,0 L36,9.36 L30.6694253,16.3853185 L31.9690606,16.0436983 L28.4526012,19.3069352 L28.4526012,24.5134324 L39.2011791,11.8378465 L42.1902641,14.2434656 L46.8367307,9.36 L51,14.56 L60.3723121,27.0292074 L43.6444444,52 Z" opacity=".324" transform="translate(0 18)"/>
+ <path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
+ <use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#icon-d"/>
+ <use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-e"/>
+ </g>
+ </g>
+</svg>
diff --git a/addons/hr_holidays/static/scss/hr_leave_mobile.scss b/addons/hr_holidays/static/scss/hr_leave_mobile.scss
new file mode 100644
index 00000000..7dabf58d
--- /dev/null
+++ b/addons/hr_holidays/static/scss/hr_leave_mobile.scss
@@ -0,0 +1,6 @@
+@include media-breakpoint-down(sm) {
+ .o_hr_holidays_dates {
+ display: flex;
+ flex-flow: column;
+ }
+}
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>
diff --git a/addons/hr_holidays/static/tests/helpers/mock_models.js b/addons/hr_holidays/static/tests/helpers/mock_models.js
new file mode 100644
index 00000000..8513f29d
--- /dev/null
+++ b/addons/hr_holidays/static/tests/helpers/mock_models.js
@@ -0,0 +1,28 @@
+odoo.define('hr_holidays/static/tests/helpers/mock_models.js', function (require) {
+'use strict';
+
+const MockModels = require('mail/static/tests/helpers/mock_models.js');
+
+MockModels.patch('hr_holidays/static/tests/helpers/mock_models.js', T =>
+ class extends T {
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ static generateData() {
+ const data = super.generateData(...arguments);
+ Object.assign(data['res.partner'].fields, {
+ // Not a real field but ease the testing
+ out_of_office_date_end: { type: 'datetime' },
+ });
+ return data;
+ }
+
+ }
+);
+
+});
diff --git a/addons/hr_holidays/static/tests/helpers/mock_server.js b/addons/hr_holidays/static/tests/helpers/mock_server.js
new file mode 100644
index 00000000..f4b8f640
--- /dev/null
+++ b/addons/hr_holidays/static/tests/helpers/mock_server.js
@@ -0,0 +1,29 @@
+odoo.define('hr_holidays/static/tests/helpers/mock_server.js', function (require) {
+'use strict';
+
+require('mail.MockServer'); // ensure mail overrides are applied first
+
+const MockServer = require('web.MockServer');
+
+MockServer.include({
+ /**
+ * Overrides to add visitor information to livechat channels.
+ *
+ * @override
+ */
+ _mockMailChannelPartnerInfo(ids, extra_info) {
+ const partnerInfos = this._super(...arguments);
+ const partners = this._getRecords(
+ 'res.partner',
+ [['id', 'in', ids]],
+ { active_test: false },
+ );
+ for (const partner of partners) {
+ // Not a real field but ease the testing
+ partnerInfos[partner.id].out_of_office_date_end = partner.out_of_office_date_end;
+ }
+ return partnerInfos;
+ },
+});
+
+});
diff --git a/addons/hr_holidays/static/tests/test_leave_stats_widget.js b/addons/hr_holidays/static/tests/test_leave_stats_widget.js
new file mode 100644
index 00000000..157459dc
--- /dev/null
+++ b/addons/hr_holidays/static/tests/test_leave_stats_widget.js
@@ -0,0 +1,156 @@
+odoo.define('hr_holidays.leave_stats_widget_tests', function (require) {
+ "use strict";
+
+ var FormView = require("web.FormView");
+ var testUtils = require('web.test_utils');
+
+ var createView = testUtils.createView;
+
+ QUnit.module('leave_stats_widget', {
+ beforeEach: function () {
+ this.data = {
+ department: {
+ fields: {
+ name: { string: "Name", type: "char" },
+ },
+ records: [{id:11, name: "R&D"}],
+ },
+ employee: {
+ fields: {
+ name: { string: "Name", type: "char" },
+ department_id: { string: "Department", type: "many2one", relation: 'department' },
+ },
+ records: [{
+ id: 100,
+ name: "Richard",
+ department_id: 11,
+ },{
+ id: 200,
+ name: "Jesus",
+ department_id: 11,
+ }],
+ },
+ 'hr.leave.type': {
+ fields: {
+ name: { string: "Name", type: "char" }
+ },
+ records: [{
+ id: 55,
+ name: "Legal Leave",
+ }]
+ },
+ 'hr.leave': {
+ fields: {
+ employee_id: { string: "Employee", type: "many2one", relation: 'employee' },
+ department_id: { string: "Department", type: "many2one", relation: 'department' },
+ date_from: { string: "From", type: "datetime" },
+ date_to: { string: "To", type: "datetime" },
+ holiday_status_id: { string: "Leave type", type: "many2one", relation: 'hr.leave.type' },
+ state: { string: "State", type: "char" },
+ holiday_type: { string: "Holiday Type", type: "char" },
+ number_of_days: { string: "State", type: "integer" },
+ },
+ records: [{
+ id: 12,
+ employee_id: 100,
+ department_id: 11,
+ date_from: "2016-10-20 09:00:00",
+ date_to: "2016-10-25 18:00:00",
+ holiday_status_id: 55,
+ state: 'validate',
+ number_of_days: 5,
+ holiday_type: 'employee',
+ },{
+ id: 13,
+ employee_id: 100,
+ department_id: 11,
+ date_from: "2016-10-2 09:00:00",
+ date_to: "2016-10-2 18:00:00",
+ holiday_status_id: 55,
+ state: 'validate',
+ number_of_days: 1,
+ holiday_type: 'employee',
+ },{
+ id: 14,
+ employee_id: 200,
+ department_id: 11,
+ date_from: "2016-10-15 09:00:00",
+ date_to: "2016-10-20 18:00:00",
+ holiday_status_id: 55,
+ state: 'validate',
+ number_of_days: 8,
+ 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);
+ },
+ });
+ var $leaveTypeBody = form.$('.o_leave_stats table:first > tbody');
+ var $leavesDepartmentBody = form.$('.o_leave_stats table:nth-child(2) > tbody');
+ var $leavesDepartmentHeader = form.$('.o_leave_stats table:nth-child(2) > thead');
+
+ assert.strictEqual($leaveTypeBody.find('td:contains(Legal Leave)').length, 1, "it should have leave type");
+ assert.strictEqual($leaveTypeBody.find('td:contains(6)').length, 1, "it should have 6 days");
+
+ assert.strictEqual($leavesDepartmentBody.find('td:contains(Richard)').length, 2, "it should have 2 leaves for Richard");
+ assert.strictEqual($leavesDepartmentBody.find('td:contains(Jesus)').length, 1, "it should have 1 leaves for Jesus");
+ assert.strictEqual($leavesDepartmentHeader.find('td:contains(R&D)').length, 1, "it should have R&D title");
+ form.destroy();
+ });
+ 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], ['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, ['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();
+ });
+ });
+});