convert.py: use the get_object_reference() API for ir.model.data
[odoo/odoo.git] / bin / tools / convert.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import cStringIO
23 import csv
24 import logging
25 import os.path
26 import pickle
27 import re
28
29 # for eval context:
30 import time
31 import release
32 try:
33     import pytz
34 except:
35     logging.getLogger("init").warning('could not find pytz library, please install it')
36     class pytzclass(object):
37         all_timezones=[]
38     pytz=pytzclass()
39
40
41 from datetime import datetime, timedelta
42 from lxml import etree
43 import misc
44 import netsvc
45 import osv
46 import pooler
47 from config import config
48 from yaml_import import convert_yaml_import
49
50 # Import of XML records requires the unsafe eval as well,
51 # almost everywhere, which is ok because it supposedly comes
52 # from trusted data, but at least we make it obvious now.
53 unsafe_eval = eval
54 from tools.safe_eval import safe_eval as eval
55
56 class ConvertError(Exception):
57     def __init__(self, doc, orig_excpt):
58         self.d = doc
59         self.orig = orig_excpt
60
61     def __str__(self):
62         return 'Exception:\n\t%s\nUsing file:\n%s' % (self.orig, self.d)
63
64 def _ref(self, cr):
65     return lambda x: self.id_get(cr, False, x)
66
67 def _obj(pool, cr, uid, model_str, context=None):
68     model = pool.get(model_str)
69     return lambda x: model.browse(cr, uid, x, context=context)
70
71 def _fix_multiple_roots(node):
72     """
73     Surround the children of the ``node`` element of an XML field with a
74     single root "data" element, to prevent having a document with multiple
75     roots once parsed separately.
76
77     XML nodes should have one root only, but we'd like to support
78     direct multiple roots in our partial documents (like inherited view architectures).
79     As a convention we'll surround multiple root with a container "data" element, to be
80     ignored later when parsing.
81     """
82
83     if len(node) > 1:
84         data_node = etree.Element("data")
85         for child in node:
86             data_node.append(child)
87         node.append(data_node)
88
89 def _eval_xml(self, node, pool, cr, uid, idref, context=None):
90     if context is None:
91         context = {}
92     if node.tag in ('field','value'):
93             t = node.get('type','char')
94             f_model = node.get('model', '').encode('utf-8')
95             if node.get('search'):
96                 f_search = node.get("search",'').encode('utf-8')
97                 f_use = node.get("use",'id').encode('utf-8')
98                 f_name = node.get("name",'').encode('utf-8')
99                 q = unsafe_eval(f_search, idref)
100                 ids = pool.get(f_model).search(cr, uid, q)
101                 if f_use != 'id':
102                     ids = map(lambda x: x[f_use], pool.get(f_model).read(cr, uid, ids, [f_use]))
103                 _cols = pool.get(f_model)._columns
104                 if (f_name in _cols) and _cols[f_name]._type=='many2many':
105                     return ids
106                 f_val = False
107                 if len(ids):
108                     f_val = ids[0]
109                     if isinstance(f_val, tuple):
110                         f_val = f_val[0]
111                 return f_val
112             a_eval = node.get('eval','')
113             if a_eval:
114                 idref2 = dict(idref,
115                               time=time,
116                               DateTime=datetime,
117                               timedelta=timedelta,
118                               version=release.major_version,
119                               ref=lambda x: self.id_get(cr, False, x),
120                               pytz=pytz)
121                 if len(f_model):
122                     idref2['obj'] = _obj(self.pool, cr, uid, f_model, context=context)
123                 try:
124                     return unsafe_eval(a_eval, idref2)
125                 except Exception:
126                     logger = logging.getLogger('init')
127                     logger.warning('could not eval(%s) for %s in %s' % (a_eval, node.get('name'), context), exc_info=True)
128                     return ""
129             if t == 'xml':
130                 def _process(s, idref):
131                     m = re.findall('[^%]%\((.*?)\)[ds]', s)
132                     for id in m:
133                         if not id in idref:
134                             idref[id]=self.id_get(cr, False, id)
135                     return s % idref
136                 _fix_multiple_roots(node)
137                 return '<?xml version="1.0"?>\n'\
138                     +_process("".join([etree.tostring(n, encoding='utf-8')
139                                        for n in node]),
140                               idref)
141             if t in ('char', 'int', 'float'):
142                 d = node.text
143                 if t == 'int':
144                     d = d.strip()
145                     if d == 'None':
146                         return None
147                     else:
148                         return int(d.strip())
149                 elif t == 'float':
150                     return float(d.strip())
151                 return d
152             elif t in ('list','tuple'):
153                 res=[]
154                 for n in node.findall('./value'):
155                     res.append(_eval_xml(self,n,pool,cr,uid,idref))
156                 if t=='tuple':
157                     return tuple(res)
158                 return res
159     elif node.tag == "getitem":
160         for n in node:
161             res=_eval_xml(self,n,pool,cr,uid,idref)
162         if not res:
163             raise LookupError
164         elif node.get('type') in ("int", "list"):
165             return res[int(node.get('index'))]
166         else:
167             return res[node.get('index','').encode("utf8")]
168     elif node.tag == "function":
169         args = []
170         a_eval = node.get('eval','')
171         if a_eval:
172             idref['ref'] = lambda x: self.id_get(cr, False, x)
173             args = unsafe_eval(a_eval, idref)
174         for n in node:
175             return_val = _eval_xml(self,n, pool, cr, uid, idref, context)
176             if return_val is not None:
177                 args.append(return_val)
178         model = pool.get(node.get('model',''))
179         method = node.get('name','')
180         res = getattr(model, method)(cr, uid, *args)
181         return res
182     elif node.tag == "test":
183         return node.text
184
185 escape_re = re.compile(r'(?<!\\)/')
186 def escape(x):
187     return x.replace('\\/', '/')
188
189 class assertion_report(object):
190     def __init__(self):
191         self._report = {}
192
193     def record_assertion(self, success, severity):
194         """
195             Records the result of an assertion for the failed/success count
196             returns success
197         """
198         if severity in self._report:
199             self._report[severity][success] += 1
200         else:
201             self._report[severity] = {success:1, not success: 0}
202         return success
203
204     def get_report(self):
205         return self._report
206
207     def __str__(self):
208         res = '\nAssertions report:\nLevel\tsuccess\tfailed\n'
209         success = failed = 0
210         for sev in self._report:
211             res += sev + '\t' + str(self._report[sev][True]) + '\t' + str(self._report[sev][False]) + '\n'
212             success += self._report[sev][True]
213             failed += self._report[sev][False]
214         res += 'total\t' + str(success) + '\t' + str(failed) + '\n'
215         res += 'end of report (' + str(success + failed) + ' assertion(s) checked)'
216         return res
217
218 class xml_import(object):
219     __logger = logging.getLogger('tools.convert.xml_import')
220     @staticmethod
221     def nodeattr2bool(node, attr, default=False):
222         if not node.get(attr):
223             return default
224         val = node.get(attr).strip()
225         if not val:
226             return default
227         return val.lower() not in ('0', 'false', 'off')
228
229     def isnoupdate(self, data_node=None):
230         return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False))
231
232     def get_context(self, data_node, node, eval_dict):
233         data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
234         node_context = node.get("context",'').encode('utf8')
235         context = {}
236         for ctx in (data_node_context, node_context):
237             if ctx:
238                 try:
239                     ctx_res = unsafe_eval(ctx, eval_dict)
240                     if isinstance(context, dict):
241                         context.update(ctx_res)
242                     else:
243                         context = ctx_res
244                 except NameError:
245                     # Some contexts contain references that are only valid at runtime at
246                     # client-side, so in that case we keep the original context string
247                     # as it is. We also log it, just in case.
248                     context = ctx
249                     logging.getLogger("init").debug('Context value (%s) for element with id "%s" or its data node does not parse '\
250                                                     'at server-side, keeping original string, in case it\'s meant for client side only',
251                                                     ctx, node.get('id','n/a'), exc_info=True)
252         return context
253
254     def get_uid(self, cr, uid, data_node, node):
255         node_uid = node.get('uid','') or (len(data_node) and data_node.get('uid',''))
256         if node_uid:
257             return self.id_get(cr, None, node_uid)
258         return uid
259
260     def _test_xml_id(self, xml_id):
261         id = xml_id
262         if '.' in xml_id:
263             module, id = xml_id.split('.', 1)
264             assert '.' not in id, """The ID reference "%s" must contain
265 maximum one dot. They are used to refer to other modules ID, in the
266 form: module.record_id""" % (xml_id,)
267             if module != self.module:
268                 modcnt = self.pool.get('ir.module.module').search_count(self.cr, self.uid, ['&', ('name', '=', module), ('state', 'in', ['installed'])])
269                 assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
270
271         if len(id) > 64:
272             self.logger.error('id: %s is to long (max: 64)', id)
273
274     def _tag_delete(self, cr, rec, data_node=None):
275         d_model = rec.get("model",'')
276         d_search = rec.get("search",'')
277         d_id = rec.get("id",'')
278         ids = []
279         if d_search:
280             ids = self.pool.get(d_model).search(cr, self.uid, unsafe_eval(d_search))
281         if d_id:
282             try:
283                 ids.append(self.id_get(cr, d_model, d_id))
284             except:
285                 # d_id cannot be found. doesn't matter in this case
286                 pass
287         if ids:
288             self.pool.get(d_model).unlink(cr, self.uid, ids)
289             self.pool.get('ir.model.data')._unlink(cr, self.uid, d_model, ids)
290
291     def _remove_ir_values(self, cr, name, value, model):
292         ir_value_ids = self.pool.get('ir.values').search(cr, self.uid, [('name','=',name),('value','=',value),('model','=',model)])
293         if ir_value_ids:
294             self.pool.get('ir.values').unlink(cr, self.uid, ir_value_ids)
295             self.pool.get('ir.model.data')._unlink(cr, self.uid, 'ir.values', ir_value_ids)
296
297         return True
298
299     def _tag_report(self, cr, rec, data_node=None):
300         res = {}
301         for dest,f in (('name','string'),('model','model'),('report_name','name')):
302             res[dest] = rec.get(f,'').encode('utf8')
303             assert res[dest], "Attribute %s of report is empty !" % (f,)
304         for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
305             if rec.get(field):
306                 res[dest] = rec.get(field).encode('utf8')
307         if rec.get('auto'):
308             res['auto'] = eval(rec.get('auto','False'))
309         if rec.get('sxw'):
310             sxw_content = misc.file_open(rec.get('sxw')).read()
311             res['report_sxw_content'] = sxw_content
312         if rec.get('header'):
313             res['header'] = eval(rec.get('header','False'))
314         if rec.get('report_type'):
315             res['report_type'] = rec.get('report_type')
316
317         res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
318
319         xml_id = rec.get('id','').encode('utf8')
320         self._test_xml_id(xml_id)
321
322         if rec.get('groups'):
323             g_names = rec.get('groups','').split(',')
324             groups_value = []
325             for group in g_names:
326                 if group.startswith('-'):
327                     group_id = self.id_get(cr, 'res.groups', group[1:])
328                     groups_value.append((3, group_id))
329                 else:
330                     group_id = self.id_get(cr, 'res.groups', group)
331                     groups_value.append((4, group_id))
332             res['groups_id'] = groups_value
333
334         id = self.pool.get('ir.model.data')._update(cr, self.uid, "ir.actions.report.xml", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
335         self.idref[xml_id] = int(id)
336
337         if not rec.get('menu') or eval(rec.get('menu','False')):
338             keyword = str(rec.get('keyword', 'client_print_multi'))
339             value = 'ir.actions.report.xml,'+str(id)
340             replace = rec.get('replace', True)
341             self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, res['name'], [res['model']], value, replace=replace, isobject=True, xml_id=xml_id)
342         elif self.mode=='update' and eval(rec.get('menu','False'))==False:
343             # Special check for report having attribute menu=False on update
344             value = 'ir.actions.report.xml,'+str(id)
345             self._remove_ir_values(cr, res['name'], value, res['model'])
346         return False
347
348     def _tag_function(self, cr, rec, data_node=None):
349         if self.isnoupdate(data_node) and self.mode != 'init':
350             return
351         context = self.get_context(data_node, rec, {'ref': _ref(self, cr)})
352         uid = self.get_uid(cr, self.uid, data_node, rec)
353         _eval_xml(self,rec, self.pool, cr, uid, self.idref, context=context)
354         return
355
356     def _tag_wizard(self, cr, rec, data_node=None):
357         string = rec.get("string",'').encode('utf8')
358         model = rec.get("model",'').encode('utf8')
359         name = rec.get("name",'').encode('utf8')
360         xml_id = rec.get('id','').encode('utf8')
361         self._test_xml_id(xml_id)
362         multi = rec.get('multi','') and eval(rec.get('multi','False'))
363         res = {'name': string, 'wiz_name': name, 'multi': multi, 'model': model}
364
365         if rec.get('groups'):
366             g_names = rec.get('groups','').split(',')
367             groups_value = []
368             for group in g_names:
369                 if group.startswith('-'):
370                     group_id = self.id_get(cr, 'res.groups', group[1:])
371                     groups_value.append((3, group_id))
372                 else:
373                     group_id = self.id_get(cr, 'res.groups', group)
374                     groups_value.append((4, group_id))
375             res['groups_id'] = groups_value
376
377         id = self.pool.get('ir.model.data')._update(cr, self.uid, "ir.actions.wizard", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
378         self.idref[xml_id] = int(id)
379         # ir_set
380         if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
381             keyword = str(rec.get('keyword','') or 'client_action_multi')
382             value = 'ir.actions.wizard,'+str(id)
383             replace = rec.get("replace",'') or True
384             self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, string, [model], value, replace=replace, isobject=True, xml_id=xml_id)
385         elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
386             # Special check for wizard having attribute menu=False on update
387             value = 'ir.actions.wizard,'+str(id)
388             self._remove_ir_values(cr, string, value, model)
389
390     def _tag_url(self, cr, rec, data_node=None):
391         url = rec.get("string",'').encode('utf8')
392         target = rec.get("target",'').encode('utf8')
393         name = rec.get("name",'').encode('utf8')
394         xml_id = rec.get('id','').encode('utf8')
395         self._test_xml_id(xml_id)
396
397         res = {'name': name, 'url': url, 'target':target}
398
399         id = self.pool.get('ir.model.data')._update(cr, self.uid, "ir.actions.url", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
400         self.idref[xml_id] = int(id)
401         # ir_set
402         if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
403             keyword = str(rec.get('keyword','') or 'client_action_multi')
404             value = 'ir.actions.url,'+str(id)
405             replace = rec.get("replace",'') or True
406             self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, url, ["ir.actions.url"], value, replace=replace, isobject=True, xml_id=xml_id)
407         elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
408             # Special check for URL having attribute menu=False on update
409             value = 'ir.actions.url,'+str(id)
410             self._remove_ir_values(cr, url, value, "ir.actions.url")
411
412     def _tag_act_window(self, cr, rec, data_node=None):
413         name = rec.get('name','').encode('utf-8')
414         xml_id = rec.get('id','').encode('utf8')
415         self._test_xml_id(xml_id)
416         type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
417         view_id = False
418         if rec.get('view'):
419             view_id = self.id_get(cr, 'ir.actions.act_window', rec.get('view','').encode('utf-8'))
420         domain = rec.get('domain','').encode('utf-8') or '{}'
421         res_model = rec.get('res_model','').encode('utf-8')
422         src_model = rec.get('src_model','').encode('utf-8')
423         view_type = rec.get('view_type','').encode('utf-8') or 'form'
424         view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
425         usage = rec.get('usage','').encode('utf-8')
426         limit = rec.get('limit','').encode('utf-8')
427         auto_refresh = rec.get('auto_refresh','').encode('utf-8')
428         uid = self.uid
429         active_id = str("active_id") # for further reference in client/bin/tools/__init__.py
430         def ref(str_id):
431             return self.id_get(cr, None, str_id)
432
433         # Include all locals() in eval_context, for backwards compatibility
434         eval_context = {
435             'name': name,
436             'xml_id': xml_id,
437             'type': type,
438             'view_id': view_id,
439             'domain': domain,
440             'res_model': res_model,
441             'src_model': src_model,
442             'view_type': view_type,
443             'view_mode': view_mode,
444             'usage': usage,
445             'limit': limit,
446             'auto_refresh': auto_refresh,
447             'uid' : uid,
448             'active_id': active_id,
449             'ref' : ref,
450         }
451         context = self.get_context(data_node, rec, eval_context)
452
453         try:
454             domain = unsafe_eval(domain, eval_context)
455         except NameError:
456             # Some domains contain references that are only valid at runtime at
457             # client-side, so in that case we keep the original domain string
458             # as it is. We also log it, just in case.
459             logging.getLogger("init").debug('Domain value (%s) for element with id "%s" does not parse '\
460                                             'at server-side, keeping original string, in case it\'s meant for client side only',
461                                             domain, xml_id or 'n/a', exc_info=True)
462         res = {
463             'name': name,
464             'type': type,
465             'view_id': view_id,
466             'domain': domain,
467             'context': context,
468             'res_model': res_model,
469             'src_model': src_model,
470             'view_type': view_type,
471             'view_mode': view_mode,
472             'usage': usage,
473             'limit': limit,
474             'auto_refresh': auto_refresh,
475         }
476
477         if rec.get('groups'):
478             g_names = rec.get('groups','').split(',')
479             groups_value = []
480             for group in g_names:
481                 if group.startswith('-'):
482                     group_id = self.id_get(cr, 'res.groups', group[1:])
483                     groups_value.append((3, group_id))
484                 else:
485                     group_id = self.id_get(cr, 'res.groups', group)
486                     groups_value.append((4, group_id))
487             res['groups_id'] = groups_value
488
489         if rec.get('target'):
490             res['target'] = rec.get('target','')
491         if rec.get('multi'):
492             res['multi'] = rec.get('multi', False)
493         id = self.pool.get('ir.model.data')._update(cr, self.uid, 'ir.actions.act_window', self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
494         self.idref[xml_id] = int(id)
495
496         if src_model:
497             #keyword = 'client_action_relate'
498             keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
499             value = 'ir.actions.act_window,'+str(id)
500             replace = rec.get('replace','') or True
501             self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, xml_id, [src_model], value, replace=replace, isobject=True, xml_id=xml_id)
502         # TODO add remove ir.model.data
503
504     def _tag_ir_set(self, cr, rec, data_node=None):
505         if self.mode != 'init':
506             return
507         res = {}
508         for field in rec.findall('./field'):
509             f_name = field.get("name",'').encode('utf-8')
510             f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
511             res[f_name] = f_val
512         self.pool.get('ir.model.data').ir_set(cr, self.uid, res['key'], res['key2'], res['name'], res['models'], res['value'], replace=res.get('replace',True), isobject=res.get('isobject', False), meta=res.get('meta',None))
513
514     def _tag_workflow(self, cr, rec, data_node=None):
515         if self.isnoupdate(data_node) and self.mode != 'init':
516             return
517         model = str(rec.get('model',''))
518         w_ref = rec.get('ref','')
519         if w_ref:
520             id = self.id_get(cr, model, w_ref)
521         else:
522             number_children = len(rec)
523             assert number_children > 0,\
524                 'You must define a child node if you dont give a ref'
525             assert number_children == 1,\
526                 'Only one child node is accepted (%d given)' % number_children
527             id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
528
529         uid = self.get_uid(cr, self.uid, data_node, rec)
530         wf_service = netsvc.LocalService("workflow")
531         wf_service.trg_validate(uid, model,
532             id,
533             str(rec.get('action','')), cr)
534
535     #
536     # Support two types of notation:
537     #   name="Inventory Control/Sending Goods"
538     # or
539     #   action="action_id"
540     #   parent="parent_id"
541     #
542     def _tag_menuitem(self, cr, rec, data_node=None):
543         rec_id = rec.get("id",'').encode('ascii')
544         self._test_xml_id(rec_id)
545         m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
546
547         values = {'parent_id': False}
548         if rec.get('parent', False) is False and len(m_l) > 1:
549             # No parent attribute specified and the menu name has several menu components, 
550             # try to determine the ID of the parent according to menu path
551             pid = False
552             res = None
553             values['name'] = m_l[-1]
554             m_l = m_l[:-1] # last part is our name, not a parent
555             for idx, menu_elem in enumerate(m_l):
556                 if pid:
557                     cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
558                 else:
559                     cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
560                 res = cr.fetchone()
561                 if res:
562                     pid = res[0]
563                 else:
564                     # the menuitem does't exist but we are in branch (not a leaf)
565                     self.logger.warning('Warning no ID for submenu %s of menu %s !', menu_elem, str(m_l))
566                     pid = self.pool.get('ir.ui.menu').create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
567             values['parent_id'] = pid
568         else:
569             # The parent attribute was specified, if non-empty determine its ID, otherwise
570             # explicitly make a top-level menu
571             if rec.get('parent'):
572                 menu_parent_id = self.id_get(cr, 'ir.ui.menu', rec.get('parent',''))
573             else:
574                 # we get here with <menuitem parent="">, explicit clear of parent, or
575                 # if no parent attribute at all but menu name is not a menu path
576                 menu_parent_id = False
577             values = {'parent_id': menu_parent_id}
578             if rec.get('name'):
579                 values['name'] = rec.get('name')
580             try:
581                 res = [ self.id_get(cr, 'ir.ui.menu', rec.get('id','')) ]
582             except:
583                 res = None
584
585         if rec.get('action'):
586             a_action = rec.get('action','').encode('utf8')
587             a_type = rec.get('type','').encode('utf8') or 'act_window'
588             icons = {
589                 "act_window": 'STOCK_NEW',
590                 "report.xml": 'STOCK_PASTE',
591                 "wizard": 'STOCK_EXECUTE',
592                 "url": 'STOCK_JUMP_TO'
593             }
594             values['icon'] = icons.get(a_type,'STOCK_NEW')
595             if a_type=='act_window':
596                 a_id = self.id_get(cr, 'ir.actions.%s'% a_type, a_action)
597                 cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
598                 rrres = cr.fetchone()
599                 assert rrres, "No window action defined for this id %s !\n" \
600                     "Verify that this is a window action or add a type argument." % (a_action,)
601                 action_type,action_mode,action_name,view_id,target = rrres
602                 if view_id:
603                     cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (int(view_id),))
604                     action_mode, = cr.fetchone()
605                 cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
606                 if cr.rowcount:
607                     action_mode, = cr.fetchone()
608                 if action_type=='tree':
609                     values['icon'] = 'STOCK_INDENT'
610                 elif action_mode and action_mode.startswith('tree'):
611                     values['icon'] = 'STOCK_JUSTIFY_FILL'
612                 elif action_mode and action_mode.startswith('graph'):
613                     values['icon'] = 'terp-graph'
614                 elif action_mode and action_mode.startswith('calendar'):
615                     values['icon'] = 'terp-calendar'
616                 if target=='new':
617                     values['icon'] = 'STOCK_EXECUTE'
618                 if not values.get('name', False):
619                     values['name'] = action_name
620             elif a_type=='wizard':
621                 a_id = self.id_get(cr, 'ir.actions.%s'% a_type, a_action)
622                 cr.execute('select name from ir_act_wizard where id=%s', (int(a_id),))
623                 resw = cr.fetchone()
624                 if (not values.get('name', False)) and resw:
625                     values['name'] = resw[0]
626         if rec.get('sequence'):
627             values['sequence'] = int(rec.get('sequence'))
628         if rec.get('icon'):
629             values['icon'] = str(rec.get('icon'))
630
631         if rec.get('groups'):
632             g_names = rec.get('groups','').split(',')
633             groups_value = []
634             for group in g_names:
635                 if group.startswith('-'):
636                     group_id = self.id_get(cr, 'res.groups', group[1:])
637                     groups_value.append((3, group_id))
638                 else:
639                     group_id = self.id_get(cr, 'res.groups', group)
640                     groups_value.append((4, group_id))
641             values['groups_id'] = groups_value
642
643         xml_id = rec.get('id','').encode('utf8')
644         self._test_xml_id(xml_id)
645         pid = self.pool.get('ir.model.data')._update(cr, self.uid, 'ir.ui.menu', self.module, values, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode, res_id=res and res[0] or False)
646
647         if rec_id and pid:
648             self.idref[rec_id] = int(pid)
649
650         if rec.get('action') and pid:
651             a_action = rec.get('action').encode('utf8')
652             a_type = rec.get('type','').encode('utf8') or 'act_window'
653             a_id = self.id_get(cr, 'ir.actions.%s' % a_type, a_action)
654             action = "ir.actions.%s,%d" % (a_type, a_id)
655             self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(pid))], action, True, True, xml_id=rec_id)
656         return ('ir.ui.menu', pid)
657
658     def _assert_equals(self, f1, f2, prec=4):
659         return not round(f1 - f2, prec)
660
661     def _tag_assert(self, cr, rec, data_node=None):
662         if self.isnoupdate(data_node) and self.mode != 'init':
663             return
664
665         rec_model = rec.get("model",'').encode('ascii')
666         model = self.pool.get(rec_model)
667         assert model, "The model %s does not exist !" % (rec_model,)
668         rec_id = rec.get("id",'').encode('ascii')
669         self._test_xml_id(rec_id)
670         rec_src = rec.get("search",'').encode('utf8')
671         rec_src_count = rec.get("count")
672
673         severity = rec.get("severity",'').encode('ascii') or netsvc.LOG_ERROR
674         rec_string = rec.get("string",'').encode('utf8') or 'unknown'
675
676         ids = None
677         eval_dict = {'ref': _ref(self, cr)}
678         context = self.get_context(data_node, rec, eval_dict)
679         uid = self.get_uid(cr, self.uid, data_node, rec)
680         if rec_id:
681             ids = [self.id_get(cr, rec_model, rec_id)]
682         elif rec_src:
683             q = unsafe_eval(rec_src, eval_dict)
684             ids = self.pool.get(rec_model).search(cr, uid, q, context=context)
685             if rec_src_count:
686                 count = int(rec_src_count)
687                 if len(ids) != count:
688                     self.assert_report.record_assertion(False, severity)
689                     msg = 'assertion "%s" failed!\n'    \
690                           ' Incorrect search count:\n'  \
691                           ' expected count: %d\n'       \
692                           ' obtained count: %d\n'       \
693                           % (rec_string, count, len(ids))
694                     self.logger.log(severity, msg)
695                     sevval = getattr(logging, severity.upper())
696                     if sevval >= config['assert_exit_level']:
697                         # TODO: define a dedicated exception
698                         raise Exception('Severe assertion failure')
699                     return
700
701         assert ids is not None,\
702             'You must give either an id or a search criteria'
703         ref = _ref(self, cr)
704         for id in ids:
705             brrec =  model.browse(cr, uid, id, context)
706             class d(dict):
707                 def __getitem__(self2, key):
708                     if key in brrec:
709                         return brrec[key]
710                     return dict.__getitem__(self2, key)
711             globals_dict = d()
712             globals_dict['floatEqual'] = self._assert_equals
713             globals_dict['ref'] = ref
714             globals_dict['_ref'] = ref
715             for test in rec.findall('./test'):
716                 f_expr = test.get("expr",'').encode('utf-8')
717                 expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
718                 expression_value = unsafe_eval(f_expr, globals_dict)
719                 if expression_value != expected_value: # assertion failed
720                     self.assert_report.record_assertion(False, severity)
721                     msg = 'assertion "%s" failed!\n'    \
722                           ' xmltag: %s\n'               \
723                           ' expected value: %r\n'       \
724                           ' obtained value: %r\n'       \
725                           % (rec_string, etree.tostring(test), expected_value, expression_value)
726                     self.logger.log(severity, msg)
727                     sevval = getattr(logging, severity.upper())
728                     if sevval >= config['assert_exit_level']:
729                         # TODO: define a dedicated exception
730                         raise Exception('Severe assertion failure')
731                     return
732         else: # all tests were successful for this assertion tag (no break)
733             self.assert_report.record_assertion(True, severity)
734
735     def _tag_record(self, cr, rec, data_node=None):
736         rec_model = rec.get("model").encode('ascii')
737         model = self.pool.get(rec_model)
738         assert model, "The model %s does not exist !" % (rec_model,)
739         rec_id = rec.get("id",'').encode('ascii')
740         rec_context = rec.get("context", None)
741         if rec_context:
742             rec_context = unsafe_eval(rec_context)
743         self._test_xml_id(rec_id)
744         if self.isnoupdate(data_node) and self.mode != 'init':
745             # check if the xml record has an id string
746             if rec_id:
747                 if '.' in rec_id:
748                     module,rec_id2 = rec_id.split('.')
749                 else:
750                     module = self.module
751                     rec_id2 = rec_id
752                 id = self.pool.get('ir.model.data')._update_dummy(cr, self.uid, rec_model, module, rec_id2)
753                 # check if the resource already existed at the last update
754                 if id:
755                     # if it existed, we don't update the data, but we need to
756                     # know the id of the existing record anyway
757                     self.idref[rec_id] = int(id)
758                     return None
759                 else:
760                     # if the resource didn't exist
761                     if not self.nodeattr2bool(rec, 'forcecreate', True):
762                         # we don't want to create it, so we skip it
763                         return None
764                     # else, we let the record to be created
765
766             else:
767                 # otherwise it is skipped
768                 return None
769         res = {}
770         for field in rec.findall('./field'):
771 #TODO: most of this code is duplicated above (in _eval_xml)...
772             f_name = field.get("name",'').encode('utf-8')
773             f_ref = field.get("ref",'').encode('utf-8')
774             f_search = field.get("search",'').encode('utf-8')
775             f_model = field.get("model",'').encode('utf-8')
776             if not f_model and model._columns.get(f_name,False):
777                 f_model = model._columns[f_name]._obj
778             f_use = field.get("use",'').encode('utf-8') or 'id'
779             f_val = False
780
781             if f_search:
782                 q = unsafe_eval(f_search, self.idref)
783                 field = []
784                 assert f_model, 'Define an attribute model="..." in your .XML file !'
785                 f_obj = self.pool.get(f_model)
786                 # browse the objects searched
787                 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
788                 # column definitions of the "local" object
789                 _cols = self.pool.get(rec_model)._columns
790                 # if the current field is many2many
791                 if (f_name in _cols) and _cols[f_name]._type=='many2many':
792                     f_val = [(6, 0, map(lambda x: x[f_use], s))]
793                 elif len(s):
794                     # otherwise (we are probably in a many2one field),
795                     # take the first element of the search
796                     f_val = s[0][f_use]
797             elif f_ref:
798                 if f_ref=="null":
799                     f_val = False
800                 else:
801                     if f_name in model._columns \
802                               and model._columns[f_name]._type == 'reference':
803                         val = self.model_id_get(cr, f_model, f_ref)
804                         f_val = val[0] + ',' + str(val[1])
805                     else:
806                         f_val = self.id_get(cr, f_model, f_ref)
807             else:
808                 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
809                 if model._columns.has_key(f_name):
810                     if isinstance(model._columns[f_name], osv.fields.integer):
811                         f_val = int(f_val)
812             res[f_name] = f_val
813
814         id = self.pool.get('ir.model.data')._update(cr, self.uid, rec_model, self.module, res, rec_id or False, not self.isnoupdate(data_node), noupdate=self.isnoupdate(data_node), mode=self.mode, context=rec_context )
815         if rec_id:
816             self.idref[rec_id] = int(id)
817         if config.get('import_partial', False):
818             cr.commit()
819         return rec_model, id
820
821     def id_get(self, cr, model, id_str):
822         if id_str in self.idref:
823             return self.idref[id_str]
824         res = self.model_id_get(cr, model, id_str)
825         if res and len(res)>1: res = res[1]
826         return res
827
828     def model_id_get(self, cr, model, id_str):
829         model_data_obj = self.pool.get('ir.model.data')
830         mod = self.module
831         if '.' in id_str:
832             mod,id_str = id_str.split('.')
833         return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
834
835     def parse(self, de):
836         if not de.tag in ['terp', 'openerp']:
837             self.logger.error("Mismatch xml format")
838             raise Exception( "Mismatch xml format: only terp or openerp as root tag" )
839
840         if de.tag == 'terp':
841             self.logger.warning("The tag <terp/> is deprecated, use <openerp/>")
842
843         for n in de.findall('./data'):
844             for rec in n:
845                     if rec.tag in self._tags:
846                         try:
847                             self._tags[rec.tag](self.cr, rec, n)
848                         except:
849                             self.__logger.error('Parse error in %s:%d: \n%s',
850                                                 rec.getroottree().docinfo.URL,
851                                                 rec.sourceline,
852                                                 etree.tostring(rec).strip())
853                             self.cr.rollback()
854                             raise
855         return True
856
857     def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
858
859         self.logger = logging.getLogger('init')
860         self.mode = mode
861         self.module = module
862         self.cr = cr
863         self.idref = idref
864         self.pool = pooler.get_pool(cr.dbname)
865         self.uid = 1
866         if report is None:
867             report = assertion_report()
868         self.assert_report = report
869         self.noupdate = noupdate
870         self._tags = {
871             'menuitem': self._tag_menuitem,
872             'record': self._tag_record,
873             'assert': self._tag_assert,
874             'report': self._tag_report,
875             'wizard': self._tag_wizard,
876             'delete': self._tag_delete,
877             'ir_set': self._tag_ir_set,
878             'function': self._tag_function,
879             'workflow': self._tag_workflow,
880             'act_window': self._tag_act_window,
881             'url': self._tag_url
882         }
883
884 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
885         noupdate=False):
886     '''Import csv file :
887         quote: "
888         delimiter: ,
889         encoding: utf-8'''
890     if not idref:
891         idref={}
892     logger = logging.getLogger('init')
893     model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
894     #remove folder path from model
895     head, model = os.path.split(model)
896
897     pool = pooler.get_pool(cr.dbname)
898
899     input = cStringIO.StringIO(csvcontent) #FIXME
900     reader = csv.reader(input, quotechar='"', delimiter=',')
901     fields = reader.next()
902     fname_partial = ""
903     if config.get('import_partial'):
904         fname_partial = module + '/'+ fname
905         if not os.path.isfile(config.get('import_partial')):
906             pickle.dump({}, file(config.get('import_partial'),'w+'))
907         else:
908             data = pickle.load(file(config.get('import_partial')))
909             if fname_partial in data:
910                 if not data[fname_partial]:
911                     return
912                 else:
913                     for i in range(data[fname_partial]):
914                         reader.next()
915
916     if not (mode == 'init' or 'id' in fields):
917         logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
918         return
919
920     uid = 1
921     datas = []
922     for line in reader:
923         if (not line) or not reduce(lambda x,y: x or y, line) :
924             continue
925         try:
926             datas.append(map(lambda x: misc.ustr(x), line))
927         except:
928             logger.error("Cannot import the line: %s", line)
929     pool.get(model).import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
930     if config.get('import_partial'):
931         data = pickle.load(file(config.get('import_partial')))
932         data[fname_partial] = 0
933         pickle.dump(data, file(config.get('import_partial'),'wb'))
934         cr.commit()
935
936 #
937 # xml import/export
938 #
939 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
940     doc = etree.parse(xmlfile)
941     relaxng = etree.RelaxNG(
942         etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
943     try:
944         relaxng.assert_(doc)
945     except Exception:
946         logger = netsvc.Logger()
947         logger.notifyChannel('init', netsvc.LOG_ERROR, 'The XML file does not fit the required schema !')
948         logger.notifyChannel('init', netsvc.LOG_ERROR, misc.ustr(relaxng.error_log.last_error))
949         raise
950
951     if idref is None:
952         idref={}
953     obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
954     obj.parse(doc.getroot())
955     return True
956