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:
29
frontend/hr/static/src/views/archive_employee_hook.js
Normal file
29
frontend/hr/static/src/views/archive_employee_hook.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useComponent } from "@odoo/owl";
|
||||
|
||||
export function useArchiveEmployee() {
|
||||
const component = useComponent();
|
||||
const action = useService("action");
|
||||
return (ids) => {
|
||||
action.doAction(
|
||||
{
|
||||
type: "ir.actions.act_window",
|
||||
name: _t("Employee Termination"),
|
||||
res_model: "hr.departure.wizard",
|
||||
views: [[false, "form"]],
|
||||
view_mode: "form",
|
||||
target: "new",
|
||||
context: {
|
||||
active_ids: ids,
|
||||
employee_termination: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
onClose: async () => {
|
||||
await component.model.load();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { AvatarCardEmployeePopover } from "@hr/components/avatar_card_employee/avatar_card_employee_popover";
|
||||
import { onWillStart } from "@odoo/owl";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
/**
|
||||
* 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();
|
||||
onWillStart(async () => {
|
||||
this.isHrUser = await user.hasGroup("hr.group_hr_user");
|
||||
});
|
||||
this.avatarCard = usePopover(AvatarCardEmployeePopover, { closeOnClickAway: true });
|
||||
}
|
||||
|
||||
get relation() {
|
||||
if (this.props.relation) {
|
||||
return this.props.relation;
|
||||
}
|
||||
return this.isHrUser ? "hr.employee" : "hr.employee.public";
|
||||
}
|
||||
|
||||
getAvatarCardProps(record) {
|
||||
const originalProps = super.getAvatarCardProps(record);
|
||||
if (["hr.employee", "hr.employee.public"].includes(this.relation)) {
|
||||
return {
|
||||
...originalProps,
|
||||
recordModel: this.relation,
|
||||
};
|
||||
}
|
||||
return originalProps;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
Many2ManyTagsAvatarUserField,
|
||||
KanbanMany2ManyTagsAvatarUserField,
|
||||
ListMany2ManyTagsAvatarUserField,
|
||||
many2ManyTagsAvatarUserField,
|
||||
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 EmployeeFieldRelationMixin(
|
||||
Many2ManyTagsAvatarUserField
|
||||
) {
|
||||
displayAvatarCard(record) {
|
||||
return (
|
||||
(!this.env.isSmall && ["hr.employee", "hr.employee.public"].includes(this.relation)) ||
|
||||
super.displayAvatarCard(record)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const many2ManyTagsAvatarEmployeeField = {
|
||||
...many2ManyTagsAvatarUserField,
|
||||
component: Many2ManyTagsAvatarEmployeeField,
|
||||
additionalClasses: [
|
||||
...many2ManyTagsAvatarUserField.additionalClasses,
|
||||
"o_field_many2many_avatar_user",
|
||||
],
|
||||
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 EmployeeFieldRelationMixin(
|
||||
KanbanMany2ManyTagsAvatarUserField
|
||||
) {
|
||||
displayAvatarCard(record) {
|
||||
return (
|
||||
(!this.env.isSmall && ["hr.employee", "hr.employee.public"].includes(this.relation)) ||
|
||||
super.displayAvatarCard(record)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const kanbanMany2ManyTagsAvatarEmployeeField = {
|
||||
...kanbanMany2ManyTagsAvatarUserField,
|
||||
component: KanbanMany2ManyTagsAvatarEmployeeField,
|
||||
additionalClasses: [
|
||||
...kanbanMany2ManyTagsAvatarUserField.additionalClasses,
|
||||
"o_field_many2many_avatar_user",
|
||||
],
|
||||
extractProps: (fieldInfo, dynamicInfo) => ({
|
||||
...kanbanMany2ManyTagsAvatarUserField.extractProps(fieldInfo, dynamicInfo),
|
||||
relation: fieldInfo.options?.relation,
|
||||
}),
|
||||
};
|
||||
|
||||
registry
|
||||
.category("fields")
|
||||
.add("kanban.many2many_avatar_employee", kanbanMany2ManyTagsAvatarEmployeeField)
|
||||
.add("activity.many2many_avatar_employee", kanbanMany2ManyTagsAvatarEmployeeField);
|
||||
|
||||
export class ListMany2ManyTagsAvatarEmployeeField extends EmployeeFieldRelationMixin(
|
||||
ListMany2ManyTagsAvatarUserField
|
||||
) {
|
||||
displayAvatarCard(record) {
|
||||
return (
|
||||
(!this.env.isSmall && ["hr.employee", "hr.employee.public"].includes(this.relation)) ||
|
||||
super.displayAvatarCard(record)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const listMany2ManyTagsAvatarEmployeeField = {
|
||||
...listMany2ManyTagsAvatarUserField,
|
||||
component: ListMany2ManyTagsAvatarEmployeeField,
|
||||
additionalClasses: [
|
||||
...listMany2ManyTagsAvatarUserField.additionalClasses,
|
||||
"o_field_many2many_avatar_user",
|
||||
],
|
||||
};
|
||||
registry
|
||||
.category("fields")
|
||||
.add("list.many2many_avatar_employee", listMany2ManyTagsAvatarEmployeeField);
|
||||
@@ -0,0 +1,73 @@
|
||||
import { AvatarEmployee } from "@hr/components/avatar_employee/avatar_employee";
|
||||
import { Component, onWillStart } from "@odoo/owl";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { user } from "@web/core/user";
|
||||
import { computeM2OProps, KanbanMany2One } from "@web/views/fields/many2one/many2one";
|
||||
import {
|
||||
buildM2OFieldDescription,
|
||||
extractM2OFieldProps,
|
||||
m2oSupportedOptions,
|
||||
Many2OneField,
|
||||
} from "@web/views/fields/many2one/many2one_field";
|
||||
|
||||
export class KanbanMany2OneAvatarEmployeeField extends Component {
|
||||
static template = "hr.KanbanMany2OneAvatarEmployeeField";
|
||||
static components = { AvatarEmployee, KanbanMany2One };
|
||||
static props = {
|
||||
...Many2OneField.props,
|
||||
displayAvatarName: { type: Boolean, optional: true },
|
||||
relation: { type: String, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
onWillStart(async () => {
|
||||
this.isHrUser = await user.hasGroup("hr.group_hr_user");
|
||||
});
|
||||
}
|
||||
|
||||
get displayName() {
|
||||
return this.props.displayAvatarName && this.value ? this.value.display_name : "";
|
||||
}
|
||||
|
||||
get m2oProps() {
|
||||
return {
|
||||
...computeM2OProps(this.props),
|
||||
canQuickCreate: false,
|
||||
relation: this.relation,
|
||||
};
|
||||
}
|
||||
|
||||
get relation() {
|
||||
return this.props.relation ?? (this.isHrUser ? "hr.employee" : "hr.employee.public");
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.props.record.data[this.props.name];
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import("registries").FieldsRegistryItemShape} */
|
||||
const fieldDescr = {
|
||||
...buildM2OFieldDescription(KanbanMany2OneAvatarEmployeeField),
|
||||
additionalClasses: ["o_field_many2one_avatar_kanban", "o_field_many2one_avatar_user"],
|
||||
extractProps(staticInfo, dynamicInfo) {
|
||||
return {
|
||||
...extractM2OFieldProps(staticInfo, dynamicInfo),
|
||||
displayAvatarName: staticInfo.options.display_avatar_name || false,
|
||||
readonly: dynamicInfo.readonly,
|
||||
relation: staticInfo.options.relation,
|
||||
};
|
||||
},
|
||||
supportedOptions: [
|
||||
...m2oSupportedOptions,
|
||||
{
|
||||
label: _t("Display avatar name"),
|
||||
name: "display_avatar_name",
|
||||
type: "boolean",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
registry.category("fields").add("activity.many2one_avatar_employee", fieldDescr);
|
||||
registry.category("fields").add("kanban.many2one_avatar_employee", fieldDescr);
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr.KanbanMany2OneAvatarEmployeeField">
|
||||
<KanbanMany2One t-props="m2oProps">
|
||||
<t t-set-slot="avatar">
|
||||
<AvatarEmployee cssClass="'o_m2o_avatar'" resModel="relation" resId="value.id" noSpacing="false" displayName="displayName"/>
|
||||
</t>
|
||||
<t t-set-slot="autoCompleteItem" t-slot-scope="autoCompleteItemScope">
|
||||
<span class="o_avatar_many2x_autocomplete o_avatar d-flex align-items-center">
|
||||
<img class="rounded me-1" t-attf-src="/web/image/{{relation}}/{{autoCompleteItemScope.record.id}}/avatar_128?unique={{autoCompleteItemScope.record.data.write_date.ts}}"/>
|
||||
<span t-out="autoCompleteItemScope.label"/>
|
||||
</span>
|
||||
</t>
|
||||
</KanbanMany2One>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,55 @@
|
||||
import { AvatarEmployee } from "@hr/components/avatar_employee/avatar_employee";
|
||||
import { Component, onWillStart } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { user } from "@web/core/user";
|
||||
import { computeM2OProps, Many2One } from "@web/views/fields/many2one/many2one";
|
||||
import {
|
||||
buildM2OFieldDescription,
|
||||
extractM2OFieldProps,
|
||||
Many2OneField,
|
||||
} from "@web/views/fields/many2one/many2one_field";
|
||||
|
||||
export class Many2OneAvatarEmployeeField extends Component {
|
||||
static template = "hr.Many2OneAvatarEmployeeField";
|
||||
static components = { AvatarEmployee, Many2One };
|
||||
static props = {
|
||||
...Many2OneField.props,
|
||||
relation: { type: String, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
onWillStart(async () => {
|
||||
this.isHrUser = await user.hasGroup("hr.group_hr_user");
|
||||
});
|
||||
}
|
||||
|
||||
get m2oProps() {
|
||||
return {
|
||||
...computeM2OProps(this.props),
|
||||
canQuickCreate: false,
|
||||
relation: this.relation,
|
||||
};
|
||||
}
|
||||
|
||||
get relation() {
|
||||
return this.props.relation ?? (this.isHrUser ? "hr.employee" : "hr.employee.public");
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("many2one_avatar_employee", {
|
||||
...buildM2OFieldDescription(Many2OneAvatarEmployeeField),
|
||||
additionalClasses: [
|
||||
"o_field_many2one_avatar",
|
||||
"o_field_many2one_avatar_kanban",
|
||||
"o_field_many2one_avatar_user",
|
||||
],
|
||||
extractProps(staticInfo, dynamicInfo) {
|
||||
return {
|
||||
...extractM2OFieldProps(staticInfo, dynamicInfo),
|
||||
relation: staticInfo.options.relation,
|
||||
canOpen: "no_open" in staticInfo.options
|
||||
? !staticInfo.options.no_open
|
||||
: staticInfo.viewType === "form",
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr.Many2OneAvatarEmployeeField">
|
||||
<t t-set="value" t-value="props.record.data[props.name]"/>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<t t-if="value !== false">
|
||||
<AvatarEmployee cssClass="'o_m2o_avatar'" resModel="relation" resId="value.id" noSpacing="true"/>
|
||||
</t>
|
||||
<Many2One t-props="m2oProps" cssClass="'w-100'">
|
||||
<t t-set-slot="autoCompleteItem" t-slot-scope="autoCompleteItemScope">
|
||||
<div class="o_avatar_many2x_autocomplete d-flex align-items-center">
|
||||
<AvatarEmployee resModel="relation" resId="autoCompleteItemScope.record.id" canOpenPopover="false"/>
|
||||
<span t-out="autoCompleteItemScope.label"/>
|
||||
</div>
|
||||
</t>
|
||||
</Many2One>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
24
frontend/hr/static/src/views/form_view.js
Normal file
24
frontend/hr/static/src/views/form_view.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
|
||||
import { useArchiveEmployee } from "@hr/views/archive_employee_hook";
|
||||
|
||||
export class EmployeeFormController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.archiveEmployee = useArchiveEmployee();
|
||||
}
|
||||
|
||||
getStaticActionMenuItems() {
|
||||
const menuItems = super.getStaticActionMenuItems();
|
||||
menuItems.archive.callback = this.archiveEmployee.bind(this, [this.model.root.resId]);
|
||||
return menuItems;
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("hr_employee_form", {
|
||||
...formView,
|
||||
Controller: EmployeeFormController,
|
||||
});
|
||||
29
frontend/hr/static/src/views/kanban_view.js
Normal file
29
frontend/hr/static/src/views/kanban_view.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
|
||||
import { useArchiveEmployee } from "@hr/views/archive_employee_hook";
|
||||
|
||||
export class EmployeeKanbanController extends KanbanController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.archiveEmployee = useArchiveEmployee();
|
||||
}
|
||||
|
||||
getStaticActionMenuItems() {
|
||||
const menuItems = super.getStaticActionMenuItems();
|
||||
const selectedRecords = this.model.root.selection;
|
||||
|
||||
menuItems.archive.callback = this.archiveEmployee.bind(
|
||||
this,
|
||||
selectedRecords.map(({ resId }) => resId)
|
||||
);
|
||||
return menuItems;
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("hr_employee_kanban", {
|
||||
...kanbanView,
|
||||
Controller: EmployeeKanbanController,
|
||||
});
|
||||
33
frontend/hr/static/src/views/list_view.js
Normal file
33
frontend/hr/static/src/views/list_view.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { registry } from '@web/core/registry';
|
||||
|
||||
import { listView } from '@web/views/list/list_view';
|
||||
import { ListController } from '@web/views/list/list_controller';
|
||||
|
||||
import { useArchiveEmployee } from '@hr/views/archive_employee_hook';
|
||||
|
||||
export class EmployeeListController extends ListController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.archiveEmployee = useArchiveEmployee();
|
||||
}
|
||||
|
||||
getStaticActionMenuItems() {
|
||||
const menuItems = super.getStaticActionMenuItems();
|
||||
const selectedRecords = this.model.root.selection;
|
||||
|
||||
menuItems.archive.callback = this.archiveEmployee.bind(
|
||||
this,
|
||||
selectedRecords.map(({resId}) => resId),
|
||||
)
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
async createRecord() {
|
||||
await this.props.createRecord();
|
||||
}
|
||||
}
|
||||
|
||||
registry.category('views').add('hr_employee_list', {
|
||||
...listView,
|
||||
Controller: EmployeeListController,
|
||||
});
|
||||
16
frontend/hr/static/src/views/open_chat_hook.js
Normal file
16
frontend/hr/static/src/views/open_chat_hook.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { helpers } from "@mail/core/web/open_chat_hook";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(helpers, {
|
||||
SUPPORTED_M2X_AVATAR_MODELS: [
|
||||
...helpers.SUPPORTED_M2X_AVATAR_MODELS,
|
||||
"hr.employee",
|
||||
"hr.employee.public",
|
||||
],
|
||||
buildOpenChatParams(resModel, id) {
|
||||
if (["hr.employee", "hr.employee.public"].includes(resModel)) {
|
||||
return { employeeId: id };
|
||||
}
|
||||
return super.buildOpenChatParams(...arguments);
|
||||
}
|
||||
});
|
||||
28
frontend/hr/static/src/views/preferences_form_view.js
Normal file
28
frontend/hr/static/src/views/preferences_form_view.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
|
||||
export class HrUserPreferencesController extends formView.Controller {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.action = useService("action");
|
||||
this.mustReload = false;
|
||||
}
|
||||
|
||||
onWillSaveRecord(record, changes) {
|
||||
this.mustReload = "lang" in changes;
|
||||
}
|
||||
|
||||
async onRecordSaved(record) {
|
||||
await super.onRecordSaved(...arguments);
|
||||
if (this.mustReload) {
|
||||
this.mustReload = false;
|
||||
return this.action.doAction("reload_context");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("hr_user_preferences_form", {
|
||||
...formView,
|
||||
Controller: HrUserPreferencesController,
|
||||
});
|
||||
Reference in New Issue
Block a user