[IMP] ir_model: add table and columns fields for many2many fields

The fields are properly reflected for base many2many fields.  Moreover, the
code has been improved to remove a custom many2many field whose table is still
used by another field.

Onchange methods have been added:
- When name, model and comodel are chosen, propose table and column names.
- When table is modified, propose column names if the table is already used by
  another many2many field.
This commit is contained in:
Thomas Groutars
2015-08-14 16:37:29 +02:00
committed by Raphael Collet
parent eadc08f531
commit 9979939cd1
4 changed files with 77 additions and 14 deletions
+4 -1
View File
@@ -39,7 +39,10 @@ CREATE TABLE ir_model_fields (
required boolean default False,
selectable boolean default False,
translate boolean default False,
serialization_field_id integer references ir_model_fields on delete cascade,
serialization_field_id integer references ir_model_fields on delete cascade,
relation_table varchar,
column1 varchar,
column2 varchar,
primary key(id)
);
+60 -8
View File
@@ -245,6 +245,9 @@ class ir_model_fields(osv.osv):
"structure of the serialization field, instead "
"of having its own database column. This cannot be "
"changed after creation."),
'relation_table': fields.char("Relation Table", help="Used for custom many2many fields to define a custom relation table name"),
'column1': fields.char("Column 1", help="Column referring to the record in the model table"),
'column2': fields.char("Column 2", help="Column referring to the record in the comodel table"),
}
_rec_name='field_description'
_defaults = {
@@ -287,26 +290,75 @@ class ir_model_fields(osv.osv):
('size_gt_zero', 'CHECK (size>=0)',_size_gt_zero_msg ),
]
@api.model
def _custom_many2many_names(self, model_name, comodel_name):
""" Return default names for the table and columns of a custom many2many field. """
rel1 = self.env[model_name]._table
rel2 = self.env[comodel_name]._table
table = 'x_%s_%s_rel' % tuple(sorted([rel1, rel2]))
if rel1 == rel2:
return (table, 'id1', 'id2')
else:
return (table, '%s_id' % rel1, '%s_id' % rel2)
@api.onchange('ttype', 'model_id', 'relation')
def _onchange_ttype(self):
if self.ttype == 'many2many' and self.model_id and self.relation:
names = self._custom_many2many_names(self.model_id.model, self.relation)
self.relation_table, self.column1, self.column2 = names
else:
self.relation_table = False
self.column1 = False
self.column2 = False
@api.onchange('relation_table')
def _onchange_relation_table(self):
if self.relation_table:
# check whether other fields use the same table
others = self.search([('ttype', '=', 'many2many'),
('relation_table', '=', self.relation_table),
('id', 'not in', self._origin.ids)])
if others:
for other in others:
if (other.model, other.relation) == (self.relation, self.model):
# other is a candidate inverse field
self.column1 = other.column2
self.column2 = other.column1
return
return {'warning':{
'title': _("Warning"),
'message': _("The table %r if used for other, possibly incompatible fields.") % self.relation_table,
}}
def _drop_column(self, cr, uid, ids, context=None):
tables_to_drop = set()
for field in self.browse(cr, uid, ids, context):
if field.name in MAGIC_COLUMNS:
continue
model = self.pool[field.model]
cr.execute('select relkind from pg_class where relname=%s', (model._table,))
cr.execute('SELECT relkind FROM pg_class WHERE relname=%s', (model._table,))
result = cr.fetchone()
cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name ='%s' and column_name='%s'" %(model._table, field.name))
cr.execute("""SELECT column_name FROM information_schema.columns
WHERE table_name=%s AND column_name=%s""",
(model._table, field.name))
column_name = cr.fetchone()
if column_name and (result and result[0] == 'r'):
cr.execute('ALTER table "%s" DROP column "%s" cascade' % (model._table, field.name))
# remove m2m relation table for custom fields
# we consider the m2m relation is only one way as it's not possible
# to specify the relation table in the interface for custom fields
# TODO master: maybe use ir.model.relations for custom fields
if field.state == 'manual' and field.ttype == 'many2many':
rel_name = model._fields[field.name].relation
cr.execute('DROP table "%s"' % (rel_name))
rel_name = field.relation_table or model._fields[field.name].relation
tables_to_drop.add(rel_name)
model._pop_field(cr, uid, field.name, context=context)
if tables_to_drop:
# drop the relation tables that are not used by other fields
cr.execute("""SELECT relation_table FROM ir_model_fields
WHERE relation_table IN %s AND id NOT IN %s""",
(tuple(tables_to_drop), tuple(ids)))
tables_to_keep = set(row[0] for row in cr.fetchall())
for rel_name in tables_to_drop - tables_to_keep:
cr.execute('DROP TABLE "%s"' % rel_name)
return True
def unlink(self, cr, user, ids, context=None):
+6
View File
@@ -68,6 +68,9 @@
<field name="ttype"/>
<field name="relation" attrs="{'required': [('ttype','in',['many2one','one2many','many2many'])],'readonly': [('ttype','!=','one2many'), ('ttype','!=','many2one'), ('ttype','!=','many2many')]}"/>
<field name="relation_field" attrs="{'required': [('ttype','=','one2many')], 'readonly': [('ttype','!=','one2many')]}"/>
<field name="relation_table" attrs="{'readonly': ['|', ('ttype', '!=', 'many2many'), ('state', '!=', 'manual')]}"/>
<field name="column1" attrs="{'readonly': ['|', ('ttype', '!=', 'many2many'), ('state', '!=', 'manual')]}"/>
<field name="column2" attrs="{'readonly': ['|', ('ttype', '!=', 'many2many'), ('state', '!=', 'manual')]}"/>
<field name="selection" attrs="{'required': [('ttype','in',['selection','reference'])], 'readonly': [('ttype','not in',['selection','reference'])]}"/>
<field name="size" attrs="{'required': [('ttype','in',['char','reference'])], 'readonly': [('ttype','not in',['char','reference'])]}"/>
<field name="domain" attrs="{'readonly': [('relation','=','')]}"/>
@@ -171,6 +174,9 @@
<field name="relation" attrs="{'required': [('ttype','in', ['many2one','one2many','many2many'])],
'readonly': [('ttype','not in', ['many2one','one2many','many2many'])]}"/>
<field name="relation_field" attrs="{'required': [('ttype','=','one2many')], 'readonly': [('ttype','!=','one2many')]}"/>
<field name="relation_table" attrs="{'readonly': ['|', ('ttype', '!=', 'many2many'), ('state', '!=', 'manual')]}"/>
<field name="column1" attrs="{'readonly': ['|', ('ttype', '!=', 'many2many'), ('state', '!=', 'manual')]}"/>
<field name="column2" attrs="{'readonly': ['|', ('ttype', '!=', 'many2many'), ('state', '!=', 'manual')]}"/>
<field name="selection" attrs="{'required': [('ttype','in',['selection','reference'])], 'readonly': [('ttype','not in',['selection','reference'])]}"/>
<field name="size" attrs="{'invisible': [('ttype','not in',['char','text','reference'])]}"/>
<field name="domain" attrs="{'readonly': [('relation','=','')]}"/>
+7 -5
View File
@@ -391,6 +391,9 @@ class BaseModel(object):
'translate': bool(getattr(f, 'translate', False)),
'relation_field': f.type == 'one2many' and f.inverse_name or None,
'serialization_field_id': None,
'relation_table': f.type == 'many2many' and f.relation or None,
'column1': f.type == 'many2many' and f.column1 or None,
'column2': f.type == 'many2many' and f.column2 or None,
}
if getattr(f, 'serialization_field', None):
# resolve link to serialization_field if specified by name
@@ -696,11 +699,10 @@ class BaseModel(object):
if partial and field['relation'] not in self.pool:
continue
attrs['comodel_name'] = field['relation']
_rel1 = field['relation'].replace('.', '_')
_rel2 = field['model'].replace('.', '_')
attrs['relation'] = 'x_%s_%s_%s_rel' % (_rel1, _rel2, name)
attrs['column1'] = 'id1'
attrs['column2'] = 'id2'
rel, col1, col2 = self.env['ir.model.fields']._custom_many2many_names(field['model'], field['relation'])
attrs['relation'] = field['relation_table'] or rel
attrs['column1'] = field['column1'] or col1
attrs['column2'] = field['column2'] or col2
attrs['domain'] = eval(field['domain']) if field['domain'] else None
self._add_field(name, Field.by_type[field['ttype']](**attrs))