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