diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 74589003611..633db6de6a3 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -557,7 +557,9 @@ class ir_model_access(osv.osv): model_name = model # TransientModel records have no access rights, only an implicit access rule - if self.pool.get(model_name).is_transient(): + if not self.pool.get(model_name): + _logger.error('Missing model %s' % (model_name, )) + elif self.pool.get(model_name).is_transient(): return True # We check if a specific rule exists diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index 4bbff5479c3..213cff18cc6 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -265,7 +265,7 @@ class ir_ui_menu(osv.osv): } if menu.action and menu.action.type in ('ir.actions.act_window','ir.actions.client') and menu.action.res_model: obj = self.pool.get(menu.action.res_model) - if obj._needaction: + if obj and obj._needaction: if menu.action.type=='ir.actions.act_window': dom = menu.action.domain and eval(menu.action.domain, {'uid': uid}) or [] else: diff --git a/openerp/addons/base/module/module_view.xml b/openerp/addons/base/module/module_view.xml index 729d3ef7b4c..c3b3684f07c 100644 --- a/openerp/addons/base/module/module_view.xml +++ b/openerp/addons/base/module/module_view.xml @@ -39,7 +39,7 @@ + 'ilike', self)]" string="Module"/> diff --git a/openerp/addons/base/res/res_bank_view.xml b/openerp/addons/base/res/res_bank_view.xml index ac91684d4d9..33b98c690c6 100644 --- a/openerp/addons/base/res/res_bank_view.xml +++ b/openerp/addons/base/res/res_bank_view.xml @@ -106,8 +106,8 @@ - - + + diff --git a/openerp/addons/base/res/res_company_view.xml b/openerp/addons/base/res/res_company_view.xml index cac53b7a76a..b8cb12d3dd0 100644 --- a/openerp/addons/base/res/res_company_view.xml +++ b/openerp/addons/base/res/res_company_view.xml @@ -46,10 +46,10 @@
- +
- +
@@ -177,7 +177,7 @@ + options='{"no_open": True}' attrs="{'invisible': [('is_company','=', True)]}" /> diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index 8bd5c3a0179..a1c06a52494 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -341,14 +341,12 @@ class res_users(osv.osv): def copy(self, cr, uid, id, default=None, context=None): user2copy = self.read(cr, uid, [id], ['login','name'])[0] - if default is None: - default = {} - copy_pattern = _("%s (copy)") - copydef = dict(login=(copy_pattern % user2copy['login']), - name=(copy_pattern % user2copy['name']), - ) - copydef.update(default) - return super(res_users, self).copy(cr, uid, id, copydef, context) + default = dict(default or {}) + if ('name' not in default) and ('partner_id' not in default): + default['name'] = _("%s (copy)") % user2copy['name'] + if 'login' not in default: + default['login'] = _("%s (copy)") % user2copy['login'] + return super(res_users, self).copy(cr, uid, id, default, context) def context_get(self, cr, uid, context=None): user = self.browse(cr, SUPERUSER_ID, uid, context) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 4c84b611629..7545fd5f4b2 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -52,6 +52,7 @@ import pickle import re import simplejson import time +import traceback import types import psycopg2 @@ -65,6 +66,7 @@ import openerp.tools as tools from openerp.tools.config import config from openerp.tools.misc import CountingStream from openerp.tools.safe_eval import safe_eval as eval +from ast import literal_eval from openerp.tools.translate import _ from openerp import SUPERUSER_ID from query import Query @@ -375,8 +377,10 @@ class browse_record(object): else: return attr else: - error_msg = "Field '%s' does not exist in object '%s'" % (name, self) + error_msg = "Field '%s' does not exist in object '%s'" % (name, self) self.__logger.warning(error_msg) + if self.__logger.isEnabledFor(logging.DEBUG): + self.__logger.debug(''.join(traceback.format_stack())) raise KeyError(error_msg) # if the field is a classic one or a many2one, we'll fetch all classic and many2one fields @@ -713,7 +717,7 @@ class BaseModel(object): def log(self, cr, uid, id, message, secondary=False, context=None): return _logger.warning("log() is deprecated. Please use OpenChatter notification system instead of the res.log mechanism.") - + def view_init(self, cr, uid, fields_list, context=None): """Override this method to do specific things when a view on the object is opened.""" pass @@ -903,7 +907,7 @@ class BaseModel(object): # If new class defines a constraint with # same function name, we let it override # the old one. - + new[c2] = c exist = True break @@ -1672,7 +1676,7 @@ class BaseModel(object): def user_has_groups(self, cr, uid, groups, context=None): """Return true if the user is at least member of one of the groups in groups_str. Typically used to resolve ``groups`` attribute - in view and model definitions. + in view and model definitions. :param str groups: comma-separated list of fully-qualified group external IDs, e.g.: ``base.group_user,base.group_system`` @@ -1715,7 +1719,7 @@ class BaseModel(object): the field should be completely removed from the view, as it is completely unavailable for non-members - :return: True if field should be included in the result of fields_view_get + :return: True if field should be included in the result of fields_view_get """ if node.tag == 'field' and node.get('name') in self._all_columns: column = self._all_columns[node.get('name')].column @@ -1808,7 +1812,13 @@ class BaseModel(object): field = model_fields.get(node.get('name')) if field: transfer_field_to_modifiers(field, modifiers) - + #evaluate the options as python code, but send it as json to the client + if node.get('options'): + try: + node.set('options', simplejson.dumps(literal_eval(node.get('options')))) + except Exception, e: + _logger.exception('Invalid `options´ attribute, should be a valid python expression: %r', node.get('options')) + raise except_orm('Invalid options', 'Invalid options: %r %s' % (node.get('options'), e)) elif node.tag in ('form', 'tree'): result = self.view_header_get(cr, user, False, node.tag, context) @@ -1990,7 +2000,7 @@ class BaseModel(object): def _get_default_calendar_view(self, cr, user, context=None): """ Generates a default calendar view by trying to infer calendar fields from a number of pre-set attribute names - + :param cr: database cursor :param int user: user id :param dict context: connection context @@ -2069,7 +2079,7 @@ class BaseModel(object): def raise_view_error(error_msg, child_view_id): view, child_view = self.pool.get('ir.ui.view').browse(cr, user, [view_id, child_view_id], context) - error_msg = error_msg % {'parent_xml_id': view.xml_id} + error_msg = error_msg % {'parent_xml_id': view.xml_id} raise AttributeError("View definition error for inherited view '%s' on model '%s': %s" % (child_view.xml_id, self._name, error_msg)) @@ -2105,7 +2115,7 @@ class BaseModel(object): if all(node.get(attr) == spec.get(attr) \ for attr in spec.attrib if attr not in ('position','version')): - # Version spec should match parent's root element's version + # Version spec should match parent's root element's version if spec.get('version') and spec.get('version') != source.get('version'): return None return node @@ -2179,7 +2189,7 @@ class BaseModel(object): raise_view_error("Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \ (tag, spec.get('version'), source.get('version')), inherit_id) raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, inherit_id) - + return source def apply_view_inheritance(cr, user, source, inherit_id): @@ -2394,7 +2404,7 @@ class BaseModel(object): or ``'='``. :param int limit: optional max number of records to return :rtype: list - :return: list of pairs ``(id,text_repr)`` for all matching records. + :return: list of pairs ``(id,text_repr)`` for all matching records. """ return self._name_search(cr, user, name, args, operator, context, limit) @@ -2524,7 +2534,7 @@ class BaseModel(object): # This is useful to implement kanban views for instance, where all columns # should be displayed even if they don't contain any record. - # Grab the list of all groups that should be displayed, including all present groups + # Grab the list of all groups that should be displayed, including all present groups present_group_ids = [x[groupby][0] for x in read_group_result if x[groupby]] all_groups,folded = self._group_by_full[groupby](self, cr, uid, present_group_ids, domain, read_group_order=read_group_order, @@ -2900,28 +2910,28 @@ class BaseModel(object): def _m2o_fix_foreign_key(self, cr, source_table, source_field, dest_model, ondelete): # Find FK constraint(s) currently established for the m2o field, - # and see whether they are stale or not + # and see whether they are stale or not cr.execute("""SELECT confdeltype as ondelete_rule, conname as constraint_name, cl2.relname as foreign_table FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, pg_attribute as att1, pg_attribute as att2 - WHERE con.conrelid = cl1.oid - AND cl1.relname = %s - AND con.confrelid = cl2.oid - AND array_lower(con.conkey, 1) = 1 - AND con.conkey[1] = att1.attnum - AND att1.attrelid = cl1.oid - AND att1.attname = %s - AND array_lower(con.confkey, 1) = 1 - AND con.confkey[1] = att2.attnum - AND att2.attrelid = cl2.oid - AND att2.attname = %s + WHERE con.conrelid = cl1.oid + AND cl1.relname = %s + AND con.confrelid = cl2.oid + AND array_lower(con.conkey, 1) = 1 + AND con.conkey[1] = att1.attnum + AND att1.attrelid = cl1.oid + AND att1.attname = %s + AND array_lower(con.confkey, 1) = 1 + AND con.confkey[1] = att2.attnum + AND att2.attrelid = cl2.oid + AND att2.attname = %s AND con.contype = 'f'""", (source_table, source_field, 'id')) constraints = cr.dictfetchall() if constraints: if len(constraints) == 1: # Is it the right constraint? - cons, = constraints + cons, = constraints if cons['ondelete_rule'] != POSTGRES_CONFDELTYPES.get((ondelete or 'set null').upper(), 'a')\ or cons['foreign_table'] != dest_model._table: _schema.debug("Table '%s': dropping obsolete FK constraint: '%s'", @@ -3043,14 +3053,14 @@ class BaseModel(object): ('numeric', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]), ('float8', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]), ] - if f_pg_type == 'varchar' and f._type == 'char' and f_pg_size < f.size: + if f_pg_type == 'varchar' and f._type == 'char' and ((f.size is None and f_pg_size) or f_pg_size < f.size): cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k)) cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, pg_varchar(f.size))) cr.execute('UPDATE "%s" SET "%s"=temp_change_size::%s' % (self._table, k, pg_varchar(f.size))) cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,)) cr.commit() _schema.debug("Table '%s': column '%s' (type varchar) changed size from %s to %s", - self._table, k, f_pg_size, f.size) + self._table, k, f_pg_size or 'unlimited', f.size or 'unlimited') for c in casts: if (f_pg_type==c[0]) and (f._type==c[1]): if f_pg_type != f_obj_type: @@ -3283,8 +3293,7 @@ class BaseModel(object): # attlen is the number of bytes necessary to represent the type when # the type has a fixed size. If the type has a varying size attlen is # -1 and atttypmod is the size limit + 4, or -1 if there is no limit. - # Thus the query can return a negative size for a unlimited varchar. - cr.execute("SELECT c.relname,a.attname,a.attlen,a.atttypmod,a.attnotnull,a.atthasdef,t.typname,CASE WHEN a.attlen=-1 THEN a.atttypmod-4 ELSE a.attlen END as size " \ + cr.execute("SELECT c.relname,a.attname,a.attlen,a.atttypmod,a.attnotnull,a.atthasdef,t.typname,CASE WHEN a.attlen=-1 THEN (CASE WHEN a.atttypmod=-1 THEN 0 ELSE a.atttypmod-4 END) ELSE a.attlen END as size " \ "FROM pg_class c,pg_attribute a,pg_type t " \ "WHERE c.relname=%s " \ "AND c.oid=a.attrelid " \ @@ -3294,7 +3303,7 @@ class BaseModel(object): def _o2m_raise_on_missing_reference(self, cr, f): # TODO this check should be a method on fields.one2many. - + other = self.pool.get(f._obj) if other: # TODO the condition could use fields_get_keys(). @@ -3811,7 +3820,7 @@ class BaseModel(object): the length of `ids`, and raise an appropriate exception if it does not. """ if cr.rowcount != len(ids): - # Attempt to distinguish record rule restriction vs deleted records, + # Attempt to distinguish record rule restriction vs deleted records, # to provide a more specific error message cr.execute('SELECT id FROM ' + self._table + ' WHERE id IN %s', (tuple(ids),)) if cr.rowcount != len(ids): @@ -3866,13 +3875,13 @@ class BaseModel(object): self._check_record_rules_result_count(cr, uid, sub_ids, operation, context=context) def _workflow_trigger(self, cr, uid, ids, trigger, context=None): - """Call given workflow trigger as a result of a CRUD operation""" + """Call given workflow trigger as a result of a CRUD operation""" wf_service = netsvc.LocalService("workflow") for res_id in ids: getattr(wf_service, trigger)(uid, self._name, res_id, cr) def _workflow_signal(self, cr, uid, ids, signal, context=None): - """Send given workflow signal""" + """Send given workflow signal""" wf_service = netsvc.LocalService("workflow") for res_id in ids: wf_service.trg_validate(uid, self._name, res_id, signal, cr) @@ -3903,7 +3912,7 @@ class BaseModel(object): self.check_access_rights(cr, uid, 'unlink') ir_property = self.pool.get('ir.property') - + # Check if the records are used as default properties. domain = [('res_id', '=', False), ('value_reference', 'in', ['%s,%s' % (self._name, i) for i in ids]), @@ -4823,56 +4832,45 @@ class BaseModel(object): else: raise IndexError( _("Record #%d of %s not found, cannot copy!") %( id, self._name)) - # TODO it seems fields_get can be replaced by _all_columns (no need for translation) - fields = self.fields_get(cr, uid, context=context) - for f in fields: - ftype = fields[f]['type'] - - if self._log_access and f in LOG_ACCESS_COLUMNS: - del data[f] + # build a black list of fields that should not be copied + blacklist = set(MAGIC_COLUMNS + ['parent_left', 'parent_right']) + def blacklist_given_fields(obj): + # blacklist the fields that are given by inheritance + for other, field_to_other in obj._inherits.items(): + blacklist.add(field_to_other) + if field_to_other in default: + # all the fields of 'other' are given by the record: default[field_to_other], + # except the ones redefined in self + blacklist.update(set(self.pool.get(other)._all_columns) - set(self._columns)) + else: + blacklist_given_fields(self.pool.get(other)) + blacklist_given_fields(self) + res = dict(default) + for f, colinfo in self._all_columns.items(): + field = colinfo.column if f in default: - data[f] = default[f] - elif 'function' in fields[f]: - del data[f] - elif ftype == 'many2one': - try: - data[f] = data[f] and data[f][0] - except: - pass - elif ftype == 'one2many': - res = [] - rel = self.pool.get(fields[f]['relation']) - if data[f]: - # duplicate following the order of the ids - # because we'll rely on it later for copying - # translations in copy_translation()! - data[f].sort() - for rel_id in data[f]: - # the lines are first duplicated using the wrong (old) - # parent but then are reassigned to the correct one thanks - # to the (0, 0, ...) - d = rel.copy_data(cr, uid, rel_id, context=context) - if d: - res.append((0, 0, d)) - data[f] = res - elif ftype == 'many2many': - data[f] = [(6, 0, data[f])] + pass + elif f in blacklist: + pass + elif isinstance(field, fields.function): + pass + elif field._type == 'many2one': + res[f] = data[f] and data[f][0] + elif field._type == 'one2many': + other = self.pool.get(field._obj) + # duplicate following the order of the ids because we'll rely on + # it later for copying translations in copy_translation()! + lines = [other.copy_data(cr, uid, line_id, context=context) for line_id in sorted(data[f])] + # the lines are duplicated using the wrong (old) parent, but then + # are reassigned to the correct one thanks to the (0, 0, ...) + res[f] = [(0, 0, line) for line in lines if line] + elif field._type == 'many2many': + res[f] = [(6, 0, data[f])] + else: + res[f] = data[f] - del data['id'] - - # make sure we don't break the current parent_store structure and - # force a clean recompute! - for parent_column in ['parent_left', 'parent_right']: - data.pop(parent_column, None) - # Remove _inherits field's from data recursively, missing parents will - # be created by create() (so that copy() copy everything). - def remove_ids(inherits_dict): - for parent_table in inherits_dict: - del data[inherits_dict[parent_table]] - remove_ids(self.pool.get(parent_table)._inherits) - remove_ids(self._inherits) - return data + return res def copy_translations(self, cr, uid, old_id, new_id, context=None): if context is None: @@ -5031,7 +5029,7 @@ class BaseModel(object): :return: map of ids to their fully qualified XML ID, defaulting to an empty string when there's none - (to be usable as a function field), + (to be usable as a function field), e.g.:: { 'id': 'module.ext_id', diff --git a/openerp/tests/test_orm.py b/openerp/tests/test_orm.py index bce5bc3903a..ef8c0cf19aa 100644 --- a/openerp/tests/test_orm.py +++ b/openerp/tests/test_orm.py @@ -6,6 +6,85 @@ import common UID = common.ADMIN_USER_ID DB = common.DB +class TestInherits(common.TransactionCase): + """ test the behavior of the orm for models that use _inherits; + specifically: res.users, that inherits from res.partner + """ + + def setUp(self): + super(TestInherits, self).setUp() + self.partner = self.registry('res.partner') + self.user = self.registry('res.users') + + def test_create(self): + """ creating a user should automatically create a new partner """ + partners_before = self.partner.search(self.cr, UID, []) + foo_id = self.user.create(self.cr, UID, {'name': 'Foo', 'login': 'foo', 'password': 'foo'}) + foo = self.user.browse(self.cr, UID, foo_id) + + self.assertNotIn(foo.partner_id.id, partners_before) + + def test_create_with_ancestor(self): + """ creating a user with a specific 'partner_id' should not create a new partner """ + par_id = self.partner.create(self.cr, UID, {'name': 'Foo'}) + partners_before = self.partner.search(self.cr, UID, []) + foo_id = self.user.create(self.cr, UID, {'partner_id': par_id, 'login': 'foo', 'password': 'foo'}) + partners_after = self.partner.search(self.cr, UID, []) + + self.assertEqual(set(partners_before), set(partners_after)) + + foo = self.user.browse(self.cr, UID, foo_id) + self.assertEqual(foo.name, 'Foo') + self.assertEqual(foo.partner_id.id, par_id) + + def test_read(self): + """ inherited fields should be read without any indirection """ + foo_id = self.user.create(self.cr, UID, {'name': 'Foo', 'login': 'foo', 'password': 'foo'}) + foo_values, = self.user.read(self.cr, UID, [foo_id]) + partner_id = foo_values['partner_id'][0] + partner_values, = self.partner.read(self.cr, UID, [partner_id]) + self.assertEqual(foo_values['name'], partner_values['name']) + + foo = self.user.browse(self.cr, UID, foo_id) + self.assertEqual(foo.name, foo.partner_id.name) + + def test_copy(self): + """ copying a user should automatically copy its partner, too """ + foo_id = self.user.create(self.cr, UID, {'name': 'Foo', 'login': 'foo', 'password': 'foo'}) + foo_before, = self.user.read(self.cr, UID, [foo_id]) + bar_id = self.user.copy(self.cr, UID, foo_id, {'login': 'bar', 'password': 'bar'}) + foo_after, = self.user.read(self.cr, UID, [foo_id]) + + self.assertEqual(foo_before, foo_after) + + foo, bar = self.user.browse(self.cr, UID, [foo_id, bar_id]) + self.assertEqual(bar.login, 'bar') + self.assertNotEqual(foo.id, bar.id) + self.assertNotEqual(foo.partner_id.id, bar.partner_id.id) + + def test_copy_with_ancestor(self): + """ copying a user with 'parent_id' in defaults should not duplicate the partner """ + foo_id = self.user.create(self.cr, UID, {'name': 'Foo', 'login': 'foo', 'password': 'foo'}) + par_id = self.partner.create(self.cr, UID, {'name': 'Bar'}) + + foo_before, = self.user.read(self.cr, UID, [foo_id]) + partners_before = self.partner.search(self.cr, UID, []) + bar_id = self.user.copy(self.cr, UID, foo_id, {'partner_id': par_id, 'login': 'bar'}) + foo_after, = self.user.read(self.cr, UID, [foo_id]) + partners_after = self.partner.search(self.cr, UID, []) + + self.assertEqual(foo_before, foo_after) + self.assertEqual(set(partners_before), set(partners_after)) + + foo, bar = self.user.browse(self.cr, UID, [foo_id, bar_id]) + self.assertNotEqual(foo.id, bar.id) + self.assertEqual(bar.partner_id.id, par_id) + self.assertEqual(bar.login, 'bar', "login is given from copy parameters") + self.assertEqual(bar.password, foo.password, "password is given from original record") + self.assertEqual(bar.name, 'Bar', "name is given from specific partner") + + + CREATE = lambda values: (0, False, values) UPDATE = lambda id, values: (1, id, values) DELETE = lambda id: (2, id, False) @@ -19,6 +98,7 @@ def sorted_by_id(list_of_dicts): return sorted(list_of_dicts, key=lambda d: d.get('id')) class TestO2MSerialization(common.TransactionCase): + """ test the orm method 'write' on one2many fields """ def setUp(self): super(TestO2MSerialization, self).setUp() diff --git a/openerp/tools/yaml_import.py b/openerp/tools/yaml_import.py index aec17d3c31f..7e1c7152b0f 100644 --- a/openerp/tools/yaml_import.py +++ b/openerp/tools/yaml_import.py @@ -313,13 +313,13 @@ class YamlInterpreter(object): #context = self.get_context(record, self.eval_context) #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module context = record.context + view_info = False if view_id: varg = view_id if view_id is True: varg = False - view = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context) - view_id = etree.fromstring(view['arch'].encode('utf-8')) + view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context) - record_dict = self._create_record(model, fields, view_id, default=default) + record_dict = self._create_record(model, fields, view_info, default=default) _logger.debug("RECORD_DICT %s" % record_dict) id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, record.model, \ self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context) @@ -327,16 +327,18 @@ class YamlInterpreter(object): if config.get('import_partial'): self.cr.commit() - def _create_record(self, model, fields, view=False, parent={}, default=True): - if view is not False: - defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {} - fg = model.fields_get(self.cr, SUPERUSER_ID, context=self.context) - else: - defaults = {} - fg = {} - record_dict = {} - fields = fields or {} - + def _create_record(self, model, fields, view_info=False, parent={}, default=True): + """This function processes the !record tag in yalm files. It simulates the record creation through an xml + view (either specified on the !record tag or the default one for this object), including the calls to + on_change() functions. + :param model: model instance + :param fields: dictonary mapping the field names and their values + :param view_info: result of fields_view_get() called on the object + :param parent: dictionary containing the values already computed for the parent, in case of one2many fields + :param default: if True, the default values must be processed too or not + :return: dictionary mapping the field names and their values, ready to use when calling the create() function + :rtype: dict + """ def process_val(key, val): if fg[key]['type']=='many2one': if type(val) in (tuple,list): @@ -348,63 +350,75 @@ class YamlInterpreter(object): val = map(lambda x: (0,0,x), val) return val - # Process all on_change calls - nodes = (view is not False) and [view] or [] - while nodes: - el = nodes.pop(0) - if el.tag=='field': - field_name = el.attrib['name'] - assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name) - if field_name in fields: - view2 = None - # if the form view is not inline, we call fields_view_get - if (view is not False) and (fg[field_name]['type']=='one2many'): - view2 = view.find("field[@name='%s']/form"%(field_name,)) - if not view2: - view2 = self.pool.get(fg[field_name]['relation']).fields_view_get(self.cr, SUPERUSER_ID, False, 'form', self.context) - view2 = etree.fromstring(view2['arch'].encode('utf-8')) + view = view_info and etree.fromstring(view_info['arch'].encode('utf-8')) or False + fields = fields or {} + if view is not False: + fg = view_info['fields'] + # gather the default values on the object. (Can't use `fields´ as parameter instead of {} because we may + # have references like `base.main_company´ in the yaml file and it's not compatible with the function) + defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {} - field_value = self._eval_field(model, field_name, fields[field_name], view2, parent=record_dict, default=default) - record_dict[field_name] = field_value - #if (field_name in defaults) and defaults[field_name] == field_value: - # print '*** You can remove these lines:', field_name, field_value - elif (field_name in defaults): - if (field_name not in record_dict): - record_dict[field_name] = process_val(field_name, defaults[field_name]) - else: - continue + # copy the default values in record_dict, only if they are in the view (because that's what the client does) + # the other default values will be added later on by the create(). + record_dict = dict([(key, val) for key, val in defaults.items() if key in fg]) - if not el.attrib.get('on_change', False): - continue - match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change']) - assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], ) + # Process all on_change calls + nodes = [view] + while nodes: + el = nodes.pop(0) + if el.tag=='field': + field_name = el.attrib['name'] + assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name) + if field_name in fields: + one2many_form_view = None + if (view is not False) and (fg[field_name]['type']=='one2many'): + # for one2many fields, we want to eval them using the inline form view defined on the parent + one2many_form_view = view_info['fields'][field_name]['views'].get('form') + # if the form view is not defined inline, we call fields_view_get() + if not one2many_form_view: + one2many_form_view = self.pool.get(fg[field_name]['relation']).fields_view_get(self.cr, SUPERUSER_ID, False, 'form', self.context) - # creating the context - class parent2(object): - def __init__(self, d): - self.d = d - def __getattr__(self, name): - return self.d.get(name, False) + field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default) + record_dict[field_name] = field_value + #if (field_name in defaults) and defaults[field_name] == field_value: + # print '*** You can remove these lines:', field_name, field_value - ctx = record_dict.copy() - ctx['context'] = self.context - ctx['uid'] = 1 - ctx['parent'] = parent2(parent) - for a in fg: - if a not in ctx: - ctx[a]=process_val(a, defaults.get(a, False)) + #if field_name has a default value or a value is given in the yaml file, we must call its on_change() + elif field_name not in defaults: + continue - # Evaluation args - args = map(lambda x: eval(x, ctx), match.group(2).split(',')) - result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args) - for key, val in (result or {}).get('value', {}).items(): - if key not in fields: - assert key in fg, "The returning field '%s' from your on_change call '%s' does not exist on the object '%s'" % (key, match.group(1), model._name) + if not el.attrib.get('on_change', False): + continue + match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change']) + assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], ) + + # creating the context + class parent2(object): + def __init__(self, d): + self.d = d + def __getattr__(self, name): + return self.d.get(name, False) + + ctx = record_dict.copy() + ctx['context'] = self.context + ctx['uid'] = SUPERUSER_ID + ctx['parent'] = parent2(parent) + for a in fg: + if a not in ctx: + ctx[a]=process_val(a, defaults.get(a, False)) + + # Evaluation args + args = map(lambda x: eval(x, ctx), match.group(2).split(',')) + result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args) + for key, val in (result or {}).get('value', {}).items(): + assert key in fg, "The returning field '%s' from your on_change call '%s' does not exist either on the object '%s', either in the view '%s' used for the creation" % (key, match.group(1), model._name, view_info['name']) record_dict[key] = process_val(key, val) #if (key in fields) and record_dict[key] == process_val(key, val): # print '*** You can remove these lines:', key, val - else: - nodes = list(el) + nodes + else: + nodes = list(el) + nodes + else: + record_dict = {} for field_name, expression in fields.items(): if field_name in record_dict: @@ -440,7 +454,7 @@ class YamlInterpreter(object): def process_eval(self, node): return eval(node.expression, self.eval_context) - def _eval_field(self, model, field_name, expression, view=False, parent={}, default=True): + def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True): # TODO this should be refactored as something like model.get_field() in bin/osv if field_name in model._columns: column = model._columns[field_name] @@ -461,7 +475,7 @@ class YamlInterpreter(object): value = self.get_id(expression) elif column._type == "one2many": other_model = self.get_model(column._obj) - value = [(0, 0, self._create_record(other_model, fields, view, parent, default=default)) for fields in expression] + value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression] elif column._type == "many2many": ids = [self.get_id(xml_id) for xml_id in expression] value = [(6, 0, ids)]