forked from odoo/odoo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ADD] hr_org_chart: add hierarchy view
This commit adds a new view called hierarchy view, that view allows the user to visualize a hierarchy in a model. For instance, that view could be used for `hr.employee` to easily visualize the hierarchy inside the company (the manager and his employees) as an Organization Chart. That commit introduces a new method for the hierarchy view, called `hierarchy_read`, that method will be when the view will be loaded to get the initial data to display. Some details for the definition of that new view: ------------------------------------------------ Some attributes can be defined on the `hierarchy` tag: - `parent_field` (default='parent_id') contains the parent field name to use to find the parent record of the current to build the hierarchy. - `child_field` (optional attribute) contains the child field name to use to find the child records (it is in fact the one2many field). If that field is defined then some queries can be avoided since we directly fetch the children when a read/search_read is done. Inside the `hierarchy` tag, the fields to read can be added and then a template as to be defined as we do when we define the kanban view. The template to create is called `hierarchy-box` and that template will be used to display the card inside a node in the hierarchy view. task-3376776
- Loading branch information
Showing
16 changed files
with
1,446 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
||
from odoo import fields, models | ||
|
||
|
||
class ActWindowView(models.Model): | ||
_inherit = 'ir.actions.act_window.view' | ||
|
||
view_mode = fields.Selection(selection_add=[('hierarchy', 'Hierarchy')], ondelete={'hierarchy': 'cascade'}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
||
from lxml import etree | ||
|
||
from odoo import fields, models, _ | ||
|
||
HIERARCHY_VALID_ATTRIBUTES = { | ||
'__validate__', # ir.ui.view implementation detail | ||
'class', | ||
'js_class', | ||
'string', | ||
'create', | ||
'edit', | ||
'delete', | ||
'parent_field', | ||
'child_field', | ||
} | ||
|
||
class View(models.Model): | ||
_inherit = 'ir.ui.view' | ||
|
||
type = fields.Selection(selection_add=[('hierarchy', "Hierarchy")]) | ||
|
||
def _is_qweb_based_view(self, view_type): | ||
return super()._is_qweb_based_view(view_type) or view_type == "hierarchy" | ||
|
||
def _validate_tag_hierarchy(self, node, name_manager, node_info): | ||
if not node_info['validate']: | ||
return | ||
|
||
templates_count = 0 | ||
for child in node.iterchildren(tag=etree.Element): | ||
if child.tag == 'templates': | ||
if not templates_count: | ||
templates_count += 1 | ||
else: | ||
msg = _('Hierarchy view can contain only one templates tag') | ||
self._raise_view_error(msg, child) | ||
elif child.tag != 'field': | ||
msg = _('Hierarchy child can only be field or template, got %s', child.tag) | ||
self._raise_view_error(msg, child) | ||
|
||
remaining = set(node.attrib) - HIERARCHY_VALID_ATTRIBUTES | ||
if remaining: | ||
msg = _( | ||
"Invalid attributes (%s) in hierarchy view. Attributes must be in (%s)", | ||
','.join(remaining), ','.join(HIERARCHY_VALID_ATTRIBUTES), | ||
) | ||
self._raise_view_error(msg, node) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
||
from odoo import api, models | ||
|
||
|
||
class Base(models.AbstractModel): | ||
_inherit = 'base' | ||
|
||
@api.model | ||
def hierarchy_read(self, domain, fields, parent_field, child_field=None): | ||
if parent_field not in fields: | ||
fields.append(parent_field) | ||
records = self.search(domain) | ||
focus_record = self.env[self._name] | ||
fetch_child_ids_for_all_records = False | ||
if len(records) == 1: | ||
domain = [(parent_field, '=', records.id), ('id', '!=', records.id)] | ||
if records[parent_field]: | ||
focus_record = records | ||
records += focus_record[parent_field] | ||
domain = [('id', 'not in', records.ids), (parent_field, 'in', records.ids)] | ||
records += self.search(domain) | ||
elif not records: | ||
records = self.search([(parent_field, '=', False)]) | ||
else: | ||
fetch_child_ids_for_all_records = True | ||
children_ids_per_record_id = {} | ||
if not child_field: | ||
children_ids_per_record_id = { | ||
record.id: child_ids | ||
for record, child_ids in self._read_group( | ||
[(parent_field, 'in', records.ids if fetch_child_ids_for_all_records else (records - records[parent_field]).ids)], | ||
(parent_field,), | ||
('id:array_agg',), | ||
) | ||
} | ||
result = records.read(fields) | ||
if children_ids_per_record_id or focus_record: | ||
for record_data in result: | ||
if record_data['id'] in children_ids_per_record_id: | ||
record_data['__child_ids__'] = children_ids_per_record_id[record_data['id']] | ||
if record_data['id'] == focus_record.id: | ||
record_data['__focus__'] = True | ||
return result |
66 changes: 66 additions & 0 deletions
66
addons/hr_org_chart/static/src/views/hierarchy/hierarchy_arch_parser.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/** @odoo-module */ | ||
|
||
import { visitXML } from "@web/core/utils/xml"; | ||
import { Field } from "@web/views/fields/field"; | ||
import { getActiveActions } from "@web/views/utils"; | ||
|
||
export class HierarchyArchParser { | ||
parse(xmlDoc, models, modelName) { | ||
const archInfo = { | ||
activeActions: getActiveActions(xmlDoc), | ||
parentFieldName: "parent_id", | ||
fieldNodes: {}, | ||
templateDocs: {}, | ||
xmlDoc, | ||
}; | ||
const fieldNextIds = {}; | ||
const fields = models[modelName]; | ||
|
||
visitXML(xmlDoc, (node) => { | ||
if (node.hasAttribute("t-name")) { | ||
archInfo.templateDocs[node.getAttribute("t-name")] = node; | ||
return; | ||
} | ||
if (node.tagName === "hierarchy") { | ||
if (node.hasAttribute("parent_field")) { | ||
const parentFieldName = node.getAttribute("parent_field"); | ||
if (!(parentFieldName in fields)) { | ||
throw new Error(`The parent field set (${parentFieldName}) is not defined in the model (${modelName}).`); | ||
} else if (fields[parentFieldName].type !== "many2one") { | ||
throw new Error(`Invalid parent field, it should be a Many2One field.`); | ||
} else if (fields[parentFieldName].relation !== modelName) { | ||
throw new Error(`Invalid parent field, the co-model should be same model than the current one (expected: ${modelName}).`); | ||
} | ||
archInfo.parentFieldName = parentFieldName; | ||
} | ||
if (node.hasAttribute("child_field")) { | ||
const childFieldName = node.getAttribute("child_field"); | ||
if (!(childFieldName in fields)) { | ||
throw new Error(`The child field set (${childFieldName}) is not defined in the model (${modelName}).`); | ||
} else if (fields[childFieldName].type !== "one2many") { | ||
throw new Error(`Invalid child field, it should be a One2Many field.`); | ||
} else if (fields[childFieldName].relation !== modelName) { | ||
throw new Error(`Invalid child field, the co-model should be same model than the current one (expected: ${modelName}).`); | ||
} | ||
archInfo.childFieldName = childFieldName; | ||
} | ||
} else if (node.tagName === "field") { | ||
const fieldInfo = Field.parseFieldNode(node, models, modelName, "hierarchy"); | ||
const name = fieldInfo.name; | ||
if (!(name in fieldNextIds)) { | ||
fieldNextIds[name] = 0; | ||
} | ||
const fieldId = `${name}_${fieldNextIds[name]++}`; | ||
archInfo.fieldNodes[fieldId] = fieldInfo; | ||
node.setAttribute("field_id", fieldId); | ||
} | ||
}); | ||
|
||
const cardDoc = archInfo.templateDocs["hierarchy-box"]; | ||
if (!cardDoc) { | ||
throw new Error("Missing 'hierarchy-box' template."); | ||
} | ||
|
||
return archInfo; | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/** @odoo-module */ | ||
|
||
import { Component } from "@odoo/owl"; | ||
|
||
import { Field } from "@web/views/fields/field"; | ||
import { Record } from "@web/views/record"; | ||
import { ViewButton } from "@web/views/view_button/view_button"; | ||
import { useViewCompiler } from "@web/views/view_compiler"; | ||
|
||
import { OrgChartCompiler } from "./hierarchy_compiler"; | ||
import { getFormattedRecord } from "@web/views/kanban/kanban_record"; | ||
|
||
export class HierarchyCard extends Component { | ||
static components = { | ||
Record, | ||
Field, | ||
ViewButton, | ||
}; | ||
static props = { | ||
node: Object, | ||
openRecord: Function, | ||
archInfo: Object, | ||
templates: Object, | ||
classNames: { type: String, optional: true }, | ||
}; | ||
static defaultProps = { | ||
classNames: "", | ||
}; | ||
static template = "hr_org_chart.HierarchyCard"; | ||
static Compiler = OrgChartCompiler; | ||
|
||
setup() { | ||
const { templates } = this.props; | ||
this.templates = useViewCompiler(this.constructor.Compiler, templates); | ||
} | ||
|
||
getRenderingContext(data) { | ||
const record = getFormattedRecord(data.record); | ||
return { | ||
context: this.props.node.context, | ||
JSON, | ||
luxon, | ||
record, | ||
__comp__: Object.assign(Object.create(this), { this: this }), | ||
__record__: data.record, | ||
}; | ||
} | ||
|
||
onGlobalClick(ev) { | ||
if (ev.target.closest("button")) { | ||
return; | ||
} | ||
this.props.openRecord(this.props.node); | ||
} | ||
|
||
onClickArrowUp(ev) { | ||
this.props.node.fetchParentNode(); | ||
} | ||
|
||
onClickArrowDown(ev) { | ||
if (this.props.node.nodes.length) { | ||
this.props.node.collapseChildNodes(); | ||
} else { | ||
this.props.node.showChildNodes(); | ||
} | ||
} | ||
} |
69 changes: 69 additions & 0 deletions
69
addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
@mixin o-hierarchy-node-color { | ||
@for $size from 2 through length($o-colors) { | ||
// Note: the first color is not defined as it is the 'no color' for org chart node | ||
.o_hierarchy_node_color_#{$size - 1} { | ||
background-color: nth($o-colors, $size); | ||
} | ||
} | ||
} | ||
|
||
.o_hierarchy_node_container { | ||
width: 250px; | ||
min-height: 130px; | ||
|
||
.o_hierarchy_node_button_container { | ||
height: 30px; | ||
} | ||
|
||
.o_hierarchy_node { | ||
&:hover { | ||
cursor: pointer; | ||
} | ||
|
||
&.o_hierarchy_node_highlighted { | ||
border: 3px solid green; | ||
} | ||
|
||
.o_hierarchy_node_header { | ||
height: 30px; | ||
|
||
div.o_field_background_image { | ||
width: 48px; | ||
height: 48px; | ||
margin-bottom: -24px; | ||
transform: translateY(-24px); | ||
z-index: 1; | ||
|
||
> img { | ||
border-radius: 50%; | ||
} | ||
} | ||
} | ||
|
||
.o_hierarchy_node_body { | ||
height: 65px; | ||
|
||
.o_employee_availability { | ||
position: absolute; | ||
top: 4px; | ||
right: 2px; | ||
} | ||
} | ||
|
||
.o_hierarchy_node_footer { | ||
height: 30px; | ||
} | ||
|
||
.o_hierarchy_node_color_0 { | ||
background-color: $gray-200; | ||
} | ||
|
||
@include o-hierarchy-node-color; | ||
} | ||
|
||
.o_hierarchy_node_button { | ||
display: grid; | ||
grid-template-columns: 50px 1fr 50px; | ||
border-radius: 0; | ||
} | ||
} |
59 changes: 59 additions & 0 deletions
59
addons/hr_org_chart/static/src/views/hierarchy/hierarchy_card.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?xml version="1.0" encoding="utf-8" ?> | ||
<templates> | ||
|
||
<t t-name="hr_org_chart.HierarchyCard"> | ||
<div class="o_hierarchy_node_container mb-4 d-flex flex-column" t-att-class="props.classNames"> | ||
<div class="o_hierarchy_node_button_container w-100 d-flex justify-content-end"> | ||
<button t-if="props.node.parentResId !== false and !props.node.parentNode" | ||
name="hierarchy_search_parent_node" | ||
class="btn p-0" | ||
t-on-click.synthetic="onClickArrowUp" | ||
> | ||
<i class="fa fa-chevron-up"/> | ||
</button> | ||
</div> | ||
<div class="o_hierarchy_node w-100 h-100 d-flex flex-column justify-content-between" | ||
t-att-class="{ | ||
o_hierarchy_node_highlighted: props.node.isFocused, | ||
border: !props.node.isFocused, | ||
'border-bottom-0': !props.node.isFocused and (props.node.nodes.length or props.node.canShowChildNodes), | ||
}" | ||
t-att-data-node-id="props.node.id" | ||
t-on-click.synthetic="onGlobalClick" | ||
> | ||
<div class="o_hierarchy_node_content"> | ||
<Record resModel="props.node.model.resModel" | ||
resId="props.node.resId" | ||
fields="props.node.model.fields" | ||
activeFields="props.node.model.activeFields" | ||
values="props.node.data" | ||
t-slot-scope="data" | ||
> | ||
<t t-call="{{ templates['hierarchy-box'] }}" t-call-context="getRenderingContext(data)"/> | ||
</Record> | ||
</div> | ||
<button t-if="props.node.nodes.length or props.node.canShowChildNodes" | ||
name="hierarchy_search_subsidiaries" | ||
t-att-class="{ | ||
'o_hierarchy_node_button w-100 btn pt-1': true, | ||
'btn-primary': !props.node.nodes.length, | ||
'btn-secondary': props.node.nodes.length > 0, | ||
}" | ||
t-on-click.synthetic="onClickArrowDown" | ||
> | ||
<t t-if="!props.node.nodes.length"> | ||
<span style="grid-column: 2;"> | ||
Unfold | ||
</span> | ||
</t> | ||
<t t-else=""> | ||
<span style="grid-column: 2;"> | ||
Fold | ||
</span> | ||
</t> | ||
</button> | ||
</div> | ||
</div> | ||
</t> | ||
|
||
</templates> |
Oops, something went wrong.