diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py
index acdcee85c50..3d3ecd622b6 100644
--- a/addons/hr/__manifest__.py
+++ b/addons/hr/__manifest__.py
@@ -52,7 +52,7 @@
'hr/static/src/scss/hr.scss',
'hr/static/src/js/chat.js',
'hr/static/src/js/language.js',
- 'hr/static/src/js/many2one_avatar_employee.js',
+ 'hr/static/src/js/m2x_avatar_employee.js',
'hr/static/src/js/standalone_m2o_avatar_employee.js',
'hr/static/src/models/employee/employee.js',
'hr/static/src/models/messaging/messaging.js',
@@ -61,7 +61,7 @@
],
'web.qunit_suite_tests': [
'hr/static/tests/helpers/mock_models.js',
- 'hr/static/tests/many2one_avatar_employee_tests.js',
+ 'hr/static/tests/m2x_avatar_employee_tests.js',
'hr/static/tests/standalone_m2o_avatar_employee_tests.js',
],
'web.assets_qweb': [
diff --git a/addons/hr/static/src/js/m2x_avatar_employee.js b/addons/hr/static/src/js/m2x_avatar_employee.js
new file mode 100644
index 00000000000..5e6961fab50
--- /dev/null
+++ b/addons/hr/static/src/js/m2x_avatar_employee.js
@@ -0,0 +1,57 @@
+/** @odoo-module alias=hr.Many2OneAvatarEmployee **/
+
+import fieldRegistry from 'web.field_registry';
+
+import { Many2OneAvatarUser, KanbanMany2OneAvatarUser } from '@mail/js/m2x_avatar_user';
+import { Many2ManyAvatarUser } from '@mail/js/m2x_avatar_user';
+
+
+// This module defines variants of the Many2OneAvatarUser and Many2ManyAvatarUser
+// field widgets, to support fields pointing to 'hr.employee'. It also defines the
+// kanban version of the Many2OneAvatarEmployee widget.
+//
+// Usage:
+//
+
+const M2XAvatarEmployeeMixin = {
+ supportedModels: ['hr.employee', 'hr.employee.public'],
+
+ //----------------------------------------------------------------------
+ // Private
+ //----------------------------------------------------------------------
+
+ _getEmployeeID() {
+ return this.value.res_id;
+ },
+
+ //----------------------------------------------------------------------
+ // Handlers
+ //----------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _onAvatarClicked(ev) {
+ ev.stopPropagation(); // in list view, prevent from opening the record
+ const employeeId = this._getEmployeeID(ev);
+ this._openChat({ employeeId: employeeId });
+ }
+};
+
+export const Many2OneAvatarEmployee = Many2OneAvatarUser.extend(M2XAvatarEmployeeMixin);
+export const KanbanMany2OneAvatarEmployee = KanbanMany2OneAvatarUser.extend(M2XAvatarEmployeeMixin);
+
+fieldRegistry.add('many2one_avatar_employee', Many2OneAvatarEmployee);
+fieldRegistry.add('kanban.many2one_avatar_employee', KanbanMany2OneAvatarEmployee);
+
+export const Many2ManyAvatarEmployee = Many2ManyAvatarUser.extend(M2XAvatarEmployeeMixin, {
+ _getEmployeeID(ev) {
+ return parseInt(ev.target.getAttribute('data-id'), 10);
+ },
+});
+
+fieldRegistry.add('many2many_avatar_employee', Many2ManyAvatarEmployee);
+
+export default {
+ Many2OneAvatarEmployee,
+};
diff --git a/addons/hr/static/src/js/many2one_avatar_employee.js b/addons/hr/static/src/js/many2one_avatar_employee.js
deleted file mode 100644
index 8801b733c0a..00000000000
--- a/addons/hr/static/src/js/many2one_avatar_employee.js
+++ /dev/null
@@ -1,43 +0,0 @@
-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/js/many2one_avatar_user');
-
- 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/tests/many2one_avatar_employee_tests.js b/addons/hr/static/tests/m2x_avatar_employee_tests.js
similarity index 54%
rename from addons/hr/static/tests/many2one_avatar_employee_tests.js
rename to addons/hr/static/tests/m2x_avatar_employee_tests.js
index f105077c47d..99bd0097d59 100644
--- a/addons/hr/static/tests/many2one_avatar_employee_tests.js
+++ b/addons/hr/static/tests/m2x_avatar_employee_tests.js
@@ -1,4 +1,4 @@
-odoo.define('hr.Many2OneAvatarEmployeeTests', function (require) {
+odoo.define('hr.M2XAvatarEmployeeTests', function (require) {
"use strict";
const {
@@ -15,7 +15,7 @@ const { Many2OneAvatarEmployee } = require('hr.Many2OneAvatarEmployee');
const { dom, mock } = require('web.test_utils');
QUnit.module('hr', {}, function () {
- QUnit.module('Many2OneAvatarEmployee', {
+ QUnit.module('M2XAvatarEmployee', {
beforeEach() {
beforeEach(this);
@@ -26,9 +26,10 @@ QUnit.module('hr', {}, function () {
'foo': {
fields: {
employee_id: { string: "Employee", type: 'many2one', relation: 'hr.employee.public' },
+ employee_ids: { string: "Employees", type: "many2many", relation: 'hr.employee.public' },
},
records: [
- { id: 1, employee_id: 11 },
+ { id: 1, employee_id: 11, employee_ids: [11, 23] },
{ id: 2, employee_id: 7 },
{ id: 3, employee_id: 11 },
{ id: 4, employee_id: 23 },
@@ -211,5 +212,171 @@ QUnit.module('hr', {}, function () {
form.destroy();
});
+
+ QUnit.test('many2many_avatar_employee widget in form view', async function (assert) {
+ assert.expect(8);
+
+ const { widget: form } = await start({
+ hasChatWindow: true,
+ 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,
+ });
+
+ assert.containsN(form, '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
+ "should have 2 records");
+ assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'),
+ '/web/image/hr.employee.public/11/image_128',
+ "should have correct avatar image");
+
+ await dom.click(form.$('.o_field_many2manytags.avatar .badge:first .o_m2m_avatar'));
+ await dom.click(form.$('.o_field_many2manytags.avatar .badge:nth(1) .o_m2m_avatar'));
+
+ assert.verifySteps([
+ "read foo 1",
+ 'read hr.employee.public 11,23',
+ "read hr.employee.public 11",
+ "read hr.employee.public 23",
+ ]);
+
+ assert.containsN(
+ document.body,
+ '.o_ChatWindowHeader_name',
+ 2,
+ "should have 2 chat windows"
+ );
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many_avatar_employee widget in list view', async function (assert) {
+ assert.expect(10);
+
+ 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:first .o_tag_badge_text').text().trim(), 'MarioYoshi');
+
+ // click on first employee badge
+ await afterNextRender(() =>
+ dom.click(list.$('.o_data_cell:nth(0) .o_m2m_avatar:first'))
+ );
+ assert.verifySteps(
+ ['read hr.employee.public 11,23', "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(0) .o_m2m_avatar:nth(1)')
+ ));
+ assert.verifySteps(
+ ['read hr.employee.public 23'],
+ "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,
+ "Yoshi",
+ 'chat window should be with clicked employee'
+ );
+
+ list.destroy();
+ });
+
+ QUnit.test('many2many_avatar_employee: click on an employee not associated with a user', async function (assert) {
+ assert.expect(10);
+
+ 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({
+ hasChatWindow: true,
+ 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.containsN(form, '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
+ "should have 2 records");
+ assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'),
+ '/web/image/hr.employee.public/11/image_128',
+ "should have correct avatar image");
+
+ await dom.click(form.$('.o_field_many2manytags.avatar .badge:first .o_m2m_avatar'));
+ await dom.click(form.$('.o_field_many2manytags.avatar .badge:nth(1) .o_m2m_avatar'));
+
+ assert.verifySteps([
+ 'read foo 1',
+ 'read hr.employee.public 11,23',
+ "read hr.employee.public 11",
+ "read hr.employee.public 23"
+ ]);
+
+ 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"
+ );
+ assert.containsOnce(document.body, '.o_ChatWindowHeader_name',
+ "should have 1 chat window");
+
+ form.destroy();
+ });
});
});