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 ##############################################################################
34 import openerp.release
35 import openerp.workflow
36 from yaml_import import convert_yaml_import
38 import assertion_report
40 _logger = logging.getLogger(__name__)
45 _logger.warning('could not find pytz library, please install it')
46 class pytzclass(object):
51 from datetime import datetime, timedelta
52 from dateutil.relativedelta import relativedelta
53 from lxml import etree, builder
55 from config import config
56 from translate import _
58 # List of etree._Element subclasses that we choose to ignore when parsing XML.
59 from misc import SKIPPED_ELEMENT_TYPES
61 from misc import unquote
63 # Import of XML records requires the unsafe eval as well,
64 # almost everywhere, which is ok because it supposedly comes
65 # from trusted data, but at least we make it obvious now.
67 from safe_eval import safe_eval as eval
69 class ParseError(Exception):
70 def __init__(self, msg, text, filename, lineno):
73 self.filename = filename
77 return '"%s" while parsing %s:%s, near\n%s' \
78 % (self.msg, self.filename, self.lineno, self.text)
81 return lambda x: self.id_get(cr, x)
83 def _obj(pool, cr, uid, model_str, context=None):
84 model = pool[model_str]
85 return lambda x: model.browse(cr, uid, x, context=context)
87 def _get_idref(self, cr, uid, model_str, context, idref):
93 relativedelta=relativedelta,
94 version=openerp.release.major_version,
98 idref2['obj'] = _obj(self.pool, cr, uid, model_str, context=context)
101 def _fix_multiple_roots(node):
103 Surround the children of the ``node`` element of an XML field with a
104 single root "data" element, to prevent having a document with multiple
105 roots once parsed separately.
107 XML nodes should have one root only, but we'd like to support
108 direct multiple roots in our partial documents (like inherited view architectures).
109 As a convention we'll surround multiple root with a container "data" element, to be
110 ignored later when parsing.
112 real_nodes = [x for x in node if not isinstance(x, SKIPPED_ELEMENT_TYPES)]
113 if len(real_nodes) > 1:
114 data_node = etree.Element("data")
116 data_node.append(child)
117 node.append(data_node)
119 def _eval_xml(self, node, pool, cr, uid, idref, context=None):
122 if node.tag in ('field','value'):
123 t = node.get('type','char')
124 f_model = node.get('model', '').encode('utf-8')
125 if node.get('search'):
126 f_search = node.get("search",'').encode('utf-8')
127 f_use = node.get("use",'id').encode('utf-8')
128 f_name = node.get("name",'').encode('utf-8')
131 idref2 = _get_idref(self, cr, uid, f_model, context, idref)
132 q = unsafe_eval(f_search, idref2)
133 ids = pool[f_model].search(cr, uid, q)
135 ids = map(lambda x: x[f_use], pool[f_model].read(cr, uid, ids, [f_use]))
136 _cols = pool[f_model]._columns
137 if (f_name in _cols) and _cols[f_name]._type=='many2many':
142 if isinstance(f_val, tuple):
145 a_eval = node.get('eval','')
147 idref2 = _get_idref(self, cr, uid, f_model, context, idref)
149 return unsafe_eval(a_eval, idref2)
151 logging.getLogger('openerp.tools.convert.init').error(
152 'Could not eval(%s) for %s in %s', a_eval, node.get('name'), context)
154 def _process(s, idref):
155 matches = re.finditer('[^%]%\((.*?)\)[ds]', s)
158 found = m.group()[1:]
164 idref[id] = self.id_get(cr, id)
165 s = s.replace(found, str(idref[id]))
167 s = s.replace('%%', '%') # Quite wierd but it's for (somewhat) backward compatibility sake
172 _fix_multiple_roots(node)
173 return '<?xml version="1.0"?>\n'\
174 +_process("".join([etree.tostring(n, encoding='utf-8')
175 for n in node]), idref)
177 return _process("".join([etree.tostring(n, encoding='utf-8')
178 for n in node]), idref)
182 with openerp.tools.file_open(node.get('file'), 'rb') as f:
186 from ..modules import module
188 if not module.get_module_resource(self.module, path):
189 raise IOError("No such file or directory: '%s' in %s" % (
191 return '%s,%s' % (self.module, path)
197 return data.encode('base64')
206 return float(data.strip())
208 if t in ('list','tuple'):
210 for n in node.iterchildren(tag='value'):
211 res.append(_eval_xml(self,n,pool,cr,uid,idref))
215 elif node.tag == "getitem":
217 res=_eval_xml(self,n,pool,cr,uid,idref)
220 elif node.get('type') in ("int", "list"):
221 return res[int(node.get('index'))]
223 return res[node.get('index','').encode("utf8")]
224 elif node.tag == "function":
226 a_eval = node.get('eval','')
228 idref['ref'] = lambda x: self.id_get(cr, x)
229 args = unsafe_eval(a_eval, idref)
231 return_val = _eval_xml(self,n, pool, cr, uid, idref, context)
232 if return_val is not None:
233 args.append(return_val)
234 model = pool[node.get('model','')]
235 method = node.get('name','')
236 res = getattr(model, method)(cr, uid, *args)
238 elif node.tag == "test":
241 escape_re = re.compile(r'(?<!\\)/')
243 return x.replace('\\/', '/')
245 class xml_import(object):
247 def nodeattr2bool(node, attr, default=False):
248 if not node.get(attr):
250 val = node.get(attr).strip()
253 return val.lower() not in ('0', 'false', 'off')
255 def isnoupdate(self, data_node=None):
256 return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False))
258 def get_context(self, data_node, node, eval_dict):
259 data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
260 node_context = node.get("context",'').encode('utf8')
262 for ctx in (data_node_context, node_context):
265 ctx_res = unsafe_eval(ctx, eval_dict)
266 if isinstance(context, dict):
267 context.update(ctx_res)
271 # Some contexts contain references that are only valid at runtime at
272 # client-side, so in that case we keep the original context string
273 # as it is. We also log it, just in case.
275 _logger.debug('Context value (%s) for element with id "%s" or its data node does not parse '\
276 'at server-side, keeping original string, in case it\'s meant for client side only',
277 ctx, node.get('id','n/a'), exc_info=True)
280 def get_uid(self, cr, uid, data_node, node):
281 node_uid = node.get('uid','') or (len(data_node) and data_node.get('uid',''))
283 return self.id_get(cr, node_uid)
286 def _test_xml_id(self, xml_id):
289 module, id = xml_id.split('.', 1)
290 assert '.' not in id, """The ID reference "%s" must contain
291 maximum one dot. They are used to refer to other modules ID, in the
292 form: module.record_id""" % (xml_id,)
293 if module != self.module:
294 modcnt = self.pool['ir.module.module'].search_count(self.cr, self.uid, ['&', ('name', '=', module), ('state', 'in', ['installed'])])
295 assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
298 _logger.error('id: %s is to long (max: 64)', id)
300 def _tag_delete(self, cr, rec, data_node=None):
301 d_model = rec.get("model",'')
302 d_search = rec.get("search",'').encode('utf-8')
303 d_id = rec.get("id",'')
307 idref = _get_idref(self, cr, self.uid, d_model, context={}, idref={})
308 ids = self.pool[d_model].search(cr, self.uid, unsafe_eval(d_search, idref))
311 ids.append(self.id_get(cr, d_id))
313 # d_id cannot be found. doesn't matter in this case
316 self.pool[d_model].unlink(cr, self.uid, ids)
318 def _remove_ir_values(self, cr, name, value, model):
319 ir_values_obj = self.pool['ir.values']
320 ir_value_ids = ir_values_obj.search(cr, self.uid, [('name','=',name),('value','=',value),('model','=',model)])
322 ir_values_obj.unlink(cr, self.uid, ir_value_ids)
326 def _tag_report(self, cr, rec, data_node=None):
328 for dest,f in (('name','string'),('model','model'),('report_name','name')):
329 res[dest] = rec.get(f,'').encode('utf8')
330 assert res[dest], "Attribute %s of report is empty !" % (f,)
331 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),
332 ('attachment','attachment'),('attachment_use','attachment_use'), ('usage','usage'),
333 ('report_type', 'report_type'), ('parser', 'parser')):
335 res[dest] = rec.get(field).encode('utf8')
337 res['auto'] = eval(rec.get('auto','False'))
339 sxw_content = misc.file_open(rec.get('sxw')).read()
340 res['report_sxw_content'] = sxw_content
341 if rec.get('header'):
342 res['header'] = eval(rec.get('header','False'))
344 res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
346 xml_id = rec.get('id','').encode('utf8')
347 self._test_xml_id(xml_id)
349 if rec.get('groups'):
350 g_names = rec.get('groups','').split(',')
352 for group in g_names:
353 if group.startswith('-'):
354 group_id = self.id_get(cr, group[1:])
355 groups_value.append((3, group_id))
357 group_id = self.id_get(cr, group)
358 groups_value.append((4, group_id))
359 res['groups_id'] = groups_value
361 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)
362 self.idref[xml_id] = int(id)
364 if not rec.get('menu') or eval(rec.get('menu','False')):
365 keyword = str(rec.get('keyword', 'client_print_multi'))
366 value = 'ir.actions.report.xml,'+str(id)
367 replace = rec.get('replace', True)
368 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)
369 elif self.mode=='update' and eval(rec.get('menu','False'))==False:
370 # Special check for report having attribute menu=False on update
371 value = 'ir.actions.report.xml,'+str(id)
372 self._remove_ir_values(cr, res['name'], value, res['model'])
375 def _tag_function(self, cr, rec, data_node=None):
376 if self.isnoupdate(data_node) and self.mode != 'init':
378 context = self.get_context(data_node, rec, {'ref': _ref(self, cr)})
379 uid = self.get_uid(cr, self.uid, data_node, rec)
380 _eval_xml(self,rec, self.pool, cr, uid, self.idref, context=context)
383 def _tag_wizard(self, cr, rec, data_node=None):
384 string = rec.get("string",'').encode('utf8')
385 model = rec.get("model",'').encode('utf8')
386 name = rec.get("name",'').encode('utf8')
387 xml_id = rec.get('id','').encode('utf8')
388 self._test_xml_id(xml_id)
389 multi = rec.get('multi','') and eval(rec.get('multi','False'))
390 res = {'name': string, 'wiz_name': name, 'multi': multi, 'model': model}
392 if rec.get('groups'):
393 g_names = rec.get('groups','').split(',')
395 for group in g_names:
396 if group.startswith('-'):
397 group_id = self.id_get(cr, group[1:])
398 groups_value.append((3, group_id))
400 group_id = self.id_get(cr, group)
401 groups_value.append((4, group_id))
402 res['groups_id'] = groups_value
404 id = self.pool['ir.model.data']._update(cr, self.uid, "ir.actions.wizard", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
405 self.idref[xml_id] = int(id)
407 if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
408 keyword = str(rec.get('keyword','') or 'client_action_multi')
409 value = 'ir.actions.wizard,'+str(id)
410 replace = rec.get("replace",'') or True
411 self.pool['ir.model.data'].ir_set(cr, self.uid, 'action', keyword, string, [model], value, replace=replace, isobject=True, xml_id=xml_id)
412 elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
413 # Special check for wizard having attribute menu=False on update
414 value = 'ir.actions.wizard,'+str(id)
415 self._remove_ir_values(cr, string, value, model)
417 def _tag_url(self, cr, rec, data_node=None):
418 url = rec.get("url",'').encode('utf8')
419 target = rec.get("target",'').encode('utf8')
420 name = rec.get("name",'').encode('utf8')
421 xml_id = rec.get('id','').encode('utf8')
422 self._test_xml_id(xml_id)
424 res = {'name': name, 'url': url, 'target':target}
426 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)
427 self.idref[xml_id] = int(id)
429 def _tag_act_window(self, cr, rec, data_node=None):
430 name = rec.get('name','').encode('utf-8')
431 xml_id = rec.get('id','').encode('utf8')
432 self._test_xml_id(xml_id)
433 type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
435 if rec.get('view_id'):
436 view_id = self.id_get(cr, rec.get('view_id','').encode('utf-8'))
437 domain = rec.get('domain','').encode('utf-8') or '[]'
438 res_model = rec.get('res_model','').encode('utf-8')
439 src_model = rec.get('src_model','').encode('utf-8')
440 view_type = rec.get('view_type','').encode('utf-8') or 'form'
441 view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
442 usage = rec.get('usage','').encode('utf-8')
443 limit = rec.get('limit','').encode('utf-8')
444 auto_refresh = rec.get('auto_refresh','').encode('utf-8')
447 # Act_window's 'domain' and 'context' contain mostly literals
448 # but they can also refer to the variables provided below
449 # in eval_context, so we need to eval() them before storing.
450 # Among the context variables, 'active_id' refers to
451 # the currently selected items in a list view, and only
452 # takes meaning at runtime on the client side. For this
453 # reason it must remain a bare variable in domain and context,
454 # even after eval() at server-side. We use the special 'unquote'
455 # class to achieve this effect: a string which has itself, unquoted,
457 active_id = unquote("active_id")
458 active_ids = unquote("active_ids")
459 active_model = unquote("active_model")
462 return self.id_get(cr, str_id)
464 # Include all locals() in eval_context, for backwards compatibility
471 'res_model': res_model,
472 'src_model': src_model,
473 'view_type': view_type,
474 'view_mode': view_mode,
477 'auto_refresh': auto_refresh,
479 'active_id': active_id,
480 'active_ids': active_ids,
481 'active_model': active_model,
484 context = self.get_context(data_node, rec, eval_context)
487 domain = unsafe_eval(domain, eval_context)
489 # Some domains contain references that are only valid at runtime at
490 # client-side, so in that case we keep the original domain string
491 # as it is. We also log it, just in case.
492 _logger.debug('Domain value (%s) for element with id "%s" does not parse '\
493 'at server-side, keeping original string, in case it\'s meant for client side only',
494 domain, xml_id or 'n/a', exc_info=True)
501 'res_model': res_model,
502 'src_model': src_model,
503 'view_type': view_type,
504 'view_mode': view_mode,
507 'auto_refresh': auto_refresh,
510 if rec.get('groups'):
511 g_names = rec.get('groups','').split(',')
513 for group in g_names:
514 if group.startswith('-'):
515 group_id = self.id_get(cr, group[1:])
516 groups_value.append((3, group_id))
518 group_id = self.id_get(cr, group)
519 groups_value.append((4, group_id))
520 res['groups_id'] = groups_value
522 if rec.get('target'):
523 res['target'] = rec.get('target','')
525 res['multi'] = eval(rec.get('multi', 'False'))
526 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)
527 self.idref[xml_id] = int(id)
530 #keyword = 'client_action_relate'
531 keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
532 value = 'ir.actions.act_window,'+str(id)
533 replace = rec.get('replace','') or True
534 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)
535 # TODO add remove ir.model.data
537 def _tag_ir_set(self, cr, rec, data_node=None):
538 if self.mode != 'init':
541 for field in rec.findall('./field'):
542 f_name = field.get("name",'').encode('utf-8')
543 f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
545 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))
547 def _tag_workflow(self, cr, rec, data_node=None):
548 if self.isnoupdate(data_node) and self.mode != 'init':
550 model = str(rec.get('model',''))
551 w_ref = rec.get('ref','')
553 id = self.id_get(cr, w_ref)
555 number_children = len(rec)
556 assert number_children > 0,\
557 'You must define a child node if you dont give a ref'
558 assert number_children == 1,\
559 'Only one child node is accepted (%d given)' % number_children
560 id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
562 uid = self.get_uid(cr, self.uid, data_node, rec)
563 openerp.workflow.trg_validate(uid, model,
565 str(rec.get('action','')), cr)
568 # Support two types of notation:
569 # name="Inventory Control/Sending Goods"
574 def _tag_menuitem(self, cr, rec, data_node=None):
575 rec_id = rec.get("id",'').encode('ascii')
576 self._test_xml_id(rec_id)
577 m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
579 values = {'parent_id': False}
580 if rec.get('parent', False) is False and len(m_l) > 1:
581 # No parent attribute specified and the menu name has several menu components,
582 # try to determine the ID of the parent according to menu path
585 values['name'] = m_l[-1]
586 m_l = m_l[:-1] # last part is our name, not a parent
587 for idx, menu_elem in enumerate(m_l):
589 cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
591 cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
596 # the menuitem does't exist but we are in branch (not a leaf)
597 _logger.warning('Warning no ID for submenu %s of menu %s !', menu_elem, str(m_l))
598 pid = self.pool['ir.ui.menu'].create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
599 values['parent_id'] = pid
601 # The parent attribute was specified, if non-empty determine its ID, otherwise
602 # explicitly make a top-level menu
603 if rec.get('parent'):
604 menu_parent_id = self.id_get(cr, rec.get('parent',''))
606 # we get here with <menuitem parent="">, explicit clear of parent, or
607 # if no parent attribute at all but menu name is not a menu path
608 menu_parent_id = False
609 values = {'parent_id': menu_parent_id}
611 values['name'] = rec.get('name')
613 res = [ self.id_get(cr, rec.get('id','')) ]
617 if rec.get('action'):
618 a_action = rec.get('action','').encode('utf8')
620 # determine the type of action
621 a_type, a_id = self.model_id_get(cr, a_action)
622 a_type = a_type.split('.')[-1] # keep only type part
625 "act_window": 'STOCK_NEW',
626 "report.xml": 'STOCK_PASTE',
627 "wizard": 'STOCK_EXECUTE',
628 "url": 'STOCK_JUMP_TO',
629 "client": 'STOCK_EXECUTE',
630 "server": 'STOCK_EXECUTE',
632 values['icon'] = icons.get(a_type,'STOCK_NEW')
634 if a_type=='act_window':
635 cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
636 rrres = cr.fetchone()
637 assert rrres, "No window action defined for this id %s !\n" \
638 "Verify that this is a window action or add a type argument." % (a_action,)
639 action_type,action_mode,action_name,view_id,target = rrres
641 view_arch = self.pool['ir.ui.view'].read(cr, 1, [view_id], ['arch'])
642 action_mode = etree.fromstring(view_arch[0]['arch'].encode('utf8')).tag
643 cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
645 action_mode, = cr.fetchone()
646 if action_type=='tree':
647 values['icon'] = 'STOCK_INDENT'
648 elif action_mode and action_mode.startswith(('tree','kanban','gantt')):
649 values['icon'] = 'STOCK_JUSTIFY_FILL'
650 elif action_mode and action_mode.startswith('graph'):
651 values['icon'] = 'terp-graph'
652 elif action_mode and action_mode.startswith('calendar'):
653 values['icon'] = 'terp-calendar'
655 values['icon'] = 'STOCK_EXECUTE'
656 if not values.get('name', False):
657 values['name'] = action_name
659 elif a_type in ['wizard', 'url', 'client', 'server'] and not values.get('name'):
660 a_table = 'ir_act_%s' % a_type
661 cr.execute('select name from %s where id=%%s' % a_table, (int(a_id),))
664 values['name'] = resw[0]
666 if not values.get('name'):
667 # ensure menu has a name
668 values['name'] = rec_id or '?'
670 if rec.get('sequence'):
671 values['sequence'] = int(rec.get('sequence'))
673 values['icon'] = str(rec.get('icon'))
674 if rec.get('web_icon'):
675 values['web_icon'] = "%s,%s" %(self.module, str(rec.get('web_icon')))
676 if rec.get('web_icon_hover'):
677 values['web_icon_hover'] = "%s,%s" %(self.module, str(rec.get('web_icon_hover')))
679 if rec.get('groups'):
680 g_names = rec.get('groups','').split(',')
682 for group in g_names:
683 if group.startswith('-'):
684 group_id = self.id_get(cr, group[1:])
685 groups_value.append((3, group_id))
687 group_id = self.id_get(cr, group)
688 groups_value.append((4, group_id))
689 values['groups_id'] = groups_value
691 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)
694 self.idref[rec_id] = int(pid)
696 if rec.get('action') and pid:
697 action = "ir.actions.%s,%d" % (a_type, a_id)
698 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)
699 return 'ir.ui.menu', pid
701 def _assert_equals(self, f1, f2, prec=4):
702 return not round(f1 - f2, prec)
704 def _tag_assert(self, cr, rec, data_node=None):
705 if self.isnoupdate(data_node) and self.mode != 'init':
708 rec_model = rec.get("model",'').encode('ascii')
709 model = self.pool[rec_model]
710 rec_id = rec.get("id",'').encode('ascii')
711 self._test_xml_id(rec_id)
712 rec_src = rec.get("search",'').encode('utf8')
713 rec_src_count = rec.get("count")
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[rec_model].search(cr, uid, q, context=context)
727 count = int(rec_src_count)
728 if len(ids) != count:
729 self.assertion_report.record_failure()
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))
738 assert ids is not None,\
739 'You must give either an id or a search criteria'
742 brrec = model.browse(cr, uid, id, context)
744 def __getitem__(self2, key):
747 return dict.__getitem__(self2, key)
749 globals_dict['floatEqual'] = self._assert_equals
750 globals_dict['ref'] = ref
751 globals_dict['_ref'] = ref
752 for test in rec.findall('./test'):
753 f_expr = test.get("expr",'').encode('utf-8')
754 expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
755 expression_value = unsafe_eval(f_expr, globals_dict)
756 if expression_value != expected_value: # assertion failed
757 self.assertion_report.record_failure()
758 msg = 'assertion "%s" failed!\n' \
760 ' expected value: %r\n' \
761 ' obtained value: %r\n' \
762 % (rec_string, etree.tostring(test), expected_value, expression_value)
765 else: # all tests were successful for this assertion tag (no break)
766 self.assertion_report.record_success()
768 def _tag_record(self, cr, rec, data_node=None):
769 rec_model = rec.get("model").encode('ascii')
770 model = self.pool[rec_model]
771 rec_id = rec.get("id",'').encode('ascii')
772 rec_context = rec.get("context", None)
774 rec_context = unsafe_eval(rec_context)
775 self._test_xml_id(rec_id)
776 if self.isnoupdate(data_node) and self.mode != 'init':
777 # check if the xml record has an id string
780 module,rec_id2 = rec_id.split('.')
784 id = self.pool['ir.model.data']._update_dummy(cr, self.uid, rec_model, module, rec_id2)
785 # check if the resource already existed at the last update
787 # if it existed, we don't update the data, but we need to
788 # know the id of the existing record anyway
789 self.idref[rec_id] = int(id)
792 # if the resource didn't exist
793 if not self.nodeattr2bool(rec, 'forcecreate', True):
794 # we don't want to create it, so we skip it
796 # else, we let the record to be created
799 # otherwise it is skipped
802 for field in rec.findall('./field'):
803 #TODO: most of this code is duplicated above (in _eval_xml)...
804 f_name = field.get("name",'').encode('utf-8')
805 f_ref = field.get("ref",'').encode('utf-8')
806 f_search = field.get("search",'').encode('utf-8')
807 f_model = field.get("model",'').encode('utf-8')
808 if not f_model and model._columns.get(f_name,False):
809 f_model = model._columns[f_name]._obj
810 f_use = field.get("use",'').encode('utf-8') or 'id'
814 q = unsafe_eval(f_search, self.idref)
816 assert f_model, 'Define an attribute model="..." in your .XML file !'
817 f_obj = self.pool[f_model]
818 # browse the objects searched
819 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
820 # column definitions of the "local" object
821 _cols = self.pool[rec_model]._columns
822 # if the current field is many2many
823 if (f_name in _cols) and _cols[f_name]._type=='many2many':
824 f_val = [(6, 0, map(lambda x: x[f_use], s))]
826 # otherwise (we are probably in a many2one field),
827 # take the first element of the search
833 if f_name in model._columns \
834 and model._columns[f_name]._type == 'reference':
835 val = self.model_id_get(cr, f_ref)
836 f_val = val[0] + ',' + str(val[1])
838 f_val = self.id_get(cr, f_ref)
840 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
841 if model._columns.has_key(f_name):
842 import openerp.osv as osv
843 if isinstance(model._columns[f_name], osv.fields.integer):
847 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 )
849 self.idref[rec_id] = int(id)
850 if config.get('import_partial', False):
854 def _tag_template(self, cr, el, data_node=None):
855 # This helper transforms a <template> element into a <record> and forwards it
856 tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
859 module, tpl_id = tpl_id.split('.', 1)
860 # set the full template name for qweb <module>.<id>
861 if not (el.get('inherit_id') or el.get('inherit_option_id')):
862 el.set('t-name', '%s.%s' % (module, tpl_id))
866 el.attrib.pop('id', None)
870 'model': 'ir.ui.view',
872 for att in ['forcecreate', 'context', 'priority']:
874 record_attrs[att] = el.attrib.pop(att)
876 Field = builder.E.field
877 name = el.get('name', tpl_id)
879 record = etree.Element('record', attrib=record_attrs)
880 record.append(Field(name, name='name'))
881 record.append(Field("qweb", name='type'))
882 record.append(Field(el, name="arch", type="xml"))
883 for field_name in ('inherit_id','inherit_option_id'):
884 value = el.attrib.pop(field_name, None)
885 if value: record.append(Field(name=field_name, ref=value))
886 if el.attrib.pop('page', None) == 'True':
887 record.append(Field(name="page", eval="True"))
889 return self._tag_record(cr, record, data_node)
891 def id_get(self, cr, id_str):
892 if id_str in self.idref:
893 return self.idref[id_str]
894 res = self.model_id_get(cr, id_str)
895 if res and len(res)>1: res = res[1]
898 def model_id_get(self, cr, id_str):
899 model_data_obj = self.pool['ir.model.data']
902 mod,id_str = id_str.split('.')
903 return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
906 if de.tag != 'openerp':
907 raise Exception("Mismatch xml format: root tag must be `openerp`.")
909 for n in de.findall('./data'):
911 if rec.tag in self._tags:
913 self._tags[rec.tag](self.cr, rec, n)
916 exc_info = sys.exc_info()
917 raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
920 def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
926 self.pool = openerp.registry(cr.dbname)
929 report = assertion_report.assertion_report()
930 self.assertion_report = report
931 self.noupdate = noupdate
933 'menuitem': self._tag_menuitem,
934 'record': self._tag_record,
935 'template': self._tag_template,
936 'assert': self._tag_assert,
937 'report': self._tag_report,
938 'wizard': self._tag_wizard,
939 'delete': self._tag_delete,
940 'ir_set': self._tag_ir_set,
941 'function': self._tag_function,
942 'workflow': self._tag_workflow,
943 'act_window': self._tag_act_window,
947 def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None):
948 pathname = os.path.join(module, filename)
949 fp = misc.file_open(pathname)
950 ext = os.path.splitext(filename)[1].lower()
953 convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
955 convert_sql_import(cr, fp)
957 convert_yaml_import(cr, module, fp, kind, idref, mode, noupdate, report)
959 convert_xml_import(cr, module, fp, idref, mode, noupdate, report)
961 pass # .js files are valid but ignored here.
963 _logger.warning("Can't load unknown file type %s.", filename)
967 def convert_sql_import(cr, fp):
968 queries = fp.read().split(';')
969 for query in queries:
970 new_query = ' '.join(query.split())
972 cr.execute(new_query)
974 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
982 model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
983 #remove folder path from model
984 head, model = os.path.split(model)
986 input = cStringIO.StringIO(csvcontent) #FIXME
987 reader = csv.reader(input, quotechar='"', delimiter=',')
988 fields = reader.next()
990 if config.get('import_partial'):
991 fname_partial = module + '/'+ fname
992 if not os.path.isfile(config.get('import_partial')):
993 pickle.dump({}, file(config.get('import_partial'),'w+'))
995 data = pickle.load(file(config.get('import_partial')))
996 if fname_partial in data:
997 if not data[fname_partial]:
1000 for i in range(data[fname_partial]):
1003 if not (mode == 'init' or 'id' in fields):
1004 _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
1010 if not (line and any(line)):
1013 datas.append(map(misc.ustr, line))
1015 _logger.error("Cannot import the line: %s", line)
1017 registry = openerp.registry(cr.dbname)
1018 result, rows, warning_msg, dummy = registry[model].import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
1020 # Report failed import and abort module install
1021 raise Exception(_('Module loading failed: file %s/%s could not be processed:\n %s') % (module, fname, warning_msg))
1022 if config.get('import_partial'):
1023 data = pickle.load(file(config.get('import_partial')))
1024 data[fname_partial] = 0
1025 pickle.dump(data, file(config.get('import_partial'),'wb'))
1031 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
1032 doc = etree.parse(xmlfile)
1033 relaxng = etree.RelaxNG(
1034 etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
1036 relaxng.assert_(doc)
1038 _logger.error('The XML file does not fit the required schema !')
1039 _logger.error(misc.ustr(relaxng.error_log.last_error))
1044 obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
1045 obj.parse(doc.getroot())
1049 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: