[IMP] data files: proper uid and context support
Linked to task 1887993, related to the de-magic-ification of the "admin"
user: currently data files are "executed" as the root user, re-linking
relevant records to the admin user is a PITA (e.g.
97b82ff7c1).
Just executing all demo files as the admin is not feasible (some things
a regular user literally can't do), but the ability to execute entire
data files as a specified user or with specific context keys set, and
furthermore override that at the function/record level, is useful.
This:
* refactors some of convert_xml to prep for the rest
* cleans up and fixes the handling of uid/context to use them in record
and function tags and properly inherit them from data parents
* cleans up some of the rng & conversion code (unused features, dead
code, broken features, …)
closes odoo/odoo#27383
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
</record>
|
||||
|
||||
<!-- Force Analytic account creation for projects allowing timesheet (default is True) -->
|
||||
<function id="_init_data_analytic_account"
|
||||
<function
|
||||
model="project.project"
|
||||
name="_init_data_analytic_account"
|
||||
eval="[]"/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<function id="init_data_user_note_stages"
|
||||
<function
|
||||
model="res.users"
|
||||
name="_init_data_user_note_stages"
|
||||
eval="[]"/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<function id="purchase_default_set"
|
||||
<function
|
||||
model="ir.default" name="set"
|
||||
eval="('purchase.requisition', 'warehouse_id', ref('stock.warehouse0'))"/>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<field name="resource_calendar_id" ref="resource_calendar_std"/>
|
||||
</record>
|
||||
|
||||
<function id="_init_data_resource_calendar"
|
||||
<function
|
||||
model="res.company"
|
||||
name="_init_data_resource_calendar"
|
||||
eval="[]"/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,4 +5,7 @@
|
||||
'name': 'test_convert',
|
||||
'description': "Data for xml conversion tests",
|
||||
'version': '0.0.1',
|
||||
'data': [
|
||||
'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
|
||||
|
@@ -1,2 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_convert
|
||||
from . import test_env
|
||||
|
||||
@@ -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')
|
||||
+8
-49
@@ -1,6 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
|
||||
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
|
||||
<rng:define name="env">
|
||||
<rng:optional><rng:attribute name="uid" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="context" /></rng:optional>
|
||||
</rng:define>
|
||||
|
||||
<rng:define name="any">
|
||||
<rng:element>
|
||||
<rng:anyName/>
|
||||
@@ -37,9 +42,7 @@
|
||||
<rng:element name="function">
|
||||
<rng:attribute name="model" />
|
||||
<rng:attribute name="name" />
|
||||
<!-- doesn't look like it has any use? -->
|
||||
<rng:optional><rng:attribute name="id" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="context" /></rng:optional>
|
||||
<rng:ref name="env"/>
|
||||
<rng:optional>
|
||||
<rng:choice>
|
||||
<rng:attribute name="eval" />
|
||||
@@ -163,7 +166,7 @@
|
||||
</rng:optional>
|
||||
</rng:optional>
|
||||
<rng:attribute name="model" />
|
||||
<rng:optional><rng:attribute name="context"/></rng:optional>
|
||||
<rng:ref name="env"/>
|
||||
<rng:zeroOrMore>
|
||||
<rng:ref name="field" />
|
||||
</rng:zeroOrMore>
|
||||
@@ -232,47 +235,6 @@
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
|
||||
<!-- TODO: remove these, useless -->
|
||||
|
||||
<rng:define name="assert">
|
||||
<rng:element name="assert">
|
||||
<rng:attribute name="model" />
|
||||
<rng:optional><rng:attribute name="search" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="count" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="string" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="id" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="context" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="severity" /></rng:optional>
|
||||
<rng:zeroOrMore>
|
||||
<rng:element name="test">
|
||||
<rng:attribute name="expr"/>
|
||||
<rng:choice>
|
||||
<rng:text />
|
||||
<rng:empty />
|
||||
</rng:choice>
|
||||
</rng:element>
|
||||
</rng:zeroOrMore>
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
|
||||
<rng:define name="ir_set">
|
||||
<rng:element name="ir_set">
|
||||
<rng:oneOrMore>
|
||||
<rng:ref name="field" />
|
||||
</rng:oneOrMore>
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
|
||||
<rng:define name="url">
|
||||
<rng:element name="url">
|
||||
<rng:attribute name="id" />
|
||||
<rng:attribute name="name" />
|
||||
<rng:attribute name="url" />
|
||||
<rng:optional><rng:attribute name="target" /></rng:optional>
|
||||
<rng:empty />
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
|
||||
<rng:define name="act_window">
|
||||
<rng:element name="act_window">
|
||||
<rng:attribute name="id" />
|
||||
@@ -303,7 +265,7 @@
|
||||
<rng:name>data</rng:name>
|
||||
</rng:choice>
|
||||
<rng:optional><rng:attribute name="noupdate" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="context" /></rng:optional>
|
||||
<rng:ref name="env"/>
|
||||
<rng:zeroOrMore>
|
||||
<rng:choice>
|
||||
<rng:text/>
|
||||
@@ -313,11 +275,8 @@
|
||||
<rng:ref name="template" />
|
||||
<rng:ref name="delete" />
|
||||
<rng:ref name="act_window" />
|
||||
<rng:ref name="url" />
|
||||
<rng:ref name="assert" />
|
||||
<rng:ref name="report" />
|
||||
<rng:ref name="function" />
|
||||
<rng:ref name="ir_set" />
|
||||
</rng:choice>
|
||||
</rng:zeroOrMore>
|
||||
</rng:element>
|
||||
|
||||
@@ -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 *
|
||||
|
||||
+169
-233
@@ -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 <function> 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 <menuitem parent="">, explicit clear of parent, or
|
||||
# if no parent attribute at all but menu name is not a menu path
|
||||
menu_parent_id = False
|
||||
values = {'parent_id': menu_parent_id}
|
||||
if rec.get('name'):
|
||||
values['name'] = rec.get('name')
|
||||
try:
|
||||
res = [ self.id_get(rec.get('id','')) ]
|
||||
except:
|
||||
res = None
|
||||
|
||||
if rec.get('action'):
|
||||
a_action = rec.get('action')
|
||||
|
||||
# determine the type of action
|
||||
action_model, action_id = self.model_id_get(a_action)
|
||||
action_type = action_model.split('.')[-1] # keep only type part
|
||||
values['action'] = "ir.actions.%s,%d" % (action_type, action_id)
|
||||
|
||||
if not values.get('name') and action_type in ('act_window', 'wizard', 'url', 'client', 'server'):
|
||||
resw = self.env[action_model].sudo().browse(action_id).name
|
||||
if resw:
|
||||
values['name'] = resw
|
||||
|
||||
if not values.get('name'):
|
||||
# ensure menu has a name
|
||||
values['name'] = rec_id or '?'
|
||||
values = {
|
||||
'parent_id': False,
|
||||
'active': nodeattr2bool(rec, 'active', default=True),
|
||||
}
|
||||
|
||||
if rec.get('sequence'):
|
||||
values['sequence'] = int(rec.get('sequence'))
|
||||
|
||||
values['active'] = self.nodeattr2bool(rec, 'active', default=True)
|
||||
|
||||
if rec.get('groups'):
|
||||
g_names = rec.get('groups','').split(',')
|
||||
groups_value = []
|
||||
for group in g_names:
|
||||
if group.startswith('-'):
|
||||
group_id = self.id_get(group[1:])
|
||||
groups_value.append((3, group_id))
|
||||
else:
|
||||
group_id = self.id_get(group)
|
||||
groups_value.append((4, group_id))
|
||||
values['groups_id'] = groups_value
|
||||
|
||||
if not values.get('parent_id'):
|
||||
if rec.get('parent'):
|
||||
values['parent_id'] = self.id_get(rec.attrib['parent'])
|
||||
else:
|
||||
if rec.get('web_icon'):
|
||||
values['web_icon'] = rec.get('web_icon')
|
||||
values['web_icon'] = rec.attrib['web_icon']
|
||||
|
||||
xid = self.make_xml_id(rec_id)
|
||||
data = dict(xml_id=xid, values=values, noupdate=self.isnoupdate(data_node))
|
||||
|
||||
if rec.get('name'):
|
||||
values['name'] = rec.attrib['name']
|
||||
|
||||
if rec.get('action'):
|
||||
a_action = rec.attrib['action']
|
||||
|
||||
if '.' not in a_action:
|
||||
a_action = '%s.%s' % (self.module, a_action)
|
||||
act = self.env.ref(a_action).sudo()
|
||||
values['action'] = "%s,%d" % (act.type, act.id)
|
||||
|
||||
if not values.get('name') and act.type.endswith(('act_window', 'wizard', 'url', 'client', 'server')) and act.name:
|
||||
values['name'] = act.name
|
||||
|
||||
if not values.get('name'):
|
||||
values['name'] = rec_id or '?'
|
||||
|
||||
|
||||
groups = []
|
||||
for group in rec.get('groups', '').split(','):
|
||||
if group.startswith('-'):
|
||||
group_id = self.id_get(group[1:])
|
||||
groups.append((3, group_id))
|
||||
elif group:
|
||||
group_id = self.id_get(group)
|
||||
groups.append((4, group_id))
|
||||
if groups:
|
||||
values['groups_id'] = groups
|
||||
|
||||
|
||||
data = {
|
||||
'xml_id': self.make_xml_id(rec_id),
|
||||
'values': values,
|
||||
'noupdate': self.noupdate,
|
||||
}
|
||||
self.env['ir.ui.menu']._load_records([data], self.mode == 'update')
|
||||
|
||||
def _assert_equals(self, f1, f2, prec=4):
|
||||
return not round(f1 - f2, prec)
|
||||
|
||||
def _tag_assert(self, rec, data_node=None, mode=None):
|
||||
if self.isnoupdate(data_node) and self.mode != 'init':
|
||||
return
|
||||
|
||||
def _tag_record(self, rec):
|
||||
rec_model = rec.get("model")
|
||||
rec_id = rec.get("id")
|
||||
self._test_xml_id(rec_id)
|
||||
rec_src = rec.get("search")
|
||||
rec_src_count = rec.get("count")
|
||||
|
||||
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)
|
||||
if rec_id:
|
||||
records = env[rec_model].browse(self.id_get(rec_id))
|
||||
elif rec_src:
|
||||
q = safe_eval(rec_src, eval_dict)
|
||||
records = env[rec_model].search(q)
|
||||
if rec_src_count:
|
||||
count = int(rec_src_count)
|
||||
if len(records) != count:
|
||||
self.assertion_report.record_failure()
|
||||
msg = 'assertion "%s" failed!\n' \
|
||||
' Incorrect search count:\n' \
|
||||
' expected count: %d\n' \
|
||||
' obtained count: %d\n' \
|
||||
% (rec_string, count, len(records))
|
||||
_logger.error(msg)
|
||||
return
|
||||
|
||||
assert records is not None,\
|
||||
'You must give either an id or a search criteria'
|
||||
ref = self.id_get
|
||||
for record in records:
|
||||
globals_dict = RecordDictWrapper(record)
|
||||
globals_dict['floatEqual'] = self._assert_equals
|
||||
globals_dict['ref'] = ref
|
||||
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
|
||||
self.assertion_report.record_failure()
|
||||
msg = 'assertion "%s" failed!\n' \
|
||||
' xmltag: %s\n' \
|
||||
' expected value: %r\n' \
|
||||
' obtained value: %r\n' \
|
||||
% (rec_string, etree.tostring(test, encoding='unicode'), expected_value, expression_value)
|
||||
_logger.error(msg)
|
||||
return
|
||||
else: # all tests were successful for this assertion tag (no break)
|
||||
self.assertion_report.record_success()
|
||||
|
||||
def _tag_record(self, rec, data_node=None, mode=None):
|
||||
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)
|
||||
@@ -586,18 +510,18 @@ form: module.record_id""" % (xml_id,)
|
||||
# in update mode, the record won't be updated if the data node explicitly
|
||||
# opt-out using @noupdate="1". A second check will be performed in
|
||||
# model._load_records() using the record's ir.model.data `noupdate` field.
|
||||
if self.isnoupdate(data_node) and self.mode != 'init':
|
||||
if self.noupdate and self.mode != 'init':
|
||||
# check if the xml record has no id, skip
|
||||
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)
|
||||
self.idref[rec_id] = record.id
|
||||
return None
|
||||
elif not self.nodeattr2bool(rec, 'forcecreate', True):
|
||||
elif not nodeattr2bool(rec, 'forcecreate', True):
|
||||
# if it doesn't exist and we shouldn't create it, skip it
|
||||
return None
|
||||
# else create it normally
|
||||
@@ -615,16 +539,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])]
|
||||
@@ -639,7 +560,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)
|
||||
@@ -649,15 +570,15 @@ form: module.record_id""" % (xml_id,)
|
||||
f_val = str2bool(f_val)
|
||||
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')
|
||||
data = dict(xml_id=xid, values=res, noupdate=self.noupdate)
|
||||
record = model._load_records([data], self.mode == 'update')
|
||||
if rec_id:
|
||||
self.idref[rec_id] = record.id
|
||||
if config.get('import_partial'):
|
||||
self.cr.commit()
|
||||
env.cr.commit()
|
||||
return rec_model, record.id
|
||||
|
||||
def _tag_template(self, el, data_node=None, mode=None):
|
||||
def _tag_template(self, el):
|
||||
# This helper transforms a <template> element into a <record> and forwards it
|
||||
tpl_id = el.get('id', el.get('t-name'))
|
||||
full_tpl_id = tpl_id
|
||||
@@ -701,7 +622,7 @@ form: module.record_id""" % (xml_id,)
|
||||
record.append(Field(el.get('key'), name='key'))
|
||||
if el.get('active') in ("True", "False"):
|
||||
view_id = self.id_get(tpl_id, raise_if_not_found=False)
|
||||
if mode != "update" or not view_id:
|
||||
if self.mode != "update" or not view_id:
|
||||
record.append(Field(name='active', eval=el.get('active')))
|
||||
if el.get('customize_show') in ("True", "False"):
|
||||
record.append(Field(name='customize_show', eval=el.get('customize_show')))
|
||||
@@ -723,7 +644,7 @@ form: module.record_id""" % (xml_id,)
|
||||
# the ``arch`` field
|
||||
record.append(Field(el, name="arch", type="xml"))
|
||||
|
||||
return self._tag_record(record, data_node)
|
||||
return self._tag_record(record)
|
||||
|
||||
def id_get(self, id_str, raise_if_not_found=True):
|
||||
if id_str in self.idref:
|
||||
@@ -736,36 +657,37 @@ form: module.record_id""" % (xml_id,)
|
||||
id_str = '%s.%s' % (self.module, id_str)
|
||||
return self.env['ir.model.data'].xmlid_to_res_model_res_id(id_str, raise_if_not_found=raise_if_not_found)
|
||||
|
||||
def parse(self, de, mode=None):
|
||||
roots = ['openerp','data','odoo']
|
||||
if de.tag not in roots:
|
||||
raise Exception("Root xml tag must be <openerp>, <odoo> or <data>.")
|
||||
for rec in de:
|
||||
if rec.tag in roots:
|
||||
self.parse(rec, mode)
|
||||
elif rec.tag in self._tags:
|
||||
try:
|
||||
self._tags[rec.tag](rec, de, mode=mode)
|
||||
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]
|
||||
)
|
||||
return True
|
||||
def _tag_root(self, el):
|
||||
for rec in el:
|
||||
f = self._tags.get(rec.tag)
|
||||
if f is None:
|
||||
continue
|
||||
|
||||
self.envs.append(self.get_env(el))
|
||||
self._noupdate.append(nodeattr2bool(el, 'noupdate', self.noupdate))
|
||||
try:
|
||||
f(rec)
|
||||
finally:
|
||||
self._noupdate.pop()
|
||||
self.envs.pop()
|
||||
|
||||
@property
|
||||
def env(self):
|
||||
return self.envs[-1]
|
||||
|
||||
@property
|
||||
def noupdate(self):
|
||||
return self._noupdate[-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.cr = cr
|
||||
self.uid = SUPERUSER_ID
|
||||
self.idref = idref
|
||||
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()
|
||||
self.assertion_report = report
|
||||
self.noupdate = noupdate
|
||||
self._noupdate = [noupdate]
|
||||
self.xml_filename = xml_filename
|
||||
self._tags = {
|
||||
'record': self._tag_record,
|
||||
@@ -775,9 +697,26 @@ form: module.record_id""" % (xml_id,)
|
||||
'template': self._tag_template,
|
||||
'report': self._tag_report,
|
||||
'act_window': self._tag_act_window,
|
||||
'assert': self._tag_assert,
|
||||
|
||||
**dict.fromkeys(self.DATA_ROOTS, self._tag_root)
|
||||
}
|
||||
|
||||
def parse(self, de):
|
||||
assert de.tag in self.DATA_ROOTS, "Root xml tag must be <openerp>, <odoo> or <data>."
|
||||
try:
|
||||
self._tag_root(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):
|
||||
if pathname is None:
|
||||
pathname = os.path.join(module, filename)
|
||||
@@ -843,12 +782,9 @@ def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=Fa
|
||||
_logger.info(ustr(relaxng.error_log.last_error))
|
||||
raise
|
||||
|
||||
if idref is None:
|
||||
idref={}
|
||||
if isinstance(xmlfile, pycompat.string_types):
|
||||
xml_filename = xmlfile
|
||||
else:
|
||||
xml_filename = xmlfile.name
|
||||
obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate, xml_filename=xml_filename)
|
||||
obj.parse(doc.getroot(), mode=mode)
|
||||
return True
|
||||
obj.parse(doc.getroot())
|
||||
|
||||
Reference in New Issue
Block a user