[MERGE] forward port of branch 7.0 up to revid 5288 mat@openerp.com-20140423150215...
[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         # in update mode, the record won't be updated if the data node explicitely
777         # opt-out using @noupdate="1". A second check will be performed in
778         # ir.model.data#_update() using the record's ir.model.data `noupdate` field.
779         if self.isnoupdate(data_node) and self.mode != 'init':
780             # check if the xml record has an id string
781             if rec_id:
782                 if '.' in rec_id:
783                     module,rec_id2 = rec_id.split('.')
784                 else:
785                     module = self.module
786                     rec_id2 = rec_id
787                 id = self.pool['ir.model.data']._update_dummy(cr, self.uid, rec_model, module, rec_id2)
788                 # check if the resource already existed at the last update
789                 if id:
790                     # if it existed, we don't update the data, but we need to
791                     # know the id of the existing record anyway
792                     self.idref[rec_id] = int(id)
793                     return None
794                 else:
795                     # if the resource didn't exist
796                     if not self.nodeattr2bool(rec, 'forcecreate', True):
797                         # we don't want to create it, so we skip it
798                         return None
799                     # else, we let the record to be created
800
801             else:
802                 # otherwise it is skipped
803                 return None
804         res = {}
805         for field in rec.findall('./field'):
806 #TODO: most of this code is duplicated above (in _eval_xml)...
807             f_name = field.get("name",'').encode('utf-8')
808             f_ref = field.get("ref",'').encode('utf-8')
809             f_search = field.get("search",'').encode('utf-8')
810             f_model = field.get("model",'').encode('utf-8')
811             if not f_model and model._all_columns.get(f_name,False):
812                 f_model = model._all_columns[f_name].column._obj
813             f_use = field.get("use",'').encode('utf-8') or 'id'
814             f_val = False
815
816             if f_search:
817                 q = unsafe_eval(f_search, self.idref)
818                 field = []
819                 assert f_model, 'Define an attribute model="..." in your .XML file !'
820                 f_obj = self.pool[f_model]
821                 # browse the objects searched
822                 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
823                 # column definitions of the "local" object
824                 _cols = self.pool[rec_model]._all_columns
825                 # if the current field is many2many
826                 if (f_name in _cols) and _cols[f_name].column._type=='many2many':
827                     f_val = [(6, 0, map(lambda x: x[f_use], s))]
828                 elif len(s):
829                     # otherwise (we are probably in a many2one field),
830                     # take the first element of the search
831                     f_val = s[0][f_use]
832             elif f_ref:
833                 if f_ref=="null":
834                     f_val = False
835                 else:
836                     if f_name in model._all_columns \
837                               and model._all_columns[f_name].column._type == 'reference':
838                         val = self.model_id_get(cr, f_ref)
839                         f_val = val[0] + ',' + str(val[1])
840                     else:
841                         f_val = self.id_get(cr, f_ref)
842             else:
843                 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
844                 if f_name in model._all_columns:
845                     import openerp.osv as osv
846                     if isinstance(model._all_columns[f_name].column, osv.fields.integer):
847                         f_val = int(f_val)
848             res[f_name] = f_val
849
850         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 )
851         if rec_id:
852             self.idref[rec_id] = int(id)
853         if config.get('import_partial', False):
854             cr.commit()
855         return rec_model, id
856
857     def _tag_template(self, cr, el, data_node=None):
858         # This helper transforms a <template> element into a <record> and forwards it
859         tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
860         module = self.module
861         if '.' in tpl_id:
862             module, tpl_id = tpl_id.split('.', 1)
863         # set the full template name for qweb <module>.<id>
864         if not (el.get('inherit_id') or el.get('inherit_option_id')):
865             el.set('t-name', '%s.%s' % (module, tpl_id))
866             el.tag = 't'
867         else:
868             el.tag = 'data'
869         el.attrib.pop('id', None)
870
871         record_attrs = {
872             'id': tpl_id,
873             'model': 'ir.ui.view',
874         }
875         for att in ['forcecreate', 'context']:
876             if att in el.keys():
877                 record_attrs[att] = el.attrib.pop(att)
878
879         Field = builder.E.field
880         name = el.get('name', tpl_id)
881
882         record = etree.Element('record', attrib=record_attrs)
883         record.append(Field(name, name='name'))
884         record.append(Field("qweb", name='type'))
885         record.append(Field(el.get('priority', "16"), name='priority'))
886         record.append(Field(el, name="arch", type="xml"))
887         for field_name in ('inherit_id','inherit_option_id'):
888             value = el.attrib.pop(field_name, None)
889             if value: record.append(Field(name=field_name, ref=value))
890         groups = el.attrib.pop('groups', None)
891         if groups:
892             grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
893             record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
894         if el.attrib.pop('page', None) == 'True':
895             record.append(Field(name="page", eval="True"))
896
897         return self._tag_record(cr, record, data_node)
898
899     def id_get(self, cr, id_str):
900         if id_str in self.idref:
901             return self.idref[id_str]
902         res = self.model_id_get(cr, id_str)
903         if res and len(res)>1: res = res[1]
904         return res
905
906     def model_id_get(self, cr, id_str):
907         model_data_obj = self.pool['ir.model.data']
908         mod = self.module
909         if '.' in id_str:
910             mod,id_str = id_str.split('.')
911         return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
912
913     def parse(self, de):
914         if de.tag != 'openerp':
915             raise Exception("Mismatch xml format: root tag must be `openerp`.")
916
917         for n in de.findall('./data'):
918             for rec in n:
919                 if rec.tag in self._tags:
920                     try:
921                         self._tags[rec.tag](self.cr, rec, n)
922                     except Exception, e:
923                         self.cr.rollback()
924                         exc_info = sys.exc_info()
925                         raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
926         return True
927
928     def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
929
930         self.mode = mode
931         self.module = module
932         self.cr = cr
933         self.idref = idref
934         self.pool = openerp.registry(cr.dbname)
935         self.uid = 1
936         if report is None:
937             report = assertion_report.assertion_report()
938         self.assertion_report = report
939         self.noupdate = noupdate
940         self._tags = {
941             'menuitem': self._tag_menuitem,
942             'record': self._tag_record,
943             'template': self._tag_template,
944             'assert': self._tag_assert,
945             'report': self._tag_report,
946             'wizard': self._tag_wizard,
947             'delete': self._tag_delete,
948             'ir_set': self._tag_ir_set,
949             'function': self._tag_function,
950             'workflow': self._tag_workflow,
951             'act_window': self._tag_act_window,
952             'url': self._tag_url
953         }
954
955 def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None):
956     pathname = os.path.join(module, filename)
957     fp = misc.file_open(pathname)
958     ext = os.path.splitext(filename)[1].lower()
959     try:
960         if ext == '.csv':
961             convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
962         elif ext == '.sql':
963             convert_sql_import(cr, fp)
964         elif ext == '.yml':
965             convert_yaml_import(cr, module, fp, kind, idref, mode, noupdate, report)
966         elif ext == '.xml':
967             convert_xml_import(cr, module, fp, idref, mode, noupdate, report)
968         elif ext == '.js':
969             pass # .js files are valid but ignored here.
970         else:
971             _logger.warning("Can't load unknown file type %s.", filename)
972     finally:
973         fp.close()
974
975 def convert_sql_import(cr, fp):
976     queries = fp.read().split(';')
977     for query in queries:
978         new_query = ' '.join(query.split())
979         if new_query:
980             cr.execute(new_query)
981
982 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
983         noupdate=False):
984     '''Import csv file :
985         quote: "
986         delimiter: ,
987         encoding: utf-8'''
988     if not idref:
989         idref={}
990     model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
991     #remove folder path from model
992     head, model = os.path.split(model)
993
994     input = cStringIO.StringIO(csvcontent) #FIXME
995     reader = csv.reader(input, quotechar='"', delimiter=',')
996     fields = reader.next()
997     fname_partial = ""
998     if config.get('import_partial'):
999         fname_partial = module + '/'+ fname
1000         if not os.path.isfile(config.get('import_partial')):
1001             pickle.dump({}, file(config.get('import_partial'),'w+'))
1002         else:
1003             data = pickle.load(file(config.get('import_partial')))
1004             if fname_partial in data:
1005                 if not data[fname_partial]:
1006                     return
1007                 else:
1008                     for i in range(data[fname_partial]):
1009                         reader.next()
1010
1011     if not (mode == 'init' or 'id' in fields):
1012         _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
1013         return
1014
1015     uid = 1
1016     datas = []
1017     for line in reader:
1018         if not (line and any(line)):
1019             continue
1020         try:
1021             datas.append(map(misc.ustr, line))
1022         except:
1023             _logger.error("Cannot import the line: %s", line)
1024
1025     registry = openerp.registry(cr.dbname)
1026     result, rows, warning_msg, dummy = registry[model].import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
1027     if result < 0:
1028         # Report failed import and abort module install
1029         raise Exception(_('Module loading %s failed: file %s could not be processed:\n %s') % (module, fname, warning_msg))
1030     if config.get('import_partial'):
1031         data = pickle.load(file(config.get('import_partial')))
1032         data[fname_partial] = 0
1033         pickle.dump(data, file(config.get('import_partial'),'wb'))
1034         cr.commit()
1035
1036 #
1037 # xml import/export
1038 #
1039 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
1040     doc = etree.parse(xmlfile)
1041     relaxng = etree.RelaxNG(
1042         etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
1043     try:
1044         relaxng.assert_(doc)
1045     except Exception:
1046         _logger.error('The XML file does not fit the required schema !')
1047         _logger.error(misc.ustr(relaxng.error_log.last_error))
1048         raise
1049
1050     if idref is None:
1051         idref={}
1052     obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
1053     obj.parse(doc.getroot())
1054     return True
1055
1056
1057 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: