Eliminate Python dependency: embed frontend assets in odoo-go

- Copy all OWL frontend assets (JS/CSS/XML/fonts/images) into frontend/
  directory (2925 files, 43MB) — no more runtime reads from Python Odoo
- Replace OdooAddonsPath config with FrontendDir pointing to local frontend/
- Rewire bundle.go, static.go, templates.go, webclient.go to read from
  frontend/ instead of external Python Odoo addons directory
- Auto-detect frontend/ and build/ dirs relative to binary in main.go
- Delete obsolete Python helper scripts (tools/*.py)

The Go server is now fully self-contained: single binary + frontend/ folder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-03-31 23:09:12 +02:00
parent 0ed29fe2fd
commit 8741282322
2933 changed files with 280644 additions and 264 deletions

View File

@@ -0,0 +1,32 @@
import { HrDepartment } from "@hr/../tests/mock_server/mock_models/hr_department";
import { HrEmployee } from "@hr/../tests/mock_server/mock_models/hr_employee";
import { HrEmployeePublic } from "@hr/../tests/mock_server/mock_models/hr_employee_public";
import { M2xAvatarEmployee } from "@hr/../tests/mock_server/mock_models/m2x_avatar_employee";
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { defineModels } from "@web/../tests/web_test_helpers";
import { FakeUser } from "@hr/../tests/mock_server/mock_models/fake_user";
import { HrVersion } from "./mock_server/mock_models/hr_version";
import { HrJob } from "./mock_server/mock_models/hr_job";
import { HrWorkLocation } from "./mock_server/mock_models/hr_work_location";
import { ResourceResource } from "@resource/../tests/mock_server/mock_models/resource_resource";
import { ResUsers } from "./mock_server/mock_models/res_users";
import { ResPartner } from "./mock_server/mock_models/res_partner";
export function defineHrModels() {
return defineModels(hrModels);
}
export const hrModels = {
...mailModels,
M2xAvatarEmployee,
HrDepartment,
HrEmployee,
HrVersion,
HrEmployeePublic,
FakeUser,
HrJob,
HrWorkLocation,
ResourceResource,
ResUsers,
ResPartner,
};

View File

@@ -0,0 +1,3 @@
import { unpatchAvatarCardPopover } from "@hr/components/avatar_card/avatar_card_popover_patch";
unpatchAvatarCardPopover();

View File

@@ -0,0 +1,501 @@
import { defineHrModels } from "@hr/../tests/hr_test_helpers";
import { start } from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { waitFor } from "@odoo/hoot-dom";
import { contains, makeMockServer, mountView, onRpc } from "@web/../tests/web_test_helpers";
import { getOrigin } from "@web/core/utils/urls";
describe.current.tags("desktop");
defineHrModels();
test("many2one in list view", async () => {
const { env } = await makeMockServer();
const [partnerId_1, partnerId_2] = env["res.partner"].create([
{ name: "Mario" },
{ name: "Luigi" },
]);
const [userId_1, userId_2] = env["res.users"].create([
{ partner_id: partnerId_1 },
{ partner_id: partnerId_2 },
]);
const [employeeId_1, employeeId_2] = env["hr.employee.public"].create([
{
name: "Mario",
user_id: userId_1,
user_partner_id: partnerId_1,
work_email: "Mario@partner.com",
},
{
name: "Luigi",
user_id: userId_2,
user_partner_id: partnerId_2,
},
]);
env["m2x.avatar.employee"].create([
{
employee_id: employeeId_1,
employee_ids: [employeeId_1, employeeId_2],
},
{ employee_id: employeeId_2 },
{ employee_id: employeeId_1 },
]);
await start();
onRpc("has_group", () => false);
await mountView({
type: "list",
resModel: "m2x.avatar.employee",
arch: `<list><field name="employee_id" widget="many2one_avatar_employee"/></list>`,
});
expect(".o_data_cell div[name='employee_id']:eq(0)").toHaveText("Mario");
expect(".o_data_cell div[name='employee_id']:eq(1)").toHaveText("Luigi");
expect(".o_data_cell div[name='employee_id']:eq(2)").toHaveText("Mario");
expect("div[name='employee_id'] a").toHaveCount(0);
// click on first employee avatar
await contains(".o_data_cell .o_m2o_avatar > img:eq(0)").click();
await waitFor(".o_avatar_card");
expect(".o_card_user_infos > span").toHaveText("Mario");
expect(".o_card_user_infos > a").toHaveText("Mario@partner.com");
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow");
await waitFor(".o-mail-ChatWindow-header:contains('Mario')");
// click on second employee
await contains(".o_data_cell .o_m2o_avatar > img:eq(1)").click();
expect(".o_card_user_infos span").toHaveText("Luigi");
expect(".o_avatar_card").toHaveCount(1);
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow-header:contains('Luigi')");
expect(".o-mail-ChatWindow").toHaveCount(2);
// click on third employee (same as first)
await contains(".o_data_cell .o_m2o_avatar > img:eq(2)").click();
expect(".o_card_user_infos span").toHaveText("Mario");
expect(".o_avatar_card").toHaveCount(1);
expect(".o_card_user_infos span:eq(0)").toHaveText("Mario");
expect(".o_card_user_infos > a").toHaveText("Mario@partner.com");
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow-header:contains('Mario')");
expect(".o-mail-ChatWindow").toHaveCount(2);
});
test("many2one in kanban view", async () => {
const { env } = await makeMockServer();
const partnerId = env["res.partner"].create({});
const userId = env["res.users"].create({ partner_id: partnerId });
const employeeId = env["hr.employee.public"].create({
user_id: userId,
user_partner_id: partnerId,
});
env["m2x.avatar.employee"].create({
employee_id: employeeId,
employee_ids: [employeeId],
});
onRpc("has_group", () => false);
await mountView({
type: "kanban",
resModel: "m2x.avatar.employee",
arch: `<kanban>
<templates>
<t t-name="card">
<field name="employee_id" widget="many2one_avatar_employee"/>
</t>
</templates>
</kanban>`,
});
expect(".o_kanban_record:eq(0)").toHaveText("");
await waitFor(".o_m2o_avatar");
expect(".o_m2o_avatar > img:eq(0)").toHaveAttribute(
"data-src",
`/web/image/hr.employee.public/${employeeId}/avatar_128`
);
});
test("many2one: click on an employee not associated with a user", async () => {
const { env } = await makeMockServer();
const employeeId = env["hr.employee.public"].create({ name: "Mario" });
const avatarId = env["m2x.avatar.employee"].create({ employee_id: employeeId });
onRpc("has_group", () => false);
await mountView({
type: "form",
resModel: "m2x.avatar.employee",
resId: avatarId,
arch: `<form><field name="employee_id" widget="many2one_avatar_employee"/></form>`,
});
await waitFor(".o_field_widget[name=employee_id] input:value(Mario)");
await contains(".o_m2o_avatar > img").click();
});
test("many2one with hr group widget in kanban view", async () => {
const { env } = await makeMockServer();
const partnerId = env["res.partner"].create({});
const userId = env["res.users"].create({ partner_id: partnerId });
const employeeId = env["hr.employee.public"].create({
user_id: userId,
user_partner_id: partnerId,
});
env["m2x.avatar.employee"].create({
employee_id: employeeId,
employee_ids: [employeeId],
});
await mountView({
type: "kanban",
resModel: "m2x.avatar.employee",
arch: `<kanban>
<templates>
<t t-name="card">
<field name="employee_id" widget="many2one_avatar_employee"/>
</t>
</templates>
</kanban>`,
});
expect(".o_kanban_record:eq(0)").toHaveText("");
await waitFor(".o_m2o_avatar");
expect(".o_m2o_avatar > img:eq(0)").toHaveAttribute(
"data-src",
`/web/image/hr.employee/${employeeId}/avatar_128`
);
});
test("many2one with relation set in options", async () => {
const { env } = await makeMockServer();
const partnerId = env["res.partner"].create({});
const userId = env["res.users"].create({ partner_id: partnerId });
const employeeId = env["hr.employee.public"].create({
user_id: userId,
user_partner_id: partnerId,
});
env["m2x.avatar.employee"].create({
employee_id: employeeId,
employee_ids: [employeeId],
});
await mountView({
type: "kanban",
resModel: "m2x.avatar.employee",
arch: `<kanban>
<templates>
<t t-name="card">
<field name="employee_id" widget="many2one_avatar_employee" options="{'relation': 'hr.employee.public'}"/>
</t>
</templates>
</kanban>`,
});
expect(".o_kanban_record:eq(0)").toHaveText("");
await waitFor(".o_m2o_avatar");
expect(".o_m2o_avatar > img:eq(0)").toHaveAttribute(
"data-src",
`/web/image/hr.employee.public/${employeeId}/avatar_128`
);
});
test("many2one without hr.group_hr_user", async () => {
const { env } = await makeMockServer();
env["m2x.avatar.employee"].create({});
env["hr.employee"].create({ name: "babar" });
env["hr.employee.public"].create({ name: "babar" });
onRpc("web_name_search", (args) => {
expect.step("web_name_search");
expect(args.model).toBe("hr.employee.public");
});
onRpc("web_search_read", (args) => {
expect.step("web_search_read");
expect(args.model).toBe("hr.employee.public");
});
onRpc("has_group", () => false);
await mountView({
type: "form",
resModel: "m2x.avatar.employee",
arch: `<form><field name="employee_id" widget="many2one_avatar_employee"/></form>`,
});
await waitFor(".o-autocomplete--input.o_input");
await contains(".o-autocomplete--input.o_input").click();
expect.verifySteps(["web_name_search"]);
await waitFor(".o_m2o_dropdown_option_search_more");
await contains(".o_m2o_dropdown_option_search_more").click();
expect.verifySteps(["web_search_read"]);
});
test("many2one in form view", async () => {
const { env } = await makeMockServer();
const [partnerId_1, partnerId_2] = env["res.partner"].create([
{ name: "Mario" },
{ name: "Luigi" },
]);
const [userId_1, userId_2] = env["res.users"].create([
{ partner_id: partnerId_1 },
{ partner_id: partnerId_2 },
]);
const [employeeId_1, employeeId_2] = env["hr.employee.public"].create([
{
user_id: userId_1,
user_partner_id: partnerId_1,
name: "Mario",
work_email: "Mario@partner.com",
},
{
name: "Luigi",
user_id: userId_2,
user_partner_id: partnerId_2,
},
]);
const avatarId_1 = env["m2x.avatar.employee"].create({
employee_ids: [employeeId_1, employeeId_2],
});
await start();
onRpc("has_group", () => false);
await mountView({
type: "form",
resId: avatarId_1,
resModel: "m2x.avatar.employee",
arch: `<form><field name="employee_ids" widget="many2many_avatar_employee"/></form>`,
});
expect(".o_field_many2many_avatar_employee .o_tag").toHaveCount(2);
expect(".o_field_many2many_avatar_employee .o_tag img:eq(0)").toHaveAttribute(
"data-src",
`${getOrigin()}/web/image/hr.employee.public/${employeeId_1}/avatar_128`
);
// Clicking on first employee's avatar
await contains(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar:eq(0)").click();
await waitFor(".o_avatar_card");
expect(".o_card_user_infos > span").toHaveText("Mario");
expect(".o_card_user_infos > a").toHaveText("Mario@partner.com");
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow");
await waitFor(".o-mail-ChatWindow-header:contains('Mario')");
// Clicking on second employee's avatar
await contains(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar:eq(1)").click();
expect(".o_card_user_infos span").toHaveText("Luigi");
expect(".o_avatar_card").toHaveCount(1);
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow-header:contains('Luigi')");
expect(".o-mail-ChatWindow").toHaveCount(2);
});
test("many2one with hr group widget in form view", async () => {
const { env } = await makeMockServer();
const [partnerId_1, partnerId_2] = env["res.partner"].create([{}, {}]);
const [userId_1, userId_2] = env["res.users"].create([
{ partner_id: partnerId_1 },
{ partner_id: partnerId_2 },
]);
const [employeeData_1, employeeData_2] = [
{ user_id: userId_1, user_partner_id: partnerId_1 },
{ user_id: userId_2, user_partner_id: partnerId_2 },
];
env["hr.employee"].create([{ ...employeeData_1 }, { ...employeeData_2 }]);
const [employeeId_1, employeeId_2] = env["hr.employee.public"].create([
{ ...employeeData_1 },
{ ...employeeData_2 },
]);
const avatarId_1 = env["m2x.avatar.employee"].create({
employee_ids: [employeeId_1, employeeId_2],
});
expect.step(`read hr.employee ${employeeId_1}`);
expect.step(`read hr.employee ${employeeId_2}`);
await mountView({
type: "form",
resId: avatarId_1,
resModel: "m2x.avatar.employee",
arch: `<form><field name="employee_ids" widget="many2many_avatar_employee"/></form>`,
});
expect(".o_field_many2many_avatar_employee .o_tag").toHaveCount(2);
expect(".o_field_many2many_avatar_employee .o_tag img:eq(0)").toHaveAttribute(
"data-src",
`${getOrigin()}/web/image/hr.employee/${employeeId_1}/avatar_128`
);
await contains(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar:eq(0)").click();
await contains(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar:eq(1)").click();
expect.verifySteps([
`read hr.employee ${employeeId_1}`,
`read hr.employee ${employeeId_2}`,
]);
});
test("many2one widget in list view", async () => {
const { env } = await makeMockServer();
const [partnerId_1, partnerId_2] = env["res.partner"].create([
{ name: "Mario" },
{ name: "Yoshi" },
]);
const [userId_1, userId_2] = env["res.users"].create([
{ partner_id: partnerId_1 },
{ partner_id: partnerId_2 },
]);
const [employeeId_1, employeeId_2] = env["hr.employee.public"].create([
{
name: "Mario",
user_id: userId_1,
user_partner_id: partnerId_1,
work_email: "Mario@partner.com",
},
{
name: "Yoshi",
user_id: userId_2,
user_partner_id: partnerId_2,
},
]);
env["m2x.avatar.employee"].create({
employee_ids: [employeeId_1, employeeId_2],
});
onRpc("has_group", () => false);
await start();
await mountView({
type: "list",
resModel: "m2x.avatar.employee",
arch: `<list><field name="employee_ids" widget="many2many_avatar_employee"/></list>`,
});
expect(".o_data_cell:first .o_field_many2many_avatar_employee > div > span").toHaveCount(2);
// Clicking on first employee's avatar
await contains(".o_data_cell .o_m2m_avatar:eq(0)").click();
await waitFor(".o_avatar_card");
expect(".o_card_user_infos > span").toHaveText("Mario");
expect(".o_card_user_infos > a").toHaveText("Mario@partner.com");
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow");
await waitFor(".o-mail-ChatWindow-header:contains('Mario')");
// Clicking on second employee's avatar
await contains(".o_data_cell .o_m2m_avatar:eq(1)").click();
expect(".o_card_user_infos span").toHaveText("Yoshi");
expect(".o_avatar_card").toHaveCount(1);
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow-header:contains('Yoshi')");
});
test("many2many in kanban view", async () => {
const { env } = await makeMockServer();
const [partnerId_1, partnerId_2] = env["res.partner"].create([
{ name: "Mario" },
{ name: "Luigi" },
]);
const [userId_1, userId_2] = env["res.users"].create([
{ partner_id: partnerId_1 },
{ partner_id: partnerId_2 },
]);
const [employeeId_1, employeeId_2] = env["hr.employee.public"].create([
{
user_id: userId_1,
user_partner_id: partnerId_1,
name: "Mario",
work_email: "Mario@partner.com",
},
{
name: "Luigi",
user_id: userId_2,
user_partner_id: partnerId_2,
},
]);
env["m2x.avatar.employee"].create({
employee_ids: [employeeId_1, employeeId_2],
});
onRpc("has_group", () => false);
await start();
await mountView({
type: "kanban",
resModel: "m2x.avatar.employee",
arch: `<kanban>
<templates>
<t t-name="card">
<footer>
<field name="employee_ids" widget="many2many_avatar_employee"/>
</footer>
</t>
</templates>
</kanban>`,
});
expect(
".o_kanban_record:first .o_field_many2many_avatar_employee img.o_m2m_avatar"
).toHaveCount(2);
expect(
".o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar:eq(0)"
).toHaveAttribute(
"data-src",
`${getOrigin()}/web/image/hr.employee.public/${employeeId_2}/avatar_128`
);
expect(
".o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar:eq(1)"
).toHaveAttribute(
"data-src",
`${getOrigin()}/web/image/hr.employee.public/${employeeId_1}/avatar_128`
);
// Clicking on first employee's avatar
await contains(".o_kanban_record img.o_m2m_avatar:eq(1)").click();
await waitFor(".o_avatar_card");
expect(".o_card_user_infos > span").toHaveText("Mario");
expect(".o_card_user_infos > a").toHaveText("Mario@partner.com");
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow");
await waitFor(".o-mail-ChatWindow-header:contains('Mario')");
// Clicking on second employee's avatar
await contains(".o_kanban_record img.o_m2m_avatar:eq(0)").click();
expect(".o_card_user_infos span").toHaveText("Luigi");
expect(".o_avatar_card").toHaveCount(1);
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow-header:contains('Luigi')");
expect(".o-mail-ChatWindow").toHaveCount(2);
});
test("many2many: click on an employee not associated with a user", async () => {
const { env } = await makeMockServer();
const partnerId = env["res.partner"].create({ name: "Luigi" });
const userId = env["res.users"].create({ partner_id: partnerId });
const [employeeId_1, employeeId_2] = env["hr.employee.public"].create([
{
name: "Mario",
work_email: "Mario@partner.com",
},
{
name: "Luigi",
user_id: userId,
user_partner_id: partnerId,
},
]);
const avatarId = env["m2x.avatar.employee"].create({
employee_ids: [employeeId_1, employeeId_2],
});
onRpc("has_group", () => false);
await start();
await mountView({
type: "form",
resModel: "m2x.avatar.employee",
resId: avatarId,
arch: `<form><field name="employee_ids" widget="many2many_avatar_employee"/></form>`,
});
expect(".o_field_many2many_avatar_employee .o_tag").toHaveCount(2);
expect(".o_field_many2many_avatar_employee .o_tag img:eq(0)").toHaveAttribute(
"data-src",
`${getOrigin()}/web/image/hr.employee.public/${employeeId_1}/avatar_128`
);
// Clicking on first employee's avatar (employee with no user)
await contains(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar:eq(0)").click();
await waitFor(".o_avatar_card");
expect(".o_card_user_infos > span").toHaveText("Mario");
expect(".o_card_user_infos > a").toHaveText("Mario@partner.com");
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("View Profile");
// Clicking on second employee's avatar (employee with user)
await contains(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar:eq(1)").click();
expect(".o_card_user_infos span").toHaveText("Luigi");
expect(".o_avatar_card").toHaveCount(1);
expect(".o_avatar_card_buttons button:eq(0)").toHaveText("Send message");
await contains(".o_avatar_card_buttons button:eq(0)").click();
await waitFor(".o-mail-ChatWindow-header:contains('Luigi')");
expect(".o-mail-ChatWindow").toHaveCount(1);
});

View File

@@ -0,0 +1,8 @@
import { fields, models } from "@web/../tests/web_test_helpers";
export class FakeUser extends models.Model {
_name = "fake.user";
name = fields.Char({ string: "Name" });
lang = fields.Char({ string: "Lang" });
}

View File

@@ -0,0 +1,31 @@
import { models, fields } from "@web/../tests/web_test_helpers";
export class HrDepartment extends models.ServerModel {
_name = "hr.department";
_rec_name = "complete_name";
name = fields.Char();
complete_name = fields.Char({
compute: "_compute_complete_name",
});
display_name = fields.Char({
compute: "_compute_display_name",
});
_compute_complete_name() {
for (const department of this) {
department.complete_name = department.name;
}
}
_compute_display_name() {
this._compute_complete_name();
for (const department of this) {
department.display_name = department.complete_name;
}
}
get _to_store_defaults() {
return ["name"];
}
}

View File

@@ -0,0 +1,29 @@
import { fields, models } from "@web/../tests/web_test_helpers";
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
export class HrEmployee extends models.ServerModel {
_name = "hr.employee";
department_id = fields.Many2one({ relation: "hr.department" });
work_email = fields.Char();
work_phone = fields.Char();
work_location_type = fields.Char();
work_location_id = fields.Many2one({ relation: "hr.work.location" });
job_title = fields.Char();
_get_store_avatar_card_fields() {
return [
"company_id",
mailDataHelpers.Store.one("department_id", ["name"]),
"work_email",
mailDataHelpers.Store.one("work_location_id", ["location_type", "name"]),
"work_phone",
"job_title",
];
}
_views = {
search: `<search><field name="display_name" string="Name" /></search>`,
list: `<list><field name="display_name"/></list>`,
};
}

View File

@@ -0,0 +1,10 @@
import { models } from "@web/../tests/web_test_helpers";
export class HrEmployeePublic extends models.ServerModel {
_name = "hr.employee.public";
_views = {
search: `<search><field name="display_name" string="Name" /></search>`,
list: `<list><field name="display_name"/></list>`,
};
}

View File

@@ -0,0 +1,10 @@
import { models } from "@web/../tests/web_test_helpers";
export class HrJob extends models.ServerModel {
_name = "hr.job";
_views = {
search: `<search><field name="display_name" string="Name" /></search>`,
list: `<list><field name="display_name"/></list>`,
};
}

View File

@@ -0,0 +1,10 @@
import { models } from "@web/../tests/web_test_helpers";
export class HrVersion extends models.ServerModel {
_name = "hr.version";
_views = {
search: `<search><field name="display_name" string="Name" /></search>`,
list: `<list><field name="display_name"/></list>`,
};
}

View File

@@ -0,0 +1,19 @@
import { models, fields } from "@web/../tests/web_test_helpers";
export class HrWorkLocation extends models.ServerModel {
_name = "hr.work.location";
name = fields.Char();
location_type = fields.Selection({
selection: [
["office", "Office"],
["home", "Home"],
["other", "Other"],
],
});
_views = {
search: `<search><field name="display_name" string="Name" /></search>`,
list: `<list><field name="display_name"/></list>`,
};
}

View File

@@ -0,0 +1,8 @@
import { fields, models } from "@web/../tests/web_test_helpers";
export class M2xAvatarEmployee extends models.Model {
_name = "m2x.avatar.employee";
employee_id = fields.Many2one({ string: "Employee", relation: "hr.employee.public" });
employee_ids = fields.Many2many({ string: "Employees", relation: "hr.employee.public" });
}

View File

@@ -0,0 +1,23 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { fields, makeKwArgs } from "@web/../tests/web_test_helpers";
import { mailDataHelpers } from "@mail/../tests/mock_server/mail_mock_server";
export class ResPartner extends mailModels.ResPartner {
employee_ids = fields.One2many({
relation: "hr.employee",
inverse: "work_contact_id",
});
_get_store_avatar_card_fields() {
return [
...super._get_store_avatar_card_fields(),
mailDataHelpers.Store.many(
"employee_ids",
makeKwArgs({
fields: this.env["hr.employee"]._get_store_avatar_card_fields(),
mode: "ADD",
})
),
];
}
}

View File

@@ -0,0 +1,22 @@
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { fields } from "@web/../tests/web_test_helpers";
export class ResUsers extends mailModels.ResUsers {
employee_id = fields.Many2one({ relation: "hr.employee" });
employee_ids = fields.One2many({
relation: "hr.employee",
inverse: "user_id",
});
department_id = fields.Many2one({
related: "employee_id.department_id",
relation: "hr.department",
});
work_email = fields.Char({ related: "employee_id.work_email" });
work_phone = fields.Char({ related: "employee_id.work_phone" });
work_location_type = fields.Char({ related: "employee_id.work_location_type" });
work_location_id = fields.Many2one({
related: "employee_id.work_location_id",
relation: "hr.work.location",
});
job_title = fields.Char({ related: "employee_id.job_title" });
}

View File

@@ -0,0 +1,12 @@
import { onRpc } from "@web/../tests/web_test_helpers";
onRpc("get_avatar_card_data", function getAvatarCardData({ args }) {
const resourceId = args[0][0];
const resources = this.env["hr.employee.public"].search_read([["id", "=", resourceId]]);
return resources.map((resource) => ({
name: resource.name,
work_email: resource.work_email,
phone: resource.phone,
user_id: resource.user_id,
}));
});

View File

@@ -0,0 +1,41 @@
import {
clickSave,
contains,
makeMockServer,
mockService,
mountView,
} from "@web/../tests/web_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { defineHrModels } from "@hr/../tests/hr_test_helpers";
describe.current.tags("desktop");
defineHrModels();
test("editing the 'lang' field and saving it triggers a 'reload_context'", async function () {
const { env } = await makeMockServer();
const userId = env["fake.user"].create({
name: "Aline",
lang: "fr",
});
mockService("action", {
doAction(action) {
expect.step(action);
},
});
await mountView({
type: "form",
resModel: "fake.user",
arch: `
<form js_class="hr_user_preferences_form">
<field name="name"/>
<field name="lang"/>
</form>`,
resId: userId,
});
await contains("[name='name'] input").edit("John");
await clickSave();
expect.verifySteps([]);
await contains("[name='lang'] input").edit("En");
await clickSave();
expect.verifySteps(["reload_context"]);
});

View File

@@ -0,0 +1,27 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("check_public_employee_link_redirect", {
// starts at /odoo/employee/<employee_id>
steps: () => {
/* ignoring inactive modals since the modal may appear multiple times
thus hiding the inactive ones and playwright doesn't like doing
actions on hidden elements */
const msgSelector = '.o_dialog:not(.o_inactive_modal) .modal-content .modal-body div[role="alert"] p';
const msg = `You are not allowed to access "Employee" (hr.employee) records.
We can redirect you to the public employee list.`;
return [
{
trigger: msgSelector,
content: "See if redirect warning popup appears for current user",
timeout: 3000,
run: () => {
const errorTxt = document.querySelector(msgSelector).innerText;
if (errorTxt !== msg) {
throw new Error("Could not find correct warning message when visiting private employee without required permissions")
}
}
},
]
},
});

View File

@@ -0,0 +1,37 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add("hr_employee_tour", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
content: "Open Employees app",
trigger: ".o_app[data-menu-xmlid='hr.menu_hr_root']",
run: "click",
},
{
content: "Open an Employee Profile",
trigger: ".o_kanban_record:contains('Johnny H.')",
run: "click",
},
{
content: "Open a chat with the employee",
trigger: ".o_employee_chat_btn",
run: "click",
},
{
trigger: ".o-mail-ChatWindow .o-mail-ChatWindow-header:contains('Johnny H.')",
},
{
content: "Open user account menu",
trigger: ".o_user_menu .dropdown-toggle",
run: "click",
},
{
content: "Open My Preferences",
trigger: "[data-menu=preferences]",
run: "click",
},
],
});

View File

@@ -0,0 +1,78 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add("hr_employee_multiple_bank_accounts_tour", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
content: "Open Employees app",
trigger: ".o_app[data-menu-xmlid='hr.menu_hr_root']",
run: "click",
},
{
content: "Open an Employee Profile",
trigger: ".o_kanban_record:contains('Johnny H.')",
run: "click",
},
{
content: "Open personal tab",
trigger: ".nav-link:contains('Personal')",
run: "click",
},
{
content: "add bank 1",
trigger: "input#bank_account_ids_2",
run: "edit 1",
},
{
content: "add bank 1",
trigger: ".dropdown-item:contains('Create and edit')",
run: "click",
},
{
content: "save bank 1",
trigger: ".o_form_button_save:contains('Save')",
run: "click",
},
{
content: "add bank 2",
trigger: "input#bank_account_ids_2",
run: "edit 2",
},
{
content: "add bank 2",
trigger: ".dropdown-item:contains('Create and edit')",
run: "click",
},
{
content: "save",
trigger: ".o_form_button_save:contains('Save')",
run: "click",
},
{
content: "add bank 3",
trigger: "input#bank_account_ids_1",
run: "edit 3",
},
{
content: "add bank 3",
trigger: ".dropdown-item:contains('Create and edit')",
run: "click",
},
{
content: "save bank 3",
trigger: ".o_form_button_save:contains('Save')",
run: "click",
},
{
content: "save employee form",
trigger: ".fa-cloud-upload",
run: "click",
},
{
content: "wait for save completion",
trigger: ".o_form_readonly, .o_form_saved",
},
],
});

View File

@@ -0,0 +1,58 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add("version_timeline_auto_save_tour", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
content: "Open Employees app",
trigger: ".o_app[data-menu-xmlid='hr.menu_hr_root']",
run: "click",
},
{
content: "Open an Employee Profile",
trigger: ".o_kanban_record:contains('Bob M.')",
run: "click",
},
{
content: "Open Payroll Page",
trigger: ".o_notebook_headers a[name='payroll_information']",
run: "click",
},
{
content: "Open contract end date",
trigger: ".o_field_widget[name='contract_date_end'] .o_input",
run: "click",
},
{
content: "Go to the next month",
trigger: ".o_next",
run: "click",
},
{
content: "Choose date X + 1",
trigger: ".o_date_item_cell:nth-child(11) > div",
run: "click",
},
{
content: "Open Create New Version",
trigger: ".o_field_widget[name='version_id'] > .o_arrow_button_wrapper > button",
run: "click",
},
{
content: "Go to the next month",
trigger: ".o_next",
run: "click",
},
{
content: "Choose date X + 2",
trigger: ".o_date_item_cell:nth-child(12) > div",
run: "click",
},
{
content: "Wait until the form is saved",
trigger: "body .o_form_saved",
},
],
});

View File

@@ -0,0 +1,131 @@
import { describe, expect, test } from "@odoo/hoot";
import { queryAllTexts } from "@odoo/hoot-dom";
import { contains, makeMockServer, mountView } from "@web/../tests/web_test_helpers";
import { contains as mailContains } from "@mail/../tests/mail_test_helpers";
import { defineHrModels } from "@hr/../tests/hr_test_helpers";
describe.current.tags("desktop");
defineHrModels();
test("avatar card preview with hr", async () => {
const { env } = await makeMockServer();
const departmentId = env["hr.department"].create({
name: "Management",
complete_name: "Management",
});
const partnerId = env["res.partner"].create({
name: "Mario",
email: "Mario@odoo.test",
phone: "+7878698799",
});
const jobId = env["hr.job"].create({
name: "sub manager",
});
const workLocationId = env["hr.work.location"].create({
name: "Odoo",
location_type: "office",
});
const versionId = env["hr.version"].create({
job_id: jobId,
work_location_id: workLocationId,
department_id: departmentId,
});
const employeeId = env["hr.employee"].create({
version_id: versionId,
work_email: "Mario@odoo.pro",
work_location_type: "office",
work_phone: "+585555555",
});
const userId = env["res.users"].create({
partner_id: partnerId,
im_status: "online",
employee_id: employeeId,
employee_ids: [employeeId],
});
env["hr.employee"].write(employeeId, {
user_id: userId,
work_contact_id: partnerId,
});
env["m2x.avatar.user"].create({ user_id: userId });
await mountView({
type: "kanban",
resModel: "m2x.avatar.user",
arch: `<kanban>
<templates>
<t t-name="card">
<field name="user_id" widget="many2one_avatar_user"/>
</t>
</templates>
</kanban>`,
});
await contains(".o_m2o_avatar > img").click();
await mailContains(".o_avatar_card");
await mailContains(".o_avatar_card span[data-tooltip='Work Location'] .fa-building-o");
expect(queryAllTexts(".o_card_user_infos > *:not(.o_avatar_card_buttons)")).toEqual([
"Mario",
"Management",
"Mario@odoo.pro",
"+585555555",
"Odoo",
]);
await contains(".o_action_manager:eq(0)").click();
await mailContains(".o_avatar_card", { count: 0 });
});
test("avatar card preview with hr (partner_id field)", async () => {
const { env } = await makeMockServer();
const departmentId = env["hr.department"].create({
name: "Management",
complete_name: "Management",
});
const partnerId = env["res.partner"].create({
name: "Mario",
email: "Mario@odoo.test",
phone: "+7878698799",
});
const jobId = env["hr.job"].create({
name: "sub manager",
});
const workLocationId = env["hr.work.location"].create({
name: "Odoo",
location_type: "office",
});
const versionId = env["hr.version"].create({
job_id: jobId,
work_location_id: workLocationId,
department_id: departmentId,
});
const employeeId = env["hr.employee"].create({
version_id: versionId,
work_email: "Mario@odoo.pro",
work_location_type: "office",
work_phone: "+585555555",
});
env["hr.employee"].write(employeeId, {
work_contact_id: partnerId,
});
env["m2x.avatar.user"].create({ partner_id: partnerId });
await mountView({
type: "kanban",
resModel: "m2x.avatar.user",
arch: `<kanban>
<templates>
<t t-name="card">
<field name="partner_id" widget="many2one_avatar_user"/>
</t>
</templates>
</kanban>`,
});
await contains(".o_m2o_avatar > img").click();
await mailContains(".o_avatar_card");
await mailContains(".o_avatar_card span[data-tooltip='Work Location'] .fa-building-o");
expect(queryAllTexts(".o_card_user_infos > *:not(.o_avatar_card_buttons)")).toEqual([
"Mario",
"Management",
"Mario@odoo.pro",
"+585555555",
"Odoo",
]);
await contains(".o_action_manager:eq(0)").click();
await mailContains(".o_avatar_card", { count: 0 });
});