Initial commit: Hr packages

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,49 @@
<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>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,40 @@
/** @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);

View file

@ -0,0 +1,50 @@
.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 {
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-hrs-timeline-entry-padding));
width: 1px;
height: 100%;
margin-left: 2.1rem;
background-color: $border-color;
content: "";
}
}
}
.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;
}
}
}

View file

@ -0,0 +1,46 @@
<?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>

View file

@ -0,0 +1,44 @@
/** @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);

View file

@ -0,0 +1,23 @@
.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;
}
}
}

View file

@ -0,0 +1,38 @@
<?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>

View file

@ -0,0 +1,12 @@
.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;
}
}

View file

@ -0,0 +1,24 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { GraphRenderer } from "@web/views/graph/graph_renderer";
import { graphView } from "@web/views/graph/graph_view";
export class SkillsGraphRenderer extends GraphRenderer {
getScaleOptions() {
const scaleOptions = super.getScaleOptions();
if ('yAxes' in scaleOptions) {
scaleOptions['yAxes'][0]['ticks']['suggestedMax'] = 100;
}
return scaleOptions;
}
}
export const skillsGraphView = {
...graphView,
Renderer: SkillsGraphRenderer,
};
registry.category("views").add("skills_graph", skillsGraphView);

View file

@ -0,0 +1,57 @@
/** @odoo-module */
import { ListRenderer } from "@web/views/list/list_renderer";
export class CommonSkillsListRenderer extends ListRenderer {
get colspan() {
const span = this.allColumns.length;
if (this.isEditable) {
return span + 1;
}
return span;
}
get groupBy() {
return '';
}
get groupedList() {
const grouped = {};
for (const record of this.list.records) {
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'),
list: {
records: [],
},
};
}
grouped[group[1]].list.records.push(record);
}
return grouped;
}
get showTable() {
return this.props.list.records.length;
}
get isEditable() {
return this.props.editable !== false;
}
async onCellClicked(record, column, ev) {
if (!this.isEditable) {
return;
}
return await super.onCellClicked(record, column, ev);
}
}
CommonSkillsListRenderer.rowsTemplate = "hr_skills.SkillsListRenderer.Rows";

View file

@ -0,0 +1,52 @@
<?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>

View file

@ -0,0 +1,24 @@
<?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>

View file

@ -0,0 +1,147 @@
/** @odoo-module **/
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",
}
]);