Tower: upload cetmix_tower_server 16.0.3.0.1 (via marketplace)
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# Copyright (C) 2024 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class CxTowerJetTemplateDependency(models.Model):
|
||||
"""Define dependencies between Jet templates"""
|
||||
|
||||
_name = "cx.tower.jet.template.dependency"
|
||||
_inherit = "cx.tower.reference.mixin"
|
||||
_description = "Cetmix Tower Jet Template Dependency"
|
||||
_log_access = False
|
||||
|
||||
name = fields.Char(related="template_id.name", readonly=True)
|
||||
template_id = fields.Many2one(
|
||||
string="Jet",
|
||||
comodel_name="cx.tower.jet.template",
|
||||
ondelete="cascade",
|
||||
required=True,
|
||||
help="The Jet template that requires another template",
|
||||
)
|
||||
|
||||
template_required_id = fields.Many2one(
|
||||
string="Required Jet",
|
||||
comodel_name="cx.tower.jet.template",
|
||||
ondelete="restrict",
|
||||
required=True,
|
||||
help="The Jet template that is required to be in a specific state",
|
||||
domain="[('id', '!=', template_id)]",
|
||||
)
|
||||
|
||||
state_required_id = fields.Many2one(
|
||||
string="Required State",
|
||||
comodel_name="cx.tower.jet.state",
|
||||
required=True,
|
||||
ondelete="restrict",
|
||||
help="The state of the required Jet",
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"unique_template_dependency",
|
||||
"UNIQUE(template_id, template_required_id)",
|
||||
"A template can only depend on another template once!",
|
||||
),
|
||||
]
|
||||
|
||||
@api.constrains(
|
||||
"template_id",
|
||||
"template_required_id",
|
||||
)
|
||||
def _check_circular_dependency(self):
|
||||
"""Check if this dependency would create a circular dependency chain"""
|
||||
for dependency in self:
|
||||
# Skip if the dependency isn't properly set yet
|
||||
if not dependency.template_id or not dependency.template_required_id:
|
||||
continue
|
||||
|
||||
# Self-dependency is not allowed and already prevented by domain constraints
|
||||
if dependency.template_id == dependency.template_required_id:
|
||||
raise ValidationError(_("A template cannot depend on itself!"))
|
||||
|
||||
# Build dependency graph
|
||||
graph = self._build_dependency_graph()
|
||||
|
||||
# Add the new dependency edge being created
|
||||
if dependency.template_id.id not in graph:
|
||||
graph[dependency.template_id.id] = set()
|
||||
graph[dependency.template_id.id].add(dependency.template_required_id.id)
|
||||
|
||||
# Check for circular dependencies
|
||||
if self._has_cycle(graph, dependency.template_id.id):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"This dependency would create a circular reference chain! "
|
||||
"Template '%(template)s' would indirectly depend on itself.",
|
||||
template=dependency.template_id.name,
|
||||
)
|
||||
)
|
||||
|
||||
@api.depends("template_id", "template_required_id")
|
||||
def _compute_display_name(self):
|
||||
for dependency in self:
|
||||
dependency.display_name = (
|
||||
(
|
||||
f"{dependency.template_id.name} ->"
|
||||
f" {dependency.template_required_id.name}"
|
||||
)
|
||||
if dependency.template_id and dependency.template_required_id
|
||||
else "..."
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
"""Do not allow modifications after creation"""
|
||||
# Allow modifications in install mode only to load demo data
|
||||
if ("template_id" in vals or "template_required_id" in vals) and not (
|
||||
self._context.get("install_mode") and self._context.get("install_xmlid")
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot modify an existing template dependency! "
|
||||
"Please remove it and create a new one."
|
||||
)
|
||||
)
|
||||
return super().write(vals)
|
||||
|
||||
def _build_dependency_graph(self):
|
||||
"""Build a directed graph of template dependencies
|
||||
|
||||
Returns:
|
||||
dict: A dictionary where keys are template IDs and values are
|
||||
sets of template IDs that are required by the key template
|
||||
"""
|
||||
graph = {}
|
||||
# Get all dependencies in the system
|
||||
# TODO: This is not efficient, we should find a better way later.
|
||||
# Eg cache the graph in the template model.
|
||||
all_deps = self.search([])
|
||||
|
||||
for dep in all_deps:
|
||||
from_id = dep.template_id.id
|
||||
to_id = dep.template_required_id.id
|
||||
|
||||
if from_id not in graph:
|
||||
graph[from_id] = set()
|
||||
|
||||
graph[from_id].add(to_id)
|
||||
|
||||
# Ensure the to_id is in the graph even if it doesn't require anything
|
||||
if to_id not in graph:
|
||||
graph[to_id] = set()
|
||||
|
||||
return graph
|
||||
|
||||
def _has_cycle(self, graph, start_node, visited=None, path=None):
|
||||
"""Check if the graph has a cycle starting from start_node
|
||||
|
||||
Args:
|
||||
graph (dict): Dependency graph where keys are template IDs and values are
|
||||
sets of template IDs that the key depends on
|
||||
start_node (int): Template ID to start the traversal from
|
||||
visited (set, optional): Set of already visited nodes
|
||||
path (set, optional): Set of nodes in the current DFS path
|
||||
|
||||
Returns:
|
||||
bool: True if a cycle is detected, False otherwise
|
||||
"""
|
||||
if visited is None:
|
||||
visited = set()
|
||||
if path is None:
|
||||
path = set()
|
||||
|
||||
visited.add(start_node)
|
||||
path.add(start_node)
|
||||
|
||||
for neighbor in graph.get(start_node, set()):
|
||||
if neighbor not in visited:
|
||||
if self._has_cycle(graph, neighbor, visited, path):
|
||||
return True
|
||||
elif neighbor in path:
|
||||
# We found a cycle
|
||||
return True
|
||||
|
||||
# Remove the current node from the path as we backtrack
|
||||
path.remove(start_node)
|
||||
return False
|
||||
Reference in New Issue
Block a user