From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/hr/static/description/icon.png | Bin 0 -> 7646 bytes addons/hr/static/description/icon.svg | 24 +++ addons/hr/static/img/employee-image.png | Bin 0 -> 18128 bytes addons/hr/static/img/employee_al-image.jpg | Bin 0 -> 5728 bytes addons/hr/static/img/employee_chs-image.jpg | Bin 0 -> 5874 bytes addons/hr/static/img/employee_fme-image.jpg | Bin 0 -> 2843 bytes addons/hr/static/img/employee_fpi-image.jpg | Bin 0 -> 4035 bytes addons/hr/static/img/employee_han-image.jpg | Bin 0 -> 16867 bytes addons/hr/static/img/employee_hne-image.jpg | Bin 0 -> 6769 bytes addons/hr/static/img/employee_jep-image.jpg | Bin 0 -> 4492 bytes addons/hr/static/img/employee_jgo-image.jpg | Bin 0 -> 5024 bytes addons/hr/static/img/employee_jod-image.jpg | Bin 0 -> 3428 bytes addons/hr/static/img/employee_jog-image.jpg | Bin 0 -> 2967 bytes addons/hr/static/img/employee_jth-image.jpg | Bin 0 -> 3473 bytes addons/hr/static/img/employee_jve-image.jpg | Bin 0 -> 3805 bytes addons/hr/static/img/employee_lur-image.jpg | Bin 0 -> 4537 bytes addons/hr/static/img/employee_mit-image.jpg | Bin 0 -> 5270 bytes addons/hr/static/img/employee_ngh-image.jpg | Bin 0 -> 3733 bytes addons/hr/static/img/employee_niv-image.jpg | Bin 0 -> 4071 bytes addons/hr/static/img/employee_qdp-image.png | Bin 0 -> 20366 bytes addons/hr/static/img/employee_stw-image.jpg | Bin 0 -> 4922 bytes addons/hr/static/img/employee_vad-image.jpg | Bin 0 -> 4859 bytes addons/hr/static/src/bugfix/bugfix.js | 10 + addons/hr/static/src/bugfix/bugfix.scss | 6 + addons/hr/static/src/bugfix/bugfix.xml | 11 ++ addons/hr/static/src/bugfix/bugfix_tests.js | 18 ++ addons/hr/static/src/default_image.png | 0 addons/hr/static/src/img/default_image.png | Bin 0 -> 2723 bytes addons/hr/static/src/js/chat.js | 74 +++++++ addons/hr/static/src/js/language.js | 27 +++ .../hr/static/src/js/many2one_avatar_employee.js | 43 +++++ .../src/js/standalone_m2o_avatar_employee.js | 61 ++++++ addons/hr/static/src/models/employee/employee.js | 204 +++++++++++++++++++ addons/hr/static/src/models/messaging/messaging.js | 36 ++++ addons/hr/static/src/models/partner/partner.js | 63 ++++++ addons/hr/static/src/models/user/user.js | 18 ++ addons/hr/static/src/scss/hr.scss | 22 +++ addons/hr/static/src/xml/hr_templates.xml | 8 + addons/hr/static/tests/helpers/mock_models.js | 34 ++++ .../static/tests/many2one_avatar_employee_tests.js | 215 +++++++++++++++++++++ .../tests/standalone_m2o_avatar_employee_tests.js | 124 ++++++++++++ addons/hr/static/xls/hr_employee.xls | Bin 0 -> 37376 bytes 42 files changed, 998 insertions(+) create mode 100644 addons/hr/static/description/icon.png create mode 100644 addons/hr/static/description/icon.svg create mode 100644 addons/hr/static/img/employee-image.png create mode 100644 addons/hr/static/img/employee_al-image.jpg create mode 100644 addons/hr/static/img/employee_chs-image.jpg create mode 100644 addons/hr/static/img/employee_fme-image.jpg create mode 100644 addons/hr/static/img/employee_fpi-image.jpg create mode 100644 addons/hr/static/img/employee_han-image.jpg create mode 100644 addons/hr/static/img/employee_hne-image.jpg create mode 100644 addons/hr/static/img/employee_jep-image.jpg create mode 100644 addons/hr/static/img/employee_jgo-image.jpg create mode 100644 addons/hr/static/img/employee_jod-image.jpg create mode 100644 addons/hr/static/img/employee_jog-image.jpg create mode 100644 addons/hr/static/img/employee_jth-image.jpg create mode 100644 addons/hr/static/img/employee_jve-image.jpg create mode 100644 addons/hr/static/img/employee_lur-image.jpg create mode 100644 addons/hr/static/img/employee_mit-image.jpg create mode 100644 addons/hr/static/img/employee_ngh-image.jpg create mode 100644 addons/hr/static/img/employee_niv-image.jpg create mode 100644 addons/hr/static/img/employee_qdp-image.png create mode 100644 addons/hr/static/img/employee_stw-image.jpg create mode 100644 addons/hr/static/img/employee_vad-image.jpg create mode 100644 addons/hr/static/src/bugfix/bugfix.js create mode 100644 addons/hr/static/src/bugfix/bugfix.scss create mode 100644 addons/hr/static/src/bugfix/bugfix.xml create mode 100644 addons/hr/static/src/bugfix/bugfix_tests.js create mode 100644 addons/hr/static/src/default_image.png create mode 100644 addons/hr/static/src/img/default_image.png create mode 100644 addons/hr/static/src/js/chat.js create mode 100644 addons/hr/static/src/js/language.js create mode 100644 addons/hr/static/src/js/many2one_avatar_employee.js create mode 100644 addons/hr/static/src/js/standalone_m2o_avatar_employee.js create mode 100644 addons/hr/static/src/models/employee/employee.js create mode 100644 addons/hr/static/src/models/messaging/messaging.js create mode 100644 addons/hr/static/src/models/partner/partner.js create mode 100644 addons/hr/static/src/models/user/user.js create mode 100644 addons/hr/static/src/scss/hr.scss create mode 100644 addons/hr/static/src/xml/hr_templates.xml create mode 100644 addons/hr/static/tests/helpers/mock_models.js create mode 100644 addons/hr/static/tests/many2one_avatar_employee_tests.js create mode 100644 addons/hr/static/tests/standalone_m2o_avatar_employee_tests.js create mode 100644 addons/hr/static/xls/hr_employee.xls (limited to 'addons/hr/static') diff --git a/addons/hr/static/description/icon.png b/addons/hr/static/description/icon.png new file mode 100644 index 00000000..845e53c8 Binary files /dev/null and b/addons/hr/static/description/icon.png 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + 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 Binary files /dev/null and b/addons/hr/static/img/employee-image.png 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 Binary files /dev/null and b/addons/hr/static/img/employee_al-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_chs-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_fme-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_fpi-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_han-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_hne-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_jep-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_jgo-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_jod-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_jog-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_jth-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_jve-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_lur-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_mit-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_ngh-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_niv-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_qdp-image.png 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 Binary files /dev/null and b/addons/hr/static/img/employee_stw-image.jpg 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 Binary files /dev/null and b/addons/hr/static/img/employee_vad-image.jpg 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 @@ + + + + + + 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 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 Binary files /dev/null and b/addons/hr/static/src/img/default_image.png 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: + // + + 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 @@ + + 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: '', + 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: ` + + + +
+ +
+
+
+
`, + }); + + 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: '
', + 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 = "
"; + const view = await createView({ + View: getHtmlView(html, "test"), + data: this.data, + model: "foo", + arch: "" + }); + + 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`
`; + + const view = await createView({ + View: getOwlView(Renderer, "test"), + data: this.data, + model: "foo", + arch: "" + }); + + 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 Binary files /dev/null and b/addons/hr/static/xls/hr_employee.xls differ -- cgit v1.2.3