diff --git a/addons/hr_timesheet/data/hr_timesheet_data.xml b/addons/hr_timesheet/data/hr_timesheet_data.xml
index 5bbea815d2c..2ae8070b787 100644
--- a/addons/hr_timesheet/data/hr_timesheet_data.xml
+++ b/addons/hr_timesheet/data/hr_timesheet_data.xml
@@ -18,7 +18,7 @@
-
diff --git a/addons/note/data/res_users_data.xml b/addons/note/data/res_users_data.xml
index 874982a310b..787bc731f87 100644
--- a/addons/note/data/res_users_data.xml
+++ b/addons/note/data/res_users_data.xml
@@ -1,7 +1,7 @@
-
diff --git a/addons/purchase_requisition/data/purchase_requisition_data.xml b/addons/purchase_requisition/data/purchase_requisition_data.xml
index 0b43a332697..84194b8d89b 100644
--- a/addons/purchase_requisition/data/purchase_requisition_data.xml
+++ b/addons/purchase_requisition/data/purchase_requisition_data.xml
@@ -2,7 +2,7 @@
-
diff --git a/addons/resource/data/resource_data.xml b/addons/resource/data/resource_data.xml
index 52bd5f1895e..1390e0bd518 100644
--- a/addons/resource/data/resource_data.xml
+++ b/addons/resource/data/resource_data.xml
@@ -12,7 +12,7 @@
-
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..244227ac239 100644
--- a/odoo/import_xml.rng
+++ b/odoo/import_xml.rng
@@ -1,6 +1,11 @@
+
+
+
+
+
@@ -37,9 +42,7 @@
-
-
-
+
@@ -163,7 +166,7 @@
-
+
@@ -232,47 +235,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -303,7 +265,7 @@
data
-
+
@@ -313,11 +275,8 @@
-
-
-
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 ab530f6bbd6..1f909d74f28 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
@@ -198,48 +210,30 @@ def _eval_xml(self, node, env):
def str2bool(value):
return value.lower() not in ('0', 'false', 'off')
+def nodeattr2bool(node, attr, default=False):
+ if not node.get(attr):
+ return default
+ val = node.get(attr).strip()
+ if not val:
+ return default
+ return str2bool(val)
class xml_import(object):
-
- @staticmethod
- def nodeattr2bool(node, attr, default=False):
- if not node.get(attr):
- return default
- val = node.get(attr).strip()
- if not val:
- return default
- return str2bool(val)
-
- 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.uid
+ 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:
@@ -247,7 +241,6 @@ class xml_import(object):
return "%s.%s" % (self.module, xml_id)
def _test_xml_id(self, xml_id):
- id = xml_id
if '.' in xml_id:
module, id = xml_id.split('.', 1)
assert '.' not in id, """The ID reference "%s" must contain
@@ -257,30 +250,30 @@ form: module.record_id""" % (xml_id,)
modcnt = self.env['ir.module.module'].search_count([('name', '=', module), ('state', '=', 'installed')])
assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
- def _tag_delete(self, rec, data_node=None, mode=None):
- d_model = rec.get("model")
- d_search = rec.get("search")
- d_id = rec.get("id")
+ def _tag_delete(self, rec, _data_node):
+ d_model = rec["model"]
records = self.env[d_model]
+ d_search = rec.get("search")
if d_search:
idref = _get_idref(self, self.env, d_model, {})
try:
records = records.search(safe_eval(d_search, idref))
except ValueError:
_logger.warning('Skipping deletion for failed search `%r`', d_search, exc_info=True)
- pass
+
+ d_id = rec.get("id")
if d_id:
try:
records += records.browse(self.id_get(d_id))
except ValueError:
# d_id cannot be found. doesn't matter in this case
_logger.warning('Skipping deletion for missing XML ID `%r`', d_id, exc_info=True)
- pass
+
if records:
records.unlink()
- def _tag_report(self, rec, data_node=None, mode=None):
+ def _tag_report(self, rec):
res = {}
for dest,f in (('name','string'),('model','model'),('report_name','name')):
res[dest] = rec.get(f)
@@ -322,7 +315,7 @@ form: module.record_id""" % (xml_id,)
res['paperformat_id'] = pf_id
xid = self.make_xml_id(xml_id)
- data = dict(xml_id=xid, values=res, noupdate=self.isnoupdate(data_node))
+ data = dict(xml_id=xid, values=res, noupdate=self.noupdate)
report = self.env['ir.actions.report']._load_records([data], self.mode == 'update')
self.idref[xml_id] = report.id
@@ -333,16 +326,13 @@ form: module.record_id""" % (xml_id,)
report.unlink_action()
return report.id
- def _tag_function(self, rec, data_node=None, mode=None):
- if self.isnoupdate(data_node) and self.mode != 'init':
+ def _tag_function(self, rec):
+ if self.noupdate 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)
- return
- def _tag_act_window(self, rec, data_node=None, mode=None):
+ def _tag_act_window(self, rec):
name = rec.get('name')
xml_id = rec.get('id','')
self._test_xml_id(xml_id)
@@ -357,7 +347,7 @@ form: module.record_id""" % (xml_id,)
view_mode = rec.get('view_mode') or 'tree,form'
usage = rec.get('usage')
limit = rec.get('limit')
- uid = self.uid
+ uid = self.env.user.id
# Act_window's 'domain' and 'context' contain mostly literals
# but they can also refer to the variables provided below
@@ -390,9 +380,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)
@@ -441,144 +430,79 @@ form: module.record_id""" % (xml_id,)
res['binding_type'] = 'action_form_only'
xid = self.make_xml_id(xml_id)
- data = dict(xml_id=xid, values=res, noupdate=self.isnoupdate(data_node))
+ data = dict(xml_id=xid, values=res, noupdate=self.noupdate)
self.env['ir.actions.act_window']._load_records([data], self.mode == 'update')
- def _tag_menuitem(self, rec, data_node=None, mode=None):
- rec_id = rec.get("id")
+ def _tag_menuitem(self, rec):
+ rec_id = rec.attrib["id"]
self._test_xml_id(rec_id)
# The parent attribute was specified, if non-empty determine its ID, otherwise
# explicitly make a top-level menu
- if rec.get('parent'):
- menu_parent_id = self.id_get(rec.get('parent',''))
- else:
- # we get here with