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={})
309 ids = self.pool[d_model].search(cr, self.uid, unsafe_eval(d_search, idref))
311 _logger.warning('Skipping deletion for failed search `%r`', d_search, exc_info=True)
315 ids.append(self.id_get(cr, d_id))
317 # d_id cannot be found. doesn't matter in this case
318 _logger.warning('Skipping deletion for missing XML ID `%r`', d_id, exc_info=True)
321 self.pool[d_model].unlink(cr, self.uid, ids)
323 def _remove_ir_values(self, cr, name, value, model):
324 ir_values_obj = self.pool['ir.values']
325 ir_value_ids = ir_values_obj.search(cr, self.uid, [('name','=',name),('value','=',value),('model','=',model)])
327 ir_values_obj.unlink(cr, self.uid, ir_value_ids)
331 def _tag_report(self, cr, rec, data_node=None):
333 for dest,f in (('name','string'),('model','model'),('report_name','name')):
334 res[dest] = rec.get(f,'').encode('utf8')
335 assert res[dest], "Attribute %s of report is empty !" % (f,)
336 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),
337 ('attachment','attachment'),('attachment_use','attachment_use'), ('usage','usage'),
338 ('report_type', 'report_type'), ('parser', 'parser')):
340 res[dest] = rec.get(field).encode('utf8')
342 res['auto'] = eval(rec.get('auto','False'))
344 sxw_content = misc.file_open(rec.get('sxw')).read()
345 res['report_sxw_content'] = sxw_content
346 if rec.get('header'):
347 res['header'] = eval(rec.get('header','False'))
349 res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
351 xml_id = rec.get('id','').encode('utf8')
352 self._test_xml_id(xml_id)
354 if rec.get('groups'):
355 g_names = rec.get('groups','').split(',')
357 for group in g_names:
358 if group.startswith('-'):
359 group_id = self.id_get(cr, group[1:])
360 groups_value.append((3, group_id))
362 group_id = self.id_get(cr, group)
363 groups_value.append((4, group_id))
364 res['groups_id'] = groups_value
366 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)
367 self.idref[xml_id] = int(id)
369 if not rec.get('menu') or eval(rec.get('menu','False')):
370 keyword = str(rec.get('keyword', 'client_print_multi'))
371 value = 'ir.actions.report.xml,'+str(id)
372 replace = rec.get('replace', True)
373 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)
374 elif self.mode=='update' and eval(rec.get('menu','False'))==False:
375 # Special check for report having attribute menu=False on update
376 value = 'ir.actions.report.xml,'+str(id)
377 self._remove_ir_values(cr, res['name'], value, res['model'])
380 def _tag_function(self, cr, rec, data_node=None):
381 if self.isnoupdate(data_node) and self.mode != 'init':
383 context = self.get_context(data_node, rec, {'ref': _ref(self, cr)})
384 uid = self.get_uid(cr, self.uid, data_node, rec)
385 _eval_xml(self,rec, self.pool, cr, uid, self.idref, context=context)
388 def _tag_wizard(self, cr, rec, data_node=None):
389 string = rec.get("string",'').encode('utf8')
390 model = rec.get("model",'').encode('utf8')
391 name = rec.get("name",'').encode('utf8')
392 xml_id = rec.get('id','').encode('utf8')
393 self._test_xml_id(xml_id)
394 multi = rec.get('multi','') and eval(rec.get('multi','False'))
395 res = {'name': string, 'wiz_name': name, 'multi': multi, 'model': model}
397 if rec.get('groups'):
398 g_names = rec.get('groups','').split(',')
400 for group in g_names:
401 if group.startswith('-'):
402 group_id = self.id_get(cr, group[1:])
403 groups_value.append((3, group_id))
405 group_id = self.id_get(cr, group)
406 groups_value.append((4, group_id))
407 res['groups_id'] = groups_value
409 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)
410 self.idref[xml_id] = int(id)
412 if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
413 keyword = str(rec.get('keyword','') or 'client_action_multi')
414 value = 'ir.actions.wizard,'+str(id)
415 replace = rec.get("replace",'') or True
416 self.pool['ir.model.data'].ir_set(cr, self.uid, 'action', keyword, string, [model], value, replace=replace, isobject=True, xml_id=xml_id)
417 elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
418 # Special check for wizard having attribute menu=False on update
419 value = 'ir.actions.wizard,'+str(id)
420 self._remove_ir_values(cr, string, value, model)
422 def _tag_url(self, cr, rec, data_node=None):
423 url = rec.get("url",'').encode('utf8')
424 target = rec.get("target",'').encode('utf8')
425 name = rec.get("name",'').encode('utf8')
426 xml_id = rec.get('id','').encode('utf8')
427 self._test_xml_id(xml_id)
429 res = {'name': name, 'url': url, 'target':target}
431 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)
432 self.idref[xml_id] = int(id)
434 def _tag_act_window(self, cr, rec, data_node=None):
435 name = rec.get('name','').encode('utf-8')
436 xml_id = rec.get('id','').encode('utf8')
437 self._test_xml_id(xml_id)
438 type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
440 if rec.get('view_id'):
441 view_id = self.id_get(cr, rec.get('view_id','').encode('utf-8'))
442 domain = rec.get('domain','').encode('utf-8') or '[]'
443 res_model = rec.get('res_model','').encode('utf-8')
444 src_model = rec.get('src_model','').encode('utf-8')
445 view_type = rec.get('view_type','').encode('utf-8') or 'form'
446 view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
447 usage = rec.get('usage','').encode('utf-8')
448 limit = rec.get('limit','').encode('utf-8')
449 auto_refresh = rec.get('auto_refresh','').encode('utf-8')
452 # Act_window's 'domain' and 'context' contain mostly literals
453 # but they can also refer to the variables provided below
454 # in eval_context, so we need to eval() them before storing.
455 # Among the context variables, 'active_id' refers to
456 # the currently selected items in a list view, and only
457 # takes meaning at runtime on the client side. For this
458 # reason it must remain a bare variable in domain and context,
459 # even after eval() at server-side. We use the special 'unquote'
460 # class to achieve this effect: a string which has itself, unquoted,
462 active_id = unquote("active_id")
463 active_ids = unquote("active_ids")
464 active_model = unquote("active_model")
467 return self.id_get(cr, str_id)
469 # Include all locals() in eval_context, for backwards compatibility
476 'res_model': res_model,
477 'src_model': src_model,
478 'view_type': view_type,
479 'view_mode': view_mode,
482 'auto_refresh': auto_refresh,
484 'active_id': active_id,
485 'active_ids': active_ids,
486 'active_model': active_model,
489 context = self.get_context(data_node, rec, eval_context)
492 domain = unsafe_eval(domain, eval_context)
494 # Some domains contain references that are only valid at runtime at
495 # client-side, so in that case we keep the original domain string
496 # as it is. We also log it, just in case.
497 _logger.debug('Domain value (%s) for element with id "%s" does not parse '\
498 'at server-side, keeping original string, in case it\'s meant for client side only',
499 domain, xml_id or 'n/a', exc_info=True)
506 'res_model': res_model,
507 'src_model': src_model,
508 'view_type': view_type,
509 'view_mode': view_mode,
512 'auto_refresh': auto_refresh,
515 if rec.get('groups'):
516 g_names = rec.get('groups','').split(',')
518 for group in g_names:
519 if group.startswith('-'):
520 group_id = self.id_get(cr, group[1:])
521 groups_value.append((3, group_id))
523 group_id = self.id_get(cr, group)
524 groups_value.append((4, group_id))
525 res['groups_id'] = groups_value
527 if rec.get('target'):
528 res['target'] = rec.get('target','')
530 res['multi'] = eval(rec.get('multi', 'False'))
531 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)
532 self.idref[xml_id] = int(id)
535 #keyword = 'client_action_relate'
536 keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
537 value = 'ir.actions.act_window,'+str(id)
538 replace = rec.get('replace','') or True
539 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)
540 # TODO add remove ir.model.data
542 def _tag_ir_set(self, cr, rec, data_node=None):
543 if self.mode != 'init':
546 for field in rec.findall('./field'):
547 f_name = field.get("name",'').encode('utf-8')
548 f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
550 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))
552 def _tag_workflow(self, cr, rec, data_node=None):
553 if self.isnoupdate(data_node) and self.mode != 'init':
555 model = str(rec.get('model',''))
556 w_ref = rec.get('ref','')
558 id = self.id_get(cr, w_ref)
560 number_children = len(rec)
561 assert number_children > 0,\
562 'You must define a child node if you dont give a ref'
563 assert number_children == 1,\
564 'Only one child node is accepted (%d given)' % number_children
565 id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
567 uid = self.get_uid(cr, self.uid, data_node, rec)
568 openerp.workflow.trg_validate(uid, model,
570 str(rec.get('action','')), cr)
573 # Support two types of notation:
574 # name="Inventory Control/Sending Goods"
579 def _tag_menuitem(self, cr, rec, data_node=None):
580 rec_id = rec.get("id",'').encode('ascii')
581 self._test_xml_id(rec_id)
582 m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
584 values = {'parent_id': False}
585 if rec.get('parent', False) is False and len(m_l) > 1:
586 # No parent attribute specified and the menu name has several menu components,
587 # try to determine the ID of the parent according to menu path
590 values['name'] = m_l[-1]
591 m_l = m_l[:-1] # last part is our name, not a parent
592 for idx, menu_elem in enumerate(m_l):
594 cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
596 cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
601 # the menuitem does't exist but we are in branch (not a leaf)
602 _logger.warning('Warning no ID for submenu %s of menu %s !', menu_elem, str(m_l))
603 pid = self.pool['ir.ui.menu'].create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
604 values['parent_id'] = pid
606 # The parent attribute was specified, if non-empty determine its ID, otherwise
607 # explicitly make a top-level menu
608 if rec.get('parent'):
609 menu_parent_id = self.id_get(cr, rec.get('parent',''))
611 # we get here with <menuitem parent="">, explicit clear of parent, or
612 # if no parent attribute at all but menu name is not a menu path
613 menu_parent_id = False
614 values = {'parent_id': menu_parent_id}
616 values['name'] = rec.get('name')
618 res = [ self.id_get(cr, rec.get('id','')) ]
622 if rec.get('action'):
623 a_action = rec.get('action','').encode('utf8')
625 # determine the type of action
626 a_type, a_id = self.model_id_get(cr, a_action)
627 a_type = a_type.split('.')[-1] # keep only type part
630 "act_window": 'STOCK_NEW',
631 "report.xml": 'STOCK_PASTE',
632 "wizard": 'STOCK_EXECUTE',
633 "url": 'STOCK_JUMP_TO',
634 "client": 'STOCK_EXECUTE',
635 "server": 'STOCK_EXECUTE',
637 values['icon'] = icons.get(a_type,'STOCK_NEW')
639 if a_type=='act_window':
640 cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
641 rrres = cr.fetchone()
642 assert rrres, "No window action defined for this id %s !\n" \
643 "Verify that this is a window action or add a type argument." % (a_action,)
644 action_type,action_mode,action_name,view_id,target = rrres
646 view_arch = self.pool['ir.ui.view'].read(cr, 1, [view_id], ['arch'])
647 action_mode = etree.fromstring(view_arch[0]['arch'].encode('utf8')).tag
648 cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
650 action_mode, = cr.fetchone()
651 if action_type=='tree':
652 values['icon'] = 'STOCK_INDENT'
653 elif action_mode and action_mode.startswith(('tree','kanban','gantt')):
654 values['icon'] = 'STOCK_JUSTIFY_FILL'
655 elif action_mode and action_mode.startswith('graph'):
656 values['icon'] = 'terp-graph'
657 elif action_mode and action_mode.startswith('calendar'):
658 values['icon'] = 'terp-calendar'
660 values['icon'] = 'STOCK_EXECUTE'
661 if not values.get('name', False):
662 values['name'] = action_name
664 elif a_type in ['wizard', 'url', 'client', 'server'] and not values.get('name'):
665 a_table = 'ir_act_%s' % a_type
666 cr.execute('select name from %s where id=%%s' % a_table, (int(a_id),))
669 values['name'] = resw[0]
671 if not values.get('name'):
672 # ensure menu has a name
673 values['name'] = rec_id or '?'
675 if rec.get('sequence'):
676 values['sequence'] = int(rec.get('sequence'))
678 values['icon'] = str(rec.get('icon'))
679 if rec.get('web_icon'):
680 values['web_icon'] = "%s,%s" %(self.module, str(rec.get('web_icon')))
681 if rec.get('web_icon_hover'):
682 values['web_icon_hover'] = "%s,%s" %(self.module, str(rec.get('web_icon_hover')))
684 if rec.get('groups'):
685 g_names = rec.get('groups','').split(',')
687 for group in g_names:
688 if group.startswith('-'):
689 group_id = self.id_get(cr, group[1:])
690 groups_value.append((3, group_id))
692 group_id = self.id_get(cr, group)
693 groups_value.append((4, group_id))
694 values['groups_id'] = groups_value
696 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)
699 self.idref[rec_id] = int(pid)
701 if rec.get('action') and pid:
702 action = "ir.actions.%s,%d" % (a_type, a_id)
703 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)
704 return 'ir.ui.menu', pid
706 def _assert_equals(self, f1, f2, prec=4):
707 return not round(f1 - f2, prec)
709 def _tag_assert(self, cr, rec, data_node=None):
710 if self.isnoupdate(data_node) and self.mode != 'init':
713 rec_model = rec.get("model",'').encode('ascii')
714 model = self.pool[rec_model]
715 rec_id = rec.get("id",'').encode('ascii')
716 self._test_xml_id(rec_id)
717 rec_src = rec.get("search",'').encode('utf8')
718 rec_src_count = rec.get("count")
720 rec_string = rec.get("string",'').encode('utf8') or 'unknown'
723 eval_dict = {'ref': _ref(self, cr)}
724 context = self.get_context(data_node, rec, eval_dict)
725 uid = self.get_uid(cr, self.uid, data_node, rec)
727 ids = [self.id_get(cr, rec_id)]
729 q = unsafe_eval(rec_src, eval_dict)
730 ids = self.pool[rec_model].search(cr, uid, q, context=context)
732 count = int(rec_src_count)
733 if len(ids) != count:
734 self.assertion_report.record_failure()
735 msg = 'assertion "%s" failed!\n' \
736 ' Incorrect search count:\n' \
737 ' expected count: %d\n' \
738 ' obtained count: %d\n' \
739 % (rec_string, count, len(ids))
743 assert ids is not None,\
744 'You must give either an id or a search criteria'
747 brrec = model.browse(cr, uid, id, context)
749 def __getitem__(self2, key):
752 return dict.__getitem__(self2, key)
754 globals_dict['floatEqual'] = self._assert_equals
755 globals_dict['ref'] = ref
756 globals_dict['_ref'] = ref
757 for test in rec.findall('./test'):
758 f_expr = test.get("expr",'').encode('utf-8')
759 expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
760 expression_value = unsafe_eval(f_expr, globals_dict)
761 if expression_value != expected_value: # assertion failed
762 self.assertion_report.record_failure()
763 msg = 'assertion "%s" failed!\n' \
765 ' expected value: %r\n' \
766 ' obtained value: %r\n' \
767 % (rec_string, etree.tostring(test), expected_value, expression_value)
770 else: # all tests were successful for this assertion tag (no break)
771 self.assertion_report.record_success()
773 def _tag_record(self, cr, rec, data_node=None):
774 rec_model = rec.get("model").encode('ascii')
775 model = self.pool[rec_model]
776 rec_id = rec.get("id",'').encode('ascii')
777 rec_context = rec.get("context", None)
779 rec_context = unsafe_eval(rec_context)
780 self._test_xml_id(rec_id)
781 # in update mode, the record won't be updated if the data node explicitely
782 # opt-out using @noupdate="1". A second check will be performed in
783 # ir.model.data#_update() using the record's ir.model.data `noupdate` field.
784 if self.isnoupdate(data_node) and self.mode != 'init':
785 # check if the xml record has an id string
788 module,rec_id2 = rec_id.split('.')
792 id = self.pool['ir.model.data']._update_dummy(cr, self.uid, rec_model, module, rec_id2)
793 # check if the resource already existed at the last update
795 # if it existed, we don't update the data, but we need to
796 # know the id of the existing record anyway
797 self.idref[rec_id] = int(id)
800 # if the resource didn't exist
801 if not self.nodeattr2bool(rec, 'forcecreate', True):
802 # we don't want to create it, so we skip it
804 # else, we let the record to be created
807 # otherwise it is skipped
810 for field in rec.findall('./field'):
811 #TODO: most of this code is duplicated above (in _eval_xml)...
812 f_name = field.get("name",'').encode('utf-8')
813 f_ref = field.get("ref",'').encode('utf-8')
814 f_search = field.get("search",'').encode('utf-8')
815 f_model = field.get("model",'').encode('utf-8')
816 if not f_model and model._all_columns.get(f_name,False):
817 f_model = model._all_columns[f_name].column._obj
818 f_use = field.get("use",'').encode('utf-8') or 'id'
822 q = unsafe_eval(f_search, self.idref)
824 assert f_model, 'Define an attribute model="..." in your .XML file !'
825 f_obj = self.pool[f_model]
826 # browse the objects searched
827 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
828 # column definitions of the "local" object
829 _cols = self.pool[rec_model]._all_columns
830 # if the current field is many2many
831 if (f_name in _cols) and _cols[f_name].column._type=='many2many':
832 f_val = [(6, 0, map(lambda x: x[f_use], s))]
834 # otherwise (we are probably in a many2one field),
835 # take the first element of the search
841 if f_name in model._all_columns \
842 and model._all_columns[f_name].column._type == 'reference':
843 val = self.model_id_get(cr, f_ref)
844 f_val = val[0] + ',' + str(val[1])
846 f_val = self.id_get(cr, f_ref)
848 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
849 if f_name in model._all_columns:
850 import openerp.osv as osv
851 if isinstance(model._all_columns[f_name].column, osv.fields.integer):
855 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 )
857 self.idref[rec_id] = int(id)
858 if config.get('import_partial', False):
862 def _tag_template(self, cr, el, data_node=None):
863 # This helper transforms a <template> element into a <record> and forwards it
864 tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
866 if '.' not in full_tpl_id:
867 full_tpl_id = '%s.%s' % (self.module, tpl_id)
868 # set the full template name for qweb <module>.<id>
869 if not (el.get('inherit_id') or el.get('inherit_option_id')):
870 el.set('t-name', full_tpl_id)
874 el.attrib.pop('id', None)
878 'model': 'ir.ui.view',
880 for att in ['forcecreate', 'context']:
882 record_attrs[att] = el.attrib.pop(att)
884 Field = builder.E.field
885 name = el.get('name', tpl_id)
887 record = etree.Element('record', attrib=record_attrs)
888 record.append(Field(name, name='name'))
889 record.append(Field("qweb", name='type'))
890 record.append(Field(el.get('priority', "16"), name='priority'))
891 record.append(Field(el, name="arch", type="xml"))
892 for field_name in ('inherit_id','inherit_option_id'):
893 value = el.attrib.pop(field_name, None)
894 if value: record.append(Field(name=field_name, ref=value))
895 groups = el.attrib.pop('groups', None)
897 grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
898 record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
899 if el.attrib.pop('page', None) == 'True':
900 record.append(Field(name="page", eval="True"))
902 return self._tag_record(cr, record, data_node)
904 def id_get(self, cr, id_str):
905 if id_str in self.idref:
906 return self.idref[id_str]
907 res = self.model_id_get(cr, id_str)
908 if res and len(res)>1: res = res[1]
911 def model_id_get(self, cr, id_str):
912 model_data_obj = self.pool['ir.model.data']
915 mod,id_str = id_str.split('.')
916 return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
919 if de.tag != 'openerp':
920 raise Exception("Mismatch xml format: root tag must be `openerp`.")
922 for n in de.findall('./data'):
924 if rec.tag in self._tags:
926 self._tags[rec.tag](self.cr, rec, n)
929 exc_info = sys.exc_info()
930 raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
933 def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
939 self.pool = openerp.registry(cr.dbname)
942 report = assertion_report.assertion_report()
943 self.assertion_report = report
944 self.noupdate = noupdate
946 'menuitem': self._tag_menuitem,
947 'record': self._tag_record,
948 'template': self._tag_template,
949 'assert': self._tag_assert,
950 'report': self._tag_report,
951 'wizard': self._tag_wizard,
952 'delete': self._tag_delete,
953 'ir_set': self._tag_ir_set,
954 'function': self._tag_function,
955 'workflow': self._tag_workflow,
956 'act_window': self._tag_act_window,
960 def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None, pathname=None):
962 pathname = os.path.join(module, filename)
963 fp = misc.file_open(pathname)
964 ext = os.path.splitext(filename)[1].lower()
967 convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
969 convert_sql_import(cr, fp)
971 convert_yaml_import(cr, module, fp, kind, idref, mode, noupdate, report)
973 convert_xml_import(cr, module, fp, idref, mode, noupdate, report)
975 pass # .js files are valid but ignored here.
977 _logger.warning("Can't load unknown file type %s.", filename)
981 def convert_sql_import(cr, fp):
982 queries = fp.read().split(';')
983 for query in queries:
984 new_query = ' '.join(query.split())
986 cr.execute(new_query)
988 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
996 model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
997 #remove folder path from model
998 head, model = os.path.split(model)
1000 input = cStringIO.StringIO(csvcontent) #FIXME
1001 reader = csv.reader(input, quotechar='"', delimiter=',')
1002 fields = reader.next()
1004 if config.get('import_partial'):
1005 fname_partial = module + '/'+ fname
1006 if not os.path.isfile(config.get('import_partial')):
1007 pickle.dump({}, file(config.get('import_partial'),'w+'))
1009 data = pickle.load(file(config.get('import_partial')))
1010 if fname_partial in data:
1011 if not data[fname_partial]:
1014 for i in range(data[fname_partial]):
1017 if not (mode == 'init' or 'id' in fields):
1018 _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
1024 if not (line and any(line)):
1027 datas.append(map(misc.ustr, line))
1029 _logger.error("Cannot import the line: %s", line)
1031 registry = openerp.registry(cr.dbname)
1032 result, rows, warning_msg, dummy = registry[model].import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
1034 # Report failed import and abort module install
1035 raise Exception(_('Module loading %s failed: file %s could not be processed:\n %s') % (module, fname, warning_msg))
1036 if config.get('import_partial'):
1037 data = pickle.load(file(config.get('import_partial')))
1038 data[fname_partial] = 0
1039 pickle.dump(data, file(config.get('import_partial'),'wb'))
1045 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
1046 doc = etree.parse(xmlfile)
1047 relaxng = etree.RelaxNG(
1048 etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
1050 relaxng.assert_(doc)
1052 _logger.error('The XML file does not fit the required schema !')
1053 _logger.error(misc.ustr(relaxng.error_log.last_error))
1058 obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
1059 obj.parse(doc.getroot())
1063 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: