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 # in update mode, the record won't be updated if the data node explicitely
777 # opt-out using @noupdate="1". A second check will be performed in
778 # ir.model.data#_update() using the record's ir.model.data `noupdate` field.
779 if self.isnoupdate(data_node) and self.mode != 'init':
780 # check if the xml record has an id string
783 module,rec_id2 = rec_id.split('.')
787 id = self.pool['ir.model.data']._update_dummy(cr, self.uid, rec_model, module, rec_id2)
788 # check if the resource already existed at the last update
790 # if it existed, we don't update the data, but we need to
791 # know the id of the existing record anyway
792 self.idref[rec_id] = int(id)
795 # if the resource didn't exist
796 if not self.nodeattr2bool(rec, 'forcecreate', True):
797 # we don't want to create it, so we skip it
799 # else, we let the record to be created
802 # otherwise it is skipped
805 for field in rec.findall('./field'):
806 #TODO: most of this code is duplicated above (in _eval_xml)...
807 f_name = field.get("name",'').encode('utf-8')
808 f_ref = field.get("ref",'').encode('utf-8')
809 f_search = field.get("search",'').encode('utf-8')
810 f_model = field.get("model",'').encode('utf-8')
811 if not f_model and model._all_columns.get(f_name,False):
812 f_model = model._all_columns[f_name].column._obj
813 f_use = field.get("use",'').encode('utf-8') or 'id'
817 q = unsafe_eval(f_search, self.idref)
819 assert f_model, 'Define an attribute model="..." in your .XML file !'
820 f_obj = self.pool[f_model]
821 # browse the objects searched
822 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
823 # column definitions of the "local" object
824 _cols = self.pool[rec_model]._all_columns
825 # if the current field is many2many
826 if (f_name in _cols) and _cols[f_name].column._type=='many2many':
827 f_val = [(6, 0, map(lambda x: x[f_use], s))]
829 # otherwise (we are probably in a many2one field),
830 # take the first element of the search
836 if f_name in model._all_columns \
837 and model._all_columns[f_name].column._type == 'reference':
838 val = self.model_id_get(cr, f_ref)
839 f_val = val[0] + ',' + str(val[1])
841 f_val = self.id_get(cr, f_ref)
843 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
844 if f_name in model._all_columns:
845 import openerp.osv as osv
846 if isinstance(model._all_columns[f_name].column, osv.fields.integer):
850 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 )
852 self.idref[rec_id] = int(id)
853 if config.get('import_partial', False):
857 def _tag_template(self, cr, el, data_node=None):
858 # This helper transforms a <template> element into a <record> and forwards it
859 tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
862 module, tpl_id = tpl_id.split('.', 1)
863 # set the full template name for qweb <module>.<id>
864 if not (el.get('inherit_id') or el.get('inherit_option_id')):
865 el.set('t-name', '%s.%s' % (module, tpl_id))
869 el.attrib.pop('id', None)
873 'model': 'ir.ui.view',
875 for att in ['forcecreate', 'context']:
877 record_attrs[att] = el.attrib.pop(att)
879 Field = builder.E.field
880 name = el.get('name', tpl_id)
882 record = etree.Element('record', attrib=record_attrs)
883 record.append(Field(name, name='name'))
884 record.append(Field("qweb", name='type'))
885 record.append(Field(el.get('priority', "16"), name='priority'))
886 record.append(Field(el, name="arch", type="xml"))
887 for field_name in ('inherit_id','inherit_option_id'):
888 value = el.attrib.pop(field_name, None)
889 if value: record.append(Field(name=field_name, ref=value))
890 groups = el.attrib.pop('groups', None)
892 grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
893 record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
894 if el.attrib.pop('page', None) == 'True':
895 record.append(Field(name="page", eval="True"))
897 return self._tag_record(cr, record, data_node)
899 def id_get(self, cr, id_str):
900 if id_str in self.idref:
901 return self.idref[id_str]
902 res = self.model_id_get(cr, id_str)
903 if res and len(res)>1: res = res[1]
906 def model_id_get(self, cr, id_str):
907 model_data_obj = self.pool['ir.model.data']
910 mod,id_str = id_str.split('.')
911 return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
914 if de.tag != 'openerp':
915 raise Exception("Mismatch xml format: root tag must be `openerp`.")
917 for n in de.findall('./data'):
919 if rec.tag in self._tags:
921 self._tags[rec.tag](self.cr, rec, n)
924 exc_info = sys.exc_info()
925 raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
928 def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
934 self.pool = openerp.registry(cr.dbname)
937 report = assertion_report.assertion_report()
938 self.assertion_report = report
939 self.noupdate = noupdate
941 'menuitem': self._tag_menuitem,
942 'record': self._tag_record,
943 'template': self._tag_template,
944 'assert': self._tag_assert,
945 'report': self._tag_report,
946 'wizard': self._tag_wizard,
947 'delete': self._tag_delete,
948 'ir_set': self._tag_ir_set,
949 'function': self._tag_function,
950 'workflow': self._tag_workflow,
951 'act_window': self._tag_act_window,
955 def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None):
956 pathname = os.path.join(module, filename)
957 fp = misc.file_open(pathname)
958 ext = os.path.splitext(filename)[1].lower()
961 convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
963 convert_sql_import(cr, fp)
965 convert_yaml_import(cr, module, fp, kind, idref, mode, noupdate, report)
967 convert_xml_import(cr, module, fp, idref, mode, noupdate, report)
969 pass # .js files are valid but ignored here.
971 _logger.warning("Can't load unknown file type %s.", filename)
975 def convert_sql_import(cr, fp):
976 queries = fp.read().split(';')
977 for query in queries:
978 new_query = ' '.join(query.split())
980 cr.execute(new_query)
982 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
990 model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
991 #remove folder path from model
992 head, model = os.path.split(model)
994 input = cStringIO.StringIO(csvcontent) #FIXME
995 reader = csv.reader(input, quotechar='"', delimiter=',')
996 fields = reader.next()
998 if config.get('import_partial'):
999 fname_partial = module + '/'+ fname
1000 if not os.path.isfile(config.get('import_partial')):
1001 pickle.dump({}, file(config.get('import_partial'),'w+'))
1003 data = pickle.load(file(config.get('import_partial')))
1004 if fname_partial in data:
1005 if not data[fname_partial]:
1008 for i in range(data[fname_partial]):
1011 if not (mode == 'init' or 'id' in fields):
1012 _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
1018 if not (line and any(line)):
1021 datas.append(map(misc.ustr, line))
1023 _logger.error("Cannot import the line: %s", line)
1025 registry = openerp.registry(cr.dbname)
1026 result, rows, warning_msg, dummy = registry[model].import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
1028 # Report failed import and abort module install
1029 raise Exception(_('Module loading %s failed: file %s could not be processed:\n %s') % (module, fname, warning_msg))
1030 if config.get('import_partial'):
1031 data = pickle.load(file(config.get('import_partial')))
1032 data[fname_partial] = 0
1033 pickle.dump(data, file(config.get('import_partial'),'wb'))
1039 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
1040 doc = etree.parse(xmlfile)
1041 relaxng = etree.RelaxNG(
1042 etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
1044 relaxng.assert_(doc)
1046 _logger.error('The XML file does not fit the required schema !')
1047 _logger.error(misc.ustr(relaxng.error_log.last_error))
1052 obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
1053 obj.parse(doc.getroot())
1057 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: