diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/hr/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/hr/static')
42 files changed, 998 insertions, 0 deletions
diff --git a/addons/hr/static/description/icon.png b/addons/hr/static/description/icon.png Binary files differnew file mode 100644 index 00000000..845e53c8 --- /dev/null +++ b/addons/hr/static/description/icon.png 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 Binary files differnew file mode 100644 index 00000000..db35d761 --- /dev/null +++ b/addons/hr/static/img/employee-image.png diff --git a/addons/hr/static/img/employee_al-image.jpg b/addons/hr/static/img/employee_al-image.jpg Binary files differnew file mode 100644 index 00000000..abddef0e --- /dev/null +++ b/addons/hr/static/img/employee_al-image.jpg diff --git a/addons/hr/static/img/employee_chs-image.jpg b/addons/hr/static/img/employee_chs-image.jpg Binary files differnew file mode 100644 index 00000000..ea4a22e5 --- /dev/null +++ b/addons/hr/static/img/employee_chs-image.jpg diff --git a/addons/hr/static/img/employee_fme-image.jpg b/addons/hr/static/img/employee_fme-image.jpg Binary files differnew file mode 100644 index 00000000..89f03a97 --- /dev/null +++ b/addons/hr/static/img/employee_fme-image.jpg diff --git a/addons/hr/static/img/employee_fpi-image.jpg b/addons/hr/static/img/employee_fpi-image.jpg Binary files differnew file mode 100644 index 00000000..6dff1dea --- /dev/null +++ b/addons/hr/static/img/employee_fpi-image.jpg diff --git a/addons/hr/static/img/employee_han-image.jpg b/addons/hr/static/img/employee_han-image.jpg Binary files differnew file mode 100644 index 00000000..8968d3cf --- /dev/null +++ b/addons/hr/static/img/employee_han-image.jpg diff --git a/addons/hr/static/img/employee_hne-image.jpg b/addons/hr/static/img/employee_hne-image.jpg Binary files differnew file mode 100644 index 00000000..402ac414 --- /dev/null +++ b/addons/hr/static/img/employee_hne-image.jpg diff --git a/addons/hr/static/img/employee_jep-image.jpg b/addons/hr/static/img/employee_jep-image.jpg Binary files differnew file mode 100644 index 00000000..98fa7b11 --- /dev/null +++ b/addons/hr/static/img/employee_jep-image.jpg diff --git a/addons/hr/static/img/employee_jgo-image.jpg b/addons/hr/static/img/employee_jgo-image.jpg Binary files differnew file mode 100644 index 00000000..dfc34c2f --- /dev/null +++ b/addons/hr/static/img/employee_jgo-image.jpg diff --git a/addons/hr/static/img/employee_jod-image.jpg b/addons/hr/static/img/employee_jod-image.jpg Binary files differnew file mode 100644 index 00000000..3e89648c --- /dev/null +++ b/addons/hr/static/img/employee_jod-image.jpg diff --git a/addons/hr/static/img/employee_jog-image.jpg b/addons/hr/static/img/employee_jog-image.jpg Binary files differnew file mode 100644 index 00000000..0d810bd3 --- /dev/null +++ b/addons/hr/static/img/employee_jog-image.jpg diff --git a/addons/hr/static/img/employee_jth-image.jpg b/addons/hr/static/img/employee_jth-image.jpg Binary files differnew file mode 100644 index 00000000..d4ec655f --- /dev/null +++ b/addons/hr/static/img/employee_jth-image.jpg diff --git a/addons/hr/static/img/employee_jve-image.jpg b/addons/hr/static/img/employee_jve-image.jpg Binary files differnew file mode 100644 index 00000000..2818c5dd --- /dev/null +++ b/addons/hr/static/img/employee_jve-image.jpg diff --git a/addons/hr/static/img/employee_lur-image.jpg b/addons/hr/static/img/employee_lur-image.jpg Binary files differnew file mode 100644 index 00000000..0b322677 --- /dev/null +++ b/addons/hr/static/img/employee_lur-image.jpg diff --git a/addons/hr/static/img/employee_mit-image.jpg b/addons/hr/static/img/employee_mit-image.jpg Binary files differnew file mode 100644 index 00000000..b6dd0668 --- /dev/null +++ b/addons/hr/static/img/employee_mit-image.jpg diff --git a/addons/hr/static/img/employee_ngh-image.jpg b/addons/hr/static/img/employee_ngh-image.jpg Binary files differnew file mode 100644 index 00000000..fc9f3fa0 --- /dev/null +++ b/addons/hr/static/img/employee_ngh-image.jpg diff --git a/addons/hr/static/img/employee_niv-image.jpg b/addons/hr/static/img/employee_niv-image.jpg Binary files differnew file mode 100644 index 00000000..8d0815e0 --- /dev/null +++ b/addons/hr/static/img/employee_niv-image.jpg diff --git a/addons/hr/static/img/employee_qdp-image.png b/addons/hr/static/img/employee_qdp-image.png Binary files differnew file mode 100644 index 00000000..87caf314 --- /dev/null +++ b/addons/hr/static/img/employee_qdp-image.png diff --git a/addons/hr/static/img/employee_stw-image.jpg b/addons/hr/static/img/employee_stw-image.jpg Binary files differnew file mode 100644 index 00000000..02fa1cac --- /dev/null +++ b/addons/hr/static/img/employee_stw-image.jpg diff --git a/addons/hr/static/img/employee_vad-image.jpg b/addons/hr/static/img/employee_vad-image.jpg Binary files differnew file mode 100644 index 00000000..97466f84 --- /dev/null +++ b/addons/hr/static/img/employee_vad-image.jpg 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 Binary files differnew file mode 100644 index 00000000..87fdbc5a --- /dev/null +++ b/addons/hr/static/src/img/default_image.png 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 Binary files differnew file mode 100644 index 00000000..dd6ffd7d --- /dev/null +++ b/addons/hr/static/xls/hr_employee.xls |
