Tower: upload cetmix_tower_server 16.0.2.2.9 (via marketplace)
This commit is contained in:
368
addons/cetmix_tower_server/models/cx_tower_variable.py
Normal file
368
addons/cetmix_tower_server/models/cx_tower_variable.py
Normal 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,
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user