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 0000000000000000000000000000000000000000..f8a05028eb33e6ccdf099641b4fe52f9a277222d --- /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: + // <field name="employee_id" widget="many2one_avatar_employee"/> + + 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 0000000000000000000000000000000000000000..de3580eaedb037332be983da1741bf2c9b8f6706 --- /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: '<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); + }, + }); + + 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: ` + <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/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: '<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 === '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: '<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); + }, + 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 8197751d378f5a6a2b76d12df585200e2421cefc..2cfac7e0cc809f5fff854e8205a6bd7c235d0f47 100644 --- a/addons/hr/views/hr_templates.xml +++ b/addons/hr/views/hr_templates.xml @@ -6,6 +6,13 @@ <link rel="stylesheet" type="text/scss" href="/hr/static/src/scss/hr.scss"/> <script type="text/javascript" src="/hr/static/src/js/chat.js"></script> <script type="text/javascript" src="/hr/static/src/js/language.js"></script> + <script type="text/javascript" src="/hr/static/src/js/many2one_avatar_employee.js"></script> + </xpath> + </template> + + <template id="qunit_suite" name="hr tests" inherit_id="web.qunit_suite_tests"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/hr/static/tests/many2one_avatar_employee_tests.js"></script> </xpath> </template> </odoo> diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index e7f350ee01c9a1067ce9985a86be7e2c449e63a3..9a78eeb830a00757889efd83069374c41c0c19b4 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.