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