[MERGE] merged trunk.
[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         idref2 = {}
138         if a_eval:
139             idref2 = _get_idref(self, cr, uid, f_model, context, idref)
140             try:
141                 return unsafe_eval(a_eval, idref2)
142             except Exception:
143                 _logger.warning('could not eval(%s) for %s in %s' % (a_eval, node.get('name'), context), exc_info=True)
144                 return ""
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.url", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
390         self.idref[xml_id] = int(id)
391         # ir_set
392         if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
393             keyword = str(rec.get('keyword','') or 'client_action_multi')
394             value = 'ir.actions.url,'+str(id)
395             replace = rec.get("replace",'') or True
396             self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, url, ["ir.actions.url"], value, replace=replace, isobject=True, xml_id=xml_id)
397         elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
398             # Special check for URL having attribute menu=False on update
399             value = 'ir.actions.url,'+str(id)
400             self._remove_ir_values(cr, url, value, "ir.actions.url")
401
402     def _tag_act_window(self, cr, rec, data_node=None):
403         name = rec.get('name','').encode('utf-8')
404         xml_id = rec.get('id','').encode('utf8')
405         self._test_xml_id(xml_id)
406         type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
407         view_id = False
408         if rec.get('view_id'):
409             view_id = self.id_get(cr, rec.get('view_id','').encode('utf-8'))
410         domain = rec.get('domain','').encode('utf-8') or '[]'
411         res_model = rec.get('res_model','').encode('utf-8')
412         src_model = rec.get('src_model','').encode('utf-8')
413         view_type = rec.get('view_type','').encode('utf-8') or 'form'
414         view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
415         usage = rec.get('usage','').encode('utf-8')
416         limit = rec.get('limit','').encode('utf-8')
417         auto_refresh = rec.get('auto_refresh','').encode('utf-8')
418         uid = self.uid
419
420         # Act_window's 'domain' and 'context' contain mostly literals
421         # but they can also refer to the variables provided below
422         # in eval_context, so we need to eval() them before storing.
423         # Among the context variables, 'active_id' refers to
424         # the currently selected items in a list view, and only
425         # takes meaning at runtime on the client side. For this
426         # reason it must remain a bare variable in domain and context,
427         # even after eval() at server-side. We use the special 'unquote'
428         # class to achieve this effect: a string which has itself, unquoted,
429         # as representation.
430         active_id = unquote("active_id")
431         active_ids = unquote("active_ids")
432         active_model = unquote("active_model")
433
434         def ref(str_id):
435             return self.id_get(cr, str_id)
436
437         # Include all locals() in eval_context, for backwards compatibility
438         eval_context = {
439             'name': name,
440             'xml_id': xml_id,
441             'type': type,
442             'view_id': view_id,
443             'domain': domain,
444             'res_model': res_model,
445             'src_model': src_model,
446             'view_type': view_type,
447             'view_mode': view_mode,
448             'usage': usage,
449             'limit': limit,
450             'auto_refresh': auto_refresh,
451             'uid' : uid,
452             'active_id': active_id,
453             'active_ids': active_ids,
454             'active_model': active_model,
455             'ref' : ref,
456         }
457         context = self.get_context(data_node, rec, eval_context)
458
459         try:
460             domain = unsafe_eval(domain, eval_context)
461         except NameError:
462             # Some domains contain references that are only valid at runtime at
463             # client-side, so in that case we keep the original domain string
464             # as it is. We also log it, just in case.
465             _logger.debug('Domain value (%s) for element with id "%s" does not parse '\
466                 'at server-side, keeping original string, in case it\'s meant for client side only',
467                 domain, xml_id or 'n/a', exc_info=True)
468         res = {
469             'name': name,
470             'type': type,
471             'view_id': view_id,
472             'domain': domain,
473             'context': context,
474             'res_model': res_model,
475             'src_model': src_model,
476             'view_type': view_type,
477             'view_mode': view_mode,
478             'usage': usage,
479             'limit': limit,
480             'auto_refresh': auto_refresh,
481         }
482
483         if rec.get('groups'):
484             g_names = rec.get('groups','').split(',')
485             groups_value = []
486             for group in g_names:
487                 if group.startswith('-'):
488                     group_id = self.id_get(cr, group[1:])
489                     groups_value.append((3, group_id))
490                 else:
491                     group_id = self.id_get(cr, group)
492                     groups_value.append((4, group_id))
493             res['groups_id'] = groups_value
494
495         if rec.get('target'):
496             res['target'] = rec.get('target','')
497         if rec.get('multi'):
498             res['multi'] = rec.get('multi', False)
499         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)
500         self.idref[xml_id] = int(id)
501
502         if src_model:
503             #keyword = 'client_action_relate'
504             keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
505             value = 'ir.actions.act_window,'+str(id)
506             replace = rec.get('replace','') or True
507             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)
508         # TODO add remove ir.model.data
509
510     def _tag_ir_set(self, cr, rec, data_node=None):
511         if self.mode != 'init':
512             return
513         res = {}
514         for field in rec.findall('./field'):
515             f_name = field.get("name",'').encode('utf-8')
516             f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
517             res[f_name] = f_val
518         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))
519
520     def _tag_workflow(self, cr, rec, data_node=None):
521         if self.isnoupdate(data_node) and self.mode != 'init':
522             return
523         model = str(rec.get('model',''))
524         w_ref = rec.get('ref','')
525         if w_ref:
526             id = self.id_get(cr, w_ref)
527         else:
528             number_children = len(rec)
529             assert number_children > 0,\
530                 'You must define a child node if you dont give a ref'
531             assert number_children == 1,\
532                 'Only one child node is accepted (%d given)' % number_children
533             id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
534
535         uid = self.get_uid(cr, self.uid, data_node, rec)
536         import openerp.netsvc as netsvc
537         wf_service = netsvc.LocalService("workflow")
538         wf_service.trg_validate(uid, model,
539             id,
540             str(rec.get('action','')), cr)
541
542     #
543     # Support two types of notation:
544     #   name="Inventory Control/Sending Goods"
545     # or
546     #   action="action_id"
547     #   parent="parent_id"
548     #
549     def _tag_menuitem(self, cr, rec, data_node=None):
550         rec_id = rec.get("id",'').encode('ascii')
551         self._test_xml_id(rec_id)
552         m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
553
554         values = {'parent_id': False}
555         if rec.get('parent', False) is False and len(m_l) > 1:
556             # No parent attribute specified and the menu name has several menu components,
557             # try to determine the ID of the parent according to menu path
558             pid = False
559             res = None
560             values['name'] = m_l[-1]
561             m_l = m_l[:-1] # last part is our name, not a parent
562             for idx, menu_elem in enumerate(m_l):
563                 if pid:
564                     cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
565                 else:
566                     cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
567                 res = cr.fetchone()
568                 if res:
569                     pid = res[0]
570                 else:
571                     # the menuitem does't exist but we are in branch (not a leaf)
572                     _logger.warning('Warning no ID for submenu %s of menu %s !', menu_elem, str(m_l))
573                     pid = self.pool.get('ir.ui.menu').create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
574             values['parent_id'] = pid
575         else:
576             # The parent attribute was specified, if non-empty determine its ID, otherwise
577             # explicitly make a top-level menu
578             if rec.get('parent'):
579                 menu_parent_id = self.id_get(cr, rec.get('parent',''))
580             else:
581                 # we get here with <menuitem parent="">, explicit clear of parent, or
582                 # if no parent attribute at all but menu name is not a menu path
583                 menu_parent_id = False
584             values = {'parent_id': menu_parent_id}
585             if rec.get('name'):
586                 values['name'] = rec.get('name')
587             try:
588                 res = [ self.id_get(cr, rec.get('id','')) ]
589             except:
590                 res = None
591
592         if rec.get('action'):
593             a_action = rec.get('action','').encode('utf8')
594             a_type = rec.get('type','').encode('utf8') or 'act_window'
595             icons = {
596                 "act_window": 'STOCK_NEW',
597                 "report.xml": 'STOCK_PASTE',
598                 "wizard": 'STOCK_EXECUTE',
599                 "url": 'STOCK_JUMP_TO'
600             }
601             values['icon'] = icons.get(a_type,'STOCK_NEW')
602             if a_type=='act_window':
603                 a_id = self.id_get(cr, a_action)
604                 cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
605                 rrres = cr.fetchone()
606                 assert rrres, "No window action defined for this id %s !\n" \
607                     "Verify that this is a window action or add a type argument." % (a_action,)
608                 action_type,action_mode,action_name,view_id,target = rrres
609                 if view_id:
610                     cr.execute('SELECT arch FROM ir_ui_view WHERE id=%s', (int(view_id),))
611                     arch, = cr.fetchone()
612                     action_mode = etree.fromstring(arch.encode('utf8')).tag
613                 cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
614                 if cr.rowcount:
615                     action_mode, = cr.fetchone()
616                 if action_type=='tree':
617                     values['icon'] = 'STOCK_INDENT'
618                 elif action_mode and action_mode.startswith(('tree','kanban','gantt')):
619                     values['icon'] = 'STOCK_JUSTIFY_FILL'
620                 elif action_mode and action_mode.startswith('graph'):
621                     values['icon'] = 'terp-graph'
622                 elif action_mode and action_mode.startswith('calendar'):
623                     values['icon'] = 'terp-calendar'
624                 if target=='new':
625                     values['icon'] = 'STOCK_EXECUTE'
626                 if not values.get('name', False):
627                     values['name'] = action_name
628             elif a_type=='wizard':
629                 a_id = self.id_get(cr, a_action)
630                 cr.execute('select name from ir_act_wizard where id=%s', (int(a_id),))
631                 resw = cr.fetchone()
632                 if (not values.get('name', False)) and resw:
633                     values['name'] = resw[0]
634             elif a_type=='url':
635                 a_id = self.id_get(cr, a_action)
636                 cr.execute('select name from ir_act_url where id=%s', (int(a_id),))
637                 resw = cr.fetchone()
638                 if (not values.get('name')) and resw:
639                     values['name'] = resw[0]
640         if rec.get('sequence'):
641             values['sequence'] = int(rec.get('sequence'))
642         if rec.get('icon'):
643             values['icon'] = str(rec.get('icon'))
644         if rec.get('web_icon'):
645             values['web_icon'] = "%s,%s" %(self.module, str(rec.get('web_icon')))
646         if rec.get('web_icon_hover'):
647             values['web_icon_hover'] = "%s,%s" %(self.module, str(rec.get('web_icon_hover')))
648
649         if rec.get('groups'):
650             g_names = rec.get('groups','').split(',')
651             groups_value = []
652             for group in g_names:
653                 if group.startswith('-'):
654                     group_id = self.id_get(cr, group[1:])
655                     groups_value.append((3, group_id))
656                 else:
657                     group_id = self.id_get(cr, group)
658                     groups_value.append((4, group_id))
659             values['groups_id'] = groups_value
660
661         xml_id = rec.get('id','').encode('utf8')
662         self._test_xml_id(xml_id)
663         pid = self.pool.get('ir.model.data')._update(cr, self.uid, 'ir.ui.menu', self.module, values, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode, res_id=res and res[0] or False)
664
665         if rec_id and pid:
666             self.idref[rec_id] = int(pid)
667
668         if rec.get('action') and pid:
669             a_action = rec.get('action').encode('utf8')
670             a_type = rec.get('type','').encode('utf8') or 'act_window'
671             a_id = self.id_get(cr, a_action)
672             action = "ir.actions.%s,%d" % (a_type, a_id)
673             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)
674         return ('ir.ui.menu', pid)
675
676     def _assert_equals(self, f1, f2, prec=4):
677         return not round(f1 - f2, prec)
678
679     def _tag_assert(self, cr, rec, data_node=None):
680         if self.isnoupdate(data_node) and self.mode != 'init':
681             return
682
683         rec_model = rec.get("model",'').encode('ascii')
684         model = self.pool.get(rec_model)
685         assert model, "The model %s does not exist !" % (rec_model,)
686         rec_id = rec.get("id",'').encode('ascii')
687         self._test_xml_id(rec_id)
688         rec_src = rec.get("search",'').encode('utf8')
689         rec_src_count = rec.get("count")
690
691         rec_string = rec.get("string",'').encode('utf8') or 'unknown'
692
693         ids = None
694         eval_dict = {'ref': _ref(self, cr)}
695         context = self.get_context(data_node, rec, eval_dict)
696         uid = self.get_uid(cr, self.uid, data_node, rec)
697         if rec_id:
698             ids = [self.id_get(cr, rec_id)]
699         elif rec_src:
700             q = unsafe_eval(rec_src, eval_dict)
701             ids = self.pool.get(rec_model).search(cr, uid, q, context=context)
702             if rec_src_count:
703                 count = int(rec_src_count)
704                 if len(ids) != count:
705                     self.assertion_report.record_failure()
706                     msg = 'assertion "%s" failed!\n'    \
707                           ' Incorrect search count:\n'  \
708                           ' expected count: %d\n'       \
709                           ' obtained count: %d\n'       \
710                           % (rec_string, count, len(ids))
711                     _logger.error(msg)
712                     return
713
714         assert ids is not None,\
715             'You must give either an id or a search criteria'
716         ref = _ref(self, cr)
717         for id in ids:
718             brrec =  model.browse(cr, uid, id, context)
719             class d(dict):
720                 def __getitem__(self2, key):
721                     if key in brrec:
722                         return brrec[key]
723                     return dict.__getitem__(self2, key)
724             globals_dict = d()
725             globals_dict['floatEqual'] = self._assert_equals
726             globals_dict['ref'] = ref
727             globals_dict['_ref'] = ref
728             for test in rec.findall('./test'):
729                 f_expr = test.get("expr",'').encode('utf-8')
730                 expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
731                 expression_value = unsafe_eval(f_expr, globals_dict)
732                 if expression_value != expected_value: # assertion failed
733                     self.assertion_report.record_failure()
734                     msg = 'assertion "%s" failed!\n'    \
735                           ' xmltag: %s\n'               \
736                           ' expected value: %r\n'       \
737                           ' obtained value: %r\n'       \
738                           % (rec_string, etree.tostring(test), expected_value, expression_value)
739                     _logger.error(msg)
740                     return
741         else: # all tests were successful for this assertion tag (no break)
742             self.assertion_report.record_success()
743
744     def _tag_record(self, cr, rec, data_node=None):
745         rec_model = rec.get("model").encode('ascii')
746         model = self.pool.get(rec_model)
747         assert model, "The model %s does not exist !" % (rec_model,)
748         rec_id = rec.get("id",'').encode('ascii')
749         rec_context = rec.get("context", None)
750         if rec_context:
751             rec_context = unsafe_eval(rec_context)
752         self._test_xml_id(rec_id)
753         if self.isnoupdate(data_node) and self.mode != 'init':
754             # check if the xml record has an id string
755             if rec_id:
756                 if '.' in rec_id:
757                     module,rec_id2 = rec_id.split('.')
758                 else:
759                     module = self.module
760                     rec_id2 = rec_id
761                 id = self.pool.get('ir.model.data')._update_dummy(cr, self.uid, rec_model, module, rec_id2)
762                 # check if the resource already existed at the last update
763                 if id:
764                     # if it existed, we don't update the data, but we need to
765                     # know the id of the existing record anyway
766                     self.idref[rec_id] = int(id)
767                     return None
768                 else:
769                     # if the resource didn't exist
770                     if not self.nodeattr2bool(rec, 'forcecreate', True):
771                         # we don't want to create it, so we skip it
772                         return None
773                     # else, we let the record to be created
774
775             else:
776                 # otherwise it is skipped
777                 return None
778         res = {}
779         for field in rec.findall('./field'):
780 #TODO: most of this code is duplicated above (in _eval_xml)...
781             f_name = field.get("name",'').encode('utf-8')
782             f_ref = field.get("ref",'').encode('utf-8')
783             f_search = field.get("search",'').encode('utf-8')
784             f_model = field.get("model",'').encode('utf-8')
785             if not f_model and model._columns.get(f_name,False):
786                 f_model = model._columns[f_name]._obj
787             f_use = field.get("use",'').encode('utf-8') or 'id'
788             f_val = False
789
790             if f_search:
791                 q = unsafe_eval(f_search, self.idref)
792                 field = []
793                 assert f_model, 'Define an attribute model="..." in your .XML file !'
794                 f_obj = self.pool.get(f_model)
795                 # browse the objects searched
796                 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
797                 # column definitions of the "local" object
798                 _cols = self.pool.get(rec_model)._columns
799                 # if the current field is many2many
800                 if (f_name in _cols) and _cols[f_name]._type=='many2many':
801                     f_val = [(6, 0, map(lambda x: x[f_use], s))]
802                 elif len(s):
803                     # otherwise (we are probably in a many2one field),
804                     # take the first element of the search
805                     f_val = s[0][f_use]
806             elif f_ref:
807                 if f_ref=="null":
808                     f_val = False
809                 else:
810                     if f_name in model._columns \
811                               and model._columns[f_name]._type == 'reference':
812                         val = self.model_id_get(cr, f_ref)
813                         f_val = val[0] + ',' + str(val[1])
814                     else:
815                         f_val = self.id_get(cr, f_ref)
816             else:
817                 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
818                 if model._columns.has_key(f_name):
819                     import openerp.osv as osv
820                     if isinstance(model._columns[f_name], osv.fields.integer):
821                         f_val = int(f_val)
822             res[f_name] = f_val
823
824         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 )
825         if rec_id:
826             self.idref[rec_id] = int(id)
827         if config.get('import_partial', False):
828             cr.commit()
829         return rec_model, id
830
831     def id_get(self, cr, id_str):
832         if id_str in self.idref:
833             return self.idref[id_str]
834         res = self.model_id_get(cr, id_str)
835         if res and len(res)>1: res = res[1]
836         return res
837
838     def model_id_get(self, cr, id_str):
839         model_data_obj = self.pool.get('ir.model.data')
840         mod = self.module
841         if '.' in id_str:
842             mod,id_str = id_str.split('.')
843         return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
844
845     def parse(self, de):
846         if not de.tag in ['terp', 'openerp']:
847             _logger.error("Mismatch xml format")
848             raise Exception( "Mismatch xml format: only terp or openerp as root tag" )
849
850         if de.tag == 'terp':
851             _logger.warning("The tag <terp/> is deprecated, use <openerp/>")
852
853         for n in de.findall('./data'):
854             for rec in n:
855                 if rec.tag in self._tags:
856                     try:
857                         self._tags[rec.tag](self.cr, rec, n)
858                     except:
859                         _logger.error('Parse error in %s:%d: \n%s',
860                                       rec.getroottree().docinfo.URL,
861                                       rec.sourceline,
862                                       etree.tostring(rec).strip(), exc_info=True)
863                         self.cr.rollback()
864                         raise
865         return True
866
867     def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
868
869         self.mode = mode
870         self.module = module
871         self.cr = cr
872         self.idref = idref
873         self.pool = pooler.get_pool(cr.dbname)
874         self.uid = 1
875         if report is None:
876             report = assertion_report.assertion_report()
877         self.assertion_report = report
878         self.noupdate = noupdate
879         self._tags = {
880             'menuitem': self._tag_menuitem,
881             'record': self._tag_record,
882             'assert': self._tag_assert,
883             'report': self._tag_report,
884             'wizard': self._tag_wizard,
885             'delete': self._tag_delete,
886             'ir_set': self._tag_ir_set,
887             'function': self._tag_function,
888             'workflow': self._tag_workflow,
889             'act_window': self._tag_act_window,
890             'url': self._tag_url
891         }
892
893 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
894         noupdate=False):
895     '''Import csv file :
896         quote: "
897         delimiter: ,
898         encoding: utf-8'''
899     if not idref:
900         idref={}
901     model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
902     #remove folder path from model
903     head, model = os.path.split(model)
904
905     pool = pooler.get_pool(cr.dbname)
906
907     input = cStringIO.StringIO(csvcontent) #FIXME
908     reader = csv.reader(input, quotechar='"', delimiter=',')
909     fields = reader.next()
910     fname_partial = ""
911     if config.get('import_partial'):
912         fname_partial = module + '/'+ fname
913         if not os.path.isfile(config.get('import_partial')):
914             pickle.dump({}, file(config.get('import_partial'),'w+'))
915         else:
916             data = pickle.load(file(config.get('import_partial')))
917             if fname_partial in data:
918                 if not data[fname_partial]:
919                     return
920                 else:
921                     for i in range(data[fname_partial]):
922                         reader.next()
923
924     if not (mode == 'init' or 'id' in fields):
925         _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
926         return
927
928     uid = 1
929     datas = []
930     for line in reader:
931         if (not line) or not reduce(lambda x,y: x or y, line) :
932             continue
933         try:
934             datas.append(map(lambda x: misc.ustr(x), line))
935         except:
936             _logger.error("Cannot import the line: %s", line)
937     result, rows, warning_msg, dummy = pool.get(model).import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
938     if result < 0:
939         # Report failed import and abort module install
940         raise Exception(_('Module loading failed: file %s/%s could not be processed:\n %s') % (module, fname, warning_msg))
941     if config.get('import_partial'):
942         data = pickle.load(file(config.get('import_partial')))
943         data[fname_partial] = 0
944         pickle.dump(data, file(config.get('import_partial'),'wb'))
945         cr.commit()
946
947 #
948 # xml import/export
949 #
950 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
951     doc = etree.parse(xmlfile)
952     relaxng = etree.RelaxNG(
953         etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
954     try:
955         relaxng.assert_(doc)
956     except Exception:
957         _logger.error('The XML file does not fit the required schema !')
958         _logger.error(misc.ustr(relaxng.error_log.last_error))
959         raise
960
961     if idref is None:
962         idref={}
963     obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
964     obj.parse(doc.getroot())
965     return True
966
967
968 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: