mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-24 01:52:03 +02:00
19.0 vanilla
This commit is contained in:
parent
a1137a1456
commit
e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions
|
|
@ -1,8 +1,7 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import session from 'web.session'
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { user } from "@web/core/user";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useEnv } from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* Redirect to the sub employee kanban view.
|
||||
|
|
@ -15,8 +14,6 @@ import { useEnv } from "@odoo/owl";
|
|||
export function onEmployeeSubRedirect() {
|
||||
const actionService = useService('action');
|
||||
const orm = useService('orm');
|
||||
const rpc = useService('rpc');
|
||||
const env = useEnv();
|
||||
|
||||
return async (event) => {
|
||||
const employeeId = parseInt(event.currentTarget.dataset.employeeId);
|
||||
|
|
@ -28,11 +25,11 @@ export function onEmployeeSubRedirect() {
|
|||
const subordinateIds = await rpc('/hr/get_subordinates', {
|
||||
employee_id: employeeId,
|
||||
subordinates_type: type,
|
||||
context: session.user_context
|
||||
context: user.context
|
||||
});
|
||||
let action = await orm.call('hr.employee', 'get_formview_action', [employeeId]);
|
||||
action = {...action,
|
||||
name: env._t('Team'),
|
||||
name: _t('Team'),
|
||||
view_mode: 'kanban,list,form',
|
||||
views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
|
||||
domain: [['id', 'in', subordinateIds]],
|
||||
|
|
|
|||
|
|
@ -1,35 +1,22 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import {Field} from '@web/views/fields/field';
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { user } from "@web/core/user";
|
||||
import { onEmployeeSubRedirect } from './hooks';
|
||||
|
||||
const { Component, onWillStart, onWillUpdateProps, useState } = owl;
|
||||
|
||||
function useUniquePopover() {
|
||||
const popover = usePopover();
|
||||
let remove = null;
|
||||
return Object.assign(Object.create(popover), {
|
||||
add(target, component, props, options) {
|
||||
if (remove) {
|
||||
remove();
|
||||
}
|
||||
remove = popover.add(target, component, props, options);
|
||||
return () => {
|
||||
remove();
|
||||
remove = null;
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { useRecordObserver } from "@web/model/relational_model/utils";
|
||||
|
||||
class HrOrgChartPopover extends Component {
|
||||
static template = "hr_org_chart.hr_orgchart_emp_popover";
|
||||
static props = {
|
||||
employee: Object,
|
||||
close: Function,
|
||||
};
|
||||
async setup() {
|
||||
super.setup();
|
||||
|
||||
this.rpc = useService('rpc');
|
||||
this.orm = useService('orm');
|
||||
this.actionService = useService("action");
|
||||
this._onEmployeeSubRedirect = onEmployeeSubRedirect();
|
||||
|
|
@ -44,61 +31,38 @@ class HrOrgChartPopover extends Component {
|
|||
*/
|
||||
async _onEmployeeRedirect(employeeId) {
|
||||
const action = await this.orm.call('hr.employee', 'get_formview_action', [employeeId]);
|
||||
this.actionService.doAction(action);
|
||||
this.actionService.doAction(action);
|
||||
}
|
||||
}
|
||||
HrOrgChartPopover.template = 'hr_org_chart.hr_orgchart_emp_popover';
|
||||
|
||||
export class HrOrgChart extends Field {
|
||||
export class HrOrgChart extends Component {
|
||||
static template = "hr_org_chart.hr_org_chart";
|
||||
static props = {...standardFieldProps};
|
||||
async setup() {
|
||||
super.setup();
|
||||
|
||||
this.rpc = useService('rpc');
|
||||
this.orm = useService('orm');
|
||||
this.actionService = useService("action");
|
||||
this.popover = useUniquePopover();
|
||||
|
||||
this.jsonStringify = JSON.stringify;
|
||||
this.popover = usePopover(HrOrgChartPopover);
|
||||
|
||||
this.state = useState({'employee_id': null});
|
||||
this.lastParent = null;
|
||||
this.max_level = null;
|
||||
this.lastEmployeeId = null;
|
||||
this._onEmployeeSubRedirect = onEmployeeSubRedirect();
|
||||
|
||||
onWillStart(async () => {
|
||||
this.employee = this.props.record.data;
|
||||
// the widget is either dispayed in the context of a hr.employee form or a res.users form
|
||||
this.state.employee_id =
|
||||
this.employee.employee_ids !== undefined
|
||||
? this.employee.employee_ids.resIds[0]
|
||||
: this.employee.id;
|
||||
const parentId =
|
||||
this.employee.parent_id && this.employee.parent_id[0]
|
||||
? this.employee.parent_id[0]
|
||||
: false;
|
||||
const forceReload =
|
||||
this.lastRecord !== this.props.record || this.lastParent != parentId;
|
||||
this.lastParent = parentId;
|
||||
this.lastRecord = this.props.record;
|
||||
await this.fetchEmployeeData(this.state.employee_id, forceReload);
|
||||
});
|
||||
|
||||
onWillUpdateProps(async (nextProps) => {
|
||||
const newParentId =
|
||||
nextProps.record.data.parent_id && nextProps.record.data.parent_id[0]
|
||||
? nextProps.record.data.parent_id[0]
|
||||
: false;
|
||||
const newEmployeeId = nextProps.record.data.id || false;
|
||||
useRecordObserver(async (record) => {
|
||||
const newParentId = record.data.parent_id?.id || false;
|
||||
const newEmployeeId = record.resId || false;
|
||||
if (this.lastParent !== newParentId || this.state.employee_id !== newEmployeeId) {
|
||||
this.lastParent = newParentId;
|
||||
this.max_level = null; // Reset max_level to default
|
||||
await this.fetchEmployeeData(newEmployeeId, true);
|
||||
await this.fetchEmployeeData(newEmployeeId, newParentId, true);
|
||||
}
|
||||
this.state.employee_id = newEmployeeId;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchEmployeeData(employeeId, force = false) {
|
||||
async fetchEmployeeData(employeeId, newParentId = null, force = false) {
|
||||
if (!employeeId) {
|
||||
this.managers = [];
|
||||
this.children = [];
|
||||
|
|
@ -108,14 +72,14 @@ export class HrOrgChart extends Field {
|
|||
this.view_employee_id = null;
|
||||
} else if (employeeId !== this.view_employee_id || force) {
|
||||
this.view_employee_id = employeeId;
|
||||
var orgData = await this.rpc(
|
||||
let orgData = await rpc(
|
||||
'/hr/get_org_chart',
|
||||
{
|
||||
employee_id: employeeId,
|
||||
new_parent_id: newParentId,
|
||||
context: {
|
||||
...Component.env.session.user_context,
|
||||
...user.context,
|
||||
max_level: this.max_level,
|
||||
new_parent_id: this.lastParent,
|
||||
},
|
||||
});
|
||||
if (Object.keys(orgData).length === 0) {
|
||||
|
|
@ -133,12 +97,7 @@ export class HrOrgChart extends Field {
|
|||
}
|
||||
|
||||
_onOpenPopover(event, employee) {
|
||||
this.popover.add(
|
||||
event.currentTarget,
|
||||
this.constructor.components.Popover,
|
||||
{employee},
|
||||
{closeOnClickAway: true}
|
||||
);
|
||||
this.popover.open(event.currentTarget, { employee });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -150,19 +109,17 @@ export class HrOrgChart extends Field {
|
|||
*/
|
||||
async _onEmployeeRedirect(employeeId) {
|
||||
const action = await this.orm.call('hr.employee', 'get_formview_action', [employeeId]);
|
||||
this.actionService.doAction(action);
|
||||
this.actionService.doAction(action);
|
||||
}
|
||||
|
||||
async _onEmployeeMoreManager(managerId) {
|
||||
this.max_level = 100; // Set a high level to fetch all managers
|
||||
await this.fetchEmployeeData(this.state.employee_id, true);
|
||||
await this.fetchEmployeeData(this.state.employee_id, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
HrOrgChart.components = {
|
||||
Popover: HrOrgChartPopover,
|
||||
export const hrOrgChart = {
|
||||
component: HrOrgChart,
|
||||
};
|
||||
|
||||
HrOrgChart.template = 'hr_org_chart.hr_org_chart';
|
||||
|
||||
registry.category("fields").add("hr_org_chart", HrOrgChart);
|
||||
registry.category("fields").add("hr_org_chart", hrOrgChart);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,27 @@
|
|||
|
||||
#o_employee_right {
|
||||
@include media-breakpoint-up(lg) {
|
||||
border-left: $border-width solid $o-form-separator-color;
|
||||
}
|
||||
#o_employee_org_chart {
|
||||
--treeEntry-padding-v: #{map-get($spacers, 2)};
|
||||
--treeEntry--after-width: #{$o-hr-org-chart-entry-pic-small-size * .25};
|
||||
--treeEntry--before-top: calc(-2 * (var(--treeEntry-padding-v)) - (#{$o-hr-org-chart-entry-pic-small-size} * .5));
|
||||
|
||||
.o_org_chart_entry {
|
||||
padding-top: var(--treeEntry-padding-v);
|
||||
padding-bottom: var(--treeEntry-padding-v);
|
||||
|
||||
&:before {
|
||||
bottom: 50%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&:first-child:before {
|
||||
--treeEntry--before-top: calc(var(--treeEntry-padding-v) * -1);
|
||||
}
|
||||
|
||||
&:after {
|
||||
top: calc(#{$o-hr-org-chart-entry-pic-small-size} * .5 + var(--treeEntry-padding-v));
|
||||
width: var(--treeEntry--after-width);
|
||||
}
|
||||
|
||||
.o_media_object {
|
||||
width: $o-hr-org-chart-entry-pic-small-size;
|
||||
height: $o-hr-org-chart-entry-pic-small-size;
|
||||
|
|
@ -17,40 +34,81 @@
|
|||
height: $o-hr-org-chart-entry-pic-size;
|
||||
}
|
||||
|
||||
&:not(.o_org_chart_entry_self):hover .o_media_object {
|
||||
box-shadow: 0 0 0 $border-width*2 $info;
|
||||
.o_employee_redirect:hover {
|
||||
.o_media_object {
|
||||
box-shadow: 0 0 0 $border-width * 2 $info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_org_chart_entry_self_container.o_org_chart_has_managers {
|
||||
margin-left: $o-hr-org-chart-entry-pic-small-size * .25;
|
||||
.o_org_chart_entry_self {
|
||||
.o_media_object:not(.placeholder) {
|
||||
outline: $border-width solid $info;
|
||||
outline-offset: $border-width;
|
||||
}
|
||||
}
|
||||
|
||||
.o_org_chart_group_down.o_org_chart_has_managers {
|
||||
padding-left: $o-hr-org-chart-entry-pic-size * .6;
|
||||
.o_org_chart_group_up {
|
||||
.o_org_chart_entry {
|
||||
--treeEntry-padding-h: 0px;
|
||||
--treeEntry--after-display: none;
|
||||
--treeEntry--beforeAfter-left: #{$o-hr-org-chart-entry-pic-small-size * .5};
|
||||
--treeEntry--before-top: calc(var(--treeEntry-padding-v) * -1);
|
||||
|
||||
&:before {
|
||||
bottom: calc(100% - 1 * var(--treeEntry-padding-v));
|
||||
}
|
||||
|
||||
&:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ORGANIGRAM LINES
|
||||
.o_org_chart_group_up .o_treeEntry {
|
||||
--treeEntry-padding-h: 0px;
|
||||
--treeEntry--before-top: 50%;
|
||||
--treeEntry--after-display: none;
|
||||
--treeEntry--beforeAfter-left: #{$o-hr-org-chart-entry-pic-small-size * .5};
|
||||
}
|
||||
|
||||
.o_org_chart_entry_self_container .o_treeEntry {
|
||||
--treeEntry-padding-h: #{$o-hr-org-chart-entry-pic-small-size * .5};
|
||||
--treeEntry-padding-v: #{$o-hr-org-chart-entry-pic-size * .25};
|
||||
}
|
||||
|
||||
.o_org_chart_group_down .o_treeEntry {
|
||||
.o_org_chart_group_down .o_treeEntry, .o_org_chart_entry_self_container .o_treeEntry {
|
||||
--treeEntry-padding-h: #{$o-hr-org-chart-entry-pic-size};
|
||||
--treeEntry-padding-v: #{$o-hr-org-chart-entry-pic-small-size * .5};
|
||||
|
||||
padding-left: calc(var(--treeEntry-padding-h) - var(--treeEntry--after-width));
|
||||
}
|
||||
|
||||
.o_org_chart_entry_self_container {
|
||||
.o_treeEntry:after {
|
||||
top: calc(#{$o-hr-org-chart-entry-pic-size} * .5 + var(--treeEntry-padding-v));
|
||||
}
|
||||
|
||||
&.o_org_chart_has_managers {
|
||||
.o_org_chart_entry_self {
|
||||
--treeEntry-padding-h: #{$o-hr-org-chart-entry-pic-small-size};
|
||||
|
||||
padding-left: calc(var(--treeEntry-padding-h) - var(--treeEntry--after-width));
|
||||
}
|
||||
|
||||
& + .o_org_chart_group_down {
|
||||
padding-left: calc(#{$o-hr-org-chart-entry-pic-small-size} - var(--treeEntry--after-width));
|
||||
}
|
||||
}
|
||||
|
||||
& + .o_org_chart_group_down {
|
||||
--treeEntry--after-width: #{$o-hr-org-chart-entry-pic-size * .25};
|
||||
--treeEntry-padding-h: calc(#{$o-hr-org-chart-entry-pic-size} - var(--treeEntry--after-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right to Left specific style to flip the popover arrow
|
||||
.o_rtl .o_org_chart_popup.popover .arrow {
|
||||
left: 100%;
|
||||
transform: matrix(-1, 0, 0, 1, 0, 0);
|
||||
// Specific popup style
|
||||
.o_org_chart_popup {
|
||||
--popover-header-padding-y: #{map-get($spacers, 1)};
|
||||
--popover-header-padding-x: #{map-get($spacers, 2)};
|
||||
|
||||
.o_employee_redirect {
|
||||
--btn-padding-x: var(--popover-header-padding-x);
|
||||
}
|
||||
|
||||
.o_media_object {
|
||||
$-media-object-size: $o-hr-org-chart-entry-pic-small-size - map-get($spacers, 2);
|
||||
width: $-media-object-size;
|
||||
height: $-media-object-size;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +1,68 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="hr_org_chart.hr_org_chart_employee" owl="1">
|
||||
<t t-name="hr_org_chart.hr_org_chart_employee">
|
||||
<t t-set="is_self" t-value="employee.id == view_employee_id"/>
|
||||
|
||||
<section t-if="employee_type == 'self'" t-attf-class="o_org_chart_entry_self_container #{managers.length > 0 ? 'o_org_chart_has_managers' : ''}">
|
||||
<div t-attf-class="o_org_chart_entry o_org_chart_entry_#{employee_type} d-flex position-relative py-2 overflow-visible #{managers.length > 0 ? 'o_treeEntry' : ''}">
|
||||
<div t-attf-class="o_org_chart_entry o_org_chart_entry_#{employee_type} position-relative d-flex align-items-center overflow-visible #{managers.length > 0 ? 'o_treeEntry' : ''}">
|
||||
<t t-call="hr_org_chart.hr_org_chart_employee_content">
|
||||
<t t-set="is_self" t-value="is_self"/>
|
||||
</t>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div t-else="" t-attf-class="o_org_chart_entry o_org_chart_entry_#{employee_type} o_treeEntry d-flex position-relative py-2 overflow-visible">
|
||||
<div t-else="" t-attf-class="o_org_chart_entry o_org_chart_entry_#{employee_type} o_treeEntry position-relative d-flex align-items-center overflow-visible">
|
||||
<t t-call="hr_org_chart.hr_org_chart_employee_content">
|
||||
<t t-set="is_self" t-value="is_self"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_org_chart.hr_org_chart_employee_content" owl="1">
|
||||
<div class="o_media_left position-relative">
|
||||
<!-- NOTE: Since by the default on not squared images odoo add white borders,
|
||||
use bg-images to get a clean and centred images -->
|
||||
<a t-if="! is_self"
|
||||
class="o_media_object d-block rounded-circle o_employee_redirect"
|
||||
<t t-name="hr_org_chart.hr_org_chart_employee_informations">
|
||||
<div class="o_media_left align-self-start rounded z-1">
|
||||
<!-- NOTE: Use bg-images instead of img to get a clean and squared image -->
|
||||
<div
|
||||
class="o_media_object img-fluid d-block rounded"
|
||||
t-att-style="'background-image:url(\'/web/image/hr.employee.public/' + employee.id + '/avatar_1024/\')'"
|
||||
t-att-alt="employee.name"
|
||||
t-att-data-employee-id="employee.id"
|
||||
t-att-href="employee.link"
|
||||
t-on-click.prevent="() => this._onEmployeeRedirect(employee.id)"/>
|
||||
<div t-if="is_self"
|
||||
class="o_media_object d-block rounded-circle border border-info"
|
||||
t-att-style="'background-image:url(\'/web/image/hr.employee.public/' + employee.id + '/avatar_1024/\')'"/>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-grow-1 align-items-center justify-content-between position-relative px-3">
|
||||
<a t-if="!is_self" t-att-href="employee.link" class="o_employee_redirect d-flex flex-column" t-att-data-employee-id="employee.id" t-on-click.prevent="() => this._onEmployeeRedirect(employee.id)">
|
||||
<b class="o_media_heading m-0 fs-6" t-esc="employee.name"/>
|
||||
<small class="text-muted fw-bold" t-esc="employee.job_title"/>
|
||||
</a>
|
||||
<div t-if="is_self" class="d-flex flex-column">
|
||||
<h5 class="o_media_heading m-0" t-esc="employee.name"/>
|
||||
<small class="text-muted fw-bold" t-esc="employee.job_title"/>
|
||||
</div>
|
||||
<button t-if="employee.indirect_sub_count > 0"
|
||||
class="btn p-0 fs-3"
|
||||
tabindex="0"
|
||||
t-att-data-emp-name="employee.name"
|
||||
t-att-data-emp-id="employee.id"
|
||||
t-att-data-emp-dir-subs="employee.direct_sub_count"
|
||||
t-att-data-emp-ind-subs="employee.indirect_sub_count"
|
||||
data-bs-trigger="focus"
|
||||
data-bs-toggle="popover"
|
||||
t-on-click="(event) => this._onOpenPopover(event, employee)">
|
||||
<a href="#"
|
||||
t-attf-class="badge rounded-pill bg-white border {{employee.indirect_sub_count < 10 ? 'px-2' : 'px-1' }}"
|
||||
t-esc="employee.indirect_sub_count"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div class="position-relative d-flex flex-column flex-grow-1 justify-content-center ps-2 lh-sm">
|
||||
<b class="o_media_heading fs-6 fw-bold" t-esc="employee.name"/>
|
||||
<small class="o_employee_job text-muted" t-attf-class="#{is_self ? 'fw-bold' : 'opacity-75 opacity-100-hover'}" t-esc="employee.job_name"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_org_chart.hr_org_chart" owl="1">
|
||||
<t t-name="hr_org_chart.hr_org_chart_employee_content">
|
||||
<a t-if="!is_self" t-att-href="employee.link" class="o_employee_redirect d-flex w-100 me-auto opacity-trigger-hover" t-att-data-employee-id="employee.id" t-on-click.prevent="() => this._onEmployeeRedirect(employee.id)">
|
||||
<t t-call="hr_org_chart.hr_org_chart_employee_informations">
|
||||
<t t-set="is_self" t-value="is_self"/>
|
||||
</t>
|
||||
</a>
|
||||
<div t-if="is_self" class="d-flex w-100 me-auto">
|
||||
<t t-call="hr_org_chart.hr_org_chart_employee_informations">
|
||||
<t t-set="is_self" t-value="is_self"/>
|
||||
</t>
|
||||
</div>
|
||||
<button t-if="employee.indirect_sub_count > 0"
|
||||
class="btn btn-secondary btn-sm d-flex ms-2 rounded-pill"
|
||||
tabindex="0"
|
||||
t-att-data-emp-name="employee.name"
|
||||
t-att-data-emp-id="employee.id"
|
||||
t-att-data-emp-dir-subs="employee.direct_sub_count"
|
||||
t-att-data-emp-ind-subs="employee.indirect_sub_count"
|
||||
data-bs-trigger="focus"
|
||||
data-bs-toggle="popover"
|
||||
t-on-click="(event) => this._onOpenPopover(event, employee)">
|
||||
<span
|
||||
class="badge px-0"
|
||||
t-esc="employee.indirect_sub_count"
|
||||
/>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_org_chart.hr_org_chart">
|
||||
<!-- NOTE: Desidered behaviour:
|
||||
The maximun number of people is always 7 (including 'self'). Managers have priority over suburdinates
|
||||
Eg. 1 Manager + 1 self = show just 5 subordinates (if availables)
|
||||
|
|
@ -73,7 +72,7 @@
|
|||
<t t-set="emp_count" t-value="0"/>
|
||||
<div t-if='managers.length > 0' class="o_org_chart_group_up position-relative">
|
||||
<div t-if='managers_more' class="o_org_chart_more pe-3" t-attf-class="{{max_level !== null ? 'invisible' : ''}}">
|
||||
<a href="#" t-att-data-employee-id="managers[0].id" class="o_employee_more_managers d-block bg-100 px-3" t-on-click.prevent="() => this._onEmployeeMoreManager(managers[0].id)">
|
||||
<a href="#" t-att-data-employee-id="managers[0].id" class="o_employee_more_managers d-block bg-100 px-2" t-on-click.prevent="() => this._onEmployeeMoreManager(managers[0].id)">
|
||||
<i class="fa fa-angle-double-up" role="img" aria-label="More managers" title="More managers"/>
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -92,11 +91,44 @@
|
|||
</t>
|
||||
|
||||
<t t-if="!children.length && !managers.length">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<p><b>No hierarchy position.</b></p>
|
||||
<p>This employee has no manager or subordinate.</p>
|
||||
<p>In order to get an organigram, set a manager and save the record.</p>
|
||||
<p class="mb-3 text-muted fst-italic">Set a manager or reports to show in org chart.</p>
|
||||
<div class="o_org_chart_entry_self_container opacity-50">
|
||||
<div class="o_org_chart_entry o_org_chart_entry_self d-flex py-2 pe-none">
|
||||
<div class="o_media_left">
|
||||
<div class="o_media_object placeholder rounded opacity-25"/>
|
||||
</div>
|
||||
<div class="position-relative d-flex flex-grow-1 align-items-center px-2">
|
||||
<div class="d-flex flex-column w-100 opacity-25">
|
||||
<b class="o_media_heading placeholder col-4 m-0"/>
|
||||
<small class="placeholder placeholder-xs col-3 mt-1 text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="o_org_chart_group_down o_org_chart_has_managers opacity-50">
|
||||
<div class="o_org_chart_entry o_treeEntry position-relative d-flex py-2 overflow-visible pe-none">
|
||||
<div class="o_media_left">
|
||||
<div class="o_media_object o_employee_redirect placeholder rounded opacity-25"/>
|
||||
</div>
|
||||
<div class="d-flex flex-grow-1 align-items-center justify-content-between px-2">
|
||||
<div class="d-flex flex-column w-100 lh-sm opacity-25">
|
||||
<b class="o_media_heading placeholder col-3 m-0"/>
|
||||
<small class="placeholder placeholder-xs col-7 mt-1 text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_org_chart_entry o_treeEntry position-relative d-flex py-2 overflow-visible pe-none">
|
||||
<div class="o_media_left">
|
||||
<div class="o_media_object o_employee_redirect placeholder rounded opacity-25"/>
|
||||
</div>
|
||||
<div class="d-flex flex-grow-1 align-items-center px-2">
|
||||
<div class="d-flex flex-column w-100 opacity-25">
|
||||
<b class="o_media_heading placeholder col-5 m-0"/>
|
||||
<small class="placeholder placeholder-xs col-4 mt-1 text-muted"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</t>
|
||||
|
||||
<div t-if="children.length" t-attf-class="o_org_chart_group_down position-relative #{managers.length > 0 ? 'o_org_chart_has_managers' : ''}">
|
||||
|
|
@ -123,16 +155,14 @@
|
|||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_org_chart.hr_orgchart_emp_popover" owl="1">
|
||||
<div class="popover o_org_chart_popup" role="tooltip">
|
||||
<div class="tooltip-arrow">
|
||||
|
||||
</div>
|
||||
<t t-name="hr_org_chart.hr_orgchart_emp_popover">
|
||||
<div class="o_org_chart_popup" role="tooltip">
|
||||
<div class="tooltip-arrow"/>
|
||||
<h3 class="popover-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="flex-shrink-0" t-att-style='"background-image:url(\"/web/image/hr.employee.public/" + props.employee.id + "/avatar_1024/\")"'/>
|
||||
<b class="flew-grow-1"><t t-esc="props.employee.name"/></b>
|
||||
<a href="#" class="ms-auto o_employee_redirect" t-att-data-employee-id="props.employee.id" t-on-click.prevent="() => this._onEmployeeRedirect(props.employee.id)"><i class="fa fa-external-link" role="img" aria-label='Redirect' title="Redirect"></i></a>
|
||||
<span class="o_media_object flex-shrink-0 rounded me-1" t-att-style='"background-image:url(\"/web/image/hr.employee.public/" + props.employee.id + "/avatar_1024/\")"'/>
|
||||
<b class="flex-grow-1 fw-medium"><t t-esc="props.employee.name"/></b>
|
||||
<a href="#" class="o_employee_redirect btn btn-link" t-att-data-employee-id="props.employee.id" t-on-click.prevent="() => this._onEmployeeRedirect(props.employee.id)"><i class="fa fa-user" role="img" aria-label='View employee' title="View employee"/></a>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="popover-body">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { HierarchyCard } from "@web_hierarchy/hierarchy_card";
|
||||
|
||||
export class HrEmployeeHierarchyCard extends HierarchyCard {
|
||||
static template = "hr_org_chart.HrEmployeeHierarchyCard";
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
|
||||
<t t-name="hr_org_chart.HrEmployeeHierarchyCard" t-inherit="web_hierarchy.HierarchyCard">
|
||||
<xpath expr="//button[@name='hierarchy_search_subsidiaries']" position="attributes">
|
||||
<attribute name="class" separator=" " remove="d-grid"/>
|
||||
<attribute name="class" separator=" " remove="rounded-0"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='hierarchy_search_subsidiaries']" position="inside">
|
||||
<t t-out="props.node.childResIds.length"/> people
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='hierarchy_search_subsidiaries']/t[@t-if]" position="replace">
|
||||
<t t-if="!props.node.nodes.length">
|
||||
<i class="fa fa-fw fa-caret-right"/>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='hierarchy_search_subsidiaries']/t[@t-else]" position="replace">
|
||||
<t t-else="">
|
||||
<i class="fa fa-fw fa-caret-down"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Avatar } from "@mail/views/web/fields/avatar/avatar";
|
||||
|
||||
import { HierarchyRenderer } from "@web_hierarchy/hierarchy_renderer";
|
||||
import { HrEmployeeHierarchyCard } from "./hr_employee_hierarchy_card";
|
||||
|
||||
export class HrEmployeeHierarchyRenderer extends HierarchyRenderer {
|
||||
static template = "hr_org_chart.HrEmployeeHierarchyRenderer";
|
||||
static components = {
|
||||
...HierarchyRenderer.components,
|
||||
HierarchyCard: HrEmployeeHierarchyCard,
|
||||
Avatar,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.o_hierarchy_renderer {
|
||||
.o_hierarchy_parent_node_container {
|
||||
.o_avatar > span {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
|
||||
<t t-name="hr_org_chart.HrEmployeeHierarchyRenderer" t-inherit="web_hierarchy.HierarchyRenderer">
|
||||
<xpath expr="//div[hasclass('o_hierarchy_parent_node_container')]/span" position="replace">
|
||||
<Avatar
|
||||
resModel="row.parentNode.model.resModel"
|
||||
resId="row.parentNode.resId"
|
||||
displayName="row.parentNode.data.display_name || row.parentNode.data.name"
|
||||
/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { hierarchyView } from "@web_hierarchy/hierarchy_view";
|
||||
import { HrEmployeeHierarchyRenderer } from "./hr_employee_hierarchy_renderer";
|
||||
|
||||
export const hrEmployeeHierarchyView = {
|
||||
...hierarchyView,
|
||||
Renderer: HrEmployeeHierarchyRenderer,
|
||||
};
|
||||
|
||||
registry.category("views").add("hr_employee_hierarchy", hrEmployeeHierarchyView);
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { contains, defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Employee extends models.Model {
|
||||
_name = "hr.employee";
|
||||
|
||||
name = fields.Char();
|
||||
parent_id = fields.Many2one({ string: "Manager", relation: "hr.employee" });
|
||||
child_ids = fields.One2many({
|
||||
string: "Subordinates",
|
||||
relation: "hr.employee",
|
||||
relation_field: "parent_id",
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "Albert", parent_id: false, child_ids: [2, 3] },
|
||||
{ id: 2, name: "Georges", parent_id: 1, child_ids: [] },
|
||||
{ id: 3, name: "Josephine", parent_id: 1, child_ids: [4] },
|
||||
{ id: 4, name: "Louis", parent_id: 3, child_ids: [] },
|
||||
];
|
||||
|
||||
show_action_helper() {
|
||||
return false;
|
||||
}
|
||||
|
||||
_views = {
|
||||
hierarchy: `
|
||||
<hierarchy js_class="hr_employee_hierarchy">
|
||||
<templates>
|
||||
<t t-name="hierarchy-box">
|
||||
<div class="o_hierarchy_node_header">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="parent_id"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</hierarchy>
|
||||
`,
|
||||
form: `
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="parent_id"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
defineModels([Employee]);
|
||||
defineMailModels();
|
||||
|
||||
test("load hierarchy view", async () => {
|
||||
await mountView({
|
||||
type: "hierarchy",
|
||||
resModel: "hr.employee",
|
||||
});
|
||||
|
||||
expect(".o_hierarchy_view").toHaveCount(1);
|
||||
expect(".o_hierarchy_button_add").toHaveCount(1);
|
||||
expect(".o_hierarchy_view .o_hierarchy_renderer").toHaveCount(1);
|
||||
expect(".o_hierarchy_view .o_hierarchy_renderer > .o_hierarchy_container").toHaveCount(1);
|
||||
expect(".o_hierarchy_row").toHaveCount(2);
|
||||
expect(".o_hierarchy_separator").toHaveCount(1);
|
||||
expect(".o_hierarchy_line_part").toHaveCount(2);
|
||||
expect(".o_hierarchy_line_left").toHaveCount(1);
|
||||
expect(".o_hierarchy_line_right").toHaveCount(1);
|
||||
expect(".o_hierarchy_node_container").toHaveCount(3);
|
||||
expect(".o_hierarchy_node").toHaveCount(3);
|
||||
expect(".o_hierarchy_node_button").toHaveCount(2);
|
||||
expect(".o_hierarchy_node_button.btn-primary").toHaveCount(1);
|
||||
expect(".o_hierarchy_node_button.btn-primary.d-grid").toHaveCount(0, {
|
||||
message: "'d-grid' class has been removed in that js_class",
|
||||
});
|
||||
expect(".o_hierarchy_node_button.btn-primary.rounded-0").toHaveCount(0, {
|
||||
message: "'d-grid' class has been removed in that js_class",
|
||||
});
|
||||
expect(".o_hierarchy_node_button.btn-primary .o_hierarchy_icon").toHaveCount(0, {
|
||||
message: "the icon has been replaced in that js_class",
|
||||
});
|
||||
expect(".o_hierarchy_node_button.btn-primary .fa-caret-right").toHaveCount(1, {
|
||||
message: "the icon has been replaced in that js_class",
|
||||
});
|
||||
expect(".o_hierarchy_node_button.btn-primary").toHaveText("1 people");
|
||||
// check nodes in each row
|
||||
expect(".o_hierarchy_row:eq(0) .o_hierarchy_node").toHaveCount(1);
|
||||
expect(".o_hierarchy_row:eq(0) .o_hierarchy_node_content").toHaveText("Albert");
|
||||
expect(".o_hierarchy_node_button.btn-secondary").toHaveCount(1);
|
||||
expect(".o_hierarchy_node_button.btn-secondary .fa-caret-down").toHaveCount(1);
|
||||
expect(".o_hierarchy_node_button.btn-secondary").toHaveText("2 people");
|
||||
});
|
||||
|
||||
test("display the avatar of the parent when there is more than one node in the same row of the parent", async () => {
|
||||
await mountView({
|
||||
type: "hierarchy",
|
||||
resModel: "hr.employee",
|
||||
});
|
||||
|
||||
expect(".o_hierarchy_row").toHaveCount(2);
|
||||
expect(".o_hierarchy_node_button.btn-primary").toHaveCount(1);
|
||||
await contains(".o_hierarchy_node_button.btn-primary").click();
|
||||
expect(".o_hierarchy_row").toHaveCount(3);
|
||||
expect(".o_hierarchy_node").toHaveCount(4);
|
||||
expect(".o_hierarchy_separator").toHaveCount(2);
|
||||
expect(".o_hierarchy_parent_node_container .o_avatar").toHaveCount(1);
|
||||
expect(".o_avatar").toHaveText("Josephine");
|
||||
});
|
||||
|
||||
test("hierarchy with a self manager employee", async () => {
|
||||
Employee._records = [{ id: 1, name: "Albert", parent_id: 1, child_ids: [1] }]
|
||||
await mountView({
|
||||
context:{
|
||||
hierarchy_res_id: 1,
|
||||
},
|
||||
type: "hierarchy",
|
||||
resModel: "hr.employee",
|
||||
});
|
||||
|
||||
expect(".o_hierarchy_row").toHaveCount(2);
|
||||
expect(".o_hierarchy_node_button.btn-secondary").toHaveCount(1);
|
||||
expect(".o_hierarchy_node").toHaveCount(2);
|
||||
await contains(".o_hierarchy_node_button.btn-secondary").click();
|
||||
expect(".o_hierarchy_row").toHaveCount(1);
|
||||
expect(".o_hierarchy_node").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("hierarchy with a cycle", async () =>{
|
||||
Employee._records = [
|
||||
{ id: 1, name: "Albert", parent_id: 4, child_ids: [2, 3] },
|
||||
{ id: 2, name: "Georges", parent_id: 1, child_ids: [] },
|
||||
{ id: 3, name: "Josephine", parent_id: 1, child_ids: [4] },
|
||||
{ id: 4, name: "Louis", parent_id: 3, child_ids: [1] },
|
||||
]
|
||||
await mountView({
|
||||
context:{
|
||||
hierarchy_res_id: 1,
|
||||
},
|
||||
type: "hierarchy",
|
||||
resModel: "hr.employee",
|
||||
});
|
||||
|
||||
expect(".o_hierarchy_row").toHaveCount(4);
|
||||
expect(".o_hierarchy_node").toHaveCount(5);
|
||||
expect(".o_hierarchy_row:nth-of-type(8) .o_hierarchy_node").toHaveCount(1);
|
||||
// check that the node in the cycle cannot be expanded
|
||||
expect(".o_hierarchy_row:nth-of-type(8) .o_hierarchy_node_button").toHaveCount(0);
|
||||
expect(".o_hierarchy_row:nth-of-type(1) .o_hierarchy_node_content").toHaveText("Albert\nLouis");
|
||||
await contains(".o_hierarchy_row:nth-of-type(1) .btn-secondary").click();
|
||||
await contains(".o_hierarchy_row:nth-of-type(1) .btn-primary").click();
|
||||
await contains(".o_hierarchy_row:nth-of-type(3) .btn-primary").click();
|
||||
await contains(".o_hierarchy_row:nth-of-type(6) .btn-primary").click();
|
||||
// check the root of the tree is still Albert
|
||||
expect(".o_hierarchy_row:nth-of-type(1) .o_hierarchy_node_content").toHaveText("Albert\nLouis");
|
||||
expect(".o_hierarchy_row:nth-of-type(8) .o_hierarchy_node_content").toHaveText("Albert\nLouis");
|
||||
});
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryOne } from "@odoo/hoot-dom";
|
||||
import { defineModels, fields, models, mountView, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class Employee extends models.Model {
|
||||
_name = "hr.employee";
|
||||
|
||||
child_ids = fields.One2many({
|
||||
string: "Subordinates",
|
||||
relation: "hr.employee",
|
||||
relation_field: "parent_id",
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
child_ids: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Employee]);
|
||||
defineMailModels();
|
||||
|
||||
test("hr org chart: empty render", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
onRpc("/hr/get_org_chart", async (request) => {
|
||||
const { params: args } = await request.json();
|
||||
expect(args).toInclude("employee_id", {
|
||||
message: "it should have 'employee_id' as argument",
|
||||
});
|
||||
expect("new_parent_id" in args).toBe(true, {
|
||||
message: "it should have 'new_parent_id' as argument",
|
||||
});
|
||||
return {
|
||||
children: [],
|
||||
managers: [],
|
||||
managers_more: false,
|
||||
};
|
||||
});
|
||||
onRpc("/hr/get_redirect_model", () => "hr.employee");
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "hr.employee",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="child_ids" widget="hr_org_chart"/>
|
||||
<field name="id" invisible="1"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(queryOne('[name="child_ids"]').children).toHaveLength(3, {
|
||||
message: "the chart should have 3 child",
|
||||
});
|
||||
});
|
||||
test("hr org chart: render without data", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
onRpc("/hr/get_org_chart", async (request) => {
|
||||
const { params: args } = await request.json();
|
||||
expect(args).toInclude("employee_id", {
|
||||
message: "it should have 'employee_id' as argument",
|
||||
});
|
||||
expect("new_parent_id" in args).toBe(true, {
|
||||
message: "it should have 'new_parent_id' as argument",
|
||||
});
|
||||
return {}; // return no data
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "hr.employee",
|
||||
arch: `
|
||||
<form>
|
||||
<field name="child_ids" widget="hr_org_chart"/>
|
||||
<field name="id" invisible="1"/>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(queryOne('[name="child_ids"]').children).toHaveLength(3, {
|
||||
message: "the chart should have 3 child",
|
||||
});
|
||||
});
|
||||
test("hr org chart: basic render", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
onRpc("/hr/get_org_chart", async (request) => {
|
||||
const { params: args } = await request.json();
|
||||
expect(args).toInclude("employee_id", {
|
||||
message: "it should have 'employee_id' as argument",
|
||||
});
|
||||
expect("new_parent_id" in args).toBe(true, {
|
||||
message: "it should have 'new_parent_id' as argument",
|
||||
});
|
||||
return {
|
||||
children: [
|
||||
{
|
||||
direct_sub_count: 0,
|
||||
indirect_sub_count: 0,
|
||||
job_id: 2,
|
||||
job_name: "Sub-Gooroo",
|
||||
link: "fake_link",
|
||||
name: "Michael Hawkins",
|
||||
id: 2,
|
||||
},
|
||||
],
|
||||
managers: [],
|
||||
managers_more: false,
|
||||
self: {
|
||||
direct_sub_count: 1,
|
||||
id: 1,
|
||||
indirect_sub_count: 1,
|
||||
job_id: 1,
|
||||
job_name: "Gooroo",
|
||||
link: "fake_link",
|
||||
name: "Antoine Langlais",
|
||||
},
|
||||
};
|
||||
});
|
||||
onRpc("/hr/get_redirect_model", () => "hr.employee");
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "hr.employee",
|
||||
arch: `<form>
|
||||
<sheet>
|
||||
<div id="o_employee_container">
|
||||
<div id="o_employee_main">
|
||||
<div id="o_employee_org_chart">
|
||||
<field name="child_ids" widget="hr_org_chart"/>
|
||||
<field name="id" invisible="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_org_chart_entry_sub").toHaveCount(1, {
|
||||
message: "the chart should have 1 subordinate",
|
||||
});
|
||||
expect(".o_org_chart_entry_self").toHaveCount(1, {
|
||||
message: "the current employee should only be displayed once in the chart",
|
||||
});
|
||||
});
|
||||
test("hr org chart: basic manager render", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
onRpc("/hr/get_org_chart", async (request) => {
|
||||
const { params: args } = await request.json();
|
||||
expect(args).toInclude("employee_id", {
|
||||
message: "it should have 'employee_id' as argument",
|
||||
});
|
||||
expect("new_parent_id" in args).toBe(true, {
|
||||
message: "it should have 'new_parent_id' as argument",
|
||||
});
|
||||
return {
|
||||
children: [
|
||||
{
|
||||
direct_sub_count: 0,
|
||||
indirect_sub_count: 0,
|
||||
job_id: 2,
|
||||
job_name: "Sub-Gooroo",
|
||||
link: "fake_link",
|
||||
name: "Michael Hawkins",
|
||||
id: 2,
|
||||
},
|
||||
],
|
||||
managers: [
|
||||
{
|
||||
direct_sub_count: 1,
|
||||
id: 1,
|
||||
indirect_sub_count: 2,
|
||||
job_id: 1,
|
||||
job_name: "Chief Gooroo",
|
||||
link: "fake_link",
|
||||
name: "Antoine Langlais",
|
||||
},
|
||||
],
|
||||
managers_more: false,
|
||||
self: {
|
||||
direct_sub_count: 1,
|
||||
id: 1,
|
||||
indirect_sub_count: 1,
|
||||
job_id: 3,
|
||||
job_name: "Gooroo",
|
||||
link: "fake_link",
|
||||
name: "John Smith",
|
||||
},
|
||||
};
|
||||
});
|
||||
onRpc("/hr/get_redirect_model", () => "hr.employee");
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "hr.employee",
|
||||
arch: `<form>
|
||||
<sheet>
|
||||
<div id="o_employee_container">
|
||||
<div id="o_employee_main">
|
||||
<div id="o_employee_org_chart">
|
||||
<field name="child_ids" widget="hr_org_chart"/>
|
||||
<field name="id" invisible="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 1,
|
||||
});
|
||||
expect(".o_org_chart_group_up .o_org_chart_entry_manager").toHaveCount(1, {
|
||||
message: "the chart should have 1 manager",
|
||||
});
|
||||
expect(".o_org_chart_group_down .o_org_chart_entry_sub").toHaveCount(1, {
|
||||
message: "the chart should have 1 subordinate",
|
||||
});
|
||||
expect(".o_org_chart_entry_self").toHaveCount(1, {
|
||||
message: "the chart should have only once the current employee",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("hr_org_chart", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
hr_employee: {
|
||||
fields: {
|
||||
child_ids: {string: "one2many Subordinates field", type: "one2many", relation: 'hr_employee'},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
child_ids: [],
|
||||
}]
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test("hr org chart: empty render", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: 'hr_employee',
|
||||
serverData: serverData,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<field name="child_ids" widget="hr_org_chart"/>' +
|
||||
'</form>',
|
||||
resId: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/hr/get_org_chart') {
|
||||
assert.ok('employee_id' in args, "it should have 'employee_id' as argument");
|
||||
return Promise.resolve({
|
||||
children: [],
|
||||
managers: [],
|
||||
managers_more: false,
|
||||
});
|
||||
} else if (route === '/hr/get_redirect_model') {
|
||||
return Promise.resolve('hr.employee');
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.strictEqual($(target.querySelector('[name="child_ids"]')).children().length, 1,
|
||||
"the chart should have 1 child");
|
||||
});
|
||||
QUnit.test("hr org chart: render without data", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: 'hr_employee',
|
||||
serverData: serverData,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<field name="child_ids" widget="hr_org_chart"/>' +
|
||||
'</form>',
|
||||
resId: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/hr/get_org_chart') {
|
||||
assert.ok('employee_id' in args, "it should have 'employee_id' as argument");
|
||||
return Promise.resolve({}); // return no data
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.strictEqual($(target.querySelector('[name="child_ids"]')).children().length, 1,
|
||||
"the chart should have 1 child");
|
||||
});
|
||||
QUnit.test("hr org chart: basic render", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: 'hr_employee',
|
||||
serverData: serverData,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<sheet>' +
|
||||
'<div id="o_employee_container"><div id="o_employee_main">' +
|
||||
'<div id="o_employee_right">' +
|
||||
'<field name="child_ids" widget="hr_org_chart"/>' +
|
||||
'</div>' +
|
||||
'</div></div>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
resId: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/hr/get_org_chart') {
|
||||
assert.ok('employee_id' in args, "it should have 'employee_id' as argument");
|
||||
return Promise.resolve({
|
||||
children: [{
|
||||
direct_sub_count: 0,
|
||||
indirect_sub_count: 0,
|
||||
job_id: 2,
|
||||
job_name: 'Sub-Gooroo',
|
||||
link: 'fake_link',
|
||||
name: 'Michael Hawkins',
|
||||
id: 2,
|
||||
}],
|
||||
managers: [],
|
||||
managers_more: false,
|
||||
self: {
|
||||
direct_sub_count: 1,
|
||||
id: 1,
|
||||
indirect_sub_count: 1,
|
||||
job_id: 1,
|
||||
job_name: 'Gooroo',
|
||||
link: 'fake_link',
|
||||
name: 'Antoine Langlais',
|
||||
}
|
||||
});
|
||||
} else if (route === '/hr/get_redirect_model') {
|
||||
return Promise.resolve('hr.employee');
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.containsOnce(target, '.o_org_chart_entry_sub',
|
||||
"the chart should have 1 subordinate");
|
||||
assert.containsOnce(target, '.o_org_chart_entry_self',
|
||||
"the current employee should only be displayed once in the chart");
|
||||
});
|
||||
QUnit.test("hr org chart: basic manager render", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: 'hr_employee',
|
||||
serverData: serverData,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<sheet>' +
|
||||
'<div id="o_employee_container"><div id="o_employee_main">' +
|
||||
'<div id="o_employee_right">' +
|
||||
'<field name="child_ids" widget="hr_org_chart"/>' +
|
||||
'</div>' +
|
||||
'</div></div>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
resId: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/hr/get_org_chart') {
|
||||
assert.ok('employee_id' in args, "should have 'employee_id' as argument");
|
||||
return Promise.resolve({
|
||||
children: [{
|
||||
direct_sub_count: 0,
|
||||
indirect_sub_count: 0,
|
||||
job_id: 2,
|
||||
job_name: 'Sub-Gooroo',
|
||||
link: 'fake_link',
|
||||
name: 'Michael Hawkins',
|
||||
id: 2,
|
||||
}],
|
||||
managers: [{
|
||||
direct_sub_count: 1,
|
||||
id: 1,
|
||||
indirect_sub_count: 2,
|
||||
job_id: 1,
|
||||
job_name: 'Chief Gooroo',
|
||||
link: 'fake_link',
|
||||
name: 'Antoine Langlais',
|
||||
}],
|
||||
managers_more: false,
|
||||
self: {
|
||||
direct_sub_count: 1,
|
||||
id: 1,
|
||||
indirect_sub_count: 1,
|
||||
job_id: 3,
|
||||
job_name: 'Gooroo',
|
||||
link: 'fake_link',
|
||||
name: 'John Smith',
|
||||
}
|
||||
});
|
||||
} else if (route === '/hr/get_redirect_model') {
|
||||
return Promise.resolve('hr.employee');
|
||||
}
|
||||
}
|
||||
});
|
||||
assert.containsOnce(target, '.o_org_chart_group_up .o_org_chart_entry_manager', "the chart should have 1 manager");
|
||||
assert.containsOnce(target, '.o_org_chart_group_down .o_org_chart_entry_sub', "the chart should have 1 subordinate");
|
||||
assert.containsOnce(target, '.o_org_chart_entry_self', "the chart should have only once the current employee");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add("indirect_subordinates_tour", {
|
||||
steps: () => [
|
||||
{
|
||||
content: "Click the number next to employee georges",
|
||||
trigger: "div[name='child_ids'] .o_org_chart_entry button > .badge:contains('2')",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Click Indirect Subordinates",
|
||||
trigger: ".o_org_chart_popup a.o_employee_sub_redirect[data-type='indirect']",
|
||||
run: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
registry.category("web_tour.tours").add("employee_view_access_multicompany", {
|
||||
steps: () => [
|
||||
{
|
||||
content: "Employee list view",
|
||||
trigger: ".o_switch_view.o_list",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Click on the employee C",
|
||||
trigger: 'table.o_list_table tbody td:contains("Employee C")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "Click the number next to employee C",
|
||||
trigger: "div[name='child_ids'] .o_org_chart_entry button > .badge:contains('1')",
|
||||
run: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue