19.0 vanilla
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 2 KiB |
|
|
@ -1,24 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
|
||||
<defs>
|
||||
<path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
|
||||
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#269396"/>
|
||||
<stop offset="100%" stop-color="#218689"/>
|
||||
</linearGradient>
|
||||
<path id="icon-d" d="M35.5,22.75 C39.6529297,22.75 43.0195312,26.1166016 43.0195312,30.2695312 C43.0195312,34.4224609 39.6529297,37.7890625 35.5,37.7890625 C31.3470703,37.7890625 27.9804687,34.4224609 27.9804687,30.2695312 C27.9804687,26.1166016 31.3470703,22.75 35.5,22.75 Z M43.6256055,38.3165755 L40.7623112,37.6007161 C37.2411654,40.1333659 32.9730794,39.5681836 30.2377604,37.6007161 L27.3744661,38.3165755 C25.0790039,38.8904232 23.46875,40.9527799 23.46875,43.3188542 L23.46875,47.671875 C23.46875,49.0957161 24.6230339,50.25 26.046875,50.25 L44.953125,50.25 C46.3769661,50.25 47.53125,49.0957161 47.53125,47.671875 L47.53125,43.3188542 C47.53125,40.9527799 45.9209961,38.8904232 43.6256055,38.3165755 Z M50.3958333,39.6510417 C53.1644531,39.6510417 55.4088542,37.4066406 55.4088542,34.6380208 C55.4088542,31.869401 53.1644531,29.625 50.3958333,29.625 C47.6272135,29.625 45.3828125,31.869401 45.3828125,34.6380208 C45.3828125,37.4066406 47.6272135,39.6510417 50.3958333,39.6510417 Z M20.6041667,39.6510417 C23.3727865,39.6510417 25.6171875,37.4066406 25.6171875,34.6380208 C25.6171875,31.869401 23.3727865,29.625 20.6041667,29.625 C17.8355469,29.625 15.5911458,31.869401 15.5911458,34.6380208 C15.5911458,37.4066406 17.8355469,39.6510417 20.6041667,39.6510417 Z M22.3229167,47.671875 L22.3229167,43.3188542 C22.3229167,42.1335612 22.6518424,41.0125781 23.2326367,40.0533008 C21.0850586,41.1074674 18.6968555,40.6769206 17.0959831,39.5255013 L15.1870964,40.0027409 C13.6568359,40.3852344 12.5833333,41.7602344 12.5833333,43.3375456 L12.5833333,46.2395833 C12.5833333,47.1888346 13.352832,47.9583333 14.3020833,47.9583333 L22.3350195,47.9583333 C22.3273388,47.8630362 22.3233016,47.7674804 22.3229167,47.671875 Z M55.8129036,40.0026693 L53.9040169,39.5254297 C51.9041797,40.9638802 49.5434049,40.902793 47.7604883,40.0423437 C48.3454362,41.004056 48.6770833,42.1289779 48.6770833,43.3188542 L48.6770833,47.671875 C48.6770833,47.7683398 48.6722135,47.8636589 48.6649805,47.9583333 L56.6979167,47.9583333 C57.647168,47.9583333 58.4166667,47.1888346 58.4166667,46.2395833 L58.4166667,43.3375456 C58.4166667,41.7602344 57.3431641,40.3852344 55.8129036,40.0026693 Z"/>
|
||||
<path id="icon-e" d="M35.5,20.75 C39.6529297,20.75 43.0195312,24.1166016 43.0195312,28.2695312 C43.0195312,32.4224609 39.6529297,35.7890625 35.5,35.7890625 C31.3470703,35.7890625 27.9804687,32.4224609 27.9804687,28.2695312 C27.9804687,24.1166016 31.3470703,20.75 35.5,20.75 Z M43.6256055,36.3165755 L40.7623112,35.6007161 C37.2411654,38.1333659 32.9730794,37.5681836 30.2377604,35.6007161 L27.3744661,36.3165755 C25.0790039,36.8904232 23.46875,38.9527799 23.46875,41.3188542 L23.46875,45.671875 C23.46875,47.0957161 24.6230339,48.25 26.046875,48.25 L44.953125,48.25 C46.3769661,48.25 47.53125,47.0957161 47.53125,45.671875 L47.53125,41.3188542 C47.53125,38.9527799 45.9209961,36.8904232 43.6256055,36.3165755 Z M50.3958333,37.6510417 C53.1644531,37.6510417 55.4088542,35.4066406 55.4088542,32.6380208 C55.4088542,29.869401 53.1644531,27.625 50.3958333,27.625 C47.6272135,27.625 45.3828125,29.869401 45.3828125,32.6380208 C45.3828125,35.4066406 47.6272135,37.6510417 50.3958333,37.6510417 Z M20.6041667,37.6510417 C23.3727865,37.6510417 25.6171875,35.4066406 25.6171875,32.6380208 C25.6171875,29.869401 23.3727865,27.625 20.6041667,27.625 C17.8355469,27.625 15.5911458,29.869401 15.5911458,32.6380208 C15.5911458,35.4066406 17.8355469,37.6510417 20.6041667,37.6510417 Z M22.3229167,45.671875 L22.3229167,41.3188542 C22.3229167,40.1335612 22.6518424,39.0125781 23.2326367,38.0533008 C21.0850586,39.1074674 18.6968555,38.6769206 17.0959831,37.5255013 L15.1870964,38.0027409 C13.6568359,38.3852344 12.5833333,39.7602344 12.5833333,41.3375456 L12.5833333,44.2395833 C12.5833333,45.1888346 13.352832,45.9583333 14.3020833,45.9583333 L22.3350195,45.9583333 C22.3273388,45.8630362 22.3233016,45.7674804 22.3229167,45.671875 Z M55.8129036,38.0026693 L53.9040169,37.5254297 C51.9041797,38.9638802 49.5434049,38.902793 47.7604883,38.0423437 C48.3454362,39.004056 48.6770833,40.1289779 48.6770833,41.3188542 L48.6770833,45.671875 C48.6770833,45.7683398 48.6722135,45.8636589 48.6649805,45.9583333 L56.6979167,45.9583333 C57.647168,45.9583333 58.4166667,45.1888346 58.4166667,44.2395833 L58.4166667,41.3375456 C58.4166667,39.7602344 57.3431641,38.3852344 55.8129036,38.0026693 Z"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<mask id="icon-b" fill="#fff">
|
||||
<use xlink:href="#icon-a"/>
|
||||
</mask>
|
||||
<g mask="url(#icon-b)">
|
||||
<rect width="70" height="70" fill="url(#icon-c)"/>
|
||||
<path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
|
||||
<path fill="#393939" d="M44,47 L4,47 C2,47 -7.10542736e-15,46.8509317 0,42.826087 L1.81527147e-16,22.6291049 L17.2090667,6.04664397 L19.583071,9.5209307 L30.2767729,0.11143939 L40.9146315,10.270152 L46.6446282,6.41116033 L55.3045682,10.7749724 L52.3812234,16.1277957 L58.2417324,21.9036543 L44,47 Z" opacity=".324" transform="translate(0 23)"/>
|
||||
<path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
|
||||
<use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#icon-d"/>
|
||||
<use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-e"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M34 17a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" fill="#985184"/><path d="M12 24a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z" fill="#FBB945"/><path d="M46 24a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z" fill="#1AD3BB"/><path d="M25 30H4a4 4 0 0 0-4 4v4a4 4 0 0 0 4 4h21V30Z" fill="#FBB945"/><path d="M46 30H25v12h21a4 4 0 0 0 4-4v-4a4 4 0 0 0-4-4Z" fill="#1AD3BB"/><path d="M12 30h14c6.627 0 12 5.373 12 12H24c-6.627 0-12-5.373-12-12Z" fill="#985184"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 511 B |
BIN
odoo-bringout-oca-ocb-hr/hr/static/description/icon_hi.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
odoo-bringout-oca-ocb-hr/hr/static/img/employee_awa-image.jpg
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
odoo-bringout-oca-ocb-hr/hr/static/img/partner_root-image.jpg
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
5
odoo-bringout-oca-ocb-hr/hr/static/src/@types/models.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
declare module "models" {
|
||||
export interface Store {
|
||||
employees: {[key: number]: {id: number, user_id: number, hasCheckedUser: boolean}};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { AvatarCardPopover } from "@mail/discuss/web/avatar_card/avatar_card_popover";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export const patchAvatarCardPopover = {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.userInfoTemplate = "hr.avatarCardUserInfos";
|
||||
},
|
||||
get email() {
|
||||
return this.employeeId?.work_email || super.email;
|
||||
},
|
||||
get phone() {
|
||||
return this.employeeId?.work_phone || super.phone;
|
||||
},
|
||||
get employeeId() {
|
||||
return this.partner?.employee_id;
|
||||
},
|
||||
async getProfileAction() {
|
||||
if (!this.employeeId) {
|
||||
return super.getProfileAction(...arguments);
|
||||
}
|
||||
return this.orm.call("hr.employee", "get_formview_action", [this.employeeId.id]);
|
||||
},
|
||||
};
|
||||
|
||||
export const unpatchAvatarCardPopover = patch(AvatarCardPopover.prototype, patchAvatarCardPopover);
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.AvatarCardPopover" t-inherit-mode="extension">
|
||||
<xpath expr="//a[hasclass('o-mail-avatar-card-tel')]" position="after">
|
||||
<span t-if="employeeId?.work_location_id" class="text-muted" data-tooltip="Work Location">
|
||||
<i t-if="employeeId.work_location_id.location_type === 'office'" class="me-1 fa fa-fw fa-building-o"/>
|
||||
<i t-elif="employeeId.work_location_id.location_type === 'home'" class="me-1 fa fa-fw fa-home"/>
|
||||
<i t-else="" class="me-1 fa fa-fw fa-map-marker"/>
|
||||
<t t-esc="employeeId.work_location_id.name"/>
|
||||
</span>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr.avatarCardUserInfos">
|
||||
<small class="text-muted text-truncate" t-if="employeeId?.job_title" t-att-title="employeeId.job_title" t-esc="employeeId.job_title"/>
|
||||
<span class="text-muted text-truncate" t-if="employeeId?.department_id" data-tooltip="Department" t-att-title="employeeId.department_id.name" t-esc="employeeId.department_id.name"/>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { AvatarCardResourcePopover } from "@resource_mail/components/avatar_card_resource/avatar_card_resource_popover";
|
||||
|
||||
export class AvatarCardEmployeePopover extends AvatarCardResourcePopover {
|
||||
static defaultProps = {
|
||||
...AvatarCardResourcePopover.defaultProps,
|
||||
recordModel: "hr.employee",
|
||||
};
|
||||
async onWillStart() {
|
||||
await super.onWillStart();
|
||||
this.record.employee_id = [this.props.id];
|
||||
}
|
||||
|
||||
get fieldNames() {
|
||||
const excludedFields = ["employee_id", "resource_type"];
|
||||
return super.fieldNames.filter((field) => !excludedFields.includes(field));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="resource_mail.AvatarCardResourcePopover" t-inherit-mode="extension">
|
||||
<xpath expr="//span[hasclass('o_avatar')]/img" position="attributes">
|
||||
<attribute name="t-if">this.record.employee_id?.length</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//span[hasclass('o_user_im_status')]" position="after">
|
||||
<span t-elif="record.show_hr_icon_display" name="icon" class="o_card_avatar_im_status position-absolute d-flex align-items-center justify-content-center o_employee_presence_status bg-inherit">
|
||||
<!-- Employee is present/connected and it is normal according to his work schedule -->
|
||||
<i t-if="record.hr_icon_display == 'presence_present'" class="fa fa-fw fa-circle text-success" title="Present" role="img" aria-label="Present"/>
|
||||
<!-- Employee is not present/connected and it is normal according to his work schedule -->
|
||||
<i t-if="record.hr_icon_display == 'presence_absent'" class="fa fa-fw fa-circle text-warning" title="Absent" role="img" aria-label="Absent"/>
|
||||
<!-- Employee is connected but according to his work schedule, he should not work for now -->
|
||||
<i t-if="record.hr_icon_display == 'presence_out_of_working_hour'" class="fa fa-fw fa-circle text-muted" title="Off-Hours" role="img" aria-label="Off-Hours"/>
|
||||
</span>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr.avatarCardResourceInfos">
|
||||
<small class="text-muted" t-if="record.job_id" t-esc="record.job_id[1]"/>
|
||||
<span class="text-muted" t-if="record.department_id" data-tooltip="Department" t-esc="record.department_id[1]"/>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { AvatarCardResourcePopover } from "@resource_mail/components/avatar_card_resource/avatar_card_resource_popover";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
|
||||
const patchAvatarCardResourcePopover = {
|
||||
setup() {
|
||||
super.setup();
|
||||
(this.userInfoTemplate = "hr.avatarCardResourceInfos"),
|
||||
(this.actionService = useService("action"));
|
||||
},
|
||||
get fieldNames() {
|
||||
return [
|
||||
...super.fieldNames,
|
||||
"department_id",
|
||||
this.props.recordModel ? "employee_id" : "employee_ids",
|
||||
"hr_icon_display",
|
||||
"job_title",
|
||||
"show_hr_icon_display",
|
||||
"work_email",
|
||||
"work_location_id",
|
||||
"work_phone",
|
||||
];
|
||||
},
|
||||
get email() {
|
||||
return this.record.work_email || this.record.email;
|
||||
},
|
||||
get phone() {
|
||||
return this.record.work_phone || this.record.phone;
|
||||
},
|
||||
get showViewProfileBtn() {
|
||||
return this.record.employee_id?.length > 0;
|
||||
},
|
||||
async getProfileAction() {
|
||||
return await this.orm.call("hr.employee", "get_formview_action", [
|
||||
this.record.employee_id[0],
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
patch(AvatarCardResourcePopover.prototype, patchAvatarCardResourcePopover);
|
||||
// Adding TagsList component allows display tag lists on the resource/employee avatar card
|
||||
// This is used by multiple modules depending on hr (planning for roles and hr_skills for skills)
|
||||
patch(AvatarCardResourcePopover, {
|
||||
components: {
|
||||
...AvatarCardResourcePopover.components,
|
||||
TagsList,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Avatar } from "@mail/views/web/fields/avatar/avatar";
|
||||
import { AvatarCardEmployeePopover } from "../avatar_card_employee/avatar_card_employee_popover";
|
||||
|
||||
export class AvatarEmployee extends Avatar {
|
||||
static components = { ...super.components, Popover: AvatarCardEmployeePopover };
|
||||
|
||||
get popoverProps() {
|
||||
return {
|
||||
...super.popoverProps,
|
||||
recordModel: this.props.resModel,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
|
||||
import { ImageField } from '@web/views/fields/image/image_field';
|
||||
import { ImageField, imageField } from '@web/views/fields/image/image_field';
|
||||
|
||||
export class BackgroundImageField extends ImageField {}
|
||||
BackgroundImageField.template = 'hr.BackgroundImage';
|
||||
export class BackgroundImageField extends ImageField {
|
||||
static template = "hr.BackgroundImage";
|
||||
}
|
||||
|
||||
registry.category("fields").add("background_image", BackgroundImageField);
|
||||
export const backgroundImageField = {
|
||||
...imageField,
|
||||
component: BackgroundImageField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("background_image", backgroundImageField);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,21 @@
|
|||
div.o_field_widget.o_field_background_image {
|
||||
display: inline-block;
|
||||
|
||||
> div {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@include media-breakpoint-down(md) {
|
||||
height: calc(100% + var(--KanbanRecord-padding-v)* 2);
|
||||
margin-top: calc(var(--KanbanRecord-padding-v)* -1);
|
||||
margin-bottom: calc(var(--KanbanRecord-padding-v)* -1);
|
||||
margin-left: calc(var(--KanbanRecord-padding-h)* -1);
|
||||
}
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="hr.BackgroundImage" owl="1">
|
||||
<div
|
||||
<t t-name="hr.BackgroundImage">
|
||||
<img
|
||||
loading="lazy"
|
||||
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-attf-src="#{getUrl(props.previewImage or props.name)}"
|
||||
alt="Binary file"
|
||||
/>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import { useDateTimePicker } from "@web/core/datetime/datetime_picker_hook";
|
||||
import { serializeDate } from "@web/core/l10n/dates";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class ButtonNewContractWidget extends Component {
|
||||
static template = "hr.ButtonNewContract";
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
};
|
||||
|
||||
/** @override **/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
|
||||
this.dateTimePicker = useDateTimePicker({
|
||||
target: `datetime-picker-target-new-contract`,
|
||||
onApply: (date) => {
|
||||
if (date) {
|
||||
this.tryAndCreateContract(serializeDate(date));
|
||||
}
|
||||
},
|
||||
get pickerProps() {
|
||||
return { type: "date" };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onClickNewContractBtn() {
|
||||
await this.props.record.save();
|
||||
await this.orm.call("hr.version", "check_contract_finished", [
|
||||
[this.props.record.data.version_id.id],
|
||||
]);
|
||||
this.dateTimePicker.open();
|
||||
}
|
||||
|
||||
async tryAndCreateContract(date) {
|
||||
await this.orm.call("hr.employee", "check_no_existing_contract", [
|
||||
[this.props.record.resId],
|
||||
date,
|
||||
]);
|
||||
const contract = await this.orm.call("hr.employee", "create_contract", [
|
||||
[this.props.record.resId],
|
||||
date,
|
||||
]);
|
||||
await this.loadVersion(contract);
|
||||
}
|
||||
|
||||
async loadVersion(version_id) {
|
||||
const { record } = this.props;
|
||||
await record.save();
|
||||
await this.props.record.model.load({
|
||||
context: {
|
||||
...this.props.record.model.env.searchModel.context,
|
||||
version_id: version_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const buttonNewContractWidget = {
|
||||
component: ButtonNewContractWidget,
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("button_new_contract", buttonNewContractWidget);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0"?>
|
||||
<template>
|
||||
<t t-name="hr.ButtonNewContract">
|
||||
<span class="w-100 d-flex justify-content-end">
|
||||
<button class="btn btn-link p-0 o_field_widget text-end w-auto" t-on-click="onClickNewContractBtn"
|
||||
t-ref="datetime-picker-target-new-contract" t-if="props.record.resId and props.record.data.contract_date_start">New Contract</button>
|
||||
</span>
|
||||
</t>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { onWillStart, useState, onWillUpdateProps, Component } from "@odoo/owl";
|
||||
|
||||
export class DepartmentChart extends Component {
|
||||
static template = "hr.DepartmentChart";
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.action = useService("action");
|
||||
this.orm = useService("orm");
|
||||
this.state = useState({
|
||||
hierarchy: {},
|
||||
});
|
||||
onWillStart(async () => await this.fetchHierarchy(this.props.record.resId));
|
||||
|
||||
onWillUpdateProps(async (nextProps) => {
|
||||
await this.fetchHierarchy(nextProps.record.resId);
|
||||
});
|
||||
}
|
||||
|
||||
async fetchHierarchy(departmentId) {
|
||||
this.state.hierarchy = await this.orm.call("hr.department", "get_department_hierarchy", [
|
||||
departmentId,
|
||||
]);
|
||||
}
|
||||
|
||||
async openDepartmentEmployees(departmentId) {
|
||||
const dialogAction = await this.orm.call(
|
||||
this.props.record.resModel,
|
||||
"action_employee_from_department",
|
||||
[departmentId],
|
||||
{}
|
||||
);
|
||||
this.action.doAction(dialogAction);
|
||||
}
|
||||
}
|
||||
|
||||
export const departmentChart = {
|
||||
component: DepartmentChart,
|
||||
};
|
||||
registry.category("view_widgets").add("hr_department_chart", departmentChart);
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
.o_widget_hr_department_chart {
|
||||
.o_hr_department_chart {
|
||||
.o_hr_department_chart_self {
|
||||
.department_name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
width: 50%;
|
||||
}
|
||||
@include media-breakpoint-down(md) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="hr.DepartmentChart">
|
||||
<div class="o_hr_department_chart w-100">
|
||||
<div class="o_horizontal_separator mb-3 text-uppercase fw-bolder small">Department Organization</div>
|
||||
<t t-if="state.hierarchy.self">
|
||||
<div class="o_hr_department_chart_parent">
|
||||
<t t-set="dept" t-value="state.hierarchy.parent"/>
|
||||
<t t-set="hideTree" t-value="true"/>
|
||||
<t t-call="hr.DepartmentChart.Department"/>
|
||||
</div>
|
||||
|
||||
<div t-att-class="state.hierarchy.parent?'ms-4':''">
|
||||
<div class="o_hr_department_chart_self">
|
||||
<t t-set="dept" t-value="state.hierarchy.self"/>
|
||||
<t t-set="hideTree" t-value="!state.hierarchy.parent"/>
|
||||
<t t-call="hr.DepartmentChart.Department"/>
|
||||
</div>
|
||||
|
||||
<t t-set="hideTree" t-value="false"/>
|
||||
<div class="o_hr_department_chart_children ms-4">
|
||||
<t t-foreach="state.hierarchy.children" t-as="dept" t-key="dept.name">
|
||||
<t t-call="hr.DepartmentChart.Department"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="hr.DepartmentChart.Department">
|
||||
<t t-if="dept">
|
||||
<div t-attf-class="#{hideTree?'':'o_treeEntry'}">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="department_name ms-1 mb-1">
|
||||
<t t-esc="dept.name"/>
|
||||
</span>
|
||||
<button class="btn btn-secondary btn-sm rounded-pill ms-2 my-1"
|
||||
t-on-click.prevent="() => this.openDepartmentEmployees(dept.id)">
|
||||
<span class="badge top-0 px-0">
|
||||
<t t-esc="dept.employees"/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,21 +1,22 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
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;
|
||||
import { useOpenChat } from "@mail/core/web/open_chat_hook";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class HrEmployeeChat extends Component {
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
};
|
||||
static template = "hr.OpenChat";
|
||||
|
||||
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);
|
||||
export const hrEmployeeChat = {
|
||||
component: HrEmployeeChat,
|
||||
};
|
||||
registry.category("view_widgets").add("hr_employee_chat", hrEmployeeChat);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?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"
|
||||
<t t-name="hr.OpenChat">
|
||||
<a t-if="props.record.data.user_id and props.record.resId"
|
||||
title="Chat"
|
||||
icon="fa-comments"
|
||||
t-on-click.prevent="() => openChat(props.record.resId)"
|
||||
|
|
@ -11,16 +11,4 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
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,14 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { floatField, FloatField } from "@web/views/fields/float/float_field";
|
||||
|
||||
const fieldRegistry = registry.category("fields");
|
||||
|
||||
class FloatWithoutTrailingZeros extends FloatField {
|
||||
get formattedValue() {
|
||||
return super.formattedValue.replace(/(\.\d*?[1-9])0+$/ , "$1").replace(/\.0+$/, "");
|
||||
}
|
||||
}
|
||||
|
||||
const floatWithoutTrailingZeros = { ...floatField, component: FloatWithoutTrailingZeros };
|
||||
|
||||
fieldRegistry.add("float_without_trailing_zeros", floatWithoutTrailingZeros);
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { Component } from "@odoo/owl";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
export class HrPresenceStatus extends Component {
|
||||
static template = "hr.HrPresenceStatus";
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
tag: { type: String, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
tag: "small",
|
||||
};
|
||||
|
||||
get classNames() {
|
||||
const classNames = ["fa"];
|
||||
classNames.push(
|
||||
this.icon,
|
||||
"fa-fw",
|
||||
"o_button_icon",
|
||||
"hr_presence",
|
||||
"align-middle",
|
||||
this.color,
|
||||
)
|
||||
return classNames.join(" ");
|
||||
}
|
||||
|
||||
get color() {
|
||||
switch (this.value) {
|
||||
case "presence_present":
|
||||
return "text-success";
|
||||
case "presence_absent":
|
||||
return "o_icon_employee_absent";
|
||||
case "presence_out_of_working_hour":
|
||||
case "presence_archive":
|
||||
return "text-muted";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return `fa-circle${this.value.startsWith("presence_archive") ? "-o" : ""}`;
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this.value !== false
|
||||
? this.options.find(([value, label]) => value === this.value)[1]
|
||||
: "";
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this.props.record.fields[this.props.name].selection.filter(
|
||||
(option) => option[0] !== false && option[1] !== ""
|
||||
);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.props.record.data[this.props.name];
|
||||
}
|
||||
}
|
||||
|
||||
export const hrPresenceStatus = {
|
||||
component: HrPresenceStatus,
|
||||
fieldDependencies: [],
|
||||
displayName: _t("HR Presence Status"),
|
||||
extractProps({ viewType }, dynamicInfo) {
|
||||
return {
|
||||
tag: viewType === "kanban" ? "span" : "small",
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("hr_presence_status", hrPresenceStatus)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr.HrPresenceStatus">
|
||||
<div class="o_employee_availability">
|
||||
<t t-tag="props.tag" role="img" t-att-class="classNames" t-att-aria-label="label" t-att-title="label"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { HrPresenceStatus, hrPresenceStatus } from "../hr_presence_status/hr_presence_status";
|
||||
|
||||
export class HrPresenceStatusPill extends HrPresenceStatus {
|
||||
static template = "hr.HrPresenceStatusPill";
|
||||
|
||||
/** @override */
|
||||
get classNames() {
|
||||
const classNames = ["fw-bold", "text-center", "btn", "rounded-pill", "cursor-default"];
|
||||
classNames.push(this.color);
|
||||
return classNames.join(" ");
|
||||
}
|
||||
|
||||
/** @override */
|
||||
get color() {
|
||||
switch (this.value) {
|
||||
case "presence_present":
|
||||
return "btn-outline-success";
|
||||
case "presence_absent":
|
||||
return "btn-outline-warning";
|
||||
case "presence_out_of_working_hour":
|
||||
case "presence_archive":
|
||||
return "btn-outline-secondary text-muted";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const hrPresenceStatusPill = {
|
||||
...hrPresenceStatus,
|
||||
component: HrPresenceStatusPill,
|
||||
};
|
||||
|
||||
registry.category("fields").add("form.hr_presence_status", hrPresenceStatusPill);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr.HrPresenceStatusPill">
|
||||
<div t-att-class="classNames" t-att-aria-label="label" t-att-title="label">
|
||||
<t t-tag="props.tag" role="img" class="me-1 fa" t-att-class="icon"/>
|
||||
<t t-esc="label"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { HrPresenceStatus, hrPresenceStatus } from "../hr_presence_status/hr_presence_status";
|
||||
|
||||
export class HrPresenceStatusPrivate extends HrPresenceStatus { }
|
||||
|
||||
export const hrPresenceStatusPrivate = {
|
||||
...hrPresenceStatus,
|
||||
component: HrPresenceStatusPrivate,
|
||||
};
|
||||
|
||||
registry.category("fields").add("hr_presence_status_private", hrPresenceStatusPrivate);
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
HrPresenceStatusPill,
|
||||
hrPresenceStatusPill,
|
||||
} from "../hr_presence_status_pill/hr_presence_status_pill";
|
||||
|
||||
export class HrPresenceStatusPrivatePill extends HrPresenceStatusPill {}
|
||||
|
||||
export const hrPresenceStatusPrivatePill = {
|
||||
...hrPresenceStatusPill,
|
||||
component: HrPresenceStatusPrivatePill,
|
||||
};
|
||||
|
||||
registry.category("fields").add("form.hr_presence_status_private", hrPresenceStatusPrivatePill);
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import {
|
||||
many2ManyTagsFieldColorEditable,
|
||||
Many2ManyTagsFieldColorEditable,
|
||||
} from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class FieldMany2ManyTagsSalaryBankTagsList extends TagsList {
|
||||
static template = "web.TagsList";
|
||||
}
|
||||
|
||||
export class FieldMany2ManyTagsSalaryBank extends Many2ManyTagsFieldColorEditable {
|
||||
static template = "web.Many2ManyTagsField";
|
||||
static components = {
|
||||
...Many2ManyTagsFieldColorEditable.components,
|
||||
TagsList: FieldMany2ManyTagsSalaryBankTagsList,
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.actionService = useService("action");
|
||||
const parentOpenMany2xRecord = this.openMany2xRecord;
|
||||
this.openMany2xRecord = async (...args) => {
|
||||
const result = await parentOpenMany2xRecord(...args);
|
||||
const isDirty = await this.props.record.model.root.isDirty();
|
||||
if (isDirty) {
|
||||
await this.props.record.model.root.save();
|
||||
}
|
||||
await this.props.record.load();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
getTagProps(record) {
|
||||
var text = record.data?.display_name;
|
||||
const amount = record.data?.employee_salary_amount;
|
||||
const has_multiple_bank_accounts = this.props.record.data["has_multiple_bank_accounts"];
|
||||
if (has_multiple_bank_accounts && amount) {
|
||||
const symbol = record.data?.currency_symbol;
|
||||
if (record.data?.employee_salary_amount_is_percentage) {
|
||||
text =
|
||||
(amount && amount <= 100 ? `(${amount.toFixed(0)}%) ` : "") +
|
||||
record.data?.display_name;
|
||||
} else if (amount) {
|
||||
text = `(${amount.toFixed(2)}${symbol ? symbol : ""}) ` + record.data?.display_name;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...super.getTagProps(record),
|
||||
text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const fieldMany2ManyTagsSalaryBank = {
|
||||
...many2ManyTagsFieldColorEditable,
|
||||
component: FieldMany2ManyTagsSalaryBank,
|
||||
relatedFields: () => [
|
||||
{ name: "employee_salary_amount" },
|
||||
{ name: "employee_salary_amount_is_percentage" },
|
||||
{ name: "display_name" },
|
||||
{ name: "currency_symbol" },
|
||||
],
|
||||
additionalClasses: [
|
||||
...(many2ManyTagsFieldColorEditable.additionalClasses || []),
|
||||
"o_field_many2many_tags",
|
||||
],
|
||||
extractProps({ options, attrs, string, placeholder }, dynamicInfo) {
|
||||
const props = many2ManyTagsFieldColorEditable.extractProps(
|
||||
{ options, attrs, string, placeholder },
|
||||
dynamicInfo
|
||||
);
|
||||
props.nameCreateField = "acc_number";
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2many_tags_salary_bank", fieldMany2ManyTagsSalaryBank);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { RadioField, radioField } from "@web/views/fields/radio/radio_field";
|
||||
|
||||
class RadioImageField extends RadioField {
|
||||
static template = "hr_homeworking.RadioImageField";
|
||||
}
|
||||
|
||||
registry.category("fields").add("hr_homeworking_radio_image", {
|
||||
...radioField,
|
||||
component: RadioImageField,
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="hr_homeworking.RadioImageField">
|
||||
<div role="radiogroup" class="d-flex flex-wrap" t-att-aria-label="string">
|
||||
<t t-foreach="items" t-as="item" t-key="item[0]">
|
||||
<t t-if="['office', 'home', 'other'].includes(item[0])">
|
||||
<div class="form-check o_radio_item me-1" aria-atomic="true">
|
||||
<input
|
||||
type="radio"
|
||||
class="form-check-input o_radio_input"
|
||||
t-att-checked="item[0] === value"
|
||||
t-att-disabled="props.readonly"
|
||||
t-att-name="id"
|
||||
t-att-data-value="item[0]"
|
||||
t-att-data-index="item_index"
|
||||
t-att-id="`${id}_${item[0]}`"
|
||||
t-on-change="() => this.onChange(item)"
|
||||
/>
|
||||
<t t-if="item[0] === 'office'">
|
||||
<i class="fa fa-building-o fa-2x" role="img"/>
|
||||
</t>
|
||||
<t t-elif="item[0] === 'home'">
|
||||
<i class="fa fa-home fa-2x" role="img"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-map-marker fa-2x" role="img"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import { onWillUpdateProps, useComponent, useState } from "@odoo/owl";
|
||||
import { useDateTimePicker } from "@web/core/datetime/datetime_picker_hook";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { getFieldDomain, useRecordObserver } from "@web/model/relational_model/utils";
|
||||
import { statusBarField, StatusBarField } from "@web/views/fields/statusbar/statusbar_field";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
export class VersionsTimeline extends StatusBarField {
|
||||
static template = "hr.VersionsTimeline";
|
||||
|
||||
/** @override **/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.actionService = useService("action");
|
||||
this.orm = useService("orm");
|
||||
|
||||
if (this.field.type === "many2one") {
|
||||
this.specialData = useSpecialDataNoCache((orm, props) => {
|
||||
const { foldField, name: fieldName, record } = props;
|
||||
const { relation } = record.fields[fieldName];
|
||||
const fieldNames = [
|
||||
"display_name",
|
||||
"contract_type_id",
|
||||
"contract_date_start",
|
||||
"contract_date_end",
|
||||
];
|
||||
if (foldField) {
|
||||
fieldNames.push(foldField);
|
||||
}
|
||||
const value = record.data[fieldName];
|
||||
let domain = getFieldDomain(record, fieldName, props.domain);
|
||||
domain = Domain.and([
|
||||
[["employee_id", "=", props.record.evalContext.id]],
|
||||
domain,
|
||||
]).toList();
|
||||
if (domain.length && value) {
|
||||
domain = Domain.or([[["id", "=", value.id]], domain]).toList(
|
||||
record.evalContext
|
||||
);
|
||||
}
|
||||
return orm.searchRead(
|
||||
relation,
|
||||
domain,
|
||||
fieldNames.filter((fName) => fName in record.fields)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
this.dateTimePicker = useDateTimePicker({
|
||||
target: `datetime-picker-target-version`,
|
||||
onApply: (date) => {
|
||||
if (date) {
|
||||
this.createVersion(date);
|
||||
}
|
||||
},
|
||||
get pickerProps() {
|
||||
return { type: "date" };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
displayContractLines() {
|
||||
return ["contract_type_id", "contract_date_start", "contract_date_end"].every(
|
||||
(fieldName) => fieldName in this.props.record.fields
|
||||
);
|
||||
}
|
||||
|
||||
async createVersion(date) {
|
||||
await this.props.record.save();
|
||||
const version_id = await this.orm.call("hr.employee", "create_version", [
|
||||
this.props.record.evalContext.id,
|
||||
{ date_version: date },
|
||||
]);
|
||||
|
||||
await this.props.record.model.load({
|
||||
context: {
|
||||
...this.props.record.model.env.searchModel.context,
|
||||
version_id: version_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onClickDateTimePickerBtn() {
|
||||
this.dateTimePicker.open();
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
async selectItem(item) {
|
||||
const { record } = this.props;
|
||||
await record.save();
|
||||
await this.props.record.model.load({
|
||||
context: {
|
||||
...this.props.record.model.env.searchModel.context,
|
||||
version_id: item.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
getAllItems() {
|
||||
function format(dateString) {
|
||||
return luxon.DateTime.fromISO(dateString).toFormat("MMM dd, yyyy");
|
||||
}
|
||||
const items = super.getAllItems();
|
||||
if (!this.displayContractLines) {
|
||||
return items;
|
||||
}
|
||||
const dataById = new Map(this.specialData.data.map((d) => [d.id, d]));
|
||||
|
||||
const selectedVersion = items.find((item) => item.isSelected)?.value;
|
||||
const selectedContractDate = dataById.get(selectedVersion)?.contract_date_start;
|
||||
|
||||
return items.map((item, index) => {
|
||||
const itemSpecialData = dataById.get(item.value) || {};
|
||||
const contractDateStart = itemSpecialData.contract_date_start;
|
||||
let contractDateEnd = itemSpecialData.contract_date_end;
|
||||
contractDateEnd = contractDateEnd ? format(contractDateEnd) : _t("Indefinite");
|
||||
const contractType = itemSpecialData.contract_type_id?.[1] ?? _t("Contract");
|
||||
const toolTip = contractDateStart
|
||||
? `${contractType}: ${format(contractDateStart)} - ${contractDateEnd}`
|
||||
: _t("No contract");
|
||||
|
||||
return {
|
||||
...item,
|
||||
isCurrentContract: contractDateStart === selectedContractDate,
|
||||
isInContract: Boolean(contractDateStart),
|
||||
toolTip,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function useSpecialDataNoCache(loadFn) {
|
||||
const component = useComponent();
|
||||
const orm = component.env.services.orm;
|
||||
|
||||
/** @type {{ data: Record<string, T> }} */
|
||||
const result = useState({ data: {} });
|
||||
useRecordObserver(async (record, props) => {
|
||||
result.data = await loadFn(orm, { ...props, record });
|
||||
});
|
||||
onWillUpdateProps(async (props) => {
|
||||
// useRecordObserver callback is not called when the record doesn't change
|
||||
if (props.record.id === component.props.record.id) {
|
||||
result.data = await loadFn(orm, props);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export const versionsTimeline = {
|
||||
...statusBarField,
|
||||
component: VersionsTimeline,
|
||||
additionalClasses: ["o_field_statusbar", "d-flex", "gap-1"],
|
||||
};
|
||||
|
||||
registry.category("fields").add("versions_timeline", versionsTimeline);
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
.o_field_statusbar {
|
||||
> .o_statusbar_status {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
.o_arrow_button_wrapper {
|
||||
.o_purple_line {
|
||||
background-color: var(--o-cc1-btn-primary);
|
||||
}
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: -1em;
|
||||
right: 1em;
|
||||
height: 2px;
|
||||
z-index: 10;
|
||||
|
||||
&.o_first {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.o_last {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_arrow_button:not(.d-none) {
|
||||
position: relative;
|
||||
padding: var(--o-statusbar-padding-y) calc(var(--o-statusbar-padding-x) * 1.375);
|
||||
border: 0;
|
||||
clip-path: polygon(
|
||||
var(--o-statusbar-point-top-left),
|
||||
var(--o-statusbar-point-top-right),
|
||||
var(--o-statusbar-point-middle-right),
|
||||
var(--o-statusbar-point-bottom-right),
|
||||
var(--o-statusbar-point-bottom-left),
|
||||
var(--o-statusbar-point-middle-left)
|
||||
);
|
||||
margin-left: calc(-1 * var(--o-statusbar-caret-width) - var(--o-statusbar-border-width) * sqrt(3));
|
||||
|
||||
&.o_last {
|
||||
--o-statusbar-point-middle-left: 0 50%;
|
||||
padding-left: var(--o-statusbar-padding-x);
|
||||
margin-left: 0;
|
||||
border-top-left-radius: var(--o-statusbar-radius);
|
||||
border-bottom-left-radius: var(--o-statusbar-radius);
|
||||
}
|
||||
|
||||
&.o_first {
|
||||
--o-statusbar-point-top-right: 100% 0;
|
||||
--o-statusbar-point-bottom-right: 100% 100%;
|
||||
padding-right: var(--o-statusbar-padding-x);
|
||||
border-top-right-radius: var(--o-statusbar-radius);
|
||||
border-bottom-right-radius: var(--o-statusbar-radius);
|
||||
}
|
||||
|
||||
&.dropdown-toggle::after {
|
||||
content: normal;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--o-statusbar-border);
|
||||
clip-path: polygon(
|
||||
var(--o-statusbar-point-top-left),
|
||||
var(--o-statusbar-point-top-right),
|
||||
var(--o-statusbar-point-middle-right),
|
||||
var(--o-statusbar-point-bottom-right),
|
||||
var(--o-statusbar-point-bottom-left),
|
||||
var(--o-statusbar-point-middle-left),
|
||||
var(--o-statusbar-point-top-left),
|
||||
var(--o-statusbar-point-inner-top-left),
|
||||
var(--o-statusbar-point-inner-middle-left),
|
||||
var(--o-statusbar-point-inner-bottom-left),
|
||||
var(--o-statusbar-point-inner-bottom-right),
|
||||
var(--o-statusbar-point-inner-middle-right),
|
||||
var(--o-statusbar-point-inner-top-right),
|
||||
var(--o-statusbar-point-inner-top-left)
|
||||
);
|
||||
}
|
||||
|
||||
&.o_last::before {
|
||||
--o-statusbar-point-inner-top-left: var(--o-statusbar-point-top-left);
|
||||
--o-statusbar-point-inner-middle-left: var(--o-statusbar-point-middle-left);
|
||||
--o-statusbar-point-inner-bottom-left: var(--o-statusbar-point-bottom-left);
|
||||
border-top-left-radius: var(--o-statusbar-radius);
|
||||
border-bottom-left-radius: var(--o-statusbar-radius);
|
||||
}
|
||||
|
||||
&.o_first::before {
|
||||
--o-statusbar-point-inner-top-right: var(--o-statusbar-point-top-right);
|
||||
--o-statusbar-point-inner-middle-right: var(--o-statusbar-point-middle-right);
|
||||
--o-statusbar-point-inner-bottom-right: var(--o-statusbar-point-bottom-right);
|
||||
border-top-right-radius: var(--o-statusbar-radius);
|
||||
border-bottom-right-radius: var(--o-statusbar-radius);
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: var(--o-statusbar-background-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 1;
|
||||
cursor: default;
|
||||
|
||||
&:not(.o_arrow_button_current) {
|
||||
&, &:hover, &:focus {
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.o_arrow_button_current:disabled, &:active:not(.o_last) {
|
||||
z-index: 1;
|
||||
background-color: var(--o-statusbar-background-active);
|
||||
|
||||
&::before {
|
||||
--o-statusbar-point-inner-top-left: calc(var(--o-statusbar-border-width) * sqrt(3)) var(--o-statusbar-border-width);
|
||||
--o-statusbar-point-inner-top-right: calc(100% - var(--o-statusbar-caret-width) - var(--o-statusbar-border-width) / sqrt(2)) var(--o-statusbar-border-width);
|
||||
--o-statusbar-point-inner-bottom-left: calc(var(--o-statusbar-border-width) * sqrt(3)) calc(100% - var(--o-statusbar-border-width));
|
||||
--o-statusbar-point-inner-bottom-right: calc(100% - var(--o-statusbar-caret-width) - var(--o-statusbar-border-width) / sqrt(2)) calc(100% - var(--o-statusbar-border-width));
|
||||
background-color: var(--o-statusbar-border-active);
|
||||
}
|
||||
|
||||
&.o_last::before {
|
||||
--o-statusbar-point-inner-top-left: calc(var(--o-statusbar-border-width) * sqrt(2)) var(--o-statusbar-border-width);
|
||||
--o-statusbar-point-inner-middle-left: calc(var(--o-statusbar-border-width) * sqrt(2)) 50%;
|
||||
--o-statusbar-point-inner-bottom-left: calc(var(--o-statusbar-border-width) * sqrt(2)) calc(100% - var(--o-statusbar-border-width));
|
||||
}
|
||||
|
||||
&.o_first::before {
|
||||
--o-statusbar-point-inner-top-right: calc(100% - var(--o-statusbar-border-width) * sqrt(2)) var(--o-statusbar-border-width);
|
||||
--o-statusbar-point-inner-middle-right: calc(100% - var(--o-statusbar-border-width) * sqrt(2)) 50%;
|
||||
--o-statusbar-point-inner-bottom-right: calc(100% - var(--o-statusbar-border-width) * sqrt(2)) calc(100% - var(--o-statusbar-border-width));
|
||||
}
|
||||
}
|
||||
|
||||
.o_rtl & {
|
||||
--o-statusbar-point-top-left: var(--o-statusbar-caret-width) 0;
|
||||
--o-statusbar-point-top-right: 100% 0;
|
||||
--o-statusbar-point-middle-left: 0 50%;
|
||||
--o-statusbar-point-middle-right: calc(100% - var(--o-statusbar-caret-width)) 50%;
|
||||
--o-statusbar-point-bottom-left: var(--o-statusbar-caret-width) 100%;
|
||||
--o-statusbar-point-bottom-right: 100% 100%;
|
||||
--o-statusbar-point-inner-top-left: calc(var(--o-statusbar-caret-width) + var(--o-statusbar-border-width) * sqrt(2)) 0;
|
||||
--o-statusbar-point-inner-top-right: calc(100% - var(--o-statusbar-border-width) * sqrt(2)) 0;
|
||||
--o-statusbar-point-inner-middle-left: calc(var(--o-statusbar-border-width) * sqrt(2)) 50%;
|
||||
--o-statusbar-point-inner-middle-right: calc(100% - var(--o-statusbar-caret-width) - var(--o-statusbar-border-width) * sqrt(2)) 50%;
|
||||
--o-statusbar-point-inner-bottom-left: calc(var(--o-statusbar-caret-width) + var(--o-statusbar-border-width) * sqrt(2)) 100%;
|
||||
--o-statusbar-point-inner-bottom-right: calc(100% - var(--o-statusbar-border-width) * sqrt(2)) 100%;
|
||||
|
||||
&.o_last {
|
||||
--o-statusbar-point-middle-right: 100% 50%;
|
||||
}
|
||||
|
||||
&.o_first {
|
||||
--o-statusbar-point-top-left: 0 0;
|
||||
--o-statusbar-point-bottom-left: 0 100%;
|
||||
}
|
||||
|
||||
&.o_last::before {
|
||||
--o-statusbar-point-inner-top-right: var(--o-statusbar-point-top-right);
|
||||
--o-statusbar-point-inner-middle-right: var(--o-statusbar-point-middle-right);
|
||||
--o-statusbar-point-inner-bottom-right: var(--o-statusbar-point-bottom-right);
|
||||
}
|
||||
|
||||
&.o_first::before {
|
||||
--o-statusbar-point-inner-top-left: var(--o-statusbar-point-top-left);
|
||||
--o-statusbar-point-inner-middle-left: var(--o-statusbar-point-middle-left);
|
||||
--o-statusbar-point-inner-bottom-left: var(--o-statusbar-point-bottom-left);
|
||||
}
|
||||
|
||||
&.o_arrow_button_current:disabled, &:active:not(.o_last) {
|
||||
&::before {
|
||||
--o-statusbar-point-inner-top-left: calc(var(--o-statusbar-caret-width) + var(--o-statusbar-border-width) / sqrt(2)) var(--o-statusbar-border-width);
|
||||
--o-statusbar-point-inner-top-right: calc(100% - var(--o-statusbar-border-width) * sqrt(3)) var(--o-statusbar-border-width);
|
||||
--o-statusbar-point-inner-bottom-left: calc(var(--o-statusbar-caret-width) + var(--o-statusbar-border-width) / sqrt(2)) calc(100% - var(--o-statusbar-border-width));
|
||||
--o-statusbar-point-inner-bottom-right: calc(100% - var(--o-statusbar-border-width) * sqrt(3)) calc(100% - var(--o-statusbar-border-width));
|
||||
}
|
||||
|
||||
&.o_last::before {
|
||||
--o-statusbar-point-inner-top-right: calc(100% - var(--o-statusbar-border-width) * sqrt(2)) var(--o-statusbar-border-width);
|
||||
--o-statusbar-point-inner-middle-right: calc(100% - var(--o-statusbar-border-width) * sqrt(2)) 50%;
|
||||
--o-statusbar-point-inner-bottom-right: calc(100% - var(--o-statusbar-border-width) * sqrt(2)) calc(100% - var(--o-statusbar-border-width));
|
||||
}
|
||||
|
||||
&.o_first::before {
|
||||
--o-statusbar-point-inner-top-left: calc(var(--o-statusbar-border-width) * sqrt(2)) var(--o-statusbar-border-width);
|
||||
--o-statusbar-point-inner-middle-left: calc(var(--o-statusbar-border-width) * sqrt(2)) 50%;
|
||||
--o-statusbar-point-inner-bottom-left: calc(var(--o-statusbar-border-width) * sqrt(2)) calc(100% - var(--o-statusbar-border-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?xml version="1.0"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr.VersionsTimeline" t-inherit="web.StatusBarField" t-inherit-mode="primary">
|
||||
<div t-ref="root" position="after">
|
||||
<div class="o_arrow_button_wrapper">
|
||||
<button
|
||||
t-ref="datetime-picker-target-version"
|
||||
title="New Employee Record"
|
||||
t-on-click="onClickDateTimePickerBtn"
|
||||
class="btn btn-primary">
|
||||
<i class="fa fa-plus"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button t-ref="after" position="replace">
|
||||
<div class="o_arrow_button_wrapper">
|
||||
<button
|
||||
t-ref="after"
|
||||
type="button"
|
||||
class="btn btn-secondary dropdown-toggle o_arrow_button o_first"
|
||||
t-att-disabled="props.isDisabled"
|
||||
aria-label="More..."
|
||||
>
|
||||
...
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button t-ref="before" position="replace">
|
||||
<div class="o_arrow_button_wrapper">
|
||||
<button
|
||||
t-ref="before"
|
||||
type="button"
|
||||
class="btn btn-secondary dropdown-toggle o_arrow_button o_last"
|
||||
t-att-disabled="props.isDisabled"
|
||||
aria-label="More..."
|
||||
>
|
||||
...
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<t t-foreach="items.inline" position="replace">
|
||||
<t t-foreach="items.inline" t-as="item" t-key="item.value">
|
||||
<div class="o_arrow_button_wrapper"
|
||||
t-att-data-tooltip="item.toolTip">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary o_arrow_button"
|
||||
t-att-class="{
|
||||
o_first: item_first,
|
||||
o_arrow_button_current: item.isSelected,
|
||||
o_last: item_last,
|
||||
}"
|
||||
t-att-disabled="props.isDisabled || item.isSelected"
|
||||
role="radio"
|
||||
t-att-aria-checked="item.isSelected.toString()"
|
||||
t-att-aria-current="item.isSelected and 'step'"
|
||||
t-att-data-value="item.value"
|
||||
t-esc="item.label"
|
||||
t-on-click="() => this.selectItem(item)"/>
|
||||
<div
|
||||
t-if="displayContractLines"
|
||||
t-att-class="{
|
||||
o_first: item_first,
|
||||
o_last: item_last,
|
||||
o_purple_line: item.isInContract and item.isCurrentContract,
|
||||
}"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { BinaryField } from "@web/views/fields/binary/binary_field";
|
||||
import { BinaryField, binaryField } from "@web/views/fields/binary/binary_field";
|
||||
|
||||
export class WorkPermitUploadField extends BinaryField {}
|
||||
WorkPermitUploadField.template = "hr.WorkPermitUploadField";
|
||||
export class WorkPermitUploadField extends BinaryField {
|
||||
static template = "hr.WorkPermitUploadField";
|
||||
}
|
||||
|
||||
registry.category("fields").add("work_permit_upload", WorkPermitUploadField);
|
||||
export const workPermitUploadField = {
|
||||
...binaryField,
|
||||
component: WorkPermitUploadField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("work_permit_upload", workPermitUploadField);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?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">
|
||||
<t t-name="hr.WorkPermitUploadField" t-inherit="web.BinaryField" t-inherit-mode="primary">
|
||||
<xpath expr="//label[hasclass('o_select_file_button')]" position="attributes">
|
||||
<attribute name="class" remove="btn-primary" add="btn-secondary" separator=" " />
|
||||
</xpath>
|
||||
|
|
|
|||
30
odoo-bringout-oca-ocb-hr/hr/static/src/core/common/@types/models.d.ts
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
declare module "models" {
|
||||
import { HrDepartment as HrDepartmentClass } from "@hr/core/common/hr_department_model";
|
||||
import { HrEmployee as HrEmployeeClass } from "@hr/core/common/hr_employee_model";
|
||||
import { HrWorkLocation as HrWorkLocationClass } from "@hr/core/common/hr_work_location_model";
|
||||
|
||||
export interface HrDepartment extends HrDepartmentClass {}
|
||||
export interface HrEmployee extends HrEmployeeClass {}
|
||||
export interface HrWorkLocation extends HrWorkLocationClass {}
|
||||
|
||||
export interface ResPartner {
|
||||
employee_id: HrEmployee;
|
||||
employee_ids: HrEmployee[];
|
||||
employeeId: number|undefined;
|
||||
}
|
||||
export interface ResUsers {
|
||||
employee_id: HrEmployee;
|
||||
employee_ids: HrEmployee[];
|
||||
}
|
||||
export interface Store {
|
||||
"hr.department": StaticMailRecord<HrDepartment, typeof HrDepartmentClass>;
|
||||
"hr.employee": StaticMailRecord<HrEmployee, typeof HrEmployeeClass>;
|
||||
"hr.work.location": StaticMailRecord<HrWorkLocation, typeof HrWorkLocationClass>;
|
||||
}
|
||||
|
||||
export interface Models {
|
||||
"hr.department": HrDepartment;
|
||||
"hr.employee": HrEmployee;
|
||||
"hr.work.location": HrWorkLocation;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Record } from "@mail/core/common/record";
|
||||
|
||||
export class HrDepartment extends Record {
|
||||
static _name = "hr.department";
|
||||
static id = "id";
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {string} */
|
||||
name;
|
||||
}
|
||||
|
||||
HrDepartment.register();
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Record, fields } from "@mail/core/common/record";
|
||||
|
||||
export class HrEmployee extends Record {
|
||||
static _name = "hr.employee";
|
||||
static id = "id";
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {number} */
|
||||
company_id = fields.One("res.company");
|
||||
department_id = fields.One("hr.department");
|
||||
/** @type {string} */
|
||||
job_title;
|
||||
work_contact_id = fields.One("res.partner");
|
||||
user_id = fields.One("res.users");
|
||||
/** @type {string} */
|
||||
work_email;
|
||||
work_location_id = fields.One("hr.work.location");
|
||||
/** @type {string} */
|
||||
work_phone;
|
||||
}
|
||||
|
||||
HrEmployee.register();
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Record } from "@mail/core/common/record";
|
||||
|
||||
export class HrWorkLocation extends Record {
|
||||
static _name = "hr.work.location";
|
||||
static id = "id";
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {string} */
|
||||
location_type;
|
||||
/** @type {string} */
|
||||
name;
|
||||
}
|
||||
|
||||
HrWorkLocation.register();
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { ResPartner } from "@mail/core/common/res_partner_model";
|
||||
import { fields } from "@mail/model/misc";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
patch(ResPartner.prototype, {
|
||||
/** @type {number|undefined} */
|
||||
employeeId: undefined,
|
||||
setup() {
|
||||
super.setup();
|
||||
this.employee_ids = fields.Many("hr.employee", {
|
||||
inverse: "work_contact_id",
|
||||
});
|
||||
this.employee_id = fields.One("hr.employee", {
|
||||
compute() {
|
||||
return (
|
||||
this.employee_ids.find(
|
||||
(employee) => employee.company_id?.id === user.activeCompany.id
|
||||
) || this.employee_ids[0]
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { fields } from "@mail/model/misc";
|
||||
import { ResUsers } from "@mail/core/common/res_users_model";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
patch(ResUsers.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.employee_ids = fields.Many("hr.employee", {
|
||||
inverse: "user_id",
|
||||
});
|
||||
this.employee_id = fields.One("hr.employee", {
|
||||
compute() {
|
||||
return (
|
||||
this.employee_ids.find(
|
||||
(employee) => employee.company_id?.id === user.activeCompany.id
|
||||
) || this.employee_ids[0]
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { registerThreadAction } from "@mail/core/common/thread_actions";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
registerThreadAction("open-hr-profile", {
|
||||
condition: ({ owner, thread }) =>
|
||||
thread?.channel_type === "chat" &&
|
||||
owner.props.chatWindow?.isOpen &&
|
||||
thread.correspondent?.partner_id?.employeeId &&
|
||||
!owner.isDiscussSidebarChannelActions,
|
||||
icon: "fa fa-fw fa-id-card",
|
||||
name: _t("Open Profile"),
|
||||
open: async ({ store, thread }) =>
|
||||
store.env.services.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_id: thread.correspondent.partner_id?.employeeId,
|
||||
res_model: "hr.employee.public",
|
||||
views: [[false, "form"]],
|
||||
}),
|
||||
async setup({ thread }) {
|
||||
let employeeId;
|
||||
if (thread?.correspondent?.partner_id && !thread.correspondent.partner_id.employeeId) {
|
||||
const employees = await this.store.env.services.orm.silent.searchRead(
|
||||
"hr.employee",
|
||||
[["user_partner_id", "=", thread.correspondent.partner_id.id]],
|
||||
["id"]
|
||||
);
|
||||
employeeId = employees[0]?.id;
|
||||
if (employeeId) {
|
||||
thread.correspondent.partner_id.employeeId = employeeId;
|
||||
}
|
||||
}
|
||||
},
|
||||
sequence: 16,
|
||||
});
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { RadioField, radioField } from "@web/views/fields/radio/radio_field";
|
||||
import { onMounted } from "@odoo/owl";
|
||||
|
||||
export class BooleanRadio extends RadioField {
|
||||
static props = {
|
||||
...RadioField.props,
|
||||
yes_label_element_id: { type: String },
|
||||
no_label_element_id: { type: String },
|
||||
};
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
onMounted(this.moveElement);
|
||||
}
|
||||
|
||||
moveElement() {
|
||||
document.querySelectorAll("[data-value='true']")[0]
|
||||
.labels[0].textContent = document.getElementById(this.props.yes_label_element_id).innerText;
|
||||
document.querySelectorAll("[data-value='false']")[0]
|
||||
.labels[0].textContent = document.getElementById(this.props.no_label_element_id).innerText;
|
||||
}
|
||||
|
||||
get items() {
|
||||
if (this.type === "boolean") return [["true", ""], ["false", ""]];
|
||||
return super.items;
|
||||
}
|
||||
|
||||
get value() {
|
||||
if (this.type === "boolean") return this.props.record.data[this.props.name].toString();
|
||||
return super.items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
*/
|
||||
onChange(value) {
|
||||
if (this.type === "boolean") this.props.record.update({ [this.props.name]: value[0] === "true" });
|
||||
super.onChange();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const booleanRadio = {
|
||||
...radioField,
|
||||
component: BooleanRadio,
|
||||
displayName: _t("Boolean display as radio field with translatable labels"),
|
||||
supportedOptions: [
|
||||
{
|
||||
label: _t("True association"),
|
||||
name: "yes_label_element_id",
|
||||
type: "string",
|
||||
help: _t("Link an element with the boolean True value."),
|
||||
},
|
||||
{
|
||||
label: _t("False association"),
|
||||
name: "no_label_element_id",
|
||||
type: "string",
|
||||
help: _t("Link an element with the boolean False value."),
|
||||
},
|
||||
],
|
||||
supportedTypes: ["boolean"],
|
||||
extractProps({ options }, dynamicInfo) {
|
||||
return {
|
||||
readonly: dynamicInfo.readonly,
|
||||
yes_label_element_id: options.yes_label_element_id,
|
||||
no_label_element_id: options.no_label_element_id,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("boolean_radio", booleanRadio);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_holidays.BooleanRadio" t-inherit="web.BooleanField" t-inherit-mode="primary">
|
||||
<xpath expr="//CheckBox" position="replace">
|
||||
<select>
|
||||
<option>YES</option>
|
||||
<option>NO</option>
|
||||
</select>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { RadioField, radioField } from "@web/views/fields/radio/radio_field";
|
||||
import {onMounted,onWillUnmount} from "@odoo/owl";
|
||||
|
||||
export class RadioFollowedByElement extends RadioField {
|
||||
static props = {
|
||||
...RadioField.props,
|
||||
links: { type: Object },
|
||||
observe: { type: String },
|
||||
};
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
|
||||
onMounted(() => {
|
||||
this.moveElement();
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
if ([...mutations].map(mutation =>
|
||||
[...mutation.addedNodes].map(node => node.id))
|
||||
.flat()
|
||||
.filter(id => Object.values(this.props.links).includes(id))) this.moveElement();
|
||||
});
|
||||
|
||||
this.observer.observe(document.getElementsByName(this.props.observe).item(0), {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: false,
|
||||
characterData: false,
|
||||
});
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
this.observer.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
moveElement() {
|
||||
for (const [key, value] of Object.entries(this.props.links)) {
|
||||
const option = document.querySelectorAll("[data-value="+key+"]")[0];
|
||||
const elementToAppend = document.getElementById(value);
|
||||
if (option === null || elementToAppend === null || elementToAppend.parentElement === option.parentElement)
|
||||
continue;
|
||||
option.parentElement.appendChild(elementToAppend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const radioFollowedByElement = {
|
||||
...radioField,
|
||||
component: RadioFollowedByElement,
|
||||
displayName: _t("Radio followed by element"),
|
||||
supportedOptions: [
|
||||
{
|
||||
label: _t("Element association"),
|
||||
name: "links",
|
||||
type: "Object",
|
||||
help: _t("An object to link select options and element id to move"),
|
||||
},
|
||||
{
|
||||
label: _t("Element to observe"),
|
||||
name: "observe",
|
||||
type: "String",
|
||||
help: _t("An element name parent of the radio to observe updates"),
|
||||
}
|
||||
],
|
||||
extractProps({ options }, dynamicInfo) {
|
||||
return {
|
||||
readonly: dynamicInfo.readonly,
|
||||
links: options.links,
|
||||
observe: options.observe,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("radio_followed_by_element", radioFollowedByElement);
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/** @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,
|
||||
};
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
/** @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;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/** @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);
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
/** @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',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/** @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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
/** @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,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/** @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',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
@ -1,19 +1,35 @@
|
|||
.o_kanban_dashboard.o_hr_department_kanban {
|
||||
.o_hr_department_kanban .o_kanban_renderer {
|
||||
--KanbanRecord-width: 450px;
|
||||
--KanbanRecord-width-small: 350px;
|
||||
}
|
||||
|
||||
.o_employee_form {
|
||||
.o_icon_employee_absent {
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.o_hr_employee_form_view .o_form_renderer {
|
||||
.o_form_sheet_bg {
|
||||
max-width: unset;
|
||||
}
|
||||
.o_employee_chat_btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-left: 0!important;
|
||||
}
|
||||
}
|
||||
.o_employee_avatar {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 10px;
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.o_employee_availability {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
z-index: 1;
|
||||
top: 0px;
|
||||
right: -5px;
|
||||
padding-bottom: 1px;
|
||||
border-radius: 50%;
|
||||
|
|
@ -21,15 +37,31 @@
|
|||
height: 1rem;
|
||||
width: 1rem;
|
||||
* {
|
||||
margin-bottom: -1px;
|
||||
margin-bottom: 4px;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.oe_title {
|
||||
max-width: 75%;
|
||||
flex: 1;
|
||||
}
|
||||
.o_employee_form_header_info{
|
||||
flex: 5;
|
||||
}
|
||||
.o_presence_status_pill_wrapper{
|
||||
flex: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_employee_kanban .o_kanban_renderer {
|
||||
.o_employee_availability {
|
||||
margin: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hr_tags {
|
||||
margin-right: 20%;
|
||||
}
|
||||
|
||||
.o_hr_narrow_field {
|
||||
|
|
@ -40,9 +72,38 @@
|
|||
}
|
||||
}
|
||||
|
||||
@for $size from 10 through 15 {
|
||||
.o_hr_percentage_narrow_field input {
|
||||
max-width: 8rem !important;
|
||||
width: 8rem !important;
|
||||
* {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@for $size from 1 through 15 {
|
||||
.o_hr_narrow_field-#{$size} {
|
||||
width: #{$size}rem!important;
|
||||
max-width: #{$size}rem!important;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.o_hr_column {
|
||||
padding: 0 calc(var(--gutter-x) * 1)!important;
|
||||
|
||||
&:first-child {
|
||||
padding-left: calc(var(--gutter-x) * .5)!important;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: calc(var(--gutter-x) * .5)!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_version_list_view th[data-name="date_version"] {
|
||||
width: 8rem !important;
|
||||
}
|
||||
|
||||
.button-on {
|
||||
color: $o-success;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
.o_form_view.o_res_users_form_view_full {
|
||||
.o_contact_image_large img {
|
||||
width: 155px;
|
||||
height: 155px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { Store } from "@mail/core/common/store_service";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
/** @type {import("models").Store} */
|
||||
const storeServicePatch = {
|
||||
setup() {
|
||||
super.setup();
|
||||
/** @type {{[key: number]: {id: number, user_id: number, hasCheckedUser: boolean}}} */
|
||||
this.employees = {};
|
||||
},
|
||||
async getChat(person) {
|
||||
const { employeeId } = person;
|
||||
if (!employeeId) {
|
||||
return super.getChat(person);
|
||||
}
|
||||
let employee = this.employees[employeeId];
|
||||
if (!employee) {
|
||||
this.employees[employeeId] = { id: employeeId };
|
||||
employee = this.employees[employeeId];
|
||||
}
|
||||
if (!employee.user_id && !employee.hasCheckedUser) {
|
||||
employee.hasCheckedUser = true;
|
||||
const [employeeData] = await this.env.services.orm.silent.read(
|
||||
"hr.employee.public",
|
||||
[employee.id],
|
||||
["user_id", "user_partner_id"],
|
||||
{ context: { active_test: false } }
|
||||
);
|
||||
if (employeeData) {
|
||||
employee.user_id = employeeData.user_id[0];
|
||||
let user = this.users[employee.user_id];
|
||||
if (!user) {
|
||||
this.users[employee.user_id] = { id: employee.user_id };
|
||||
user = this.users[employee.user_id];
|
||||
}
|
||||
user.partner_id = employeeData.user_partner_id[0];
|
||||
this["res.partner"].insert({
|
||||
display_name: employeeData.user_partner_id[1],
|
||||
id: employeeData.user_partner_id[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!employee.user_id) {
|
||||
this.env.services.notification.add(
|
||||
_t("You can only chat with employees that have a dedicated user."),
|
||||
{ type: "info" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
return super.getChat({ userId: employee.user_id });
|
||||
},
|
||||
};
|
||||
|
||||
patch(Store.prototype, storeServicePatch);
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/** @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 })
|
||||
|
|
@ -1,30 +1,29 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { useComponent, useEnv } = owl;
|
||||
import { useComponent } from "@odoo/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();
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,32 +1,89 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2ManyTagsAvatarUserField, KanbanMany2ManyTagsAvatarUserField } from "@mail/views/fields/many2many_avatar_user_field/many2many_avatar_user_field";
|
||||
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 Many2ManyTagsAvatarUserField {
|
||||
get relation() {
|
||||
return "hr.employee.public";
|
||||
export class Many2ManyTagsAvatarEmployeeField extends EmployeeFieldRelationMixin(
|
||||
Many2ManyTagsAvatarUserField
|
||||
) {
|
||||
displayAvatarCard(record) {
|
||||
return (
|
||||
(!this.env.isSmall && ["hr.employee", "hr.employee.public"].includes(this.relation)) ||
|
||||
super.displayAvatarCard(record)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Many2ManyTagsAvatarEmployeeField.extractProps = ({ field, attrs }) => {
|
||||
return {
|
||||
...Many2ManyTagsAvatarUserField.extractProps({ field, attrs }),
|
||||
export const many2ManyTagsAvatarEmployeeField = {
|
||||
...many2ManyTagsAvatarUserField,
|
||||
component: Many2ManyTagsAvatarEmployeeField,
|
||||
additionalClasses: [
|
||||
...many2ManyTagsAvatarUserField.additionalClasses,
|
||||
"o_field_many2many_avatar_user",
|
||||
],
|
||||
extractProps: (fieldInfo, dynamicInfo) => ({
|
||||
...many2ManyTagsAvatarUserField.extractProps(fieldInfo, dynamicInfo),
|
||||
canQuickCreate: false,
|
||||
relation: (attrs.options && attrs.options.relation) || field.relation,
|
||||
}
|
||||
relation: fieldInfo.options?.relation,
|
||||
}),
|
||||
};
|
||||
|
||||
Many2ManyTagsAvatarEmployeeField.additionalClasses = [...Many2ManyTagsAvatarUserField.additionalClasses, "o_field_many2many_avatar_user"];
|
||||
registry.category("fields").add("many2many_avatar_employee", many2ManyTagsAvatarEmployeeField);
|
||||
|
||||
registry.category("fields").add("many2many_avatar_employee", Many2ManyTagsAvatarEmployeeField);
|
||||
|
||||
export class KanbanMany2ManyTagsAvatarEmployeeField extends KanbanMany2ManyTagsAvatarUserField {
|
||||
get relation() {
|
||||
return "hr.employee.public";
|
||||
export class KanbanMany2ManyTagsAvatarEmployeeField extends EmployeeFieldRelationMixin(
|
||||
KanbanMany2ManyTagsAvatarUserField
|
||||
) {
|
||||
displayAvatarCard(record) {
|
||||
return (
|
||||
(!this.env.isSmall && ["hr.employee", "hr.employee.public"].includes(this.relation)) ||
|
||||
super.displayAvatarCard(record)
|
||||
);
|
||||
}
|
||||
}
|
||||
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);
|
||||
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="true" 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"/>
|
||||
<span t-out="autoCompleteItemScope.label"/>
|
||||
</span>
|
||||
</t>
|
||||
</KanbanMany2One>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,36 +1,55 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { AvatarEmployee } from "@hr/components/avatar_employee/avatar_employee";
|
||||
import { Component, onWillStart } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2OneAvatarUserField, KanbanMany2OneAvatarUserField } from "@mail/views/fields/many2one_avatar_user_field/many2one_avatar_user_field";
|
||||
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 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,
|
||||
export class Many2OneAvatarEmployeeField extends Component {
|
||||
static template = "hr.Many2OneAvatarEmployeeField";
|
||||
static components = { AvatarEmployee, Many2One };
|
||||
static props = {
|
||||
...Many2OneField.props,
|
||||
relation: { type: String, optional: true },
|
||||
};
|
||||
};
|
||||
|
||||
registry.category("fields").add("kanban.many2one_avatar_employee", KanbanMany2OneAvatarEmployeeField);
|
||||
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>
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
/** @odoo-module */
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
|
||||
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";
|
||||
import { useArchiveEmployee } from "@hr/views/archive_employee_hook";
|
||||
|
||||
export class EmployeeFormController extends FormController {
|
||||
setup() {
|
||||
|
|
@ -15,30 +11,14 @@ export class EmployeeFormController extends FormController {
|
|||
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);
|
||||
}
|
||||
getStaticActionMenuItems() {
|
||||
const menuItems = super.getStaticActionMenuItems();
|
||||
menuItems.archive.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', {
|
||||
registry.category("views").add("hr_employee_form", {
|
||||
...formView,
|
||||
Controller: EmployeeFormController,
|
||||
Renderer: EmployeeFormRenderer,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
/** @odoo-module */
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
|
||||
import { kanbanView } from '@web/views/kanban/kanban_view';
|
||||
import { KanbanModel } from '@web/views/kanban/kanban_model';
|
||||
import { useArchiveEmployee } from "@hr/views/archive_employee_hook";
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
}
|
||||
|
||||
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', {
|
||||
registry.category("views").add("hr_employee_kanban", {
|
||||
...kanbanView,
|
||||
Model: EmployeeKanbanModel,
|
||||
Controller: EmployeeKanbanController,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
|
||||
import { listView } from '@web/views/list/list_view';
|
||||
|
|
@ -13,21 +11,20 @@ export class EmployeeListController extends ListController {
|
|||
this.archiveEmployee = useArchiveEmployee();
|
||||
}
|
||||
|
||||
getActionMenuItems() {
|
||||
const menuItems = super.getActionMenuItems();
|
||||
getStaticActionMenuItems() {
|
||||
const menuItems = super.getStaticActionMenuItems();
|
||||
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);
|
||||
}
|
||||
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', {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { helpers } from "@mail/views/open_chat_hook";
|
||||
import { helpers } from "@mail/core/web/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) {
|
||||
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 this._super(...arguments);
|
||||
},
|
||||
return super.buildOpenChatParams(...arguments);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/** @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,
|
||||
});
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { addFakeModel, addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
|
||||
|
||||
addModelNamesToFetch(['hr.employee.public']);
|
||||
|
||||
addFakeModel('m2x.avatar.employee', {
|
||||
employee_id: { string: "Employee", type: 'many2one', relation: 'hr.employee.public' },
|
||||
employee_ids: { string: "Employees", type: "many2many", relation: 'hr.employee.public' },
|
||||
});
|
||||
32
odoo-bringout-oca-ocb-hr/hr/static/tests/hr_test_helpers.js
Normal 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,
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { unpatchAvatarCardPopover } from "@hr/components/avatar_card/avatar_card_popover_patch";
|
||||
|
||||
unpatchAvatarCardPopover();
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
|
||||
|
||||
import { Many2OneAvatarEmployee } from '@hr/js/m2x_avatar_employee';
|
||||
import { dom } from 'web.test_utils';
|
||||
|
||||
QUnit.module('hr', {}, function () {
|
||||
QUnit.module('M2XAvatarEmployeeLegacy', {
|
||||
beforeEach() {
|
||||
Many2OneAvatarEmployee.prototype.partnerIds = {};
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('many2one_avatar_employee: click on an employee not associated with a user', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const hrEmployeePublicId1 = pyEnv['hr.employee.public'].create({ name: 'Mario' });
|
||||
const m2xHrAvatarUserId1 = pyEnv['m2x.avatar.employee'].create({ employee_id: hrEmployeePublicId1 });
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,form': '<form js_class="legacy_form"><field name="employee_id" widget="many2one_avatar_employee"/></form>',
|
||||
};
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'read') {
|
||||
assert.step(`read ${args.model} ${args.args[0]}`);
|
||||
}
|
||||
},
|
||||
serverData: { views },
|
||||
services: {
|
||||
notification: makeFakeNotificationService(message => {
|
||||
assert.ok(
|
||||
true,
|
||||
"should display a toast notification after failing to open chat"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message,
|
||||
"You can only chat with employees that have a dedicated user.",
|
||||
"should display the correct information in the notification"
|
||||
);
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
res_id: m2xHrAvatarUserId1,
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
assert.strictEqual(document.querySelector('.o_field_widget[name=employee_id]').innerText.trim(), 'Mario');
|
||||
|
||||
await dom.click(document.querySelector('.o_m2o_avatar > img'));
|
||||
|
||||
assert.verifySteps([
|
||||
`read m2x.avatar.employee ${m2xHrAvatarUserId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1}`,
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('many2many_avatar_employee widget in form view', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{}, {}]);
|
||||
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{}, {}]);
|
||||
const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([
|
||||
{ user_id: resUsersId1, user_partner_id: resPartnerId1 },
|
||||
{ user_id: resUsersId2, user_partner_id: resPartnerId2 },
|
||||
]);
|
||||
const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create(
|
||||
{ employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] },
|
||||
);
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,form': '<form js_class="legacy_form"><field name="employee_ids" widget="many2many_avatar_employee"/></form>',
|
||||
};
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'read') {
|
||||
assert.step(`read ${args.model} ${args.args[0]}`);
|
||||
}
|
||||
},
|
||||
serverData: { views },
|
||||
});
|
||||
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
res_id: m2xAvatarEmployeeId1,
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
|
||||
"should have 2 records");
|
||||
assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'),
|
||||
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
|
||||
"should have correct avatar image");
|
||||
|
||||
await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar'));
|
||||
await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]);
|
||||
|
||||
assert.verifySteps([
|
||||
`read m2x.avatar.employee ${m2xAvatarEmployeeId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId2}`,
|
||||
]);
|
||||
|
||||
assert.containsN(
|
||||
document.body,
|
||||
'.o_ChatWindowHeader_name',
|
||||
2,
|
||||
"should have 2 chat windows"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('many2many_avatar_employee: click on an employee not associated with a user', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv['res.partner'].create({});
|
||||
const resUsersId1 = pyEnv['res.users'].create({});
|
||||
const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([
|
||||
{},
|
||||
{ user_id: resUsersId1, user_partner_id: resPartnerId1 },
|
||||
]);
|
||||
const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create(
|
||||
{ employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] },
|
||||
);
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,form': '<form js_class="legacy_form"><field name="employee_ids" widget="many2many_avatar_employee"/></form>',
|
||||
};
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'read') {
|
||||
assert.step(`read ${args.model} ${args.args[0]}`);
|
||||
}
|
||||
},
|
||||
serverData: { views },
|
||||
services: {
|
||||
notification: makeFakeNotificationService(message => {
|
||||
assert.ok(
|
||||
true,
|
||||
"should display a toast notification after failing to open chat"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message,
|
||||
"You can only chat with employees that have a dedicated user.",
|
||||
"should display the correct information in the notification"
|
||||
);
|
||||
}),
|
||||
},
|
||||
});
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
res_id: m2xAvatarEmployeeId1,
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
|
||||
assert.containsN(document.body, '.o_field_many2manytags.avatar.o_field_widget .badge', 2,
|
||||
"should have 2 records");
|
||||
assert.strictEqual(document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'),
|
||||
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
|
||||
"should have correct avatar image");
|
||||
|
||||
await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar'));
|
||||
await dom.click(document.querySelectorAll('.o_field_many2manytags.avatar .badge .o_m2m_avatar')[1]);
|
||||
|
||||
assert.verifySteps([
|
||||
`read m2x.avatar.employee ${hrEmployeePublicId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId2}`
|
||||
]);
|
||||
|
||||
assert.containsOnce(document.body, '.o_ChatWindowHeader_name',
|
||||
"should have 1 chat window");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,450 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
afterNextRender,
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
|
||||
|
||||
import { Many2OneAvatarEmployee } from '@hr/js/m2x_avatar_employee';
|
||||
import { dom } from 'web.test_utils';
|
||||
|
||||
QUnit.module('hr', {}, function () {
|
||||
QUnit.module('M2XAvatarEmployee', {
|
||||
beforeEach() {
|
||||
Many2OneAvatarEmployee.prototype.partnerIds = {};
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('many2one_avatar_employee widget in list view', async function (assert) {
|
||||
assert.expect(13);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([
|
||||
{ display_name: "Mario" },
|
||||
{ display_name: "Luigi" },
|
||||
]);
|
||||
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([
|
||||
{ partner_id: resPartnerId1 },
|
||||
{ partner_id: resPartnerId2 },
|
||||
]);
|
||||
const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([
|
||||
{ name: "Mario", user_id: resUsersId1, user_partner_id: resPartnerId1 },
|
||||
{ name: "Luigi", user_id: resUsersId2, user_partner_id: resPartnerId2 },
|
||||
]);
|
||||
pyEnv['m2x.avatar.employee'].create([
|
||||
{ employee_id: hrEmployeePublicId1, employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] },
|
||||
{ employee_id: hrEmployeePublicId2 },
|
||||
{ employee_id: hrEmployeePublicId1 },
|
||||
]);
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,list': '<tree><field name="employee_id" widget="many2one_avatar_employee"/></tree>',
|
||||
};
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'read') {
|
||||
assert.step(`read ${args.model} ${args.args[0]}`);
|
||||
}
|
||||
},
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
views: [[false, 'list']],
|
||||
});
|
||||
|
||||
assert.strictEqual(document.querySelector('.o_data_cell span:not(.o_m2o_avatar) span').innerText, 'Mario');
|
||||
assert.strictEqual(document.querySelectorAll('.o_data_cell span:not(.o_m2o_avatar) span')[1].innerText, 'Luigi');
|
||||
assert.strictEqual(document.querySelectorAll('.o_data_cell span:not(.o_m2o_avatar) span')[2].innerText, 'Mario');
|
||||
|
||||
// click on first employee
|
||||
await afterNextRender(() =>
|
||||
dom.click(document.querySelector('.o_data_cell .o_m2o_avatar > img'))
|
||||
);
|
||||
assert.verifySteps(
|
||||
[`read hr.employee.public ${hrEmployeePublicId1}`],
|
||||
"first employee should have been read to find its partner"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_ChatWindowHeader_name',
|
||||
'should have opened chat window'
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_ChatWindowHeader_name').textContent,
|
||||
"Mario",
|
||||
'chat window should be with clicked employee'
|
||||
);
|
||||
|
||||
// click on second employee
|
||||
await afterNextRender(() =>
|
||||
dom.click(document.querySelectorAll('.o_data_cell .o_m2o_avatar > img')[1]
|
||||
));
|
||||
assert.verifySteps(
|
||||
[`read hr.employee.public ${hrEmployeePublicId2}`],
|
||||
"second employee should have been read to find its partner"
|
||||
);
|
||||
assert.containsN(
|
||||
document.body,
|
||||
'.o_ChatWindowHeader_name',
|
||||
2,
|
||||
'should have opened second chat window'
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll('.o_ChatWindowHeader_name')[1].textContent,
|
||||
"Luigi",
|
||||
'chat window should be with clicked employee'
|
||||
);
|
||||
|
||||
// click on third employee (same as first)
|
||||
await afterNextRender(() =>
|
||||
dom.click(document.querySelectorAll('.o_data_cell .o_m2o_avatar > img')[2])
|
||||
);
|
||||
assert.verifySteps(
|
||||
[],
|
||||
"employee should not have been read again because we already know its partner"
|
||||
);
|
||||
assert.containsN(
|
||||
document.body,
|
||||
'.o_ChatWindowHeader_name',
|
||||
2,
|
||||
"should still have only 2 chat windows because third is the same partner as first"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('many2one_avatar_employee widget in kanban view', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv['res.partner'].create({});
|
||||
const resUsersId1 = pyEnv['res.users'].create({ partner_id: resPartnerId1 });
|
||||
const hrEmployeePublicId1 = pyEnv['hr.employee.public'].create({ user_id: resUsersId1, user_partner_id: resPartnerId1 });
|
||||
pyEnv['m2x.avatar.employee'].create({ employee_id: hrEmployeePublicId1, employee_ids: [hrEmployeePublicId1] });
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,kanban':
|
||||
`<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="employee_id" widget="many2one_avatar_employee"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
};
|
||||
const { openView } = await start({
|
||||
serverData: { views },
|
||||
});
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
|
||||
assert.strictEqual(document.querySelector('.o_kanban_record').innerText.trim(), '');
|
||||
assert.containsOnce(document.body, '.o_m2o_avatar');
|
||||
assert.strictEqual(document.querySelector('.o_m2o_avatar > img').getAttribute('data-src'), `/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`);
|
||||
});
|
||||
|
||||
QUnit.test('many2one_avatar_employee: click on an employee not associated with a user', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const hrEmployeePublicId1 = pyEnv['hr.employee.public'].create({ name: 'Mario' });
|
||||
const m2xHrAvatarUserId1 = pyEnv['m2x.avatar.employee'].create({ employee_id: hrEmployeePublicId1 });
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,form': '<form><field name="employee_id" widget="many2one_avatar_employee"/></form>',
|
||||
};
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'read') {
|
||||
assert.step(`read ${args.model} ${args.args[0]}`);
|
||||
}
|
||||
},
|
||||
serverData: { views },
|
||||
services: {
|
||||
notification: makeFakeNotificationService(message => {
|
||||
assert.ok(
|
||||
true,
|
||||
"should display a toast notification after failing to open chat"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message,
|
||||
"You can only chat with employees that have a dedicated user.",
|
||||
"should display the correct information in the notification"
|
||||
);
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
res_id: m2xHrAvatarUserId1,
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
assert.strictEqual(document.querySelector('.o_field_widget[name=employee_id] input').value.trim(), 'Mario');
|
||||
|
||||
await dom.click(document.querySelector('.o_m2o_avatar > img'));
|
||||
|
||||
assert.verifySteps([
|
||||
`read m2x.avatar.employee ${m2xHrAvatarUserId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1}`,
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('many2many_avatar_employee widget in form view', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{}, {}]);
|
||||
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{}, {}]);
|
||||
const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([
|
||||
{ user_id: resUsersId1, user_partner_id: resPartnerId1 },
|
||||
{ user_id: resUsersId2, user_partner_id: resPartnerId2 },
|
||||
]);
|
||||
const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create(
|
||||
{ employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] },
|
||||
);
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,form': '<form><field name="employee_ids" widget="many2many_avatar_employee"/></form>',
|
||||
};
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'read') {
|
||||
assert.step(`read ${args.model} ${args.args[0]}`);
|
||||
}
|
||||
},
|
||||
serverData: { views },
|
||||
});
|
||||
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
res_id: m2xAvatarEmployeeId1,
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
assert.containsN(document.body, '.o_field_many2many_avatar_employee .badge', 2,
|
||||
"should have 2 records");
|
||||
assert.strictEqual(document.querySelector('.o_field_many2many_avatar_employee .badge img').getAttribute('data-src'),
|
||||
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
|
||||
"should have correct avatar image");
|
||||
|
||||
await dom.click(document.querySelector('.o_field_many2many_avatar_employee .badge .o_m2m_avatar'));
|
||||
await dom.click(document.querySelectorAll('.o_field_many2many_avatar_employee .badge .o_m2m_avatar')[1]);
|
||||
|
||||
assert.verifySteps([
|
||||
`read m2x.avatar.employee ${m2xAvatarEmployeeId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId2}`,
|
||||
]);
|
||||
|
||||
assert.containsN(
|
||||
document.body,
|
||||
'.o_ChatWindowHeader_name',
|
||||
2,
|
||||
"should have 2 chat windows"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('many2many_avatar_employee widget in list view', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([
|
||||
{ name: "Mario" },
|
||||
{ name: "Yoshi" },
|
||||
]);
|
||||
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{}, {}]);
|
||||
const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([
|
||||
{ user_id: resUsersId1, user_partner_id: resPartnerId1 },
|
||||
{ user_id: resUsersId2, user_partner_id: resPartnerId2 },
|
||||
]);
|
||||
pyEnv['m2x.avatar.employee'].create(
|
||||
{ employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] },
|
||||
);
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,list': '<tree><field name="employee_ids" widget="many2many_avatar_employee"/></tree>',
|
||||
};
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'read') {
|
||||
assert.step(`read ${args.model} ${args.args[0]}`);
|
||||
}
|
||||
},
|
||||
serverData: { views },
|
||||
});
|
||||
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
views: [[false, 'list']],
|
||||
});
|
||||
assert.containsN(document.body, '.o_data_cell:first .o_field_many2many_avatar_employee > div > span', 2,
|
||||
"should have two avatar");
|
||||
|
||||
// click on first employee badge
|
||||
await afterNextRender(() =>
|
||||
dom.click(document.querySelector('.o_data_cell .o_m2m_avatar'))
|
||||
);
|
||||
assert.verifySteps(
|
||||
[`read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`, `read hr.employee.public ${hrEmployeePublicId1}`],
|
||||
"first employee should have been read to find its partner"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_ChatWindowHeader_name',
|
||||
'should have opened chat window'
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_ChatWindowHeader_name').textContent,
|
||||
"Mario",
|
||||
'chat window should be with clicked employee'
|
||||
);
|
||||
|
||||
// click on second employee
|
||||
await afterNextRender(() =>
|
||||
dom.click(document.querySelectorAll('.o_data_cell .o_m2m_avatar')[1])
|
||||
);
|
||||
assert.verifySteps(
|
||||
[`read hr.employee.public ${hrEmployeePublicId2}`],
|
||||
"second employee should have been read to find its partner"
|
||||
);
|
||||
assert.containsN(
|
||||
document.body,
|
||||
'.o_ChatWindowHeader_name',
|
||||
2,
|
||||
'should have opened second chat window'
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll('.o_ChatWindowHeader_name')[1].textContent,
|
||||
"Yoshi",
|
||||
'chat window should be with clicked employee'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('many2many_avatar_employee widget in kanban view', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{}, {}]);
|
||||
const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{}, {}]);
|
||||
const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([
|
||||
{ user_id: resUsersId1, user_partner_id: resPartnerId1 },
|
||||
{ user_id: resUsersId2, user_partner_id: resPartnerId2 },
|
||||
]);
|
||||
pyEnv['m2x.avatar.employee'].create(
|
||||
{ employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] },
|
||||
);
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,kanban':
|
||||
`<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<div class="oe_kanban_footer">
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="employee_ids" widget="many2many_avatar_employee"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
};
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'read') {
|
||||
assert.step(`read ${args.model} ${args.args[0]}`);
|
||||
}
|
||||
},
|
||||
serverData: { views },
|
||||
});
|
||||
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
views: [[false, 'kanban']],
|
||||
});
|
||||
assert.containsN(document.body, '.o_kanban_record:first .o_field_many2many_avatar_employee img.o_m2m_avatar', 2,
|
||||
"should have 2 avatar images");
|
||||
assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar').getAttribute('data-src'),
|
||||
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
|
||||
"should have correct avatar image");
|
||||
assert.strictEqual(document.querySelectorAll('.o_kanban_record .o_field_many2many_avatar_employee img.o_m2m_avatar')[1].getAttribute('data-src'),
|
||||
`/web/image/hr.employee.public/${hrEmployeePublicId2}/avatar_128`,
|
||||
"should have correct avatar image");
|
||||
|
||||
await dom.click(document.querySelector('.o_kanban_record .o_m2m_avatar'));
|
||||
await dom.click(document.querySelectorAll('.o_kanban_record .o_m2m_avatar')[1]);
|
||||
|
||||
assert.verifySteps([
|
||||
`read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId2}`
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('many2many_avatar_employee: click on an employee not associated with a user', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv['res.partner'].create({});
|
||||
const resUsersId1 = pyEnv['res.users'].create({});
|
||||
const [hrEmployeePublicId1, hrEmployeePublicId2] = pyEnv['hr.employee.public'].create([
|
||||
{},
|
||||
{ user_id: resUsersId1, user_partner_id: resPartnerId1 },
|
||||
]);
|
||||
const m2xAvatarEmployeeId1 = pyEnv['m2x.avatar.employee'].create(
|
||||
{ employee_ids: [hrEmployeePublicId1, hrEmployeePublicId2] },
|
||||
);
|
||||
const views = {
|
||||
'm2x.avatar.employee,false,form': '<form><field name="employee_ids" widget="many2many_avatar_employee"/></form>',
|
||||
};
|
||||
const { openView } = await start({
|
||||
mockRPC(route, args) {
|
||||
if (args.method === 'read') {
|
||||
assert.step(`read ${args.model} ${args.args[0]}`);
|
||||
}
|
||||
},
|
||||
serverData: { views },
|
||||
services: {
|
||||
notification: makeFakeNotificationService(message => {
|
||||
assert.ok(
|
||||
true,
|
||||
"should display a toast notification after failing to open chat"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message,
|
||||
"You can only chat with employees that have a dedicated user.",
|
||||
"should display the correct information in the notification"
|
||||
);
|
||||
}),
|
||||
},
|
||||
});
|
||||
await openView({
|
||||
res_model: 'm2x.avatar.employee',
|
||||
res_id: m2xAvatarEmployeeId1,
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
|
||||
assert.containsN(document.body, '.o_field_many2many_avatar_employee .badge', 2,
|
||||
"should have 2 records");
|
||||
assert.strictEqual(document.querySelector('.o_field_many2many_avatar_employee .badge img').getAttribute('data-src'),
|
||||
`/web/image/hr.employee.public/${hrEmployeePublicId1}/avatar_128`,
|
||||
"should have correct avatar image");
|
||||
|
||||
await dom.click(document.querySelector('.o_field_many2many_avatar_employee .badge .o_m2m_avatar'));
|
||||
await dom.click(document.querySelectorAll('.o_field_many2many_avatar_employee .badge .o_m2m_avatar')[1]);
|
||||
|
||||
assert.verifySteps([
|
||||
`read m2x.avatar.employee ${hrEmployeePublicId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1},${hrEmployeePublicId2}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId1}`,
|
||||
`read hr.employee.public ${hrEmployeePublicId2}`
|
||||
]);
|
||||
|
||||
assert.containsOnce(document.body, '.o_ChatWindowHeader_name',
|
||||
"should have 1 chat window");
|
||||
});
|
||||
});
|
||||
|
|
@ -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" });
|
||||
}
|
||||
|
|
@ -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"];
|
||||
}
|
||||
}
|
||||
|
|
@ -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>`,
|
||||
};
|
||||
}
|
||||
|
|
@ -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>`,
|
||||
};
|
||||
}
|
||||
|
|
@ -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>`,
|
||||
};
|
||||
}
|
||||
|
|
@ -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>`,
|
||||
};
|
||||
}
|
||||
|
|
@ -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>`,
|
||||
};
|
||||
}
|
||||
|
|
@ -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" });
|
||||
}
|
||||
|
|
@ -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",
|
||||
})
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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" });
|
||||
}
|
||||
|
|
@ -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,
|
||||
}));
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
|
||||
import AbstractRendererOwl from 'web.AbstractRendererOwl';
|
||||
import BasicView from "web.BasicView";
|
||||
import BasicRenderer from "web.BasicRenderer";
|
||||
import RendererWrapper from 'web.RendererWrapper';
|
||||
import { createView } from 'web.test_utils';
|
||||
|
||||
import StandaloneM2OAvatarEmployee from '@hr/js/standalone_m2o_avatar_employee';
|
||||
|
||||
const { xml } = owl;
|
||||
|
||||
function getHtmlRenderer(html) {
|
||||
return BasicRenderer.extend({
|
||||
start: function () {
|
||||
this.$el.html(html);
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getOwlView(owlRenderer, viewType) {
|
||||
viewType = viewType || "test";
|
||||
return BasicView.extend({
|
||||
viewType: viewType,
|
||||
config: Object.assign({}, BasicView.prototype.config, {
|
||||
Renderer: owlRenderer,
|
||||
}),
|
||||
getRenderer() {
|
||||
return new RendererWrapper(null, this.config.Renderer, {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getHtmlView(html, viewType) {
|
||||
viewType = viewType || "test";
|
||||
return BasicView.extend({
|
||||
viewType: viewType,
|
||||
config: Object.assign({}, BasicView.prototype.config, {
|
||||
Renderer: getHtmlRenderer(html)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module('hr', {}, function () {
|
||||
QUnit.module('StandaloneM2OEmployeeTests', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'foo': {
|
||||
fields: {
|
||||
employee_id: {string: "Employee", type: 'many2one', relation: 'hr.employee'},
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
'hr.employee': {
|
||||
fields: {},
|
||||
records: [
|
||||
{id: 10, name: "Mario"},
|
||||
{id: 20, name: "Luigi"},
|
||||
{id: 30, name: "Yoshi"}
|
||||
],
|
||||
},
|
||||
'hr.employee.public': {
|
||||
fields: {},
|
||||
records: [
|
||||
{id: 10, name: "Mario"},
|
||||
{id: 20, name: "Luigi"},
|
||||
{id: 30, name: "Yoshi"}
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('standalone_m2o_avatar_employee: legacy view', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const html = "<div class='coucou_test'></div>";
|
||||
const view = await createView({
|
||||
View: getHtmlView(html, "test"),
|
||||
data: this.data,
|
||||
model: "foo",
|
||||
arch: "<test/>"
|
||||
});
|
||||
|
||||
const avatar10 = new StandaloneM2OAvatarEmployee(view, 10);
|
||||
const avatar20 = new StandaloneM2OAvatarEmployee(view, 20);
|
||||
const avatar30 = new StandaloneM2OAvatarEmployee(view, [30, 'Bowser']);
|
||||
|
||||
await avatar10.appendTo(view.el.querySelector('.coucou_test'));
|
||||
await avatar20.appendTo(view.el.querySelector('.coucou_test'));
|
||||
await avatar30.appendTo(view.el.querySelector('.coucou_test'));
|
||||
|
||||
assert.deepEqual(
|
||||
[...view.el.querySelectorAll('.o_field_many2one_avatar span')].map(el => el.innerText),
|
||||
["Mario", "Luigi", "Bowser"]
|
||||
);
|
||||
|
||||
view.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('standalone_m2o_avatar_employee: Owl view', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
class Renderer extends AbstractRendererOwl { }
|
||||
Renderer.template = xml`<div class='coucou_test'></div>`;
|
||||
|
||||
const view = await createView({
|
||||
View: getOwlView(Renderer, "test"),
|
||||
data: this.data,
|
||||
model: "foo",
|
||||
arch: "<test/>"
|
||||
});
|
||||
|
||||
const avatar10 = new StandaloneM2OAvatarEmployee(view, 10);
|
||||
const avatar20 = new StandaloneM2OAvatarEmployee(view, 20);
|
||||
const avatar30 = new StandaloneM2OAvatarEmployee(view, [30, 'Bowser']);
|
||||
|
||||
await avatar10.appendTo(view.el.querySelector('.coucou_test'));
|
||||
await avatar20.appendTo(view.el.querySelector('.coucou_test'));
|
||||
await avatar30.appendTo(view.el.querySelector('.coucou_test'));
|
||||
|
||||
assert.deepEqual(
|
||||
[...view.el.querySelectorAll('.o_field_many2one_avatar span')].map(el => el.innerText),
|
||||
["Mario", "Luigi", "Bowser"]
|
||||
);
|
||||
|
||||
view.destroy();
|
||||
});
|
||||
});
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -1,29 +1,37 @@
|
|||
/** @odoo-module **/
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('hr_employee_tour', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
}, [
|
||||
tour.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_title:contains('Johnny H.')",
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Open user account menu",
|
||||
trigger: ".o_user_menu .oe_topbar_name",
|
||||
run: 'click',
|
||||
}, {
|
||||
content: "Open My Profile",
|
||||
trigger: "[data-menu=settings]",
|
||||
run: 'click',
|
||||
},
|
||||
]);
|
||||
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",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
/**
|
||||
* As 'hr' changes the flow a bit and displays the user preferences form in a full view instead of
|
||||
* a modal, we adapt the steps of the original tour accordingly.
|
||||
*/
|
||||
tour.tours['mail/static/tests/tours/user_modify_own_profile_tour.js'].steps = [{
|
||||
content: 'Open user account menu',
|
||||
trigger: '.o_user_menu button',
|
||||
}, {
|
||||
content: "Open preferences / profile screen",
|
||||
trigger: '[data-menu=settings]',
|
||||
}, {
|
||||
content: "Update the email address",
|
||||
trigger: 'div[name="email"] input',
|
||||
run: 'text updatedemail@example.com',
|
||||
}, ...tour.stepUtils.saveForm()];
|
||||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -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 });
|
||||
});
|
||||