Initial commit: Hr packages
|
|
@ -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);
|
||||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
0
odoo-bringout-oca-ocb-hr/hr/static/src/default_image.png
Normal file
BIN
odoo-bringout-oca-ocb-hr/hr/static/src/img/default_image.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
179
odoo-bringout-oca-ocb-hr/hr/static/src/models/employee.js
Normal 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',
|
||||
}),
|
||||
},
|
||||
});
|
||||
33
odoo-bringout-oca-ocb-hr/hr/static/src/models/messaging.js
Normal 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);
|
||||
},
|
||||
},
|
||||
});
|
||||
62
odoo-bringout-oca-ocb-hr/hr/static/src/models/partner.js
Normal 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,
|
||||
}),
|
||||
},
|
||||
});
|
||||
16
odoo-bringout-oca-ocb-hr/hr/static/src/models/user.js
Normal 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',
|
||||
}),
|
||||
},
|
||||
});
|
||||
48
odoo-bringout-oca-ocb-hr/hr/static/src/scss/hr.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
input#hr_presence_control_email_amount {
|
||||
max-width: 5rem;
|
||||
}
|
||||
|
|
@ -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 })
|
||||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
44
odoo-bringout-oca-ocb-hr/hr/static/src/views/form_view.js
Normal 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,
|
||||
});
|
||||
28
odoo-bringout-oca-ocb-hr/hr/static/src/views/kanban_view.js
Normal 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,
|
||||
});
|
||||
36
odoo-bringout-oca-ocb-hr/hr/static/src/views/list_view.js
Normal 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,
|
||||
});
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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>
|
||||