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