Tower: upload cetmix_tower_server 16.0.3.0.1 (via marketplace)

This commit is contained in:
2026-04-27 08:15:56 +00:00
parent c2923e01e6
commit bbb71840c1

View File

@@ -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