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