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