16d31c0a42ce6fdfeeeb20ca6ff6ebe3cb604a24
[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") and mode != "update":
811             record.append(Field(name='active', eval=el.get('active')))
812         if el.get('customize_show') in ("True", "False"):
813             record.append(Field(name='customize_show', eval=el.get('customize_show')))
814         groups = el.attrib.pop('groups', None)
815         if groups:
816             grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
817             record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
818         if el.attrib.pop('page', None) == 'True':
819             record.append(Field(name="page", eval="True"))
820         if el.get('primary') == 'True':
821             # Pseudo clone mode, we'll set the t-name to the full canonical xmlid
822             el.append(
823                 builder.E.xpath(
824                     builder.E.attribute(full_tpl_id, name='t-name'),
825                     expr=".",
826                     position="attributes",
827                 )
828             )
829             record.append(Field('primary', name='mode'))
830         # inject complete <template> element (after changing node name) into
831         # the ``arch`` field
832         record.append(Field(el, name="arch", type="xml"))
833
834         return self._tag_record(cr, record, data_node)
835
836     def id_get(self, cr, id_str):
837         if id_str in self.idref:
838             return self.idref[id_str]
839         res = self.model_id_get(cr, id_str)
840         if res and len(res)>1: res = res[1]
841         return res
842
843     def model_id_get(self, cr, id_str):
844         model_data_obj = self.pool['ir.model.data']
845         mod = self.module
846         if '.' in id_str:
847             mod,id_str = id_str.split('.')
848         return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
849
850     def parse(self, de, mode=None):
851         if de.tag != 'openerp':
852             raise Exception("Mismatch xml format: root tag must be `openerp`.")
853
854         for n in de.findall('./data'):
855             for rec in n:
856                 if rec.tag in self._tags:
857                     try:
858                         self._tags[rec.tag](self.cr, rec, n, mode=mode)
859                     except Exception, e:
860                         self.cr.rollback()
861                         exc_info = sys.exc_info()
862                         raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
863         return True
864
865     def __init__(self, cr, module, idref, mode, report=None, noupdate=False, xml_filename=None):
866
867         self.mode = mode
868         self.module = module
869         self.cr = cr
870         self.idref = idref
871         self.pool = openerp.registry(cr.dbname)
872         self.uid = 1
873         if report is None:
874             report = assertion_report.assertion_report()
875         self.assertion_report = report
876         self.noupdate = noupdate
877         self.xml_filename = xml_filename
878         self._tags = {
879             'record': self._tag_record,
880             'delete': self._tag_delete,
881             'function': self._tag_function,
882             'menuitem': self._tag_menuitem,
883             'template': self._tag_template,
884             'workflow': self._tag_workflow,
885             'report': self._tag_report,
886
887             'ir_set': self._tag_ir_set,
888             'act_window': self._tag_act_window,
889             'url': self._tag_url,
890             'assert': self._tag_assert,
891         }
892
893 def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None, pathname=None):
894     if pathname is None:
895         pathname = os.path.join(module, filename)
896     fp = misc.file_open(pathname)
897     ext = os.path.splitext(filename)[1].lower()
898
899     try:
900         if ext == '.csv':
901             convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
902         elif ext == '.sql':
903             convert_sql_import(cr, fp)
904         elif ext == '.yml':
905             convert_yaml_import(cr, module, fp, kind, idref, mode, noupdate, report)
906         elif ext == '.xml':
907             convert_xml_import(cr, module, fp, idref, mode, noupdate, report)
908         elif ext == '.js':
909             pass # .js files are valid but ignored here.
910         else:
911             _logger.warning("Can't load unknown file type %s.", filename)
912     finally:
913         fp.close()
914
915 def convert_sql_import(cr, fp):
916     queries = fp.read().split(';')
917     for query in queries:
918         new_query = ' '.join(query.split())
919         if new_query:
920             cr.execute(new_query)
921
922 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
923         noupdate=False):
924     '''Import csv file :
925         quote: "
926         delimiter: ,
927         encoding: utf-8'''
928     if not idref:
929         idref={}
930     model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
931     #remove folder path from model
932     head, model = os.path.split(model)
933
934     input = cStringIO.StringIO(csvcontent) #FIXME
935     reader = csv.reader(input, quotechar='"', delimiter=',')
936     fields = reader.next()
937     fname_partial = ""
938     if config.get('import_partial'):
939         fname_partial = module + '/'+ fname
940         if not os.path.isfile(config.get('import_partial')):
941             pickle.dump({}, file(config.get('import_partial'),'w+'))
942         else:
943             data = pickle.load(file(config.get('import_partial')))
944             if fname_partial in data:
945                 if not data[fname_partial]:
946                     return
947                 else:
948                     for i in range(data[fname_partial]):
949                         reader.next()
950
951     if not (mode == 'init' or 'id' in fields):
952         _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
953         return
954
955     uid = 1
956     datas = []
957     for line in reader:
958         if not (line and any(line)):
959             continue
960         try:
961             datas.append(map(misc.ustr, line))
962         except:
963             _logger.error("Cannot import the line: %s", line)
964
965     registry = openerp.registry(cr.dbname)
966     result, rows, warning_msg, dummy = registry[model].import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
967     if result < 0:
968         # Report failed import and abort module install
969         raise Exception(_('Module loading %s failed: file %s could not be processed:\n %s') % (module, fname, warning_msg))
970     if config.get('import_partial'):
971         data = pickle.load(file(config.get('import_partial')))
972         data[fname_partial] = 0
973         pickle.dump(data, file(config.get('import_partial'),'wb'))
974         cr.commit()
975
976 #
977 # xml import/export
978 #
979 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
980     doc = etree.parse(xmlfile)
981     relaxng = etree.RelaxNG(
982         etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
983     try:
984         relaxng.assert_(doc)
985     except Exception:
986         _logger.error('The XML file does not fit the required schema !')
987         _logger.error(misc.ustr(relaxng.error_log.last_error))
988         raise
989
990     if idref is None:
991         idref={}
992     if isinstance(xmlfile, file):
993         xml_filename = xmlfile.name
994     else:
995         xml_filename = xmlfile
996     obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate, xml_filename=xml_filename)
997     obj.parse(doc.getroot(), mode=mode)
998     return True
999
1000
1001 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: