Tower: upload cetmix_tower_server 16.0.2.2.9 (via marketplace)

This commit is contained in:
2026-04-27 08:43:51 +00:00
parent c501af7d45
commit 833346a1a8

View File

@@ -0,0 +1,368 @@
# Copyright (C) 2022 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import _, api, fields, models
from odoo.tools.safe_eval import wrap_module
_logger = logging.getLogger(__name__)
re = wrap_module(
__import__("re"),
[
"match",
"fullmatch",
"search",
"sub",
"subn",
"split",
"findall",
"finditer",
"compile",
"template",
"escape",
"error",
],
)
class TowerVariable(models.Model):
"""Variables"""
_name = "cx.tower.variable"
_description = "Cetmix Tower Variable"
_inherit = [
"cx.tower.reference.mixin",
"cx.tower.access.mixin",
"cx.tower.tag.mixin",
]
_order = "name"
DEFAULT_VALIDATION_MESSAGE = _("Invalid value!")
value_ids = fields.One2many(
string="Values",
comodel_name="cx.tower.variable.value",
inverse_name="variable_id",
)
value_ids_count = fields.Integer(
string="Value Count", compute="_compute_variable_counters"
)
option_ids = fields.One2many(
comodel_name="cx.tower.variable.option",
inverse_name="variable_id",
string="Options",
auto_join=True,
)
variable_type = fields.Selection(
selection=[("s", "String"), ("o", "Options")],
default="s",
required=True,
string="Type",
)
applied_expression = fields.Text(
help="Python expression to apply to the variable value. \n"
"You can use general python sting functions and 're' module "
"for regex operations. "
"Use 'value' variable to refer to the variable value, use 'result'"
" to assign the final result that will be used as a variable value.\n"
"Eg 'result = value.lower().replace(' ', '_')'",
)
validation_pattern = fields.Char(
help="Regex pattern to validate the variable values using the "
"'re.match' function. Eg. ^[a-z0-9]+$ \n"
"If empty, the variable values will not be validated.",
)
validation_message = fields.Char(
translate=True,
help="Message to display when the variable value is invalid. \n"
"First line will be added automatically: "
"`Variable:<variable_name>, Value: <value>`\n"
"Eg: `Variable: Customer Name, Value: Test\nInvalid value!`\n"
"If empty, the default message will be used.",
)
note = fields.Text(
help="Additional notes about the variable. \n"
"This field will be displayed in the variable form.",
)
# --- Link to records where the variable is used
command_ids = fields.Many2many(
comodel_name="cx.tower.command",
relation="cx_tower_command_variable_rel",
column1="variable_id",
column2="command_id",
copy=False,
)
command_ids_count = fields.Integer(
string="Command Count", compute="_compute_variable_counters"
)
plan_line_ids = fields.Many2many(
comodel_name="cx.tower.plan.line",
relation="cx_tower_plan_line_variable_rel",
column1="variable_id",
column2="plan_line_id",
copy=False,
)
plan_line_ids_count = fields.Integer(
string="Plan Line Count", compute="_compute_variable_counters"
)
file_ids = fields.Many2many(
comodel_name="cx.tower.file",
relation="cx_tower_file_variable_rel",
column1="variable_id",
column2="file_id",
copy=False,
)
file_ids_count = fields.Integer(
string="File Count", compute="_compute_variable_counters"
)
file_template_ids = fields.Many2many(
comodel_name="cx.tower.file.template",
relation="cx_tower_file_template_variable_rel",
column1="variable_id",
column2="file_template_id",
copy=False,
)
file_template_ids_count = fields.Integer(
string="File Template Count", compute="_compute_variable_counters"
)
variable_value_ids = fields.Many2many(
comodel_name="cx.tower.variable.value",
relation="cx_tower_variable_value_variable_rel",
column1="variable_id",
column2="variable_value_id",
copy=False,
)
variable_value_ids_count = fields.Integer(
string="Variable Value Count", compute="_compute_variable_counters"
)
_sql_constraints = [("name_uniq", "unique (name)", "Variable names must be unique")]
def _compute_variable_counters(self):
"""Count number of variable values for the variable"""
for rec in self:
rec.update(
{
"variable_value_ids_count": len(rec.variable_value_ids),
"command_ids_count": len(rec.command_ids),
"plan_line_ids_count": len(rec.plan_line_ids),
"file_ids_count": len(rec.file_ids),
"file_template_ids_count": len(rec.file_template_ids),
"value_ids_count": len(rec.value_ids),
}
)
def action_open_values(self):
self.ensure_one()
context = self.env.context.copy()
context.update(
{
"default_variable_id": self.id,
}
)
return {
"type": "ir.actions.act_window",
"name": _("Variable Values"),
"res_model": "cx.tower.variable.value",
"views": [[False, "tree"]],
"target": "current",
"context": context,
"domain": [("variable_id", "=", self.id)],
}
def action_open_commands(self):
"""Open the commands where the variable is used"""
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"cetmix_tower_server.action_cx_tower_command"
)
action.update(
{
"domain": [("variable_ids", "in", self.ids)],
}
)
return action
def action_open_plan_lines(self):
"""Open the plan lines where the variable is used"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Plan Lines"),
"res_model": "cx.tower.plan.line",
"views": [
[False, "tree"],
[
self.env.ref("cetmix_tower_server.cx_tower_plan_line_view_form").id,
"form",
],
],
"target": "current",
"domain": [("variable_ids", "in", self.ids)],
}
def action_open_files(self):
"""Open the files where the variable is used"""
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"cetmix_tower_server.cx_tower_file_action"
)
action.update(
{
"domain": [("variable_ids", "in", self.ids)],
}
)
return action
def action_open_file_templates(self):
"""Open the file templates where the variable is used"""
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"cetmix_tower_server.cx_tower_file_template_action"
)
action.update(
{
"domain": [("variable_ids", "in", self.ids)],
}
)
return action
def action_open_variable_values(self):
"""Open the variable values where the variable is used"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Variable Values"),
"res_model": "cx.tower.variable.value",
"views": [[False, "tree"]],
"target": "current",
"domain": [("variable_ids", "in", self.ids)],
}
@api.model
def _get_eval_context(self, value_char=None):
"""
Evaluation context to pass to safe_eval to evaluate
the Python expression used in the `applied_expression` field
Args:
value_char (Char): variable value
Returns:
dict: evaluation context
"""
return {
"re": re,
"value": value_char,
}
# Reference rename propagation
def write(self, vals):
"""Override the write method to propagate variable reference updates.
Records the old reference values, performs the write, and if the reference
field has changed, initiates propagation to update related records.
"""
old_refs = (
{rec.id: rec.reference for rec in self} if "reference" in vals else {}
)
res = super().write(vals)
if "reference" in vals:
for rec in self:
old_ref = old_refs.get(rec.id)
if old_ref and old_ref != rec.reference:
rec._propagate_reference_change(old_ref, rec.reference)
return res
def _propagate_reference_change(self, old_ref, new_ref):
"""Replace all occurrences of an old variable reference with a new one.
Compiles a pattern matching the old Jinja-style reference, then searches across
configured models and fields to substitute any matches, preserving formatting.
"""
pattern = re.compile(r"(\{\{\s*)" + re.escape(old_ref) + r"(\s*\}\})")
def _replace(text):
"""Helper to replace old_ref with new_ref in the given text."""
return pattern.sub(lambda m: f"{m.group(1)}{new_ref}{m.group(2)}", text)
model_fields_map = self._get_propagation_field_mapping()
for model_name, field_names in model_fields_map.items():
Model = self.env[model_name]
if model_name == "cx.tower.variable.value":
domain = [("variable_id", "=", self.id)]
else:
domain = [("variable_ids", "in", self.ids)]
for record in Model.search(domain):
vals = {}
for field_name in field_names:
value = record[field_name]
if isinstance(value, str) and old_ref in value:
new_value = _replace(value)
if new_value != value:
vals[field_name] = new_value
if vals:
record.with_context(skip_reference_propagation=True).write(vals)
_logger.debug(
"Variable reference updated in %s(%s): %s",
model_name,
record.id,
", ".join(vals.keys()),
)
def _get_propagation_field_mapping(self):
"""Return the mapping of models to fields for reference change propagation.
The returned dict maps each model name to a list of field names
that may contain variable references requiring updates.
"""
return {
"cx.tower.command": ["code", "path"],
"cx.tower.file": ["code", "server_dir", "name"],
"cx.tower.file.template": ["code", "server_dir", "file_name"],
"cx.tower.variable.value": ["value_char"],
"cx.tower.plan.line": ["condition"],
}
def _get_dependent_model_relation_fields(self):
"""Check cx.tower.reference.mixin for the function documentation"""
res = super()._get_dependent_model_relation_fields()
return res + ["value_ids"]
def _validate_value(self, value_char=None):
"""
Validate the variable value
Args:
value_char (Char): variable value
Returns:
(Boolean, Char): (is_valid, validation_message)
"""
self.ensure_one()
if (
not self.validation_pattern
or not value_char
or re.match(self.validation_pattern, value_char)
):
return True, None
message = self.validation_message or self.DEFAULT_VALIDATION_MESSAGE
return (
False,
_(
"Variable: %(var)s, Value: %(val)s\n%(msg)s",
msg=message,
var=self.name,
val=value_char,
),
)