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