1 # -*- coding: utf-8 -*-
4 import time # used to eval time.strftime expressions
5 from datetime import datetime, timedelta
8 import openerp.pooler as pooler
9 import openerp.sql_db as sql_db
11 from config import config
15 from lxml import etree
16 from openerp import SUPERUSER_ID
18 # YAML import needs both safe and unsafe eval, but let's
21 from safe_eval import safe_eval as eval
23 import assertion_report
25 _logger = logging.getLogger(__name__)
27 class YamlImportException(Exception):
30 class YamlImportAbortion(Exception):
33 def _is_yaml_mapping(node, tag_constructor):
34 value = isinstance(node, types.DictionaryType) \
35 and len(node.keys()) == 1 \
36 and isinstance(node.keys()[0], tag_constructor)
40 return isinstance(node, types.StringTypes)
43 return isinstance(node, yaml_tag.Assert) \
44 or _is_yaml_mapping(node, yaml_tag.Assert)
47 return _is_yaml_mapping(node, yaml_tag.Record)
50 return _is_yaml_mapping(node, yaml_tag.Python)
52 def is_menuitem(node):
53 return isinstance(node, yaml_tag.Menuitem) \
54 or _is_yaml_mapping(node, yaml_tag.Menuitem)
56 def is_function(node):
57 return isinstance(node, yaml_tag.Function) \
58 or _is_yaml_mapping(node, yaml_tag.Function)
61 return isinstance(node, yaml_tag.Report)
63 def is_workflow(node):
64 return isinstance(node, yaml_tag.Workflow)
66 def is_act_window(node):
67 return isinstance(node, yaml_tag.ActWindow)
70 return isinstance(node, yaml_tag.Delete)
73 return isinstance(node, yaml_tag.Context)
76 return isinstance(node, yaml_tag.Url)
79 return isinstance(node, yaml_tag.Eval)
82 return isinstance(node, yaml_tag.Ref) \
83 or _is_yaml_mapping(node, yaml_tag.Ref)
86 return _is_yaml_mapping(node, yaml_tag.IrSet)
89 return isinstance(node, basestring)
91 class RecordDictWrapper(dict):
93 Used to pass a record as locals in eval:
94 records do not strictly behave like dict, so we force them to.
96 def __init__(self, record):
98 def __getitem__(self, key):
99 if key in self.record:
100 return self.record[key]
101 return dict.__getitem__(self, key)
103 class YamlInterpreter(object):
104 def __init__(self, cr, module, id_map, mode, filename, report=None, noupdate=False, loglevel=logging.DEBUG):
109 self.filename = filename
111 report = assertion_report.assertion_report()
112 self.assertion_report = report
113 self.noupdate = noupdate
114 self.loglevel = loglevel
115 self.pool = pooler.get_pool(cr.dbname)
117 self.context = {} # opererp context
118 self.eval_context = {'ref': self._ref(),
119 '_ref': self._ref(), # added '_ref' so that record['ref'] is possible
121 'datetime': datetime,
122 'timedelta': timedelta}
124 def _log(self, *args, **kwargs):
125 _logger.log(self.loglevel, *args, **kwargs)
128 return lambda xml_id: self.get_id(xml_id)
130 def get_model(self, model_name):
131 model = self.pool.get(model_name)
132 assert model, "The model %s does not exist." % (model_name,)
135 def validate_xml_id(self, xml_id):
138 module, id = xml_id.split('.', 1)
139 assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
140 "It is used to refer to other modules ID, in the form: module.record_id" \
142 if module != self.module:
143 module_count = self.pool.get('ir.module.module').search_count(self.cr, self.uid, \
144 ['&', ('name', '=', module), ('state', 'in', ['installed'])])
145 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
146 if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
147 _logger.error('id: %s is to long (max: 64)', id)
149 def get_id(self, xml_id):
150 if xml_id is False or xml_id is None:
153 # raise YamlImportException("The xml_id should be a non empty string.")
154 elif isinstance(xml_id, types.IntType):
156 elif xml_id in self.id_map:
157 id = self.id_map[xml_id]
160 module, checked_xml_id = xml_id.split('.', 1)
163 checked_xml_id = xml_id
165 _, id = self.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, module, checked_xml_id)
166 self.id_map[xml_id] = id
168 raise ValueError("""%s not found when processing %s.
169 This Yaml file appears to depend on missing data. This often happens for
170 tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
174 def get_context(self, node, eval_dict):
175 context = self.context.copy()
177 context.update(eval(node.context, eval_dict))
180 def isnoupdate(self, node):
181 return self.noupdate or node.noupdate or False
183 def _get_first_result(self, results, default=False):
186 if isinstance(value, types.TupleType):
192 def process_comment(self, node):
195 def _log_assert_failure(self, msg, *args):
196 self.assertion_report.record_failure()
197 _logger.error(msg, *args)
199 def _get_assertion_id(self, assertion):
201 ids = [self.get_id(assertion.id)]
202 elif assertion.search:
203 q = eval(assertion.search, self.eval_context)
204 ids = self.pool.get(assertion.model).search(self.cr, self.uid, q, context=assertion.context)
206 raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
209 def process_assert(self, node):
210 if isinstance(node, dict):
211 assertion, expressions = node.items()[0]
213 assertion, expressions = node, []
215 if self.isnoupdate(assertion) and self.mode != 'init':
216 _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
218 model = self.get_model(assertion.model)
219 ids = self._get_assertion_id(assertion)
220 if assertion.count is not None and len(ids) != assertion.count:
221 msg = 'assertion "%s" failed!\n' \
222 ' Incorrect search count:\n' \
223 ' expected count: %d\n' \
224 ' obtained count: %d\n'
225 args = (assertion.string, assertion.count, len(ids))
226 self._log_assert_failure(msg, *args)
228 context = self.get_context(assertion, self.eval_context)
230 record = model.browse(self.cr, self.uid, id, context)
231 for test in expressions:
233 success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
235 _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
236 raise YamlImportAbortion(e)
238 msg = 'Assertion "%s" FAILED\ntest: %s\n'
239 args = (assertion.string, test)
240 for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
242 left, right = test.split(aop,1)
246 lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
251 rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
255 msg += 'values: ! %s %s %s'
256 args += ( lmsg, aop, rmsg )
259 self._log_assert_failure(msg, *args)
261 else: # all tests were successful for this assertion tag (no break)
262 self.assertion_report.record_success()
264 def _coerce_bool(self, value, default=False):
265 if isinstance(value, types.BooleanType):
267 if isinstance(value, types.StringTypes):
268 b = value.strip().lower() not in ('0', 'false', 'off', 'no')
269 elif isinstance(value, types.IntType):
275 def create_osv_memory_record(self, record, fields):
276 model = self.get_model(record.model)
277 context = self.get_context(record, self.eval_context)
278 record_dict = self._create_record(model, fields)
279 id_new = model.create(self.cr, self.uid, record_dict, context=context)
280 self.id_map[record.id] = int(id_new)
283 def process_record(self, node):
284 import openerp.osv as osv
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'):
359 if len(val) and type(val[0]) == dict:
360 #we want to return only the fields that aren't readonly
361 #For that, we need to first get the right tree view to consider for the field `key´
362 one2many_tree_view = _get_right_one2many_view(fg, key, 'tree')
364 #make a copy for the iteration, as we will alterate the size of `rec´ dictionary
365 rec_copy = rec.copy()
366 for field_key in rec_copy:
367 #seek in the view for the field `field_key´ and removing it from `key´ values, as this column is readonly in the tree view
368 subfield_obj = etree.fromstring(one2many_tree_view['arch'].encode('utf-8')).xpath("//field[@name='%s']"%(field_key))
369 if subfield_obj and (subfield_obj[0].get('modifiers', '{}').find('"readonly": true') >= 0):
370 #TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
371 #order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
374 #now that unwanted values have been removed from val, we can encapsulate it in a tuple as returned value
375 val = map(lambda x: (0,0,x), val)
377 #we want to return only the fields that aren't readonly
378 if el.get('modifiers', '{}').find('"readonly": true') >= 0:
379 #TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
380 #order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
385 arch = etree.fromstring(view_info['arch'].encode('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():
450 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'])
451 record_dict[key] = process_val(key, val)
452 #if (key in fields) and record_dict[key] == process_val(key, val):
453 # print '*** You can remove these lines:', key, val
455 nodes = list(el) + nodes
459 for field_name, expression in fields.items():
460 if field_name in record_dict:
462 field_value = self._eval_field(model, field_name, expression, default=False)
463 record_dict[field_name] = field_value
466 def process_ref(self, node, column=None):
467 assert node.search or node.id, '!ref node should have a `search` attribute or `id` attribute'
470 model_name = node.model
472 model_name = column._obj
474 raise YamlImportException('You need to give a model for the search, or a column to infer it.')
475 model = self.get_model(model_name)
476 q = eval(node.search, self.eval_context)
477 ids = model.search(self.cr, self.uid, q)
479 instances = model.browse(self.cr, self.uid, ids)
480 value = [inst[node.use] for inst in instances]
484 value = self.get_id(node.id)
489 def process_eval(self, node):
490 return eval(node.expression, self.eval_context)
492 def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True):
493 # TODO this should be refactored as something like model.get_field() in bin/osv
494 if field_name in model._columns:
495 column = model._columns[field_name]
496 elif field_name in model._inherit_fields:
497 column = model._inherit_fields[field_name][2]
499 raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name))
500 if is_ref(expression):
501 elements = self.process_ref(expression, column)
502 if column._type in ("many2many", "one2many"):
503 value = [(6, 0, elements)]
505 if isinstance(elements, (list,tuple)):
506 value = self._get_first_result(elements)
509 elif column._type == "many2one":
510 value = self.get_id(expression)
511 elif column._type == "one2many":
512 other_model = self.get_model(column._obj)
513 value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression]
514 elif column._type == "many2many":
515 ids = [self.get_id(xml_id) for xml_id in expression]
516 value = [(6, 0, ids)]
517 elif column._type == "date" and is_string(expression):
518 # enforce ISO format for string date values, to be locale-agnostic during tests
519 time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
521 elif column._type == "datetime" and is_string(expression):
522 # enforce ISO format for string datetime values, to be locale-agnostic during tests
523 time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
526 if is_eval(expression):
527 value = self.process_eval(expression)
530 # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
533 def process_context(self, node):
534 self.context = node.__dict__
536 self.uid = self.get_id(node.uid)
538 self.noupdate = node.noupdate
540 def process_python(self, node):
541 python, statements = node.items()[0]
542 model = self.get_model(python.model)
543 statements = statements.replace("\r\n", "\n")
544 code_context = { 'model': model, 'cr': self.cr, 'uid': self.uid, 'log': self._log, 'context': self.context }
545 code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
547 code_obj = compile(statements, self.filename, 'exec')
548 unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
549 except AssertionError, e:
550 self._log_assert_failure('AssertionError in Python code %s: %s', python.name, e)
553 _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
556 self.assertion_report.record_success()
558 def process_workflow(self, node):
559 workflow, values = node.items()[0]
560 if self.isnoupdate(workflow) and self.mode != 'init':
563 id = self.get_id(workflow.ref)
566 raise YamlImportException('You must define a child node if you do not give a ref.')
567 if not len(values) == 1:
568 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
570 if not 'model' in value and (not 'eval' in value or not 'search' in value):
571 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
572 value_model = self.get_model(value['model'])
573 local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
574 local_context.update(self.id_map)
575 id = eval(value['eval'], self.eval_context, local_context)
577 if workflow.uid is not None:
581 self.cr.execute('select distinct signal from wkf_transition')
582 signals=[x['signal'] for x in self.cr.dictfetchall()]
583 if workflow.action not in signals:
584 raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
585 import openerp.netsvc as netsvc
586 wf_service = netsvc.LocalService("workflow")
587 wf_service.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
589 def _eval_params(self, model, params):
591 for i, param in enumerate(params):
592 if isinstance(param, types.ListType):
593 value = self._eval_params(model, param)
595 value = self.process_ref(param)
597 value = self.process_eval(param)
598 elif isinstance(param, types.DictionaryType): # supports XML syntax
599 param_model = self.get_model(param.get('model', model))
600 if 'search' in param:
601 q = eval(param['search'], self.eval_context)
602 ids = param_model.search(self.cr, self.uid, q)
603 value = self._get_first_result(ids)
604 elif 'eval' in param:
605 local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
606 local_context.update(self.id_map)
607 value = eval(param['eval'], self.eval_context, local_context)
609 raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
611 value = param # scalar value
615 def process_function(self, node):
616 function, params = node.items()[0]
617 if self.isnoupdate(function) and self.mode != 'init':
619 model = self.get_model(function.model)
621 args = self.process_eval(function.eval)
623 args = self._eval_params(function.model, params)
624 method = function.name
625 getattr(model, method)(self.cr, self.uid, *args)
627 def _set_group_values(self, node, values):
629 group_names = node.groups.split(',')
631 for group in group_names:
632 if group.startswith('-'):
633 group_id = self.get_id(group[1:])
634 groups_value.append((3, group_id))
636 group_id = self.get_id(group)
637 groups_value.append((4, group_id))
638 values['groups_id'] = groups_value
640 def process_menuitem(self, node):
641 self.validate_xml_id(node.id)
645 self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
646 res = self.cr.fetchone()
647 values = {'parent_id': parent_id, 'name': node.name}
649 parent_id = self.get_id(node.parent)
650 values = {'parent_id': parent_id}
652 values['name'] = node.name
654 res = [ self.get_id(node.id) ]
655 except: # which exception ?
659 action_type = node.type or 'act_window'
661 "act_window": 'STOCK_NEW',
662 "report.xml": 'STOCK_PASTE',
663 "wizard": 'STOCK_EXECUTE',
664 "url": 'STOCK_JUMP_TO',
666 values['icon'] = icons.get(action_type, 'STOCK_NEW')
667 if action_type == 'act_window':
668 action_id = self.get_id(node.action)
669 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
670 ir_act_window_result = self.cr.fetchone()
671 assert ir_act_window_result, "No window action defined for this id %s !\n" \
672 "Verify that this is a window action or add a type argument." % (node.action,)
673 action_type, action_mode, action_name, view_id, target = ir_act_window_result
675 self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
676 # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
677 action_mode = self.cr.fetchone()
678 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
680 action_mode = self.cr.fetchone()
681 if action_type == 'tree':
682 values['icon'] = 'STOCK_INDENT'
683 elif action_mode and action_mode.startswith('tree'):
684 values['icon'] = 'STOCK_JUSTIFY_FILL'
685 elif action_mode and action_mode.startswith('graph'):
686 values['icon'] = 'terp-graph'
687 elif action_mode and action_mode.startswith('calendar'):
688 values['icon'] = 'terp-calendar'
690 values['icon'] = 'STOCK_EXECUTE'
691 if not values.get('name', False):
692 values['name'] = action_name
693 elif action_type == 'wizard':
694 action_id = self.get_id(node.action)
695 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
696 ir_act_wizard_result = self.cr.fetchone()
697 if (not values.get('name', False)) and ir_act_wizard_result:
698 values['name'] = ir_act_wizard_result[0]
700 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
702 values['sequence'] = node.sequence
704 values['icon'] = node.icon
706 self._set_group_values(node, values)
708 pid = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
709 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
710 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
712 if node.id and parent_id:
713 self.id_map[node.id] = int(parent_id)
715 if node.action and pid:
716 action_type = node.type or 'act_window'
717 action_id = self.get_id(node.action)
718 action = "ir.actions.%s,%d" % (action_type, action_id)
719 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
720 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
722 def process_act_window(self, node):
723 assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
724 assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
725 assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
726 self.validate_xml_id(node.id)
729 view_id = self.get_id(node.view)
732 context = eval(str(node.context), self.eval_context)
735 'type': node.type or 'ir.actions.act_window',
737 'domain': node.domain,
739 'res_model': node.res_model,
740 'src_model': node.src_model,
741 'view_type': node.view_type or 'form',
742 'view_mode': node.view_mode or 'tree,form',
745 'auto_refresh': node.auto_refresh,
746 'multi': getattr(node, 'multi', False),
749 self._set_group_values(node, values)
752 values['target'] = node.target
753 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
754 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
755 self.id_map[node.id] = int(id)
758 keyword = 'client_action_relate'
759 value = 'ir.actions.act_window,%s' % id
760 replace = node.replace or True
761 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
762 node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
763 # TODO add remove ir.model.data
765 def process_delete(self, node):
766 assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
767 if self.pool.get(node.model):
769 ids = self.pool.get(node.model).search(self.cr, self.uid, eval(node.search, self.eval_context))
771 ids = [self.get_id(node.id)]
773 self.pool.get(node.model).unlink(self.cr, self.uid, ids)
775 self._log("Record not deleted.")
777 def process_url(self, node):
778 self.validate_xml_id(node.id)
780 res = {'name': node.name, 'url': node.url, 'target': node.target}
782 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
783 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
784 self.id_map[node.id] = int(id)
786 if (not node.menu or eval(node.menu)) and id:
787 keyword = node.keyword or 'client_action_multi'
788 value = 'ir.actions.act_url,%s' % id
789 replace = node.replace or True
790 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
791 keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
792 noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
794 def process_ir_set(self, node):
795 if not self.mode == 'init':
797 _, fields = node.items()[0]
799 for fieldname, expression in fields.items():
800 if is_eval(expression):
801 value = eval(expression.expression, self.eval_context)
804 res[fieldname] = value
805 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
806 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
807 isobject=res.get('isobject', False), meta=res.get('meta',None))
809 def process_report(self, node):
811 for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
812 values[dest] = getattr(node, f)
813 assert values[dest], "Attribute %s of report is empty !" % (f,)
814 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
815 if getattr(node, field):
816 values[dest] = getattr(node, field)
818 values['auto'] = eval(node.auto)
820 sxw_file = misc.file_open(node.sxw)
822 sxw_content = sxw_file.read()
823 values['report_sxw_content'] = sxw_content
827 values['header'] = eval(node.header)
828 values['multi'] = node.multi and eval(node.multi)
830 self.validate_xml_id(xml_id)
832 self._set_group_values(node, values)
834 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
835 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
836 self.id_map[xml_id] = int(id)
838 if not node.menu or eval(node.menu):
839 keyword = node.keyword or 'client_print_multi'
840 value = 'ir.actions.report.xml,%s' % id
841 replace = node.replace or True
842 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
843 keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
845 def process_none(self):
847 Empty node or commented node should not pass silently.
849 self._log_assert_failure("You have an empty block in your tests.")
852 def process(self, yaml_string):
854 Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
856 yaml_tag.add_constructors()
858 is_preceded_by_comment = False
859 for node in yaml.load(yaml_string):
860 is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
862 self._process_node(node)
867 def _process_node(self, node):
869 self.process_comment(node)
870 elif is_assert(node):
871 self.process_assert(node)
872 elif is_record(node):
873 self.process_record(node)
874 elif is_python(node):
875 self.process_python(node)
876 elif is_menuitem(node):
877 self.process_menuitem(node)
878 elif is_delete(node):
879 self.process_delete(node)
881 self.process_url(node)
882 elif is_context(node):
883 self.process_context(node)
884 elif is_ir_set(node):
885 self.process_ir_set(node)
886 elif is_act_window(node):
887 self.process_act_window(node)
888 elif is_report(node):
889 self.process_report(node)
890 elif is_workflow(node):
891 if isinstance(node, types.DictionaryType):
892 self.process_workflow(node)
894 self.process_workflow({node: []})
895 elif is_function(node):
896 if isinstance(node, types.DictionaryType):
897 self.process_function(node)
899 self.process_function({node: []})
903 raise YamlImportException("Can not process YAML block: %s" % node)
905 def _log_node(self, node, is_preceded_by_comment):
907 is_preceded_by_comment = True
909 elif not is_preceded_by_comment:
910 if isinstance(node, types.DictionaryType):
911 msg = "Creating %s\n with %s"
912 args = node.items()[0]
913 self._log(msg, *args)
917 is_preceded_by_comment = False
918 return is_preceded_by_comment
920 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
923 loglevel = logging.TEST if kind == 'test' else logging.DEBUG
924 yaml_string = yamlfile.read()
925 yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
926 yaml_interpreter.process(yaml_string)
928 # keeps convention of convert.py
929 convert_yaml_import = yaml_import
931 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
937 cr = sql_db.db_connect(db_name).cursor()
938 fp = misc.file_open(file_name)
939 convert_yaml_import(cr, module_name, fp, {}, 'update', True)
943 threading.Thread(target=f).start()
946 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: