first commit, nothing working yet
This commit is contained in:
parent
4db6dab0eb
commit
ae086d645c
12 changed files with 511 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
|
||||
42
recursive_tree_view/__manifest__.py
Normal file
42
recursive_tree_view/__manifest__.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#
|
||||
# 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/list_arch_parser.js",
|
||||
"recursive_tree_view/static/src/list_controller.js",
|
||||
"recursive_tree_view/static/src/list_renderer.js",
|
||||
"recursive_tree_view/static/src/tree_recursive_styles.css",
|
||||
"recursive_tree_view/static/src/recursive_tree_templates.xml",
|
||||
]
|
||||
},
|
||||
"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>
|
||||
23
recursive_tree_view/static/src/list_arch_parser.js
Normal file
23
recursive_tree_view/static/src/list_arch_parser.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/** @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";
|
||||
result.childField = xmlDoc.getAttribute("child-field")
|
||||
|
||||
if (!(result.childField in result)) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
})
|
||||
81
recursive_tree_view/static/src/list_controller.js
Normal file
81
recursive_tree_view/static/src/list_controller.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {ListController} from '@web/views/list/list_controller';
|
||||
import {patch} from '@web/core/utils/patch';
|
||||
import {useService} from '@web/core/utils/hooks';
|
||||
import {onWillRender} from '@odoo/owl';
|
||||
import {useBus} from "@web/core/utils/hooks";
|
||||
|
||||
patch(ListController.prototype, {
|
||||
async setup() {
|
||||
useBus(this.env.bus, "expandRow", this.onExpandRow)
|
||||
useBus(this.env.bus, "collapseRow", this.onCollapseRow)
|
||||
super.setup();
|
||||
if (this.props.archInfo.recursive) {
|
||||
// Fetch the parent field from the model
|
||||
const parentField = await this.orm.call(
|
||||
'parent.field.service',
|
||||
'get_parent_field',
|
||||
[this.props.resModel],
|
||||
);
|
||||
|
||||
// Validate the parentField and adjust archInfo accordingly
|
||||
if (parentField) {
|
||||
// If parentField is valid, store it and continue with recursive setup
|
||||
this.parentField = parentField;
|
||||
this.props.archInfo.parentField = parentField;
|
||||
} else {
|
||||
// If no valid parentField, disable recursion and childField functionality
|
||||
this.props.archInfo.recursive = false;
|
||||
delete this.props.archInfo.childField;
|
||||
}
|
||||
|
||||
// Proceed with recursive setup if recursive is enabled
|
||||
if (this.props.archInfo.recursive) {
|
||||
this.childrenByParent = {}; // Cache for loaded children
|
||||
|
||||
// Bind event listeners for expand and collapse
|
||||
this.model.hooks.onRootLoaded = async () => {
|
||||
const rootRecords = this.model.root.records;
|
||||
const rootIds = rootRecords.map(record => record.resId)
|
||||
const childRecords = await this.orm.searchRead(
|
||||
this.props.resModel,
|
||||
[[this.parentField, 'in', rootIds]],
|
||||
[],
|
||||
)
|
||||
const rootIdsWithChildren = new Set(childRecords.map(child => child[this.parentField]));
|
||||
rootRecords.forEach(record => {
|
||||
record.data.hasChildren = rootIdsWithChildren.has(record.data.resId);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async fetchChildren(parentId) {
|
||||
if (this.childrenByParent[parentId]) {
|
||||
return this.childrenByParent[parentId];
|
||||
}
|
||||
|
||||
const children = await this.rpc({
|
||||
model: this.props.resModel,
|
||||
method: 'search_read',
|
||||
args: [[this.parentField, '=', parentId]],
|
||||
kwargs: {fields: ['id', 'name', this.parentField]},
|
||||
});
|
||||
|
||||
this.childrenByParent[parentId] = children;
|
||||
return children;
|
||||
},
|
||||
|
||||
async onExpandRow(event) {
|
||||
const {parentId} = event.data;
|
||||
const children = await this.fetchChildren(parentId);
|
||||
this.renderer.renderChildrenRows(children, parentId);
|
||||
},
|
||||
|
||||
onCollapseRow(event) {
|
||||
const {parentId} = event.data;
|
||||
this.renderer.removeChildrenRows(parentId);
|
||||
},
|
||||
});
|
||||
59
recursive_tree_view/static/src/list_renderer.js
Normal file
59
recursive_tree_view/static/src/list_renderer.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ListRenderer } from '@web/views/list/list_renderer';
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
|
||||
patch(ListRenderer.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.recursive = this.props.archInfo.recursive;
|
||||
},
|
||||
|
||||
async _onExpandClick(ev) {
|
||||
const $button = $(ev.currentTarget);
|
||||
const isExpanded = $button.hasClass('expanded');
|
||||
$button.toggleClass('expanded', !isExpanded).text(isExpanded ? '+' : '-');
|
||||
|
||||
const parentId = $button.data('expand');
|
||||
if (!isExpanded) {
|
||||
// Emit an event to request the controller to expand the row
|
||||
this.env.bus.trigger('expandRow', { parentId });
|
||||
} else {
|
||||
// Emit an event to request the controller to collapse the row
|
||||
this.env.bus.trigger('collapseRow', { parentId });
|
||||
}
|
||||
},
|
||||
|
||||
renderChildrenRows(children, parentId) {
|
||||
const $parentRow = this.$(`tr[data-id="${parentId}"]`);
|
||||
const parentDepth = $parentRow.data('depth') || 0;
|
||||
|
||||
children.forEach(child => {
|
||||
const $row = $('<tr>')
|
||||
.attr('data-id', child.id)
|
||||
.attr('data-parent-id', parentId)
|
||||
.attr('data-depth', parentDepth + 1)
|
||||
.attr('data-has-children', child.hasChildren)
|
||||
.addClass('o_recursive_child_row')
|
||||
.css('padding-left', `${(parentDepth + 1) * 20}px`);
|
||||
|
||||
for (const [fieldName, fieldValue] of Object.entries(child)) {
|
||||
const $cell = $('<td>').text(fieldValue);
|
||||
$row.append($cell);
|
||||
}
|
||||
|
||||
$parentRow.after($row);
|
||||
});
|
||||
},
|
||||
|
||||
removeChildrenRows(parentId) {
|
||||
const childRows = this.$(`tr[data-parent-id="${parentId}"]`);
|
||||
|
||||
childRows.each((index, childRow) => {
|
||||
const childId = $(childRow).data('id');
|
||||
this.removeChildrenRows(childId);
|
||||
});
|
||||
|
||||
childRows.remove();
|
||||
},
|
||||
});
|
||||
22
recursive_tree_view/static/src/recursive_tree_templates.xml
Normal file
22
recursive_tree_view/static/src/recursive_tree_templates.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?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"></th>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<!-- Extend List Renderer Row to Add Expand/Collapse Button -->
|
||||
<t t-name="recursive_tree_view.RecordRow" t-inherit="web.ListRenderer.RecordRow" t-inherit-mode="extension">
|
||||
<xpath expr="//td[1]" position="before">
|
||||
<td t-if="this.recursive" class="o_recursive_expand_column">
|
||||
<!-- Use @click to bind the button click to _onExpandClick -->
|
||||
<button t-if="record.data.hasChildren"
|
||||
t-att-data-expand="props.record.id.raw_value"
|
||||
t-on-click.prevent="_onExpandClick"
|
||||
class="o_expand_button">+</button>
|
||||
</td>
|
||||
</xpath>
|
||||
</t>
|
||||
</odoo>
|
||||
80
recursive_tree_view/static/src/relational_model.js
Normal file
80
recursive_tree_view/static/src/relational_model.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {RelationalModel} from '@web/model/relational_model/relational_model';
|
||||
import {patch} from '@web/core/utils/patch';
|
||||
import {
|
||||
getFieldsSpec,
|
||||
makeActiveField,
|
||||
getBasicEvalContext
|
||||
} from "@web/model/relational_model/utils";
|
||||
|
||||
patch(RelationalModel.prototype, 'recursive-list-extension', {
|
||||
/**
|
||||
* Override to add child record tracking.
|
||||
* Fetches records and marks records with `hasChildren` if they have child records.
|
||||
*/
|
||||
async _loadRecords(config, evalContext = config.context) {
|
||||
const {resModel, resIds, activeFields, fields, context} = config;
|
||||
const parentField = await this.orm.call(
|
||||
'parent.field.service',
|
||||
'get_parent_field',
|
||||
[resModel],
|
||||
);
|
||||
activeFields[parentField] = makeActiveField()
|
||||
const records = await super._loadRecords(config, evalContext);
|
||||
// Get the parent ID field if there is one
|
||||
if (records && parentField) {
|
||||
const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);
|
||||
// Fetch records with the additional field
|
||||
const parentIds = resIds
|
||||
const children = await this.orm.webSearchRead(resModel, [[parentField, "in", parentIds]], {
|
||||
context: {bin_size: true, ...context},
|
||||
specification: fieldSpec,
|
||||
});
|
||||
|
||||
// Track children by grouping child records under each parent
|
||||
const childrenByParent = {};
|
||||
for (const child of children) {
|
||||
const parentId = child[parentField];
|
||||
if (parentId) {
|
||||
if (!childrenByParent[parentId]) {
|
||||
childrenByParent[parentId] = [];
|
||||
}
|
||||
childrenByParent[parentId].push(child);
|
||||
}
|
||||
}
|
||||
|
||||
records.forEach(record => {
|
||||
record.children = new this.constructor.DynamicRecordList(this, config, childrenByParent[record.resId]);
|
||||
});
|
||||
}
|
||||
return records;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch and load child records dynamically for a given parent record ID.
|
||||
* Adds children to the model’s records to ensure they’re tracked in the model’s state.
|
||||
* @param {number} parentId - The ID of the parent record.
|
||||
* @returns {Promise<Array>} - List of child records as model records.
|
||||
*/
|
||||
async fetchChildren(parentId) {
|
||||
const config = self.config
|
||||
const {resModel, resIds, activeFields, fields, context} = config;
|
||||
const evalContext = getBasicEvalContext(config);
|
||||
const fieldSpec = getFieldsSpec(activeFields, fields, evalContext);
|
||||
const parentRecord = this.root.records.find((r) => r.resId == parentId)
|
||||
if (!parentRecord) {
|
||||
throw Error("Attempt to find parent record failed.");
|
||||
}
|
||||
// Fetch child records where `parent_id` matches the given `parentId`
|
||||
const childrenData = await this.orm.webSearchRead(resModel, [[this.parentField, '=', parentId]], {
|
||||
context: context,
|
||||
specification: fieldSpec,
|
||||
});
|
||||
if (childrenData) {
|
||||
parentRecord.children = new this.constructor.DynamicRecordList(this, this.config, childrenData);
|
||||
}
|
||||
return parentRecord.children;
|
||||
},
|
||||
|
||||
});
|
||||
15
recursive_tree_view/static/src/tree_recursive_styles.css
Normal file
15
recursive_tree_view/static/src/tree_recursive_styles.css
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.o_recursive_expand_column {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
tr[data-depth="1"] .o_recursive_expand_column {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
tr[data-depth="2"] .o_recursive_expand_column {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
tr[data-depth="3"] .o_recursive_expand_column {
|
||||
padding-left: 60px;
|
||||
}
|
||||
64
recursive_tree_view/validation.py
Normal file
64
recursive_tree_view/validation.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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",
|
||||
)
|
||||
child_field_attr = etree.SubElement(
|
||||
optional_attr,
|
||||
_tag="{%s}attribute" % RNG_NS,
|
||||
name="child-field",
|
||||
)
|
||||
|
||||
# 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