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