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 00000000000..f8a05028eb3
--- /dev/null
+++ b/addons/hr/static/src/js/many2one_avatar_employee.js
@@ -0,0 +1,66 @@
+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 { _t } = require('web.core');
+ const fieldRegistry = require('web.field_registry');
+ const { Many2OneAvatarUser, KanbanMany2OneAvatarUser } = require('mail.Many2OneAvatarUser');
+ const session = require('web.session');
+
+
+ const Many2OneAvatarEmployeeMixin = {
+ supportedModel: 'hr.employee',
+
+ /**
+ * Set the field to read on 'hr.employee' to get the partner id.
+ *
+ * @override
+ */
+ init() {
+ this._super(...arguments);
+ this.partnerField = 'user_partner_id';
+ },
+
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ /**
+ * Display a warning if the user clicked on himself, or on an employee
+ * not associated with any user.
+ *
+ * @override
+ * @param {number} [partnerId] the id of the clicked partner
+ */
+ _displayWarning(partnerId) {
+ if (partnerId !== session.partner_id) {
+ // this is not ourself, so if we get here it means that the
+ // employee is not associated with any user
+ this.displayNotification({
+ title: _t('No user to chat with'),
+ message: _t('You can only chat with employees that have a dedicated user.'),
+ type: 'info',
+ });
+ } else {
+ this._super(...arguments);
+ }
+ },
+ };
+
+ 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/tests/many2one_avatar_employee_tests.js b/addons/hr/static/tests/many2one_avatar_employee_tests.js
new file mode 100644
index 00000000000..de3580eaedb
--- /dev/null
+++ b/addons/hr/static/tests/many2one_avatar_employee_tests.js
@@ -0,0 +1,218 @@
+odoo.define('hr.Many2OneAvatarEmployeeTests', function (require) {
+"use strict";
+
+const FormView = require('web.FormView');
+const KanbanView = require('web.KanbanView');
+const ListView = require('web.ListView');
+const { Many2OneAvatarEmployee } = require('hr.Many2OneAvatarEmployee');
+const { createView, dom, mock } = require('web.test_utils');
+
+
+QUnit.module('hr', {}, function () {
+ QUnit.module('Many2OneAvatarEmployee', {
+ beforeEach: function () {
+ // reset the cache before each test
+ Many2OneAvatarEmployee.prototype.partnerIds = {};
+
+ this.data = {
+ 'foo': {
+ fields: {
+ employee_id: { string: "Employee", type: 'many2one', relation: 'hr.employee' },
+ },
+ records: [
+ { id: 1, employee_id: 11 },
+ { id: 2, employee_id: 7 },
+ { id: 3, employee_id: 11 },
+ { id: 4, employee_id: 23 },
+ ],
+ },
+ 'hr.employee': {
+ fields: {
+ display_name: { string: "Name", type: "char" },
+ user_partner_id: { string: "Partner", type: "many2one", relation: 'res.partner' },
+ },
+ records: [{
+ id: 11,
+ name: "Mario",
+ user_partner_id: 1,
+ }, {
+ id: 7,
+ name: "Luigi",
+ user_partner_id: 2,
+ }, {
+ id: 23,
+ name: "Yoshi",
+ user_partner_id: 3,
+ }],
+ },
+ 'res.partner': {
+ fields: {
+ display_name: { string: "Name", type: "char" },
+ },
+ records: [{
+ id: 1,
+ display_name: "Partner 1",
+ }, {
+ id: 2,
+ display_name: "Partner 2",
+ }, {
+ id: 3,
+ display_name: "Partner 3",
+ }],
+ },
+ };
+ },
+ });
+
+ QUnit.test('many2one_avatar_employee widget in list view', async function (assert) {
+ assert.expect(7);
+
+ const list = await createView({
+ 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);
+ },
+ });
+
+ mock.intercept(list, 'call_service', ev => {
+ if (ev.data.service === 'mail_service') {
+ assert.step(`call service ${ev.data.method} ${ev.data.args[0]}`);
+ }
+ }, true);
+
+ assert.strictEqual(list.$('.o_data_cell span').text(), 'MarioLuigiMarioYoshi');
+
+ await dom.click(list.$('.o_data_cell:nth(0) .o_m2o_avatar'));
+ await dom.click(list.$('.o_data_cell:nth(1) .o_m2o_avatar'));
+ await dom.click(list.$('.o_data_cell:nth(2) .o_m2o_avatar'));
+
+
+ assert.verifySteps([
+ 'read hr.employee 11',
+ 'call service openDMChatWindow 1',
+ 'read hr.employee 7',
+ 'call service openDMChatWindow 2',
+ 'call service openDMChatWindow 1',
+ ]);
+
+ list.destroy();
+ });
+
+ QUnit.test('many2one_avatar_employee widget in kanban view', async function (assert) {
+ assert.expect(6);
+
+ const kanban = await createView({
+ 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/11/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(1)').data('src'), '/web/image/hr.employee/7/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(2)').data('src'), '/web/image/hr.employee/11/image_128');
+ assert.strictEqual(kanban.$('.o_m2o_avatar:nth(3)').data('src'), '/web/image/hr.employee/23/image_128');
+
+ kanban.destroy();
+ });
+
+ QUnit.test('many2one_avatar_employee: click on an employee not associated with a user', async function (assert) {
+ assert.expect(5);
+
+ this.data['hr.employee'].records[0].user_partner_id = false;
+ const form = await createView({
+ 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 === 'mail_service') {
+ throw new Error('should not call mail_service');
+ }
+ if (ev.data.service === 'notification') {
+ assert.step(`display notification "${ev.data.args[0].title}"`);
+ }
+ }, 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 11',
+ 'display notification "No user to chat with"',
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one_avatar_employee: click on self', async function (assert) {
+ assert.expect(5);
+
+ const form = await createView({
+ 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);
+ },
+ session: {
+ partner_id: 1,
+ },
+ res_id: 1,
+ });
+
+ mock.intercept(form, 'call_service', (ev) => {
+ if (ev.data.service === 'mail_service') {
+ throw new Error('should not call mail_service');
+ }
+ if (ev.data.service === 'notification') {
+ assert.step(`display notification "${ev.data.args[0].title}"`);
+ }
+ }, 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 11',
+ 'display notification "Cannot chat with yourself"',
+ ]);
+
+ form.destroy();
+ });
+});
+});
diff --git a/addons/hr/views/hr_templates.xml b/addons/hr/views/hr_templates.xml
index 8197751d378..2cfac7e0cc8 100644
--- a/addons/hr/views/hr_templates.xml
+++ b/addons/hr/views/hr_templates.xml
@@ -6,6 +6,13 @@
+
+
+
+
+
+
+
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index e7f350ee01c..9a78eeb830a 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -2155,6 +2155,11 @@ Relational fields
- Supported field types: *many2one* (pointing to 'res.users')
+- many2one_avatar_employee (Many2OneAvatarEmployee)
+ Same as Many2OneAvatarUser, but for many2one fields pointing to 'hr.employee'.
+
+ - Supported field types: *many2one* (pointing to 'hr.employee')
+
- kanban.many2one (KanbanFieldMany2One)
Default widget for many2one fields (in kanban view). We need to disable all
editing in kanban views.