mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-26 05:51:59 +02:00
19.0 vanilla
This commit is contained in:
parent
a1137a1456
commit
e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2 KiB |
|
|
@ -1,49 +1 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 70 70">
|
||||
<defs>
|
||||
<mask id="mask" x="0" y="0" width="70" height="70" maskUnits="userSpaceOnUse">
|
||||
<g id="mask-2" data-name="mask">
|
||||
<g id="b">
|
||||
<path id="a" d="M4,0H65c4,0,5,1,5,5V65c0,4-1,5-5,5H4c-3,0-4-1-4-5V5C0,1,1,0,4,0Z" fill="#fff" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="linear-gradient" x1="-909.8" y1="-216.96" x2="-910.8" y2="-217.96" gradientTransform="matrix(70, 0, 0, -70, 63756, -15187.42)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#269396"/>
|
||||
<stop offset="1" stop-color="#218689"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g mask="url(#mask)">
|
||||
<g>
|
||||
<path d="M0,0H70V70H0Z" fill-rule="evenodd" fill="url(#linear-gradient)"/>
|
||||
<path d="M4,1H65c2.67,0,4.33.67,5,2V0H0V3C.67,1.67,2,1,4,1Z" fill="#fff" fill-opacity="0.38" fill-rule="evenodd"/>
|
||||
<path d="M43,69H4c-2,0-4-.14-4-4V36.23L10.93,25.3l6-5,4.15,2.53L30.9,13l5.49-1.51,3.87,2.33L40,21.49,16.5,45.05l.72,1.51L30.16,33.63,40.85,33,52.13,21.68,54.77,21l5,9.6L41.22,49.17l7.69,1.15,5.64-5.64,1.24,11.49Z" fill="#393939" fill-rule="evenodd" opacity="0.32" style="isolation: isolate"/>
|
||||
<path d="M4,69H65c2.67,0,4.33-1,5-3v4H0V66A3.92,3.92,0,0,0,4,69Z" fill-opacity="0.38" fill-rule="evenodd"/>
|
||||
<g>
|
||||
<g opacity="0.4">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M33.94,32.4h0a9.12,9.12,0,0,0,0,18.23h.19a9.12,9.12,0,0,0-.19-18.23Z"/>
|
||||
<path d="M55.66,45a2.5,2.5,0,0,0-3.1,1.7L50.71,53,40.34,50.41a11,11,0,0,1-12.77,0L17.18,53.13,15.3,46.67a2.5,2.5,0,0,0-4.81,1.4L13,56.69a2.49,2.49,0,0,0,2.39,1.8,2.38,2.38,0,0,0,.46,0l8.94-1.64v4.76H43.08V56.38l8.81,2a2.74,2.74,0,0,0,.56.06,2.49,2.49,0,0,0,2.4-1.8l2.51-8.62A2.49,2.49,0,0,0,55.66,45Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M14.91,36a1.77,1.77,0,0,1-1.26-.52L8.08,29.94a1.77,1.77,0,0,1-.52-1.26,1.74,1.74,0,0,1,.52-1.26l5.57-5.58a1.8,1.8,0,0,1,2.53,0l5.57,5.57a1.8,1.8,0,0,1,0,2.53l-5.57,5.57A1.78,1.78,0,0,1,14.91,36ZM9.65,28.67l5.27,5.27,5.26-5.26-5.27-5.27Z"/>
|
||||
<path d="M56.24,33.48H48a1.79,1.79,0,0,1-1.59-2.58h0l4.14-8.28a1.76,1.76,0,0,1,1.59-1h0a1.79,1.79,0,0,1,1.6,1l4.14,8.28a1.75,1.75,0,0,1-.08,1.73A1.77,1.77,0,0,1,56.24,33.48Zm-7.93-2h7.58L52.1,23.9Z"/>
|
||||
<path d="M33.58,25.22a6.38,6.38,0,0,1-4.53-1.87h0a6.42,6.42,0,1,1,4.53,1.87Zm-3.12-3.28a4.41,4.41,0,1,0,0-6.24,4.43,4.43,0,0,0,0,6.24Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M35.93,30.42h0a9.12,9.12,0,0,0,0,18.23h.19a9.12,9.12,0,0,0-.19-18.23Z" fill="#fff"/>
|
||||
<path d="M57.65,43a2.5,2.5,0,0,0-3.1,1.7L52.7,51l-10.37-2.6a11.06,11.06,0,0,1-12.77,0l-10.4,2.71-1.88-6.46a2.5,2.5,0,1,0-4.8,1.4L15,54.7a2.45,2.45,0,0,0,2.86,1.76l8.94-1.64v4.76H45.07V54.39l8.8,2a2.78,2.78,0,0,0,.57.07,2.5,2.5,0,0,0,2.4-1.81l2.51-8.62A2.49,2.49,0,0,0,57.65,43Z" fill="#fff"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M16.9,34a1.77,1.77,0,0,1-1.26-.52L10.07,28a1.77,1.77,0,0,1-.52-1.26,1.74,1.74,0,0,1,.52-1.26l5.57-5.58a1.79,1.79,0,0,1,2.52,0l5.58,5.58a1.74,1.74,0,0,1,.52,1.26A1.81,1.81,0,0,1,23.74,28l-5.57,5.57A1.78,1.78,0,0,1,16.9,34Zm-5.26-7.35L16.9,32l5.27-5.27L16.9,21.42Z" fill="#fff"/>
|
||||
<path d="M58.23,31.49H50a1.75,1.75,0,0,1-1.51-.84,1.77,1.77,0,0,1-.08-1.74h0l4.14-8.28a1.75,1.75,0,0,1,1.59-1h0a1.78,1.78,0,0,1,1.6,1l4.14,8.28a1.79,1.79,0,0,1-1.6,2.58Zm-7.93-2h7.58l-3.79-7.58Z" fill="#fff"/>
|
||||
<path d="M35.57,23.24A6.39,6.39,0,0,1,31,21.36h0a6.4,6.4,0,1,1,4.53,1.88ZM32.45,20a4.41,4.41,0,1,0,0-6.23,4.41,4.41,0,0,0,0,6.23Z" fill="#fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M30.576 14.907a4.029 4.029 0 0 1 5.697 0l8.547 8.546a4.029 4.029 0 0 1 0 5.698l-15.669 15.67a4.029 4.029 0 0 1-5.698 0l-8.546-8.547a4.029 4.029 0 0 1 0-5.698l15.669-15.67Z" fill="#FBB945"/><path d="M18.721 9.047a4.029 4.029 0 0 1 5.264-2.18l11.167 4.625a4.029 4.029 0 0 1 2.18 5.264l-8.48 20.472a4.029 4.029 0 0 1-5.263 2.181l-11.167-4.626a4.029 4.029 0 0 1-2.18-5.264l8.48-20.472Z" fill="#985184"/><path d="M37.527 16.16c-.048.2-.113.4-.194.596l-8.48 20.472a4.029 4.029 0 0 1-5.265 2.18l-9.236-3.825a4.03 4.03 0 0 1 .555-5.007l15.669-15.67a4.029 4.029 0 0 1 5.697 0l1.254 1.254Z" fill="#712258"/><path d="M4 8.029A4.029 4.029 0 0 1 8.029 4h12.087a4.029 4.029 0 0 1 4.029 4.029v22.16a4.029 4.029 0 0 1-4.03 4.028H8.03A4.029 4.029 0 0 1 4 30.188V8.03Z" fill="#1AD3BB"/><path d="M23.973 6.861c.112.37.172.762.172 1.168v22.16a4.029 4.029 0 0 1-4.029 4.029h-8.658a4.03 4.03 0 0 1-1.217-4.699l8.48-20.472a4.029 4.029 0 0 1 5.252-2.186Z" fill="#005E7A"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="resource_mail.AvatarCardResourcePopover" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_avatar_card_details')]" position="inside">
|
||||
<div t-if="skills?.length">
|
||||
<span class="fw-bold">Skills</span>
|
||||
<div class="d-flex flex-wrap gap-1 o_employee_skills_tags mt-1 overflow-hidden">
|
||||
<TagsList tags="skillTags" visibleItemsLimit="5"/>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { AvatarCardResourcePopover } from "@resource_mail/components/avatar_card_resource/avatar_card_resource_popover";
|
||||
|
||||
|
||||
export const patchAvatarCardResourcePopover = {
|
||||
loadAdditionalData() {
|
||||
const promises = super.loadAdditionalData();
|
||||
this.skills = false;
|
||||
if (this.record.employee_skill_ids?.length) {
|
||||
promises.push(
|
||||
this.orm
|
||||
.read("hr.employee.skill", this.record.employee_skill_ids, ["display_name", "color"])
|
||||
.then((skills) => {
|
||||
this.skills = skills;
|
||||
})
|
||||
);
|
||||
}
|
||||
return promises;
|
||||
},
|
||||
get fieldNames() {
|
||||
return [
|
||||
...super.fieldNames,
|
||||
"employee_skill_ids",
|
||||
];
|
||||
},
|
||||
get hasFooter() {
|
||||
return this.skills?.length > 0 || super.hasFooter;
|
||||
},
|
||||
get skillTags() {
|
||||
return this.skills.map(({ id, display_name, color }) => ({
|
||||
id,
|
||||
text: display_name,
|
||||
colorIndex: color,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
export const unpatchAvatarCardResourcePopover = patch(AvatarCardResourcePopover.prototype, patchAvatarCardResourcePopover);
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { formatDate } from "@web/core/l10n/dates";
|
||||
|
||||
export class InternalResumeLineComponent extends Component {
|
||||
static template = "hr_skills.InternalResumeLineComponent";
|
||||
static props = { ...standardWidgetProps };
|
||||
|
||||
setup(){
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
onWillStart(async () => {
|
||||
this.internalResumeLines = await this.getInternalResumeLines(
|
||||
this.props.record.resId,
|
||||
this.props.record.resModel
|
||||
)
|
||||
})
|
||||
onWillUpdateProps(async (nextProps) => {
|
||||
this.internalResumeLines = await this.getInternalResumeLines(
|
||||
nextProps.record.resId,
|
||||
nextProps.record.resModel
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async getInternalResumeLines(resId, resModel){
|
||||
const internalResumeLines = await this.orm.call(
|
||||
"hr.employee",
|
||||
"get_internal_resume_lines",
|
||||
[resId, resModel]
|
||||
);
|
||||
return internalResumeLines;
|
||||
}
|
||||
|
||||
get companyId(){
|
||||
return this.props.record.data.company_id.display_name;
|
||||
}
|
||||
|
||||
get haveResumeLines(){
|
||||
return this.props.record.data.resume_line_ids.records.length || this.internalResumeLines.length;
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
const formattedDate = luxon.DateTime.fromISO(date);
|
||||
return formatDate(formattedDate);
|
||||
}
|
||||
}
|
||||
|
||||
export const internalResumeLinesComponent = {
|
||||
component: InternalResumeLineComponent,
|
||||
fieldDependencies: [
|
||||
{ name: "company_id", type: "many2one" },
|
||||
{ name: "resume_line_ids", type: "one2many"}
|
||||
],
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("internal_resume_lines", internalResumeLinesComponent);
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.o_field_resume_one2many {
|
||||
.o_widget_internal_resume_lines {
|
||||
$o-hrs-timeline-entry-padding: .5rem;
|
||||
$o-hrs-timeline-dot-size: .6rem;
|
||||
|
||||
|
|
@ -16,12 +16,19 @@
|
|||
}
|
||||
|
||||
&:before {
|
||||
@include o-position-absolute(0, $left: ($o-hrs-timeline-dot-size * .5 + $o-hrs-timeline-entry-padding));
|
||||
width: 1px;
|
||||
@include o-position-absolute(0, $left: ($o-hrs-timeline-dot-size * .5 + o-to-rem($o-horizontal-padding) - o-to-rem($border-width)));
|
||||
width: $border-width;
|
||||
height: 100%;
|
||||
margin-left: 2.1rem;
|
||||
background-color: $border-color;
|
||||
content: "";
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
left: ($o-hrs-timeline-dot-size * .5 + o-to-rem(map-get($spacers, 4)) - o-to-rem($border-width))
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xxl) {
|
||||
left: ($o-hrs-timeline-dot-size * .5 + o-to-rem($o-horizontal-padding) - o-to-rem($border-width))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<div t-name="hr_skills.InternalResumeLineComponent" class="internalResume o_list_view o_field_x2many o_field_x2many_list">
|
||||
<div class="o_list_renderer o_renderer table-responsive" tabindex="-1">
|
||||
<table class="o_list_table table table-sm table-hover position-relative mb-0 o_list_table_ungrouped table-striped mb-3 overflow-hidden o_skill_table table-borderless">
|
||||
<thead style="visibility: collapse;">
|
||||
<tr>
|
||||
<th style="width: 32px; min-width: 32px;"></th>
|
||||
<th class="w-100"></th>
|
||||
<th style="width: 32px; min-width: 32px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="ui-sortable">
|
||||
<t t-if="internalResumeLines.length">
|
||||
<tr class="o_group_has_content o_group_header o_resume_group_header">
|
||||
<th tabindex="-1" class="o_group_name" colspan="2">
|
||||
<div class="d-flex align-items-center">
|
||||
Experience - <t t-out="companyId"/>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-foreach="internalResumeLines" t-as="record" t-key="record.id">
|
||||
<tr class="o_data_row">
|
||||
<td class="o_resume_timeline_cell position-relative pe-lg-2">
|
||||
<div class="rounded-circle bg-info position-relative"/>
|
||||
</td>
|
||||
<td class="o_data_cell pt-0">
|
||||
<div t-attf-class="o_resume_line" t-att-data-id="id">
|
||||
<small class="o_resume_line_dates fw-bold">
|
||||
<t t-out="formatDate(record.date_start)"/>
|
||||
-
|
||||
<t t-if="record.date_end" t-out="formatDate(record.date_end)"/>
|
||||
<t t-else="">
|
||||
Current
|
||||
</t>
|
||||
</small>
|
||||
<h4 class="o_resume_line_title mt-2" t-esc="record.job_title"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
<t t-if="!haveResumeLines">
|
||||
<div name="no_resume_line" class="ms-4 mt-3 text-muted fst-italic oe_inline">
|
||||
<p>
|
||||
There are no resume lines on this employee.
|
||||
</p>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { registry } from '@web/core/registry';
|
||||
import { ListBooleanToggleField, listBooleanToggleField } from "@web/views/fields/boolean_toggle/list_boolean_toggle_field";
|
||||
|
||||
export class ListBooleanToggleLoadField extends ListBooleanToggleField {
|
||||
async onChange(newValue) {
|
||||
this.state.value = newValue;
|
||||
// technical_is_new_default ensure to the backend which level trigger the onchange
|
||||
const changes = { [this.props.name]: newValue, technical_is_new_default: newValue };
|
||||
await this.props.record.update(changes, { save: this.props.autosave });
|
||||
}
|
||||
}
|
||||
|
||||
export const listBooleanToggleLoadField = {
|
||||
...listBooleanToggleField,
|
||||
component: ListBooleanToggleLoadField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("boolean_toggle_load", listBooleanToggleLoadField);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="hr_skills.X2ManyFieldDialog" t-inherit="web.X2ManyFieldDialog" t-inherit-mode="primary">
|
||||
<xpath expr="//button[hasclass('o_form_button_save')]" position="replace">
|
||||
<button class="btn btn-primary o_form_button_save" t-on-click="() => this.save()" data-hotkey="c">Select & Close</button>
|
||||
</xpath>
|
||||
<xpath expr="//button[hasclass('o_form_button_save_new')]" position="replace">
|
||||
<button class="btn btn-primary o_form_button_save_new" t-on-click="saveAndNew" data-hotkey="n">Select & New</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { Component } from "@odoo/owl";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { evaluateBooleanExpr } from "@web/core/py_js/py";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
export class FormattedDate extends Component {
|
||||
static template = "hr_skills.FormattedDate";
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
dayFormat: String,
|
||||
monthFormat: String,
|
||||
yearFormat: String,
|
||||
color: Object,
|
||||
};
|
||||
|
||||
get value() {
|
||||
return this.props.record.data[this.props.name];
|
||||
}
|
||||
|
||||
get placeholder() {
|
||||
return _t("Indefinite");
|
||||
}
|
||||
|
||||
get colorClass() {
|
||||
const colors = Object.keys(this.props.color);
|
||||
if (colors) {
|
||||
for (const colorName of colors) {
|
||||
if (evaluateBooleanExpr(`${this.props.color[colorName]}`, this.props.record.evalContextWithVirtualIds)) {
|
||||
return "text-" + colorName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export const formattedDate = {
|
||||
component: FormattedDate,
|
||||
supportedOptions: [
|
||||
{
|
||||
label: _t("Day Format"),
|
||||
name: "day_format",
|
||||
type: "string",
|
||||
default: "numeric",
|
||||
|
||||
},
|
||||
{
|
||||
label: _t("Month Format"),
|
||||
name: "month_format",
|
||||
type: "string",
|
||||
default: "numeric",
|
||||
},
|
||||
{
|
||||
label: _t("Year Format"),
|
||||
name: "year_format",
|
||||
type: "string",
|
||||
default: "numeric",
|
||||
},
|
||||
{
|
||||
label: _t("Color"),
|
||||
name: "color",
|
||||
type: "string",
|
||||
default: {},
|
||||
},
|
||||
],
|
||||
supportedTypes: ["date"],
|
||||
extractProps({ options }) {
|
||||
return {
|
||||
dayFormat: options.day_format || "numeric",
|
||||
monthFormat: options.month_format || "numeric",
|
||||
yearFormat: options.year_format || "numeric",
|
||||
color: options.color || {},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("formatted_date", formattedDate);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="hr_skills.FormattedDate">
|
||||
<span t-out="this.value.toLocaleString({day: this.props.dayFormat, month: this.props.monthFormat, year: this.props.yearFormat})" t-if="this.value" t-att-class="this.colorClass"/>
|
||||
<span t-else="" t-out="this.placeholder" class='text-muted opacity-50'/>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
|
||||
|
||||
export class SkillsTagList extends TagsList {
|
||||
static template = "hr_skills.SkillsTagsList";
|
||||
|
||||
getTextStyle(tag) {
|
||||
return tag.defaultLevel
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_skills.SkillsTagsList" t-inherit="web.TagsList">
|
||||
<xpath expr='//span[hasclass("o_tag")]' position="attributes">
|
||||
<attribute name="t-attf-class" add="{{ getTextStyle(tag) ? 'border border-2' : '' }}" separator=" "/>
|
||||
<attribute name="t-attf-style" add="{{ getTextStyle(tag) ? 'border-color: rgb(140, 140, 140) !important ;' : ''}}" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr='//div[hasclass("o_tag_badge_text")]' position="attributes">
|
||||
<attribute name="t-attf-class" add="{{ getTextStyle(tag) ? 'fw-bold' : '' }}" separator=" "/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
Many2ManyTagsField,
|
||||
many2ManyTagsField,
|
||||
} from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
|
||||
import { SkillsTagList } from "../hr_skills_tags_list/hr_skills_tags_list";
|
||||
|
||||
|
||||
class SkillsMany2ManyTags extends Many2ManyTagsField {
|
||||
static components = { ...Many2ManyTagsField.components, TagsList: SkillsTagList };
|
||||
getTagProps(record) {
|
||||
return { ...super.getTagProps(record), defaultLevel: record.data.default_level };
|
||||
}
|
||||
}
|
||||
|
||||
export const skillsMany2ManyTags = {
|
||||
...many2ManyTagsField,
|
||||
component: SkillsMany2ManyTags,
|
||||
relatedFields: (fieldInfo) => {
|
||||
return [
|
||||
...many2ManyTagsField.relatedFields(fieldInfo),
|
||||
{ name: "default_level", type: "boolean"},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2many_tags_skills", skillsMany2ManyTags);
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { useX2ManyCrud, useOpenX2ManyRecord } from "@web/views/fields/relational_utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
|
||||
export class One2ManyTagsSkillsField extends X2ManyField {
|
||||
static components = {
|
||||
...X2ManyField.components,
|
||||
TagsList,
|
||||
};
|
||||
static template = "hr_recruitment_skills.One2ManyTagsSkillsField";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
const { saveRecord, updateRecord } = useX2ManyCrud(() => this.list, this.isMany2Many);
|
||||
|
||||
const openRecord = useOpenX2ManyRecord({
|
||||
resModel: this.list.resModel,
|
||||
activeField: this.activeField,
|
||||
activeActions: this.activeActions,
|
||||
getList: () => this.list,
|
||||
saveRecord: saveRecord,
|
||||
updateRecord: updateRecord,
|
||||
withParentId: this.props.widget !== "many2many",
|
||||
});
|
||||
|
||||
this._openRecord = (params) => {
|
||||
params.title = _t("Select Skills");
|
||||
openRecord({ ...params });
|
||||
};
|
||||
}
|
||||
|
||||
getTagProps(record) {
|
||||
const tagProps = {
|
||||
id: record.id,
|
||||
resId: record.resId,
|
||||
text: record.data.display_name,
|
||||
colorIndex: record.data.color,
|
||||
canEdit: true,
|
||||
onClick: (ev) => this.onTagClick(ev, record),
|
||||
onDelete: !this.props.readonly ? () => this.activeActions.onDelete(record) : undefined,
|
||||
};
|
||||
return tagProps;
|
||||
}
|
||||
|
||||
get tags() {
|
||||
return this.props.record.data[this.props.name].records.map((record) =>
|
||||
this.getTagProps(record)
|
||||
);
|
||||
}
|
||||
|
||||
onTagClick(ev, record) {
|
||||
this.openRecord(record);
|
||||
}
|
||||
}
|
||||
|
||||
export const one2ManyTagsSkillsField = {
|
||||
...x2ManyField,
|
||||
component: One2ManyTagsSkillsField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2one_tags_skills", one2ManyTagsSkillsField);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.o_field_many2one_tags_skills a.o_delete {
|
||||
color: inherit;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_recruitment_skills.One2ManyTagsSkillsField">
|
||||
<div class="d-flex align-items-baseline"
|
||||
t-attf-class="{{ tags.length && 'gap-1'}}">
|
||||
<div class=" d-inline-flex flex-wrap gap-1 mw-100">
|
||||
<TagsList tags="tags" />
|
||||
</div>
|
||||
<button t-on-click="onAdd"
|
||||
class="btn btn-link text-action"
|
||||
role="button"
|
||||
style="padding: 0; line-height: 0; height: min-content;">
|
||||
<i class="fa fa-plus text-info" />
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { formatDate } from "@web/core/l10n/dates";
|
||||
|
||||
import { SkillsX2ManyField } from "./skills_one2many";
|
||||
import { CommonSkillsListRenderer } from "../views/skills_list_renderer";
|
||||
|
||||
export class ResumeListRenderer extends CommonSkillsListRenderer {
|
||||
get groupBy() {
|
||||
return 'line_type_id';
|
||||
}
|
||||
|
||||
get colspan() {
|
||||
if (this.props.activeActions) {
|
||||
return 3;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
return formatDate(date);
|
||||
}
|
||||
|
||||
setDefaultColumnWidths() {}
|
||||
}
|
||||
ResumeListRenderer.template = 'hr_skills.ResumeListRenderer';
|
||||
ResumeListRenderer.rowsTemplate = "hr_skills.ResumeListRenderer.Rows";
|
||||
ResumeListRenderer.recordRowTemplate = "hr_skills.ResumeListRenderer.RecordRow";
|
||||
|
||||
|
||||
export class ResumeX2ManyField extends SkillsX2ManyField {}
|
||||
ResumeX2ManyField.components = {
|
||||
...SkillsX2ManyField.components,
|
||||
ListRenderer: ResumeListRenderer,
|
||||
};
|
||||
|
||||
registry.category("fields")
|
||||
.add("resume_one2many", ResumeX2ManyField);
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<t t-name="hr_skills.ResumeListRenderer" owl="1" t-inherit-mode="primary" t-inherit="hr_skills.SkillsListRenderer">
|
||||
<xpath expr="//table" position="attributes">
|
||||
<attribute name="t-attf-class" add="table-borderless {{ !showTable ? 'd-none' : ''}}" remove="table-striped" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//thead/tr" position="replace">
|
||||
<tr>
|
||||
<th style="width: 32px; min-width: 32px;"></th>
|
||||
<th class="w-100"></th>
|
||||
<th t-if="isEditable" class="o_list_actions_header" style="width: 32px; min-width: 32px"></th>
|
||||
</tr>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_skills.ResumeListRenderer.Rows" owl="1" t-inherit-mode="primary" t-inherit="hr_skills.SkillsListRenderer.Rows">
|
||||
<xpath expr="//tr" position="attributes">
|
||||
<attribute name="class" add="o_resume_group_header" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//th[hasclass('o_group_name')]" position="after">
|
||||
<th></th>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_skills.ResumeListRenderer.RecordRow" owl="1" t-inherit-mode="primary" t-inherit="web.ListRenderer.RecordRow">
|
||||
<xpath expr="//t[@t-foreach='getColumns(record)']" position="replace">
|
||||
<t t-set="data" t-value="record.data"/>
|
||||
<t t-if="data.display_type === 'classic'" id='row'>
|
||||
<td class="o_resume_timeline_cell position-relative pe-lg-2" id='hiii'>
|
||||
<div class="rounded-circle bg-info position-relative"/>
|
||||
</td>
|
||||
<td class="o_data_cell pt-0" t-on-click="(ev) => this.onCellClicked(record, null, ev)">
|
||||
<div t-attf-class="o_resume_line {{data.display_type == 'certification' ? 'o_resume_line_display_certification' : ''}}" t-att-data-id="id">
|
||||
<small class="o_resume_line_dates fw-bold">
|
||||
<t t-out="formatDate(data.date_start)"/> -
|
||||
<t t-if="data.date_end" t-out="formatDate(data.date_end)"/>
|
||||
<t t-else="">Current</t>
|
||||
</small>
|
||||
<h4 class="o_resume_line_title mt-2" t-esc="data.name"/>
|
||||
<p t-if="data.description" class="o_resume_line_desc" t-out="data.description" t-ref="link-target-blank"/>
|
||||
</div>
|
||||
</td>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { onMounted, onPatched, useRef } from "@odoo/owl";
|
||||
|
||||
import { formatDate } from "@web/core/l10n/dates";
|
||||
|
||||
import { SkillsX2ManyField, skillsX2ManyField } from "../skills_one2many/skills_one2many";
|
||||
import { CommonSkillsListRenderer } from "../../views/skills_list_renderer";
|
||||
|
||||
export class ResumeListRenderer extends CommonSkillsListRenderer {
|
||||
static template = "hr_skills.ResumeListRenderer";
|
||||
static rowsTemplate = "hr_skills.ResumeListRenderer.Rows";
|
||||
static recordRowTemplate = "hr_skills.ResumeListRenderer.RecordRow";
|
||||
static useMagicColumnWidths = false;
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.linkRef = useRef("link-target-blank");
|
||||
onMounted(this._setLinksToOpenInNewTab);
|
||||
onPatched(this._setLinksToOpenInNewTab);
|
||||
}
|
||||
get groupBy() {
|
||||
return "line_type_id";
|
||||
}
|
||||
|
||||
get colspan() {
|
||||
if (this.props.activeActions) {
|
||||
return 3;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
return formatDate(date);
|
||||
}
|
||||
|
||||
_setLinksToOpenInNewTab() {
|
||||
const resumeLines = this.linkRef.el;
|
||||
|
||||
// Find all links within the resume description and set target to "_blank"
|
||||
if (resumeLines){
|
||||
const links = resumeLines.querySelectorAll('a');
|
||||
|
||||
links.forEach(link => {
|
||||
link.setAttribute('target', '_blank'); // Set target="_blank" to open links in new tab
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ResumeX2ManyField extends SkillsX2ManyField {
|
||||
static components = {
|
||||
...SkillsX2ManyField.components,
|
||||
ListRenderer: ResumeListRenderer,
|
||||
};
|
||||
getWizardTitleName() {
|
||||
return _t("New Resume Line");
|
||||
}
|
||||
}
|
||||
|
||||
export const resumeX2ManyField = {
|
||||
...skillsX2ManyField,
|
||||
component: ResumeX2ManyField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("resume_one2many", resumeX2ManyField);
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
.o_field_resume_one2many {
|
||||
$o-hrs-timeline-entry-padding: .5rem;
|
||||
$o-hrs-timeline-dot-size: .6rem;
|
||||
|
||||
.o_data_row {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.o_data_row td {
|
||||
padding: $o-hrs-timeline-entry-padding;
|
||||
|
||||
&.o_resume_timeline_cell {
|
||||
overflow: hidden !important;
|
||||
div {
|
||||
width: $o-hrs-timeline-dot-size;
|
||||
height: $o-hrs-timeline-dot-size;
|
||||
}
|
||||
|
||||
&:before {
|
||||
@include o-position-absolute(0, $left: ($o-hrs-timeline-dot-size * .5 + o-to-rem($o-horizontal-padding) - o-to-rem($border-width)));
|
||||
width: $border-width;
|
||||
height: 100%;
|
||||
background-color: $border-color;
|
||||
content: "";
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
left: ($o-hrs-timeline-dot-size * .5 + o-to-rem(map-get($spacers, 4)) - o-to-rem($border-width))
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xxl) {
|
||||
left: ($o-hrs-timeline-dot-size * .5 + o-to-rem($o-horizontal-padding) - o-to-rem($border-width))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_resume_line_desc {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.o_resume_line_title, .o_resume_line_desc {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.o_resume_line_title, .o_resume_line_dates {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.o_resume_group_header + .o_data_row .o_resume_timeline_cell:before {
|
||||
top: $o-hrs-timeline-entry-padding;
|
||||
}
|
||||
|
||||
.o_data_row.o_data_row_last {
|
||||
.o_resume_line_desc {
|
||||
margin-bottom: $headings-margin-bottom;
|
||||
}
|
||||
|
||||
.o_resume_timeline_cell:before {
|
||||
height: $o-hrs-timeline-entry-padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<t t-name="hr_skills.ResumeListRenderer" t-inherit-mode="primary" t-inherit="hr_skills.SkillsListRenderer">
|
||||
<xpath expr="//table" position="attributes">
|
||||
<attribute name="t-attf-class" add="table-borderless {{ !showTable ? 'd-none' : ''}}" remove="table-striped" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//table" position="after">
|
||||
<t t-if="!showTable">
|
||||
<button t-on-click="props.onAdd" class="btn btn-secondary ms-4 mt-3" role="button" t-if="isEditable">
|
||||
Create Resume Lines
|
||||
</button>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='skills_header']" position="replace"/>
|
||||
<xpath expr="//thead/tr" position="replace">
|
||||
<tr>
|
||||
<th style="width: 32px; min-width: 32px;"></th>
|
||||
<th class="w-100"></th>
|
||||
<th t-if="isEditable" class="o_list_actions_header" style="width: 32px; min-width: 32px"></th>
|
||||
</tr>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='no_skills']" position="replace"/>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_skills.ResumeListRenderer.Rows" t-inherit-mode="primary" t-inherit="hr_skills.SkillsListRenderer.Rows">
|
||||
<xpath expr="//tr" position="attributes">
|
||||
<attribute name="class" add="o_resume_group_header" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//th[hasclass('o_group_name')]" position="after">
|
||||
<th></th>
|
||||
</xpath>
|
||||
<xpath expr="//button[@id='add_button']" position="attributes">
|
||||
<attribute name="t-on-click">() => props.onAdd({ context: { default_line_type_id: skill_group[1].id }})</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_skills.ResumeListRenderer.RecordRow" t-inherit-mode="primary" t-inherit="web.ListRenderer.RecordRow">
|
||||
<xpath expr="//t[@t-foreach='getColumns(record)']" position="replace">
|
||||
<t t-set="data" t-value="record.data"/>
|
||||
<td class="o_resume_timeline_cell position-relative pe-lg-2">
|
||||
<div class="rounded-circle bg-info position-relative"/>
|
||||
</td>
|
||||
<td class="o_data_cell pt-0" t-on-click="(ev) => this.onCellClicked(record, null, ev)">
|
||||
<div t-attf-class="o_resume_line" t-att-data-id="id">
|
||||
<small class="o_resume_line_dates fw-bold">
|
||||
<t t-out="formatDate(data.date_start)"/>
|
||||
<t t-if="!data.is_course">
|
||||
-
|
||||
<t t-if="data.date_end" t-out="formatDate(data.date_end)"/>
|
||||
<t t-else="">
|
||||
Current
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="data.is_course && data.duration > 0">
|
||||
(<t t-out="data.duration"/> hours)
|
||||
</t>
|
||||
</small>
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="o_resume_line_title mt-2 me-2" t-esc="data.name"/>
|
||||
<t t-if="data.is_course">
|
||||
<a id="external_link" t-if="data.external_url" t-attf-href="#{data.external_url}" target="_blank" class="ms-2 fa fa-external-link btn-secondary" style="font-size: 1rem;"/>
|
||||
</t>
|
||||
</div>
|
||||
<p t-if="data.description" class="o_resume_line_desc" t-out="data.description" t-ref="link-target-blank"/>
|
||||
</div>
|
||||
</td>
|
||||
</xpath>
|
||||
</t>
|
||||
</odoo>
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { CommonSkillsListRenderer } from "../views/skills_list_renderer";
|
||||
|
||||
|
||||
export class SkillsListRenderer extends CommonSkillsListRenderer {
|
||||
get groupBy() {
|
||||
return 'skill_type_id';
|
||||
}
|
||||
|
||||
calculateColumnWidth(column) {
|
||||
if (column.name != 'skill_level_id') {
|
||||
return {
|
||||
type: 'absolute',
|
||||
value: '90px',
|
||||
}
|
||||
}
|
||||
|
||||
return super.calculateColumnWidth(column);
|
||||
}
|
||||
}
|
||||
SkillsListRenderer.template = 'hr_skills.SkillsListRenderer';
|
||||
|
||||
export class SkillsX2ManyField extends X2ManyField {
|
||||
async onAdd({ context, editable } = {}) {
|
||||
const employeeId = this.props.record.resId;
|
||||
return super.onAdd({
|
||||
editable,
|
||||
context: {
|
||||
...context,
|
||||
default_employee_id: employeeId,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
SkillsX2ManyField.components = {
|
||||
...X2ManyField.components,
|
||||
ListRenderer: SkillsListRenderer,
|
||||
};
|
||||
|
||||
registry.category("fields").add("skills_one2many", SkillsX2ManyField);
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
.o_field_skills_one2many, .o_field_resume_one2many {
|
||||
.o_progress {
|
||||
background: $gray-300;
|
||||
border: 0;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.o_progressbar_value {
|
||||
font-size: $font-size-sm;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
table.o_skill_table, .o_hr_skills_dialog_form {
|
||||
.o_progressbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.o_progressbar_value input {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<t t-name="hr_skills.SkillsListRenderer" owl="1" t-inherit-mode="primary" t-inherit="web.ListRenderer">
|
||||
<xpath expr="//table" position="attributes">
|
||||
<attribute name="t-attf-class" add="mb-1 {{ !isEditable ? 'cursor-default' : '' }} {{ !showTable ? 'd-none' : ''}} o_skill_table" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//thead" position="attributes">
|
||||
<attribute name="style">visibility: collapse;</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//table" position="after">
|
||||
<t t-if="!showTable">
|
||||
<button t-on-click="props.onAdd" class="btn btn-secondary ms-4 mt-3" role="button" t-if="isEditable">
|
||||
Create a new entry
|
||||
</button>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_skills.SkillsListRenderer.Rows" owl="1">
|
||||
<t t-foreach="Object.entries(groupedList)" t-as="skill_group" t-key="skill_group[0]">
|
||||
<tr class="o_group_has_content o_group_header">
|
||||
<th tabindex="-1" class="o_group_name" t-att-colspan="colspan">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span t-esc="skill_group[1].name"/>
|
||||
<button class="btn btn-secondary btn-sm"
|
||||
t-if="isEditable"
|
||||
t-on-click="() => props.onAdd({ context: { default_skill_type_id: skill_group[1].id }})"
|
||||
role="button">ADD</button>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<t t-foreach="skill_group[1].list.records" t-as="record" t-key="record.id">
|
||||
<t t-set="group" t-value="skill_group[1]"/>
|
||||
<t t-call="{{ constructor.recordRowTemplate }}"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { useX2ManyCrud, useOpenX2ManyRecord } from "@web/views/fields/relational_utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { user } from "@web/core/user";
|
||||
import { CommonSkillsListRenderer } from "../../views/skills_list_renderer";
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { onWillStart } from "@odoo/owl";
|
||||
|
||||
|
||||
export class SkillsListRenderer extends CommonSkillsListRenderer {
|
||||
static template = "hr_skills.SkillsListRenderer";
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService('orm');
|
||||
this.actionService = useService("action");
|
||||
|
||||
onWillStart(async () => {
|
||||
const res = await this.orm.searchCount('hr.skill.type', []);
|
||||
this.anySkills = res > 0;
|
||||
[this.user] = await this.orm.read("res.users", [user.userId], ["employee_ids"]);
|
||||
this.IsHrUser = await user.hasGroup("hr.group_hr_user");
|
||||
this.userSubordinates = (await this.orm.searchRead(
|
||||
"hr.employee",
|
||||
[["id", "child_of", this.user.employee_ids]],
|
||||
["id"]
|
||||
)).map((record) => record["id"]);
|
||||
});
|
||||
}
|
||||
|
||||
get groupBy() {
|
||||
return 'skill_type_id';
|
||||
}
|
||||
|
||||
async skillTypesAction() {
|
||||
return this.actionService.doAction("hr_skills.hr_skill_type_action");
|
||||
}
|
||||
|
||||
async openSkillsReport() {
|
||||
// fetch id through employee or public.employee
|
||||
const id = this.env.model.root.data.id || this.env.model.root.data.employee_id.id;
|
||||
this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: _t("Skills Report"),
|
||||
res_model: "hr.employee.skill.history.report",
|
||||
view_mode: "graph,list",
|
||||
views: [[false, "graph"]],
|
||||
context: {
|
||||
'fill_temporal': false,
|
||||
},
|
||||
target: "current",
|
||||
domain: [['employee_id', '=', id]],
|
||||
});
|
||||
}
|
||||
|
||||
get showTimeline() {
|
||||
return this.SkillsRight && !this.props.list.context.no_timeline;
|
||||
}
|
||||
|
||||
get SkillsRight() {
|
||||
let isSubordinate = false;
|
||||
if (this.env.model.root.data.employee_id) {
|
||||
isSubordinate = this.userSubordinates.includes(this.env.model.root.data.employee_id.id);
|
||||
}
|
||||
return this.IsHrUser || isSubordinate;
|
||||
}
|
||||
}
|
||||
|
||||
export class SkillsX2ManyField extends X2ManyField {
|
||||
static components = {
|
||||
...X2ManyField.components,
|
||||
ListRenderer: SkillsListRenderer,
|
||||
};
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService('orm');
|
||||
this.actionService = useService('action');
|
||||
|
||||
const { saveRecord, updateRecord } = useX2ManyCrud(
|
||||
() => this.list,
|
||||
this.isMany2Many
|
||||
);
|
||||
|
||||
const openRecord = useOpenX2ManyRecord({
|
||||
resModel: this.list.resModel,
|
||||
activeField: this.activeField,
|
||||
activeActions: this.activeActions,
|
||||
getList: () => this.list,
|
||||
saveRecord: saveRecord,
|
||||
updateRecord: updateRecord,
|
||||
withParentId: this.props.widget !== "many2many",
|
||||
});
|
||||
|
||||
this._openRecord = (params) => {
|
||||
params.title = this.getWizardTitleName();
|
||||
openRecord({...params});
|
||||
};
|
||||
}
|
||||
|
||||
getWizardTitleName() {
|
||||
return _t("Update Skills")
|
||||
}
|
||||
|
||||
async onAdd({ context, editable } = {}) {
|
||||
const employeeId = this.props.record.resModel === "res.users" ? this.props.record.data.employee_id.id : this.props.record.resId;
|
||||
return super.onAdd({
|
||||
editable,
|
||||
context: {
|
||||
...context,
|
||||
default_employee_id: employeeId,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const skillsX2ManyField = {
|
||||
...x2ManyField,
|
||||
component: SkillsX2ManyField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("skills_one2many", skillsX2ManyField);
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
.o_field_skills_one2many, .o_field_resume_one2many {
|
||||
.o_progress {
|
||||
background: $gray-300;
|
||||
border: 0;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.o_progressbar_value {
|
||||
font-size: $font-size-sm;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
table.o_skill_table {
|
||||
.o_hr_skills_dialog_form {
|
||||
.o_progressbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.o_progressbar_value input {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.skills_header {
|
||||
padding-inline: var(--ListRenderer-table-padding-x);
|
||||
}
|
||||
|
||||
.o_skill_table thead {
|
||||
/* This is to remove the top row with the column names from most displays of the skill table */
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
.o_skill_table:not(.o_group_resume .o_skill_table) > thead {
|
||||
/* This is needed because the border is rendered on chrome by default but not firefox but we also */
|
||||
/* don't want to render any extra borders for the resume display */
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.o_employee_skills .skills_header {
|
||||
/* Margin and padding classes are added to .skills_header to work in both HR and Recruitment contexts. */
|
||||
/* However, the combined spacing is too much for either scenario, so one is removed depending on context. */
|
||||
padding-block: 0 !important;
|
||||
}
|
||||
|
||||
.o_employee_skills .o_list_renderer {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.skills_helper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
padding-inline: var(--ListRenderer-table-padding-x);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<t t-name="hr_skills.SkillsListRenderer" t-inherit-mode="primary" t-inherit="web.ListRenderer">
|
||||
<xpath expr="//table" position="attributes">
|
||||
<attribute name="t-attf-class" add="mb-1 {{ !isEditable ? 'cursor-default' : '' }} {{ !showTable ? 'd-none' : ''}} o_skill_table overflow-hidden" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//table" position="before">
|
||||
<div name="skills_header" class="skills_header text-uppercase small fw-bolder py-2 mt-4" t-att-class="{'o_horizontal_separator': !showTable }">
|
||||
Skills & Certifications
|
||||
<a t-if="!isEmpty && showTimeline" t-on-click="openSkillsReport" class="float-end cursor-pointer">
|
||||
<span class="fa fa-line-chart me-1"/>
|
||||
Timeline
|
||||
</a>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//table" position="after">
|
||||
<t t-if="!showTable">
|
||||
<t t-if="!anySkills">
|
||||
<div name="no_skills" class="skills_helper">
|
||||
<p class="mt-2">
|
||||
There are no skills defined in the library.
|
||||
</p>
|
||||
<button t-on-click="skillTypesAction"
|
||||
class="btn btn-secondary align-self-center"
|
||||
role="button" t-if="isEditable">
|
||||
Create Skills
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div name="skills_available" class="skills_helper text-muted fst-italic">
|
||||
<p class="mt-2">You can add skills from our library to the employee profile.<br/>
|
||||
If skills are missing, they can be created by an HR officer.
|
||||
</p>
|
||||
<button t-on-click="props.onAdd"
|
||||
class="btn btn-secondary align-self-center"
|
||||
role="button" t-if="isEditable">
|
||||
Pick a skill from the list
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_skills.SkillsListRenderer.Rows">
|
||||
<t t-foreach="Object.entries(groupedList)" t-as="skill_group" t-key="skill_group[0]">
|
||||
<tr class="o_group_has_content o_group_header">
|
||||
<th tabindex="-1" class="o_group_name" t-att-colspan="colspan">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span t-esc="skill_group[1].name"/>
|
||||
<button id="add_button" class="btn btn-secondary btn-sm"
|
||||
t-if="isEditable"
|
||||
t-on-click="() => props.onAdd({ context: { default_skill_type_id: skill_group[1].id }})"
|
||||
role="button">ADD</button>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<t t-foreach="skill_group[1].list.records" t-as="record" t-key="record.id">
|
||||
<t t-set="group" t-value="skill_group[1]"/>
|
||||
<t t-call="{{ constructor.recordRowTemplate }}"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</odoo>
|
||||
|
|
@ -1,12 +1,35 @@
|
|||
.o_hr_skills_group {
|
||||
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_list_renderer {
|
||||
overflow-x: hidden!important;
|
||||
.o_hr_skill_type_form {
|
||||
th[data-name="level_progress"] div span {
|
||||
text-align: left !important;
|
||||
}
|
||||
}
|
||||
|
||||
.o_list_renderer {
|
||||
.o_list_table {
|
||||
.o_data_row:not(.o_selected_row) .o_data_cell {
|
||||
&.o_boolean_toggle_load_cell {
|
||||
> .o_field_widget:not(.o_readonly_modifier) .form-check {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hide expand button when the form view is readonly
|
||||
.o_hr_skills_dialog_form:has(main > div.o_form_readonly) > header > button.o_expand_button {
|
||||
display: None !important;
|
||||
}
|
||||
|
||||
.o_skill_level_tree{
|
||||
.o_list_renderer .o_list_table thead .o_list_number_th{
|
||||
text-align: start !important;
|
||||
}
|
||||
.o_progressbar_value .o_input {
|
||||
width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.o_resume_html .note-editable {
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,246 @@
|
|||
$text-color: #16171a;
|
||||
$text-color-secondary: #16171a;
|
||||
$text-muted: #3b4757;
|
||||
$progess-bar-background: #f5f5f5;
|
||||
|
||||
div.o_employee_cv {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
color: $text-color-secondary;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&:not(:last-child) .o_new_page {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.o_sidebar {
|
||||
color: #fff;
|
||||
margin-top: -150px;
|
||||
margin-left: 500px;
|
||||
width: inherit;
|
||||
display: inline-block;
|
||||
max-width: 50%;
|
||||
min-width: 50%;
|
||||
box-sizing: border-box;
|
||||
vertical-align: top;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.o_profile {
|
||||
padding: 30px;
|
||||
padding-top: 15px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.o_profile_name {
|
||||
font-size: 32px;
|
||||
font-weight: bolder;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.o_profile_job {
|
||||
color: rgba(256, 256, 256, 0.9);
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.o_profile_image {
|
||||
margin-bottom: 15px;
|
||||
width: 100px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.o_social {
|
||||
list-style-type: none;
|
||||
li {
|
||||
margin-bottom: 15px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_sidebar_section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.o_sidebar_title {
|
||||
text-transform: uppercase;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.o_sidebar_education {
|
||||
.o_sidebar_education_degree {
|
||||
font-size: 14px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.o_sidebar_education_line {
|
||||
margin-bottom: 15px;
|
||||
overflow-wrap: break-word;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_sidebar_education_description {
|
||||
color: rgba(256, 256, 256, 0.6);
|
||||
font-weight: bold;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.o_sidebar_education_description_year {
|
||||
color: rgba(256, 256, 256, 0.6);
|
||||
font-weight: bold;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_sidebar_language {
|
||||
.o_sidebar_language_level {
|
||||
color: rgba(256, 256, 256, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.o_sidebar_list {
|
||||
margin-bottom: 0;
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_company {
|
||||
width: inherit;
|
||||
display: inline-block;
|
||||
max-width: 49%;
|
||||
min-width: 49%;
|
||||
box-sizing: border-box;
|
||||
vertical-align: top;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.o_main_panel {
|
||||
background: #fff;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
display: inline-block;
|
||||
padding: 5px 30px 0 30px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: top;
|
||||
|
||||
.o_main_panel_title {
|
||||
text-transform: uppercase;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.o_main_panel_icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 8px;
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
background-clip: padding-box;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
padding-top: 2px;
|
||||
fa {
|
||||
font-size: 14px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.o_main_panel_resume_title {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
.o_main_panel_resume_title_job {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.o_main_panel_resume_title_dates {
|
||||
float: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.o_main_panel_resume_job {
|
||||
color: $text-color;
|
||||
font-size: 16px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.o_main_panel_resume_year {
|
||||
right: 0;
|
||||
top: 0;
|
||||
color: $text-muted;
|
||||
position: static;
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.o_main_panel_skills {
|
||||
.o_main_panel_skill_line {
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_main_panel_skill_name {
|
||||
font-size: 14px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
|
||||
.o_main_panel_skill_progress_bar {
|
||||
height: 12px;
|
||||
background: $progess-bar-background;
|
||||
border-radius: 2px;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { GraphRenderer } from "@web/views/graph/graph_renderer";
|
||||
import { graphView } from "@web/views/graph/graph_view";
|
||||
|
|
@ -8,8 +6,8 @@ export class SkillsGraphRenderer extends GraphRenderer {
|
|||
getScaleOptions() {
|
||||
const scaleOptions = super.getScaleOptions();
|
||||
|
||||
if ('yAxes' in scaleOptions) {
|
||||
scaleOptions['yAxes'][0]['ticks']['suggestedMax'] = 100;
|
||||
if ('y' in scaleOptions) {
|
||||
scaleOptions.y.suggestedMax = 100;
|
||||
}
|
||||
|
||||
return scaleOptions;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
|
||||
export class CommonSkillsListRenderer extends ListRenderer {
|
||||
|
|
@ -23,17 +22,17 @@ export class CommonSkillsListRenderer extends ListRenderer {
|
|||
const data = record.data;
|
||||
const group = data[this.groupBy];
|
||||
|
||||
if (grouped[group[1]] === undefined) {
|
||||
grouped[group[1]] = {
|
||||
id: parseInt(group[0]),
|
||||
name: group[1] || this.env._t('Other'),
|
||||
if (grouped[group.display_name] === undefined) {
|
||||
grouped[group.display_name] = {
|
||||
id: parseInt(group.id),
|
||||
name: group.display_name || _t('Other'),
|
||||
list: {
|
||||
records: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
grouped[group[1]].list.records.push(record);
|
||||
grouped[group.display_name].list.records.push(record);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="hr_resume_data_row">
|
||||
<tr class="o_data_row cursor-default" t-attf-class="o_data_row #{is_last? 'o_data_row_last' : ''}" t-att-data-id="id">
|
||||
<t t-if="data.display_type === 'classic'">
|
||||
<td class="o_resume_timeline_cell position-relative pe-lg-2">
|
||||
<div class="rounded-circle bg-info position-relative"/>
|
||||
</td>
|
||||
<td class="o_data_cell pt-0 w-100">
|
||||
<div class="o_resume_line" t-att-data-id="id">
|
||||
<small class="o_resume_line_dates">
|
||||
<b t-esc="data.date_start"/> - <b t-esc="data.date_end"/>
|
||||
</small>
|
||||
<h4 class="o_resume_line_title mt-2" t-esc="data.name"/>
|
||||
<p t-if="data.description" class="o_resume_line_desc" t-esc="data.description"/>
|
||||
</div>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_trash_button">
|
||||
<td class="o_list_record_remove pe-3">
|
||||
<button name="delete" arial-label="Delete row" class="btn btn-link text-danger">
|
||||
<i class="fa fa-trash"/>
|
||||
</button>
|
||||
</td>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_resume_group_row">
|
||||
<tr class="o_resume_group_header">
|
||||
<td class="o_group_name" colspan="100%"><span class="o_horizontal_separator my-0" t-esc="display_name"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
<t t-name="group_add_item">
|
||||
<t t-set="empty" t-value="Object.keys(context).length == 2"/>
|
||||
|
||||
<div t-attf-class="o_field_x2many_list_row_add #{empty? 'd-block w-100' : 'd-inline float-end'}">
|
||||
<a href="#"
|
||||
role="button"
|
||||
t-attf-class="btn btn-secondary o-kanban-button-new #{empty? 'btn-primary mt-3' : 'btn-secondary btn-sm'}"
|
||||
t-attf-data-context="{{ context }}">
|
||||
<t t-if="empty">CREATE A NEW ENTRY</t>
|
||||
<t t-else="">ADD</t>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="hr_skill_data_row">
|
||||
<tr class="o_data_row cursor-default" t-att-data-id="id">
|
||||
<td class="o_data_cell o_skill_cell w-100">
|
||||
<t t-esc="data.skill_id.data.display_name"/>
|
||||
</td>
|
||||
<td class="o_data_cell o_skill_cell pe-3">
|
||||
<t t-esc="data.skill_level_id.data.display_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_default_group_row">
|
||||
<tr class="o_group_header o_group_has_content">
|
||||
<td class="o_group_name border-0 pe-2" colspan="99">
|
||||
<b t-esc="display_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { hrModels } from "@hr/../tests/hr_test_helpers";
|
||||
import { defineModels } from "@web/../tests/web_test_helpers";
|
||||
import { HrEmployeeSkill } from "./mock_server/mock_models/hr_employee_skill";
|
||||
import { HrSkill } from "./mock_server/mock_models/hr_skill";
|
||||
import { M2oAvatarEmployee } from "./mock_server/mock_models/m2o_avatar_employee";
|
||||
|
||||
export function defineHrSkillModels() {
|
||||
return defineModels(hrSkillModels);
|
||||
}
|
||||
|
||||
export const hrSkillModels = {
|
||||
...hrModels,
|
||||
HrEmployeeSkill,
|
||||
HrSkill,
|
||||
M2oAvatarEmployee,
|
||||
};
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { click, contains, start, startServer } from "@mail/../tests/mail_test_helpers";
|
||||
import { mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { queryAttribute } from "@odoo/hoot-dom";
|
||||
import { defineHrSkillModels } from "@hr_skills/../tests/hr_skills_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineHrSkillModels();
|
||||
|
||||
test("many2one_avatar_employee widget in kanban view with skills on avatar card", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [java, tigrinya] = pyEnv["hr.skill"].create([{ name: "Java" }, { name: "Tigrinya" }]);
|
||||
const pierrePid = pyEnv["res.partner"].create({ name: "Pierre" });
|
||||
const pierreUid = pyEnv["res.users"].create({ name: "Pierre", partner_id: pierrePid });
|
||||
const pierreEid = pyEnv["hr.employee"].create({
|
||||
name: "Pierre",
|
||||
user_id: pierreUid,
|
||||
user_partner_id: pierrePid,
|
||||
});
|
||||
const [javaForPierre, tigrinyaForPierre] = pyEnv["hr.employee.skill"].create([
|
||||
{ employee_id: pierreEid, skill_id: java },
|
||||
{ employee_id: pierreEid, skill_id: tigrinya },
|
||||
]);
|
||||
pyEnv["hr.employee.public"].create({
|
||||
name: "Pierre",
|
||||
employee_skill_ids: [javaForPierre, tigrinyaForPierre],
|
||||
});
|
||||
pyEnv["m2o.avatar.employee"].create([{ employee_id: pierreEid }]);
|
||||
await start();
|
||||
|
||||
onRpc("hr.employee", "get_avatar_card_data", (params) => {
|
||||
const resourceIdArray = params.args[0];
|
||||
const resourceId = resourceIdArray[0];
|
||||
const resources = pyEnv['hr.employee.public'].read([resourceId]);
|
||||
const result = resources.map(resource => ({
|
||||
name: resource.name,
|
||||
role_ids: resource.role_ids,
|
||||
email:resource.email,
|
||||
phone: resource.phone,
|
||||
user_id: resource.user_id,
|
||||
employee_skill_ids: resource.employee_skill_ids
|
||||
}));
|
||||
return result;
|
||||
});
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "m2o.avatar.employee",
|
||||
arch: `<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="employee_id" widget="many2one_avatar_employee"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
await contains(".o_m2o_avatar", { count: 1 });
|
||||
await contains(".o_field_many2one_avatar_employee img", { count: 1 });
|
||||
expect(
|
||||
queryAttribute(".o_kanban_record .o_field_many2one_avatar_employee img", "data-src")
|
||||
).toBe(`/web/image/hr.employee/${pierreEid}/avatar_128`);
|
||||
await click(".o_kanban_record .o_m2o_avatar > img");
|
||||
await contains(".o_avatar_card");
|
||||
await contains(".o_avatar_card .o_employee_skills_tags > .o_tag", { count: 2 });
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class HrEmployeeSkill extends models.ServerModel {
|
||||
_name = "hr.employee.skill";
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class HrSkill extends models.ServerModel {
|
||||
_name = "hr.skill";
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class M2oAvatarEmployee extends models.Model {
|
||||
_name = "m2o.avatar.employee";
|
||||
|
||||
employee_id = fields.Many2one({ string: "Employee", relation: "hr.employee" });
|
||||
}
|
||||
|
|
@ -1,147 +1,169 @@
|
|||
/** @odoo-module **/
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('hr_skills_tour', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
}, [
|
||||
tour.stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
content: "Open Employees app",
|
||||
trigger: ".o_app[data-menu-xmlid='hr.menu_hr_root']",
|
||||
},
|
||||
{
|
||||
content: "Create a new employee",
|
||||
trigger: ".o-kanban-button-new",
|
||||
},
|
||||
{
|
||||
content: "Pick a name",
|
||||
trigger: ".o_field_widget[name='name'] input",
|
||||
run: "text Jony McHallyFace",
|
||||
},
|
||||
{
|
||||
content: "Save",
|
||||
trigger: ".o_form_button_save",
|
||||
},
|
||||
{
|
||||
content: "Add a new Resume experience",
|
||||
trigger: ".o_field_resume_one2many tr.o_resume_group_header button.btn-secondary",
|
||||
},
|
||||
{
|
||||
content: "Enter some company name",
|
||||
trigger: ".modal-body .o_field_widget[name='name'] input",
|
||||
run: "text Mamie Rock",
|
||||
},
|
||||
{
|
||||
content: "Set start date",
|
||||
trigger: ".o_field_widget[name='date_start'] input",
|
||||
run: "text 12/05/2017",
|
||||
},
|
||||
{
|
||||
content: "Give some description",
|
||||
trigger: ".o_field_widget[name='description'] textarea",
|
||||
run: "text Sang some songs and played some music",
|
||||
},
|
||||
{
|
||||
content: "Save it",
|
||||
trigger: ".o_form_button_save",
|
||||
in_modal: true,
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Edit newly created experience",
|
||||
trigger: ".o_resume_line_title:contains('Mamie Rock')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Change type",
|
||||
trigger: ".o_field_widget[name='line_type_id'] input",
|
||||
run: "text Experience",
|
||||
},
|
||||
{
|
||||
content: "Choose experience",
|
||||
trigger: '.ui-autocomplete .ui-menu-item a:contains("Experience")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Save experience change",
|
||||
trigger: ".o_form_button_save",
|
||||
in_modal: true,
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Add a new Skill",
|
||||
trigger: ".o_field_skills_one2many button:contains('Create a new entry')",
|
||||
},
|
||||
{
|
||||
content: "Select Music",
|
||||
trigger: ".o_field_widget[name='skill_type_id'] label:contains('Best Music')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Select a song",
|
||||
trigger: ".o_field_widget[name='skill_id'] input",
|
||||
run: "text Fortun",
|
||||
},
|
||||
{
|
||||
content: "Choose the song",
|
||||
trigger: '.ui-autocomplete .ui-menu-item a:contains("Fortunate Son")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Select a level",
|
||||
trigger: ".o_field_widget[name='skill_level_id'] input",
|
||||
run: "text Level",
|
||||
},
|
||||
{
|
||||
content: "Choose the level",
|
||||
trigger: '.ui-autocomplete .ui-menu-item a:contains("Level 2")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Save new skill",
|
||||
trigger: ".o_form_button_save",
|
||||
in_modal: true,
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Add a new Skill",
|
||||
trigger: ".o_field_skills_one2many button:contains('ADD')",
|
||||
},
|
||||
{
|
||||
content: "Select a song", // "Music" should be already selected
|
||||
trigger: ".o_field_widget[name='skill_id'] input",
|
||||
run: "text Mary",
|
||||
},
|
||||
{
|
||||
content: "Choose the song",
|
||||
trigger: '.ui-autocomplete .ui-menu-item a:contains("Oh Mary")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Select a level",
|
||||
trigger: ".o_field_widget[name='skill_level_id'] input",
|
||||
run: "text Level 7",
|
||||
},
|
||||
{
|
||||
content: "Choose the level",
|
||||
trigger: '.ui-autocomplete .ui-menu-item a:contains("Level 7")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Save new skill",
|
||||
trigger: ".o_form_button_save",
|
||||
in_modal: true,
|
||||
run: "click",
|
||||
},
|
||||
...tour.stepUtils.saveForm({
|
||||
content: "save Form",
|
||||
extra_trigger: 'td:containsExact("Oh Mary")',
|
||||
}),
|
||||
{
|
||||
content: "Go back to employees",
|
||||
trigger: 'a[data-menu-xmlid="hr.menu_hr_root"]',
|
||||
run: "click",
|
||||
}
|
||||
]);
|
||||
registry.category("web_tour.tours").add("hr_skills_tour", {
|
||||
url: "/odoo",
|
||||
steps: () => [
|
||||
stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
content: "Open Employees app",
|
||||
trigger: ".o_app[data-menu-xmlid='hr.menu_hr_root']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Create a new employee",
|
||||
trigger: ".o-kanban-button-new",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Pick a name",
|
||||
trigger: ".o_field_widget[name='name'] input",
|
||||
run: "edit Jony McHallyFace",
|
||||
},
|
||||
{
|
||||
content: "Save",
|
||||
trigger: ".o_form_button_save",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Add Experience",
|
||||
trigger: ".nav-link:contains('Resume')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Add a new Resume experience",
|
||||
trigger: ".o_field_resume_one2many button.btn-secondary",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Enter some company name",
|
||||
trigger:
|
||||
".modal:contains(new resume line) .modal-body .o_field_widget[name='name'] input",
|
||||
run: "edit Mamie Rock",
|
||||
},
|
||||
{
|
||||
trigger: ".modal:contains(new resume line) .o_field_widget[name='date_start'] button",
|
||||
content: "open date picker",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Set start date",
|
||||
trigger: ".modal:contains(new resume line) .o_field_widget[name='date_start'] input",
|
||||
run: "edit 12/05/2017",
|
||||
},
|
||||
{
|
||||
content: "Give some description",
|
||||
trigger: `.modal:contains(new resume line) .o_field_html[name='description'] div.o-paragraph`,
|
||||
run: "editor Sang some songs and played some music",
|
||||
},
|
||||
{
|
||||
content: "Save it",
|
||||
trigger: ".modal:contains(new resume line) .o_form_button_save:contains(save)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "body:not(:has(.modal:contains(new resume line)))",
|
||||
},
|
||||
{
|
||||
content: "Edit newly created experience",
|
||||
trigger: ".o_resume_line_title:contains(Mamie Rock)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Change type",
|
||||
trigger: ".modal:contains(new resume line) .o_field_widget[name='line_type_id'] .o_selection_badge:contains(Other Experience)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Save experience change",
|
||||
trigger: ".modal:contains(new resume line) .o_form_button_save:contains(save)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "body:not(:has(.modal:contains(new resume line)))",
|
||||
},
|
||||
{
|
||||
content: "Add a new Skill",
|
||||
trigger: ".o_field_skills_one2many button:contains('Pick a skill from the list')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Select Music",
|
||||
trigger: ".o_field_widget[name='skill_type_id'] span:contains('Best Music')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Choose the song",
|
||||
trigger: ".o_field_widget[name='skill_id'] span:contains('Fortunate Son')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Choose the level",
|
||||
trigger: ".o_field_widget[name='skill_level_id'] span:contains('Level 2')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Save new skill",
|
||||
trigger: ".modal:contains(update skills) .o_form_button_save:contains(save & close)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"Wait the new skill is completely saved. Ensure also the modal is closed before open a new one.",
|
||||
trigger: "body:not(:has(.modal))",
|
||||
},
|
||||
{
|
||||
content: "Check if item is added",
|
||||
trigger: ".o_data_row td.o_data_cell:contains('Fortunate Son')",
|
||||
},
|
||||
{
|
||||
content: "Add a new Skill",
|
||||
trigger: ".o_field_skills_one2many button:contains('ADD')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Select Certification",
|
||||
trigger: ".o_field_widget[name='skill_type_id'] span:contains('Music Certification')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Choose the instrument",
|
||||
trigger: ".o_field_widget[name='skill_id'] span:contains('Piano')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Choose the level",
|
||||
trigger: "div[name='valid_from'] button",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Choose the level",
|
||||
trigger: ".o_field_widget[name='valid_from'] input",
|
||||
run: "edit 02/03/2025",
|
||||
},
|
||||
{
|
||||
content: "Choose the level",
|
||||
trigger: ".o_field_widget[name='valid_to']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Choose the level",
|
||||
trigger: ".o_field_widget[name='valid_to'] input",
|
||||
run: "edit 03/04/2025",
|
||||
},
|
||||
{
|
||||
content: "Save new skill",
|
||||
trigger: ".modal:contains(update skills) .o_form_button_save:contains(save & close)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Wait the new skill is completely saved",
|
||||
trigger: "body:not(:has(.modal))",
|
||||
},
|
||||
{
|
||||
content: "Check if item is added",
|
||||
trigger: ".o_data_row td.o_data_cell:contains('Piano')",
|
||||
},
|
||||
...stepUtils.saveForm(),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
|
||||
registry.category("web_tour.tours").add("hr_skills_type_tour", {
|
||||
url: "/odoo",
|
||||
steps: () => [
|
||||
stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
content: "Open Employees app",
|
||||
trigger: ".o_app[data-menu-xmlid='hr.menu_hr_root']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Open skill type menu",
|
||||
trigger: "[data-menu-xmlid='hr.menu_human_resources_configuration']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Open skill type menu",
|
||||
trigger: "[data-menu-xmlid='hr_skills.hr_skill_type_menu']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Create a skill type",
|
||||
trigger: ".o_list_button_add",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Write skill type name",
|
||||
trigger: ".o_field_widget[name='name'] input",
|
||||
run: "edit Cooking Skill",
|
||||
},
|
||||
{
|
||||
trigger: "div[name=skill_ids] .o_field_x2many_list_row_add a",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "div[name=skill_ids] div[name=name] input",
|
||||
run: "edit Macaroon",
|
||||
},
|
||||
{
|
||||
trigger: "div[name=skill_level_ids] .o_field_x2many_list_row_add a",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "div[name=skill_level_ids] div[name=name] input",
|
||||
run: "edit Beginner",
|
||||
},
|
||||
{
|
||||
trigger: "div[name=skill_level_ids] div[name=default_level] input[type='checkbox']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "div[name=skill_level_ids] .o_field_x2many_list_row_add a",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "tr:nth-child(2).o_selected_row div[name=name] input",
|
||||
run: "edit Intermediate",
|
||||
},
|
||||
{
|
||||
trigger: "tr:nth-child(2).o_selected_row [name=default_level] input[type='checkbox']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "div[name=skill_level_ids] .o_field_x2many_list_row_add a",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "tr:nth-child(3).o_selected_row div[name=name] input",
|
||||
run: "edit Expert",
|
||||
},
|
||||
{
|
||||
trigger: "tr:nth-child(3).o_selected_row [name=default_level] input[type='checkbox']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "tr:nth-child(1) [name=default_level] input[type='checkbox']",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: "tr:nth-child(2) [name=default_level] input[type='checkbox']",
|
||||
run: "click",
|
||||
},
|
||||
...stepUtils.saveForm(),
|
||||
],
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue