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