1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
31 import openerp.release as release
35 logging.getLogger("init").warning('could not find pytz library, please install it')
36 class pytzclass(object):
41 from datetime import datetime, timedelta
42 from lxml import etree
44 import openerp.loglevels as loglevels
45 import openerp.pooler as pooler
46 from config import config
47 from translate import _
49 # List of etree._Element subclasses that we choose to ignore when parsing XML.
50 from misc import SKIPPED_ELEMENT_TYPES
52 from misc import unquote
54 # Import of XML records requires the unsafe eval as well,
55 # almost everywhere, which is ok because it supposedly comes
56 # from trusted data, but at least we make it obvious now.
58 from safe_eval import safe_eval as eval
60 class ConvertError(Exception):
61 def __init__(self, doc, orig_excpt):
63 self.orig = orig_excpt
66 return 'Exception:\n\t%s\nUsing file:\n%s' % (self.orig, self.d)
69 return lambda x: self.id_get(cr, x)
71 def _obj(pool, cr, uid, model_str, context=None):
72 model = pool.get(model_str)
73 return lambda x: model.browse(cr, uid, x, context=context)
75 def _get_idref(self, cr, uid, model_str, context, idref):
80 version=release.major_version,
84 idref2['obj'] = _obj(self.pool, cr, uid, model_str, context=context)
87 def _fix_multiple_roots(node):
89 Surround the children of the ``node`` element of an XML field with a
90 single root "data" element, to prevent having a document with multiple
91 roots once parsed separately.
93 XML nodes should have one root only, but we'd like to support
94 direct multiple roots in our partial documents (like inherited view architectures).
95 As a convention we'll surround multiple root with a container "data" element, to be
96 ignored later when parsing.
98 real_nodes = [x for x in node if not isinstance(x, SKIPPED_ELEMENT_TYPES)]
99 if len(real_nodes) > 1:
100 data_node = etree.Element("data")
102 data_node.append(child)
103 node.append(data_node)
105 def _eval_xml(self, node, pool, cr, uid, idref, context=None):
108 if node.tag in ('field','value'):
109 t = node.get('type','char')
110 f_model = node.get('model', '').encode('utf-8')
111 if node.get('search'):
112 f_search = node.get("search",'').encode('utf-8')
113 f_use = node.get("use",'id').encode('utf-8')
114 f_name = node.get("name",'').encode('utf-8')
117 idref2 = _get_idref(self, cr, uid, f_model, context, idref)
118 q = unsafe_eval(f_search, idref2)
119 ids = pool.get(f_model).search(cr, uid, q)
121 ids = map(lambda x: x[f_use], pool.get(f_model).read(cr, uid, ids, [f_use]))
122 _cols = pool.get(f_model)._columns
123 if (f_name in _cols) and _cols[f_name]._type=='many2many':
128 if isinstance(f_val, tuple):
131 a_eval = node.get('eval','')
134 idref2 = _get_idref(self, cr, uid, f_model, context, idref)
136 return unsafe_eval(a_eval, idref2)
138 logger = logging.getLogger('init')
139 logger.warning('could not eval(%s) for %s in %s' % (a_eval, node.get('name'), context), exc_info=True)
142 def _process(s, idref):
143 m = re.findall('[^%]%\((.*?)\)[ds]', s)
146 idref[id]=self.id_get(cr, id)
148 _fix_multiple_roots(node)
149 return '<?xml version="1.0"?>\n'\
150 +_process("".join([etree.tostring(n, encoding='utf-8')
153 if t in ('char', 'int', 'float'):
160 return int(d.strip())
162 return float(d.strip())
164 elif t in ('list','tuple'):
166 for n in node.findall('./value'):
167 res.append(_eval_xml(self,n,pool,cr,uid,idref))
171 elif node.tag == "getitem":
173 res=_eval_xml(self,n,pool,cr,uid,idref)
176 elif node.get('type') in ("int", "list"):
177 return res[int(node.get('index'))]
179 return res[node.get('index','').encode("utf8")]
180 elif node.tag == "function":
182 a_eval = node.get('eval','')
184 idref['ref'] = lambda x: self.id_get(cr, x)
185 args = unsafe_eval(a_eval, idref)
187 return_val = _eval_xml(self,n, pool, cr, uid, idref, context)
188 if return_val is not None:
189 args.append(return_val)
190 model = pool.get(node.get('model',''))
191 method = node.get('name','')
192 res = getattr(model, method)(cr, uid, *args)
194 elif node.tag == "test":
197 escape_re = re.compile(r'(?<!\\)/')
199 return x.replace('\\/', '/')
201 class assertion_report(object):
205 def record_assertion(self, success, severity):
207 Records the result of an assertion for the failed/success count
210 if severity in self._report:
211 self._report[severity][success] += 1
213 self._report[severity] = {success:1, not success: 0}
216 def get_report(self):
220 res = '\nAssertions report:\nLevel\tsuccess\tfailed\n'
222 for sev in self._report:
223 res += sev + '\t' + str(self._report[sev][True]) + '\t' + str(self._report[sev][False]) + '\n'
224 success += self._report[sev][True]
225 failed += self._report[sev][False]
226 res += 'total\t' + str(success) + '\t' + str(failed) + '\n'
227 res += 'end of report (' + str(success + failed) + ' assertion(s) checked)'
230 class xml_import(object):
231 __logger = logging.getLogger('tools.convert.xml_import')
233 def nodeattr2bool(node, attr, default=False):
234 if not node.get(attr):
236 val = node.get(attr).strip()
239 return val.lower() not in ('0', 'false', 'off')
241 def isnoupdate(self, data_node=None):
242 return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False))
244 def get_context(self, data_node, node, eval_dict):
245 data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
246 node_context = node.get("context",'').encode('utf8')
248 for ctx in (data_node_context, node_context):
251 ctx_res = unsafe_eval(ctx, eval_dict)
252 if isinstance(context, dict):
253 context.update(ctx_res)
257 # Some contexts contain references that are only valid at runtime at
258 # client-side, so in that case we keep the original context string
259 # as it is. We also log it, just in case.
261 logging.getLogger("init").debug('Context value (%s) for element with id "%s" or its data node does not parse '\
262 'at server-side, keeping original string, in case it\'s meant for client side only',
263 ctx, node.get('id','n/a'), exc_info=True)
266 def get_uid(self, cr, uid, data_node, node):
267 node_uid = node.get('uid','') or (len(data_node) and data_node.get('uid',''))
269 return self.id_get(cr, node_uid)
272 def _test_xml_id(self, xml_id):
275 module, id = xml_id.split('.', 1)
276 assert '.' not in id, """The ID reference "%s" must contain
277 maximum one dot. They are used to refer to other modules ID, in the
278 form: module.record_id""" % (xml_id,)
279 if module != self.module:
280 modcnt = self.pool.get('ir.module.module').search_count(self.cr, self.uid, ['&', ('name', '=', module), ('state', 'in', ['installed'])])
281 assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
284 self.logger.error('id: %s is to long (max: 64)', id)
286 def _tag_delete(self, cr, rec, data_node=None):
287 d_model = rec.get("model",'')
288 d_search = rec.get("search",'').encode('utf-8')
289 d_id = rec.get("id",'')
293 idref = _get_idref(self, cr, self.uid, d_model, context={}, idref={})
294 ids = self.pool.get(d_model).search(cr, self.uid, unsafe_eval(d_search, idref))
297 ids.append(self.id_get(cr, d_id))
299 # d_id cannot be found. doesn't matter in this case
302 self.pool.get(d_model).unlink(cr, self.uid, ids)
304 def _remove_ir_values(self, cr, name, value, model):
305 ir_values_obj = self.pool.get('ir.values')
306 ir_value_ids = ir_values_obj.search(cr, self.uid, [('name','=',name),('value','=',value),('model','=',model)])
308 ir_values_obj.unlink(cr, self.uid, ir_value_ids)
312 def _tag_report(self, cr, rec, data_node=None):
314 for dest,f in (('name','string'),('model','model'),('report_name','name')):
315 res[dest] = rec.get(f,'').encode('utf8')
316 assert res[dest], "Attribute %s of report is empty !" % (f,)
317 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),
318 ('attachment','attachment'),('attachment_use','attachment_use'), ('usage','usage')):
320 res[dest] = rec.get(field).encode('utf8')
322 res['auto'] = eval(rec.get('auto','False'))
324 sxw_content = misc.file_open(rec.get('sxw')).read()
325 res['report_sxw_content'] = sxw_content
326 if rec.get('header'):
327 res['header'] = eval(rec.get('header','False'))
328 if rec.get('report_type'):
329 res['report_type'] = rec.get('report_type')
331 res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
333 xml_id = rec.get('id','').encode('utf8')
334 self._test_xml_id(xml_id)
336 if rec.get('groups'):
337 g_names = rec.get('groups','').split(',')
339 for group in g_names:
340 if group.startswith('-'):
341 group_id = self.id_get(cr, group[1:])
342 groups_value.append((3, group_id))
344 group_id = self.id_get(cr, group)
345 groups_value.append((4, group_id))
346 res['groups_id'] = groups_value
348 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)
349 self.idref[xml_id] = int(id)
351 if not rec.get('menu') or eval(rec.get('menu','False')):
352 keyword = str(rec.get('keyword', 'client_print_multi'))
353 value = 'ir.actions.report.xml,'+str(id)
354 replace = rec.get('replace', True)
355 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)
356 elif self.mode=='update' and eval(rec.get('menu','False'))==False:
357 # Special check for report having attribute menu=False on update
358 value = 'ir.actions.report.xml,'+str(id)
359 self._remove_ir_values(cr, res['name'], value, res['model'])
362 def _tag_function(self, cr, rec, data_node=None):
363 if self.isnoupdate(data_node) and self.mode != 'init':
365 context = self.get_context(data_node, rec, {'ref': _ref(self, cr)})
366 uid = self.get_uid(cr, self.uid, data_node, rec)
367 _eval_xml(self,rec, self.pool, cr, uid, self.idref, context=context)
370 def _tag_wizard(self, cr, rec, data_node=None):
371 string = rec.get("string",'').encode('utf8')
372 model = rec.get("model",'').encode('utf8')
373 name = rec.get("name",'').encode('utf8')
374 xml_id = rec.get('id','').encode('utf8')
375 self._test_xml_id(xml_id)
376 multi = rec.get('multi','') and eval(rec.get('multi','False'))
377 res = {'name': string, 'wiz_name': name, 'multi': multi, 'model': model}
379 if rec.get('groups'):
380 g_names = rec.get('groups','').split(',')
382 for group in g_names:
383 if group.startswith('-'):
384 group_id = self.id_get(cr, group[1:])
385 groups_value.append((3, group_id))
387 group_id = self.id_get(cr, group)
388 groups_value.append((4, group_id))
389 res['groups_id'] = groups_value
391 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)
392 self.idref[xml_id] = int(id)
394 if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
395 keyword = str(rec.get('keyword','') or 'client_action_multi')
396 value = 'ir.actions.wizard,'+str(id)
397 replace = rec.get("replace",'') or True
398 self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, string, [model], value, replace=replace, isobject=True, xml_id=xml_id)
399 elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
400 # Special check for wizard having attribute menu=False on update
401 value = 'ir.actions.wizard,'+str(id)
402 self._remove_ir_values(cr, string, value, model)
404 def _tag_url(self, cr, rec, data_node=None):
405 url = rec.get("url",'').encode('utf8')
406 target = rec.get("target",'').encode('utf8')
407 name = rec.get("name",'').encode('utf8')
408 xml_id = rec.get('id','').encode('utf8')
409 self._test_xml_id(xml_id)
411 res = {'name': name, 'url': url, 'target':target}
413 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)
414 self.idref[xml_id] = int(id)
416 if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
417 keyword = str(rec.get('keyword','') or 'client_action_multi')
418 value = 'ir.actions.url,'+str(id)
419 replace = rec.get("replace",'') or True
420 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)
421 elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
422 # Special check for URL having attribute menu=False on update
423 value = 'ir.actions.url,'+str(id)
424 self._remove_ir_values(cr, url, value, "ir.actions.url")
426 def _tag_act_window(self, cr, rec, data_node=None):
427 name = rec.get('name','').encode('utf-8')
428 xml_id = rec.get('id','').encode('utf8')
429 self._test_xml_id(xml_id)
430 type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
432 if rec.get('view_id'):
433 view_id = self.id_get(cr, rec.get('view_id','').encode('utf-8'))
434 domain = rec.get('domain','').encode('utf-8') or '[]'
435 res_model = rec.get('res_model','').encode('utf-8')
436 src_model = rec.get('src_model','').encode('utf-8')
437 view_type = rec.get('view_type','').encode('utf-8') or 'form'
438 view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
439 usage = rec.get('usage','').encode('utf-8')
440 limit = rec.get('limit','').encode('utf-8')
441 auto_refresh = rec.get('auto_refresh','').encode('utf-8')
444 # Act_window's 'domain' and 'context' contain mostly literals
445 # but they can also refer to the variables provided below
446 # in eval_context, so we need to eval() them before storing.
447 # Among the context variables, 'active_id' refers to
448 # the currently selected items in a list view, and only
449 # takes meaning at runtime on the client side. For this
450 # reason it must remain a bare variable in domain and context,
451 # even after eval() at server-side. We use the special 'unquote'
452 # class to achieve this effect: a string which has itself, unquoted,
454 active_id = unquote("active_id")
455 active_ids = unquote("active_ids")
456 active_model = unquote("active_model")
459 return self.id_get(cr, str_id)
461 # Include all locals() in eval_context, for backwards compatibility
468 'res_model': res_model,
469 'src_model': src_model,
470 'view_type': view_type,
471 'view_mode': view_mode,
474 'auto_refresh': auto_refresh,
476 'active_id': active_id,
477 'active_ids': active_ids,
478 'active_model': active_model,
481 context = self.get_context(data_node, rec, eval_context)
484 domain = unsafe_eval(domain, eval_context)
486 # Some domains contain references that are only valid at runtime at
487 # client-side, so in that case we keep the original domain string
488 # as it is. We also log it, just in case.
489 logging.getLogger("init").debug('Domain value (%s) for element with id "%s" does not parse '\
490 'at server-side, keeping original string, in case it\'s meant for client side only',
491 domain, xml_id or 'n/a', exc_info=True)
498 'res_model': res_model,
499 'src_model': src_model,
500 'view_type': view_type,
501 'view_mode': view_mode,
504 'auto_refresh': auto_refresh,
507 if rec.get('groups'):
508 g_names = rec.get('groups','').split(',')
510 for group in g_names:
511 if group.startswith('-'):
512 group_id = self.id_get(cr, group[1:])
513 groups_value.append((3, group_id))
515 group_id = self.id_get(cr, group)
516 groups_value.append((4, group_id))
517 res['groups_id'] = groups_value
519 if rec.get('target'):
520 res['target'] = rec.get('target','')
522 res['multi'] = rec.get('multi', False)
523 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)
524 self.idref[xml_id] = int(id)
527 #keyword = 'client_action_relate'
528 keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
529 value = 'ir.actions.act_window,'+str(id)
530 replace = rec.get('replace','') or True
531 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)
532 # TODO add remove ir.model.data
534 def _tag_ir_set(self, cr, rec, data_node=None):
535 if self.mode != 'init':
538 for field in rec.findall('./field'):
539 f_name = field.get("name",'').encode('utf-8')
540 f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
542 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))
544 def _tag_workflow(self, cr, rec, data_node=None):
545 if self.isnoupdate(data_node) and self.mode != 'init':
547 model = str(rec.get('model',''))
548 w_ref = rec.get('ref','')
550 id = self.id_get(cr, w_ref)
552 number_children = len(rec)
553 assert number_children > 0,\
554 'You must define a child node if you dont give a ref'
555 assert number_children == 1,\
556 'Only one child node is accepted (%d given)' % number_children
557 id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
559 uid = self.get_uid(cr, self.uid, data_node, rec)
560 import openerp.netsvc as netsvc
561 wf_service = netsvc.LocalService("workflow")
562 wf_service.trg_validate(uid, model,
564 str(rec.get('action','')), cr)
567 # Support two types of notation:
568 # name="Inventory Control/Sending Goods"
573 def _tag_menuitem(self, cr, rec, data_node=None):
574 rec_id = rec.get("id",'').encode('ascii')
575 self._test_xml_id(rec_id)
576 m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
578 values = {'parent_id': False}
579 if rec.get('parent', False) is False and len(m_l) > 1:
580 # No parent attribute specified and the menu name has several menu components,
581 # try to determine the ID of the parent according to menu path
584 values['name'] = m_l[-1]
585 m_l = m_l[:-1] # last part is our name, not a parent
586 for idx, menu_elem in enumerate(m_l):
588 cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
590 cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
595 # the menuitem does't exist but we are in branch (not a leaf)
596 self.logger.warning('Warning no ID for submenu %s of menu %s !', menu_elem, str(m_l))
597 pid = self.pool.get('ir.ui.menu').create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
598 values['parent_id'] = pid
600 # The parent attribute was specified, if non-empty determine its ID, otherwise
601 # explicitly make a top-level menu
602 if rec.get('parent'):
603 menu_parent_id = self.id_get(cr, rec.get('parent',''))
605 # we get here with <menuitem parent="">, explicit clear of parent, or
606 # if no parent attribute at all but menu name is not a menu path
607 menu_parent_id = False
608 values = {'parent_id': menu_parent_id}
610 values['name'] = rec.get('name')
612 res = [ self.id_get(cr, rec.get('id','')) ]
616 if rec.get('action'):
617 a_action = rec.get('action','').encode('utf8')
618 a_type = rec.get('type','').encode('utf8') or 'act_window'
620 "act_window": 'STOCK_NEW',
621 "report.xml": 'STOCK_PASTE',
622 "wizard": 'STOCK_EXECUTE',
623 "url": 'STOCK_JUMP_TO'
625 values['icon'] = icons.get(a_type,'STOCK_NEW')
626 if a_type=='act_window':
627 a_id = self.id_get(cr, a_action)
628 cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
629 rrres = cr.fetchone()
630 assert rrres, "No window action defined for this id %s !\n" \
631 "Verify that this is a window action or add a type argument." % (a_action,)
632 action_type,action_mode,action_name,view_id,target = rrres
634 cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (int(view_id),))
635 action_mode, = cr.fetchone()
636 cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
638 action_mode, = cr.fetchone()
639 if action_type=='tree':
640 values['icon'] = 'STOCK_INDENT'
641 elif action_mode and action_mode.startswith('tree'):
642 values['icon'] = 'STOCK_JUSTIFY_FILL'
643 elif action_mode and action_mode.startswith('graph'):
644 values['icon'] = 'terp-graph'
645 elif action_mode and action_mode.startswith('calendar'):
646 values['icon'] = 'terp-calendar'
648 values['icon'] = 'STOCK_EXECUTE'
649 if not values.get('name', False):
650 values['name'] = action_name
651 elif a_type=='wizard':
652 a_id = self.id_get(cr, a_action)
653 cr.execute('select name from ir_act_wizard where id=%s', (int(a_id),))
655 if (not values.get('name', False)) and resw:
656 values['name'] = resw[0]
658 a_id = self.id_get(cr, a_action)
659 cr.execute('select name from ir_act_url where id=%s', (int(a_id),))
661 if (not values.get('name')) and resw:
662 values['name'] = resw[0]
663 if rec.get('sequence'):
664 values['sequence'] = int(rec.get('sequence'))
666 values['icon'] = str(rec.get('icon'))
667 if rec.get('web_icon'):
668 values['web_icon'] = "%s,%s" %(self.module, str(rec.get('web_icon')))
669 if rec.get('web_icon_hover'):
670 values['web_icon_hover'] = "%s,%s" %(self.module, str(rec.get('web_icon_hover')))
672 if rec.get('groups'):
673 g_names = rec.get('groups','').split(',')
675 for group in g_names:
676 if group.startswith('-'):
677 group_id = self.id_get(cr, group[1:])
678 groups_value.append((3, group_id))
680 group_id = self.id_get(cr, group)
681 groups_value.append((4, group_id))
682 values['groups_id'] = groups_value
684 xml_id = rec.get('id','').encode('utf8')
685 self._test_xml_id(xml_id)
686 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)
689 self.idref[rec_id] = int(pid)
691 if rec.get('action') and pid:
692 a_action = rec.get('action').encode('utf8')
693 a_type = rec.get('type','').encode('utf8') or 'act_window'
694 a_id = self.id_get(cr, a_action)
695 action = "ir.actions.%s,%d" % (a_type, a_id)
696 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)
697 return ('ir.ui.menu', pid)
699 def _assert_equals(self, f1, f2, prec=4):
700 return not round(f1 - f2, prec)
702 def _tag_assert(self, cr, rec, data_node=None):
703 if self.isnoupdate(data_node) and self.mode != 'init':
706 rec_model = rec.get("model",'').encode('ascii')
707 model = self.pool.get(rec_model)
708 assert model, "The model %s does not exist !" % (rec_model,)
709 rec_id = rec.get("id",'').encode('ascii')
710 self._test_xml_id(rec_id)
711 rec_src = rec.get("search",'').encode('utf8')
712 rec_src_count = rec.get("count")
714 severity = rec.get("severity",'').encode('ascii') or loglevels.LOG_ERROR
715 rec_string = rec.get("string",'').encode('utf8') or 'unknown'
718 eval_dict = {'ref': _ref(self, cr)}
719 context = self.get_context(data_node, rec, eval_dict)
720 uid = self.get_uid(cr, self.uid, data_node, rec)
722 ids = [self.id_get(cr, rec_id)]
724 q = unsafe_eval(rec_src, eval_dict)
725 ids = self.pool.get(rec_model).search(cr, uid, q, context=context)
727 count = int(rec_src_count)
728 if len(ids) != count:
729 self.assert_report.record_assertion(False, severity)
730 msg = 'assertion "%s" failed!\n' \
731 ' Incorrect search count:\n' \
732 ' expected count: %d\n' \
733 ' obtained count: %d\n' \
734 % (rec_string, count, len(ids))
735 sevval = getattr(logging, severity.upper())
736 self.logger.log(sevval, msg)
737 if sevval >= config['assert_exit_level']:
738 # TODO: define a dedicated exception
739 raise Exception('Severe assertion failure')
742 assert ids is not None,\
743 'You must give either an id or a search criteria'
746 brrec = model.browse(cr, uid, id, context)
748 def __getitem__(self2, key):
751 return dict.__getitem__(self2, key)
753 globals_dict['floatEqual'] = self._assert_equals
754 globals_dict['ref'] = ref
755 globals_dict['_ref'] = ref
756 for test in rec.findall('./test'):
757 f_expr = test.get("expr",'').encode('utf-8')
758 expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
759 expression_value = unsafe_eval(f_expr, globals_dict)
760 if expression_value != expected_value: # assertion failed
761 self.assert_report.record_assertion(False, severity)
762 msg = 'assertion "%s" failed!\n' \
764 ' expected value: %r\n' \
765 ' obtained value: %r\n' \
766 % (rec_string, etree.tostring(test), expected_value, expression_value)
767 sevval = getattr(logging, severity.upper())
768 self.logger.log(sevval, msg)
769 if sevval >= config['assert_exit_level']:
770 # TODO: define a dedicated exception
771 raise Exception('Severe assertion failure')
773 else: # all tests were successful for this assertion tag (no break)
774 self.assert_report.record_assertion(True, severity)
776 def _tag_record(self, cr, rec, data_node=None):
777 rec_model = rec.get("model").encode('ascii')
778 model = self.pool.get(rec_model)
779 assert model, "The model %s does not exist !" % (rec_model,)
780 rec_id = rec.get("id",'').encode('ascii')
781 rec_context = rec.get("context", None)
783 rec_context = unsafe_eval(rec_context)
784 self._test_xml_id(rec_id)
785 if self.isnoupdate(data_node) and self.mode != 'init':
786 # check if the xml record has an id string
789 module,rec_id2 = rec_id.split('.')
793 id = self.pool.get('ir.model.data')._update_dummy(cr, self.uid, rec_model, module, rec_id2)
794 # check if the resource already existed at the last update
796 # if it existed, we don't update the data, but we need to
797 # know the id of the existing record anyway
798 self.idref[rec_id] = int(id)
801 # if the resource didn't exist
802 if not self.nodeattr2bool(rec, 'forcecreate', True):
803 # we don't want to create it, so we skip it
805 # else, we let the record to be created
808 # otherwise it is skipped
811 for field in rec.findall('./field'):
812 #TODO: most of this code is duplicated above (in _eval_xml)...
813 f_name = field.get("name",'').encode('utf-8')
814 f_ref = field.get("ref",'').encode('utf-8')
815 f_search = field.get("search",'').encode('utf-8')
816 f_model = field.get("model",'').encode('utf-8')
817 if not f_model and model._columns.get(f_name,False):
818 f_model = model._columns[f_name]._obj
819 f_use = field.get("use",'').encode('utf-8') or 'id'
823 q = unsafe_eval(f_search, self.idref)
825 assert f_model, 'Define an attribute model="..." in your .XML file !'
826 f_obj = self.pool.get(f_model)
827 # browse the objects searched
828 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
829 # column definitions of the "local" object
830 _cols = self.pool.get(rec_model)._columns
831 # if the current field is many2many
832 if (f_name in _cols) and _cols[f_name]._type=='many2many':
833 f_val = [(6, 0, map(lambda x: x[f_use], s))]
835 # otherwise (we are probably in a many2one field),
836 # take the first element of the search
842 if f_name in model._columns \
843 and model._columns[f_name]._type == 'reference':
844 val = self.model_id_get(cr, f_ref)
845 f_val = val[0] + ',' + str(val[1])
847 f_val = self.id_get(cr, f_ref)
849 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
850 if model._columns.has_key(f_name):
851 import openerp.osv as osv
852 if isinstance(model._columns[f_name], osv.fields.integer):
856 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 )
858 self.idref[rec_id] = int(id)
859 if config.get('import_partial', False):
863 def id_get(self, cr, id_str):
864 if id_str in self.idref:
865 return self.idref[id_str]
866 res = self.model_id_get(cr, id_str)
867 if res and len(res)>1: res = res[1]
870 def model_id_get(self, cr, id_str):
871 model_data_obj = self.pool.get('ir.model.data')
874 mod,id_str = id_str.split('.')
875 return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
878 if not de.tag in ['terp', 'openerp']:
879 self.logger.error("Mismatch xml format")
880 raise Exception( "Mismatch xml format: only terp or openerp as root tag" )
883 self.logger.warning("The tag <terp/> is deprecated, use <openerp/>")
885 for n in de.findall('./data'):
887 if rec.tag in self._tags:
889 self._tags[rec.tag](self.cr, rec, n)
891 self.__logger.error('Parse error in %s:%d: \n%s',
892 rec.getroottree().docinfo.URL,
894 etree.tostring(rec).strip(), exc_info=True)
899 def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
901 self.logger = logging.getLogger('init')
906 self.pool = pooler.get_pool(cr.dbname)
909 report = assertion_report()
910 self.assert_report = report
911 self.noupdate = noupdate
913 'menuitem': self._tag_menuitem,
914 'record': self._tag_record,
915 'assert': self._tag_assert,
916 'report': self._tag_report,
917 'wizard': self._tag_wizard,
918 'delete': self._tag_delete,
919 'ir_set': self._tag_ir_set,
920 'function': self._tag_function,
921 'workflow': self._tag_workflow,
922 'act_window': self._tag_act_window,
926 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
934 logger = logging.getLogger('init')
935 model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
936 #remove folder path from model
937 head, model = os.path.split(model)
939 pool = pooler.get_pool(cr.dbname)
941 input = cStringIO.StringIO(csvcontent) #FIXME
942 reader = csv.reader(input, quotechar='"', delimiter=',')
943 fields = reader.next()
945 if config.get('import_partial'):
946 fname_partial = module + '/'+ fname
947 if not os.path.isfile(config.get('import_partial')):
948 pickle.dump({}, file(config.get('import_partial'),'w+'))
950 data = pickle.load(file(config.get('import_partial')))
951 if fname_partial in data:
952 if not data[fname_partial]:
955 for i in range(data[fname_partial]):
958 if not (mode == 'init' or 'id' in fields):
959 logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
965 if (not line) or not reduce(lambda x,y: x or y, line) :
968 datas.append(map(lambda x: misc.ustr(x), line))
970 logger.error("Cannot import the line: %s", line)
971 result, rows, warning_msg, dummy = pool.get(model).import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
973 # Report failed import and abort module install
974 raise Exception(_('Module loading failed: file %s/%s could not be processed:\n %s') % (module, fname, warning_msg))
975 if config.get('import_partial'):
976 data = pickle.load(file(config.get('import_partial')))
977 data[fname_partial] = 0
978 pickle.dump(data, file(config.get('import_partial'),'wb'))
984 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
985 doc = etree.parse(xmlfile)
986 relaxng = etree.RelaxNG(
987 etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
991 logger = loglevels.Logger()
992 logger.notifyChannel('init', loglevels.LOG_ERROR, 'The XML file does not fit the required schema !')
993 logger.notifyChannel('init', loglevels.LOG_ERROR, misc.ustr(relaxng.error_log.last_error))
998 obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
999 obj.parse(doc.getroot())