Merge remote-tracking branch 'odoo/7.0' into 7.0
[odoo/odoo.git] / openerp / tools / yaml_import.py
1 # -*- coding: utf-8 -*-
2 import os
3 import threading
4 import types
5 import time # used to eval time.strftime expressions
6 from datetime import datetime, timedelta
7 import logging
8
9 from copy import deepcopy
10 import openerp.pooler as pooler
11 import openerp.sql_db as sql_db
12 import misc
13 from config import config
14 import yaml_tag
15 import yaml
16 import re
17 from lxml import etree
18 from openerp import SUPERUSER_ID
19
20 # YAML import needs both safe and unsafe eval, but let's
21 # default to /safe/.
22 unsafe_eval = eval
23 from safe_eval import safe_eval as eval
24
25 import assertion_report
26
27 _logger = logging.getLogger(__name__)
28
29 class YamlImportException(Exception):
30     pass
31
32 class YamlImportAbortion(Exception):
33     pass
34
35 def _is_yaml_mapping(node, tag_constructor):
36     value = isinstance(node, types.DictionaryType) \
37         and len(node.keys()) == 1 \
38         and isinstance(node.keys()[0], tag_constructor)
39     return value
40
41 def is_comment(node):
42     return isinstance(node, types.StringTypes)
43
44 def is_assert(node):
45     return isinstance(node, yaml_tag.Assert) \
46         or _is_yaml_mapping(node, yaml_tag.Assert)
47
48 def is_record(node):
49     return _is_yaml_mapping(node, yaml_tag.Record)
50
51 def is_python(node):
52     return _is_yaml_mapping(node, yaml_tag.Python)
53
54 def is_menuitem(node):
55     return isinstance(node, yaml_tag.Menuitem) \
56         or _is_yaml_mapping(node, yaml_tag.Menuitem)
57
58 def is_function(node):
59     return isinstance(node, yaml_tag.Function) \
60         or _is_yaml_mapping(node, yaml_tag.Function)
61
62 def is_report(node):
63     return isinstance(node, yaml_tag.Report)
64
65 def is_workflow(node):
66     return isinstance(node, yaml_tag.Workflow)
67
68 def is_act_window(node):
69     return isinstance(node, yaml_tag.ActWindow)
70
71 def is_delete(node):
72     return isinstance(node, yaml_tag.Delete)
73
74 def is_context(node):
75     return isinstance(node, yaml_tag.Context)
76
77 def is_url(node):
78     return isinstance(node, yaml_tag.Url)
79
80 def is_eval(node):
81     return isinstance(node, yaml_tag.Eval)
82
83 def is_ref(node):
84     return isinstance(node, yaml_tag.Ref) \
85         or _is_yaml_mapping(node, yaml_tag.Ref)
86
87 def is_ir_set(node):
88     return _is_yaml_mapping(node, yaml_tag.IrSet)
89
90 def is_string(node):
91     return isinstance(node, basestring)
92
93 class RecordDictWrapper(dict):
94     """
95     Used to pass a record as locals in eval:
96     records do not strictly behave like dict, so we force them to.
97     """
98     def __init__(self, record):
99         self.record = record
100     def __getitem__(self, key):
101         if key in self.record:
102             return self.record[key]
103         return dict.__getitem__(self, key)
104
105 class YamlInterpreter(object):
106     def __init__(self, cr, module, id_map, mode, filename, report=None, noupdate=False, loglevel=logging.DEBUG):
107         self.cr = cr
108         self.module = module
109         self.id_map = id_map
110         self.mode = mode
111         self.filename = filename
112         if report is None:
113             report = assertion_report.assertion_report()
114         self.assertion_report = report
115         self.noupdate = noupdate
116         self.loglevel = loglevel
117         self.pool = pooler.get_pool(cr.dbname)
118         self.uid = 1
119         self.context = {} # opererp context
120         self.eval_context = {'ref': self._ref(),
121                              '_ref': self._ref(), # added '_ref' so that record['ref'] is possible
122                              'time': time,
123                              'datetime': datetime,
124                              'timedelta': timedelta}
125
126     def _log(self, *args, **kwargs):
127         _logger.log(self.loglevel, *args, **kwargs)
128
129     def _ref(self):
130         return lambda xml_id: self.get_id(xml_id)
131
132     def get_model(self, model_name):
133         model = self.pool.get(model_name)
134         assert model, "The model %s does not exist." % (model_name,)
135         return model
136
137     def validate_xml_id(self, xml_id):
138         id = xml_id
139         if '.' in xml_id:
140             module, id = xml_id.split('.', 1)
141             assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
142                                   "It is used to refer to other modules ID, in the form: module.record_id" \
143                                   % (xml_id,)
144             if module != self.module:
145                 module_count = self.pool.get('ir.module.module').search_count(self.cr, self.uid, \
146                         ['&', ('name', '=', module), ('state', 'in', ['installed'])])
147                 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
148         if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
149             _logger.error('id: %s is to long (max: 64)', id)
150
151     def get_id(self, xml_id):
152         if xml_id is False or xml_id is None:
153             return False
154         #if not xml_id:
155         #    raise YamlImportException("The xml_id should be a non empty string.")
156         elif isinstance(xml_id, types.IntType):
157             id = xml_id
158         elif xml_id in self.id_map:
159             id = self.id_map[xml_id]
160         else:
161             if '.' in xml_id:
162                 module, checked_xml_id = xml_id.split('.', 1)
163             else:
164                 module = self.module
165                 checked_xml_id = xml_id
166             try:
167                 _, id = self.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, module, checked_xml_id)
168                 self.id_map[xml_id] = id
169             except ValueError:
170                 raise ValueError("""%s not found when processing %s.
171     This Yaml file appears to depend on missing data. This often happens for
172     tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
173
174         return id
175
176     def get_context(self, node, eval_dict):
177         context = self.context.copy()
178         if node.context:
179             context.update(eval(node.context, eval_dict))
180         return context
181
182     def isnoupdate(self, node):
183         return self.noupdate or node.noupdate or False
184
185     def _get_first_result(self, results, default=False):
186         if len(results):
187             value = results[0]
188             if isinstance(value, types.TupleType):
189                 value = value[0]
190         else:
191             value = default
192         return value
193
194     def process_comment(self, node):
195         return node
196
197     def _log_assert_failure(self, msg, *args):
198         from openerp.modules import module  # cannot be made before (loop)
199         basepath = module.get_module_path(self.module)
200         self.assertion_report.record_failure(
201             details=dict(module=self.module,
202                          testfile=os.path.relpath(self.filename, basepath),
203                          msg=msg,
204                          msg_args=deepcopy(args)))
205         _logger.error(msg, *args)
206
207     def _get_assertion_id(self, assertion):
208         if assertion.id:
209             ids = [self.get_id(assertion.id)]
210         elif assertion.search:
211             q = eval(assertion.search, self.eval_context)
212             ids = self.pool.get(assertion.model).search(self.cr, self.uid, q, context=assertion.context)
213         else:
214             raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
215         return ids
216
217     def process_assert(self, node):
218         if isinstance(node, dict):
219             assertion, expressions = node.items()[0]
220         else:
221             assertion, expressions = node, []
222
223         if self.isnoupdate(assertion) and self.mode != 'init':
224             _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
225             return
226         model = self.get_model(assertion.model)
227         ids = self._get_assertion_id(assertion)
228         if assertion.count is not None and len(ids) != assertion.count:
229             msg = 'assertion "%s" failed!\n'   \
230                   ' Incorrect search count:\n' \
231                   ' expected count: %d\n'      \
232                   ' obtained count: %d\n'
233             args = (assertion.string, assertion.count, len(ids))
234             self._log_assert_failure(msg, *args)
235         else:
236             context = self.get_context(assertion, self.eval_context)
237             for id in ids:
238                 record = model.browse(self.cr, self.uid, id, context)
239                 for test in expressions:
240                     try:
241                         success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
242                     except Exception, e:
243                         _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
244                         raise YamlImportAbortion(e)
245                     if not success:
246                         msg = 'Assertion "%s" FAILED\ntest: %s\n'
247                         args = (assertion.string, test)
248                         for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
249                             if aop in test:
250                                 left, right = test.split(aop,1)
251                                 lmsg = ''
252                                 rmsg = ''
253                                 try:
254                                     lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
255                                 except Exception, e:
256                                     lmsg = '<exc>'
257
258                                 try:
259                                     rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
260                                 except Exception, e:
261                                     rmsg = '<exc>'
262
263                                 msg += 'values: ! %s %s %s'
264                                 args += ( lmsg, aop, rmsg )
265                                 break
266
267                         self._log_assert_failure(msg, *args)
268                         return
269             else: # all tests were successful for this assertion tag (no break)
270                 self.assertion_report.record_success()
271
272     def _coerce_bool(self, value, default=False):
273         if isinstance(value, types.BooleanType):
274             b = value
275         if isinstance(value, types.StringTypes):
276             b = value.strip().lower() not in ('0', 'false', 'off', 'no')
277         elif isinstance(value, types.IntType):
278             b = bool(value)
279         else:
280             b = default
281         return b
282
283     def create_osv_memory_record(self, record, fields):
284         model = self.get_model(record.model)
285         context = self.get_context(record, self.eval_context)
286         record_dict = self._create_record(model, fields)
287         id_new = model.create(self.cr, self.uid, record_dict, context=context)
288         self.id_map[record.id] = int(id_new)
289         return record_dict
290
291     def process_record(self, node):
292         import openerp.osv as osv
293         record, fields = node.items()[0]
294         model = self.get_model(record.model)
295
296         view_id = record.view
297         if view_id and (view_id is not True) and isinstance(view_id, basestring):
298             module = self.module
299             if '.' in view_id:
300                 module, view_id = view_id.split('.',1)
301             view_id = self.pool.get('ir.model.data').get_object_reference(self.cr, SUPERUSER_ID, module, view_id)[1]
302
303         if model.is_transient():
304             record_dict=self.create_osv_memory_record(record, fields)
305         else:
306             self.validate_xml_id(record.id)
307             try:
308                 self.pool.get('ir.model.data')._get_id(self.cr, SUPERUSER_ID, self.module, record.id)
309                 default = False
310             except ValueError:
311                 default = True
312
313             if self.isnoupdate(record) and self.mode != 'init':
314                 id = self.pool.get('ir.model.data')._update_dummy(self.cr, SUPERUSER_ID, record.model, self.module, record.id)
315                 # check if the resource already existed at the last update
316                 if id:
317                     self.id_map[record] = int(id)
318                     return None
319                 else:
320                     if not self._coerce_bool(record.forcecreate):
321                         return None
322
323
324             #context = self.get_context(record, self.eval_context)
325             #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module
326             context = record.context
327             view_info = False
328             if view_id:
329                 varg = view_id
330                 if view_id is True: varg = False
331                 view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context)
332
333             record_dict = self._create_record(model, fields, view_info, default=default)
334             _logger.debug("RECORD_DICT %s" % record_dict)
335             id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, record.model, \
336                     self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context)
337             self.id_map[record.id] = int(id)
338             if config.get('import_partial'):
339                 self.cr.commit()
340
341     def _create_record(self, model, fields, view_info=False, parent={}, default=True):
342         """This function processes the !record tag in yalm files. It simulates the record creation through an xml
343             view (either specified on the !record tag or the default one for this object), including the calls to
344             on_change() functions, and sending only values for fields that aren't set as readonly.
345             :param model: model instance
346             :param fields: dictonary mapping the field names and their values
347             :param view_info: result of fields_view_get() called on the object
348             :param parent: dictionary containing the values already computed for the parent, in case of one2many fields
349             :param default: if True, the default values must be processed too or not
350             :return: dictionary mapping the field names and their values, ready to use when calling the create() function
351             :rtype: dict
352         """
353         def _get_right_one2many_view(fg, field_name, view_type):
354             one2many_view = fg[field_name]['views'].get(view_type)
355             # if the view is not defined inline, we call fields_view_get()
356             if not one2many_view:
357                 one2many_view = self.pool.get(fg[field_name]['relation']).fields_view_get(self.cr, SUPERUSER_ID, False, view_type, self.context)
358             return one2many_view
359
360         def process_val(key, val):
361             if fg[key]['type'] == 'many2one':
362                 if type(val) in (tuple,list):
363                     val = val[0]
364             elif fg[key]['type'] == 'one2many':
365                 if val and isinstance(val, (list,tuple)) and isinstance(val[0], dict):
366                     # we want to return only the fields that aren't readonly
367                     # For that, we need to first get the right tree view to consider for the field `key´
368                     one2many_tree_view = _get_right_one2many_view(fg, key, 'tree')
369                     arch = etree.fromstring(one2many_tree_view['arch'].encode('utf-8'))
370                     for rec in val:
371                         # make a copy for the iteration, as we will alter `rec´
372                         rec_copy = rec.copy()
373                         for field_key in rec_copy:
374                             # if field is missing in view or has a readonly modifier, drop it
375                             field_elem = arch.xpath("//field[@name='%s']" % field_key)
376                             if field_elem and (field_elem[0].get('modifiers', '{}').find('"readonly": true') >= 0):
377                                 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in 
378                                 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
379                                 del rec[field_key]
380                     # now that unwanted values have been removed from val, we can encapsulate it in a tuple as returned value
381                     val = map(lambda x: (0,0,x), val)
382             elif fg[key]['type'] == 'many2many':
383                 if val and isinstance(val,(list,tuple)) and isinstance(val[0], (int,long)):
384                     val = [(6,0,val)]
385
386             # we want to return only the fields that aren't readonly
387             if el.get('modifiers', '{}').find('"readonly": true') >= 0:
388                 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in 
389                 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
390                 return False
391
392             return val
393
394         if view_info:
395             arch = etree.fromstring(view_info['arch'].decode('utf-8'))
396             view = arch if len(arch) else False
397         else:
398             view = False
399         fields = fields or {}
400         if view is not False:
401             fg = view_info['fields']
402             # gather the default values on the object. (Can't use `fields´ as parameter instead of {} because we may
403             # have references like `base.main_company´ in the yaml file and it's not compatible with the function)
404             defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {}
405
406             # copy the default values in record_dict, only if they are in the view (because that's what the client does)
407             # the other default values will be added later on by the create().
408             record_dict = dict([(key, val) for key, val in defaults.items() if key in fg])
409
410             # Process all on_change calls
411             nodes = [view]
412             while nodes:
413                 el = nodes.pop(0)
414                 if el.tag=='field':
415                     field_name = el.attrib['name']
416                     assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name)
417                     if field_name in fields:
418                         one2many_form_view = None
419                         if (view is not False) and (fg[field_name]['type']=='one2many'):
420                             # for one2many fields, we want to eval them using the inline form view defined on the parent
421                             one2many_form_view = _get_right_one2many_view(fg, field_name, 'form')
422
423                         field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default)
424
425                         #call process_val to not update record_dict if values were given for readonly fields
426                         val = process_val(field_name, field_value)
427                         if val:
428                             record_dict[field_name] = val
429                         #if (field_name in defaults) and defaults[field_name] == field_value:
430                         #    print '*** You can remove these lines:', field_name, field_value
431
432                     #if field_name has a default value or a value is given in the yaml file, we must call its on_change()
433                     elif field_name not in defaults:
434                         continue
435
436                     if not el.attrib.get('on_change', False):
437                         continue
438                     match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change'])
439                     assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], )
440
441                     # creating the context
442                     class parent2(object):
443                         def __init__(self, d):
444                             self.d = d
445                         def __getattr__(self, name):
446                             return self.d.get(name, False)
447
448                     ctx = record_dict.copy()
449                     ctx['context'] = self.context
450                     ctx['uid'] = SUPERUSER_ID
451                     ctx['parent'] = parent2(parent)
452                     for a in fg:
453                         if a not in ctx:
454                             ctx[a] = process_val(a, defaults.get(a, False))
455
456                     # Evaluation args
457                     args = map(lambda x: eval(x, ctx), match.group(2).split(','))
458                     result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
459                     for key, val in (result or {}).get('value', {}).items():
460                         if key in fg:
461                             record_dict[key] = process_val(key, val)
462                         else:
463                             _logger.debug("The returning field '%s' from your on_change call '%s'"
464                                             " does not exist either on the object '%s', either in"
465                                             " the view '%s'",
466                                             key, match.group(1), model._name, view_info['name'])
467                 else:
468                     nodes = list(el) + nodes
469         else:
470             record_dict = {}
471
472         for field_name, expression in fields.items():
473             if field_name in record_dict:
474                 continue
475             field_value = self._eval_field(model, field_name, expression, default=False)
476             record_dict[field_name] = field_value
477         return record_dict
478
479     def process_ref(self, node, column=None):
480         assert node.search or node.id, '!ref node should have a `search` attribute or `id` attribute'
481         if node.search:
482             if node.model:
483                 model_name = node.model
484             elif column:
485                 model_name = column._obj
486             else:
487                 raise YamlImportException('You need to give a model for the search, or a column to infer it.')
488             model = self.get_model(model_name)
489             q = eval(node.search, self.eval_context)
490             ids = model.search(self.cr, self.uid, q)
491             if node.use:
492                 instances = model.browse(self.cr, self.uid, ids)
493                 value = [inst[node.use] for inst in instances]
494             else:
495                 value = ids
496         elif node.id:
497             value = self.get_id(node.id)
498         else:
499             value = None
500         return value
501
502     def process_eval(self, node):
503         return eval(node.expression, self.eval_context)
504
505     def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True):
506         # TODO this should be refactored as something like model.get_field() in bin/osv
507         if field_name in model._columns:
508             column = model._columns[field_name]
509         elif field_name in model._inherit_fields:
510             column = model._inherit_fields[field_name][2]
511         else:
512             raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name))
513         if is_ref(expression):
514             elements = self.process_ref(expression, column)
515             if column._type in ("many2many", "one2many"):
516                 value = [(6, 0, elements)]
517             else: # many2one
518                 if isinstance(elements, (list,tuple)):
519                     value = self._get_first_result(elements)
520                 else:
521                     value = elements
522         elif column._type == "many2one":
523             value = self.get_id(expression)
524         elif column._type == "one2many":
525             other_model = self.get_model(column._obj)
526             value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression]
527         elif column._type == "many2many":
528             ids = [self.get_id(xml_id) for xml_id in expression]
529             value = [(6, 0, ids)]
530         elif column._type == "date" and is_string(expression):
531             # enforce ISO format for string date values, to be locale-agnostic during tests
532             time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
533             value = expression
534         elif column._type == "datetime" and is_string(expression):
535             # enforce ISO format for string datetime values, to be locale-agnostic during tests
536             time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
537             value = expression
538         else: # scalar field
539             if is_eval(expression):
540                 value = self.process_eval(expression)
541             else:
542                 value = expression
543             # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
544         return value
545
546     def process_context(self, node):
547         self.context = node.__dict__
548         if node.uid:
549             self.uid = self.get_id(node.uid)
550         if node.noupdate:
551             self.noupdate = node.noupdate
552
553     def process_python(self, node):
554         python, statements = node.items()[0]
555         model = self.get_model(python.model)
556         statements = statements.replace("\r\n", "\n")
557         code_context = { 'model': model, 'cr': self.cr, 'uid': self.uid, 'log': self._log, 'context': self.context }
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         import openerp.netsvc as netsvc
599         wf_service = netsvc.LocalService("workflow")
600         wf_service.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
601
602     def _eval_params(self, model, params):
603         args = []
604         for i, param in enumerate(params):
605             if isinstance(param, types.ListType):
606                 value = self._eval_params(model, param)
607             elif is_ref(param):
608                 value = self.process_ref(param)
609             elif is_eval(param):
610                 value = self.process_eval(param)
611             elif isinstance(param, types.DictionaryType): # supports XML syntax
612                 param_model = self.get_model(param.get('model', model))
613                 if 'search' in param:
614                     q = eval(param['search'], self.eval_context)
615                     ids = param_model.search(self.cr, self.uid, q)
616                     value = self._get_first_result(ids)
617                 elif 'eval' in param:
618                     local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
619                     local_context.update(self.id_map)
620                     value = eval(param['eval'], self.eval_context, local_context)
621                 else:
622                     raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
623             else:
624                 value = param # scalar value
625             args.append(value)
626         return args
627
628     def process_function(self, node):
629         function, params = node.items()[0]
630         if self.isnoupdate(function) and self.mode != 'init':
631             return
632         model = self.get_model(function.model)
633         if function.eval:
634             args = self.process_eval(function.eval)
635         else:
636             args = self._eval_params(function.model, params)
637         method = function.name
638         getattr(model, method)(self.cr, self.uid, *args)
639
640     def _set_group_values(self, node, values):
641         if node.groups:
642             group_names = node.groups.split(',')
643             groups_value = []
644             for group in group_names:
645                 if group.startswith('-'):
646                     group_id = self.get_id(group[1:])
647                     groups_value.append((3, group_id))
648                 else:
649                     group_id = self.get_id(group)
650                     groups_value.append((4, group_id))
651             values['groups_id'] = groups_value
652
653     def process_menuitem(self, node):
654         self.validate_xml_id(node.id)
655
656         if not node.parent:
657             parent_id = False
658             self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
659             res = self.cr.fetchone()
660             values = {'parent_id': parent_id, 'name': node.name}
661         else:
662             parent_id = self.get_id(node.parent)
663             values = {'parent_id': parent_id}
664             if node.name:
665                 values['name'] = node.name
666             try:
667                 res = [ self.get_id(node.id) ]
668             except: # which exception ?
669                 res = None
670
671         if node.action:
672             action_type = node.type or 'act_window'
673             icons = {
674                 "act_window": 'STOCK_NEW',
675                 "report.xml": 'STOCK_PASTE',
676                 "wizard": 'STOCK_EXECUTE',
677                 "url": 'STOCK_JUMP_TO',
678             }
679             values['icon'] = icons.get(action_type, 'STOCK_NEW')
680             if action_type == 'act_window':
681                 action_id = self.get_id(node.action)
682                 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
683                 ir_act_window_result = self.cr.fetchone()
684                 assert ir_act_window_result, "No window action defined for this id %s !\n" \
685                         "Verify that this is a window action or add a type argument." % (node.action,)
686                 action_type, action_mode, action_name, view_id, target = ir_act_window_result
687                 if view_id:
688                     self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
689                     # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
690                     action_mode = self.cr.fetchone()
691                 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
692                 if self.cr.rowcount:
693                     action_mode = self.cr.fetchone()
694                 if action_type == 'tree':
695                     values['icon'] = 'STOCK_INDENT'
696                 elif action_mode and action_mode.startswith('tree'):
697                     values['icon'] = 'STOCK_JUSTIFY_FILL'
698                 elif action_mode and action_mode.startswith('graph'):
699                     values['icon'] = 'terp-graph'
700                 elif action_mode and action_mode.startswith('calendar'):
701                     values['icon'] = 'terp-calendar'
702                 if target == 'new':
703                     values['icon'] = 'STOCK_EXECUTE'
704                 if not values.get('name', False):
705                     values['name'] = action_name
706             elif action_type == 'wizard':
707                 action_id = self.get_id(node.action)
708                 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
709                 ir_act_wizard_result = self.cr.fetchone()
710                 if (not values.get('name', False)) and ir_act_wizard_result:
711                     values['name'] = ir_act_wizard_result[0]
712             else:
713                 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
714         if node.sequence:
715             values['sequence'] = node.sequence
716         if node.icon:
717             values['icon'] = node.icon
718
719         self._set_group_values(node, values)
720
721         pid = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
722                 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
723                 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
724
725         if node.id and parent_id:
726             self.id_map[node.id] = int(parent_id)
727
728         if node.action and pid:
729             action_type = node.type or 'act_window'
730             action_id = self.get_id(node.action)
731             action = "ir.actions.%s,%d" % (action_type, action_id)
732             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
733                     'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
734
735     def process_act_window(self, node):
736         assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
737         assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
738         assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
739         self.validate_xml_id(node.id)
740         view_id = False
741         if node.view:
742             view_id = self.get_id(node.view)
743         if not node.context:
744             node.context={}
745         context = eval(str(node.context), self.eval_context)
746         values = {
747             'name': node.name,
748             'type': node.type or 'ir.actions.act_window',
749             'view_id': view_id,
750             'domain': node.domain,
751             'context': context,
752             'res_model': node.res_model,
753             'src_model': node.src_model,
754             'view_type': node.view_type or 'form',
755             'view_mode': node.view_mode or 'tree,form',
756             'usage': node.usage,
757             'limit': node.limit,
758             'auto_refresh': node.auto_refresh,
759             'multi': getattr(node, 'multi', False),
760         }
761
762         self._set_group_values(node, values)
763
764         if node.target:
765             values['target'] = node.target
766         id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
767                 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
768         self.id_map[node.id] = int(id)
769
770         if node.src_model:
771             keyword = 'client_action_relate'
772             value = 'ir.actions.act_window,%s' % id
773             replace = node.replace or True
774             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
775                     node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
776         # TODO add remove ir.model.data
777
778     def process_delete(self, node):
779         assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
780         if self.pool.get(node.model):
781             if node.search:
782                 ids = self.pool.get(node.model).search(self.cr, self.uid, eval(node.search, self.eval_context))
783             else:
784                 ids = [self.get_id(node.id)]
785             if len(ids):
786                 self.pool.get(node.model).unlink(self.cr, self.uid, ids)
787         else:
788             self._log("Record not deleted.")
789
790     def process_url(self, node):
791         self.validate_xml_id(node.id)
792
793         res = {'name': node.name, 'url': node.url, 'target': node.target}
794
795         id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
796                 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
797         self.id_map[node.id] = int(id)
798         # ir_set
799         if (not node.menu or eval(node.menu)) and id:
800             keyword = node.keyword or 'client_action_multi'
801             value = 'ir.actions.act_url,%s' % id
802             replace = node.replace or True
803             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
804                     keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
805                     noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
806
807     def process_ir_set(self, node):
808         if not self.mode == 'init':
809             return False
810         _, fields = node.items()[0]
811         res = {}
812         for fieldname, expression in fields.items():
813             if is_eval(expression):
814                 value = eval(expression.expression, self.eval_context)
815             else:
816                 value = expression
817             res[fieldname] = value
818         self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
819                 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
820                 isobject=res.get('isobject', False), meta=res.get('meta',None))
821
822     def process_report(self, node):
823         values = {}
824         for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
825             values[dest] = getattr(node, f)
826             assert values[dest], "Attribute %s of report is empty !" % (f,)
827         for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
828             if getattr(node, field):
829                 values[dest] = getattr(node, field)
830         if node.auto:
831             values['auto'] = eval(node.auto)
832         if node.sxw:
833             sxw_file = misc.file_open(node.sxw)
834             try:
835                 sxw_content = sxw_file.read()
836                 values['report_sxw_content'] = sxw_content
837             finally:
838                 sxw_file.close()
839         if node.header:
840             values['header'] = eval(node.header)
841         values['multi'] = node.multi and eval(node.multi)
842         xml_id = node.id
843         self.validate_xml_id(xml_id)
844
845         self._set_group_values(node, values)
846
847         id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
848                 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
849         self.id_map[xml_id] = int(id)
850
851         if not node.menu or eval(node.menu):
852             keyword = node.keyword or 'client_print_multi'
853             value = 'ir.actions.report.xml,%s' % id
854             replace = node.replace or True
855             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
856                     keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
857
858     def process_none(self):
859         """
860         Empty node or commented node should not pass silently.
861         """
862         self._log_assert_failure("You have an empty block in your tests.")
863
864
865     def process(self, yaml_string):
866         """
867         Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
868         """
869         yaml_tag.add_constructors()
870
871         is_preceded_by_comment = False
872         for node in yaml.load(yaml_string):
873             is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
874             try:
875                 self._process_node(node)
876             except Exception, e:
877                 _logger.exception(e)
878                 raise
879
880     def _process_node(self, node):
881         if is_comment(node):
882             self.process_comment(node)
883         elif is_assert(node):
884             self.process_assert(node)
885         elif is_record(node):
886             self.process_record(node)
887         elif is_python(node):
888             self.process_python(node)
889         elif is_menuitem(node):
890             self.process_menuitem(node)
891         elif is_delete(node):
892             self.process_delete(node)
893         elif is_url(node):
894             self.process_url(node)
895         elif is_context(node):
896             self.process_context(node)
897         elif is_ir_set(node):
898             self.process_ir_set(node)
899         elif is_act_window(node):
900             self.process_act_window(node)
901         elif is_report(node):
902             self.process_report(node)
903         elif is_workflow(node):
904             if isinstance(node, types.DictionaryType):
905                 self.process_workflow(node)
906             else:
907                 self.process_workflow({node: []})
908         elif is_function(node):
909             if isinstance(node, types.DictionaryType):
910                 self.process_function(node)
911             else:
912                 self.process_function({node: []})
913         elif node is None:
914             self.process_none()
915         else:
916             raise YamlImportException("Can not process YAML block: %s" % node)
917
918     def _log_node(self, node, is_preceded_by_comment):
919         if is_comment(node):
920             is_preceded_by_comment = True
921             self._log(node)
922         elif not is_preceded_by_comment:
923             if isinstance(node, types.DictionaryType):
924                 msg = "Creating %s\n with %s"
925                 args = node.items()[0]
926                 self._log(msg, *args)
927             else:
928                 self._log(node)
929         else:
930             is_preceded_by_comment = False
931         return is_preceded_by_comment
932
933 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
934     if idref is None:
935         idref = {}
936     loglevel = logging.TEST if kind == 'test' else logging.DEBUG
937     yaml_string = yamlfile.read()
938     yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
939     yaml_interpreter.process(yaml_string)
940
941 # keeps convention of convert.py
942 convert_yaml_import = yaml_import
943
944 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
945     def f():
946         time.sleep(delay)
947         cr = None
948         fp = None
949         try:
950             cr = sql_db.db_connect(db_name).cursor()
951             fp = misc.file_open(file_name)
952             convert_yaml_import(cr, module_name, fp, {}, 'update', True)
953         finally:
954             if cr: cr.close()
955             if fp: fp.close()
956     threading.Thread(target=f).start()
957
958
959 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: