Compare commits
2 commits
18.0
...
17.0-recur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6065636123 | ||
|
|
ae086d645c |
12 changed files with 501 additions and 0 deletions
2
recursive_tree_view/__init__.py
Normal file
2
recursive_tree_view/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import models
|
||||||
|
from . import validation
|
||||||
38
recursive_tree_view/__manifest__.py
Normal file
38
recursive_tree_view/__manifest__.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#
|
||||||
|
# Bemade Inc.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||||
|
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||||
|
#
|
||||||
|
# This program is under the terms of the GNU Lesser General Public License,
|
||||||
|
# version 3.
|
||||||
|
#
|
||||||
|
# For full license details, see https://www.gnu.org/licenses/lgpl-3.0.en.html.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
# DEALINGS IN THE SOFTWARE.
|
||||||
|
#
|
||||||
|
{
|
||||||
|
"name": "Recursive Tree View",
|
||||||
|
"version": "17.0.0.0.1",
|
||||||
|
"summary": "Adds the ability to mark a tree view as recursive, to expand "
|
||||||
|
"descendants of a parent record.",
|
||||||
|
"category": "Technical",
|
||||||
|
"author": "Bemade Inc.",
|
||||||
|
"website": "http://www.bemade.org",
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"depends": ["web"],
|
||||||
|
"data": [],
|
||||||
|
"assets": {
|
||||||
|
"web.assets_backend": [
|
||||||
|
"recursive_tree_view/static/src/**/*",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"installable": True,
|
||||||
|
"application": False,
|
||||||
|
}
|
||||||
1
recursive_tree_view/models/__init__.py
Normal file
1
recursive_tree_view/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
18
recursive_tree_view/models/models.py
Normal file
18
recursive_tree_view/models/models.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# models/parent_field_service.py
|
||||||
|
from odoo import models, api
|
||||||
|
|
||||||
|
|
||||||
|
class ParentFieldService(models.AbstractModel):
|
||||||
|
_name = "parent.field.service"
|
||||||
|
_description = "Service to retrieve parent field dynamically for any model"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_parent_field(self, model_name):
|
||||||
|
"""
|
||||||
|
Returns the parent field name for the given model.
|
||||||
|
If _parent_name is not set, defaults to 'parent_id' if the field exists.
|
||||||
|
"""
|
||||||
|
Model = self.env[model_name] # Access the model dynamically
|
||||||
|
return Model._parent_name or (
|
||||||
|
"parent_id" if hasattr(Model, "parent_id") else None
|
||||||
|
)
|
||||||
104
recursive_tree_view/rng/tree_view.rng
Normal file
104
recursive_tree_view/rng/tree_view.rng
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
|
||||||
|
xmlns:a="http://relaxng.org/ns/annotation/1.0"
|
||||||
|
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
|
||||||
|
<!-- Handling of element overloading when inheriting from a base
|
||||||
|
template
|
||||||
|
-->
|
||||||
|
<rng:include href="odoo/base/rng/common.rng"/>
|
||||||
|
|
||||||
|
<rng:define name="groupby">
|
||||||
|
<rng:element name="groupby">
|
||||||
|
<rng:attribute name="name"/>
|
||||||
|
<rng:optional><rng:attribute name="expand"/></rng:optional>
|
||||||
|
<rng:zeroOrMore>
|
||||||
|
<rng:ref name="field"/>
|
||||||
|
</rng:zeroOrMore>
|
||||||
|
<rng:zeroOrMore>
|
||||||
|
<rng:ref name="button"/>
|
||||||
|
</rng:zeroOrMore>
|
||||||
|
</rng:element>
|
||||||
|
</rng:define>
|
||||||
|
|
||||||
|
<rng:define name="tree">
|
||||||
|
<rng:element name="tree">
|
||||||
|
<rng:ref name="overload"/>
|
||||||
|
<rng:optional><rng:attribute name="name"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="create"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="delete"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="edit"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="multi_edit"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="export_xlsx"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="duplicate"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="import"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="string"/></rng:optional> <!-- deprecated, has no effect anymore -->
|
||||||
|
<rng:optional><rng:attribute name="class"/></rng:optional>
|
||||||
|
<!-- Allows to take a custom View widget for handling -->
|
||||||
|
<rng:optional><rng:attribute name="js_class"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="default_order"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="default_group_by"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="decoration-bf"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="decoration-it"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="decoration-danger"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="decoration-info"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="decoration-muted"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="decoration-primary"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="decoration-success"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="decoration-warning"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="banner_route"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="sample"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="action"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="type"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="open_form_view"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="recursive"/></rng:optional>
|
||||||
|
<rng:optional>
|
||||||
|
<rng:attribute name="limit">
|
||||||
|
<rng:data type="int"/>
|
||||||
|
</rng:attribute>
|
||||||
|
</rng:optional>
|
||||||
|
<rng:optional>
|
||||||
|
<rng:attribute name="count_limit">
|
||||||
|
<rng:data type="int"/>
|
||||||
|
</rng:attribute>
|
||||||
|
</rng:optional>
|
||||||
|
<rng:optional>
|
||||||
|
<rng:attribute name="groups_limit">
|
||||||
|
<rng:data type="int"/>
|
||||||
|
</rng:attribute>
|
||||||
|
</rng:optional>
|
||||||
|
<rng:optional>
|
||||||
|
<rng:attribute name="editable">
|
||||||
|
<rng:choice>
|
||||||
|
<rng:value>top</rng:value>
|
||||||
|
<rng:value>bottom</rng:value>
|
||||||
|
</rng:choice>
|
||||||
|
</rng:attribute>
|
||||||
|
</rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="expand"/></rng:optional>
|
||||||
|
<rng:zeroOrMore>
|
||||||
|
<rng:choice>
|
||||||
|
<rng:element name="header">
|
||||||
|
<rng:zeroOrMore>
|
||||||
|
<rng:ref name="button"/>
|
||||||
|
</rng:zeroOrMore>
|
||||||
|
</rng:element>
|
||||||
|
<rng:ref name="control"/>
|
||||||
|
<rng:ref name="field"/>
|
||||||
|
<rng:ref name="widget"/>
|
||||||
|
<rng:ref name="separator"/>
|
||||||
|
<rng:ref name="tree"/>
|
||||||
|
<rng:ref name="groupby"/>
|
||||||
|
<rng:ref name="button"/>
|
||||||
|
<rng:ref name="filter"/>
|
||||||
|
<rng:ref name="html"/>
|
||||||
|
<rng:element name="newline"><rng:empty/></rng:element>
|
||||||
|
</rng:choice>
|
||||||
|
</rng:zeroOrMore>
|
||||||
|
</rng:element>
|
||||||
|
</rng:define>
|
||||||
|
<rng:start>
|
||||||
|
<rng:choice>
|
||||||
|
<rng:ref name="tree" />
|
||||||
|
</rng:choice>
|
||||||
|
</rng:start>
|
||||||
|
</rng:grammar>
|
||||||
16
recursive_tree_view/static/src/list_arch_parser.js
Normal file
16
recursive_tree_view/static/src/list_arch_parser.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { ListArchParser} from "@web/views/list/list_arch_parser";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
patch(ListArchParser.prototype, {
|
||||||
|
parse(xmlDoc, models, modelName) {
|
||||||
|
const result = super.parse(...arguments);
|
||||||
|
|
||||||
|
const recursiveAttr = xmlDoc.getAttribute("recursive")
|
||||||
|
if ( recursiveAttr ) {
|
||||||
|
result.recursive = recursiveAttr === "1" || recursiveAttr === "true" || recursiveAttr === "True";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
})
|
||||||
37
recursive_tree_view/static/src/list_controller.js
Normal file
37
recursive_tree_view/static/src/list_controller.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import {ListController} from '@web/views/list/list_controller';
|
||||||
|
import {patch} from '@web/core/utils/patch';
|
||||||
|
import {useBus} from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
patch(ListController.prototype, {
|
||||||
|
async setup() {
|
||||||
|
if (this.props.archInfo.recursive) {
|
||||||
|
this.recursive = true;
|
||||||
|
useBus(this.env.bus, "expand-collapse-parent", this.onExpandCollapseParent);
|
||||||
|
} else {
|
||||||
|
this.recursive = false;
|
||||||
|
}
|
||||||
|
super.setup();
|
||||||
|
},
|
||||||
|
|
||||||
|
async onExpandCollapseParent(ev) {
|
||||||
|
const parentId = ev.detail;
|
||||||
|
const record = this.model.findRecordInHierarchy(parentId);
|
||||||
|
if (record.expanded) {
|
||||||
|
record.expanded = false;
|
||||||
|
} else {
|
||||||
|
await this.model._loadChildren(record.children);
|
||||||
|
record.expanded = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get modelParams() {
|
||||||
|
const params = super.modelParams;
|
||||||
|
if (this.recursive) {
|
||||||
|
params["config"]["recursive"] = true;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
19
recursive_tree_view/static/src/list_renderer.js
Normal file
19
recursive_tree_view/static/src/list_renderer.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { ListRenderer } from '@web/views/list/list_renderer';
|
||||||
|
import { patch } from '@web/core/utils/patch';
|
||||||
|
import { useState } from '@odoo/owl';
|
||||||
|
|
||||||
|
patch(ListRenderer.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
this.recursive = this.props.archInfo.recursive;
|
||||||
|
useState(this.props.list.records);
|
||||||
|
},
|
||||||
|
|
||||||
|
async _onExpandClick(ev) {
|
||||||
|
const $button = $(ev.currentTarget);
|
||||||
|
const parent = $button.data('expand');
|
||||||
|
this.env.bus.trigger("expand-collapse-parent", parent);
|
||||||
|
},
|
||||||
|
});
|
||||||
40
recursive_tree_view/static/src/recursive_tree_templates.xml
Normal file
40
recursive_tree_view/static/src/recursive_tree_templates.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Extend List Renderer Header to Add Extra Column for Expand/Collapse Button Alignment -->
|
||||||
|
<t t-name="recursive_tree_view.Renderer" t-inherit="web.ListRenderer"
|
||||||
|
t-inherit-mode="extension">
|
||||||
|
<xpath expr="//th[hasclass('o_list_record_selector')]" position="before">
|
||||||
|
<th t-if="recursive"
|
||||||
|
class="o_recursive_expand_column cursor-default o_list_button">
|
||||||
|
<div style="min-width: 100px;"/>
|
||||||
|
</th>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Extend List Renderer Row to Add Expand/Collapse Button -->
|
||||||
|
<t t-name="web.ListRenderer.RecordRow" t-inherit="web.ListRenderer.RecordRow"
|
||||||
|
t-inherit-mode="extension">
|
||||||
|
<xpath expr="//tr[hasclass('o_data_row')]" position="after">
|
||||||
|
<t t-if="record.children && record.expanded">
|
||||||
|
<t t-foreach="record.children" t-as="record" t-key="record.id">
|
||||||
|
<t t-call="{{ constructor.recordRowTemplate }}"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//td[1]" position="before">
|
||||||
|
<td t-if="this.recursive"
|
||||||
|
class="o_recursive_expand_column"
|
||||||
|
>
|
||||||
|
<button t-if="record.children"
|
||||||
|
t-att-data-expand="record.resId"
|
||||||
|
t-on-click.prevent="_onExpandClick"
|
||||||
|
t-att-data-depth="record.depth"
|
||||||
|
class="o_expand_button">
|
||||||
|
<span role="img"
|
||||||
|
t-att-class="'fa ' + (record.expanded ? 'fa-angle-down' : 'fa-angle-right')"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
</odoo>
|
||||||
139
recursive_tree_view/static/src/relational_model.js
Normal file
139
recursive_tree_view/static/src/relational_model.js
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import {RelationalModel} from '@web/model/relational_model/relational_model';
|
||||||
|
import {patch} from '@web/core/utils/patch';
|
||||||
|
import {getFieldsSpec, getBasicEvalContext} from "@web/model/relational_model/utils";
|
||||||
|
|
||||||
|
patch(RelationalModel.prototype, {
|
||||||
|
// TODO: Modify the domain to include only records with parentField = False in the root search
|
||||||
|
|
||||||
|
setup(params, services) {
|
||||||
|
super.setup(...arguments);
|
||||||
|
this.hooks.onRootLoaded = () => {
|
||||||
|
const root = this.root;
|
||||||
|
const config = this.root.config;
|
||||||
|
if (config.recursive && root.records) {
|
||||||
|
this._loadChildren(root.records, config).then(() => {
|
||||||
|
return root;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async _loadData(config) {
|
||||||
|
if (config.recursive) {
|
||||||
|
const parentField = await this._get_parent_field(config.resModel);
|
||||||
|
const domain = [parentField, '=', false];
|
||||||
|
if (!(domain in config.domain)) {
|
||||||
|
config.domain = config.domain.concat([domain]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super._loadData(config);
|
||||||
|
},
|
||||||
|
async _loadChildren(records, config = undefined) {
|
||||||
|
if (!records) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!config) {
|
||||||
|
config = this.config;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(records)) {
|
||||||
|
records = [records];
|
||||||
|
}
|
||||||
|
const {resModel, activeFields, fields, context} = config;
|
||||||
|
if (!resModel) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const parentField = await this._get_parent_field(resModel);
|
||||||
|
if (!parentField) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const evalContext = getBasicEvalContext(config);
|
||||||
|
const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);
|
||||||
|
// Fetch records with the additional field
|
||||||
|
const parentIds = records.map(record => record.resId);
|
||||||
|
|
||||||
|
const children = await this.orm.webSearchRead(
|
||||||
|
resModel, [[parentField, "in", parentIds]], {
|
||||||
|
context: {...context},
|
||||||
|
specification: fieldSpec,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (children && children.length) {
|
||||||
|
// Track children by grouping child records under each parent
|
||||||
|
const childrenByParent = {};
|
||||||
|
for (const child of children.records) {
|
||||||
|
const parentId = child[parentField].id;
|
||||||
|
if (parentId) {
|
||||||
|
if (!childrenByParent[parentId]) {
|
||||||
|
childrenByParent[parentId] = [];
|
||||||
|
}
|
||||||
|
childrenByParent[parentId].push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parent of records) {
|
||||||
|
if (parent.depth == undefined) {
|
||||||
|
parent.depth = 0;
|
||||||
|
}
|
||||||
|
if (parent.expanded == undefined) {
|
||||||
|
parent.expanded = false;
|
||||||
|
}
|
||||||
|
if (!parent.childrenFetched) {
|
||||||
|
if (childrenByParent[parent.resId]) {
|
||||||
|
const childRecords = childrenByParent[parent.resId];
|
||||||
|
parent.children = []
|
||||||
|
for (const child of childRecords) {
|
||||||
|
const childRecord = new this.constructor.Record(
|
||||||
|
this,
|
||||||
|
{
|
||||||
|
context: context,
|
||||||
|
activeFields: activeFields,
|
||||||
|
resModel: resModel,
|
||||||
|
fields: fields,
|
||||||
|
resId: child.id,
|
||||||
|
resIds: [child.id],
|
||||||
|
isMonoRecord: true,
|
||||||
|
currentCompanyId: parent.currentCompanyId,
|
||||||
|
mode: parent.mode,
|
||||||
|
},
|
||||||
|
child,
|
||||||
|
{manuallyAdded: false},
|
||||||
|
);
|
||||||
|
parent.children.push(childRecord);
|
||||||
|
childRecord.parent = parent;
|
||||||
|
childRecord.depth = parent.depth + 1;
|
||||||
|
childRecord.expanded = false;
|
||||||
|
childRecord.childrenFetched = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent.childrenFetched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async _get_parent_field(model) {
|
||||||
|
return await this.orm.call(
|
||||||
|
'parent.field.service',
|
||||||
|
'get_parent_field',
|
||||||
|
[model],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
findRecordInHierarchy(resId) {
|
||||||
|
const records = this.root.records;
|
||||||
|
function findRecord(records) {
|
||||||
|
for (let record of records) {
|
||||||
|
if (record.resId === resId) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
if (record.children && record.children.length >0) {
|
||||||
|
const found = findRecord(record.children);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findRecord(records) || null;
|
||||||
|
}
|
||||||
|
});
|
||||||
28
recursive_tree_view/static/src/tree_recursive_styles.css
Normal file
28
recursive_tree_view/static/src/tree_recursive_styles.css
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
.o_recursive_expand_column {
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_expand_button {
|
||||||
|
border-color: transparent;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.o_expand_button[data-depth="1"] {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.o_expand_button[data-depth="2"] {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.o_expand_button[data-depth="3"] {
|
||||||
|
margin-left: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.o_expand_button[data-depth="4"] {
|
||||||
|
margin-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.o_expand_button[data-depth="5"] {
|
||||||
|
margin-left: 75px;
|
||||||
|
}
|
||||||
59
recursive_tree_view/validation.py
Normal file
59
recursive_tree_view/validation.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from lxml import etree
|
||||||
|
from odoo.tools import misc, view_validation
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_tree_validator = None
|
||||||
|
RNG_NS = "http://relaxng.org/ns/structure/1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def schema_tree(arch, **kwargs):
|
||||||
|
global _tree_validator
|
||||||
|
|
||||||
|
if _tree_validator is None:
|
||||||
|
try:
|
||||||
|
# Load and parse the existing schema file
|
||||||
|
schema_path = os.path.join("addons", "base", "rng", "tree_view.rng")
|
||||||
|
with misc.file_open(schema_path) as f:
|
||||||
|
rng_doc = etree.parse(f)
|
||||||
|
|
||||||
|
# Find the `tree` definition in the parsed document
|
||||||
|
tree_define = rng_doc.find(".//{%s}define[@name='tree']" % RNG_NS)
|
||||||
|
if tree_define is not None:
|
||||||
|
# Add the `recursive` attribute as an optional attribute in the `tree` definition
|
||||||
|
tree_elem = tree_define.find(".//{%s}element[@name='tree']" % RNG_NS)
|
||||||
|
if tree_elem is not None:
|
||||||
|
optional_attr = etree.SubElement(
|
||||||
|
tree_elem,
|
||||||
|
_tag="{%s}optional" % RNG_NS,
|
||||||
|
)
|
||||||
|
recursive_attr = etree.SubElement(
|
||||||
|
optional_attr,
|
||||||
|
_tag="{%s}attribute" % RNG_NS,
|
||||||
|
name="recursive",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create RelaxNG validator from the modified schema
|
||||||
|
_tree_validator = etree.RelaxNG(rng_doc)
|
||||||
|
|
||||||
|
except (etree.RelaxNGParseError, etree.XMLSyntaxError) as e:
|
||||||
|
_logger.error("Error parsing RelaxNG schema: %s", e.error_log)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error("General error loading RelaxNG schema: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate the XML arch
|
||||||
|
if _tree_validator.validate(arch):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Log validation errors
|
||||||
|
for error in _tree_validator.error_log:
|
||||||
|
_logger.error("Validation error: %s", error)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Register the validator with Odoo's view validation
|
||||||
|
view_validation._validators.update(tree=[schema_tree])
|
||||||
Loading…
Reference in a new issue