diff --git a/openerp/fields.py b/openerp/fields.py index cf90a8a01d4..ce58e4ea0c0 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -20,13 +20,14 @@ from openerp.tools import float_precision, float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, ustr, OrderedSet from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT -from openerp.tools.translate import html_translate +from openerp.tools.translate import html_translate, _ DATE_LENGTH = len(date.today().strftime(DATE_FORMAT)) DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT)) EMPTY_DICT = frozendict() _logger = logging.getLogger(__name__) +_schema = logging.getLogger(__name__[:-7] + '.schema') Default = object() # default value for __init__() methods @@ -866,6 +867,12 @@ class Field(object): # Read from/write to database # + def check_schema(self, model): + """ Make sure the database contains everything for this field. Return + ``True`` if the schema was altered and the field must be recomputed. + """ + pass + def read(self, records): """ Read the value of ``self`` on ``records``, and store it in cache. """ return NotImplementedError("Method read() undefined on %s" % self) @@ -2172,6 +2179,12 @@ class One2many(_RelationalMulti): fnames = [name for name in fnames if name != self.inverse_name] return super(One2many, self).convert_to_onchange(value, record, fnames) + def check_schema(self, model): + if self.comodel_name in model.env: + comodel = model.env[self.comodel_name] + if self.inverse_name not in comodel._fields: + raise UserError(_("No inverse field %r found for %r") % (self.inverse_name, self.comodel_name)) + def read(self, records): # retrieve the lines in the comodel comodel = records.env[self.comodel_name].with_context(**self.context) @@ -2285,12 +2298,21 @@ class Many2many(_RelationalMulti): def _setup_regular_base(self, model): super(Many2many, self)._setup_regular_base(model) - if not self.relation and self.store: - # retrieve self.relation from the corresponding column - column = self.to_column() - if isinstance(column, fields.many2many): - self.relation, self.column1, self.column2 = column._sql_names(model) - elif self.store: + if self.store: + if not (self.relation and self.column1 and self.column2): + # table name is based on the stable alphabetical order of tables + comodel = model.env[self.comodel_name] + if not self.relation: + tables = sorted([model._table, comodel._table]) + assert tables[0] != tables[1], \ + "%s: Implicit/canonical naming of many2many relationship " \ + "table is not possible when source and destination models " \ + "are the same" % self + self.relation = '%s_%s_rel' % tuple(tables) + if not self.column1: + self.column1 = '%s_id' % model._table + if not self.column2: + self.column2 = '%s_id' % comodel._table # check validity of table name check_pg_name(self.relation) @@ -2314,6 +2336,35 @@ class Many2many(_RelationalMulti): _column_auto_join = property(attrgetter('auto_join')) _column_limit = property(attrgetter('limit')) + def check_schema(self, model): + cr = model._cr + rel, id1, id2 = self.relation, self.column1, self.column2 + # do not create relations for custom fields as they do not belong to a module + # they will be automatically removed when dropping the corresponding ir.model.field + # table name for custom relation all starts with x_, see __init__ + if not rel.startswith('x_'): + model._save_relation_table(rel, self._module) + cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (rel,)) + if not cr.dictfetchall(): + if self.comodel_name not in model.env: + raise UserError(_('Many2many comodel does not exist: %r') % (self.comodel_name,)) + comodel = model.env[self.comodel_name] + cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (rel, id1, id2, id1, id2)) + # create foreign key references with ondelete=cascade, unless the targets are SQL views + cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (comodel._table,)) + if not cr.fetchall(): + model._m2o_add_foreign_key_unchecked(rel, id2, comodel, 'cascade', self._module) + cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (model._table,)) + if not cr.fetchall(): + model._m2o_add_foreign_key_unchecked(rel, id1, model, 'cascade', self._module) + + cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id1)) + cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id2)) + cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (rel, model._table, comodel._table)) + cr.commit() + _schema.debug("Create table '%s': m2m relation between '%s' and '%s'", rel, model._table, comodel._table) + return True + def read(self, records): comodel = records.env[self.comodel_name] @@ -2434,6 +2485,6 @@ class Id(Field): # imported here to avoid dependency cycle issues from openerp import SUPERUSER_ID -from .exceptions import AccessError, MissingError +from .exceptions import AccessError, MissingError, UserError from .models import check_pg_name, BaseModel, IdType from .osv import fields diff --git a/openerp/models.py b/openerp/models.py index ad8aa8f1c00..284deff5c6a 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -2399,12 +2399,9 @@ class BaseModel(object): # Don't update custom (also called manual) fields continue - if field.type == 'one2many': - self._o2m_raise_on_missing_reference(field.column) - - elif field.type == 'many2many': - res = self._m2m_raise_or_create_relation(field.column) - if res and field.compute: + if not field.column_type: + # the field is not stored as a column + if field.check_schema(self) and field.compute: stored_fields.append(field) else: @@ -2537,7 +2534,7 @@ class BaseModel(object): if comodel._auto and comodel._table != 'ir_actions': self._m2o_fix_foreign_key(self._table, name, comodel, field.ondelete) - elif field.column_type: + else: # the column doesn't exist in database, create it cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, name, field.column_type[1])) cr.execute("COMMENT ON COLUMN %s.\"%s\" IS %%s" % (self._table, name), (field.string,)) @@ -2673,48 +2670,6 @@ class BaseModel(object): self._cr.execute(query, (self._table,)) return {row['attname']: row for row in self._cr.dictfetchall()} - @api.model_cr - def _o2m_raise_on_missing_reference(self, column): - # TODO this check should be a method on fields.one2many. - if column._obj in self.env: - other = self.env[column._obj] - # TODO the condition could use fields_get_keys(). - if column._fields_id not in other._fields: - raise UserError(_("There is no reference field '%s' found for '%s'") % (column._fields_id, column._obj)) - - @api.model_cr - def _m2m_raise_or_create_relation(self, column): - """ Create the table for the relation if necessary. - Return ``True`` if the relation had to be created. - """ - cr = self._cr - m2m_tbl, col1, col2 = column._sql_names(self) - # do not create relations for custom fields as they do not belong to a module - # they will be automatically removed when dropping the corresponding ir.model.field - # table name for custom relation all starts with x_, see __init__ - if not m2m_tbl.startswith('x_'): - self._save_relation_table(m2m_tbl, column._module) - cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (m2m_tbl,)) - if not cr.dictfetchall(): - if column._obj not in self.env: - raise UserError(_('Many2Many destination model does not exist: `%s`') % (column._obj,)) - dest_model = self.env[column._obj] - cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (m2m_tbl, col1, col2, col1, col2)) - # create foreign key references with ondelete=cascade, unless the targets are SQL views - cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (dest_model._table,)) - if not cr.fetchall(): - self._m2o_add_foreign_key_unchecked(m2m_tbl, col2, dest_model, 'cascade', column._module) - cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (self._table,)) - if not cr.fetchall(): - self._m2o_add_foreign_key_unchecked(m2m_tbl, col1, self, 'cascade', column._module) - - cr.execute('CREATE INDEX ON "%s" ("%s")' % (m2m_tbl, col1)) - cr.execute('CREATE INDEX ON "%s" ("%s")' % (m2m_tbl, col2)) - cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (m2m_tbl, self._table, dest_model._table)) - cr.commit() - _schema.debug("Create table '%s': m2m relation between '%s' and '%s'", m2m_tbl, self._table, dest_model._table) - return True - @api.model_cr def _add_sql_constraints(self): """ diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 985b1135d4b..14bd0ec76b2 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -966,7 +966,7 @@ class expression(object): push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, column._fields_id, comodel._table)), model)) elif column._type == 'many2many': - rel_table, rel_id1, rel_id2 = column._sql_names(model) + rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2 if operator in HIERARCHY_FUNCS: def _rec_convert(ids):