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
37 import assertion_report
39 _logger = logging.getLogger(__name__)
44 _logger.warning('could not find pytz library, please install it')
45 class pytzclass(object):
50 from datetime import datetime, timedelta
51 from dateutil.relativedelta import relativedelta
52 from lxml import etree
54 from config import config
55 from translate import _
57 # List of etree._Element subclasses that we choose to ignore when parsing XML.
58 from misc import SKIPPED_ELEMENT_TYPES
60 from misc import unquote
62 # Import of XML records requires the unsafe eval as well,
63 # almost everywhere, which is ok because it supposedly comes
64 # from trusted data, but at least we make it obvious now.
66 from safe_eval import safe_eval as eval
68 class ParseError(Exception):
69 def __init__(self, msg, text, filename, lineno):
72 self.filename = filename
76 return '"%s" while parsing %s:%s, near\n%s' \
77 % (self.msg, self.filename, self.lineno, self.text)
80 return lambda x: self.id_get(cr, x)
82 def _obj(pool, cr, uid, model_str, context=None):
83 model = pool[model_str]
84 return lambda x: model.browse(cr, uid, x, context=context)
86 def _get_idref(self, cr, uid, model_str, context, idref):
92 relativedelta=relativedelta,
93 version=openerp.release.major_version,
97 idref2['obj'] = _obj(self.pool, cr, uid, model_str, context=context)
100 def _fix_multiple_roots(node):
102 Surround the children of the ``node`` element of an XML field with a
103 single root "data" element, to prevent having a document with multiple
104 roots once parsed separately.
106 XML nodes should have one root only, but we'd like to support
107 direct multiple roots in our partial documents (like inherited view architectures).
108 As a convention we'll surround multiple root with a container "data" element, to be
109 ignored later when parsing.
111 real_nodes = [x for x in node if not isinstance(x, SKIPPED_ELEMENT_TYPES)]
112 if len(real_nodes) > 1:
113 data_node = etree.Element("data")
115 data_node.append(child)
116 node.append(data_node)
118 def _eval_xml(self, node, pool, cr, uid, idref, context=None):
121 if node.tag in ('field','value'):
122 t = node.get('type','char')
123 f_model = node.get('model', '').encode('utf-8')
124 if node.get('search'):
125 f_search = node.get("search",'').encode('utf-8')
126 f_use = node.get("use",'id').encode('utf-8')
127 f_name = node.get("name",'').encode('utf-8')
130 idref2 = _get_idref(self, cr, uid, f_model, context, idref)
131 q = unsafe_eval(f_search, idref2)
132 ids = pool[f_model].search(cr, uid, q)
134 ids = map(lambda x: x[f_use], pool[f_model].read(cr, uid, ids, [f_use]))
135 _cols = pool[f_model]._columns
136 if (f_name in _cols) and _cols[f_name]._type=='many2many':
141 if isinstance(f_val, tuple):
144 a_eval = node.get('eval','')
146 idref2 = _get_idref(self, cr, uid, f_model, context, idref)
148 return unsafe_eval(a_eval, idref2)
150 logging.getLogger('openerp.tools.convert.init').error(
151 'Could not eval(%s) for %s in %s', a_eval, node.get('name'), context)
153 def _process(s, idref):
154 matches = re.finditer('[^%]%\((.*?)\)[ds]', s)
157 found = m.group()[1:]
163 idref[id] = self.id_get(cr, id)
164 s = s.replace(found, str(idref[id]))
166 s = s.replace('%%', '%') # Quite wierd but it's for (somewhat) backward compatibility sake
171 _fix_multiple_roots(node)
172 return '<?xml version="1.0"?>\n'\
173 +_process("".join([etree.tostring(n, encoding='utf-8')
174 for n in node]), idref)
176 return _process("".join([etree.tostring(n, encoding='utf-8')
177 for n in node]), idref)
181 fp = openerp.tools.file_open(node.get('file'))
182 result = base64.b64encode(fp.read())
186 from ..modules import module
187 path = node.text.strip()
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)
192 if t in ('char', 'int', 'float'):
199 return int(d.strip())
201 return float(d.strip())
203 elif t in ('list','tuple'):
205 for n in node.findall('./value'):
206 res.append(_eval_xml(self,n,pool,cr,uid,idref))
210 elif node.tag == "getitem":
212 res=_eval_xml(self,n,pool,cr,uid,idref)
215 elif node.get('type') in ("int", "list"):
216 return res[int(node.get('index'))]
218 return res[node.get('index','').encode("utf8")]
219 elif node.tag == "function":
221 a_eval = node.get('eval','')
223 idref['ref'] = lambda x: self.id_get(cr, x)
224 args = unsafe_eval(a_eval, idref)
226 return_val = _eval_xml(self,n, pool, cr, uid, idref, context)
227 if return_val is not None:
228 args.append(return_val)
229 model = pool[node.get('model','')]
230 method = node.get('name','')
231 res = getattr(model, method)(cr, uid, *args)
233 elif node.tag == "test":
236 escape_re = re.compile(r'(?<!\\)/')
238 return x.replace('\\/', '/')
240 class xml_import(object):
242 def nodeattr2bool(node, attr, default=False):
243 if not node.get(attr):
245 val = node.get(attr).strip()
248 return val.lower() not in ('0', 'false', 'off')
250 def isnoupdate(self, data_node=None):
251 return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False))
253 def get_context(self, data_node, node, eval_dict):
254 data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
255 node_context = node.get("context",'').encode('utf8')
257 for ctx in (data_node_context, node_context):
260 ctx_res = unsafe_eval(ctx, eval_dict)
261 if isinstance(context, dict):
262 context.update(ctx_res)
266 # Some contexts contain references that are only valid at runtime at
267 # client-side, so in that case we keep the original context string
268 # as it is. We also log it, just in case.
270 _logger.debug('Context value (%s) for element with id "%s" or its data node does not parse '\
271 'at server-side, keeping original string, in case it\'s meant for client side only',
272 ctx, node.get('id','n/a'), exc_info=True)
275 def get_uid(self, cr, uid, data_node, node):
276 node_uid = node.get('uid','') or (len(data_node) and data_node.get('uid',''))
278 return self.id_get(cr, node_uid)
281 def _test_xml_id(self, xml_id):
284 module, id = xml_id.split('.', 1)
285 assert '.' not in id, """The ID reference "%s" must contain
286 maximum one dot. They are used to refer to other modules ID, in the
287 form: module.record_id""" % (xml_id,)
288 if module != self.module:
289 modcnt = self.pool['ir.module.module'].search_count(self.cr, self.uid, ['&', ('name', '=', module), ('state', 'in', ['installed'])])
290 assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
293 _logger.error('id: %s is to long (max: 64)', id)
295 def _tag_delete(self, cr, rec, data_node=None):
296 d_model = rec.get("model",'')
297 d_search = rec.get("search",'').encode('utf-8')
298 d_id = rec.get("id",'')
302 idref = _get_idref(self, cr, self.uid, d_model, context={}, idref={})
303 ids = self.pool[d_model].search(cr, self.uid, unsafe_eval(d_search, idref))
306 ids.append(self.id_get(cr, d_id))
308 # d_id cannot be found. doesn't matter in this case
311 self.pool[d_model].unlink(cr, self.uid, ids)
313 def _remove_ir_values(self, cr, name, value, model):
314 ir_values_obj = self.pool['ir.values']
315 ir_value_ids = ir_values_obj.search(cr, self.uid, [('name','=',name),('value','=',value),('model','=',model)])
317 ir_values_obj.unlink(cr, self.uid, ir_value_ids)
321 def _tag_report(self, cr, rec, data_node=None):
323 for dest,f in (('name','string'),('model','model'),('report_name','name')):
324 res[dest] = rec.get(f,'').encode('utf8')
325 assert res[dest], "Attribute %s of report is empty !" % (f,)
326 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),
327 ('attachment','attachment'),('attachment_use','attachment_use'), ('usage','usage'),
328 ('report_type', 'report_type'), ('parser', 'parser')):
330 res[dest] = rec.get(field).encode('utf8')
332 res['auto'] = eval(rec.get('auto','False'))
334 sxw_content = misc.file_open(rec.get('sxw')).read()
335 res['report_sxw_content'] = sxw_content
336 if rec.get('header'):
337 res['header'] = eval(rec.get('header','False'))
339 res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
341 xml_id = rec.get('id','').encode('utf8')
342 self._test_xml_id(xml_id)
344 if rec.get('groups'):
345 g_names = rec.get('groups','').split(',')
347 for group in g_names:
348 if group.startswith('-'):
349 group_id = self.id_get(cr, group[1:])
350 groups_value.append((3, group_id))
352 group_id = self.id_get(cr, group)
353 groups_value.append((4, group_id))
354 res['groups_id'] = groups_value
356 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)
357 self.idref[xml_id] = int(id)
359 if not rec.get('menu') or eval(rec.get('menu','False')):
360 keyword = str(rec.get('keyword', 'client_print_multi'))
361 value = 'ir.actions.report.xml,'+str(id)
362 replace = rec.get('replace', True)
363 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)
364 elif self.mode=='update' and eval(rec.get('menu','False'))==False:
365 # Special check for report having attribute menu=False on update
366 value = 'ir.actions.report.xml,'+str(id)
367 self._remove_ir_values(cr, res['name'], value, res['model'])
370 def _tag_function(self, cr, rec, data_node=None):
371 if self.isnoupdate(data_node) and self.mode != 'init':
373 context = self.get_context(data_node, rec, {'ref': _ref(self, cr)})
374 uid = self.get_uid(cr, self.uid, data_node, rec)
375 _eval_xml(self,rec, self.pool, cr, uid, self.idref, context=context)
378 def _tag_wizard(self, cr, rec, data_node=None):
379 string = rec.get("string",'').encode('utf8')
380 model = rec.get("model",'').encode('utf8')
381 name = rec.get("name",'').encode('utf8')
382 xml_id = rec.get('id','').encode('utf8')
383 self._test_xml_id(xml_id)
384 multi = rec.get('multi','') and eval(rec.get('multi','False'))
385 res = {'name': string, 'wiz_name': name, 'multi': multi, 'model': model}
387 if rec.get('groups'):
388 g_names = rec.get('groups','').split(',')
390 for group in g_names:
391 if group.startswith('-'):
392 group_id = self.id_get(cr, group[1:])
393 groups_value.append((3, group_id))
395 group_id = self.id_get(cr, group)
396 groups_value.append((4, group_id))
397 res['groups_id'] = groups_value
399 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)
400 self.idref[xml_id] = int(id)
402 if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
403 keyword = str(rec.get('keyword','') or 'client_action_multi')
404 value = 'ir.actions.wizard,'+str(id)
405 replace = rec.get("replace",'') or True
406 self.pool['ir.model.data'].ir_set(cr, self.uid, 'action', keyword, string, [model], value, replace=replace, isobject=True, xml_id=xml_id)
407 elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
408 # Special check for wizard having attribute menu=False on update
409 value = 'ir.actions.wizard,'+str(id)
410 self._remove_ir_values(cr, string, value, model)
412 def _tag_url(self, cr, rec, data_node=None):
413 url = rec.get("url",'').encode('utf8')
414 target = rec.get("target",'').encode('utf8')
415 name = rec.get("name",'').encode('utf8')
416 xml_id = rec.get('id','').encode('utf8')
417 self._test_xml_id(xml_id)
419 res = {'name': name, 'url': url, 'target':target}
421 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)
422 self.idref[xml_id] = int(id)
424 def _tag_act_window(self, cr, rec, data_node=None):
425 name = rec.get('name','').encode('utf-8')
426 xml_id = rec.get('id','').encode('utf8')
427 self._test_xml_id(xml_id)
428 type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
430 if rec.get('view_id'):
431 view_id = self.id_get(cr, rec.get('view_id','').encode('utf-8'))
432 domain = rec.get('domain','').encode('utf-8') or '[]'
433 res_model = rec.get('res_model','').encode('utf-8')
434 src_model = rec.get('src_model','').encode('utf-8')
435 view_type = rec.get('view_type','').encode('utf-8') or 'form'
436 view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
437 usage = rec.get('usage','').encode('utf-8')
438 limit = rec.get('limit','').encode('utf-8')
439 auto_refresh = rec.get('auto_refresh','').encode('utf-8')
442 # Act_window's 'domain' and 'context' contain mostly literals
443 # but they can also refer to the variables provided below
444 # in eval_context, so we need to eval() them before storing.
445 # Among the context variables, 'active_id' refers to
446 # the currently selected items in a list view, and only
447 # takes meaning at runtime on the client side. For this
448 # reason it must remain a bare variable in domain and context,
449 # even after eval() at server-side. We use the special 'unquote'
450 # class to achieve this effect: a string which has itself, unquoted,
452 active_id = unquote("active_id")
453 active_ids = unquote("active_ids")
454 active_model = unquote("active_model")
457 return self.id_get(cr, str_id)
459 # Include all locals() in eval_context, for backwards compatibility
466 'res_model': res_model,
467 'src_model': src_model,
468 'view_type': view_type,
469 'view_mode': view_mode,
472 'auto_refresh': auto_refresh,
474 'active_id': active_id,
475 'active_ids': active_ids,
476 'active_model': active_model,
479 context = self.get_context(data_node, rec, eval_context)
482 domain = unsafe_eval(domain, eval_context)
484 # Some domains contain references that are only valid at runtime at
485 # client-side, so in that case we keep the original domain string
486 # as it is. We also log it, just in case.
487 _logger.debug('Domain value (%s) for element with id "%s" does not parse '\
488 'at server-side, keeping original string, in case it\'s meant for client side only',
489 domain, xml_id or 'n/a', exc_info=True)
496 'res_model': res_model,
497 'src_model': src_model,
498 'view_type': view_type,
499 'view_mode': view_mode,
502 'auto_refresh': auto_refresh,
505 if rec.get('groups'):
506 g_names = rec.get('groups','').split(',')
508 for group in g_names:
509 if group.startswith('-'):
510 group_id = self.id_get(cr, group[1:])
511 groups_value.append((3, group_id))
513 group_id = self.id_get(cr, group)
514 groups_value.append((4, group_id))
515 res['groups_id'] = groups_value
517 if rec.get('target'):
518 res['target'] = rec.get('target','')
520 res['multi'] = eval(rec.get('multi', 'False'))
521 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)
522 self.idref[xml_id] = int(id)
525 #keyword = 'client_action_relate'
526 keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
527 value = 'ir.actions.act_window,'+str(id)
528 replace = rec.get('replace','') or True
529 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)
530 # TODO add remove ir.model.data
532 def _tag_ir_set(self, cr, rec, data_node=None):
533 if self.mode != 'init':
536 for field in rec.findall('./field'):
537 f_name = field.get("name",'').encode('utf-8')
538 f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
540 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))
542 def _tag_workflow(self, cr, rec, data_node=None):
543 if self.isnoupdate(data_node) and self.mode != 'init':
545 model = str(rec.get('model',''))
546 w_ref = rec.get('ref','')
548 id = self.id_get(cr, w_ref)
550 number_children = len(rec)
551 assert number_children > 0,\
552 'You must define a child node if you dont give a ref'
553 assert number_children == 1,\
554 'Only one child node is accepted (%d given)' % number_children
555 id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
557 uid = self.get_uid(cr, self.uid, data_node, rec)
558 openerp.workflow.trg_validate(uid, model,
560 str(rec.get('action','')), cr)
563 # Support two types of notation:
564 # name="Inventory Control/Sending Goods"
569 def _tag_menuitem(self, cr, rec, data_node=None):
570 rec_id = rec.get("id",'').encode('ascii')
571 self._test_xml_id(rec_id)
572 m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
574 values = {'parent_id': False}
575 if rec.get('parent', False) is False and len(m_l) > 1:
576 # No parent attribute specified and the menu name has several menu components,
577 # try to determine the ID of the parent according to menu path
580 values['name'] = m_l[-1]
581 m_l = m_l[:-1] # last part is our name, not a parent
582 for idx, menu_elem in enumerate(m_l):
584 cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
586 cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
591 # the menuitem does't exist but we are in branch (not a leaf)
592 _logger.warning('Warning no ID for submenu %s of menu %s !', menu_elem, str(m_l))
593 pid = self.pool['ir.ui.menu'].create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
594 values['parent_id'] = pid
596 # The parent attribute was specified, if non-empty determine its ID, otherwise
597 # explicitly make a top-level menu
598 if rec.get('parent'):
599 menu_parent_id = self.id_get(cr, rec.get('parent',''))
601 # we get here with <menuitem parent="">, explicit clear of parent, or
602 # if no parent attribute at all but menu name is not a menu path
603 menu_parent_id = False
604 values = {'parent_id': menu_parent_id}
606 values['name'] = rec.get('name')
608 res = [ self.id_get(cr, rec.get('id','')) ]
612 if rec.get('action'):
613 a_action = rec.get('action','').encode('utf8')
615 # determine the type of action
616 a_type, a_id = self.model_id_get(cr, a_action)
617 a_type = a_type.split('.')[-1] # keep only type part
620 "act_window": 'STOCK_NEW',
621 "report.xml": 'STOCK_PASTE',
622 "wizard": 'STOCK_EXECUTE',
623 "url": 'STOCK_JUMP_TO',
624 "client": 'STOCK_EXECUTE',
625 "server": 'STOCK_EXECUTE',
627 values['icon'] = icons.get(a_type,'STOCK_NEW')
629 if a_type=='act_window':
630 cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
631 rrres = cr.fetchone()
632 assert rrres, "No window action defined for this id %s !\n" \
633 "Verify that this is a window action or add a type argument." % (a_action,)
634 action_type,action_mode,action_name,view_id,target = rrres
636 view_arch = self.pool['ir.ui.view'].read(cr, 1, [view_id], ['arch'])
637 action_mode = etree.fromstring(view_arch[0]['arch'].encode('utf8')).tag
638 cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
640 action_mode, = cr.fetchone()
641 if action_type=='tree':
642 values['icon'] = 'STOCK_INDENT'
643 elif action_mode and action_mode.startswith(('tree','kanban','gantt')):
644 values['icon'] = 'STOCK_JUSTIFY_FILL'
645 elif action_mode and action_mode.startswith('graph'):
646 values['icon'] = 'terp-graph'
647 elif action_mode and action_mode.startswith('calendar'):
648 values['icon'] = 'terp-calendar'
650 values['icon'] = 'STOCK_EXECUTE'
651 if not values.get('name', False):
652 values['name'] = action_name
654 elif a_type in ['wizard', 'url', 'client', 'server'] and not values.get('name'):
655 a_table = 'ir_act_%s' % a_type
656 cr.execute('select name from %s where id=%%s' % a_table, (int(a_id),))
659 values['name'] = resw[0]
661 if not values.get('name'):
662 # ensure menu has a name
663 values['name'] = rec_id or '?'
665 if rec.get('sequence'):
666 values['sequence'] = int(rec.get('sequence'))
668 values['icon'] = str(rec.get('icon'))
669 if rec.get('web_icon'):
670 values['web_icon'] = "%s,%s" %(self.module, str(rec.get('web_icon')))
671 if rec.get('web_icon_hover'):
672 values['web_icon_hover'] = "%s,%s" %(self.module, str(rec.get('web_icon_hover')))
674 if rec.get('groups'):
675 g_names = rec.get('groups','').split(',')
677 for group in g_names:
678 if group.startswith('-'):
679 group_id = self.id_get(cr, group[1:])
680 groups_value.append((3, group_id))
682 group_id = self.id_get(cr, group)
683 groups_value.append((4, group_id))
684 values['groups_id'] = groups_value
686 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)
689 self.idref[rec_id] = int(pid)
691 if rec.get('action') and pid:
692 action = "ir.actions.%s,%d" % (a_type, a_id)
693 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)
694 return 'ir.ui.menu', pid
696 def _assert_equals(self, f1, f2, prec=4):
697 return not round(f1 - f2, prec)
699 def _tag_assert(self, cr, rec, data_node=None):
700 if self.isnoupdate(data_node) and self.mode != 'init':
703 rec_model = rec.get("model",'').encode('ascii')
704 model = self.pool[rec_model]
705 rec_id = rec.get("id",'').encode('ascii')
706 self._test_xml_id(rec_id)
707 rec_src = rec.get("search",'').encode('utf8')
708 rec_src_count = rec.get("count")
710 rec_string = rec.get("string",'').encode('utf8') or 'unknown'
713 eval_dict = {'ref': _ref(self, cr)}
714 context = self.get_context(data_node, rec, eval_dict)
715 uid = self.get_uid(cr, self.uid, data_node, rec)
717 ids = [self.id_get(cr, rec_id)]
719 q = unsafe_eval(rec_src, eval_dict)
720 ids = self.pool[rec_model].search(cr, uid, q, context=context)
722 count = int(rec_src_count)
723 if len(ids) != count:
724 self.assertion_report.record_failure()
725 msg = 'assertion "%s" failed!\n' \
726 ' Incorrect search count:\n' \
727 ' expected count: %d\n' \
728 ' obtained count: %d\n' \
729 % (rec_string, count, len(ids))
733 assert ids is not None,\
734 'You must give either an id or a search criteria'
737 brrec = model.browse(cr, uid, id, context)
739 def __getitem__(self2, key):
742 return dict.__getitem__(self2, key)
744 globals_dict['floatEqual'] = self._assert_equals
745 globals_dict['ref'] = ref
746 globals_dict['_ref'] = ref
747 for test in rec.findall('./test'):
748 f_expr = test.get("expr",'').encode('utf-8')
749 expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
750 expression_value = unsafe_eval(f_expr, globals_dict)
751 if expression_value != expected_value: # assertion failed
752 self.assertion_report.record_failure()
753 msg = 'assertion "%s" failed!\n' \
755 ' expected value: %r\n' \
756 ' obtained value: %r\n' \
757 % (rec_string, etree.tostring(test), expected_value, expression_value)
760 else: # all tests were successful for this assertion tag (no break)
761 self.assertion_report.record_success()
763 def _tag_record(self, cr, rec, data_node=None):
764 rec_model = rec.get("model").encode('ascii')
765 model = self.pool[rec_model]
766 rec_id = rec.get("id",'').encode('ascii')
767 rec_context = rec.get("context", None)
769 rec_context = unsafe_eval(rec_context)
770 self._test_xml_id(rec_id)
771 if self.isnoupdate(data_node) and self.mode != 'init':
772 # check if the xml record has an id string
775 module,rec_id2 = rec_id.split('.')
779 id = self.pool['ir.model.data']._update_dummy(cr, self.uid, rec_model, module, rec_id2)
780 # check if the resource already existed at the last update
782 # if it existed, we don't update the data, but we need to
783 # know the id of the existing record anyway
784 self.idref[rec_id] = int(id)
787 # if the resource didn't exist
788 if not self.nodeattr2bool(rec, 'forcecreate', True):
789 # we don't want to create it, so we skip it
791 # else, we let the record to be created
794 # otherwise it is skipped
797 for field in rec.findall('./field'):
798 #TODO: most of this code is duplicated above (in _eval_xml)...
799 f_name = field.get("name",'').encode('utf-8')
800 f_ref = field.get("ref",'').encode('utf-8')
801 f_search = field.get("search",'').encode('utf-8')
802 f_model = field.get("model",'').encode('utf-8')
803 if not f_model and model._columns.get(f_name,False):
804 f_model = model._columns[f_name]._obj
805 f_use = field.get("use",'').encode('utf-8') or 'id'
809 q = unsafe_eval(f_search, self.idref)
811 assert f_model, 'Define an attribute model="..." in your .XML file !'
812 f_obj = self.pool[f_model]
813 # browse the objects searched
814 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
815 # column definitions of the "local" object
816 _cols = self.pool[rec_model]._columns
817 # if the current field is many2many
818 if (f_name in _cols) and _cols[f_name]._type=='many2many':
819 f_val = [(6, 0, map(lambda x: x[f_use], s))]
821 # otherwise (we are probably in a many2one field),
822 # take the first element of the search
828 if f_name in model._columns \
829 and model._columns[f_name]._type == 'reference':
830 val = self.model_id_get(cr, f_ref)
831 f_val = val[0] + ',' + str(val[1])
833 f_val = self.id_get(cr, f_ref)
835 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
836 if model._columns.has_key(f_name):
837 import openerp.osv as osv
838 if isinstance(model._columns[f_name], osv.fields.integer):
842 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 )
844 self.idref[rec_id] = int(id)
845 if config.get('import_partial', False):
849 def _tag_template(self, cr, el, data_node=None):
850 # This helper transforms a <template> element into a <record> and forwards it
851 tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
854 module, tpl_id = tpl_id.split('.', 1)
855 # set the full template name for qweb <module>.<id>
856 if not (el.get('inherit_id') or el.get('inherit_option_id')):
857 el.attrib['t-name'] = '%s.%s' % (module, tpl_id)
861 el.attrib.pop('id', None)
863 record = etree.Element('record')
866 'model': 'ir.ui.view',
868 for att in ['forcecreate', 'context']:
870 record_attrs[att] = el.attrib.pop(att)
872 record.attrib.update(record_attrs)
873 name = el.get('name', tpl_id)
874 record.append(etree.fromstring('<field name="name">%s</field>' % name))
875 record.append(etree.fromstring('<field name="type">qweb</field>'))
876 record.append(etree.fromstring('<field name="arch" type="xml"/>'))
877 record[-1].append(el)
878 for key in ('inherit_id','inherit_option_id'):
880 record.append(etree.fromstring('<field name="%s" ref="%s"/>' % (key, el.get(key))))
881 el.attrib.pop(key, None)
883 record.append(etree.Element('field', name="page", eval="True"))
884 return self._tag_record(cr, record, data_node)
886 def id_get(self, cr, id_str):
887 if id_str in self.idref:
888 return self.idref[id_str]
889 res = self.model_id_get(cr, id_str)
890 if res and len(res)>1: res = res[1]
893 def model_id_get(self, cr, id_str):
894 model_data_obj = self.pool['ir.model.data']
897 mod,id_str = id_str.split('.')
898 return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
901 if de.tag != 'openerp':
902 raise Exception("Mismatch xml format: root tag must be `openerp`.")
904 for n in de.findall('./data'):
906 if rec.tag in self._tags:
908 self._tags[rec.tag](self.cr, rec, n)
911 exc_info = sys.exc_info()
912 raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
915 def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
921 self.pool = openerp.registry(cr.dbname)
924 report = assertion_report.assertion_report()
925 self.assertion_report = report
926 self.noupdate = noupdate
928 'menuitem': self._tag_menuitem,
929 'record': self._tag_record,
930 'template': self._tag_template,
931 'assert': self._tag_assert,
932 'report': self._tag_report,
933 'wizard': self._tag_wizard,
934 'delete': self._tag_delete,
935 'ir_set': self._tag_ir_set,
936 'function': self._tag_function,
937 'workflow': self._tag_workflow,
938 'act_window': self._tag_act_window,
942 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
950 model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
951 #remove folder path from model
952 head, model = os.path.split(model)
954 input = cStringIO.StringIO(csvcontent) #FIXME
955 reader = csv.reader(input, quotechar='"', delimiter=',')
956 fields = reader.next()
958 if config.get('import_partial'):
959 fname_partial = module + '/'+ fname
960 if not os.path.isfile(config.get('import_partial')):
961 pickle.dump({}, file(config.get('import_partial'),'w+'))
963 data = pickle.load(file(config.get('import_partial')))
964 if fname_partial in data:
965 if not data[fname_partial]:
968 for i in range(data[fname_partial]):
971 if not (mode == 'init' or 'id' in fields):
972 _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
978 if not (line and any(line)):
981 datas.append(map(misc.ustr, line))
983 _logger.error("Cannot import the line: %s", line)
985 registry = openerp.registry(cr.dbname)
986 result, rows, warning_msg, dummy = registry[model].import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
988 # Report failed import and abort module install
989 raise Exception(_('Module loading failed: file %s/%s could not be processed:\n %s') % (module, fname, warning_msg))
990 if config.get('import_partial'):
991 data = pickle.load(file(config.get('import_partial')))
992 data[fname_partial] = 0
993 pickle.dump(data, file(config.get('import_partial'),'wb'))
999 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
1000 doc = etree.parse(xmlfile)
1001 relaxng = etree.RelaxNG(
1002 etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
1004 relaxng.assert_(doc)
1006 _logger.error('The XML file does not fit the required schema !')
1007 _logger.error(misc.ustr(relaxng.error_log.last_error))
1012 obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
1013 obj.parse(doc.getroot())
1017 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: