1 # -*- coding: utf-8 -*-
4 import time # used to eval time.strftime expressions
5 from datetime import datetime, timedelta
9 import openerp.pooler as pooler
10 import openerp.sql_db as sql_db
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 = pooler.get_pool(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 model = self.pool.get(model_name)
133 assert model, "The model %s does not exist." % (model_name,)
136 def validate_xml_id(self, xml_id):
139 module, id = xml_id.split('.', 1)
140 assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
141 "It is used to refer to other modules ID, in the form: module.record_id" \
143 if module != self.module:
144 module_count = self.pool.get('ir.module.module').search_count(self.cr, self.uid, \
145 ['&', ('name', '=', module), ('state', 'in', ['installed'])])
146 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
147 if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
148 _logger.error('id: %s is to long (max: 64)', id)
150 def get_id(self, xml_id):
151 if xml_id is False or xml_id is None:
154 # raise YamlImportException("The xml_id should be a non empty string.")
155 elif isinstance(xml_id, types.IntType):
157 elif xml_id in self.id_map:
158 id = self.id_map[xml_id]
161 module, checked_xml_id = xml_id.split('.', 1)
164 checked_xml_id = xml_id
166 _, id = self.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, module, checked_xml_id)
167 self.id_map[xml_id] = id
169 raise ValueError("""%s not found when processing %s.
170 This Yaml file appears to depend on missing data. This often happens for
171 tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
175 def get_context(self, node, eval_dict):
176 context = self.context.copy()
178 context.update(eval(node.context, eval_dict))
181 def isnoupdate(self, node):
182 return self.noupdate or node.noupdate or False
184 def _get_first_result(self, results, default=False):
187 if isinstance(value, types.TupleType):
193 def process_comment(self, node):
196 def _log_assert_failure(self, msg, *args):
197 self.assertion_report.record_failure()
198 _logger.error(msg, *args)
200 def _get_assertion_id(self, assertion):
202 ids = [self.get_id(assertion.id)]
203 elif assertion.search:
204 q = eval(assertion.search, self.eval_context)
205 ids = self.pool.get(assertion.model).search(self.cr, self.uid, q, context=assertion.context)
207 raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
210 def process_assert(self, node):
211 if isinstance(node, dict):
212 assertion, expressions = node.items()[0]
214 assertion, expressions = node, []
216 if self.isnoupdate(assertion) and self.mode != 'init':
217 _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
219 model = self.get_model(assertion.model)
220 ids = self._get_assertion_id(assertion)
221 if assertion.count is not None and len(ids) != assertion.count:
222 msg = 'assertion "%s" failed!\n' \
223 ' Incorrect search count:\n' \
224 ' expected count: %d\n' \
225 ' obtained count: %d\n'
226 args = (assertion.string, assertion.count, len(ids))
227 self._log_assert_failure(msg, *args)
229 context = self.get_context(assertion, self.eval_context)
231 record = model.browse(self.cr, self.uid, id, context)
232 for test in expressions:
234 success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
236 _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
237 raise YamlImportAbortion(e)
239 msg = 'Assertion "%s" FAILED\ntest: %s\n'
240 args = (assertion.string, test)
241 for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
243 left, right = test.split(aop,1)
247 lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
252 rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
256 msg += 'values: ! %s %s %s'
257 args += ( lmsg, aop, rmsg )
260 self._log_assert_failure(msg, *args)
262 else: # all tests were successful for this assertion tag (no break)
263 self.assertion_report.record_success()
265 def _coerce_bool(self, value, default=False):
266 if isinstance(value, types.BooleanType):
268 if isinstance(value, types.StringTypes):
269 b = value.strip().lower() not in ('0', 'false', 'off', 'no')
270 elif isinstance(value, types.IntType):
276 def create_osv_memory_record(self, record, fields):
277 model = self.get_model(record.model)
278 context = self.get_context(record, self.eval_context)
279 record_dict = self._create_record(model, fields)
280 id_new = model.create(self.cr, self.uid, record_dict, context=context)
281 self.id_map[record.id] = int(id_new)
284 def process_record(self, node):
285 record, fields = node.items()[0]
286 model = self.get_model(record.model)
288 view_id = record.view
289 if view_id and (view_id is not True) and isinstance(view_id, basestring):
292 module, view_id = view_id.split('.',1)
293 view_id = self.pool.get('ir.model.data').get_object_reference(self.cr, SUPERUSER_ID, module, view_id)[1]
295 if model.is_transient():
296 record_dict=self.create_osv_memory_record(record, fields)
298 self.validate_xml_id(record.id)
300 self.pool.get('ir.model.data')._get_id(self.cr, SUPERUSER_ID, self.module, record.id)
305 if self.isnoupdate(record) and self.mode != 'init':
306 id = self.pool.get('ir.model.data')._update_dummy(self.cr, SUPERUSER_ID, record.model, self.module, record.id)
307 # check if the resource already existed at the last update
309 self.id_map[record] = int(id)
312 if not self._coerce_bool(record.forcecreate):
316 #context = self.get_context(record, self.eval_context)
317 #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module
318 context = record.context
322 if view_id is True: varg = False
323 view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context)
325 record_dict = self._create_record(model, fields, view_info, default=default)
326 _logger.debug("RECORD_DICT %s" % record_dict)
327 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, record.model, \
328 self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context)
329 self.id_map[record.id] = int(id)
330 if config.get('import_partial'):
333 def _create_record(self, model, fields, view_info=False, parent={}, default=True):
334 """This function processes the !record tag in yalm files. It simulates the record creation through an xml
335 view (either specified on the !record tag or the default one for this object), including the calls to
336 on_change() functions, and sending only values for fields that aren't set as readonly.
337 :param model: model instance
338 :param fields: dictonary mapping the field names and their values
339 :param view_info: result of fields_view_get() called on the object
340 :param parent: dictionary containing the values already computed for the parent, in case of one2many fields
341 :param default: if True, the default values must be processed too or not
342 :return: dictionary mapping the field names and their values, ready to use when calling the create() function
345 def _get_right_one2many_view(fg, field_name, view_type):
346 one2many_view = fg[field_name]['views'].get(view_type)
347 # if the view is not defined inline, we call fields_view_get()
348 if not one2many_view:
349 one2many_view = self.pool.get(fg[field_name]['relation']).fields_view_get(self.cr, SUPERUSER_ID, False, view_type, self.context)
352 def process_val(key, val):
353 if fg[key]['type'] == 'many2one':
354 if type(val) in (tuple,list):
356 elif fg[key]['type'] == 'one2many':
357 if val and isinstance(val, (list,tuple)) and isinstance(val[0], dict):
358 # we want to return only the fields that aren't readonly
359 # For that, we need to first get the right tree view to consider for the field `key´
360 one2many_tree_view = _get_right_one2many_view(fg, key, 'tree')
361 arch = etree.fromstring(one2many_tree_view['arch'].encode('utf-8'))
363 # make a copy for the iteration, as we will alter `rec´
364 rec_copy = rec.copy()
365 for field_key in rec_copy:
366 # if field is missing in view or has a readonly modifier, drop it
367 field_elem = arch.xpath("//field[@name='%s']" % field_key)
368 if field_elem and (field_elem[0].get('modifiers', '{}').find('"readonly": true') >= 0):
369 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
370 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
372 # now that unwanted values have been removed from val, we can encapsulate it in a tuple as returned value
373 val = map(lambda x: (0,0,x), val)
374 elif fg[key]['type'] == 'many2many':
375 if val and isinstance(val,(list,tuple)) and isinstance(val[0], (int,long)):
378 # we want to return only the fields that aren't readonly
379 if el.get('modifiers', '{}').find('"readonly": true') >= 0:
380 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
381 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
387 arch = etree.fromstring(view_info['arch'].decode('utf-8'))
388 view = arch if len(arch) else False
391 fields = fields or {}
392 if view is not False:
393 fg = view_info['fields']
394 # gather the default values on the object. (Can't use `fields´ as parameter instead of {} because we may
395 # have references like `base.main_company´ in the yaml file and it's not compatible with the function)
396 defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {}
398 # copy the default values in record_dict, only if they are in the view (because that's what the client does)
399 # the other default values will be added later on by the create().
400 record_dict = dict([(key, val) for key, val in defaults.items() if key in fg])
402 # Process all on_change calls
407 field_name = el.attrib['name']
408 assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name)
409 if field_name in fields:
410 one2many_form_view = None
411 if (view is not False) and (fg[field_name]['type']=='one2many'):
412 # for one2many fields, we want to eval them using the inline form view defined on the parent
413 one2many_form_view = _get_right_one2many_view(fg, field_name, 'form')
415 field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default)
417 #call process_val to not update record_dict if values were given for readonly fields
418 val = process_val(field_name, field_value)
420 record_dict[field_name] = val
421 #if (field_name in defaults) and defaults[field_name] == field_value:
422 # print '*** You can remove these lines:', field_name, field_value
424 #if field_name has a default value or a value is given in the yaml file, we must call its on_change()
425 elif field_name not in defaults:
428 if not el.attrib.get('on_change', False):
430 match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change'])
431 assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], )
433 # creating the context
434 class parent2(object):
435 def __init__(self, d):
437 def __getattr__(self, name):
438 return self.d.get(name, False)
440 ctx = record_dict.copy()
441 ctx['context'] = self.context
442 ctx['uid'] = SUPERUSER_ID
443 ctx['parent'] = parent2(parent)
446 ctx[a] = process_val(a, defaults.get(a, False))
449 args = map(lambda x: eval(x, ctx), match.group(2).split(','))
450 result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
451 for key, val in (result or {}).get('value', {}).items():
452 assert key in fg, "The returning field '%s' from your on_change call '%s' does not exist either on the object '%s', either in the view '%s' used for the creation" % (key, match.group(1), model._name, view_info['name'])
453 record_dict[key] = process_val(key, val)
454 #if (key in fields) and record_dict[key] == process_val(key, val):
455 # print '*** You can remove these lines:', key, val
457 nodes = list(el) + nodes
461 for field_name, expression in fields.items():
462 if field_name in record_dict:
464 field_value = self._eval_field(model, field_name, expression, default=False)
465 record_dict[field_name] = field_value
468 def process_ref(self, node, column=None):
469 assert node.search or node.id, '!ref node should have a `search` attribute or `id` attribute'
472 model_name = node.model
474 model_name = column._obj
476 raise YamlImportException('You need to give a model for the search, or a column to infer it.')
477 model = self.get_model(model_name)
478 q = eval(node.search, self.eval_context)
479 ids = model.search(self.cr, self.uid, q)
481 instances = model.browse(self.cr, self.uid, ids)
482 value = [inst[node.use] for inst in instances]
486 value = self.get_id(node.id)
491 def process_eval(self, node):
492 return eval(node.expression, self.eval_context)
494 def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True):
495 # TODO this should be refactored as something like model.get_field() in bin/osv
496 if field_name in model._columns:
497 column = model._columns[field_name]
498 elif field_name in model._inherit_fields:
499 column = model._inherit_fields[field_name][2]
501 raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name))
502 if is_ref(expression):
503 elements = self.process_ref(expression, column)
504 if column._type in ("many2many", "one2many"):
505 value = [(6, 0, elements)]
507 if isinstance(elements, (list,tuple)):
508 value = self._get_first_result(elements)
511 elif column._type == "many2one":
512 value = self.get_id(expression)
513 elif column._type == "one2many":
514 other_model = self.get_model(column._obj)
515 value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression]
516 elif column._type == "many2many":
517 ids = [self.get_id(xml_id) for xml_id in expression]
518 value = [(6, 0, ids)]
519 elif column._type == "date" and is_string(expression):
520 # enforce ISO format for string date values, to be locale-agnostic during tests
521 time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
523 elif column._type == "datetime" and is_string(expression):
524 # enforce ISO format for string datetime values, to be locale-agnostic during tests
525 time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
528 if is_eval(expression):
529 value = self.process_eval(expression)
532 # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
535 def process_context(self, node):
536 self.context = node.__dict__
538 self.uid = self.get_id(node.uid)
540 self.noupdate = node.noupdate
542 def process_python(self, node):
543 python, statements = node.items()[0]
544 model = self.get_model(python.model)
545 statements = statements.replace("\r\n", "\n")
551 'context': self.context,
554 code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
556 code_obj = compile(statements, self.filename, 'exec')
557 unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
558 except AssertionError, e:
559 self._log_assert_failure('AssertionError in Python code %s: %s', python.name, e)
562 _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
565 self.assertion_report.record_success()
567 def process_workflow(self, node):
568 workflow, values = node.items()[0]
569 if self.isnoupdate(workflow) and self.mode != 'init':
572 id = self.get_id(workflow.ref)
575 raise YamlImportException('You must define a child node if you do not give a ref.')
576 if not len(values) == 1:
577 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
579 if not 'model' in value and (not 'eval' in value or not 'search' in value):
580 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
581 value_model = self.get_model(value['model'])
582 local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
583 local_context.update(self.id_map)
584 id = eval(value['eval'], self.eval_context, local_context)
586 if workflow.uid is not None:
590 self.cr.execute('select distinct signal from wkf_transition')
591 signals=[x['signal'] for x in self.cr.dictfetchall()]
592 if workflow.action not in signals:
593 raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
594 import openerp.netsvc as netsvc
595 wf_service = netsvc.LocalService("workflow")
596 wf_service.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
598 def _eval_params(self, model, params):
600 for i, param in enumerate(params):
601 if isinstance(param, types.ListType):
602 value = self._eval_params(model, param)
604 value = self.process_ref(param)
606 value = self.process_eval(param)
607 elif isinstance(param, types.DictionaryType): # supports XML syntax
608 param_model = self.get_model(param.get('model', model))
609 if 'search' in param:
610 q = eval(param['search'], self.eval_context)
611 ids = param_model.search(self.cr, self.uid, q)
612 value = self._get_first_result(ids)
613 elif 'eval' in param:
614 local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
615 local_context.update(self.id_map)
616 value = eval(param['eval'], self.eval_context, local_context)
618 raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
620 value = param # scalar value
624 def process_function(self, node):
625 function, params = node.items()[0]
626 if self.isnoupdate(function) and self.mode != 'init':
628 model = self.get_model(function.model)
630 args = self.process_eval(function.eval)
632 args = self._eval_params(function.model, params)
633 method = function.name
634 getattr(model, method)(self.cr, self.uid, *args)
636 def _set_group_values(self, node, values):
638 group_names = node.groups.split(',')
640 for group in group_names:
641 if group.startswith('-'):
642 group_id = self.get_id(group[1:])
643 groups_value.append((3, group_id))
645 group_id = self.get_id(group)
646 groups_value.append((4, group_id))
647 values['groups_id'] = groups_value
649 def process_menuitem(self, node):
650 self.validate_xml_id(node.id)
654 self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
655 res = self.cr.fetchone()
656 values = {'parent_id': parent_id, 'name': node.name}
658 parent_id = self.get_id(node.parent)
659 values = {'parent_id': parent_id}
661 values['name'] = node.name
663 res = [ self.get_id(node.id) ]
664 except: # which exception ?
668 action_type = node.type or 'act_window'
670 "act_window": 'STOCK_NEW',
671 "report.xml": 'STOCK_PASTE',
672 "wizard": 'STOCK_EXECUTE',
673 "url": 'STOCK_JUMP_TO',
675 values['icon'] = icons.get(action_type, 'STOCK_NEW')
676 if action_type == 'act_window':
677 action_id = self.get_id(node.action)
678 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
679 ir_act_window_result = self.cr.fetchone()
680 assert ir_act_window_result, "No window action defined for this id %s !\n" \
681 "Verify that this is a window action or add a type argument." % (node.action,)
682 action_type, action_mode, action_name, view_id, target = ir_act_window_result
684 self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
685 # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
686 action_mode = self.cr.fetchone()
687 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
689 action_mode = self.cr.fetchone()
690 if action_type == 'tree':
691 values['icon'] = 'STOCK_INDENT'
692 elif action_mode and action_mode.startswith('tree'):
693 values['icon'] = 'STOCK_JUSTIFY_FILL'
694 elif action_mode and action_mode.startswith('graph'):
695 values['icon'] = 'terp-graph'
696 elif action_mode and action_mode.startswith('calendar'):
697 values['icon'] = 'terp-calendar'
699 values['icon'] = 'STOCK_EXECUTE'
700 if not values.get('name', False):
701 values['name'] = action_name
702 elif action_type == 'wizard':
703 action_id = self.get_id(node.action)
704 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
705 ir_act_wizard_result = self.cr.fetchone()
706 if (not values.get('name', False)) and ir_act_wizard_result:
707 values['name'] = ir_act_wizard_result[0]
709 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
711 values['sequence'] = node.sequence
713 values['icon'] = node.icon
715 self._set_group_values(node, values)
717 pid = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
718 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
719 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
721 if node.id and parent_id:
722 self.id_map[node.id] = int(parent_id)
724 if node.action and pid:
725 action_type = node.type or 'act_window'
726 action_id = self.get_id(node.action)
727 action = "ir.actions.%s,%d" % (action_type, action_id)
728 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
729 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
731 def process_act_window(self, node):
732 assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
733 assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
734 assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
735 self.validate_xml_id(node.id)
738 view_id = self.get_id(node.view)
741 context = eval(str(node.context), self.eval_context)
744 'type': node.type or 'ir.actions.act_window',
746 'domain': node.domain,
748 'res_model': node.res_model,
749 'src_model': node.src_model,
750 'view_type': node.view_type or 'form',
751 'view_mode': node.view_mode or 'tree,form',
754 'auto_refresh': node.auto_refresh,
755 'multi': getattr(node, 'multi', False),
758 self._set_group_values(node, values)
761 values['target'] = node.target
762 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
763 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
764 self.id_map[node.id] = int(id)
767 keyword = 'client_action_relate'
768 value = 'ir.actions.act_window,%s' % id
769 replace = node.replace or True
770 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
771 node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
772 # TODO add remove ir.model.data
774 def process_delete(self, node):
775 assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
776 if self.pool.get(node.model):
778 ids = self.pool.get(node.model).search(self.cr, self.uid, eval(node.search, self.eval_context))
780 ids = [self.get_id(node.id)]
782 self.pool.get(node.model).unlink(self.cr, self.uid, ids)
784 self._log("Record not deleted.")
786 def process_url(self, node):
787 self.validate_xml_id(node.id)
789 res = {'name': node.name, 'url': node.url, 'target': node.target}
791 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
792 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
793 self.id_map[node.id] = int(id)
795 if (not node.menu or eval(node.menu)) and id:
796 keyword = node.keyword or 'client_action_multi'
797 value = 'ir.actions.act_url,%s' % id
798 replace = node.replace or True
799 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
800 keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
801 noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
803 def process_ir_set(self, node):
804 if not self.mode == 'init':
806 _, fields = node.items()[0]
808 for fieldname, expression in fields.items():
809 if is_eval(expression):
810 value = eval(expression.expression, self.eval_context)
813 res[fieldname] = value
814 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
815 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
816 isobject=res.get('isobject', False), meta=res.get('meta',None))
818 def process_report(self, node):
820 for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
821 values[dest] = getattr(node, f)
822 assert values[dest], "Attribute %s of report is empty !" % (f,)
823 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
824 if getattr(node, field):
825 values[dest] = getattr(node, field)
827 values['auto'] = eval(node.auto)
829 sxw_file = misc.file_open(node.sxw)
831 sxw_content = sxw_file.read()
832 values['report_sxw_content'] = sxw_content
836 values['header'] = eval(node.header)
837 values['multi'] = node.multi and eval(node.multi)
839 self.validate_xml_id(xml_id)
841 self._set_group_values(node, values)
843 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
844 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
845 self.id_map[xml_id] = int(id)
847 if not node.menu or eval(node.menu):
848 keyword = node.keyword or 'client_print_multi'
849 value = 'ir.actions.report.xml,%s' % id
850 replace = node.replace or True
851 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
852 keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
854 def process_none(self):
856 Empty node or commented node should not pass silently.
858 self._log_assert_failure("You have an empty block in your tests.")
861 def process(self, yaml_string):
863 Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
865 yaml_tag.add_constructors()
867 is_preceded_by_comment = False
868 for node in yaml.load(yaml_string):
869 is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
871 self._process_node(node)
876 def _process_node(self, node):
878 self.process_comment(node)
879 elif is_assert(node):
880 self.process_assert(node)
881 elif is_record(node):
882 self.process_record(node)
883 elif is_python(node):
884 self.process_python(node)
885 elif is_menuitem(node):
886 self.process_menuitem(node)
887 elif is_delete(node):
888 self.process_delete(node)
890 self.process_url(node)
891 elif is_context(node):
892 self.process_context(node)
893 elif is_ir_set(node):
894 self.process_ir_set(node)
895 elif is_act_window(node):
896 self.process_act_window(node)
897 elif is_report(node):
898 self.process_report(node)
899 elif is_workflow(node):
900 if isinstance(node, types.DictionaryType):
901 self.process_workflow(node)
903 self.process_workflow({node: []})
904 elif is_function(node):
905 if isinstance(node, types.DictionaryType):
906 self.process_function(node)
908 self.process_function({node: []})
912 raise YamlImportException("Can not process YAML block: %s" % node)
914 def _log_node(self, node, is_preceded_by_comment):
916 is_preceded_by_comment = True
918 elif not is_preceded_by_comment:
919 if isinstance(node, types.DictionaryType):
920 msg = "Creating %s\n with %s"
921 args = node.items()[0]
922 self._log(msg, *args)
926 is_preceded_by_comment = False
927 return is_preceded_by_comment
929 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
932 loglevel = logging.TEST if kind == 'test' else logging.DEBUG
933 yaml_string = yamlfile.read()
934 yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
935 yaml_interpreter.process(yaml_string)
937 # keeps convention of convert.py
938 convert_yaml_import = yaml_import
940 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
946 cr = sql_db.db_connect(db_name).cursor()
947 fp = misc.file_open(file_name)
948 convert_yaml_import(cr, module_name, fp, {}, 'update', True)
952 threading.Thread(target=f).start()
955 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: