Initial commit: Hr packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 62531cd146
2820 changed files with 1432848 additions and 0 deletions

View file

@ -0,0 +1,10 @@
/** @odoo-module */
import { registry } from '@web/core/registry';
import { ImageField } from '@web/views/fields/image/image_field';
export class BackgroundImageField extends ImageField {}
BackgroundImageField.template = 'hr.BackgroundImage';
registry.category("fields").add("background_image", BackgroundImageField);

View file

@ -0,0 +1,11 @@
div.o_field_widget.o_field_background_image {
display: inline-block;
> div {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
width: 100%;
height: 100%;
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr.BackgroundImage" owl="1">
<div
t-att-data-tooltip-template="hasTooltip and tooltipAttributes.template"
t-att-data-tooltip-info="hasTooltip and tooltipAttributes.info"
t-att-data-tooltip-delay="hasTooltip and props.zoomDelay"
t-attf-style="background-image: url('#{getUrl(props.previewImage or props.name)}');"/>
</t>
</templates>

View file

@ -0,0 +1,21 @@
/** @odoo-module */
import { registry } from '@web/core/registry';
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
import { useOpenChat } from "@mail/views/open_chat_hook";
const { Component } = owl;
export class HrEmployeeChat extends Component {
setup() {
super.setup();
this.openChat = useOpenChat(this.props.record.resModel);
}
}
HrEmployeeChat.props = {
...standardWidgetProps,
};
HrEmployeeChat.template = 'hr.OpenChat';
registry.category("view_widgets").add("hr_employee_chat", HrEmployeeChat);

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr.OpenChat" owl="1">
<a t-if="props.record.data.user_id"
title="Chat"
icon="fa-comments"
t-on-click.prevent="() => openChat(props.record.resId)"
href="#"
class="ml8 o_employee_chat_btn"
role="button">
<i class="fa fa-comments align-middle fs-6"/>
</a>
</t>
<!-- TODO KBA: remove when Studio converted to owl -->
<t t-name="hr.OpenChatLegacy">
<a
title="Chat"
icon="fa-comments"
href="#"
class="ml8 o_employee_chat_btn"
role="button">
<i class="fa fa-comments align-middle fs-6"/>
</a>
</t>
</templates>

View file

@ -0,0 +1,13 @@
odoo.define('hr.OpenChatLegacy', function (require) {
"use strict";
const widgetRegistry = require('web.widget_registry');
const Widget = require('web.Widget');
const HrEmployeeChatLegacy = Widget.extend({
template: 'hr.OpenChatLegacy',
});
// TODO KBA remove when Studio converted to Owl
widgetRegistry.add('hr_employee_chat', HrEmployeeChatLegacy);
});

View file

@ -0,0 +1,9 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { BinaryField } from "@web/views/fields/binary/binary_field";
export class WorkPermitUploadField extends BinaryField {}
WorkPermitUploadField.template = "hr.WorkPermitUploadField";
registry.category("fields").add("work_permit_upload", WorkPermitUploadField);

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr.WorkPermitUploadField" t-inherit="web.BinaryField" t-inherit-mode="primary" owl="1">
<xpath expr="//label[hasclass('o_select_file_button')]" position="attributes">
<attribute name="class" remove="btn-primary" add="btn-secondary" separator=" " />
</xpath>
</t>
</templates>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M83.13,35.69a7.9,7.9,0,0,0-4.94-7.33L34.33,72.22H47.06L83.13,36.15Z" style="fill:#875b7b"/><path d="M53.39,27.78,16.87,64.3h0a7.9,7.9,0,0,0,5.21,7.43l44-44Z" style="fill:#875b7b"/><path d="M24.78,27.78a7.91,7.91,0,0,0-7.91,7.91V51.57L40.66,27.78Z" style="fill:#875b7b"/><path d="M59.78,72.22H75.22a7.91,7.91,0,0,0,7.91-7.91V48.87Z" style="fill:#875b7b"/></svg>

After

Width:  |  Height:  |  Size: 431 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M83.13,35.69a7.9,7.9,0,0,0-4.94-7.33L34.33,72.22H47.06L83.13,36.15Z" style="fill:#bb86fc"/><path d="M53.39,27.78,16.87,64.3h0a7.9,7.9,0,0,0,5.21,7.43l44-44Z" style="fill:#bb86fc"/><path d="M24.78,27.78a7.91,7.91,0,0,0-7.91,7.91V51.57L40.66,27.78Z" style="fill:#bb86fc"/><path d="M59.78,72.22H75.22a7.91,7.91,0,0,0,7.91-7.91V48.87Z" style="fill:#bb86fc"/></svg>

After

Width:  |  Height:  |  Size: 432 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M21.93,55H78.07a5,5,0,0,0,0-10H21.93a5,5,0,0,0,0,10Z" style="fill:#875b7b"/></svg>

After

Width:  |  Height:  |  Size: 153 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M21.93,55H78.07a5,5,0,0,0,0-10H21.93a5,5,0,0,0,0,10Z" style="fill:#bb86fc"/></svg>

After

Width:  |  Height:  |  Size: 154 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="16.87" y="27.78" width="66.26" height="44.44" rx="7.91" style="fill:#875b7b"/></svg>

After

Width:  |  Height:  |  Size: 155 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="16.87" y="27.78" width="66.26" height="44.44" rx="7.91" style="fill:#bb86fc"/></svg>

After

Width:  |  Height:  |  Size: 156 B

View file

@ -0,0 +1,74 @@
/** @odoo-module alias=hr.Many2OneAvatarEmployee **/
import fieldRegistry from 'web.field_registry';
import {
Many2OneAvatarUser,
KanbanMany2OneAvatarUser,
KanbanMany2ManyAvatarUser,
ListMany2ManyAvatarUser,
Many2ManyAvatarUser,
} from '@mail/js/m2x_avatar_user';
// This module defines variants of the Many2OneAvatarUser and Many2ManyAvatarUser
// field widgets, to support fields pointing to 'hr.employee'. It also defines the
// kanban version of the Many2OneAvatarEmployee widget.
//
// Usage:
// <field name="employee_id" widget="many2one_avatar_employee"/>
const M2XAvatarEmployeeMixin = {
supportedModels: ['hr.employee', 'hr.employee.public'],
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
_getEmployeeID() {
return this.value.res_id;
},
//----------------------------------------------------------------------
// Handlers
//----------------------------------------------------------------------
/**
* @override
*/
_onAvatarClicked(ev) {
ev.stopPropagation(); // in list view, prevent from opening the record
const employeeId = this._getEmployeeID(ev);
this._openChat({ employeeId: employeeId });
}
};
export const Many2OneAvatarEmployee = Many2OneAvatarUser.extend(M2XAvatarEmployeeMixin);
export const KanbanMany2OneAvatarEmployee = KanbanMany2OneAvatarUser.extend(M2XAvatarEmployeeMixin);
fieldRegistry.add('many2one_avatar_employee', Many2OneAvatarEmployee);
fieldRegistry.add('kanban.many2one_avatar_employee', KanbanMany2OneAvatarEmployee);
const M2MAvatarEmployeeMixin = Object.assign(M2XAvatarEmployeeMixin, {
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
_getEmployeeID(ev) {
return parseInt(ev.target.getAttribute('data-id'), 10);
},
});
export const Many2ManyAvatarEmployee = Many2ManyAvatarUser.extend(M2MAvatarEmployeeMixin, {});
export const KanbanMany2ManyAvatarEmployee = KanbanMany2ManyAvatarUser.extend(M2MAvatarEmployeeMixin, {});
export const ListMany2ManyAvatarEmployee = ListMany2ManyAvatarUser.extend(M2MAvatarEmployeeMixin, {});
fieldRegistry.add('many2many_avatar_employee', Many2ManyAvatarEmployee);
fieldRegistry.add('kanban.many2many_avatar_employee', KanbanMany2ManyAvatarEmployee);
fieldRegistry.add('list.many2many_avatar_employee', ListMany2ManyAvatarEmployee);
export default {
Many2OneAvatarEmployee,
};

View file

@ -0,0 +1,59 @@
/** @odoo-module **/
import StandaloneFieldManagerMixin from 'web.StandaloneFieldManagerMixin';
import Widget from 'web.Widget';
import { Many2OneAvatarEmployee } from '@hr/js/m2x_avatar_employee';
const StandaloneM2OAvatarEmployee = Widget.extend(StandaloneFieldManagerMixin, {
className: 'o_standalone_avatar_employee',
/**
* @override
*/
init(parent, value) {
this._super(...arguments);
StandaloneFieldManagerMixin.init.call(this);
this.value = value;
},
/**
* @override
*/
willStart() {
return Promise.all([this._super(...arguments), this._makeAvatarWidget()]);
},
/**
* @override
*/
start() {
this.avatarWidget.$el.appendTo(this.$el);
return this._super(...arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Create a record, and initialize and start the avatar widget.
*
* @private
* @returns {Promise}
*/
async _makeAvatarWidget() {
const modelName = 'hr.employee.public';
const fieldName = 'employee_id';
const recordId = await this.model.makeRecord(modelName, [{
name: fieldName,
relation: modelName,
type: 'many2one',
value: this.value,
}]);
const state = this.model.get(recordId);
this.avatarWidget = new Many2OneAvatarEmployee(this, fieldName, state);
this._registerWidget(recordId, fieldName, this.avatarWidget);
return this.avatarWidget.appendTo(document.createDocumentFragment());
},
});
export default StandaloneM2OAvatarEmployee;

View file

@ -0,0 +1,10 @@
/** @odoo-module **/
import basicFields from 'web.basic_fields';
import fieldRegistry from 'web.field_registry';
const WorkPermitUpload = basicFields.FieldBinaryFile.extend({
template: "hr.WorkPermitUpload",
});
fieldRegistry.add('work_permit_upload', WorkPermitUpload);

View file

@ -0,0 +1,179 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear, insert } from '@mail/model/model_field_command';
registerModel({
name: 'Employee',
modelMethods: {
/**
* @param {Object} data
* @returns {Object}
*/
convertData(data) {
const data2 = {};
if ('id' in data) {
data2.id = data.id;
}
if ('user_id' in data) {
data2.hasCheckedUser = true;
if (!data.user_id) {
data2.user = clear();
} else {
const partnerNameGet = data['user_partner_id'];
const partnerData = {
display_name: partnerNameGet[1],
id: partnerNameGet[0],
};
const userNameGet = data['user_id'];
const userData = {
id: userNameGet[0],
partner: insert(partnerData),
display_name: userNameGet[1],
};
data2.user = insert(userData);
}
}
return data2;
},
/**
* Performs the `read` RPC on the `hr.employee.public`.
*
* @param {Object} param0
* @param {Object} param0.context
* @param {string[]} param0.fields
* @param {integer[]} param0.ids
*/
async performRpcRead({ context, fields, ids }) {
const employeesData = await this.messaging.rpc({
model: 'hr.employee.public',
method: 'read',
args: [ids, fields],
kwargs: {
context,
},
});
this.messaging.models['Employee'].insert(employeesData.map(employeeData =>
this.messaging.models['Employee'].convertData(employeeData)
));
},
/**
* Performs the `search_read` RPC on `hr.employee.public`.
*
* @param {Object} param0
* @param {Object} param0.context
* @param {Array[]} param0.domain
* @param {string[]} param0.fields
*/
async performRpcSearchRead({ context, domain, fields }) {
const employeesData = await this.messaging.rpc({
model: 'hr.employee.public',
method: 'search_read',
kwargs: {
context,
domain,
fields,
},
});
this.messaging.models['Employee'].insert(employeesData.map(employeeData =>
this.messaging.models['Employee'].convertData(employeeData)
));
},
},
recordMethods: {
/**
* Checks whether this employee has a related user and partner and links
* them if applicable.
*/
async checkIsUser() {
return this.messaging.models['Employee'].performRpcRead({
ids: [this.id],
fields: ['user_id', 'user_partner_id'],
context: { active_test: false },
});
},
/**
* Gets the chat between the user of this employee and the current user.
*
* If a chat is not appropriate, a notification is displayed instead.
*
* @returns {Channel|undefined}
*/
async getChat() {
if (!this.user && !this.hasCheckedUser) {
await this.checkIsUser();
}
if (!this.exists()) {
return;
}
// prevent chatting with non-users
if (!this.user) {
this.messaging.notify({
message: this.env._t("You can only chat with employees that have a dedicated user."),
type: 'info',
});
return;
}
return this.user.getChat();
},
/**
* Opens a chat between the user of this employee and the current user
* and returns it.
*
* If a chat is not appropriate, a notification is displayed instead.
*
* @param {Object} [options] forwarded to @see `Thread:open()`
*/
async openChat(options) {
const chat = await this.getChat();
if (!this.exists()) {
return;
}
if (!chat) {
return;
}
await chat.thread.open(options);
if (!this.exists()) {
return;
}
},
/**
* Opens the most appropriate view that is a profile for this employee.
*/
async openProfile(model = 'hr.employee.public') {
return this.messaging.openDocument({
id: this.id,
model: model,
});
},
},
fields: {
/**
* Whether an attempt was already made to fetch the user corresponding
* to this employee. This prevents doing the same RPC multiple times.
*/
hasCheckedUser: attr({
default: false,
}),
/**
* Unique identifier for this employee.
*/
id: attr({
identifying: true,
}),
/**
* Partner related to this employee.
*/
partner: one('Partner', {
inverse: 'employee',
related: 'user.partner',
}),
/**
* User related to this employee.
*/
user: one('User', {
inverse: 'employee',
}),
},
});

View file

@ -0,0 +1,33 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
// dummy import to ensure mail Messaging patches are loaded beforehand
import '@mail/models/messaging';
registerPatch({
name: 'Messaging',
recordMethods: {
/**
* @override
* @param {integer} [param0.employeeId]
*/
async getChat({ employeeId }) {
if (employeeId) {
const employee = this.messaging.models['Employee'].insert({ id: employeeId });
return employee.getChat();
}
return this._super(...arguments);
},
/**
* @override
*/
async openProfile({ id, model }) {
if (model === 'hr.employee' || model === 'hr.employee.public') {
const employee = this.messaging.models['Employee'].insert({ id });
return employee.openProfile(model);
}
return this._super(...arguments);
},
},
});

View file

@ -0,0 +1,62 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
registerPatch({
name: 'Partner',
recordMethods: {
/**
* Checks whether this partner has a related employee and links them if
* applicable.
*/
async checkIsEmployee() {
await this.messaging.models['Employee'].performRpcSearchRead({
context: { active_test: false },
domain: [['user_partner_id', '=', this.id]],
fields: ['user_id', 'user_partner_id'],
});
if (!this.exists()) {
return;
}
this.update({ hasCheckedEmployee: true });
},
/**
* When a partner is an employee, its employee profile contains more
* useful information to know who he is than its partner profile.
*
* @override
*/
async openProfile() {
// limitation of patch, `this._super` becomes unavailable after `await`
const _super = this._super.bind(this, ...arguments);
if (!this.employee && !this.hasCheckedEmployee) {
await this.checkIsEmployee();
}
if (!this.exists()) {
return;
}
if (this.employee) {
return this.employee.openProfile();
}
return _super();
},
},
fields: {
/**
* Employee related to this partner. It is computed through
* the inverse relation and should be considered read-only.
*/
employee: one('Employee', {
inverse: 'partner',
}),
/**
* Whether an attempt was already made to fetch the employee
* corresponding to this partner. This prevents doing the same RPC
* multiple times.
*/
hasCheckedEmployee: attr({
default: false,
}),
},
});

View file

@ -0,0 +1,16 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
registerPatch({
name: 'User',
fields: {
/**
* Employee related to this user.
*/
employee: one('Employee', {
inverse: 'user',
}),
},
});

View file

@ -0,0 +1,48 @@
.o_kanban_dashboard.o_hr_department_kanban {
--KanbanRecord-width: 450px;
--KanbanRecord-width-small: 350px;
}
.o_employee_form {
.o_employee_avatar {
position: absolute;
top: 60px;
right: 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
.o_employee_availability {
position: absolute;
top: -5px;
right: -5px;
padding-bottom: 1px;
border-radius: 50%;
background-color: $o-view-background-color;
height: 1rem;
width: 1rem;
* {
margin-bottom: -1px;
height: 1rem;
width: 1rem;
}
}
}
.oe_title {
max-width: 75%;
}
}
.o_hr_narrow_field {
width: 8rem!important;
max-width: 8rem!important;
* {
max-width: 100%;
}
}
@for $size from 10 through 15 {
.o_hr_narrow_field-#{$size} {
width: #{$size}rem!important;
max-width: #{$size}rem!important;
}
}

View file

@ -0,0 +1,3 @@
input#hr_presence_control_email_amount {
max-width: 5rem;
}

View file

@ -0,0 +1,16 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { preferencesItem } from "@web/webclient/user_menu/user_menu_items";
export function hrPreferencesItem(env) {
return Object.assign(
{},
preferencesItem(env),
{
description: env._t('My Profile'),
}
);
}
registry.category("user_menuitems").add('profile', hrPreferencesItem, { force: true })

View file

@ -0,0 +1,30 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
const { useComponent, useEnv } = owl;
export function useArchiveEmployee() {
const component = useComponent();
const env = useEnv();
const action = useService("action");
return (id) => {
action.doAction({
type: 'ir.actions.act_window',
name: env._t('Employee Termination'),
res_model: 'hr.departure.wizard',
views: [[false, 'form']],
view_mode: 'form',
target: 'new',
context: {
'active_id': id,
'toggle_active': true,
}
}, {
onClose: async () => {
await component.model.load();
component.model.notify();
},
});
}
}

View file

@ -0,0 +1,32 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Many2ManyTagsAvatarUserField, KanbanMany2ManyTagsAvatarUserField } from "@mail/views/fields/many2many_avatar_user_field/many2many_avatar_user_field";
export class Many2ManyTagsAvatarEmployeeField extends Many2ManyTagsAvatarUserField {
get relation() {
return "hr.employee.public";
}
}
Many2ManyTagsAvatarEmployeeField.extractProps = ({ field, attrs }) => {
return {
...Many2ManyTagsAvatarUserField.extractProps({ field, attrs }),
canQuickCreate: false,
relation: (attrs.options && attrs.options.relation) || field.relation,
}
};
Many2ManyTagsAvatarEmployeeField.additionalClasses = [...Many2ManyTagsAvatarUserField.additionalClasses, "o_field_many2many_avatar_user"];
registry.category("fields").add("many2many_avatar_employee", Many2ManyTagsAvatarEmployeeField);
export class KanbanMany2ManyTagsAvatarEmployeeField extends KanbanMany2ManyTagsAvatarUserField {
get relation() {
return "hr.employee.public";
}
}
KanbanMany2ManyTagsAvatarEmployeeField.additionalClasses = [...KanbanMany2ManyTagsAvatarUserField.additionalClasses, "o_field_many2many_avatar_user"];
registry.category("fields").add("kanban.many2many_avatar_employee", KanbanMany2ManyTagsAvatarEmployeeField);
registry.category("fields").add("list.many2many_avatar_employee", KanbanMany2ManyTagsAvatarEmployeeField);

View file

@ -0,0 +1,36 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Many2OneAvatarUserField, KanbanMany2OneAvatarUserField } from "@mail/views/fields/many2one_avatar_user_field/many2one_avatar_user_field";
export class Many2OneAvatarEmployeeField extends Many2OneAvatarUserField {
get relation() {
return "hr.employee.public";
}
}
Many2OneAvatarEmployeeField.extractProps = ({ field, attrs }) => {
return {
...Many2OneAvatarUserField.extractProps({ field, attrs }),
relation: (attrs.options && attrs.options.relation) || field.relation,
canQuickCreate: false,
}
};
Many2OneAvatarEmployeeField.additionalClasses = [...Many2OneAvatarUserField.additionalClasses, "o_field_many2one_avatar_user"];
registry.category("fields").add("many2one_avatar_employee", Many2OneAvatarEmployeeField);
export class KanbanMany2OneAvatarEmployeeField extends KanbanMany2OneAvatarUserField {
get relation() {
return "hr.employee.public";
}
}
KanbanMany2OneAvatarEmployeeField.extractProps = ({ attrs, field }) => {
return {
...KanbanMany2OneAvatarUserField.extractProps({ attrs, field }),
relation: (attrs.options && attrs.options.relation) || field.relation,
};
};
registry.category("fields").add("kanban.many2one_avatar_employee", KanbanMany2OneAvatarEmployeeField);

View file

@ -0,0 +1,44 @@
/** @odoo-module */
import { registry } from '@web/core/registry';
import { formView } from '@web/views/form/form_view';
import { FormController } from '@web/views/form/form_controller';
import { FormRenderer } from '@web/views/form/form_renderer';
import { useArchiveEmployee } from '@hr/views/archive_employee_hook';
import { useOpenChat } from "@mail/views/open_chat_hook";
export class EmployeeFormController extends FormController {
setup() {
super.setup();
this.archiveEmployee = useArchiveEmployee();
}
getActionMenuItems() {
const menuItems = super.getActionMenuItems();
if (!this.archiveEnabled || !this.model.root.isActive) {
return menuItems;
}
const archiveAction = menuItems.other.find((item) => item.key === "archive");
if (archiveAction) {
archiveAction.callback = this.archiveEmployee.bind(this, this.model.root.resId);
}
return menuItems;
}
}
// TODO KBA: to remove in master
export class EmployeeFormRenderer extends FormRenderer {
setup() {
super.setup();
this.openChat = useOpenChat(this.props.record.resModel);
}
}
registry.category('views').add('hr_employee_form', {
...formView,
Controller: EmployeeFormController,
Renderer: EmployeeFormRenderer,
});

View file

@ -0,0 +1,28 @@
/** @odoo-module */
import { registry } from '@web/core/registry';
import { kanbanView } from '@web/views/kanban/kanban_view';
import { KanbanModel } from '@web/views/kanban/kanban_model';
// TODO KBA: to remove in master
export class EmployeeKanbanRecord extends KanbanModel.Record {
async openChat(employeeId) {
const messaging = await this.model.env.services.messaging.get();
messaging.openChat({ employeeId });
}
}
export class EmployeeKanbanModel extends KanbanModel {
setup(params, { messaging }) {
super.setup(...arguments);
this.messagingService = messaging;
}
}
EmployeeKanbanModel.services = [...KanbanModel.services, "messaging"];
EmployeeKanbanModel.Record = EmployeeKanbanRecord;
registry.category('views').add('hr_employee_kanban', {
...kanbanView,
Model: EmployeeKanbanModel,
});

View file

@ -0,0 +1,36 @@
/** @odoo-module */
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();
}
getActionMenuItems() {
const menuItems = super.getActionMenuItems();
const selectedRecords = this.model.root.selection;
// Only override the Archive action when only 1 record is selected.
if (!this.archiveEnabled || selectedRecords.length > 1 || !selectedRecords[0].data.active) {
return menuItems;
}
const archiveAction = menuItems.other.find((item) => item.key === "archive");
if (archiveAction) {
archiveAction.callback = this.archiveEmployee.bind(this, selectedRecords[0].resId);
}
return menuItems;
}
}
registry.category('views').add('hr_employee_list', {
...listView,
Controller: EmployeeListController,
});

View file

@ -0,0 +1,14 @@
/** @odoo-module **/
import { helpers } from "@mail/views/open_chat_hook";
import { patch } from "@web/core/utils/patch";
patch(helpers, "hr_m2x_avatar_employee", {
SUPPORTED_M2X_AVATAR_MODELS: [...helpers.SUPPORTED_M2X_AVATAR_MODELS, "hr.employee", "hr.employee.public"],
buildOpenChatParams: function (resModel, id) {
if (["hr.employee", "hr.employee.public"].includes(resModel)) {
return { employeeId: id };
}
return this._super(...arguments);
},
});

View file

@ -0,0 +1,25 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { formView } from "@web/views/form/form_view";
import { Record, RelationalModel } from "@web/views/basic_relational_model";
export class EmployeeProfileRecord extends Record {
async save() {
const dirtyFields = this.dirtyFields.map((f) => f.name);
const isSaved = await super.save(...arguments);
if (isSaved && dirtyFields.includes("lang")) {
this.model.actionService.doAction("reload_context");
}
return isSaved;
}
}
class EmployeeProfileModel extends RelationalModel {}
EmployeeProfileModel.Record = EmployeeProfileRecord;
registry.category("views").add("hr_employee_profile_form", {
...formView,
Model: EmployeeProfileModel,
});

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr.WorkPermitUpload" t-inherit="web.FieldBinaryFile" t-inherit-mode="primary">
<xpath expr="//button[@title='Select']" position="attributes">
<attribute name="class" remove="btn-primary" add="btn-secondary" separator=" "/>
</xpath>
</t>
</templates>