[MERGE] Forward-port of latest saas-2 bugfixes, up to rev.5012 revid:mat@openerp...
[odoo/odoo.git] / openerp / tools / yaml_import.py
1 # -*- coding: utf-8 -*-
2 import threading
3 import types
4 import time # used to eval time.strftime expressions
5 from datetime import datetime, timedelta
6 import logging
7
8 import openerp
9 import openerp.sql_db as sql_db
10 import openerp.workflow
11 import misc
12 from config import config
13 import yaml_tag
14 import yaml
15 import re
16 from lxml import etree
17 from openerp import SUPERUSER_ID
18
19 # YAML import needs both safe and unsafe eval, but let's
20 # default to /safe/.
21 unsafe_eval = eval
22 from safe_eval import safe_eval as eval
23
24 import assertion_report
25
26 _logger = logging.getLogger(__name__)
27
28 class YamlImportException(Exception):
29     pass
30
31 class YamlImportAbortion(Exception):
32     pass
33
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)
38     return value
39
40 def is_comment(node):
41     return isinstance(node, types.StringTypes)
42
43 def is_assert(node):
44     return isinstance(node, yaml_tag.Assert) \
45         or _is_yaml_mapping(node, yaml_tag.Assert)
46
47 def is_record(node):
48     return _is_yaml_mapping(node, yaml_tag.Record)
49
50 def is_python(node):
51     return _is_yaml_mapping(node, yaml_tag.Python)
52
53 def is_menuitem(node):
54     return isinstance(node, yaml_tag.Menuitem) \
55         or _is_yaml_mapping(node, yaml_tag.Menuitem)
56
57 def is_function(node):
58     return isinstance(node, yaml_tag.Function) \
59         or _is_yaml_mapping(node, yaml_tag.Function)
60
61 def is_report(node):
62     return isinstance(node, yaml_tag.Report)
63
64 def is_workflow(node):
65     return isinstance(node, yaml_tag.Workflow)
66
67 def is_act_window(node):
68     return isinstance(node, yaml_tag.ActWindow)
69
70 def is_delete(node):
71     return isinstance(node, yaml_tag.Delete)
72
73 def is_context(node):
74     return isinstance(node, yaml_tag.Context)
75
76 def is_url(node):
77     return isinstance(node, yaml_tag.Url)
78
79 def is_eval(node):
80     return isinstance(node, yaml_tag.Eval)
81
82 def is_ref(node):
83     return isinstance(node, yaml_tag.Ref) \
84         or _is_yaml_mapping(node, yaml_tag.Ref)
85
86 def is_ir_set(node):
87     return _is_yaml_mapping(node, yaml_tag.IrSet)
88
89 def is_string(node):
90     return isinstance(node, basestring)
91
92 class RecordDictWrapper(dict):
93     """
94     Used to pass a record as locals in eval:
95     records do not strictly behave like dict, so we force them to.
96     """
97     def __init__(self, record):
98         self.record = record
99     def __getitem__(self, key):
100         if key in self.record:
101             return self.record[key]
102         return dict.__getitem__(self, key)
103
104 class YamlInterpreter(object):
105     def __init__(self, cr, module, id_map, mode, filename, report=None, noupdate=False, loglevel=logging.DEBUG):
106         self.cr = cr
107         self.module = module
108         self.id_map = id_map
109         self.mode = mode
110         self.filename = filename
111         if report is None:
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)
117         self.uid = 1
118         self.context = {} # opererp context
119         self.eval_context = {'ref': self._ref(),
120                              '_ref': self._ref(), # added '_ref' so that record['ref'] is possible
121                              'time': time,
122                              'datetime': datetime,
123                              'timedelta': timedelta}
124
125     def _log(self, *args, **kwargs):
126         _logger.log(self.loglevel, *args, **kwargs)
127
128     def _ref(self):
129         return lambda xml_id: self.get_id(xml_id)
130
131     def get_model(self, model_name):
132         return self.pool[model_name]
133
134     def validate_xml_id(self, xml_id):
135         id = xml_id
136         if '.' in 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" \
140                                   % (xml_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)
147
148     def get_id(self, xml_id):
149         if xml_id is False or xml_id is None:
150             return False
151         #if not xml_id:
152         #    raise YamlImportException("The xml_id should be a non empty string.")
153         elif isinstance(xml_id, types.IntType):
154             id = xml_id
155         elif xml_id in self.id_map:
156             id = self.id_map[xml_id]
157         else:
158             if '.' in xml_id:
159                 module, checked_xml_id = xml_id.split('.', 1)
160             else:
161                 module = self.module
162                 checked_xml_id = xml_id
163             try:
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
166             except ValueError:
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))
170
171         return id
172
173     def get_context(self, node, eval_dict):
174         context = self.context.copy()
175         if node.context:
176             context.update(eval(node.context, eval_dict))
177         return context
178
179     def isnoupdate(self, node):
180         return self.noupdate or node.noupdate or False
181
182     def _get_first_result(self, results, default=False):
183         if len(results):
184             value = results[0]
185             if isinstance(value, types.TupleType):
186                 value = value[0]
187         else:
188             value = default
189         return value
190
191     def process_comment(self, node):
192         return node
193
194     def _log_assert_failure(self, msg, *args):
195         self.assertion_report.record_failure()
196         _logger.error(msg, *args)
197
198     def _get_assertion_id(self, assertion):
199         if assertion.id:
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)
204         else:
205             raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
206         return ids
207
208     def process_assert(self, node):
209         if isinstance(node, dict):
210             assertion, expressions = node.items()[0]
211         else:
212             assertion, expressions = node, []
213
214         if self.isnoupdate(assertion) and self.mode != 'init':
215             _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
216             return
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)
226         else:
227             context = self.get_context(assertion, self.eval_context)
228             for id in ids:
229                 record = model.browse(self.cr, self.uid, id, context)
230                 for test in expressions:
231                     try:
232                         success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
233                     except Exception, e:
234                         _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
235                         raise YamlImportAbortion(e)
236                     if not success:
237                         msg = 'Assertion "%s" FAILED\ntest: %s\n'
238                         args = (assertion.string, test)
239                         for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
240                             if aop in test:
241                                 left, right = test.split(aop,1)
242                                 lmsg = ''
243                                 rmsg = ''
244                                 try:
245                                     lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
246                                 except Exception, e:
247                                     lmsg = '<exc>'
248
249                                 try:
250                                     rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
251                                 except Exception, e:
252                                     rmsg = '<exc>'
253
254                                 msg += 'values: ! %s %s %s'
255                                 args += ( lmsg, aop, rmsg )
256                                 break
257
258                         self._log_assert_failure(msg, *args)
259                         return
260             else: # all tests were successful for this assertion tag (no break)
261                 self.assertion_report.record_success()
262
263     def _coerce_bool(self, value, default=False):
264         if isinstance(value, types.BooleanType):
265             b = value
266         if isinstance(value, types.StringTypes):
267             b = value.strip().lower() not in ('0', 'false', 'off', 'no')
268         elif isinstance(value, types.IntType):
269             b = bool(value)
270         else:
271             b = default
272         return b
273
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)
280         return record_dict
281
282     def process_record(self, node):
283         record, fields = node.items()[0]
284         model = self.get_model(record.model)
285
286         view_id = record.view
287         if view_id and (view_id is not True) and isinstance(view_id, basestring):
288             module = self.module
289             if '.' in view_id:
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]
292
293         if model.is_transient():
294             record_dict=self.create_osv_memory_record(record, fields)
295         else:
296             self.validate_xml_id(record.id)
297             try:
298                 self.pool['ir.model.data']._get_id(self.cr, SUPERUSER_ID, self.module, record.id)
299                 default = False
300             except ValueError:
301                 default = True
302
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
306                 if id:
307                     self.id_map[record] = int(id)
308                     return None
309                 else:
310                     if not self._coerce_bool(record.forcecreate):
311                         return None
312
313
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
317             view_info = False
318             if view_id:
319                 varg = view_id
320                 if view_id is True: varg = False
321                 view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context)
322
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'):
329                 self.cr.commit()
330
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
341             :rtype: dict
342         """
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)
348             return one2many_view
349
350         def process_val(key, val):
351             if fg[key]['type'] == 'many2one':
352                 if type(val) in (tuple,list):
353                     val = val[0]
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'))
360                     for rec in val:
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"]]]}
369                                 del rec[field_key]
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)):
374                     val = [(6,0,val)]
375
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"]]]}
380                 return False
381
382             return val
383
384         if view_info:
385             arch = etree.fromstring(view_info['arch'].decode('utf-8'))
386             view = arch if len(arch) else False
387         else:
388             view = 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 {}
395
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])
399
400             # Process all on_change calls
401             nodes = [view]
402             while nodes:
403                 el = nodes.pop(0)
404                 if el.tag=='field':
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')
412
413                         field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default)
414
415                         #call process_val to not update record_dict if values were given for readonly fields
416                         val = process_val(field_name, field_value)
417                         if val:
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
421
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:
424                         continue
425
426                     if not el.attrib.get('on_change', False):
427                         continue
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'], )
430
431                     # creating the context
432                     class parent2(object):
433                         def __init__(self, d):
434                             self.d = d
435                         def __getattr__(self, name):
436                             return self.d.get(name, False)
437
438                     ctx = record_dict.copy()
439                     ctx['context'] = self.context
440                     ctx['uid'] = SUPERUSER_ID
441                     ctx['parent'] = parent2(parent)
442                     for a in fg:
443                         if a not in ctx:
444                             ctx[a] = process_val(a, defaults.get(a, False))
445
446                     # Evaluation args
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, (
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
456                             ))
457                         if key not in fields:
458                             # do not shadow values explicitly set in yaml.
459                             record_dict[key] = process_val(key, val)
460                 else:
461                     nodes = list(el) + nodes
462         else:
463             record_dict = {}
464
465         for field_name, expression in fields.items():
466             if field_name in record_dict:
467                 continue
468             field_value = self._eval_field(model, field_name, expression, default=False)
469             record_dict[field_name] = field_value
470         return record_dict
471
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'
474         if node.search:
475             if node.model:
476                 model_name = node.model
477             elif column:
478                 model_name = column._obj
479             else:
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)
484             if node.use:
485                 instances = model.browse(self.cr, self.uid, ids)
486                 value = [inst[node.use] for inst in instances]
487             else:
488                 value = ids
489         elif node.id:
490             value = self.get_id(node.id)
491         else:
492             value = None
493         return value
494
495     def process_eval(self, node):
496         return eval(node.expression, self.eval_context)
497
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]
504         else:
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)]
510             else: # many2one
511                 if isinstance(elements, (list,tuple)):
512                     value = self._get_first_result(elements)
513                 else:
514                     value = 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)
526             value = expression
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)
530             value = expression
531         else: # scalar field
532             if is_eval(expression):
533                 value = self.process_eval(expression)
534             else:
535                 value = expression
536             # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
537         return value
538
539     def process_context(self, node):
540         self.context = node.__dict__
541         if node.uid:
542             self.uid = self.get_id(node.uid)
543         if node.noupdate:
544             self.noupdate = node.noupdate
545
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")
550         code_context = {
551             'model': model,
552             'cr': self.cr,
553             'uid': self.uid,
554             'log': self._log,
555             'context': self.context,
556             'openerp': openerp,
557         }
558         code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
559         try:
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)
564             return
565         except Exception, e:
566             _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
567             raise
568         else:
569             self.assertion_report.record_success()
570
571     def process_workflow(self, node):
572         workflow, values = node.items()[0]
573         if self.isnoupdate(workflow) and self.mode != 'init':
574             return
575         if workflow.ref:
576             id = self.get_id(workflow.ref)
577         else:
578             if not values:
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))
582             value = values[0]
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)
589
590         if workflow.uid is not None:
591             uid = workflow.uid
592         else:
593             uid = self.uid
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)
599
600     def _eval_params(self, model, params):
601         args = []
602         for i, param in enumerate(params):
603             if isinstance(param, types.ListType):
604                 value = self._eval_params(model, param)
605             elif is_ref(param):
606                 value = self.process_ref(param)
607             elif is_eval(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)
619                 else:
620                     raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
621             else:
622                 value = param # scalar value
623             args.append(value)
624         return args
625
626     def process_function(self, node):
627         function, params = node.items()[0]
628         if self.isnoupdate(function) and self.mode != 'init':
629             return
630         model = self.get_model(function.model)
631         if function.eval:
632             args = self.process_eval(function.eval)
633         else:
634             args = self._eval_params(function.model, params)
635         method = function.name
636         getattr(model, method)(self.cr, self.uid, *args)
637
638     def _set_group_values(self, node, values):
639         if node.groups:
640             group_names = node.groups.split(',')
641             groups_value = []
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))
646                 else:
647                     group_id = self.get_id(group)
648                     groups_value.append((4, group_id))
649             values['groups_id'] = groups_value
650
651     def process_menuitem(self, node):
652         self.validate_xml_id(node.id)
653
654         if not node.parent:
655             parent_id = False
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}
659         else:
660             parent_id = self.get_id(node.parent)
661             values = {'parent_id': parent_id}
662             if node.name:
663                 values['name'] = node.name
664             try:
665                 res = [ self.get_id(node.id) ]
666             except: # which exception ?
667                 res = None
668
669         if node.action:
670             action_type = node.type or 'act_window'
671             icons = {
672                 "act_window": 'STOCK_NEW',
673                 "report.xml": 'STOCK_PASTE',
674                 "wizard": 'STOCK_EXECUTE',
675                 "url": 'STOCK_JUMP_TO',
676             }
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
685                 if view_id:
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,))
690                 if self.cr.rowcount:
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'
700                 if target == 'new':
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]
710             else:
711                 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
712         if node.sequence:
713             values['sequence'] = node.sequence
714         if node.icon:
715             values['icon'] = node.icon
716
717         self._set_group_values(node, values)
718
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)
722
723         if node.id and parent_id:
724             self.id_map[node.id] = int(parent_id)
725
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)
732
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)
738         view_id = False
739         if node.view:
740             view_id = self.get_id(node.view)
741         if not node.context:
742             node.context={}
743         context = eval(str(node.context), self.eval_context)
744         values = {
745             'name': node.name,
746             'type': node.type or 'ir.actions.act_window',
747             'view_id': view_id,
748             'domain': node.domain,
749             'context': context,
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',
754             'usage': node.usage,
755             'limit': node.limit,
756             'auto_refresh': node.auto_refresh,
757             'multi': getattr(node, 'multi', False),
758         }
759
760         self._set_group_values(node, values)
761
762         if node.target:
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)
767
768         if node.src_model:
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
775
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:
779             if node.search:
780                 ids = self.pool[node.model].search(self.cr, self.uid, eval(node.search, self.eval_context))
781             else:
782                 ids = [self.get_id(node.id)]
783             if len(ids):
784                 self.pool[node.model].unlink(self.cr, self.uid, ids)
785         else:
786             self._log("Record not deleted.")
787
788     def process_url(self, node):
789         self.validate_xml_id(node.id)
790
791         res = {'name': node.name, 'url': node.url, 'target': node.target}
792
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)
796         # ir_set
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)
804
805     def process_ir_set(self, node):
806         if not self.mode == 'init':
807             return False
808         _, fields = node.items()[0]
809         res = {}
810         for fieldname, expression in fields.items():
811             if is_eval(expression):
812                 value = eval(expression.expression, self.eval_context)
813             else:
814                 value = expression
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))
819
820     def process_report(self, node):
821         values = {}
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)
828         if node.auto:
829             values['auto'] = eval(node.auto)
830         if node.sxw:
831             sxw_file = misc.file_open(node.sxw)
832             try:
833                 sxw_content = sxw_file.read()
834                 values['report_sxw_content'] = sxw_content
835             finally:
836                 sxw_file.close()
837         if node.header:
838             values['header'] = eval(node.header)
839         values['multi'] = node.multi and eval(node.multi)
840         xml_id = node.id
841         self.validate_xml_id(xml_id)
842
843         self._set_group_values(node, values)
844
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)
848
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)
855
856     def process_none(self):
857         """
858         Empty node or commented node should not pass silently.
859         """
860         self._log_assert_failure("You have an empty block in your tests.")
861
862
863     def process(self, yaml_string):
864         """
865         Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
866         """
867         yaml_tag.add_constructors()
868
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)
872             try:
873                 self._process_node(node)
874             except Exception, e:
875                 _logger.exception(e)
876                 raise
877
878     def _process_node(self, node):
879         if is_comment(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)
891         elif is_url(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)
904             else:
905                 self.process_workflow({node: []})
906         elif is_function(node):
907             if isinstance(node, types.DictionaryType):
908                 self.process_function(node)
909             else:
910                 self.process_function({node: []})
911         elif node is None:
912             self.process_none()
913         else:
914             raise YamlImportException("Can not process YAML block: %s" % node)
915
916     def _log_node(self, node, is_preceded_by_comment):
917         if is_comment(node):
918             is_preceded_by_comment = True
919             self._log(node)
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)
925             else:
926                 self._log(node)
927         else:
928             is_preceded_by_comment = False
929         return is_preceded_by_comment
930
931 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
932     if idref is None:
933         idref = {}
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)
938
939 # keeps convention of convert.py
940 convert_yaml_import = yaml_import
941
942 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
943     def f():
944         time.sleep(delay)
945         cr = None
946         fp = None
947         try:
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)
951         finally:
952             if cr: cr.close()
953             if fp: fp.close()
954     threading.Thread(target=f).start()
955
956
957 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: