[MERGE] Forward-port of latest 7.0 bugfixes, up to rev. 5294 revid:odo@openerp.com...
[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             try:
309                 ids = self.pool[d_model].search(cr, self.uid, unsafe_eval(d_search, idref))
310             except ValueError:
311                 _logger.warning('Skipping deletion for failed search `%r`', d_search, exc_info=True)
312                 pass
313         if d_id:
314             try:
315                 ids.append(self.id_get(cr, d_id))
316             except ValueError:
317                 # d_id cannot be found. doesn't matter in this case
318                 _logger.warning('Skipping deletion for missing XML ID `%r`', d_id, exc_info=True)
319                 pass
320         if ids:
321             self.pool[d_model].unlink(cr, self.uid, ids)
322
323     def _remove_ir_values(self, cr, name, value, model):
324         ir_values_obj = self.pool['ir.values']
325         ir_value_ids = ir_values_obj.search(cr, self.uid, [('name','=',name),('value','=',value),('model','=',model)])
326         if ir_value_ids:
327             ir_values_obj.unlink(cr, self.uid, ir_value_ids)
328
329         return True
330
331     def _tag_report(self, cr, rec, data_node=None):
332         res = {}
333         for dest,f in (('name','string'),('model','model'),('report_name','name')):
334             res[dest] = rec.get(f,'').encode('utf8')
335             assert res[dest], "Attribute %s of report is empty !" % (f,)
336         for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),
337                            ('attachment','attachment'),('attachment_use','attachment_use'), ('usage','usage'),
338                            ('report_type', 'report_type'), ('parser', 'parser')):
339             if rec.get(field):
340                 res[dest] = rec.get(field).encode('utf8')
341         if rec.get('auto'):
342             res['auto'] = eval(rec.get('auto','False'))
343         if rec.get('sxw'):
344             sxw_content = misc.file_open(rec.get('sxw')).read()
345             res['report_sxw_content'] = sxw_content
346         if rec.get('header'):
347             res['header'] = eval(rec.get('header','False'))
348
349         res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
350
351         xml_id = rec.get('id','').encode('utf8')
352         self._test_xml_id(xml_id)
353
354         if rec.get('groups'):
355             g_names = rec.get('groups','').split(',')
356             groups_value = []
357             for group in g_names:
358                 if group.startswith('-'):
359                     group_id = self.id_get(cr, group[1:])
360                     groups_value.append((3, group_id))
361                 else:
362                     group_id = self.id_get(cr, group)
363                     groups_value.append((4, group_id))
364             res['groups_id'] = groups_value
365
366         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)
367         self.idref[xml_id] = int(id)
368
369         if not rec.get('menu') or eval(rec.get('menu','False')):
370             keyword = str(rec.get('keyword', 'client_print_multi'))
371             value = 'ir.actions.report.xml,'+str(id)
372             replace = rec.get('replace', True)
373             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)
374         elif self.mode=='update' and eval(rec.get('menu','False'))==False:
375             # Special check for report having attribute menu=False on update
376             value = 'ir.actions.report.xml,'+str(id)
377             self._remove_ir_values(cr, res['name'], value, res['model'])
378         return id
379
380     def _tag_function(self, cr, rec, data_node=None):
381         if self.isnoupdate(data_node) and self.mode != 'init':
382             return
383         context = self.get_context(data_node, rec, {'ref': _ref(self, cr)})
384         uid = self.get_uid(cr, self.uid, data_node, rec)
385         _eval_xml(self,rec, self.pool, cr, uid, self.idref, context=context)
386         return
387
388     def _tag_wizard(self, cr, rec, data_node=None):
389         string = rec.get("string",'').encode('utf8')
390         model = rec.get("model",'').encode('utf8')
391         name = rec.get("name",'').encode('utf8')
392         xml_id = rec.get('id','').encode('utf8')
393         self._test_xml_id(xml_id)
394         multi = rec.get('multi','') and eval(rec.get('multi','False'))
395         res = {'name': string, 'wiz_name': name, 'multi': multi, 'model': model}
396
397         if rec.get('groups'):
398             g_names = rec.get('groups','').split(',')
399             groups_value = []
400             for group in g_names:
401                 if group.startswith('-'):
402                     group_id = self.id_get(cr, group[1:])
403                     groups_value.append((3, group_id))
404                 else:
405                     group_id = self.id_get(cr, group)
406                     groups_value.append((4, group_id))
407             res['groups_id'] = groups_value
408
409         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)
410         self.idref[xml_id] = int(id)
411         # ir_set
412         if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
413             keyword = str(rec.get('keyword','') or 'client_action_multi')
414             value = 'ir.actions.wizard,'+str(id)
415             replace = rec.get("replace",'') or True
416             self.pool['ir.model.data'].ir_set(cr, self.uid, 'action', keyword, string, [model], value, replace=replace, isobject=True, xml_id=xml_id)
417         elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
418             # Special check for wizard having attribute menu=False on update
419             value = 'ir.actions.wizard,'+str(id)
420             self._remove_ir_values(cr, string, value, model)
421
422     def _tag_url(self, cr, rec, data_node=None):
423         url = rec.get("url",'').encode('utf8')
424         target = rec.get("target",'').encode('utf8')
425         name = rec.get("name",'').encode('utf8')
426         xml_id = rec.get('id','').encode('utf8')
427         self._test_xml_id(xml_id)
428
429         res = {'name': name, 'url': url, 'target':target}
430
431         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)
432         self.idref[xml_id] = int(id)
433
434     def _tag_act_window(self, cr, rec, data_node=None):
435         name = rec.get('name','').encode('utf-8')
436         xml_id = rec.get('id','').encode('utf8')
437         self._test_xml_id(xml_id)
438         type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
439         view_id = False
440         if rec.get('view_id'):
441             view_id = self.id_get(cr, rec.get('view_id','').encode('utf-8'))
442         domain = rec.get('domain','').encode('utf-8') or '[]'
443         res_model = rec.get('res_model','').encode('utf-8')
444         src_model = rec.get('src_model','').encode('utf-8')
445         view_type = rec.get('view_type','').encode('utf-8') or 'form'
446         view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
447         usage = rec.get('usage','').encode('utf-8')
448         limit = rec.get('limit','').encode('utf-8')
449         auto_refresh = rec.get('auto_refresh','').encode('utf-8')
450         uid = self.uid
451
452         # Act_window's 'domain' and 'context' contain mostly literals
453         # but they can also refer to the variables provided below
454         # in eval_context, so we need to eval() them before storing.
455         # Among the context variables, 'active_id' refers to
456         # the currently selected items in a list view, and only
457         # takes meaning at runtime on the client side. For this
458         # reason it must remain a bare variable in domain and context,
459         # even after eval() at server-side. We use the special 'unquote'
460         # class to achieve this effect: a string which has itself, unquoted,
461         # as representation.
462         active_id = unquote("active_id")
463         active_ids = unquote("active_ids")
464         active_model = unquote("active_model")
465
466         def ref(str_id):
467             return self.id_get(cr, str_id)
468
469         # Include all locals() in eval_context, for backwards compatibility
470         eval_context = {
471             'name': name,
472             'xml_id': xml_id,
473             'type': type,
474             'view_id': view_id,
475             'domain': domain,
476             'res_model': res_model,
477             'src_model': src_model,
478             'view_type': view_type,
479             'view_mode': view_mode,
480             'usage': usage,
481             'limit': limit,
482             'auto_refresh': auto_refresh,
483             'uid' : uid,
484             'active_id': active_id,
485             'active_ids': active_ids,
486             'active_model': active_model,
487             'ref' : ref,
488         }
489         context = self.get_context(data_node, rec, eval_context)
490
491         try:
492             domain = unsafe_eval(domain, eval_context)
493         except NameError:
494             # Some domains contain references that are only valid at runtime at
495             # client-side, so in that case we keep the original domain string
496             # as it is. We also log it, just in case.
497             _logger.debug('Domain value (%s) for element with id "%s" does not parse '\
498                 'at server-side, keeping original string, in case it\'s meant for client side only',
499                 domain, xml_id or 'n/a', exc_info=True)
500         res = {
501             'name': name,
502             'type': type,
503             'view_id': view_id,
504             'domain': domain,
505             'context': context,
506             'res_model': res_model,
507             'src_model': src_model,
508             'view_type': view_type,
509             'view_mode': view_mode,
510             'usage': usage,
511             'limit': limit,
512             'auto_refresh': auto_refresh,
513         }
514
515         if rec.get('groups'):
516             g_names = rec.get('groups','').split(',')
517             groups_value = []
518             for group in g_names:
519                 if group.startswith('-'):
520                     group_id = self.id_get(cr, group[1:])
521                     groups_value.append((3, group_id))
522                 else:
523                     group_id = self.id_get(cr, group)
524                     groups_value.append((4, group_id))
525             res['groups_id'] = groups_value
526
527         if rec.get('target'):
528             res['target'] = rec.get('target','')
529         if rec.get('multi'):
530             res['multi'] = eval(rec.get('multi', 'False'))
531         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)
532         self.idref[xml_id] = int(id)
533
534         if src_model:
535             #keyword = 'client_action_relate'
536             keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
537             value = 'ir.actions.act_window,'+str(id)
538             replace = rec.get('replace','') or True
539             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)
540         # TODO add remove ir.model.data
541
542     def _tag_ir_set(self, cr, rec, data_node=None):
543         if self.mode != 'init':
544             return
545         res = {}
546         for field in rec.findall('./field'):
547             f_name = field.get("name",'').encode('utf-8')
548             f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
549             res[f_name] = f_val
550         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))
551
552     def _tag_workflow(self, cr, rec, data_node=None):
553         if self.isnoupdate(data_node) and self.mode != 'init':
554             return
555         model = str(rec.get('model',''))
556         w_ref = rec.get('ref','')
557         if w_ref:
558             id = self.id_get(cr, w_ref)
559         else:
560             number_children = len(rec)
561             assert number_children > 0,\
562                 'You must define a child node if you dont give a ref'
563             assert number_children == 1,\
564                 'Only one child node is accepted (%d given)' % number_children
565             id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
566
567         uid = self.get_uid(cr, self.uid, data_node, rec)
568         openerp.workflow.trg_validate(uid, model,
569             id,
570             str(rec.get('action','')), cr)
571
572     #
573     # Support two types of notation:
574     #   name="Inventory Control/Sending Goods"
575     # or
576     #   action="action_id"
577     #   parent="parent_id"
578     #
579     def _tag_menuitem(self, cr, rec, data_node=None):
580         rec_id = rec.get("id",'').encode('ascii')
581         self._test_xml_id(rec_id)
582         m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
583
584         values = {'parent_id': False}
585         if rec.get('parent', False) is False and len(m_l) > 1:
586             # No parent attribute specified and the menu name has several menu components,
587             # try to determine the ID of the parent according to menu path
588             pid = False
589             res = None
590             values['name'] = m_l[-1]
591             m_l = m_l[:-1] # last part is our name, not a parent
592             for idx, menu_elem in enumerate(m_l):
593                 if pid:
594                     cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
595                 else:
596                     cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
597                 res = cr.fetchone()
598                 if res:
599                     pid = res[0]
600                 else:
601                     # the menuitem does't exist but we are in branch (not a leaf)
602                     _logger.warning('Warning no ID for submenu %s of menu %s !', menu_elem, str(m_l))
603                     pid = self.pool['ir.ui.menu'].create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
604             values['parent_id'] = pid
605         else:
606             # The parent attribute was specified, if non-empty determine its ID, otherwise
607             # explicitly make a top-level menu
608             if rec.get('parent'):
609                 menu_parent_id = self.id_get(cr, rec.get('parent',''))
610             else:
611                 # we get here with <menuitem parent="">, explicit clear of parent, or
612                 # if no parent attribute at all but menu name is not a menu path
613                 menu_parent_id = False
614             values = {'parent_id': menu_parent_id}
615             if rec.get('name'):
616                 values['name'] = rec.get('name')
617             try:
618                 res = [ self.id_get(cr, rec.get('id','')) ]
619             except:
620                 res = None
621
622         if rec.get('action'):
623             a_action = rec.get('action','').encode('utf8')
624
625             # determine the type of action
626             a_type, a_id = self.model_id_get(cr, a_action)
627             a_type = a_type.split('.')[-1] # keep only type part
628
629             icons = {
630                 "act_window": 'STOCK_NEW',
631                 "report.xml": 'STOCK_PASTE',
632                 "wizard": 'STOCK_EXECUTE',
633                 "url": 'STOCK_JUMP_TO',
634                 "client": 'STOCK_EXECUTE',
635                 "server": 'STOCK_EXECUTE',
636             }
637             values['icon'] = icons.get(a_type,'STOCK_NEW')
638
639             if a_type=='act_window':
640                 cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
641                 rrres = cr.fetchone()
642                 assert rrres, "No window action defined for this id %s !\n" \
643                     "Verify that this is a window action or add a type argument." % (a_action,)
644                 action_type,action_mode,action_name,view_id,target = rrres
645                 if view_id:
646                     view_arch = self.pool['ir.ui.view'].read(cr, 1, [view_id], ['arch'])
647                     action_mode = etree.fromstring(view_arch[0]['arch'].encode('utf8')).tag
648                 cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
649                 if cr.rowcount:
650                     action_mode, = cr.fetchone()
651                 if action_type=='tree':
652                     values['icon'] = 'STOCK_INDENT'
653                 elif action_mode and action_mode.startswith(('tree','kanban','gantt')):
654                     values['icon'] = 'STOCK_JUSTIFY_FILL'
655                 elif action_mode and action_mode.startswith('graph'):
656                     values['icon'] = 'terp-graph'
657                 elif action_mode and action_mode.startswith('calendar'):
658                     values['icon'] = 'terp-calendar'
659                 if target=='new':
660                     values['icon'] = 'STOCK_EXECUTE'
661                 if not values.get('name', False):
662                     values['name'] = action_name
663
664             elif a_type in ['wizard', 'url', 'client', 'server'] and not values.get('name'):
665                 a_table = 'ir_act_%s' % a_type
666                 cr.execute('select name from %s where id=%%s' % a_table, (int(a_id),))
667                 resw = cr.fetchone()
668                 if resw:
669                     values['name'] = resw[0]
670
671         if not values.get('name'):
672             # ensure menu has a name
673             values['name'] = rec_id or '?'
674
675         if rec.get('sequence'):
676             values['sequence'] = int(rec.get('sequence'))
677         if rec.get('icon'):
678             values['icon'] = str(rec.get('icon'))
679         if rec.get('web_icon'):
680             values['web_icon'] = "%s,%s" %(self.module, str(rec.get('web_icon')))
681         if rec.get('web_icon_hover'):
682             values['web_icon_hover'] = "%s,%s" %(self.module, str(rec.get('web_icon_hover')))
683
684         if rec.get('groups'):
685             g_names = rec.get('groups','').split(',')
686             groups_value = []
687             for group in g_names:
688                 if group.startswith('-'):
689                     group_id = self.id_get(cr, group[1:])
690                     groups_value.append((3, group_id))
691                 else:
692                     group_id = self.id_get(cr, group)
693                     groups_value.append((4, group_id))
694             values['groups_id'] = groups_value
695
696         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)
697
698         if rec_id and pid:
699             self.idref[rec_id] = int(pid)
700
701         if rec.get('action') and pid:
702             action = "ir.actions.%s,%d" % (a_type, a_id)
703             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)
704         return 'ir.ui.menu', pid
705
706     def _assert_equals(self, f1, f2, prec=4):
707         return not round(f1 - f2, prec)
708
709     def _tag_assert(self, cr, rec, data_node=None):
710         if self.isnoupdate(data_node) and self.mode != 'init':
711             return
712
713         rec_model = rec.get("model",'').encode('ascii')
714         model = self.pool[rec_model]
715         rec_id = rec.get("id",'').encode('ascii')
716         self._test_xml_id(rec_id)
717         rec_src = rec.get("search",'').encode('utf8')
718         rec_src_count = rec.get("count")
719
720         rec_string = rec.get("string",'').encode('utf8') or 'unknown'
721
722         ids = None
723         eval_dict = {'ref': _ref(self, cr)}
724         context = self.get_context(data_node, rec, eval_dict)
725         uid = self.get_uid(cr, self.uid, data_node, rec)
726         if rec_id:
727             ids = [self.id_get(cr, rec_id)]
728         elif rec_src:
729             q = unsafe_eval(rec_src, eval_dict)
730             ids = self.pool[rec_model].search(cr, uid, q, context=context)
731             if rec_src_count:
732                 count = int(rec_src_count)
733                 if len(ids) != count:
734                     self.assertion_report.record_failure()
735                     msg = 'assertion "%s" failed!\n'    \
736                           ' Incorrect search count:\n'  \
737                           ' expected count: %d\n'       \
738                           ' obtained count: %d\n'       \
739                           % (rec_string, count, len(ids))
740                     _logger.error(msg)
741                     return
742
743         assert ids is not None,\
744             'You must give either an id or a search criteria'
745         ref = _ref(self, cr)
746         for id in ids:
747             brrec =  model.browse(cr, uid, id, context)
748             class d(dict):
749                 def __getitem__(self2, key):
750                     if key in brrec:
751                         return brrec[key]
752                     return dict.__getitem__(self2, key)
753             globals_dict = d()
754             globals_dict['floatEqual'] = self._assert_equals
755             globals_dict['ref'] = ref
756             globals_dict['_ref'] = ref
757             for test in rec.findall('./test'):
758                 f_expr = test.get("expr",'').encode('utf-8')
759                 expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
760                 expression_value = unsafe_eval(f_expr, globals_dict)
761                 if expression_value != expected_value: # assertion failed
762                     self.assertion_report.record_failure()
763                     msg = 'assertion "%s" failed!\n'    \
764                           ' xmltag: %s\n'               \
765                           ' expected value: %r\n'       \
766                           ' obtained value: %r\n'       \
767                           % (rec_string, etree.tostring(test), expected_value, expression_value)
768                     _logger.error(msg)
769                     return
770         else: # all tests were successful for this assertion tag (no break)
771             self.assertion_report.record_success()
772
773     def _tag_record(self, cr, rec, data_node=None):
774         rec_model = rec.get("model").encode('ascii')
775         model = self.pool[rec_model]
776         rec_id = rec.get("id",'').encode('ascii')
777         rec_context = rec.get("context", None)
778         if rec_context:
779             rec_context = unsafe_eval(rec_context)
780         self._test_xml_id(rec_id)
781         # in update mode, the record won't be updated if the data node explicitely
782         # opt-out using @noupdate="1". A second check will be performed in
783         # ir.model.data#_update() using the record's ir.model.data `noupdate` field.
784         if self.isnoupdate(data_node) and self.mode != 'init':
785             # check if the xml record has an id string
786             if rec_id:
787                 if '.' in rec_id:
788                     module,rec_id2 = rec_id.split('.')
789                 else:
790                     module = self.module
791                     rec_id2 = rec_id
792                 id = self.pool['ir.model.data']._update_dummy(cr, self.uid, rec_model, module, rec_id2)
793                 # check if the resource already existed at the last update
794                 if id:
795                     # if it existed, we don't update the data, but we need to
796                     # know the id of the existing record anyway
797                     self.idref[rec_id] = int(id)
798                     return None
799                 else:
800                     # if the resource didn't exist
801                     if not self.nodeattr2bool(rec, 'forcecreate', True):
802                         # we don't want to create it, so we skip it
803                         return None
804                     # else, we let the record to be created
805
806             else:
807                 # otherwise it is skipped
808                 return None
809         res = {}
810         for field in rec.findall('./field'):
811 #TODO: most of this code is duplicated above (in _eval_xml)...
812             f_name = field.get("name",'').encode('utf-8')
813             f_ref = field.get("ref",'').encode('utf-8')
814             f_search = field.get("search",'').encode('utf-8')
815             f_model = field.get("model",'').encode('utf-8')
816             if not f_model and model._all_columns.get(f_name,False):
817                 f_model = model._all_columns[f_name].column._obj
818             f_use = field.get("use",'').encode('utf-8') or 'id'
819             f_val = False
820
821             if f_search:
822                 q = unsafe_eval(f_search, self.idref)
823                 field = []
824                 assert f_model, 'Define an attribute model="..." in your .XML file !'
825                 f_obj = self.pool[f_model]
826                 # browse the objects searched
827                 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
828                 # column definitions of the "local" object
829                 _cols = self.pool[rec_model]._all_columns
830                 # if the current field is many2many
831                 if (f_name in _cols) and _cols[f_name].column._type=='many2many':
832                     f_val = [(6, 0, map(lambda x: x[f_use], s))]
833                 elif len(s):
834                     # otherwise (we are probably in a many2one field),
835                     # take the first element of the search
836                     f_val = s[0][f_use]
837             elif f_ref:
838                 if f_ref=="null":
839                     f_val = False
840                 else:
841                     if f_name in model._all_columns \
842                               and model._all_columns[f_name].column._type == 'reference':
843                         val = self.model_id_get(cr, f_ref)
844                         f_val = val[0] + ',' + str(val[1])
845                     else:
846                         f_val = self.id_get(cr, f_ref)
847             else:
848                 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
849                 if f_name in model._all_columns:
850                     import openerp.osv as osv
851                     if isinstance(model._all_columns[f_name].column, osv.fields.integer):
852                         f_val = int(f_val)
853             res[f_name] = f_val
854
855         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 )
856         if rec_id:
857             self.idref[rec_id] = int(id)
858         if config.get('import_partial', False):
859             cr.commit()
860         return rec_model, id
861
862     def _tag_template(self, cr, el, data_node=None):
863         # This helper transforms a <template> element into a <record> and forwards it
864         tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
865         module = self.module
866         if '.' in tpl_id:
867             module, tpl_id = tpl_id.split('.', 1)
868         # set the full template name for qweb <module>.<id>
869         if not (el.get('inherit_id') or el.get('inherit_option_id')):
870             el.set('t-name', '%s.%s' % (module, tpl_id))
871             el.tag = 't'
872         else:
873             el.tag = 'data'
874         el.attrib.pop('id', None)
875
876         record_attrs = {
877             'id': tpl_id,
878             'model': 'ir.ui.view',
879         }
880         for att in ['forcecreate', 'context']:
881             if att in el.keys():
882                 record_attrs[att] = el.attrib.pop(att)
883
884         Field = builder.E.field
885         name = el.get('name', tpl_id)
886
887         record = etree.Element('record', attrib=record_attrs)
888         record.append(Field(name, name='name'))
889         record.append(Field("qweb", name='type'))
890         record.append(Field(el.get('priority', "16"), name='priority'))
891         record.append(Field(el, name="arch", type="xml"))
892         for field_name in ('inherit_id','inherit_option_id'):
893             value = el.attrib.pop(field_name, None)
894             if value: record.append(Field(name=field_name, ref=value))
895         groups = el.attrib.pop('groups', None)
896         if groups:
897             grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
898             record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
899         if el.attrib.pop('page', None) == 'True':
900             record.append(Field(name="page", eval="True"))
901
902         return self._tag_record(cr, record, data_node)
903
904     def id_get(self, cr, id_str):
905         if id_str in self.idref:
906             return self.idref[id_str]
907         res = self.model_id_get(cr, id_str)
908         if res and len(res)>1: res = res[1]
909         return res
910
911     def model_id_get(self, cr, id_str):
912         model_data_obj = self.pool['ir.model.data']
913         mod = self.module
914         if '.' in id_str:
915             mod,id_str = id_str.split('.')
916         return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
917
918     def parse(self, de):
919         if de.tag != 'openerp':
920             raise Exception("Mismatch xml format: root tag must be `openerp`.")
921
922         for n in de.findall('./data'):
923             for rec in n:
924                 if rec.tag in self._tags:
925                     try:
926                         self._tags[rec.tag](self.cr, rec, n)
927                     except Exception, e:
928                         self.cr.rollback()
929                         exc_info = sys.exc_info()
930                         raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
931         return True
932
933     def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
934
935         self.mode = mode
936         self.module = module
937         self.cr = cr
938         self.idref = idref
939         self.pool = openerp.registry(cr.dbname)
940         self.uid = 1
941         if report is None:
942             report = assertion_report.assertion_report()
943         self.assertion_report = report
944         self.noupdate = noupdate
945         self._tags = {
946             'menuitem': self._tag_menuitem,
947             'record': self._tag_record,
948             'template': self._tag_template,
949             'assert': self._tag_assert,
950             'report': self._tag_report,
951             'wizard': self._tag_wizard,
952             'delete': self._tag_delete,
953             'ir_set': self._tag_ir_set,
954             'function': self._tag_function,
955             'workflow': self._tag_workflow,
956             'act_window': self._tag_act_window,
957             'url': self._tag_url
958         }
959
960 def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None):
961     pathname = os.path.join(module, filename)
962     fp = misc.file_open(pathname)
963     ext = os.path.splitext(filename)[1].lower()
964     try:
965         if ext == '.csv':
966             convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
967         elif ext == '.sql':
968             convert_sql_import(cr, fp)
969         elif ext == '.yml':
970             convert_yaml_import(cr, module, fp, kind, idref, mode, noupdate, report)
971         elif ext == '.xml':
972             convert_xml_import(cr, module, fp, idref, mode, noupdate, report)
973         elif ext == '.js':
974             pass # .js files are valid but ignored here.
975         else:
976             _logger.warning("Can't load unknown file type %s.", filename)
977     finally:
978         fp.close()
979
980 def convert_sql_import(cr, fp):
981     queries = fp.read().split(';')
982     for query in queries:
983         new_query = ' '.join(query.split())
984         if new_query:
985             cr.execute(new_query)
986
987 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
988         noupdate=False):
989     '''Import csv file :
990         quote: "
991         delimiter: ,
992         encoding: utf-8'''
993     if not idref:
994         idref={}
995     model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
996     #remove folder path from model
997     head, model = os.path.split(model)
998
999     input = cStringIO.StringIO(csvcontent) #FIXME
1000     reader = csv.reader(input, quotechar='"', delimiter=',')
1001     fields = reader.next()
1002     fname_partial = ""
1003     if config.get('import_partial'):
1004         fname_partial = module + '/'+ fname
1005         if not os.path.isfile(config.get('import_partial')):
1006             pickle.dump({}, file(config.get('import_partial'),'w+'))
1007         else:
1008             data = pickle.load(file(config.get('import_partial')))
1009             if fname_partial in data:
1010                 if not data[fname_partial]:
1011                     return
1012                 else:
1013                     for i in range(data[fname_partial]):
1014                         reader.next()
1015
1016     if not (mode == 'init' or 'id' in fields):
1017         _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
1018         return
1019
1020     uid = 1
1021     datas = []
1022     for line in reader:
1023         if not (line and any(line)):
1024             continue
1025         try:
1026             datas.append(map(misc.ustr, line))
1027         except:
1028             _logger.error("Cannot import the line: %s", line)
1029
1030     registry = openerp.registry(cr.dbname)
1031     result, rows, warning_msg, dummy = registry[model].import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
1032     if result < 0:
1033         # Report failed import and abort module install
1034         raise Exception(_('Module loading %s failed: file %s could not be processed:\n %s') % (module, fname, warning_msg))
1035     if config.get('import_partial'):
1036         data = pickle.load(file(config.get('import_partial')))
1037         data[fname_partial] = 0
1038         pickle.dump(data, file(config.get('import_partial'),'wb'))
1039         cr.commit()
1040
1041 #
1042 # xml import/export
1043 #
1044 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
1045     doc = etree.parse(xmlfile)
1046     relaxng = etree.RelaxNG(
1047         etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
1048     try:
1049         relaxng.assert_(doc)
1050     except Exception:
1051         _logger.error('The XML file does not fit the required schema !')
1052         _logger.error(misc.ustr(relaxng.error_log.last_error))
1053         raise
1054
1055     if idref is None:
1056         idref={}
1057     obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
1058     obj.parse(doc.getroot())
1059     return True
1060
1061
1062 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: