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 @@ - - +