From 43122b3efc9fca3532eff36a35c15e35aa2aa125 Mon Sep 17 00:00:00 2001 From: Leonardo Pavan Rocha Date: Wed, 27 Sep 2023 13:42:23 +0200 Subject: [PATCH] [IMP] hr*: change employee field widgets to show private/public employee In HR modules we have both hr.employee and hr.employee.public models and the issue is that if a certain user doesn't have access to read the hr.employee record, it will throw errors/avatar pictures won't appear. For this reason, https://github.com/odoo/odoo/pull/116828 introduced a change in many2many employee fields so that they always show hr.employee.public records. However, we can improve this behavior by checking if the user has access to the record. If they have access, the private record is shown (making it easier to navigate, edit employee information) and if they don't have access, we show the public record. In order to allow forcing a relation, it was decided to convert all employee fields to accept a relation parameter in the options. If this option is set, we use that as the relation. This commit also changes the HR views using these widgets, removing the relation option from fields that would benefit from this dynamic check for the most suitable relation. task-3524305 closes odoo/odoo#136786 Related: odoo/enterprise#48008 Signed-off-by: Yannick Tivisse (yti) --- .../fields/employee_field_relation_mixin.js | 33 ++++ .../many2many_avatar_employee_field.js | 22 +-- .../many2one_avatar_employee_field.js | 11 +- .../static/tests/m2x_avatar_employee_tests.js | 156 ++++++++++++++++++ .../hr_contract/views/hr_contract_views.xml | 2 +- .../views/hr_leave_allocation_views.xml | 6 +- .../views/maintenance_views.xml | 9 +- .../hr_timesheet/views/project_task_views.xml | 17 +- 8 files changed, 217 insertions(+), 39 deletions(-) create mode 100644 addons/hr/static/src/views/fields/employee_field_relation_mixin.js diff --git a/addons/hr/static/src/views/fields/employee_field_relation_mixin.js b/addons/hr/static/src/views/fields/employee_field_relation_mixin.js new file mode 100644 index 00000000000..9db7d997f09 --- /dev/null +++ b/addons/hr/static/src/views/fields/employee_field_relation_mixin.js @@ -0,0 +1,33 @@ +/** @odoo-module **/ + +import { onWillStart } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +/** + * Mixin that handles public/private access of employee records in many2X fields + * @param { Class } fieldClass + * @returns Class + */ +export function EmployeeFieldRelationMixin(fieldClass) { + return class extends fieldClass { + static props = { + ...fieldClass.props, + relation: { type: String, optional: true }, + }; + + setup() { + super.setup(); + this.user = useService("user"); + onWillStart(async () => { + this.isHrUser = await this.user.hasGroup("hr.group_hr_user"); + }); + } + + get relation() { + if (this.props.relation) { + return this.props.relation; + } + return this.isHrUser ? "hr.employee" : "hr.employee.public"; + } + }; +} diff --git a/addons/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js b/addons/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js index 219be96469a..a1909f14d01 100644 --- a/addons/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js +++ b/addons/hr/static/src/views/fields/many2many_avatar_employee_field/many2many_avatar_employee_field.js @@ -8,12 +8,11 @@ import { kanbanMany2ManyTagsAvatarUserField, listMany2ManyTagsAvatarUserField, } from "@mail/views/web/fields/many2many_avatar_user_field/many2many_avatar_user_field"; +import { EmployeeFieldRelationMixin } from "@hr/views/fields/employee_field_relation_mixin"; -export class Many2ManyTagsAvatarEmployeeField extends Many2ManyTagsAvatarUserField { - get relation() { - return 'hr.employee.public'; - } -} +export class Many2ManyTagsAvatarEmployeeField extends EmployeeFieldRelationMixin( + Many2ManyTagsAvatarUserField +) {} export const many2ManyTagsAvatarEmployeeField = { ...many2ManyTagsAvatarUserField, @@ -25,16 +24,15 @@ export const many2ManyTagsAvatarEmployeeField = { extractProps: (fieldInfo, dynamicInfo) => ({ ...many2ManyTagsAvatarUserField.extractProps(fieldInfo, dynamicInfo), canQuickCreate: false, + relation: fieldInfo.options?.relation, }), }; registry.category("fields").add("many2many_avatar_employee", many2ManyTagsAvatarEmployeeField); -export class KanbanMany2ManyTagsAvatarEmployeeField extends KanbanMany2ManyTagsAvatarUserField { - get relation() { - return 'hr.employee.public'; - } -} +export class KanbanMany2ManyTagsAvatarEmployeeField extends EmployeeFieldRelationMixin( + KanbanMany2ManyTagsAvatarUserField +) {} export const kanbanMany2ManyTagsAvatarEmployeeField = { ...kanbanMany2ManyTagsAvatarUserField, @@ -43,6 +41,10 @@ export const kanbanMany2ManyTagsAvatarEmployeeField = { ...kanbanMany2ManyTagsAvatarUserField.additionalClasses, "o_field_many2many_avatar_user", ], + extractProps: (fieldInfo, dynamicInfo) => ({ + ...kanbanMany2ManyTagsAvatarUserField.extractProps(fieldInfo, dynamicInfo), + relation: fieldInfo.options?.relation, + }), }; registry diff --git a/addons/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js b/addons/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js index e36e793b436..aa475f05bb0 100644 --- a/addons/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js +++ b/addons/hr/static/src/views/fields/many2one_avatar_employee_field/many2one_avatar_employee_field.js @@ -7,8 +7,11 @@ import { many2OneAvatarUserField, kanbanMany2OneAvatarUserField, } from "@mail/views/web/fields/many2one_avatar_user_field/many2one_avatar_user_field"; +import { EmployeeFieldRelationMixin } from "@hr/views/fields/employee_field_relation_mixin"; -export class Many2OneAvatarEmployeeField extends Many2OneAvatarUserField { } +export class Many2OneAvatarEmployeeField extends EmployeeFieldRelationMixin( + Many2OneAvatarUserField +) {} export const many2OneAvatarEmployeeField = { ...many2OneAvatarUserField, @@ -26,7 +29,9 @@ export const many2OneAvatarEmployeeField = { registry.category("fields").add("many2one_avatar_employee", many2OneAvatarEmployeeField); -export class KanbanMany2OneAvatarEmployeeField extends KanbanMany2OneAvatarUserField { } +export class KanbanMany2OneAvatarEmployeeField extends EmployeeFieldRelationMixin( + KanbanMany2OneAvatarUserField +) {} export const kanbanMany2OneAvatarEmployeeField = { ...kanbanMany2OneAvatarUserField, @@ -34,7 +39,7 @@ export const kanbanMany2OneAvatarEmployeeField = { extractProps: (fieldInfo, dynamicInfo) => ({ ...kanbanMany2OneAvatarUserField.extractProps(fieldInfo, dynamicInfo), relation: fieldInfo.options?.relation, - }) + }), }; registry diff --git a/addons/hr/static/tests/m2x_avatar_employee_tests.js b/addons/hr/static/tests/m2x_avatar_employee_tests.js index fa3b2732e3e..24fe8c75122 100644 --- a/addons/hr/static/tests/m2x_avatar_employee_tests.js +++ b/addons/hr/static/tests/m2x_avatar_employee_tests.js @@ -6,6 +6,10 @@ import { start } from "@mail/../tests/helpers/test_utils"; import { dom } from "@web/../tests/legacy/helpers/test_utils"; import { contains } from "@web/../tests/utils"; +import { registry } from "@web/core/registry"; +import { makeFakeUserService } from "@web/../tests/helpers/mock_services"; + +const serviceRegistry = registry.category("services"); QUnit.module("M2XAvatarEmployee"); @@ -125,6 +129,94 @@ QUnit.test("many2one_avatar_employee widget in kanban view", async function (ass ); }); +QUnit.test("many2one_avatar_employee with hr group widget in kanban view", async function (assert) { + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({}); + const userId = pyEnv["res.users"].create({ partner_id: partnerId }); + const employeeId = pyEnv["hr.employee.public"].create({ + user_id: userId, + user_partner_id: partnerId, + }); + pyEnv["m2x.avatar.employee"].create({ + employee_id: employeeId, + employee_ids: [employeeId], + }); + + serviceRegistry.add( + "user", + makeFakeUserService(() => true), + { force: true } + ); + + const views = { + "m2x.avatar.employee,false,kanban": ` + + +
+ +
+
+
+
`, + }; + + const { openView } = await start({ serverData: { views } }); + await openView({ + res_model: "m2x.avatar.employee", + views: [[false, "kanban"]], + }); + assert.strictEqual(document.querySelector(".o_kanban_record").innerText.trim(), ""); + await contains(".o_m2o_avatar"); + assert.strictEqual( + document.querySelector(".o_m2o_avatar > img").getAttribute("data-src"), + `/web/image/hr.employee/${employeeId}/avatar_128` + ); +}); + +QUnit.test("many2one_avatar_employee with relation set in options", async function (assert) { + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({}); + const userId = pyEnv["res.users"].create({ partner_id: partnerId }); + const employeeId = pyEnv["hr.employee.public"].create({ + user_id: userId, + user_partner_id: partnerId, + }); + pyEnv["m2x.avatar.employee"].create({ + employee_id: employeeId, + employee_ids: [employeeId], + }); + + serviceRegistry.add( + "user", + makeFakeUserService(() => true), + { force: true } + ); + + const views = { + "m2x.avatar.employee,false,kanban": ` + + +
+ +
+
+
+
`, + }; + + const { openView } = await start({ serverData: { views } }); + await openView({ + res_model: "m2x.avatar.employee", + views: [[false, "kanban"]], + }); + assert.strictEqual(document.querySelector(".o_kanban_record").innerText.trim(), ""); + await contains(".o_m2o_avatar"); + assert.strictEqual( + document.querySelector(".o_m2o_avatar > img").getAttribute("data-src"), + `/web/image/hr.employee.public/${employeeId}/avatar_128` + ); +}); + QUnit.test( "many2one_avatar_employee: click on an employee not associated with a user", async function (assert) { @@ -220,6 +312,70 @@ QUnit.test("many2many_avatar_employee widget in form view", async function (asse assert.verifySteps([`web_read m2x.avatar.employee ${avatarId_1}`]); }); +QUnit.test("many2many_avatar_employee with hr group widget in form view", async function (assert) { + const pyEnv = await startServer(); + const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([{}, {}]); + const [userId_1, userId_2] = pyEnv["res.users"].create([ + { partner_id: partnerId_1 }, + { partner_id: partnerId_2 }, + ]); + const [employeeId_1, employeeId_2] = pyEnv["hr.employee.public"].create([ + { user_id: userId_1, user_partner_id: partnerId_1 }, + { user_id: userId_2, user_partner_id: partnerId_2 }, + ]); + const avatarId_1 = pyEnv["m2x.avatar.employee"].create({ + employee_ids: [employeeId_1, employeeId_2], + }); + const views = { + "m2x.avatar.employee,false,form": + '
', + }; + + // if the user doesn't have access to hr.group_hr_user, the employee field should show the public employee + serviceRegistry.add( + "user", + makeFakeUserService(() => true), + { force: true } + ); + + const { openView } = await start({ + mockRPC(route, args) { + if (args.method === "web_read") { + assert.step(`web_read ${args.model} ${args.args[0]}`); + } + if (args.method === "read") { + assert.step(`read ${args.model} ${args.args[0]}`); + } + }, + serverData: { views }, + }); + await openView({ + res_model: "m2x.avatar.employee", + res_id: avatarId_1, + views: [[false, "form"]], + }); + assert.containsN( + document.body, + ".o_field_many2many_avatar_employee .o_tag", + 2, + "should have 2 records" + ); + assert.strictEqual( + document + .querySelector(".o_field_many2many_avatar_employee .o_tag img") + .getAttribute("data-src"), + `/web/image/hr.employee/${employeeId_1}/avatar_128` + ); + + await dom.click( + document.querySelector(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar") + ); + await dom.click( + document.querySelectorAll(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar")[1] + ); + assert.verifySteps([`web_read m2x.avatar.employee ${avatarId_1}`]); +}); + QUnit.test("many2many_avatar_employee widget in list view", async function (assert) { const pyEnv = await startServer(); const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([ diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml index b86a678538c..eee0594cce5 100644 --- a/addons/hr_contract/views/hr_contract_views.xml +++ b/addons/hr_contract/views/hr_contract_views.xml @@ -194,7 +194,7 @@ - + diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml index 6aa65e67eba..63b2907d7dd 100644 --- a/addons/hr_holidays/views/hr_leave_allocation_views.xml +++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml @@ -174,14 +174,12 @@ + widget="many2one_avatar_employee"/> + required="holiday_type == 'employee' and state == 'confirm'"/> - + widget="many2one_avatar_employee" + options="{'no_create_edit': True, 'no_open': True}"/> diff --git a/addons/hr_timesheet/views/project_task_views.xml b/addons/hr_timesheet/views/project_task_views.xml index dbed1193568..73ce73662b6 100644 --- a/addons/hr_timesheet/views/project_task_views.xml +++ b/addons/hr_timesheet/views/project_task_views.xml @@ -43,10 +43,7 @@ - - @@ -69,10 +66,7 @@
- - +
@@ -99,12 +93,7 @@ - - +