diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst index 3ca49846fb6..832f8209c06 100644 --- a/doc/reference/orm.rst +++ b/doc/reference/orm.rst @@ -220,6 +220,10 @@ Relational Fields .. autoclass:: Many2many() +.. autoclass:: Command() + :members: + :undoc-members: + Pseudo-relational fields '''''''''''''''''''''''' diff --git a/odoo/__init__.py b/odoo/__init__.py index 07b1835b1b8..13bea94a9f4 100644 --- a/odoo/__init__.py +++ b/odoo/__init__.py @@ -125,6 +125,7 @@ from . import models from . import fields from . import api from odoo.tools.translate import _, _lt +from odoo.fields import Command #---------------------------------------------------------- # Other imports, which may require stuff from above diff --git a/odoo/addons/base/controllers/rpc.py b/odoo/addons/base/controllers/rpc.py index 930c3170e52..2fe7d794505 100644 --- a/odoo/addons/base/controllers/rpc.py +++ b/odoo/addons/base/controllers/rpc.py @@ -6,7 +6,7 @@ from werkzeug.wrappers import Response from odoo.http import Controller, dispatch_rpc, request, route from odoo.service import wsgi_server -from odoo.fields import Date, Datetime +from odoo.fields import Date, Datetime, Command from odoo.tools import lazy @@ -34,6 +34,8 @@ class OdooMarshaller(xmlrpc.client.Marshaller): return self.dispatch[type(v)](self, v, write) dispatch[lazy] = dump_lazy + dispatch[Command] = dispatch[int] + # monkey-patch xmlrpc.client's marshaller xmlrpc.client.Marshaller = OdooMarshaller diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py index 2cbd196debf..170ff2f374b 100644 --- a/odoo/addons/base/models/ir_actions.py +++ b/odoo/addons/base/models/ir_actions.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import odoo -from odoo import api, fields, models, tools, SUPERUSER_ID, _ +from odoo import api, fields, models, tools, SUPERUSER_ID, _, Command from odoo.exceptions import MissingError, UserError, ValidationError, AccessError from odoo.osv import expression from odoo.tools.safe_eval import safe_eval, test_python_expr @@ -76,6 +76,7 @@ class IrActions(models.Model): 'timezone': timezone, 'b64encode': base64.b64encode, 'b64decode': base64.b64decode, + 'Command': Command, } @api.model @@ -376,6 +377,7 @@ class IrActionsServer(models.Model): # - time, datetime, dateutil, timezone: useful Python libraries # - log: log(message, level='info'): logging function to record debug information in ir.logging table # - UserError: Warning Exception to use with raise +# - Command: x2Many commands namespace # To return an action, assign: action = {...}\n\n\n\n""" @api.model diff --git a/odoo/fields.py b/odoo/fields.py index 0d60ebef544..e92edf0f21f 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -11,6 +11,7 @@ import itertools import logging import base64 import binascii +import enum import pytz import psycopg2 @@ -1015,10 +1016,11 @@ class Field(MetaField('DummyField', (object,), {})): defaults = record.default_get([self.name]) if self.name in defaults: # The null value above is necessary to convert x2many field - # values. For instance, converting [(4, id)] accesses the - # field's current value, then adds the given id. Without an - # initial value, the conversion ends up here to determine - # the field's value, and generates an infinite recursion. + # values. For instance, converting [(Command.LINK, id)] + # accesses the field's current value, then adds the given + # id. Without an initial value, the conversion ends up here + # to determine the field's value, and generates an infinite + # recursion. value = self.convert_to_cache(defaults[self.name], record) env.cache.set(record, self, value) @@ -2804,6 +2806,123 @@ class Many2oneReference(Integer): return model_ids +class Command(enum.IntEnum): + """ + :class:`~odoo.fields.One2many` and :class:`~odoo.fields.Many2many` fields + expect a special command to manipulate the relation they implement. + + Internally, each command is a 3-elements tuple where the first element is a + mandatory integer that identifies the command, the second element is either + the related record id to apply the command on (commands update, delete, + unlink and link) either 0 (commands create, clear and set), the third + element is either the ``values`` to write on the record (commands create + and update) either the new ``ids`` list of related records (command set), + either 0 (commands delete, unlink, link, and clear). + + Via Python, we encourage developers craft new commands via the various + functions of this namespace. We also encourage developers to use the + command identifier constant names when comparing the 1st element of + existing commands. + + Via RPC, it is impossible nor to use the functions nor the command constant + names. It is required to instead write the literal 3-elements tuple where + the first element is the integer identifier of the command. + """ + + CREATE = 0 + UPDATE = 1 + DELETE = 2 + UNLINK = 3 + LINK = 4 + CLEAR = 5 + SET = 6 + + @classmethod + def create(cls, values: dict): + """ + Create new records in the comodel using ``values``, link the created + records to ``self``. + + In case of a :class:`~odoo.fields.Many2many` relation, one unique + new record is created in the comodel such that all records in `self` + are linked to the new record. + + In case of a :class:`~odoo.fields.One2many` relation, one new record + is created in the comodel for every record in ``self`` such that every + record in ``self`` is linked to exactly one of the new records. + + Return the command triple :samp:`(CREATE, 0, {values})` + """ + return (cls.CREATE, 0, values) + + @classmethod + def update(cls, id: int, values: dict): + """ + Write ``values`` on the related record. + + Return the command triple :samp:`(UPDATE, {id}, {values})` + """ + return (cls.UPDATE, id, values) + + @classmethod + def delete(cls, id: int): + """ + Remove the related record from the database and remove its relation + with ``self``. + + In case of a :class:`~odoo.fields.Many2many` relation, removing the + record from the database may be prevented if it is still linked to + other records. + + Return the command triple :samp:`(DELETE, {id}, 0)` + """ + return (cls.DELETE, id, 0) + + @classmethod + def unlink(cls, id: int): + """ + Remove the relation between ``self`` and the related record. + + In case of a :class:`~odoo.fields.One2many` relation, the given record + is deleted from the database if the inverse field is set as + ``ondelete='cascade'``. Otherwise, the value of the inverse field is + set to False and the record is kept. + + Return the command triple :samp:`(UNLINK, {id}, 0)` + """ + return (cls.UNLINK, id, 0) + + @classmethod + def link(cls, id: int): + """ + Add a relation between ``self`` and the related record. + + Return the command triple :samp:`(LINK, {id}, 0)` + """ + return (cls.LINK, id, 0) + + @classmethod + def clear(cls): + """ + Remove all records from the relation with ``self``. It behaves like + executing the `unlink` command on every record. + + Return the command triple :samp:`(CLEAR, 0, 0)` + """ + return (cls.CLEAR, 0, 0) + + @classmethod + def set(cls, ids: list): + """ + Replace the current relations of ``self`` by the given ones. It behaves + like executing the ``unlink`` command on every removed relation then + executing the ``link`` command on every new relation. + + Return the command triple :samp:`(SET, 0, {ids})` + """ + return (cls.SET, 0, ids) + + class _RelationalMulti(_Relational): """ Abstract class for relational fields *2many. """ @@ -2862,22 +2981,22 @@ class _RelationalMulti(_Relational): # modify ids with the commands for command in value: if isinstance(command, (tuple, list)): - if command[0] == 0: + if command[0] == Command.CREATE: ids.add(comodel.new(command[2], ref=command[1]).id) - elif command[0] == 1: + elif command[0] == Command.UPDATE: line = browse(command[1]) if validate: line.update(command[2]) else: line._update_cache(command[2], validate=False) ids.add(line.id) - elif command[0] in (2, 3): + elif command[0] in (Command.DELETE, Command.UNLINK): ids.discard(browse(command[1]).id) - elif command[0] == 4: + elif command[0] == Command.LINK: ids.add(browse(command[1]).id) - elif command[0] == 5: + elif command[0] == Command.CLEAR: ids.clear() - elif command[0] == 6: + elif command[0] == Command.SET: ids = OrderedSet(browse(it).id for it in command[2]) elif isinstance(command, dict): ids.add(comodel.new(command).id) @@ -2927,7 +3046,7 @@ class _RelationalMulti(_Relational): if isinstance(value, BaseModel) and value._name == self.comodel_name: # make result with new and existing records inv_names = {field.name for field in record._field_inverses[self]} - result = [(6, 0, [])] + result = [Command.set([])] for record in value: origin = record._origin if not origin: @@ -2936,7 +3055,7 @@ class _RelationalMulti(_Relational): for name in record._cache if name not in inv_names }) - result.append((0, 0, values)) + result.append(Command.create(values)) else: result[0][2].append(origin.id) if record != origin: @@ -2946,11 +3065,11 @@ class _RelationalMulti(_Relational): if name not in inv_names and record[name] != origin[name] }) if values: - result.append((1, origin.id, values)) + result.append(Command.update(origin.id, values)) return result if value is False or value is None: - return [(5,)] + return [Command.clear()] if isinstance(value, list): return value @@ -2992,13 +3111,13 @@ class _RelationalMulti(_Relational): for idx, (recs, value) in enumerate(records_commands_list): if isinstance(value, tuple): - value = [(6, 0, value)] + value = [Command.set(value)] elif isinstance(value, BaseModel) and value._name == self.comodel_name: - value = [(6, 0, value._ids)] + value = [Command.set(value._ids)] elif value is False or value is None: - value = [(5,)] + value = [Command.clear()] elif isinstance(value, list) and value and not isinstance(value[0], (tuple, list)): - value = [(6, 0, tuple(value))] + value = [Command.set(tuple(value))] if not isinstance(value, list): raise ValueError("Wrong value for %s: %s" % (self, value)) records_commands_list[idx] = (recs, value) @@ -3151,22 +3270,22 @@ class One2many(_RelationalMulti): for recs, commands in records_commands_list: for command in (commands or ()): - if command[0] == 0: + if command[0] == Command.CREATE: for record in recs: to_create.append(dict(command[2], **{inverse: record.id})) allow_full_delete = False - elif command[0] == 1: + elif command[0] == Command.UPDATE: comodel.browse(command[1]).write(command[2]) - elif command[0] == 2: + elif command[0] == Command.DELETE: to_delete.append(command[1]) - elif command[0] == 3: + elif command[0] == Command.UNLINK: unlink(comodel.browse(command[1])) - elif command[0] == 4: + elif command[0] == Command.LINK: to_inverse.setdefault(recs[-1], set()).add(command[1]) allow_full_delete = False - elif command[0] in (5, 6) : + elif command[0] in (Command.CLEAR, Command.SET): # do not try to delete anything in creation mode if nothing has been created before - line_ids = command[2] if command[0] == 6 else [] + line_ids = command[2] if command[0] == Command.SET else [] if not allow_full_delete and not line_ids: continue flush() @@ -3192,21 +3311,21 @@ class One2many(_RelationalMulti): for recs, commands in records_commands_list: for command in (commands or ()): - if command[0] == 0: + if command[0] == Command.CREATE: for record in recs: link(record, comodel.new(command[2], ref=command[1])) - elif command[0] == 1: + elif command[0] == Command.UPDATE: comodel.browse(command[1]).write(command[2]) - elif command[0] == 2: + elif command[0] == Command.DELETE: unlink(comodel.browse(command[1])) - elif command[0] == 3: + elif command[0] == Command.UNLINK: unlink(comodel.browse(command[1])) - elif command[0] == 4: + elif command[0] == Command.LINK: link(recs[-1], comodel.browse(command[1])) - elif command[0] in (5, 6): + elif command[0] in (Command.CLEAR, Command.SET): # assign the given lines to the last record only cache.update(recs, self, [()] * len(recs)) - lines = comodel.browse(command[2] if command[0] == 6 else []) + lines = comodel.browse(command[2] if command[0] == Command.SET else []) cache.set(recs[-1], self, lines._ids) return records @@ -3238,22 +3357,22 @@ class One2many(_RelationalMulti): for recs, commands in records_commands_list: for command in commands: - if command[0] == 0: + if command[0] == Command.CREATE: for record in recs: line = comodel.new(command[2], ref=command[1]) line[inverse] = record - elif command[0] == 1: + elif command[0] == Command.UPDATE: browse([command[1]]).update(command[2]) - elif command[0] == 2: + elif command[0] == Command.DELETE: browse([command[1]])[inverse] = False - elif command[0] == 3: + elif command[0] == Command.UNLINK: browse([command[1]])[inverse] = False - elif command[0] == 4: + elif command[0] == Command.LINK: browse([command[1]])[inverse] = recs[-1] - elif command[0] in (5, 6): + elif command[0] in (Command.CLEAR, Command.SET): # assign the given lines to the last record only cache.update(recs, self, [()] * len(recs)) - lines = comodel.browse(command[2] if command[0] == 6 else []) + lines = comodel.browse(command[2] if command[0] == Command.SET else []) cache.set(recs[-1], self, lines._ids) else: @@ -3267,21 +3386,21 @@ class One2many(_RelationalMulti): for recs, commands in records_commands_list: for command in commands: - if command[0] == 0: + if command[0] == Command.CREATE: for record in recs: link(record, comodel.new(command[2], ref=command[1])) - elif command[0] == 1: + elif command[0] == Command.UPDATE: browse([command[1]]).update(command[2]) - elif command[0] == 2: + elif command[0] == Command.DELETE: unlink(browse([command[1]])) - elif command[0] == 3: + elif command[0] == Command.UNLINK: unlink(browse([command[1]])) - elif command[0] == 4: + elif command[0] == Command.LINK: link(recs[-1], browse([command[1]])) - elif command[0] in (5, 6): + elif command[0] in (Command.CLEAR, Command.SET): # assign the given lines to the last record only cache.update(recs, self, [()] * len(recs)) - lines = comodel.browse(command[2] if command[0] == 6 else []) + lines = comodel.browse(command[2] if command[0] == Command.SET else []) cache.set(recs[-1], self, lines._ids) return records @@ -3531,20 +3650,20 @@ class Many2many(_RelationalMulti): for command in (commands or ()): if not isinstance(command, (list, tuple)) or not command: continue - if command[0] == 0: + if command[0] == Command.CREATE: to_create.append((recs._ids, command[2])) - elif command[0] == 1: + elif command[0] == Command.UPDATE: comodel.browse(command[1]).write(command[2]) - elif command[0] == 2: + elif command[0] == Command.DELETE: to_delete.append(command[1]) - elif command[0] == 3: + elif command[0] == Command.UNLINK: relation_remove(recs._ids, command[1]) - elif command[0] == 4: + elif command[0] == Command.LINK: relation_add(recs._ids, command[1]) - elif command[0] in (5, 6): + elif command[0] in (Command.CLEAR, Command.SET): # new lines must no longer be linked to records to_create = [(set(ids) - set(recs._ids), vals) for (ids, vals) in to_create] - relation_set(recs._ids, command[2] if command[0] == 6 else ()) + relation_set(recs._ids, command[2] if command[0] == Command.SET else ()) if to_create: # create lines in batch, and link them @@ -3648,28 +3767,28 @@ class Many2many(_RelationalMulti): for command in commands: if not isinstance(command, (list, tuple)) or not command: continue - if command[0] == 0: + if command[0] == Command.CREATE: line_id = comodel.new(command[2], ref=command[1]).id for line_ids in new_relation.values(): line_ids.add(line_id) - elif command[0] == 1: + elif command[0] == Command.UPDATE: line_id = new(command[1]) comodel.browse([line_id]).update(command[2]) - elif command[0] == 2: + elif command[0] == Command.DELETE: line_id = new(command[1]) for line_ids in new_relation.values(): line_ids.discard(line_id) - elif command[0] == 3: + elif command[0] == Command.UNLINK: line_id = new(command[1]) for line_ids in new_relation.values(): line_ids.discard(line_id) - elif command[0] == 4: + elif command[0] == Command.LINK: line_id = new(command[1]) for line_ids in new_relation.values(): line_ids.add(line_id) - elif command[0] in (5, 6): + elif command[0] in (Command.CLEAR, Command.SET): # new lines must no longer be linked to records - line_ids = command[2] if command[0] == 6 else () + line_ids = command[2] if command[0] == Command.SET else () line_ids = set(new(line_id) for line_id in line_ids) for id_ in recs._ids: new_relation[id_] = set(line_ids) diff --git a/odoo/models.py b/odoo/models.py index 98a2ba07d8b..c9071c269dd 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -1300,10 +1300,11 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # convert default values to the right format # # we explicitly avoid using _convert_to_write() for x2many fields, - # because the latter leaves values like [(4, 2), (4, 3)], which are not - # supported by the web client as default values; stepping through the - # cache allows to normalize such a list to [(6, 0, [2, 3])], which is - # properly supported by the web client + # because the latter leaves values like [(Command.LINK, 2), + # (Command.LINK, 3)], which are not supported by the web client as + # default values; stepping through the cache allows to normalize + # such a list to [(Command.SET, 0, [2, 3])], which is properly + # supported by the web client for fname, value in defaults.items(): if fname in self._fields: field = self._fields[fname] @@ -1844,10 +1845,10 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): for name, value in defaults.items(): if self._fields[name].type == 'many2many' and value and isinstance(value[0], int): # convert a list of ids into a list of commands - defaults[name] = [(6, 0, value)] + defaults[name] = [Command.set(value)] elif self._fields[name].type == 'one2many' and value and isinstance(value[0], dict): # convert a list of dicts into a list of commands - defaults[name] = [(0, 0, x) for x in value] + defaults[name] = [Command.create(x) for x in value] defaults.update(values) return defaults @@ -3529,37 +3530,18 @@ Fields: :const:`odoo.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT` * .. _openerp/models/relationals/format: - :class:`~odoo.fields.One2many` and - :class:`~odoo.fields.Many2many` use a special "commands" format to - manipulate the set of records stored in/associated with the field. - - This format is a list of triplets executed sequentially, where each - triplet is a command to execute on the set of records. Not all - commands apply in all situations. Possible commands are: - - ``(0, 0, values)`` - adds a new record created from the provided ``value`` dict. - ``(1, id, values)`` - updates an existing record of id ``id`` with the values in - ``values``. Can not be used in :meth:`~.create`. - ``(2, id, 0)`` - removes the record of id ``id`` from the set, then deletes it - (from the database). Can not be used in :meth:`~.create`. - ``(3, id, 0)`` - removes the record of id ``id`` from the set, but does not - delete it. Can not be used in - :meth:`~.create`. - ``(4, id, 0)`` - adds an existing record of id ``id`` to the set. - ``(5, 0, 0)`` - removes all records from the set, equivalent to using the - command ``3`` on every record explicitly. Can not be used in - :meth:`~.create`. - ``(6, 0, ids)`` - replaces all existing records in the set by the ``ids`` list, - equivalent to using the command ``5`` followed by a command - ``4`` for each ``id`` in ``ids``. - """ + The expected value of a :class:`~odoo.fields.One2many` or + :class:`~odoo.fields.Many2many` relational field is a list of + :class:`~odoo.fields.Command` that manipulate the relation the + implement. There are a total of 7 commands: + :meth:`~odoo.fields.Command.create`, + :meth:`~odoo.fields.Command.update`, + :meth:`~odoo.fields.Command.delete`, + :meth:`~odoo.fields.Command.unlink`, + :meth:`~odoo.fields.Command.link`, + :meth:`~odoo.fields.Command.clear`, and + :meth:`~odoo.fields.Command.set`. + """ if not self: return True @@ -4534,11 +4516,11 @@ Fields: # duplicate following the order of the ids because we'll rely on # it later for copying translations in copy_translation()! lines = [rec.copy_data()[0] for rec in self[name].sorted(key='id')] - # the lines are duplicated using the wrong (old) parent, but then - # are reassigned to the correct one thanks to the (0, 0, ...) - default[name] = [(0, 0, line) for line in lines if line] + # the lines are duplicated using the wrong (old) parent, but then are + # reassigned to the correct one thanks to the (Command.CREATE, 0, ...) + default[name] = [Command.create(line) for line in lines if line] elif field.type == 'many2many': - default[name] = [(6, 0, self[name].ids)] + default[name] = [Command.set(self[name].ids)] else: default[name] = field.convert_to_write(self[name], self) @@ -6047,7 +6029,7 @@ Fields: result[name] = field.convert_to_onchange(self[name], record, {}) else: # x2many fields: serialize value as commands - result[name] = commands = [(5,)] + result[name] = commands = [Command.clear()] # The purpose of the following line is to enable the prefetching. # In the loop below, line._prefetch_ids actually depends on the # value of record[name] in cache (see prefetch_ids on x2many @@ -6062,7 +6044,7 @@ Fields: if not line.id: # new line: send diff from scratch line_diff = line_snapshot.diff({}) - commands.append((0, line.id.ref or 0, line_diff)) + commands.append((Command.CREATE, line.id.ref or 0, line_diff)) else: # existing line: check diff from database # (requires a clean record cache!) @@ -6071,9 +6053,9 @@ Fields: # send all fields because the web client # might need them to evaluate modifiers line_diff = line_snapshot.diff({}) - commands.append((1, line.id, line_diff)) + commands.append(Command.update(line.id, line_diff)) else: - commands.append((4, line.id)) + commands.append(Command.link(line.id)) return result nametree = PrefixTree(self.browse(), field_onchange) @@ -6093,9 +6075,9 @@ Fields: # retrieve all line ids in commands line_ids = set() for cmd in values[name]: - if cmd[0] in (1, 4): + if cmd[0] in (Command.UPDATE, Command.LINK): line_ids.add(cmd[1]) - elif cmd[0] == 6: + elif cmd[0] == Command.SET: line_ids.update(cmd[2]) # prefetch stored fields on lines lines = self[name].browse(line_ids) @@ -6507,4 +6489,4 @@ def lazy_name_get(self): # keep those imports here to avoid dependency cycle errors from .osv import expression -from .fields import Field, Datetime +from .fields import Field, Datetime, Command diff --git a/odoo/tools/convert.py b/odoo/tools/convert.py index 6295f03e56c..cca2529374c 100644 --- a/odoo/tools/convert.py +++ b/odoo/tools/convert.py @@ -53,6 +53,7 @@ class RecordDictWrapper(dict): def _get_idref(self, env, model_str, idref): idref2 = dict(idref, + Command=odoo.fields.Command, time=time, DateTime=datetime, datetime=datetime, @@ -301,10 +302,10 @@ form: module.record_id""" % (xml_id,) for group in g_names: if group.startswith('-'): group_id = self.id_get(group[1:]) - groups_value.append((3, group_id)) + groups_value.append(odoo.Command.unlink(group_id)) else: group_id = self.id_get(group) - groups_value.append((4, group_id)) + groups_value.append(odoo.Command.link(group_id)) res['groups_id'] = groups_value if rec.get('paperformat'): pf_name = rec.get('paperformat') @@ -405,10 +406,10 @@ form: module.record_id""" % (xml_id,) for group in g_names: if group.startswith('-'): group_id = self.id_get(group[1:]) - groups_value.append((3, group_id)) + groups_value.append(odoo.Command.unlink(group_id)) else: group_id = self.id_get(group) - groups_value.append((4, group_id)) + groups_value.append(odoo.Command.link(group_id)) res['groups_id'] = groups_value if rec.get('target'): @@ -467,10 +468,10 @@ form: module.record_id""" % (xml_id,) for group in rec.get('groups', '').split(','): if group.startswith('-'): group_id = self.id_get(group[1:]) - groups.append((3, group_id)) + groups.append(odoo.Command.unlink(group_id)) elif group: group_id = self.id_get(group) - groups.append((4, group_id)) + groups.append(odoo.Command.link(group_id)) if groups: values['groups_id'] = groups @@ -551,7 +552,7 @@ form: module.record_id""" % (xml_id,) _fields = env[rec_model]._fields # if the current field is many2many if (f_name in _fields) and _fields[f_name].type == 'many2many': - f_val = [(6, 0, [x[f_use] for x in s])] + f_val = [odoo.Command.set([x[f_use] for x in s])] elif len(s): # otherwise (we are probably in a many2one field), # take the first element of the search @@ -637,7 +638,7 @@ form: module.record_id""" % (xml_id,) groups = el.attrib.pop('groups', None) if groups: grp_lst = [("ref('%s')" % x) for x in groups.split(',')] - record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]")) + record.append(Field(name="groups_id", eval="[Command.set(["+', '.join(grp_lst)+"])]")) if el.get('primary') == 'True': # Pseudo clone mode, we'll set the t-name to the full canonical xmlid el.append(