summaryrefslogtreecommitdiff
path: root/addons/hr/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/static
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/hr/static')
-rw-r--r--addons/hr/static/description/icon.pngbin0 -> 7646 bytes
-rw-r--r--addons/hr/static/description/icon.svg24
-rw-r--r--addons/hr/static/img/employee-image.pngbin0 -> 18128 bytes
-rw-r--r--addons/hr/static/img/employee_al-image.jpgbin0 -> 5728 bytes
-rw-r--r--addons/hr/static/img/employee_chs-image.jpgbin0 -> 5874 bytes
-rw-r--r--addons/hr/static/img/employee_fme-image.jpgbin0 -> 2843 bytes
-rw-r--r--addons/hr/static/img/employee_fpi-image.jpgbin0 -> 4035 bytes
-rw-r--r--addons/hr/static/img/employee_han-image.jpgbin0 -> 16867 bytes
-rw-r--r--addons/hr/static/img/employee_hne-image.jpgbin0 -> 6769 bytes
-rw-r--r--addons/hr/static/img/employee_jep-image.jpgbin0 -> 4492 bytes
-rw-r--r--addons/hr/static/img/employee_jgo-image.jpgbin0 -> 5024 bytes
-rw-r--r--addons/hr/static/img/employee_jod-image.jpgbin0 -> 3428 bytes
-rw-r--r--addons/hr/static/img/employee_jog-image.jpgbin0 -> 2967 bytes
-rw-r--r--addons/hr/static/img/employee_jth-image.jpgbin0 -> 3473 bytes
-rw-r--r--addons/hr/static/img/employee_jve-image.jpgbin0 -> 3805 bytes
-rw-r--r--addons/hr/static/img/employee_lur-image.jpgbin0 -> 4537 bytes
-rw-r--r--addons/hr/static/img/employee_mit-image.jpgbin0 -> 5270 bytes
-rw-r--r--addons/hr/static/img/employee_ngh-image.jpgbin0 -> 3733 bytes
-rw-r--r--addons/hr/static/img/employee_niv-image.jpgbin0 -> 4071 bytes
-rw-r--r--addons/hr/static/img/employee_qdp-image.pngbin0 -> 20366 bytes
-rw-r--r--addons/hr/static/img/employee_stw-image.jpgbin0 -> 4922 bytes
-rw-r--r--addons/hr/static/img/employee_vad-image.jpgbin0 -> 4859 bytes
-rw-r--r--addons/hr/static/src/bugfix/bugfix.js10
-rw-r--r--addons/hr/static/src/bugfix/bugfix.scss6
-rw-r--r--addons/hr/static/src/bugfix/bugfix.xml11
-rw-r--r--addons/hr/static/src/bugfix/bugfix_tests.js18
-rw-r--r--addons/hr/static/src/default_image.png0
-rw-r--r--addons/hr/static/src/img/default_image.pngbin0 -> 2723 bytes
-rw-r--r--addons/hr/static/src/js/chat.js74
-rw-r--r--addons/hr/static/src/js/language.js27
-rw-r--r--addons/hr/static/src/js/many2one_avatar_employee.js43
-rw-r--r--addons/hr/static/src/js/standalone_m2o_avatar_employee.js61
-rw-r--r--addons/hr/static/src/models/employee/employee.js204
-rw-r--r--addons/hr/static/src/models/messaging/messaging.js36
-rw-r--r--addons/hr/static/src/models/partner/partner.js63
-rw-r--r--addons/hr/static/src/models/user/user.js18
-rw-r--r--addons/hr/static/src/scss/hr.scss22
-rw-r--r--addons/hr/static/src/xml/hr_templates.xml8
-rw-r--r--addons/hr/static/tests/helpers/mock_models.js34
-rw-r--r--addons/hr/static/tests/many2one_avatar_employee_tests.js215
-rw-r--r--addons/hr/static/tests/standalone_m2o_avatar_employee_tests.js124
-rw-r--r--addons/hr/static/xls/hr_employee.xlsbin0 -> 37376 bytes
42 files changed, 998 insertions, 0 deletions
diff --git a/addons/hr/static/description/icon.png b/addons/hr/static/description/icon.png
new file mode 100644
index 00000000..845e53c8
--- /dev/null
+++ b/addons/hr/static/description/icon.png
Binary files differ
diff --git a/addons/hr/static/description/icon.svg b/addons/hr/static/description/icon.svg
new file mode 100644
index 00000000..753554b0
--- /dev/null
+++ b/addons/hr/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="#269396"/>
+ <stop offset="100%" stop-color="#218689"/>
+ </linearGradient>
+ <path id="icon-d" d="M35.5,22.75 C39.6529297,22.75 43.0195312,26.1166016 43.0195312,30.2695312 C43.0195312,34.4224609 39.6529297,37.7890625 35.5,37.7890625 C31.3470703,37.7890625 27.9804687,34.4224609 27.9804687,30.2695312 C27.9804687,26.1166016 31.3470703,22.75 35.5,22.75 Z M43.6256055,38.3165755 L40.7623112,37.6007161 C37.2411654,40.1333659 32.9730794,39.5681836 30.2377604,37.6007161 L27.3744661,38.3165755 C25.0790039,38.8904232 23.46875,40.9527799 23.46875,43.3188542 L23.46875,47.671875 C23.46875,49.0957161 24.6230339,50.25 26.046875,50.25 L44.953125,50.25 C46.3769661,50.25 47.53125,49.0957161 47.53125,47.671875 L47.53125,43.3188542 C47.53125,40.9527799 45.9209961,38.8904232 43.6256055,38.3165755 Z M50.3958333,39.6510417 C53.1644531,39.6510417 55.4088542,37.4066406 55.4088542,34.6380208 C55.4088542,31.869401 53.1644531,29.625 50.3958333,29.625 C47.6272135,29.625 45.3828125,31.869401 45.3828125,34.6380208 C45.3828125,37.4066406 47.6272135,39.6510417 50.3958333,39.6510417 Z M20.6041667,39.6510417 C23.3727865,39.6510417 25.6171875,37.4066406 25.6171875,34.6380208 C25.6171875,31.869401 23.3727865,29.625 20.6041667,29.625 C17.8355469,29.625 15.5911458,31.869401 15.5911458,34.6380208 C15.5911458,37.4066406 17.8355469,39.6510417 20.6041667,39.6510417 Z M22.3229167,47.671875 L22.3229167,43.3188542 C22.3229167,42.1335612 22.6518424,41.0125781 23.2326367,40.0533008 C21.0850586,41.1074674 18.6968555,40.6769206 17.0959831,39.5255013 L15.1870964,40.0027409 C13.6568359,40.3852344 12.5833333,41.7602344 12.5833333,43.3375456 L12.5833333,46.2395833 C12.5833333,47.1888346 13.352832,47.9583333 14.3020833,47.9583333 L22.3350195,47.9583333 C22.3273388,47.8630362 22.3233016,47.7674804 22.3229167,47.671875 Z M55.8129036,40.0026693 L53.9040169,39.5254297 C51.9041797,40.9638802 49.5434049,40.902793 47.7604883,40.0423437 C48.3454362,41.004056 48.6770833,42.1289779 48.6770833,43.3188542 L48.6770833,47.671875 C48.6770833,47.7683398 48.6722135,47.8636589 48.6649805,47.9583333 L56.6979167,47.9583333 C57.647168,47.9583333 58.4166667,47.1888346 58.4166667,46.2395833 L58.4166667,43.3375456 C58.4166667,41.7602344 57.3431641,40.3852344 55.8129036,40.0026693 Z"/>
+ <path id="icon-e" d="M35.5,20.75 C39.6529297,20.75 43.0195312,24.1166016 43.0195312,28.2695312 C43.0195312,32.4224609 39.6529297,35.7890625 35.5,35.7890625 C31.3470703,35.7890625 27.9804687,32.4224609 27.9804687,28.2695312 C27.9804687,24.1166016 31.3470703,20.75 35.5,20.75 Z M43.6256055,36.3165755 L40.7623112,35.6007161 C37.2411654,38.1333659 32.9730794,37.5681836 30.2377604,35.6007161 L27.3744661,36.3165755 C25.0790039,36.8904232 23.46875,38.9527799 23.46875,41.3188542 L23.46875,45.671875 C23.46875,47.0957161 24.6230339,48.25 26.046875,48.25 L44.953125,48.25 C46.3769661,48.25 47.53125,47.0957161 47.53125,45.671875 L47.53125,41.3188542 C47.53125,38.9527799 45.9209961,36.8904232 43.6256055,36.3165755 Z M50.3958333,37.6510417 C53.1644531,37.6510417 55.4088542,35.4066406 55.4088542,32.6380208 C55.4088542,29.869401 53.1644531,27.625 50.3958333,27.625 C47.6272135,27.625 45.3828125,29.869401 45.3828125,32.6380208 C45.3828125,35.4066406 47.6272135,37.6510417 50.3958333,37.6510417 Z M20.6041667,37.6510417 C23.3727865,37.6510417 25.6171875,35.4066406 25.6171875,32.6380208 C25.6171875,29.869401 23.3727865,27.625 20.6041667,27.625 C17.8355469,27.625 15.5911458,29.869401 15.5911458,32.6380208 C15.5911458,35.4066406 17.8355469,37.6510417 20.6041667,37.6510417 Z M22.3229167,45.671875 L22.3229167,41.3188542 C22.3229167,40.1335612 22.6518424,39.0125781 23.2326367,38.0533008 C21.0850586,39.1074674 18.6968555,38.6769206 17.0959831,37.5255013 L15.1870964,38.0027409 C13.6568359,38.3852344 12.5833333,39.7602344 12.5833333,41.3375456 L12.5833333,44.2395833 C12.5833333,45.1888346 13.352832,45.9583333 14.3020833,45.9583333 L22.3350195,45.9583333 C22.3273388,45.8630362 22.3233016,45.7674804 22.3229167,45.671875 Z M55.8129036,38.0026693 L53.9040169,37.5254297 C51.9041797,38.9638802 49.5434049,38.902793 47.7604883,38.0423437 C48.3454362,39.004056 48.6770833,40.1289779 48.6770833,41.3188542 L48.6770833,45.671875 C48.6770833,45.7683398 48.6722135,45.8636589 48.6649805,45.9583333 L56.6979167,45.9583333 C57.647168,45.9583333 58.4166667,45.1888346 58.4166667,44.2395833 L58.4166667,41.3375456 C58.4166667,39.7602344 57.3431641,38.3852344 55.8129036,38.0026693 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="M44,47 L4,47 C2,47 -7.10542736e-15,46.8509317 0,42.826087 L1.81527147e-16,22.6291049 L17.2090667,6.04664397 L19.583071,9.5209307 L30.2767729,0.11143939 L40.9146315,10.270152 L46.6446282,6.41116033 L55.3045682,10.7749724 L52.3812234,16.1277957 L58.2417324,21.9036543 L44,47 Z" opacity=".324" transform="translate(0 23)"/>
+ <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/static/img/employee-image.png b/addons/hr/static/img/employee-image.png
new file mode 100644
index 00000000..db35d761
--- /dev/null
+++ b/addons/hr/static/img/employee-image.png
Binary files differ
diff --git a/addons/hr/static/img/employee_al-image.jpg b/addons/hr/static/img/employee_al-image.jpg
new file mode 100644
index 00000000..abddef0e
--- /dev/null
+++ b/addons/hr/static/img/employee_al-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_chs-image.jpg b/addons/hr/static/img/employee_chs-image.jpg
new file mode 100644
index 00000000..ea4a22e5
--- /dev/null
+++ b/addons/hr/static/img/employee_chs-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_fme-image.jpg b/addons/hr/static/img/employee_fme-image.jpg
new file mode 100644
index 00000000..89f03a97
--- /dev/null
+++ b/addons/hr/static/img/employee_fme-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_fpi-image.jpg b/addons/hr/static/img/employee_fpi-image.jpg
new file mode 100644
index 00000000..6dff1dea
--- /dev/null
+++ b/addons/hr/static/img/employee_fpi-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_han-image.jpg b/addons/hr/static/img/employee_han-image.jpg
new file mode 100644
index 00000000..8968d3cf
--- /dev/null
+++ b/addons/hr/static/img/employee_han-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_hne-image.jpg b/addons/hr/static/img/employee_hne-image.jpg
new file mode 100644
index 00000000..402ac414
--- /dev/null
+++ b/addons/hr/static/img/employee_hne-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_jep-image.jpg b/addons/hr/static/img/employee_jep-image.jpg
new file mode 100644
index 00000000..98fa7b11
--- /dev/null
+++ b/addons/hr/static/img/employee_jep-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_jgo-image.jpg b/addons/hr/static/img/employee_jgo-image.jpg
new file mode 100644
index 00000000..dfc34c2f
--- /dev/null
+++ b/addons/hr/static/img/employee_jgo-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_jod-image.jpg b/addons/hr/static/img/employee_jod-image.jpg
new file mode 100644
index 00000000..3e89648c
--- /dev/null
+++ b/addons/hr/static/img/employee_jod-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_jog-image.jpg b/addons/hr/static/img/employee_jog-image.jpg
new file mode 100644
index 00000000..0d810bd3
--- /dev/null
+++ b/addons/hr/static/img/employee_jog-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_jth-image.jpg b/addons/hr/static/img/employee_jth-image.jpg
new file mode 100644
index 00000000..d4ec655f
--- /dev/null
+++ b/addons/hr/static/img/employee_jth-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_jve-image.jpg b/addons/hr/static/img/employee_jve-image.jpg
new file mode 100644
index 00000000..2818c5dd
--- /dev/null
+++ b/addons/hr/static/img/employee_jve-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_lur-image.jpg b/addons/hr/static/img/employee_lur-image.jpg
new file mode 100644
index 00000000..0b322677
--- /dev/null
+++ b/addons/hr/static/img/employee_lur-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_mit-image.jpg b/addons/hr/static/img/employee_mit-image.jpg
new file mode 100644
index 00000000..b6dd0668
--- /dev/null
+++ b/addons/hr/static/img/employee_mit-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_ngh-image.jpg b/addons/hr/static/img/employee_ngh-image.jpg
new file mode 100644
index 00000000..fc9f3fa0
--- /dev/null
+++ b/addons/hr/static/img/employee_ngh-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_niv-image.jpg b/addons/hr/static/img/employee_niv-image.jpg
new file mode 100644
index 00000000..8d0815e0
--- /dev/null
+++ b/addons/hr/static/img/employee_niv-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_qdp-image.png b/addons/hr/static/img/employee_qdp-image.png
new file mode 100644
index 00000000..87caf314
--- /dev/null
+++ b/addons/hr/static/img/employee_qdp-image.png
Binary files differ
diff --git a/addons/hr/static/img/employee_stw-image.jpg b/addons/hr/static/img/employee_stw-image.jpg
new file mode 100644
index 00000000..02fa1cac
--- /dev/null
+++ b/addons/hr/static/img/employee_stw-image.jpg
Binary files differ
diff --git a/addons/hr/static/img/employee_vad-image.jpg b/addons/hr/static/img/employee_vad-image.jpg
new file mode 100644
index 00000000..97466f84
--- /dev/null
+++ b/addons/hr/static/img/employee_vad-image.jpg
Binary files differ
diff --git a/addons/hr/static/src/bugfix/bugfix.js b/addons/hr/static/src/bugfix/bugfix.js
new file mode 100644
index 00000000..0351046f
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix.js
@@ -0,0 +1,10 @@
+/**
+ * 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/static/src/bugfix/bugfix.js', function (require) {
+'use strict';
+
+});
diff --git a/addons/hr/static/src/bugfix/bugfix.scss b/addons/hr/static/src/bugfix/bugfix.scss
new file mode 100644
index 00000000..c4272e52
--- /dev/null
+++ b/addons/hr/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/static/src/bugfix/bugfix.xml b/addons/hr/static/src/bugfix/bugfix.xml
new file mode 100644
index 00000000..c17906f7
--- /dev/null
+++ b/addons/hr/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/static/src/bugfix/bugfix_tests.js b/addons/hr/static/src/bugfix/bugfix_tests.js
new file mode 100644
index 00000000..8b23d372
--- /dev/null
+++ b/addons/hr/static/src/bugfix/bugfix_tests.js
@@ -0,0 +1,18 @@
+odoo.define('hr/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', {}, function () {
+QUnit.module('bugfix', {}, function () {
+QUnit.module('bugfix_tests.js', {
+
+});
+});
+});
+
+});
diff --git a/addons/hr/static/src/default_image.png b/addons/hr/static/src/default_image.png
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/addons/hr/static/src/default_image.png
diff --git a/addons/hr/static/src/img/default_image.png b/addons/hr/static/src/img/default_image.png
new file mode 100644
index 00000000..87fdbc5a
--- /dev/null
+++ b/addons/hr/static/src/img/default_image.png
Binary files differ
diff --git a/addons/hr/static/src/js/chat.js b/addons/hr/static/src/js/chat.js
new file mode 100644
index 00000000..9bae9d57
--- /dev/null
+++ b/addons/hr/static/src/js/chat.js
@@ -0,0 +1,74 @@
+odoo.define('hr.employee_chat', function (require) {
+'use strict';
+ var viewRegistry = require('web.view_registry');
+
+ var FormController = require('web.FormController');
+ var FormView = require('web.FormView');
+ var FormRenderer = require('web.FormRenderer');
+
+ var KanbanController = require('web.KanbanController');
+ var KanbanView = require('web.KanbanView');
+ var KanbanRenderer = require('web.KanbanRenderer');
+ var KanbanRecord = require('web.KanbanRecord');
+
+ const { Component } = owl;
+
+ // CHAT MIXIN
+ var ChatMixin = {
+ /**
+ * @override
+ */
+ _render: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ var $chat_button = self.$el.find('.o_employee_chat_btn');
+ $chat_button.off('click').on('click', self._onOpenChat.bind(self));
+ });
+ },
+
+ destroy: function () {
+ if (this.$el) {
+ this.$el.find('.o_employee_chat_btn').off('click');
+ }
+ return this._super();
+ },
+
+ _onOpenChat: function (ev) {
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ const env = Component.env;
+ env.messaging.openChat({ employeeId: this.state.data.id });
+ return true;
+ },
+ };
+
+ // USAGE OF CHAT MIXIN IN FORM VIEWS
+ var EmployeeFormRenderer = FormRenderer.extend(ChatMixin);
+
+ var EmployeeFormView = FormView.extend({
+ config: _.extend({}, FormView.prototype.config, {
+ Controller: FormController,
+ Renderer: EmployeeFormRenderer
+ }),
+ });
+
+ viewRegistry.add('hr_employee_form', EmployeeFormView);
+
+ // USAGE OF CHAT MIXIN IN KANBAN VIEWS
+ var EmployeeKanbanRecord = KanbanRecord.extend(ChatMixin);
+
+ var EmployeeKanbanRenderer = KanbanRenderer.extend({
+ config: Object.assign({}, KanbanRenderer.prototype.config, {
+ KanbanRecord: EmployeeKanbanRecord,
+ }),
+ });
+
+ var EmployeeKanbanView = KanbanView.extend({
+ config: _.extend({}, KanbanView.prototype.config, {
+ Controller: KanbanController,
+ Renderer: EmployeeKanbanRenderer
+ }),
+ });
+
+ viewRegistry.add('hr_employee_kanban', EmployeeKanbanView);
+});
diff --git a/addons/hr/static/src/js/language.js b/addons/hr/static/src/js/language.js
new file mode 100644
index 00000000..35140c7f
--- /dev/null
+++ b/addons/hr/static/src/js/language.js
@@ -0,0 +1,27 @@
+odoo.define('hr.employee_language', function (require) {
+'use strict';
+
+var FormController = require('web.FormController');
+var FormView = require('web.FormView');
+var viewRegistry = require('web.view_registry');
+
+var EmployeeFormController = FormController.extend({
+ saveRecord: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ if (arguments[0].indexOf('lang') >= 0) {
+ self.do_action('reload_context');
+ }
+ });
+ },
+});
+
+var EmployeeProfileFormView = FormView.extend({
+ config: _.extend({}, FormView.prototype.config, {
+ Controller: EmployeeFormController,
+ }),
+});
+
+viewRegistry.add('hr_employee_profile_form', EmployeeProfileFormView);
+return EmployeeProfileFormView;
+});
diff --git a/addons/hr/static/src/js/many2one_avatar_employee.js b/addons/hr/static/src/js/many2one_avatar_employee.js
new file mode 100644
index 00000000..0d5a245a
--- /dev/null
+++ b/addons/hr/static/src/js/many2one_avatar_employee.js
@@ -0,0 +1,43 @@
+odoo.define('hr.Many2OneAvatarEmployee', function (require) {
+ "use strict";
+
+ // This module defines a variant of the Many2OneAvatarUser field widget,
+ // to support many2one fields pointing to 'hr.employee'. It also defines the
+ // kanban version of this widget.
+ //
+ // Usage:
+ // <field name="employee_id" widget="many2one_avatar_employee"/>
+
+ const fieldRegistry = require('web.field_registry');
+ const { Many2OneAvatarUser, KanbanMany2OneAvatarUser } = require('mail.Many2OneAvatarUser');
+
+ const { Component } = owl;
+
+ const Many2OneAvatarEmployeeMixin = {
+ supportedModels: ['hr.employee', 'hr.employee.public'],
+
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ async _onAvatarClicked(ev) {
+ ev.stopPropagation(); // in list view, prevent from opening the record
+ const env = Component.env;
+ await env.messaging.openChat({ employeeId: this.value.res_id });
+ }
+ };
+
+ const Many2OneAvatarEmployee = Many2OneAvatarUser.extend(Many2OneAvatarEmployeeMixin);
+ const KanbanMany2OneAvatarEmployee = KanbanMany2OneAvatarUser.extend(Many2OneAvatarEmployeeMixin);
+
+ fieldRegistry.add('many2one_avatar_employee', Many2OneAvatarEmployee);
+ fieldRegistry.add('kanban.many2one_avatar_employee', KanbanMany2OneAvatarEmployee);
+
+ return {
+ Many2OneAvatarEmployee,
+ KanbanMany2OneAvatarEmployee,
+ };
+});
diff --git a/addons/hr/static/src/js/standalone_m2o_avatar_employee.js b/addons/hr/static/src/js/standalone_m2o_avatar_employee.js
new file mode 100644
index 00000000..64da43ec
--- /dev/null
+++ b/addons/hr/static/src/js/standalone_m2o_avatar_employee.js
@@ -0,0 +1,61 @@
+odoo.define('hr.StandaloneM2OAvatarEmployee', function (require) {
+ 'use strict';
+
+ const StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
+ const Widget = require('web.Widget');
+
+ const { Many2OneAvatarEmployee } = require('hr.Many2OneAvatarEmployee');
+
+ const StandaloneM2OAvatarEmployee = Widget.extend(StandaloneFieldManagerMixin, {
+ className: 'o_standalone_avatar_employee',
+
+ /**
+ * @override
+ */
+ init(parent, value) {
+ this._super(...arguments);
+ StandaloneFieldManagerMixin.init.call(this);
+ this.value = value;
+ },
+ /**
+ * @override
+ */
+ willStart() {
+ return Promise.all([this._super(...arguments), this._makeAvatarWidget()]);
+ },
+ /**
+ * @override
+ */
+ start() {
+ this.avatarWidget.$el.appendTo(this.$el);
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Create a record, and initialize and start the avatar widget.
+ *
+ * @private
+ * @returns {Promise}
+ */
+ async _makeAvatarWidget() {
+ const modelName = 'hr.employee';
+ const fieldName = 'employee_id';
+ const recordId = await this.model.makeRecord(modelName, [{
+ name: fieldName,
+ relation: modelName,
+ type: 'many2one',
+ value: this.value,
+ }]);
+ const state = this.model.get(recordId);
+ this.avatarWidget = new Many2OneAvatarEmployee(this, fieldName, state);
+ this._registerWidget(recordId, fieldName, this.avatarWidget);
+ return this.avatarWidget.appendTo(document.createDocumentFragment());
+ },
+ });
+
+ return StandaloneM2OAvatarEmployee;
+});
diff --git a/addons/hr/static/src/models/employee/employee.js b/addons/hr/static/src/models/employee/employee.js
new file mode 100644
index 00000000..6d3c12cb
--- /dev/null
+++ b/addons/hr/static/src/models/employee/employee.js
@@ -0,0 +1,204 @@
+odoo.define('hr/static/src/models/employee/employee.js', function (require) {
+'use strict';
+
+const { registerNewModel } = require('mail/static/src/model/model_core.js');
+const { attr, one2one } = require('mail/static/src/model/model_field.js');
+
+function factory(dependencies) {
+
+ class Employee extends dependencies['mail.model'] {
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @static
+ * @param {Object} data
+ * @returns {Object}
+ */
+ static convertData(data) {
+ const data2 = {};
+ if ('id' in data) {
+ data2.id = data.id;
+ }
+ if ('user_id' in data) {
+ data2.hasCheckedUser = true;
+ if (!data.user_id) {
+ data2.user = [['unlink']];
+ } else {
+ const partnerNameGet = data['user_partner_id'];
+ const partnerData = {
+ display_name: partnerNameGet[1],
+ id: partnerNameGet[0],
+ };
+ const userNameGet = data['user_id'];
+ const userData = {
+ id: userNameGet[0],
+ partner: [['insert', partnerData]],
+ display_name: userNameGet[1],
+ };
+ data2.user = [['insert', userData]];
+ }
+ }
+ return data2;
+ }
+
+ /**
+ * Performs the `read` RPC on the `hr.employee.public`.
+ *
+ * @static
+ * @param {Object} param0
+ * @param {Object} param0.context
+ * @param {string[]} param0.fields
+ * @param {integer[]} param0.ids
+ */
+ static async performRpcRead({ context, fields, ids }) {
+ const employeesData = await this.env.services.rpc({
+ model: 'hr.employee.public',
+ method: 'read',
+ args: [ids],
+ kwargs: {
+ context,
+ fields,
+ },
+ });
+ this.env.models['hr.employee'].insert(employeesData.map(employeeData =>
+ this.env.models['hr.employee'].convertData(employeeData)
+ ));
+ }
+
+ /**
+ * Performs the `search_read` RPC on `hr.employee.public`.
+ *
+ * @static
+ * @param {Object} param0
+ * @param {Object} param0.context
+ * @param {Array[]} param0.domain
+ * @param {string[]} param0.fields
+ */
+ static async performRpcSearchRead({ context, domain, fields }) {
+ const employeesData = await this.env.services.rpc({
+ model: 'hr.employee.public',
+ method: 'search_read',
+ kwargs: {
+ context,
+ domain,
+ fields,
+ },
+ });
+ this.env.models['hr.employee'].insert(employeesData.map(employeeData =>
+ this.env.models['hr.employee'].convertData(employeeData)
+ ));
+ }
+
+ /**
+ * Checks whether this employee has a related user and partner and links
+ * them if applicable.
+ */
+ async checkIsUser() {
+ return this.env.models['hr.employee'].performRpcRead({
+ ids: [this.id],
+ fields: ['user_id', 'user_partner_id'],
+ context: { active_test: false },
+ });
+ }
+
+ /**
+ * Gets the chat between the user of this employee and the current user.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @returns {mail.thread|undefined}
+ */
+ async getChat() {
+ if (!this.user && !this.hasCheckedUser) {
+ await this.async(() => this.checkIsUser());
+ }
+ // prevent chatting with non-users
+ if (!this.user) {
+ this.env.services['notification'].notify({
+ message: this.env._t("You can only chat with employees that have a dedicated user."),
+ type: 'info',
+ });
+ return;
+ }
+ return this.user.getChat();
+ }
+
+ /**
+ * Opens a chat between the user of this employee and the current user
+ * and returns it.
+ *
+ * If a chat is not appropriate, a notification is displayed instead.
+ *
+ * @param {Object} [options] forwarded to @see `mail.thread:open()`
+ * @returns {mail.thread|undefined}
+ */
+ async openChat(options) {
+ const chat = await this.async(() => this.getChat());
+ if (!chat) {
+ return;
+ }
+ await this.async(() => chat.open(options));
+ return chat;
+ }
+
+ /**
+ * Opens the most appropriate view that is a profile for this employee.
+ */
+ async openProfile() {
+ return this.env.messaging.openDocument({
+ id: this.id,
+ model: 'hr.employee.public',
+ });
+ }
+
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ static _createRecordLocalId(data) {
+ return `${this.modelName}_${data.id}`;
+ }
+
+ }
+
+ Employee.fields = {
+ /**
+ * Whether an attempt was already made to fetch the user corresponding
+ * to this employee. This prevents doing the same RPC multiple times.
+ */
+ hasCheckedUser: attr({
+ default: false,
+ }),
+ /**
+ * Unique identifier for this employee.
+ */
+ id: attr(),
+ /**
+ * Partner related to this employee.
+ */
+ partner: one2one('mail.partner', {
+ inverse: 'employee',
+ related: 'user.partner',
+ }),
+ /**
+ * User related to this employee.
+ */
+ user: one2one('mail.user', {
+ inverse: 'employee',
+ }),
+ };
+
+ Employee.modelName = 'hr.employee';
+
+ return Employee;
+}
+
+registerNewModel('hr.employee', factory);
+
+});
diff --git a/addons/hr/static/src/models/messaging/messaging.js b/addons/hr/static/src/models/messaging/messaging.js
new file mode 100644
index 00000000..435761a9
--- /dev/null
+++ b/addons/hr/static/src/models/messaging/messaging.js
@@ -0,0 +1,36 @@
+odoo.define('hr/static/src/models/messaging/messaging.js', function (require) {
+'use strict';
+
+const {
+ registerInstancePatchModel,
+} = require('mail/static/src/model/model_core.js');
+
+registerInstancePatchModel('mail.messaging', 'hr/static/src/models/messaging/messaging.js', {
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @param {integer} [param0.employeeId]
+ */
+ async getChat({ employeeId }) {
+ if (employeeId) {
+ const employee = this.env.models['hr.employee'].insert({ id: employeeId });
+ return employee.getChat();
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ async openProfile({ id, model }) {
+ if (model === 'hr.employee' || model === 'hr.employee.public') {
+ const employee = this.env.models['hr.employee'].insert({ id });
+ return employee.openProfile();
+ }
+ return this._super(...arguments);
+ },
+});
+
+});
diff --git a/addons/hr/static/src/models/partner/partner.js b/addons/hr/static/src/models/partner/partner.js
new file mode 100644
index 00000000..cbbcb987
--- /dev/null
+++ b/addons/hr/static/src/models/partner/partner.js
@@ -0,0 +1,63 @@
+odoo.define('hr/static/src/models/partner/partner.js', function (require) {
+'use strict';
+
+const {
+ registerInstancePatchModel,
+ registerFieldPatchModel,
+} = require('mail/static/src/model/model_core.js');
+const { attr, one2one } = require('mail/static/src/model/model_field.js');
+
+registerInstancePatchModel('mail.partner', 'hr/static/src/models/partner/partner.js', {
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Checks whether this partner has a related employee and links them if
+ * applicable.
+ */
+ async checkIsEmployee() {
+ await this.async(() => this.env.models['hr.employee'].performRpcSearchRead({
+ context: { active_test: false },
+ domain: [['user_partner_id', '=', this.id]],
+ fields: ['user_id', 'user_partner_id'],
+ }));
+ this.update({ hasCheckedEmployee: true });
+ },
+ /**
+ * When a partner is an employee, its employee profile contains more useful
+ * information to know who he is than its partner profile.
+ *
+ * @override
+ */
+ async openProfile() {
+ // limitation of patch, `this._super` becomes unavailable after `await`
+ const _super = this._super.bind(this, ...arguments);
+ if (!this.employee && !this.hasCheckedEmployee) {
+ await this.async(() => this.checkIsEmployee());
+ }
+ if (this.employee) {
+ return this.employee.openProfile();
+ }
+ return _super();
+ },
+});
+
+registerFieldPatchModel('mail.partner', 'hr/static/src/models/partner/partner.js', {
+ /**
+ * Employee related to this partner. It is computed through
+ * the inverse relation and should be considered read-only.
+ */
+ employee: one2one('hr.employee', {
+ inverse: 'partner',
+ }),
+ /**
+ * Whether an attempt was already made to fetch the employee corresponding
+ * to this partner. This prevents doing the same RPC multiple times.
+ */
+ hasCheckedEmployee: attr({
+ default: false,
+ }),
+});
+
+});
diff --git a/addons/hr/static/src/models/user/user.js b/addons/hr/static/src/models/user/user.js
new file mode 100644
index 00000000..a0482d50
--- /dev/null
+++ b/addons/hr/static/src/models/user/user.js
@@ -0,0 +1,18 @@
+odoo.define('hr/static/src/models/user/user.js', function (require) {
+'use strict';
+
+const {
+ registerFieldPatchModel,
+} = require('mail/static/src/model/model_core.js');
+const { one2one } = require('mail/static/src/model/model_field.js');
+
+registerFieldPatchModel('mail.user', 'hr/static/src/models/user/user.js', {
+ /**
+ * Employee related to this user.
+ */
+ employee: one2one('hr.employee', {
+ inverse: 'user',
+ }),
+});
+
+});
diff --git a/addons/hr/static/src/scss/hr.scss b/addons/hr/static/src/scss/hr.scss
new file mode 100644
index 00000000..af5f76ee
--- /dev/null
+++ b/addons/hr/static/src/scss/hr.scss
@@ -0,0 +1,22 @@
+.o_web_client .o_hr_employee_kanban {
+
+ .o_follow_btn.o_following {
+ .o_unfollow {
+ display: none;
+ }
+
+ &:hover {
+ .o_following {
+ display: none;
+ }
+ .o_unfollow {
+ display: inline;
+ }
+ }
+ }
+
+ .o_employee_summary_icons > span {
+ white-space: nowrap;
+ }
+
+}
diff --git a/addons/hr/static/src/xml/hr_templates.xml b/addons/hr/static/src/xml/hr_templates.xml
new file mode 100644
index 00000000..c805e592
--- /dev/null
+++ b/addons/hr/static/src/xml/hr_templates.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<template xml:space="preserve">
+ <t t-extend="UserMenu.Actions">
+ <t t-jquery="a[data-menu='settings']" t-operation='inner'>
+ My Profile
+ </t>
+ </t>
+</template>
diff --git a/addons/hr/static/tests/helpers/mock_models.js b/addons/hr/static/tests/helpers/mock_models.js
new file mode 100644
index 00000000..d4ae7c86
--- /dev/null
+++ b/addons/hr/static/tests/helpers/mock_models.js
@@ -0,0 +1,34 @@
+odoo.define('hr/static/tests/helpers/mock_models.js', function (require) {
+'use strict';
+
+const MockModels = require('mail/static/tests/helpers/mock_models.js');
+
+MockModels.patch('hr/static/tests/helpers/mock_models.js', T =>
+ class extends T {
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ static generateData() {
+ const data = super.generateData(...arguments);
+ Object.assign(data, {
+ 'hr.employee.public': {
+ fields: {
+ display_name: { string: "Name", type: "char" },
+ user_id: { string: "User", type: "many2one", relation: 'res.users' },
+ user_partner_id: { string: "Partner", type: "many2one", relation: 'res.partner' },
+ },
+ records: [],
+ },
+ });
+ return data;
+ }
+
+ }
+);
+
+});
diff --git a/addons/hr/static/tests/many2one_avatar_employee_tests.js b/addons/hr/static/tests/many2one_avatar_employee_tests.js
new file mode 100644
index 00000000..c1eef47b
--- /dev/null
+++ b/addons/hr/static/tests/many2one_avatar_employee_tests.js
@@ -0,0 +1,215 @@
+odoo.define('hr.Many2OneAvatarEmployeeTests', function (require) {
+"use strict";
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+const FormView = require('web.FormView');
+const KanbanView = require('web.KanbanView');
+const ListView = require('web.ListView');
+const { Many2OneAvatarEmployee } = require('hr.Many2OneAvatarEmployee');
+const { dom, mock } = require('web.test_utils');
+
+QUnit.module('hr', {}, function () {
+ QUnit.module('Many2OneAvatarEmployee', {
+ beforeEach() {
+ beforeEach(this);
+
+ // reset the cache before each test
+ Many2OneAvatarEmployee.prototype.partnerIds = {};
+
+ Object.assign(this.data, {
+ 'foo': {
+ fields: {
+ employee_id: { string: "Employee", type: 'many2one', relation: 'hr.employee.public' },
+ },
+ records: [
+ { id: 1, employee_id: 11 },
+ { id: 2, employee_id: 7 },
+ { id: 3, employee_id: 11 },
+ { id: 4, employee_id: 23 },
+ ],
+ },
+ });
+ this.data['hr.employee.public'].records.push(
+ { id: 11, name: "Mario", user_id: 11, user_partner_id: 11 },
+ { id: 7, name: "Luigi", user_id: 12, user_partner_id: 12 },
+ { id: 23, name: "Yoshi", user_id: 13, user_partner_id: 13 }
+ );
+ this.data['res.users'].records.push(
+ { id: 11, partner_id: 11 },
+ { id: 12, partner_id: 12 },
+ { id: 13, partner_id: 13 }
+ );
+ this.data['res.partner'].records.push(
+ { id: 11, display_name: "Mario" },
+ { id: 12, display_name: "Luigi" },
+ { id: 13, display_name: "Yoshi" }
+ );
+ },
+ afterEach() {
+ afterEach(this);
+ },
+ });
+
+ QUnit.test('many2one_avatar_employee widget in list view', async function (assert) {
+ assert.expect(11);
+
+ const { widget: list } = await start({
+ hasChatWindow: true,
+ hasView: true,
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="employee_id" widget="many2one_avatar_employee"/></tree>',
+ mockRPC(route, args) {
+ if (args.method === 'read') {
+ assert.step(`read ${args.model} ${args.args[0]}`);
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ assert.strictEqual(list.$('.o_data_cell span').text(), 'MarioLuigiMarioYoshi');
+
+ // click on first employee
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(0) .o_m2o_avatar'))
+ );
+ assert.verifySteps(
+ ['read hr.employee.public 11'],
+ "first employee should have been read to find its partner"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 'should have opened chat window'
+ );
+ assert.strictEqual(
+ document.querySelector('.o_ChatWindowHeader_name').textContent,
+ "Mario",
+ 'chat window should be with clicked employee'
+ );
+
+ // click on second employee
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(1) .o_m2o_avatar')
+ ));
+ assert.verifySteps(
+ ['read hr.employee.public 7'],
+ "second employee should have been read to find its partner"
+ );
+ assert.containsN(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 2,
+ 'should have opened second chat window'
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_ChatWindowHeader_name')[1].textContent,
+ "Luigi",
+ 'chat window should be with clicked employee'
+ );
+
+ // click on third employee (same as first)
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(2) .o_m2o_avatar'))
+ );
+ assert.verifySteps(
+ [],
+ "employee should not have been read again because we already know its partner"
+ );
+ assert.containsN(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 2,
+ "should still have only 2 chat windows because third is the same partner as first"
+ );
+
+ list.destroy();
+ });
+
+ QUnit.test('many2one_avatar_employee widget in kanban view', async function (assert) {
+ assert.expect(6);
+
+ const { widget: kanban } = await start({
+ hasView: true,
+ View: KanbanView,
+ model: 'foo',
+ data: this.data,
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div>
+ <field name="employee_id" widget="many2one_avatar_employee"/>
+ </div>
+ </t>
+ </templates>
+ </kanban>`,
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_record').text().trim(), '');
+ assert.containsN(kanban, '.o_m2o_avatar', 4);
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(0)').data('src'), '/web/image/hr.employee.public/11/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(1)').data('src'), '/web/image/hr.employee.public/7/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(2)').data('src'), '/web/image/hr.employee.public/11/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(3)').data('src'), '/web/image/hr.employee.public/23/image_128');
+
+ kanban.destroy();
+ });
+
+ QUnit.test('many2one_avatar_employee: click on an employee not associated with a user', async function (assert) {
+ assert.expect(6);
+
+ this.data['hr.employee.public'].records[0].user_id = false;
+ this.data['hr.employee.public'].records[0].user_partner_id = false;
+ const { widget: form } = await start({
+ hasView: true,
+ View: FormView,
+ model: 'foo',
+ data: this.data,
+ arch: '<form><field name="employee_id" widget="many2one_avatar_employee"/></form>',
+ mockRPC(route, args) {
+ if (args.method === 'read') {
+ assert.step(`read ${args.model} ${args.args[0]}`);
+ }
+ return this._super(...arguments);
+ },
+ res_id: 1,
+ });
+
+ mock.intercept(form, 'call_service', (ev) => {
+ if (ev.data.service === 'notification') {
+ assert.step(`display notification "${ev.data.args[0].message}"`);
+ }
+ }, true);
+
+ assert.strictEqual(form.$('.o_field_widget[name=employee_id]').text().trim(), 'Mario');
+
+ await dom.click(form.$('.o_m2o_avatar'));
+
+ assert.verifySteps([
+ 'read foo 1',
+ 'read hr.employee.public 11',
+ ]);
+
+ assert.containsOnce(
+ document.body,
+ '.toast .o_notification_content',
+ "should display a toast notification after failing to open chat"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_notification_content').textContent,
+ "You can only chat with employees that have a dedicated user.",
+ "should display the correct information in the notification"
+ );
+
+ form.destroy();
+ });
+});
+});
diff --git a/addons/hr/static/tests/standalone_m2o_avatar_employee_tests.js b/addons/hr/static/tests/standalone_m2o_avatar_employee_tests.js
new file mode 100644
index 00000000..976cd34d
--- /dev/null
+++ b/addons/hr/static/tests/standalone_m2o_avatar_employee_tests.js
@@ -0,0 +1,124 @@
+odoo.define('hr.StandaloneM2OEmployeeTests', function (require) {
+ "use strict";
+
+ const { xml } = owl.tags;
+
+ const AbstractRendererOwl = require('web.AbstractRendererOwl');
+ const BasicView = require("web.BasicView");
+ const BasicRenderer = require("web.BasicRenderer");
+ const RendererWrapper = require('web.RendererWrapper');
+ const { createView } = require('web.test_utils');
+
+ const StandaloneM2OAvatarEmployee = require('hr.StandaloneM2OAvatarEmployee');
+
+ function getHtmlRenderer(html) {
+ return BasicRenderer.extend({
+ start: function () {
+ this.$el.html(html);
+ return this._super.apply(this, arguments);
+ }
+ });
+ }
+
+ function getOwlView(owlRenderer, viewType) {
+ viewType = viewType || "test";
+ return BasicView.extend({
+ viewType: viewType,
+ config: Object.assign({}, BasicView.prototype.config, {
+ Renderer: owlRenderer,
+ }),
+ getRenderer() {
+ return new RendererWrapper(null, this.config.Renderer, {});
+ }
+ });
+ }
+
+ function getHtmlView(html, viewType) {
+ viewType = viewType || "test";
+ return BasicView.extend({
+ viewType: viewType,
+ config: Object.assign({}, BasicView.prototype.config, {
+ Renderer: getHtmlRenderer(html)
+ })
+ });
+ }
+
+ QUnit.module('hr', {}, function () {
+ QUnit.module('StandaloneM2OEmployeeTests', {
+ beforeEach: function () {
+ this.data = {
+ 'foo': {
+ fields: {
+ employee_id: {string: "Employee", type: 'many2one', relation: 'hr.employee'},
+ },
+ records: [],
+ },
+ 'hr.employee': {
+ fields: {},
+ records: [
+ {id: 10, name: "Mario"},
+ {id: 20, name: "Luigi"},
+ {id: 30, name: "Yoshi"}
+ ],
+ },
+ };
+ },
+ });
+
+ QUnit.test('standalone_m2o_avatar_employee: legacy view', async function (assert) {
+ assert.expect(1);
+
+ const html = "<div class='coucou_test'></div>";
+ const view = await createView({
+ View: getHtmlView(html, "test"),
+ data: this.data,
+ model: "foo",
+ arch: "<test/>"
+ });
+
+ const avatar10 = new StandaloneM2OAvatarEmployee(view, 10);
+ const avatar20 = new StandaloneM2OAvatarEmployee(view, 20);
+ const avatar30 = new StandaloneM2OAvatarEmployee(view, [30, 'Bowser']);
+
+ await avatar10.appendTo(view.el.querySelector('.coucou_test'));
+ await avatar20.appendTo(view.el.querySelector('.coucou_test'));
+ await avatar30.appendTo(view.el.querySelector('.coucou_test'));
+
+ assert.deepEqual(
+ [...view.el.querySelectorAll('.o_field_many2one_avatar span')].map(el => el.innerText),
+ ["Mario", "Luigi", "Bowser"]
+ );
+
+ view.destroy();
+ });
+
+ QUnit.test('standalone_m2o_avatar_employee: Owl view', async function (assert) {
+ assert.expect(1);
+
+ class Renderer extends AbstractRendererOwl { }
+ Renderer.template = xml`<div class='coucou_test'></div>`;
+
+ const view = await createView({
+ View: getOwlView(Renderer, "test"),
+ data: this.data,
+ model: "foo",
+ arch: "<test/>"
+ });
+
+ const avatar10 = new StandaloneM2OAvatarEmployee(view, 10);
+ const avatar20 = new StandaloneM2OAvatarEmployee(view, 20);
+ const avatar30 = new StandaloneM2OAvatarEmployee(view, [30, 'Bowser']);
+
+ await avatar10.appendTo(view.el.querySelector('.coucou_test'));
+ await avatar20.appendTo(view.el.querySelector('.coucou_test'));
+ await avatar30.appendTo(view.el.querySelector('.coucou_test'));
+
+ assert.deepEqual(
+ [...view.el.querySelectorAll('.o_field_many2one_avatar span')].map(el => el.innerText),
+ ["Mario", "Luigi", "Bowser"]
+ );
+
+ view.destroy();
+ });
+ });
+});
diff --git a/addons/hr/static/xls/hr_employee.xls b/addons/hr/static/xls/hr_employee.xls
new file mode 100644
index 00000000..dd6ffd7d
--- /dev/null
+++ b/addons/hr/static/xls/hr_employee.xls
Binary files differ