diff --git a/odoo/addons/test_convert/__init__.py b/odoo/addons/test_convert/__init__.py index e69de29bb2d..a301ab24b52 100644 --- a/odoo/addons/test_convert/__init__.py +++ b/odoo/addons/test_convert/__init__.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class Usered(models.Model): + _name = 'test_convert.usered' + _description = "z test model ignore" + + name = fields.Char() + user_id = fields.Many2one('res.users', default=lambda self: self.env.user) + tz = fields.Char(default=lambda self: self.env.context.get('tz') or self.env.user.tz) diff --git a/odoo/addons/test_convert/__manifest__.py b/odoo/addons/test_convert/__manifest__.py index af6cd1af6e4..1411f093f90 100644 --- a/odoo/addons/test_convert/__manifest__.py +++ b/odoo/addons/test_convert/__manifest__.py @@ -5,4 +5,7 @@ 'name': 'test_convert', 'description': "Data for xml conversion tests", 'version': '0.0.1', + 'data': [ + 'ir.model.access.csv', + ] } diff --git a/odoo/addons/test_convert/ir.model.access.csv b/odoo/addons/test_convert/ir.model.access.csv new file mode 100644 index 00000000000..0282a269c41 --- /dev/null +++ b/odoo/addons/test_convert/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_test_convert_usered,access_test_convert_usered,model_test_convert_usered,base.group_user,1,1,1,1 diff --git a/odoo/addons/test_convert/tests/__init__.py b/odoo/addons/test_convert/tests/__init__.py index 2b608870b30..14d4ce40ead 100644 --- a/odoo/addons/test_convert/tests/__init__.py +++ b/odoo/addons/test_convert/tests/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- from . import test_convert +from . import test_env diff --git a/odoo/addons/test_convert/tests/test_env.py b/odoo/addons/test_convert/tests/test_env.py new file mode 100644 index 00000000000..fc5d9a094d3 --- /dev/null +++ b/odoo/addons/test_convert/tests/test_env.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +""" +Tests the ability to update environmental information on various nodes (e.g. +change user, add context keys, ...) +""" +import os.path + +from lxml import etree +from lxml.builder import E + +from odoo.tests import common +from odoo.tools import config +from odoo.tools.convert import xml_import + +odoo = E.odoo +data = E.data +record = E.record +field = E.field +function = E.function + +class TestEnv(common.TransactionCase): + def setUp(self): + super().setUp() + self._importer = xml_import(self.env.cr, 'test_convert', None, 'init') + + def importer(self, doc): + etree.RelaxNG( + etree.parse( + os.path.join(config['root_path'], 'import_xml.rng') + ) + ).assert_(doc) + self._importer.parse(doc) + + def test_uid_data_record(self): + self.importer( + odoo( + record( + field("a", name="name"), + model="test_convert.usered", + id="test_convert.testing" + ), + uid="base.user_demo" + ) + ) + + r = self.env.ref('test_convert.testing') + self.assertEqual(r.name, 'a') + self.assertEqual(r.create_uid, self.env.ref('base.user_demo')) + self.assertEqual(r.user_id, self.env.ref('base.user_demo')) + + def test_uid_data_function(self): + self.importer( + odoo( + function( + model="test_convert.usered", + name="create", + eval="[[{'name': 'b'}]]", + ), + uid="base.user_demo" + ) + ) + + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'b') + self.assertEqual(r.create_uid, self.env.ref('base.user_demo')) + self.assertEqual(r.user_id, self.env.ref('base.user_demo')) + + def test_uid_record(self): + self.importer( + odoo( + record( + field('c', name="name"), + model="test_convert.usered", + id="test_convert.testing", + uid="base.user_demo" + ), + uid="base.user_root" + ) + ) + + r = self.env.ref('test_convert.testing') + self.assertEqual(r.name, 'c') + self.assertEqual(r.create_uid, self.env.ref('base.user_demo')) + self.assertEqual(r.user_id, self.env.ref('base.user_demo')) + + + def test_uid_function(self): + self.importer( + odoo( + function( + model="test_convert.usered", + name="create", + uid="base.user_demo", + eval="[[{'name': 'd'}]]" + ), + uid="base.user_root" + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'd') + self.assertEqual(r.create_uid, self.env.ref('base.user_demo')) + self.assertEqual(r.user_id, self.env.ref('base.user_demo')) + + def test_context_data_function(self): + self.env.user.tz = 'UTC' + self.importer( + odoo( + function( + model="test_convert.usered", + name="create", + eval="[[{'name': 'e'}]]", + ), + context="{'tz': 'Asia/Kabul'}", + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'e') + self.assertEqual(r.tz, 'Asia/Kabul') + + def test_context_function(self): + self.env.user.tz = 'UTC' + self.importer( + odoo( + function( + model="test_convert.usered", + name="create", + context="{'tz': 'Pacific/Apia'}", + eval="[[{'name': 'e'}]]", + ), + context="{'tz': 'Asia/Kabul'}", + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'e') + self.assertEqual(r.tz, 'Pacific/Apia') + + def test_context_data_record(self): + self.env.user.tz = 'UTC' + self.importer( + odoo( + record( + field("f", name="name"), + model="test_convert.usered", + ), + context="{'tz': 'America/Knox_IN'}" + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'f') + self.assertEqual(r.tz, 'America/Knox_IN') + + def test_context_record(self): + self.env.user.tz = 'UTC' + self.importer( + odoo( + record( + field("f", name="name"), + model="test_convert.usered", + context="{'tz': 'America/Adak'}", + ), + context="{'tz': 'America/Knox_IN'}" + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'f') + self.assertEqual(r.tz, 'America/Adak') diff --git a/odoo/import_xml.rng b/odoo/import_xml.rng index 52ff0f1b420..ccc75af1665 100644 --- a/odoo/import_xml.rng +++ b/odoo/import_xml.rng @@ -1,6 +1,11 @@ + + + + + @@ -37,9 +42,7 @@ - - - + @@ -163,7 +166,7 @@ - + @@ -303,7 +306,7 @@ data - + diff --git a/odoo/tools/__init__.py b/odoo/tools/__init__.py index 6e89bdd9d43..2381f8c3472 100644 --- a/odoo/tools/__init__.py +++ b/odoo/tools/__init__.py @@ -7,7 +7,6 @@ from . import appdirs from . import pdf from .config import config from .misc import * -from .convert import * from .translate import * from .graph import graph from .image import * @@ -18,3 +17,4 @@ from .func import * from .debugger import * from .xml_utils import * from .date_utils import * +from .convert import * diff --git a/odoo/tools/convert.py b/odoo/tools/convert.py index 4363fa25de6..0399394f887 100644 --- a/odoo/tools/convert.py +++ b/odoo/tools/convert.py @@ -19,7 +19,7 @@ from . import assertion_report, pycompat from .config import config from .misc import file_open, unquote, ustr, SKIPPED_ELEMENT_TYPES from .translate import _ -from odoo import SUPERUSER_ID +from odoo import SUPERUSER_ID, api _logger = logging.getLogger(__name__) @@ -177,20 +177,32 @@ def _eval_xml(self, node, env): return tuple(res) return res elif node.tag == "function": - args = [] a_eval = node.get('eval') - # FIXME: should probably be exclusive if a_eval: self.idref['ref'] = self.id_get - args = safe_eval(a_eval, self.idref) - for n in node: - return_val = _eval_xml(self, n, env) - if return_val is not None: - args.append(return_val) + # ensure the args are a list (sometimes folks eval a tuple which + # is inconvenient when trying to concatenate w/ a list) + args = list(safe_eval(a_eval, self.idref)) + else: + args = [ + r for r in (_eval_xml(self, n, env) for n in node) + if r is not None + ] + model = env[node.get('model')] - method = node.get('name') - # this one still depends on the old API - return odoo.api.call_kw(model, method, args, {}) + method_name = node.get('name') + method = getattr(model, method_name) + + # this mess is necessary to merge @context and a possible positional + # context parameter, as works on the old API so ids and + # context are just args + ids, args = [], list(args) + if getattr(method, '_api', None) not in ('model', 'model_create'): + ids, args = args[:1], args[1:] + context, args, kwargs = api.split_context(method, args, {}) + kwargs['context'] = {**env.context, **(context or {})} + + return odoo.api.call_kw(model, method_name, ids + args, kwargs) elif node.tag == "test": return node.text @@ -213,33 +225,21 @@ class xml_import(object): def isnoupdate(self, data_node=None): return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False)) - def get_context(self, data_node, node, eval_dict): - data_node_context = (len(data_node) and data_node.get('context','')) - node_context = node.get("context") - context = {} - for ctx in (data_node_context, node_context): - if ctx: - try: - ctx_res = safe_eval(ctx, eval_dict) - if isinstance(context, dict): - context.update(ctx_res) - else: - context = ctx_res - except (ValueError, NameError): - # Some contexts contain references that are only valid at runtime at - # client-side, so in that case we keep the original context string - # as it is. We also log it, just in case. - context = ctx - _logger.debug('Context value (%s) for element with id "%s" or its data node does not parse '\ - 'at server-side, keeping original string, in case it\'s meant for client side only', - ctx, node.get('id','n/a'), exc_info=True) - return context - - def get_uid(self, data_node, node): - node_uid = node.get('uid','') or (len(data_node) and data_node.get('uid','')) - if node_uid: - return self.id_get(node_uid) - return self.env.user.id + def get_env(self, node, eval_context=None): + uid = node.get('uid') + context = node.get('context') + if uid or context: + return self.env( + user=uid and self.id_get(uid), + context=context and { + **self.env.context, + **safe_eval(context, { + 'ref': self.id_get, + **(eval_context or {}) + }) + } + ) + return self.env def make_xml_id(self, xml_id): if not xml_id or '.' in xml_id: @@ -335,9 +335,7 @@ form: module.record_id""" % (xml_id,) def _tag_function(self, rec, data_node): if self.isnoupdate(data_node) and self.mode != 'init': return - context = self.get_context(data_node, rec, {'ref': self.id_get}) - uid = self.get_uid(data_node, rec) - env = self.env(user=uid, context=context) + env = self.get_env(rec) _eval_xml(self, rec, env) def _tag_act_window(self, rec, data_node): @@ -388,9 +386,8 @@ form: module.record_id""" % (xml_id,) 'active_id': active_id, 'active_ids': active_ids, 'active_model': active_model, - 'ref': self.id_get, } - context = self.get_context(data_node, rec, eval_context) + context = self.get_env(rec, eval_context).context try: domain = safe_eval(domain, eval_context) @@ -516,14 +513,11 @@ form: module.record_id""" % (xml_id,) rec_string = rec.get("string") or 'unknown' records = None - eval_dict = {'ref': self.id_get} - context = self.get_context(data_node, rec, eval_dict) - uid = self.get_uid(data_node, rec) - env = self.env(user=uid, context=context) + env = self.get_env(rec) if rec_id: records = env[rec_model].browse(self.id_get(rec_id)) elif rec_src: - q = safe_eval(rec_src, eval_dict) + q = safe_eval(rec_src, {'ref': self.id_get}) records = env[rec_model].search(q) if rec_src_count: count = int(rec_src_count) @@ -547,7 +541,6 @@ form: module.record_id""" % (xml_id,) globals_dict['_ref'] = ref for test in rec.findall('./test'): f_expr = test.get("expr",'') - env = self.env(user=uid, context=context) expected_value = _eval_xml(self, test, env) or True expression_value = safe_eval(f_expr, globals_dict) if expression_value != expected_value: # assertion failed @@ -564,15 +557,16 @@ form: module.record_id""" % (xml_id,) def _tag_record(self, rec, data_node): rec_model = rec.get("model") - model = self.env[rec_model] + env = self.get_env(rec) rec_id = rec.get("id", '') - rec_context = rec.get("context", {}) - if rec_context: - rec_context = safe_eval(rec_context) + + model = env[rec_model] if self.xml_filename and rec_id: - rec_context['install_module'] = self.module - rec_context['install_filename'] = self.xml_filename + model = model.with_context( + install_module=self.module, + install_filename=self.xml_filename + ) self._test_xml_id(rec_id) xid = self.make_xml_id(rec_id) @@ -585,7 +579,7 @@ form: module.record_id""" % (xml_id,) if not rec_id: return None - record = self.env['ir.model.data']._load_xmlid(xid) + record = env['ir.model.data']._load_xmlid(xid) if record: # if the resource already exists, don't update it but store # its database id (can be useful) @@ -609,16 +603,13 @@ form: module.record_id""" % (xml_id,) f_val = False if f_search: - context = self.get_context(data_node, rec, {'ref': self.id_get}) - uid = self.get_uid(data_node, rec) - env = self.env(user=uid, context=context) idref2 = _get_idref(self, env, f_model, self.idref) q = safe_eval(f_search, idref2) assert f_model, 'Define an attribute model="..." in your .XML file !' # browse the objects searched - s = self.env[f_model].search(q) + s = env[f_model].search(q) # column definitions of the "local" object - _fields = self.env[rec_model]._fields + _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])] @@ -633,7 +624,7 @@ form: module.record_id""" % (xml_id,) else: f_val = self.id_get(f_ref) else: - f_val = _eval_xml(self, field, self.env) + f_val = _eval_xml(self, field, env) if f_name in model._fields: if model._fields[f_name].type == 'integer': f_val = int(f_val) @@ -644,11 +635,11 @@ form: module.record_id""" % (xml_id,) res[f_name] = f_val data = dict(xml_id=xid, values=res, noupdate=self.isnoupdate(data_node)) - record = model.with_context(rec_context)._load_records([data], self.mode == 'update') + record = model._load_records([data], self.mode == 'update') if rec_id: self.idref[rec_id] = record.id if config.get('import_partial'): - self.env.cr.commit() + env.cr.commit() return rec_model, record.id def _tag_template(self, el, data_node): @@ -735,20 +726,21 @@ form: module.record_id""" % (xml_id,) f = self._tags.get(rec.tag) if f is None: continue + + self.envs.append(self.get_env(el)) try: f(rec, el) - except Exception as e: - exc_info = sys.exc_info() - pycompat.reraise( - ParseError, - ParseError(ustr(e), etree.tostring(rec, encoding='unicode').rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), - exc_info[2] - ) + finally: + self.envs.pop() + + @property + def env(self): + return self.envs[-1] def __init__(self, cr, module, idref, mode, report=None, noupdate=False, xml_filename=None): self.mode = mode self.module = module - self.env = odoo.api.Environment(cr, SUPERUSER_ID, {}) + self.envs = [odoo.api.Environment(cr, SUPERUSER_ID, {})] self.idref = {} if idref is None else idref if report is None: report = assertion_report.assertion_report() @@ -767,9 +759,21 @@ form: module.record_id""" % (xml_id,) **dict.fromkeys(self.DATA_ROOTS, self._tag_root) } + def parse(self, de): assert de.tag in self.DATA_ROOTS, "Root xml tag must be , or ." - self._tag_root(de, de) + try: + self._tag_root(de, de) + except Exception as e: + exc_info = sys.exc_info() + pycompat.reraise( + ParseError, + ParseError( + ustr(e), + etree.tostring(de, encoding='unicode').rstrip(), + de.getroottree().docinfo.URL, de.sourceline), + exc_info[2] + ) DATA_ROOTS = ['odoo', 'data', 'openerp'] def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None, pathname=None):