1 # -*- coding: utf-8 -*-
4 import time # used to eval time.strftime expressions
5 from datetime import datetime, timedelta
9 import openerp.sql_db as sql_db
10 import openerp.workflow
12 from config import config
16 from lxml import etree
17 from openerp import SUPERUSER_ID
19 # YAML import needs both safe and unsafe eval, but let's
22 from safe_eval import safe_eval as eval
24 import assertion_report
26 _logger = logging.getLogger(__name__)
28 class YamlImportException(Exception):
31 class YamlImportAbortion(Exception):
34 def _is_yaml_mapping(node, tag_constructor):
35 value = isinstance(node, types.DictionaryType) \
36 and len(node.keys()) == 1 \
37 and isinstance(node.keys()[0], tag_constructor)
41 return isinstance(node, types.StringTypes)
44 return isinstance(node, yaml_tag.Assert) \
45 or _is_yaml_mapping(node, yaml_tag.Assert)
48 return _is_yaml_mapping(node, yaml_tag.Record)
51 return _is_yaml_mapping(node, yaml_tag.Python)
53 def is_menuitem(node):
54 return isinstance(node, yaml_tag.Menuitem) \
55 or _is_yaml_mapping(node, yaml_tag.Menuitem)
57 def is_function(node):
58 return isinstance(node, yaml_tag.Function) \
59 or _is_yaml_mapping(node, yaml_tag.Function)
62 return isinstance(node, yaml_tag.Report)
64 def is_workflow(node):
65 return isinstance(node, yaml_tag.Workflow)
67 def is_act_window(node):
68 return isinstance(node, yaml_tag.ActWindow)
71 return isinstance(node, yaml_tag.Delete)
74 return isinstance(node, yaml_tag.Context)
77 return isinstance(node, yaml_tag.Url)
80 return isinstance(node, yaml_tag.Eval)
83 return isinstance(node, yaml_tag.Ref) \
84 or _is_yaml_mapping(node, yaml_tag.Ref)
87 return _is_yaml_mapping(node, yaml_tag.IrSet)
90 return isinstance(node, basestring)
92 class RecordDictWrapper(dict):
94 Used to pass a record as locals in eval:
95 records do not strictly behave like dict, so we force them to.
97 def __init__(self, record):
99 def __getitem__(self, key):
100 if key in self.record:
101 return self.record[key]
102 return dict.__getitem__(self, key)
104 class YamlInterpreter(object):
105 def __init__(self, cr, module, id_map, mode, filename, report=None, noupdate=False, loglevel=logging.DEBUG):
110 self.filename = filename
112 report = assertion_report.assertion_report()
113 self.assertion_report = report
114 self.noupdate = noupdate
115 self.loglevel = loglevel
116 self.pool = openerp.registry(cr.dbname)
118 self.context = {} # opererp context
119 self.eval_context = {'ref': self._ref(),
120 '_ref': self._ref(), # added '_ref' so that record['ref'] is possible
122 'datetime': datetime,
123 'timedelta': timedelta}
125 def _log(self, *args, **kwargs):
126 _logger.log(self.loglevel, *args, **kwargs)
129 return lambda xml_id: self.get_id(xml_id)
131 def get_model(self, model_name):
132 return self.pool[model_name]
134 def validate_xml_id(self, xml_id):
137 module, id = xml_id.split('.', 1)
138 assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
139 "It is used to refer to other modules ID, in the form: module.record_id" \
141 if module != self.module:
142 module_count = self.pool['ir.module.module'].search_count(self.cr, self.uid, \
143 ['&', ('name', '=', module), ('state', 'in', ['installed'])])
144 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
145 if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
146 _logger.error('id: %s is to long (max: 64)', id)
148 def get_id(self, xml_id):
149 if xml_id is False or xml_id is None:
152 # raise YamlImportException("The xml_id should be a non empty string.")
153 elif isinstance(xml_id, types.IntType):
155 elif xml_id in self.id_map:
156 id = self.id_map[xml_id]
159 module, checked_xml_id = xml_id.split('.', 1)
162 checked_xml_id = xml_id
164 _, id = self.pool['ir.model.data'].get_object_reference(self.cr, self.uid, module, checked_xml_id)
165 self.id_map[xml_id] = id
167 raise ValueError("""%s not found when processing %s.
168 This Yaml file appears to depend on missing data. This often happens for
169 tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
173 def get_context(self, node, eval_dict):
174 context = self.context.copy()
176 context.update(eval(node.context, eval_dict))
179 def isnoupdate(self, node):
180 return self.noupdate or node.noupdate or False
182 def _get_first_result(self, results, default=False):
185 if isinstance(value, types.TupleType):
191 def process_comment(self, node):
194 def _log_assert_failure(self, msg, *args):
195 self.assertion_report.record_failure()
196 _logger.error(msg, *args)
198 def _get_assertion_id(self, assertion):
200 ids = [self.get_id(assertion.id)]
201 elif assertion.search:
202 q = eval(assertion.search, self.eval_context)
203 ids = self.pool[assertion.model].search(self.cr, self.uid, q, context=assertion.context)
205 raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
208 def process_assert(self, node):
209 if isinstance(node, dict):
210 assertion, expressions = node.items()[0]
212 assertion, expressions = node, []
214 if self.isnoupdate(assertion) and self.mode != 'init':
215 _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
217 model = self.get_model(assertion.model)
218 ids = self._get_assertion_id(assertion)
219 if assertion.count is not None and len(ids) != assertion.count:
220 msg = 'assertion "%s" failed!\n' \
221 ' Incorrect search count:\n' \
222 ' expected count: %d\n' \
223 ' obtained count: %d\n'
224 args = (assertion.string, assertion.count, len(ids))
225 self._log_assert_failure(msg, *args)
227 context = self.get_context(assertion, self.eval_context)
229 record = model.browse(self.cr, self.uid, id, context)
230 for test in expressions:
232 success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
234 _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
235 raise YamlImportAbortion(e)
237 msg = 'Assertion "%s" FAILED\ntest: %s\n'
238 args = (assertion.string, test)
239 for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
241 left, right = test.split(aop,1)
245 lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
250 rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
254 msg += 'values: ! %s %s %s'
255 args += ( lmsg, aop, rmsg )
258 self._log_assert_failure(msg, *args)
260 else: # all tests were successful for this assertion tag (no break)
261 self.assertion_report.record_success()
263 def _coerce_bool(self, value, default=False):
264 if isinstance(value, types.BooleanType):
266 if isinstance(value, types.StringTypes):
267 b = value.strip().lower() not in ('0', 'false', 'off', 'no')
268 elif isinstance(value, types.IntType):
274 def create_osv_memory_record(self, record, fields):
275 model = self.get_model(record.model)
276 context = self.get_context(record, self.eval_context)
277 record_dict = self._create_record(model, fields)
278 id_new = model.create(self.cr, self.uid, record_dict, context=context)
279 self.id_map[record.id] = int(id_new)
282 def process_record(self, node):
283 record, fields = node.items()[0]
284 model = self.get_model(record.model)
286 view_id = record.view
287 if view_id and (view_id is not True) and isinstance(view_id, basestring):
290 module, view_id = view_id.split('.',1)
291 view_id = self.pool['ir.model.data'].get_object_reference(self.cr, SUPERUSER_ID, module, view_id)[1]
293 if model.is_transient():
294 record_dict=self.create_osv_memory_record(record, fields)
296 self.validate_xml_id(record.id)
298 self.pool['ir.model.data']._get_id(self.cr, SUPERUSER_ID, self.module, record.id)
303 if self.isnoupdate(record) and self.mode != 'init':
304 id = self.pool['ir.model.data']._update_dummy(self.cr, SUPERUSER_ID, record.model, self.module, record.id)
305 # check if the resource already existed at the last update
307 self.id_map[record] = int(id)
310 if not self._coerce_bool(record.forcecreate):
314 #context = self.get_context(record, self.eval_context)
315 #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module
316 context = record.context
320 if view_id is True: varg = False
321 view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context)
323 record_dict = self._create_record(model, fields, view_info, default=default)
324 _logger.debug("RECORD_DICT %s" % record_dict)
325 id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, record.model, \
326 self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context)
327 self.id_map[record.id] = int(id)
328 if config.get('import_partial'):
331 def _create_record(self, model, fields, view_info=None, parent={}, default=True):
332 """This function processes the !record tag in yalm files. It simulates the record creation through an xml
333 view (either specified on the !record tag or the default one for this object), including the calls to
334 on_change() functions, and sending only values for fields that aren't set as readonly.
335 :param model: model instance
336 :param fields: dictonary mapping the field names and their values
337 :param view_info: result of fields_view_get() called on the object
338 :param parent: dictionary containing the values already computed for the parent, in case of one2many fields
339 :param default: if True, the default values must be processed too or not
340 :return: dictionary mapping the field names and their values, ready to use when calling the create() function
343 def _get_right_one2many_view(fg, field_name, view_type):
344 one2many_view = fg[field_name]['views'].get(view_type)
345 # if the view is not defined inline, we call fields_view_get()
346 if not one2many_view:
347 one2many_view = self.pool[fg[field_name]['relation']].fields_view_get(self.cr, SUPERUSER_ID, False, view_type, self.context)
350 def process_val(key, val):
351 if fg[key]['type'] == 'many2one':
352 if type(val) in (tuple,list):
354 elif fg[key]['type'] == 'one2many':
355 if val and isinstance(val, (list,tuple)) and isinstance(val[0], dict):
356 # we want to return only the fields that aren't readonly
357 # For that, we need to first get the right tree view to consider for the field `key´
358 one2many_tree_view = _get_right_one2many_view(fg, key, 'tree')
359 arch = etree.fromstring(one2many_tree_view['arch'].encode('utf-8'))
361 # make a copy for the iteration, as we will alter `rec´
362 rec_copy = rec.copy()
363 for field_key in rec_copy:
364 # if field is missing in view or has a readonly modifier, drop it
365 field_elem = arch.xpath("//field[@name='%s']" % field_key)
366 if field_elem and (field_elem[0].get('modifiers', '{}').find('"readonly": true') >= 0):
367 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
368 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
370 # now that unwanted values have been removed from val, we can encapsulate it in a tuple as returned value
371 val = map(lambda x: (0,0,x), val)
372 elif fg[key]['type'] == 'many2many':
373 if val and isinstance(val,(list,tuple)) and isinstance(val[0], (int,long)):
376 # we want to return only the fields that aren't readonly
377 if el.get('modifiers', '{}').find('"readonly": true') >= 0:
378 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
379 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
385 arch = etree.fromstring(view_info['arch'].decode('utf-8'))
386 view = arch if len(arch) else False
389 fields = fields or {}
390 if view is not False:
391 fg = view_info['fields']
392 # gather the default values on the object. (Can't use `fields´ as parameter instead of {} because we may
393 # have references like `base.main_company´ in the yaml file and it's not compatible with the function)
394 defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {}
396 # copy the default values in record_dict, only if they are in the view (because that's what the client does)
397 # the other default values will be added later on by the create().
398 record_dict = dict([(key, val) for key, val in defaults.items() if key in fg])
400 # Process all on_change calls
405 field_name = el.attrib['name']
406 assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name)
407 if field_name in fields:
408 one2many_form_view = None
409 if (view is not False) and (fg[field_name]['type']=='one2many'):
410 # for one2many fields, we want to eval them using the inline form view defined on the parent
411 one2many_form_view = _get_right_one2many_view(fg, field_name, 'form')
413 field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default)
415 #call process_val to not update record_dict if values were given for readonly fields
416 val = process_val(field_name, field_value)
418 record_dict[field_name] = val
419 #if (field_name in defaults) and defaults[field_name] == field_value:
420 # print '*** You can remove these lines:', field_name, field_value
422 #if field_name has a default value or a value is given in the yaml file, we must call its on_change()
423 elif field_name not in defaults:
426 if not el.attrib.get('on_change', False):
428 match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change'])
429 assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], )
431 # creating the context
432 class parent2(object):
433 def __init__(self, d):
435 def __getattr__(self, name):
436 return self.d.get(name, False)
438 ctx = record_dict.copy()
439 ctx['context'] = self.context
440 ctx['uid'] = SUPERUSER_ID
441 ctx['parent'] = parent2(parent)
444 ctx[a] = process_val(a, defaults.get(a, False))
447 args = map(lambda x: eval(x, ctx), match.group(2).split(','))
448 result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
449 for key, val in (result or {}).get('value', {}).items():
451 "The field %r returned from the onchange call %r "
452 "does not exist in the source view %r (of object "
453 "%r). This field will be ignored (and thus not "
454 "populated) when clients saves the new record" % (
455 key, match.group(1), view_info.get('name', '?'), model._name
457 if key not in fields:
458 # do not shadow values explicitly set in yaml.
459 record_dict[key] = process_val(key, val)
461 nodes = list(el) + nodes
465 for field_name, expression in fields.items():
466 if field_name in record_dict:
468 field_value = self._eval_field(model, field_name, expression, default=False)
469 record_dict[field_name] = field_value
472 def process_ref(self, node, column=None):
473 assert node.search or node.id, '!ref node should have a `search` attribute or `id` attribute'
476 model_name = node.model
478 model_name = column._obj
480 raise YamlImportException('You need to give a model for the search, or a column to infer it.')
481 model = self.get_model(model_name)
482 q = eval(node.search, self.eval_context)
483 ids = model.search(self.cr, self.uid, q)
485 instances = model.browse(self.cr, self.uid, ids)
486 value = [inst[node.use] for inst in instances]
490 value = self.get_id(node.id)
495 def process_eval(self, node):
496 return eval(node.expression, self.eval_context)
498 def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True):
499 # TODO this should be refactored as something like model.get_field() in bin/osv
500 if field_name in model._columns:
501 column = model._columns[field_name]
502 elif field_name in model._inherit_fields:
503 column = model._inherit_fields[field_name][2]
505 raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name))
506 if is_ref(expression):
507 elements = self.process_ref(expression, column)
508 if column._type in ("many2many", "one2many"):
509 value = [(6, 0, elements)]
511 if isinstance(elements, (list,tuple)):
512 value = self._get_first_result(elements)
515 elif column._type == "many2one":
516 value = self.get_id(expression)
517 elif column._type == "one2many":
518 other_model = self.get_model(column._obj)
519 value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression]
520 elif column._type == "many2many":
521 ids = [self.get_id(xml_id) for xml_id in expression]
522 value = [(6, 0, ids)]
523 elif column._type == "date" and is_string(expression):
524 # enforce ISO format for string date values, to be locale-agnostic during tests
525 time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
527 elif column._type == "datetime" and is_string(expression):
528 # enforce ISO format for string datetime values, to be locale-agnostic during tests
529 time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
532 if is_eval(expression):
533 value = self.process_eval(expression)
536 # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
539 def process_context(self, node):
540 self.context = node.__dict__
542 self.uid = self.get_id(node.uid)
544 self.noupdate = node.noupdate
546 def process_python(self, node):
547 python, statements = node.items()[0]
548 model = self.get_model(python.model)
549 statements = statements.replace("\r\n", "\n")
555 'context': self.context,
558 code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
560 code_obj = compile(statements, self.filename, 'exec')
561 unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
562 except AssertionError, e:
563 self._log_assert_failure('AssertionError in Python code %s: %s', python.name, e)
566 _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
569 self.assertion_report.record_success()
571 def process_workflow(self, node):
572 workflow, values = node.items()[0]
573 if self.isnoupdate(workflow) and self.mode != 'init':
576 id = self.get_id(workflow.ref)
579 raise YamlImportException('You must define a child node if you do not give a ref.')
580 if not len(values) == 1:
581 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
583 if not 'model' in value and (not 'eval' in value or not 'search' in value):
584 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
585 value_model = self.get_model(value['model'])
586 local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
587 local_context.update(self.id_map)
588 id = eval(value['eval'], self.eval_context, local_context)
590 if workflow.uid is not None:
594 self.cr.execute('select distinct signal from wkf_transition')
595 signals=[x['signal'] for x in self.cr.dictfetchall()]
596 if workflow.action not in signals:
597 raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
598 openerp.workflow.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
600 def _eval_params(self, model, params):
602 for i, param in enumerate(params):
603 if isinstance(param, types.ListType):
604 value = self._eval_params(model, param)
606 value = self.process_ref(param)
608 value = self.process_eval(param)
609 elif isinstance(param, types.DictionaryType): # supports XML syntax
610 param_model = self.get_model(param.get('model', model))
611 if 'search' in param:
612 q = eval(param['search'], self.eval_context)
613 ids = param_model.search(self.cr, self.uid, q)
614 value = self._get_first_result(ids)
615 elif 'eval' in param:
616 local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
617 local_context.update(self.id_map)
618 value = eval(param['eval'], self.eval_context, local_context)
620 raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
622 value = param # scalar value
626 def process_function(self, node):
627 function, params = node.items()[0]
628 if self.isnoupdate(function) and self.mode != 'init':
630 model = self.get_model(function.model)
632 args = self.process_eval(function.eval)
634 args = self._eval_params(function.model, params)
635 method = function.name
636 getattr(model, method)(self.cr, self.uid, *args)
638 def _set_group_values(self, node, values):
640 group_names = node.groups.split(',')
642 for group in group_names:
643 if group.startswith('-'):
644 group_id = self.get_id(group[1:])
645 groups_value.append((3, group_id))
647 group_id = self.get_id(group)
648 groups_value.append((4, group_id))
649 values['groups_id'] = groups_value
651 def process_menuitem(self, node):
652 self.validate_xml_id(node.id)
656 self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
657 res = self.cr.fetchone()
658 values = {'parent_id': parent_id, 'name': node.name}
660 parent_id = self.get_id(node.parent)
661 values = {'parent_id': parent_id}
663 values['name'] = node.name
665 res = [ self.get_id(node.id) ]
666 except: # which exception ?
670 action_type = node.type or 'act_window'
672 "act_window": 'STOCK_NEW',
673 "report.xml": 'STOCK_PASTE',
674 "wizard": 'STOCK_EXECUTE',
675 "url": 'STOCK_JUMP_TO',
677 values['icon'] = icons.get(action_type, 'STOCK_NEW')
678 if action_type == 'act_window':
679 action_id = self.get_id(node.action)
680 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
681 ir_act_window_result = self.cr.fetchone()
682 assert ir_act_window_result, "No window action defined for this id %s !\n" \
683 "Verify that this is a window action or add a type argument." % (node.action,)
684 action_type, action_mode, action_name, view_id, target = ir_act_window_result
686 self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
687 # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
688 action_mode = self.cr.fetchone()
689 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
691 action_mode = self.cr.fetchone()
692 if action_type == 'tree':
693 values['icon'] = 'STOCK_INDENT'
694 elif action_mode and action_mode.startswith('tree'):
695 values['icon'] = 'STOCK_JUSTIFY_FILL'
696 elif action_mode and action_mode.startswith('graph'):
697 values['icon'] = 'terp-graph'
698 elif action_mode and action_mode.startswith('calendar'):
699 values['icon'] = 'terp-calendar'
701 values['icon'] = 'STOCK_EXECUTE'
702 if not values.get('name', False):
703 values['name'] = action_name
704 elif action_type == 'wizard':
705 action_id = self.get_id(node.action)
706 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
707 ir_act_wizard_result = self.cr.fetchone()
708 if (not values.get('name', False)) and ir_act_wizard_result:
709 values['name'] = ir_act_wizard_result[0]
711 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
713 values['sequence'] = node.sequence
715 values['icon'] = node.icon
717 self._set_group_values(node, values)
719 pid = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, \
720 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
721 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
723 if node.id and parent_id:
724 self.id_map[node.id] = int(parent_id)
726 if node.action and pid:
727 action_type = node.type or 'act_window'
728 action_id = self.get_id(node.action)
729 action = "ir.actions.%s,%d" % (action_type, action_id)
730 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', \
731 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
733 def process_act_window(self, node):
734 assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
735 assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
736 assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
737 self.validate_xml_id(node.id)
740 view_id = self.get_id(node.view)
743 context = eval(str(node.context), self.eval_context)
746 'type': node.type or 'ir.actions.act_window',
748 'domain': node.domain,
750 'res_model': node.res_model,
751 'src_model': node.src_model,
752 'view_type': node.view_type or 'form',
753 'view_mode': node.view_mode or 'tree,form',
756 'auto_refresh': node.auto_refresh,
757 'multi': getattr(node, 'multi', False),
760 self._set_group_values(node, values)
763 values['target'] = node.target
764 id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, \
765 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
766 self.id_map[node.id] = int(id)
769 keyword = 'client_action_relate'
770 value = 'ir.actions.act_window,%s' % id
771 replace = node.replace or True
772 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
773 node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
774 # TODO add remove ir.model.data
776 def process_delete(self, node):
777 assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
778 if node.model in self.pool:
780 ids = self.pool[node.model].search(self.cr, self.uid, eval(node.search, self.eval_context))
782 ids = [self.get_id(node.id)]
784 self.pool[node.model].unlink(self.cr, self.uid, ids)
786 self._log("Record not deleted.")
788 def process_url(self, node):
789 self.validate_xml_id(node.id)
791 res = {'name': node.name, 'url': node.url, 'target': node.target}
793 id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, \
794 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
795 self.id_map[node.id] = int(id)
797 if (not node.menu or eval(node.menu)) and id:
798 keyword = node.keyword or 'client_action_multi'
799 value = 'ir.actions.act_url,%s' % id
800 replace = node.replace or True
801 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', \
802 keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
803 noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
805 def process_ir_set(self, node):
806 if not self.mode == 'init':
808 _, fields = node.items()[0]
810 for fieldname, expression in fields.items():
811 if is_eval(expression):
812 value = eval(expression.expression, self.eval_context)
815 res[fieldname] = value
816 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
817 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
818 isobject=res.get('isobject', False), meta=res.get('meta',None))
820 def process_report(self, node):
822 for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
823 values[dest] = getattr(node, f)
824 assert values[dest], "Attribute %s of report is empty !" % (f,)
825 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
826 if getattr(node, field):
827 values[dest] = getattr(node, field)
829 values['auto'] = eval(node.auto)
831 sxw_file = misc.file_open(node.sxw)
833 sxw_content = sxw_file.read()
834 values['report_sxw_content'] = sxw_content
838 values['header'] = eval(node.header)
839 values['multi'] = node.multi and eval(node.multi)
841 self.validate_xml_id(xml_id)
843 self._set_group_values(node, values)
845 id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
846 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
847 self.id_map[xml_id] = int(id)
849 if not node.menu or eval(node.menu):
850 keyword = node.keyword or 'client_print_multi'
851 value = 'ir.actions.report.xml,%s' % id
852 replace = node.replace or True
853 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', \
854 keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
856 def process_none(self):
858 Empty node or commented node should not pass silently.
860 self._log_assert_failure("You have an empty block in your tests.")
863 def process(self, yaml_string):
865 Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
867 yaml_tag.add_constructors()
869 is_preceded_by_comment = False
870 for node in yaml.load(yaml_string):
871 is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
873 self._process_node(node)
878 def _process_node(self, node):
880 self.process_comment(node)
881 elif is_assert(node):
882 self.process_assert(node)
883 elif is_record(node):
884 self.process_record(node)
885 elif is_python(node):
886 self.process_python(node)
887 elif is_menuitem(node):
888 self.process_menuitem(node)
889 elif is_delete(node):
890 self.process_delete(node)
892 self.process_url(node)
893 elif is_context(node):
894 self.process_context(node)
895 elif is_ir_set(node):
896 self.process_ir_set(node)
897 elif is_act_window(node):
898 self.process_act_window(node)
899 elif is_report(node):
900 self.process_report(node)
901 elif is_workflow(node):
902 if isinstance(node, types.DictionaryType):
903 self.process_workflow(node)
905 self.process_workflow({node: []})
906 elif is_function(node):
907 if isinstance(node, types.DictionaryType):
908 self.process_function(node)
910 self.process_function({node: []})
914 raise YamlImportException("Can not process YAML block: %s" % node)
916 def _log_node(self, node, is_preceded_by_comment):
918 is_preceded_by_comment = True
920 elif not is_preceded_by_comment:
921 if isinstance(node, types.DictionaryType):
922 msg = "Creating %s\n with %s"
923 args = node.items()[0]
924 self._log(msg, *args)
928 is_preceded_by_comment = False
929 return is_preceded_by_comment
931 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
934 loglevel = logging.INFO if kind == 'test' else logging.DEBUG
935 yaml_string = yamlfile.read()
936 yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
937 yaml_interpreter.process(yaml_string)
939 # keeps convention of convert.py
940 convert_yaml_import = yaml_import
942 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
948 cr = sql_db.db_connect(db_name).cursor()
949 fp = misc.file_open(file_name)
950 convert_yaml_import(cr, module_name, fp, {}, 'update', True)
954 threading.Thread(target=f).start()
957 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: