Wipe addons/: full reset for clean re-upload
This commit is contained in:
@@ -1,481 +0,0 @@
|
||||
# Copyright (C) 2024 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import ormcache
|
||||
|
||||
|
||||
class CxTowerReferenceMixin(models.AbstractModel):
|
||||
"""
|
||||
Used to create and manage unique record references.
|
||||
"""
|
||||
|
||||
_name = "cx.tower.reference.mixin"
|
||||
_description = "Cetmix Tower reference mixin"
|
||||
_rec_names_search = ["name", "reference"]
|
||||
|
||||
# Used to check the reference before it's being fixed.
|
||||
# Ensures there's at least one valid symbol
|
||||
# that can be used later as a new reference basis.
|
||||
REFERENCE_PRELIMINARY_PATTERN = r"[\da-zA-Z]"
|
||||
|
||||
name = fields.Char(required=True, index="trigram")
|
||||
reference = fields.Char(
|
||||
index=True,
|
||||
unaccent=False,
|
||||
help="Can contain English letters, digits and '_'. Leave blank to autogenerate",
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
("reference_unique", "UNIQUE(reference)", "Reference must be unique")
|
||||
]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""
|
||||
Overrides create to ensure 'reference' is auto-corrected
|
||||
or validated for each record.
|
||||
|
||||
Add `reference_mixin_override` context key to skip the reference check
|
||||
|
||||
Args:
|
||||
vals_list (list[dict]): List of dictionaries with record values.
|
||||
|
||||
Returns:
|
||||
Records: The created record(s).
|
||||
"""
|
||||
|
||||
if vals_list and not self._context.get("reference_mixin_override"):
|
||||
# Check if we need to populate references based on parent record
|
||||
auto_generate_settings = self._get_pre_populated_model_data().get(
|
||||
self._name
|
||||
)
|
||||
if auto_generate_settings:
|
||||
parent_model, relation_field = auto_generate_settings
|
||||
vals_list = self._pre_populate_references(
|
||||
parent_model, relation_field, vals_list
|
||||
)
|
||||
|
||||
# Fix or create references
|
||||
for vals in vals_list:
|
||||
if not vals:
|
||||
continue
|
||||
|
||||
# Remove leading and trailing whitespaces from name
|
||||
vals_name = vals.get("name")
|
||||
name = vals_name.strip() if vals_name else vals_name
|
||||
|
||||
# Remove leading and trailing whitespaces from reference
|
||||
vals_reference = vals.get("reference")
|
||||
reference = vals_reference.strip() if vals_reference else vals_reference
|
||||
|
||||
# Nothing can be done if no name or reference is provided
|
||||
if not name and not reference:
|
||||
continue
|
||||
|
||||
# Save name back to vals if it was modified
|
||||
if vals_name != name:
|
||||
vals["name"] = name
|
||||
|
||||
# Generate reference
|
||||
vals.update(
|
||||
{"reference": self._generate_or_fix_reference(reference or name)}
|
||||
)
|
||||
|
||||
res = super().create(vals_list)
|
||||
self.clear_caches()
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Updates record, auto-correcting or validating 'reference'
|
||||
based on 'name' or existing value.
|
||||
|
||||
Add `reference_mixin_override` context key to skip the reference check
|
||||
|
||||
Args:
|
||||
vals (dict): Values to update, may include 'reference'.
|
||||
|
||||
Returns:
|
||||
Result of the super `write` call.
|
||||
"""
|
||||
if not self._context.get("reference_mixin_override") and "reference" in vals:
|
||||
reference = vals.get("reference", False)
|
||||
if not reference:
|
||||
# Get name from vals
|
||||
updated_name = vals.get("name")
|
||||
|
||||
# No name in vals. Update records one by one
|
||||
if not updated_name:
|
||||
for record in self:
|
||||
record_vals = vals.copy()
|
||||
record_vals.update(
|
||||
{"reference": self._generate_or_fix_reference(record.name)}
|
||||
)
|
||||
super(CxTowerReferenceMixin, record).write(record_vals)
|
||||
return True
|
||||
# Name is present in vals
|
||||
reference = self._generate_or_fix_reference(updated_name)
|
||||
else:
|
||||
reference = self._generate_or_fix_reference(reference)
|
||||
vals.update({"reference": reference})
|
||||
|
||||
res = super().write(vals)
|
||||
|
||||
# Update references of dependent models
|
||||
if "reference" in vals:
|
||||
self._update_dependent_model_references()
|
||||
# Clear caches
|
||||
self.clear_caches()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
"""
|
||||
Overrides unlink to clear cache for this method
|
||||
"""
|
||||
res = super().unlink()
|
||||
self.clear_caches()
|
||||
return res
|
||||
|
||||
def copy(self, default=None):
|
||||
"""
|
||||
Overrides the copy method to ensure unique reference values
|
||||
for duplicated records.
|
||||
|
||||
Args:
|
||||
default (dict, optional): Default values for the new record.
|
||||
|
||||
Returns:
|
||||
Record: The newly copied record with adjusted defaults.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if default is None:
|
||||
default = {}
|
||||
|
||||
# skip copying 'name' because this function can be used in models
|
||||
# where 'name' field is not stored
|
||||
if not self.env.context.get("reference_mixin_skip_copy"):
|
||||
default["name"] = self._get_copied_name(force_name=default.get("name"))
|
||||
if "reference" not in default:
|
||||
default["reference"] = self._generate_or_fix_reference(default["name"])
|
||||
return super().copy(default=default)
|
||||
|
||||
def _get_reference_pattern(self):
|
||||
"""
|
||||
Returns the regex pattern used for validating and correcting references.
|
||||
This allows for easy modification of the pattern in one place.
|
||||
|
||||
Important: pattern must be enclosed in square brackets!
|
||||
|
||||
Returns:
|
||||
str: A regex pattern
|
||||
"""
|
||||
return "[a-z0-9_]"
|
||||
|
||||
def _get_pre_populated_model_data(self):
|
||||
"""Returns List of models that should try to generate
|
||||
references based on the related model reference.
|
||||
|
||||
Eg flight plan lines references are generated based on the flight plan one.
|
||||
|
||||
Returns:
|
||||
dict: Model values dictionary:
|
||||
{model_name: [parent_model, relation_field]}
|
||||
"""
|
||||
return {}
|
||||
|
||||
def _get_extra_vals_fields(self):
|
||||
"""Returns list of extra fields that are needed for reference generation.
|
||||
This method if used to make custom reference generation logic more flexible.
|
||||
Eg for 'cx.tower.variable.value':
|
||||
'server_id', 'server_template_id', 'plan_line_action_id'.
|
||||
So for common models like 'cx.tower.server' this method is not needed.
|
||||
|
||||
Returns:
|
||||
list: List of fields:
|
||||
[field_name1, field_name2, ...]
|
||||
"""
|
||||
return []
|
||||
|
||||
def _get_dependent_model_relation_fields(self):
|
||||
"""Returns list of fields that reference dependent models.
|
||||
|
||||
Eg flight plan lines references are generated based on the flight plan one.
|
||||
|
||||
Returns:
|
||||
list: List of fields:
|
||||
[field_name1, field_name2, ...]
|
||||
"""
|
||||
return []
|
||||
|
||||
def _update_dependent_model_references(self):
|
||||
"""Update references of dependent models"""
|
||||
dependent_model_relation_fields = self._get_dependent_model_relation_fields()
|
||||
if dependent_model_relation_fields:
|
||||
for field in dependent_model_relation_fields:
|
||||
related_model_name = self[field]._name
|
||||
|
||||
# Check if the related model has auto-generate settings
|
||||
auto_generate_settings = (
|
||||
self[field]._get_pre_populated_model_data().get(related_model_name)
|
||||
)
|
||||
if auto_generate_settings:
|
||||
parent_model, relation_field = auto_generate_settings
|
||||
else:
|
||||
continue
|
||||
|
||||
# Parse the field for all records
|
||||
for record in self:
|
||||
related_records = record[field]
|
||||
# Get vals list
|
||||
rec_vals_list = related_records.read(
|
||||
[relation_field] + related_records._get_extra_vals_fields()
|
||||
)
|
||||
# Transform Many2one tuples to IDs
|
||||
for rv in rec_vals_list:
|
||||
for k, v in rv.items():
|
||||
# Transform m2o fields from (id, name) to id
|
||||
if isinstance(v, tuple):
|
||||
rv[k] = v[0]
|
||||
related_records._pre_populate_references(
|
||||
parent_model, relation_field, rec_vals_list
|
||||
)
|
||||
ref_by_id = {rv["id"]: rv["reference"] for rv in rec_vals_list}
|
||||
for related_record in related_records:
|
||||
related_record.reference = ref_by_id[related_record.id]
|
||||
|
||||
def _generate_or_fix_reference(self, reference_source):
|
||||
"""
|
||||
Generate a new reference of fix an existing one.
|
||||
|
||||
Args:
|
||||
reference_source (str): Original string.
|
||||
|
||||
Returns:
|
||||
str: Generated or fixed reference.
|
||||
"""
|
||||
|
||||
# Check if reference matches the pattern
|
||||
reference_pattern = self._get_reference_pattern()
|
||||
|
||||
if re.fullmatch(rf"{reference_pattern}+", reference_source):
|
||||
reference = reference_source
|
||||
|
||||
# Fix reference if it doesn't match
|
||||
else:
|
||||
# Modify the pattern to be used in `sub`
|
||||
inner_pattern = reference_pattern[1:-1]
|
||||
reference = (
|
||||
re.sub(
|
||||
rf"[^{inner_pattern}]",
|
||||
"",
|
||||
reference_source.strip().replace(" ", "_").lower(),
|
||||
)
|
||||
or self._get_model_generic_reference()
|
||||
)
|
||||
|
||||
# Check if the same reference already exists and add a suffix if yes
|
||||
counter = 1
|
||||
final_reference = reference
|
||||
|
||||
# If exclude same records from search results
|
||||
if self and not self.env.context.get("reference_mixin_skip_self"):
|
||||
domain = [("id", "not in", self.ids)]
|
||||
else:
|
||||
domain = []
|
||||
final_domain = expression.AND([domain, [("reference", "=", final_reference)]])
|
||||
|
||||
# Search all records without restrictions including archived
|
||||
self_with_sudo_and_context = self.sudo().with_context(active_test=False)
|
||||
while self_with_sudo_and_context.search_count(final_domain) > 0:
|
||||
counter += 1
|
||||
final_reference = f"{reference}_{counter}"
|
||||
final_domain = expression.AND(
|
||||
[domain, [("reference", "=", final_reference)]]
|
||||
)
|
||||
|
||||
return final_reference
|
||||
|
||||
def _get_copied_name(self, force_name=None):
|
||||
"""
|
||||
Return a copied name of the record
|
||||
by adding the suffix (copy) at the end
|
||||
and counter until the name is unique.
|
||||
|
||||
Args:
|
||||
force_name (str): Used to use force name instead of record name.
|
||||
|
||||
Returns:
|
||||
An unique name for the copied record
|
||||
"""
|
||||
self.ensure_one()
|
||||
original_name = force_name or self.name
|
||||
copy_name = _("%(name)s (copy)", name=original_name)
|
||||
|
||||
counter = 1
|
||||
# Ensures that the generated copy name is unique by
|
||||
# appending a counter until a unique name is found.
|
||||
while self.search_count([("name", "=", copy_name)]) > 0:
|
||||
counter += 1
|
||||
copy_name = _(
|
||||
"%(name)s (copy %(number)s)",
|
||||
name=original_name,
|
||||
number=str(counter),
|
||||
)
|
||||
|
||||
return copy_name
|
||||
|
||||
def _get_model_generic_reference(self):
|
||||
"""Get generic reference for current model.
|
||||
Generic references are used as a fallback in the automatic
|
||||
reference generation.
|
||||
When a reference cannot be generated neither from the 'reference'
|
||||
nor from the 'name' field values.
|
||||
|
||||
Eg for the 'cx.tower.plan' model such reference will look like
|
||||
'tower_plan'.
|
||||
|
||||
Returns:
|
||||
Char: generated prefix
|
||||
"""
|
||||
model_prefix = self._name.replace("cx.tower.", "").replace(".", "_")
|
||||
return model_prefix
|
||||
|
||||
def get_by_reference(self, reference):
|
||||
"""Get record based on its reference.
|
||||
|
||||
Important: references are case sensitive!
|
||||
|
||||
Args:
|
||||
reference (Char): record reference
|
||||
|
||||
Returns:
|
||||
Record: Record that matches provided reference
|
||||
"""
|
||||
return self.browse(self._get_id_by_reference(reference))
|
||||
|
||||
@ormcache("self.env.uid", "self.env.su", "reference")
|
||||
def _get_id_by_reference(self, reference):
|
||||
"""Get record id based on its reference.
|
||||
|
||||
Important: references are case sensitive!
|
||||
|
||||
Args:
|
||||
reference (Char): record reference
|
||||
|
||||
Returns:
|
||||
Record: Record id that matches provided reference
|
||||
"""
|
||||
records = self.search([("reference", "=", reference)])
|
||||
|
||||
# This is in case some models will remove reference uniqueness constraint
|
||||
return records and records[0].id
|
||||
|
||||
@api.model
|
||||
def _prepare_references(self, model, key_name, vals_list):
|
||||
"""
|
||||
Prepare a dictionary of references for given model records.
|
||||
|
||||
This function extracts unique IDs from a list of dictionaries (vals_list)
|
||||
based on a specified key (key_name), fetches the corresponding records
|
||||
from the specified model, and returns a dictionary mapping record IDs to
|
||||
their references.
|
||||
|
||||
Args:
|
||||
model (str): The name of the model to fetch records from.
|
||||
key_name (str): The key in the dictionaries of vals_list that contains
|
||||
the record IDs.
|
||||
vals_list (list of dict): A list of dictionaries containing the values
|
||||
to be processed.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping record IDs to their references.
|
||||
"""
|
||||
if not vals_list:
|
||||
# No entries to process, return an empty dictionary
|
||||
return {}
|
||||
|
||||
try:
|
||||
CxModel = self.env[model]
|
||||
except KeyError as err:
|
||||
raise ValueError(
|
||||
_(
|
||||
(
|
||||
"Model '%(model)s' does not exist. "
|
||||
"Please provide a valid model name."
|
||||
),
|
||||
model=model,
|
||||
)
|
||||
) from err
|
||||
|
||||
# Extract all unique ids from vals_list
|
||||
line_ids = {
|
||||
vals.get(key_name)
|
||||
for vals in vals_list
|
||||
if vals.get(key_name) and not vals.get("reference")
|
||||
}
|
||||
|
||||
# Fetch all line references in a single query
|
||||
lines = CxModel.browse(line_ids)
|
||||
return {line.id: line.reference for line in lines if line.reference}
|
||||
|
||||
@api.model
|
||||
def _pre_populate_references(self, model_name, field_name, vals_list):
|
||||
"""
|
||||
Populates reference fields in a list of dictionaries (vals_list)
|
||||
intended for record creation.
|
||||
|
||||
This method generates unique references for each dictionary entry in
|
||||
`vals_list` based on a specified field that links to records in
|
||||
another model (indicated by `model_name`). It uses existing references
|
||||
from the related records as a basis and appends a suffix and an
|
||||
incrementing index to ensure uniqueness.
|
||||
If reference is present in values it will not be overwritten.
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the related model to extract
|
||||
reference data from.
|
||||
field_name (str): The key in each dictionary in `vals_list`
|
||||
containing the related record's ID.
|
||||
vals_list (list of dict): A list of dictionaries where each dictionary
|
||||
represents values for a new record.
|
||||
|
||||
Returns:
|
||||
list: The modified `vals_list`, with a unique 'reference'
|
||||
entry in each dictionary.
|
||||
"""
|
||||
|
||||
# Extract parent record references from vals_list
|
||||
parent_record_refs = self._prepare_references(model_name, field_name, vals_list)
|
||||
line_index_dict = defaultdict(int)
|
||||
|
||||
# Used to make reference more readable
|
||||
model_reference = self._get_model_generic_reference()
|
||||
|
||||
# Populate vals with references
|
||||
for vals in vals_list:
|
||||
# Skip if reference is provided explicitly and has symbols
|
||||
existing_reference = vals.get("reference")
|
||||
if existing_reference and bool(
|
||||
re.search(self.REFERENCE_PRELIMINARY_PATTERN, existing_reference)
|
||||
):
|
||||
continue
|
||||
|
||||
# Compose based on related record reference if exists
|
||||
record_id = vals.get(field_name)
|
||||
if record_id and parent_record_refs.get(record_id):
|
||||
line_index_dict[record_id] += 1
|
||||
line_index = line_index_dict[record_id]
|
||||
vals[
|
||||
"reference"
|
||||
] = f"{parent_record_refs[record_id]}_{model_reference}_{line_index}"
|
||||
else:
|
||||
# Handle cases where the field is not present
|
||||
line_index_dict["no_record"] += 1
|
||||
line_index = line_index_dict["no_record"]
|
||||
vals["reference"] = f"no_{model_reference}_{line_index}"
|
||||
|
||||
return vals_list
|
||||
Reference in New Issue
Block a user