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