[IMP] reports: support generic report_file field instead of report_xml ; add "file...
[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     @staticmethod
220     def nodeattr2bool(node, attr, default=False):
221         if not node.get(attr):
222             return default
223         val = node.get(attr).strip()
224         if not val:
225             return default
226         return val.lower() not in ('0', 'false', 'off')
227
228     def isnoupdate(self, data_node=None):
229         return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False))
230
231     def get_context(self, data_node, node, eval_dict):
232         data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
233         node_context = node.get("context",'').encode('utf8')
234         context = {}
235         for ctx in (data_node_context, node_context):
236             if ctx:
237                 try:
238                     ctx_res = unsafe_eval(ctx, eval_dict)
239                     if isinstance(context, dict):
240                         context.update(ctx_res)
241                     else:
242                         context = ctx_res
243                 except NameError:
244                     # Some contexts contain references that are only valid at runtime at
245                     # client-side, so in that case we keep the original context string
246                     # as it is. We also log it, just in case.
247                     context = ctx
248                     logging.getLogger("init").debug('Context value (%s) for element with id "%s" or its data node does not parse '\
249                                                     'at server-side, keeping original string, in case it\'s meant for client side only',
250                                                     ctx, node.get('id','n/a'), exc_info=True)
251         return context
252
253     def get_uid(self, cr, uid, data_node, node):
254         node_uid = node.get('uid','') or (len(data_node) and data_node.get('uid',''))
255         if node_uid:
256             return self.id_get(cr, None, node_uid)
257         return uid
258
259     def _test_xml_id(self, xml_id):
260         id = xml_id
261         if '.' in xml_id:
262             module, id = xml_id.split('.', 1)
263             assert '.' not in id, """The ID reference "%s" must contain
264 maximum one dot. They are used to refer to other modules ID, in the
265 form: module.record_id""" % (xml_id,)
266             if module != self.module:
267                 modcnt = self.pool.get('ir.module.module').search_count(self.cr, self.uid, ['&', ('name', '=', module), ('state', 'in', ['installed'])])
268                 assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
269
270         if len(id) > 64:
271             self.logger.notifyChannel('init', netsvc.LOG_ERROR, 'id: %s is to long (max: 64)'% (id,))
272
273     def _tag_delete(self, cr, rec, data_node=None):
274         d_model = rec.get("model",'')
275         d_search = rec.get("search",'')
276         d_id = rec.get("id",'')
277         ids = []
278         if d_search:
279             ids = self.pool.get(d_model).search(cr, self.uid, unsafe_eval(d_search))
280         if d_id:
281             try:
282                 ids.append(self.id_get(cr, d_model, d_id))
283             except:
284                 # d_id cannot be found. doesn't matter in this case
285                 pass
286         if ids:
287             self.pool.get(d_model).unlink(cr, self.uid, ids)
288             self.pool.get('ir.model.data')._unlink(cr, self.uid, d_model, ids)
289
290     def _remove_ir_values(self, cr, name, value, model):
291         ir_value_ids = self.pool.get('ir.values').search(cr, self.uid, [('name','=',name),('value','=',value),('model','=',model)])
292         if ir_value_ids:
293             self.pool.get('ir.values').unlink(cr, self.uid, ir_value_ids)
294             self.pool.get('ir.model.data')._unlink(cr, self.uid, 'ir.values', ir_value_ids)
295
296         return True
297
298     def _tag_report(self, cr, rec, data_node=None):
299         res = {}
300         for dest,f in (('name','string'),('model','model'),('report_name','name')):
301             res[dest] = rec.get(f,'').encode('utf8')
302             assert res[dest], "Attribute %s of report is empty !" % (f,)
303         for field,dest in (('rml','report_rml'),('file','report_file'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
304             if rec.get(field):
305                 res[dest] = rec.get(field).encode('utf8')
306         if rec.get('auto'):
307             res['auto'] = eval(rec.get('auto','False'))
308         if rec.get('sxw'):
309             sxw_content = misc.file_open(rec.get('sxw')).read()
310             res['report_sxw_content'] = sxw_content
311         if rec.get('header'):
312             res['header'] = eval(rec.get('header','False'))
313         if rec.get('report_type'):
314             res['report_type'] = rec.get('report_type')
315
316         res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
317
318         xml_id = rec.get('id','').encode('utf8')
319         self._test_xml_id(xml_id)
320
321         if rec.get('groups'):
322             g_names = rec.get('groups','').split(',')
323             groups_value = []
324             for group in g_names:
325                 if group.startswith('-'):
326                     group_id = self.id_get(cr, 'res.groups', group[1:])
327                     groups_value.append((3, group_id))
328                 else:
329                     group_id = self.id_get(cr, 'res.groups', group)
330                     groups_value.append((4, group_id))
331             res['groups_id'] = groups_value
332
333         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)
334         self.idref[xml_id] = int(id)
335
336         if not rec.get('menu') or eval(rec.get('menu','False')):
337             keyword = str(rec.get('keyword', 'client_print_multi'))
338             value = 'ir.actions.report.xml,'+str(id)
339             replace = rec.get('replace', True)
340             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)
341         elif self.mode=='update' and eval(rec.get('menu','False'))==False:
342             # Special check for report having attribute menu=False on update
343             value = 'ir.actions.report.xml,'+str(id)
344             self._remove_ir_values(cr, res['name'], value, res['model'])
345         return False
346
347     def _tag_function(self, cr, rec, data_node=None):
348         if self.isnoupdate(data_node) and self.mode != 'init':
349             return
350         context = self.get_context(data_node, rec, {'ref': _ref(self, cr)})
351         uid = self.get_uid(cr, self.uid, data_node, rec)
352         _eval_xml(self,rec, self.pool, cr, uid, self.idref, context=context)
353         return
354
355     def _tag_wizard(self, cr, rec, data_node=None):
356         string = rec.get("string",'').encode('utf8')
357         model = rec.get("model",'').encode('utf8')
358         name = rec.get("name",'').encode('utf8')
359         xml_id = rec.get('id','').encode('utf8')
360         self._test_xml_id(xml_id)
361         multi = rec.get('multi','') and eval(rec.get('multi','False'))
362         res = {'name': string, 'wiz_name': name, 'multi': multi, 'model': model}
363
364         if rec.get('groups'):
365             g_names = rec.get('groups','').split(',')
366             groups_value = []
367             for group in g_names:
368                 if group.startswith('-'):
369                     group_id = self.id_get(cr, 'res.groups', group[1:])
370                     groups_value.append((3, group_id))
371                 else:
372                     group_id = self.id_get(cr, 'res.groups', group)
373                     groups_value.append((4, group_id))
374             res['groups_id'] = groups_value
375
376         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)
377         self.idref[xml_id] = int(id)
378         # ir_set
379         if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
380             keyword = str(rec.get('keyword','') or 'client_action_multi')
381             value = 'ir.actions.wizard,'+str(id)
382             replace = rec.get("replace",'') or True
383             self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, string, [model], value, replace=replace, isobject=True, xml_id=xml_id)
384         elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
385             # Special check for wizard having attribute menu=False on update
386             value = 'ir.actions.wizard,'+str(id)
387             self._remove_ir_values(cr, string, value, model)
388
389     def _tag_url(self, cr, rec, data_node=None):
390         url = rec.get("string",'').encode('utf8')
391         target = rec.get("target",'').encode('utf8')
392         name = rec.get("name",'').encode('utf8')
393         xml_id = rec.get('id','').encode('utf8')
394         self._test_xml_id(xml_id)
395
396         res = {'name': name, 'url': url, 'target':target}
397
398         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)
399         self.idref[xml_id] = int(id)
400         # ir_set
401         if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
402             keyword = str(rec.get('keyword','') or 'client_action_multi')
403             value = 'ir.actions.url,'+str(id)
404             replace = rec.get("replace",'') or True
405             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)
406         elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
407             # Special check for URL having attribute menu=False on update
408             value = 'ir.actions.url,'+str(id)
409             self._remove_ir_values(cr, url, value, "ir.actions.url")
410
411     def _tag_act_window(self, cr, rec, data_node=None):
412         name = rec.get('name','').encode('utf-8')
413         xml_id = rec.get('id','').encode('utf8')
414         self._test_xml_id(xml_id)
415         type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
416         view_id = False
417         if rec.get('view'):
418             view_id = self.id_get(cr, 'ir.actions.act_window', rec.get('view','').encode('utf-8'))
419         domain = rec.get('domain','').encode('utf-8') or '{}'
420         res_model = rec.get('res_model','').encode('utf-8')
421         src_model = rec.get('src_model','').encode('utf-8')
422         view_type = rec.get('view_type','').encode('utf-8') or 'form'
423         view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
424         usage = rec.get('usage','').encode('utf-8')
425         limit = rec.get('limit','').encode('utf-8')
426         auto_refresh = rec.get('auto_refresh','').encode('utf-8')
427         uid = self.uid
428         active_id = str("active_id") # for further reference in client/bin/tools/__init__.py
429         def ref(str_id):
430             return self.id_get(cr, None, str_id)
431
432         # Include all locals() in eval_context, for backwards compatibility
433         eval_context = {
434             'name': name,
435             'xml_id': xml_id,
436             'type': type,
437             'view_id': view_id,
438             'domain': domain,
439             'res_model': res_model,
440             'src_model': src_model,
441             'view_type': view_type,
442             'view_mode': view_mode,
443             'usage': usage,
444             'limit': limit,
445             'auto_refresh': auto_refresh,
446             'uid' : uid,
447             'active_id': active_id,
448             'ref' : ref,
449         }
450         context = self.get_context(data_node, rec, eval_context)
451
452         try:
453             domain = unsafe_eval(domain, eval_context)
454         except NameError:
455             # Some domains contain references that are only valid at runtime at
456             # client-side, so in that case we keep the original domain string
457             # as it is. We also log it, just in case.
458             logging.getLogger("init").debug('Domain value (%s) for element with id "%s" does not parse '\
459                                             'at server-side, keeping original string, in case it\'s meant for client side only',
460                                             domain, xml_id or 'n/a', exc_info=True)
461         res = {
462             'name': name,
463             'type': type,
464             'view_id': view_id,
465             'domain': domain,
466             'context': context,
467             'res_model': res_model,
468             'src_model': src_model,
469             'view_type': view_type,
470             'view_mode': view_mode,
471             'usage': usage,
472             'limit': limit,
473             'auto_refresh': auto_refresh,
474         }
475
476         if rec.get('groups'):
477             g_names = rec.get('groups','').split(',')
478             groups_value = []
479             for group in g_names:
480                 if group.startswith('-'):
481                     group_id = self.id_get(cr, 'res.groups', group[1:])
482                     groups_value.append((3, group_id))
483                 else:
484                     group_id = self.id_get(cr, 'res.groups', group)
485                     groups_value.append((4, group_id))
486             res['groups_id'] = groups_value
487
488         if rec.get('target'):
489             res['target'] = rec.get('target','')
490         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)
491         self.idref[xml_id] = int(id)
492
493         if src_model:
494             #keyword = 'client_action_relate'
495             keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
496             value = 'ir.actions.act_window,'+str(id)
497             replace = rec.get('replace','') or True
498             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)
499         # TODO add remove ir.model.data
500
501     def _tag_ir_set(self, cr, rec, data_node=None):
502         if self.mode != 'init':
503             return
504         res = {}
505         for field in rec.findall('./field'):
506             f_name = field.get("name",'').encode('utf-8')
507             f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
508             res[f_name] = f_val
509         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))
510
511     def _tag_workflow(self, cr, rec, data_node=None):
512         if self.isnoupdate(data_node) and self.mode != 'init':
513             return
514         model = str(rec.get('model',''))
515         w_ref = rec.get('ref','')
516         if w_ref:
517             id = self.id_get(cr, model, w_ref)
518         else:
519             number_children = len(rec)
520             assert number_children > 0,\
521                 'You must define a child node if you dont give a ref'
522             assert number_children == 1,\
523                 'Only one child node is accepted (%d given)' % number_children
524             id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
525
526         uid = self.get_uid(cr, self.uid, data_node, rec)
527         wf_service = netsvc.LocalService("workflow")
528         wf_service.trg_validate(uid, model,
529             id,
530             str(rec.get('action','')), cr)
531
532     #
533     # Support two types of notation:
534     #   name="Inventory Control/Sending Goods"
535     # or
536     #   action="action_id"
537     #   parent="parent_id"
538     #
539     def _tag_menuitem(self, cr, rec, data_node=None):
540         rec_id = rec.get("id",'').encode('ascii')
541         self._test_xml_id(rec_id)
542         m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
543
544         values = {'parent_id': False}
545         if rec.get('parent', False) is False and len(m_l) > 1:
546             # No parent attribute specified and the menu name has several menu components, 
547             # try to determine the ID of the parent according to menu path
548             pid = False
549             res = None
550             values['name'] = m_l[-1]
551             m_l = m_l[:-1] # last part is our name, not a parent
552             for idx, menu_elem in enumerate(m_l):
553                 if pid:
554                     cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
555                 else:
556                     cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
557                 res = cr.fetchone()
558                 if res:
559                     pid = res[0]
560                 else:
561                     # the menuitem does't exist but we are in branch (not a leaf)
562                     self.logger.notifyChannel("init", netsvc.LOG_WARNING, 'Warning no ID for submenu %s of menu %s !' % (menu_elem, str(m_l)))
563                     pid = self.pool.get('ir.ui.menu').create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
564             values['parent_id'] = pid
565         else:
566             # The parent attribute was specified, if non-empty determine its ID, otherwise
567             # explicitly make a top-level menu
568             if rec.get('parent'):
569                 menu_parent_id = self.id_get(cr, 'ir.ui.menu', rec.get('parent',''))
570             else:
571                 # we get here with <menuitem parent="">, explicit clear of parent, or
572                 # if no parent attribute at all but menu name is not a menu path
573                 menu_parent_id = False
574             values = {'parent_id': menu_parent_id}
575             if rec.get('name'):
576                 values['name'] = rec.get('name')
577             try:
578                 res = [ self.id_get(cr, 'ir.ui.menu', rec.get('id','')) ]
579             except:
580                 res = None
581
582         if rec.get('action'):
583             a_action = rec.get('action','').encode('utf8')
584             a_type = rec.get('type','').encode('utf8') or 'act_window'
585             icons = {
586                 "act_window": 'STOCK_NEW',
587                 "report.xml": 'STOCK_PASTE',
588                 "wizard": 'STOCK_EXECUTE',
589                 "url": 'STOCK_JUMP_TO'
590             }
591             values['icon'] = icons.get(a_type,'STOCK_NEW')
592             if a_type=='act_window':
593                 a_id = self.id_get(cr, 'ir.actions.%s'% a_type, a_action)
594                 cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
595                 rrres = cr.fetchone()
596                 assert rrres, "No window action defined for this id %s !\n" \
597                     "Verify that this is a window action or add a type argument." % (a_action,)
598                 action_type,action_mode,action_name,view_id,target = rrres
599                 if view_id:
600                     cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (int(view_id),))
601                     action_mode, = cr.fetchone()
602                 cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
603                 if cr.rowcount:
604                     action_mode, = cr.fetchone()
605                 if action_type=='tree':
606                     values['icon'] = 'STOCK_INDENT'
607                 elif action_mode and action_mode.startswith('tree'):
608                     values['icon'] = 'STOCK_JUSTIFY_FILL'
609                 elif action_mode and action_mode.startswith('graph'):
610                     values['icon'] = 'terp-graph'
611                 elif action_mode and action_mode.startswith('calendar'):
612                     values['icon'] = 'terp-calendar'
613                 if target=='new':
614                     values['icon'] = 'STOCK_EXECUTE'
615                 if not values.get('name', False):
616                     values['name'] = action_name
617             elif a_type=='wizard':
618                 a_id = self.id_get(cr, 'ir.actions.%s'% a_type, a_action)
619                 cr.execute('select name from ir_act_wizard where id=%s', (int(a_id),))
620                 resw = cr.fetchone()
621                 if (not values.get('name', False)) and resw:
622                     values['name'] = resw[0]
623         if rec.get('sequence'):
624             values['sequence'] = int(rec.get('sequence'))
625         if rec.get('icon'):
626             values['icon'] = str(rec.get('icon'))
627
628         if rec.get('groups'):
629             g_names = rec.get('groups','').split(',')
630             groups_value = []
631             for group in g_names:
632                 if group.startswith('-'):
633                     group_id = self.id_get(cr, 'res.groups', group[1:])
634                     groups_value.append((3, group_id))
635                 else:
636                     group_id = self.id_get(cr, 'res.groups', group)
637                     groups_value.append((4, group_id))
638             values['groups_id'] = groups_value
639
640         xml_id = rec.get('id','').encode('utf8')
641         self._test_xml_id(xml_id)
642         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)
643
644         if rec_id and pid:
645             self.idref[rec_id] = int(pid)
646
647         if rec.get('action') and pid:
648             a_action = rec.get('action').encode('utf8')
649             a_type = rec.get('type','').encode('utf8') or 'act_window'
650             a_id = self.id_get(cr, 'ir.actions.%s' % a_type, a_action)
651             action = "ir.actions.%s,%d" % (a_type, a_id)
652             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)
653         return ('ir.ui.menu', pid)
654
655     def _assert_equals(self, f1, f2, prec = 4):
656         return not round(f1 - f2, prec)
657
658     def _tag_assert(self, cr, rec, data_node=None):
659         if self.isnoupdate(data_node) and self.mode != 'init':
660             return
661
662         rec_model = rec.get("model",'').encode('ascii')
663         model = self.pool.get(rec_model)
664         assert model, "The model %s does not exist !" % (rec_model,)
665         rec_id = rec.get("id",'').encode('ascii')
666         self._test_xml_id(rec_id)
667         rec_src = rec.get("search",'').encode('utf8')
668         rec_src_count = rec.get("count")
669
670         severity = rec.get("severity",'').encode('ascii') or netsvc.LOG_ERROR
671         rec_string = rec.get("string",'').encode('utf8') or 'unknown'
672
673         ids = None
674         eval_dict = {'ref': _ref(self, cr)}
675         context = self.get_context(data_node, rec, eval_dict)
676         uid = self.get_uid(cr, self.uid, data_node, rec)
677         if rec_id:
678             ids = [self.id_get(cr, rec_model, rec_id)]
679         elif rec_src:
680             q = unsafe_eval(rec_src, eval_dict)
681             ids = self.pool.get(rec_model).search(cr, uid, q, context=context)
682             if rec_src_count:
683                 count = int(rec_src_count)
684                 if len(ids) != count:
685                     self.assert_report.record_assertion(False, severity)
686                     msg = 'assertion "%s" failed!\n'    \
687                           ' Incorrect search count:\n'  \
688                           ' expected count: %d\n'       \
689                           ' obtained count: %d\n'       \
690                           % (rec_string, count, len(ids))
691                     self.logger.notifyChannel('init', severity, msg)
692                     sevval = getattr(logging, severity.upper())
693                     if sevval >= config['assert_exit_level']:
694                         # TODO: define a dedicated exception
695                         raise Exception('Severe assertion failure')
696                     return
697
698         assert ids is not None,\
699             'You must give either an id or a search criteria'
700         ref = _ref(self, cr)
701         for id in ids:
702             brrec =  model.browse(cr, uid, id, context)
703             class d(dict):
704                 def __getitem__(self2, key):
705                     if key in brrec:
706                         return brrec[key]
707                     return dict.__getitem__(self2, key)
708             globals_dict = d()
709             globals_dict['floatEqual'] = self._assert_equals
710             globals_dict['ref'] = ref
711             globals_dict['_ref'] = ref
712             for test in rec.findall('./test'):
713                 f_expr = test.get("expr",'').encode('utf-8')
714                 expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
715                 expression_value = unsafe_eval(f_expr, globals_dict)
716                 if expression_value != expected_value: # assertion failed
717                     self.assert_report.record_assertion(False, severity)
718                     msg = 'assertion "%s" failed!\n'    \
719                           ' xmltag: %s\n'               \
720                           ' expected value: %r\n'       \
721                           ' obtained value: %r\n'       \
722                           % (rec_string, etree.tostring(test), expected_value, expression_value)
723                     self.logger.notifyChannel('init', severity, msg)
724                     sevval = getattr(logging, severity.upper())
725                     if sevval >= config['assert_exit_level']:
726                         # TODO: define a dedicated exception
727                         raise Exception('Severe assertion failure')
728                     return
729         else: # all tests were successful for this assertion tag (no break)
730             self.assert_report.record_assertion(True, severity)
731
732     def _tag_record(self, cr, rec, data_node=None):
733         rec_model = rec.get("model").encode('ascii')
734         model = self.pool.get(rec_model)
735         assert model, "The model %s does not exist !" % (rec_model,)
736         rec_id = rec.get("id",'').encode('ascii')
737         rec_context = rec.get("context", None)
738         if rec_context:
739             rec_context = unsafe_eval(rec_context)
740         self._test_xml_id(rec_id)
741         if self.isnoupdate(data_node) and self.mode != 'init':
742             # check if the xml record has an id string
743             if rec_id:
744                 if '.' in rec_id:
745                     module,rec_id2 = rec_id.split('.')
746                 else:
747                     module = self.module
748                     rec_id2 = rec_id
749                 id = self.pool.get('ir.model.data')._update_dummy(cr, self.uid, rec_model, module, rec_id2)
750                 # check if the resource already existed at the last update
751                 if id:
752                     # if it existed, we don't update the data, but we need to
753                     # know the id of the existing record anyway
754                     self.idref[rec_id] = int(id)
755                     return None
756                 else:
757                     # if the resource didn't exist
758                     if not self.nodeattr2bool(rec, 'forcecreate', True):
759                         # we don't want to create it, so we skip it
760                         return None
761                     # else, we let the record to be created
762
763             else:
764                 # otherwise it is skipped
765                 return None
766         res = {}
767         for field in rec.findall('./field'):
768 #TODO: most of this code is duplicated above (in _eval_xml)...
769             f_name = field.get("name",'').encode('utf-8')
770             f_ref = field.get("ref",'').encode('utf-8')
771             f_search = field.get("search",'').encode('utf-8')
772             f_model = field.get("model",'').encode('utf-8')
773             if not f_model and model._columns.get(f_name,False):
774                 f_model = model._columns[f_name]._obj
775             f_use = field.get("use",'').encode('utf-8') or 'id'
776             f_val = False
777
778             if f_search:
779                 q = unsafe_eval(f_search, self.idref)
780                 field = []
781                 assert f_model, 'Define an attribute model="..." in your .XML file !'
782                 f_obj = self.pool.get(f_model)
783                 # browse the objects searched
784                 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
785                 # column definitions of the "local" object
786                 _cols = self.pool.get(rec_model)._columns
787                 # if the current field is many2many
788                 if (f_name in _cols) and _cols[f_name]._type=='many2many':
789                     f_val = [(6, 0, map(lambda x: x[f_use], s))]
790                 elif len(s):
791                     # otherwise (we are probably in a many2one field),
792                     # take the first element of the search
793                     f_val = s[0][f_use]
794             elif f_ref:
795                 if f_ref=="null":
796                     f_val = False
797                 else:
798                     if f_name in model._columns \
799                               and model._columns[f_name]._type == 'reference':
800                         val = self.model_id_get(cr, f_model, f_ref)
801                         f_val = val[0] + ',' + str(val[1])
802                     else:
803                         f_val = self.id_get(cr, f_model, f_ref)
804             else:
805                 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
806                 if model._columns.has_key(f_name):
807                     if isinstance(model._columns[f_name], osv.fields.integer):
808                         f_val = int(f_val)
809             res[f_name] = f_val
810
811         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 )
812         if rec_id:
813             self.idref[rec_id] = int(id)
814         if config.get('import_partial', False):
815             cr.commit()
816         return rec_model, id
817
818     def id_get(self, cr, model, id_str):
819         if id_str in self.idref:
820             return self.idref[id_str]
821         res = self.model_id_get(cr, model, id_str)
822         if res and len(res)>1: res = res[1]
823         return res
824
825     def model_id_get(self, cr, model, id_str):
826         model_data_obj = self.pool.get('ir.model.data')
827         mod = self.module
828         if '.' in id_str:
829             mod,id_str = id_str.split('.')
830         result = model_data_obj._get_id(cr, self.uid, mod, id_str)
831         res = model_data_obj.read(cr, self.uid, [result], ['model', 'res_id'])
832         if res and res[0] and res[0]['res_id']:
833             return res[0]['model'], int(res[0]['res_id'])
834         return False
835
836     def parse(self, de):
837         if not de.tag in ['terp', 'openerp']:
838             self.logger.notifyChannel("init", netsvc.LOG_ERROR, "Mismatch xml format" )
839             raise Exception( "Mismatch xml format: only terp or openerp as root tag" )
840
841         if de.tag == 'terp':
842             self.logger.notifyChannel("init", netsvc.LOG_WARNING, "The tag <terp/> is deprecated, use <openerp/>")
843
844         for n in de.findall('./data'):
845             for rec in n:
846                     if rec.tag in self._tags:
847                         try:
848                             self._tags[rec.tag](self.cr, rec, n)
849                         except:
850                             self.logger.notifyChannel("init", netsvc.LOG_ERROR, '\n'+etree.tostring(rec))
851                             self.cr.rollback()
852                             raise
853         return True
854
855     def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
856
857         self.logger = netsvc.Logger()
858         self.mode = mode
859         self.module = module
860         self.cr = cr
861         self.idref = idref
862         self.pool = pooler.get_pool(cr.dbname)
863         self.uid = 1
864         if report is None:
865             report = assertion_report()
866         self.assert_report = report
867         self.noupdate = noupdate
868         self._tags = {
869             'menuitem': self._tag_menuitem,
870             'record': self._tag_record,
871             'assert': self._tag_assert,
872             'report': self._tag_report,
873             'wizard': self._tag_wizard,
874             'delete': self._tag_delete,
875             'ir_set': self._tag_ir_set,
876             'function': self._tag_function,
877             'workflow': self._tag_workflow,
878             'act_window': self._tag_act_window,
879             'url': self._tag_url
880         }
881
882 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
883         noupdate=False):
884     '''Import csv file :
885         quote: "
886         delimiter: ,
887         encoding: utf-8'''
888     if not idref:
889         idref={}
890     model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
891     #remove folder path from model
892     head, model = os.path.split(model)
893
894     pool = pooler.get_pool(cr.dbname)
895
896     input = cStringIO.StringIO(csvcontent)
897     reader = csv.reader(input, quotechar='"', delimiter=',')
898     fields = reader.next()
899     fname_partial = ""
900     if config.get('import_partial'):
901         fname_partial = module + '/'+ fname
902         if not os.path.isfile(config.get('import_partial')):
903             pickle.dump({}, file(config.get('import_partial'),'w+'))
904         else:
905             data = pickle.load(file(config.get('import_partial')))
906             if fname_partial in data:
907                 if not data[fname_partial]:
908                     return
909                 else:
910                     for i in range(data[fname_partial]):
911                         reader.next()
912
913     if not (mode == 'init' or 'id' in fields):
914         logger = netsvc.Logger()
915         logger.notifyChannel("init", netsvc.LOG_ERROR,
916             "Import specification does not contain 'id' and we are in init mode, Cannot continue.")
917         return
918
919     uid = 1
920     datas = []
921     for line in reader:
922         if (not line) or not reduce(lambda x,y: x or y, line) :
923             continue
924         try:
925             datas.append(map(lambda x: misc.ustr(x), line))
926         except:
927             logger = netsvc.Logger()
928             logger.notifyChannel("init", netsvc.LOG_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