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