diff --git a/addons/mrp/report/mrp_production_templates.xml b/addons/mrp/report/mrp_production_templates.xml index 702e7cdf6d1..6a9f15bac1d 100644 --- a/addons/mrp/report/mrp_production_templates.xml +++ b/addons/mrp/report/mrp_production_templates.xml @@ -34,13 +34,13 @@ Printing date:
-
+
Partner Ref:
- +
-
+
SO Number:
- +
diff --git a/addons/survey/tests/test_survey.py b/addons/survey/tests/test_survey.py index a827fc29ee7..b04858c3058 100644 --- a/addons/survey/tests/test_survey.py +++ b/addons/survey/tests/test_survey.py @@ -43,7 +43,7 @@ class TestSurvey(TransactionCase): self.assertEqual(question.question, "Q0", msg="Title of the Question is somehow modified.") def test_01_question_type_validation_save_line_function(self): - for (question_type, text) in self.env['survey.question']._columns['type'].selection: + for (question_type, text) in self.env['survey.question']._fields['type'].selection: # Each question ype must have validation function. self.assertTrue(hasattr(self.env['survey.question'], 'validate_' + question_type), msg="Question must have a validation method in\ the form of 'validate_' followed by the name of the type.") @@ -53,7 +53,7 @@ class TestSurvey(TransactionCase): the form of 'save_line_' followed by the name of the type.") def test_02_question_answer_required(self): - for (question_type, text) in self.env['survey.question']._columns['type'].selection: + for (question_type, text) in self.env['survey.question']._fields['type'].selection: # Blank value of field is not accepted for mandatory questions. if question_type == 'multiple_choice': question = self.env['survey.question'].sudo(self.survey_manager).create({ diff --git a/openerp/addons/base/tests/test_acl.py b/openerp/addons/base/tests/test_acl.py index 84e851add5b..f65f4209a68 100644 --- a/openerp/addons/base/tests/test_acl.py +++ b/openerp/addons/base/tests/test_acl.py @@ -22,9 +22,6 @@ class TestACL(TransactionCase): def _set_field_groups(self, model, field_name, groups): field = model._fields[field_name] self.patch(field, 'groups', groups) - if field.column: - self.patch(field.column, 'groups', groups) - self.patch(field.column, '_prefetch', False) def test_field_visibility_restriction(self): """Check that model-level ``groups`` parameter effectively restricts access to that diff --git a/openerp/addons/base/tests/test_base.py b/openerp/addons/base/tests/test_base.py index bf4195d770d..2616a4b535f 100644 --- a/openerp/addons/base/tests/test_base.py +++ b/openerp/addons/base/tests/test_base.py @@ -442,7 +442,6 @@ class TestParentStore(TransactionCase): # force res_partner_category.copy() to copy children category = self.env['res.partner.category'] self.patch(category._fields['child_ids'], 'copy', True) - self.patch(category._columns['child_ids'], 'copy', True) # setup categories self.root = category.create({'name': 'Root category'}) diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index 62f42f92bdd..12ac2ae79f8 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -641,11 +641,9 @@ class TestAutoJoin(TransactionCase): # Get test columns def patch_auto_join(model, fname, value): self.patch(model._fields[fname], 'auto_join', value) - self.patch(model._columns[fname], '_auto_join', value) def patch_domain(model, fname, value): self.patch(model._fields[fname], 'domain', value) - self.patch(model._columns[fname], '_domain', value) # Get country/state data country_us = self.env['res.country'].search([('code', 'like', 'US')], limit=1) diff --git a/openerp/addons/test_inherit/tests/test_inherit.py b/openerp/addons/test_inherit/tests/test_inherit.py index 6e9b80726b6..4c93556a8c5 100644 --- a/openerp/addons/test_inherit/tests/test_inherit.py +++ b/openerp/addons/test_inherit/tests/test_inherit.py @@ -67,8 +67,6 @@ class test_inherits(common.TransactionCase): # the extra values are added, both in the field and the column self.assertEqual(mother._fields['state'].selection, [('a', 'A'), ('b', 'B'), ('c', 'C'), ('d', 'D')]) - self.assertEqual(mother._columns['state'].selection, - [('a', 'A'), ('b', 'B'), ('c', 'C'), ('d', 'D')]) def test_50_search_one2many(self): """ check search on one2many field based on inherited many2one field. """ diff --git a/openerp/addons/test_new_api/tests/__init__.py b/openerp/addons/test_new_api/tests/__init__.py index eef5f0f4aea..2bbcfe40990 100644 --- a/openerp/addons/test_new_api/tests/__init__.py +++ b/openerp/addons/test_new_api/tests/__init__.py @@ -2,7 +2,6 @@ from . import test_new_fields from . import test_onchange -from . import test_field_conversions from . import test_attributes from . import test_ui from . import test_domain diff --git a/openerp/addons/test_new_api/tests/test_field_conversions.py b/openerp/addons/test_new_api/tests/test_field_conversions.py deleted file mode 100644 index dc6d5da372b..00000000000 --- a/openerp/addons/test_new_api/tests/test_field_conversions.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- - -from openerp import fields -from openerp.tests import common - -class TestFieldToColumn(common.TransactionCase): - def test_char(self): - # create a field, initialize its attributes, and convert it to a column - field = fields.Char(string="test string", required=True) - field.setup_base(self.env['res.partner'], 'test') - column = field.to_column() - - self.assertEqual(column.string, "test string") - self.assertTrue(column.required) diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py index 3c12988d194..93f3e3d881e 100644 --- a/openerp/addons/test_new_api/tests/test_new_fields.py +++ b/openerp/addons/test_new_api/tests/test_new_fields.py @@ -68,7 +68,6 @@ class TestFields(common.TransactionCase): self.assertFalse(field.store) self.assertFalse(field.compute) self.assertFalse(field.inverse) - self.assertFalse(field.column) # find messages for message in self.env['test_new_api.message'].search([]): diff --git a/openerp/fields.py b/openerp/fields.py index 77b75980d8d..e14384bef12 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -104,13 +104,10 @@ class MetaField(type): MetaField.by_type[cls.type] = cls # compute class attributes to avoid calling dir() on fields - cls.column_attrs = [] cls.related_attrs = [] cls.description_attrs = [] for attr in dir(cls): - if attr.startswith('_column_'): - cls.column_attrs.append((attr[8:], attr)) - elif attr.startswith('_related_'): + if attr.startswith('_related_'): cls.related_attrs.append((attr[9:], attr)) elif attr.startswith('_description_'): cls.description_attrs.append((attr[13:], attr)) @@ -306,8 +303,6 @@ class Field(object): 'automatic': False, # whether the field is automatically created ("magic" field) 'inherited': False, # whether the field is inherited (_inherits) - 'origin': None, # the column from which the field was created - 'column': None, # the column corresponding to the field 'name': None, # name of the field 'model_name': None, # name of the model of this field @@ -733,7 +728,7 @@ class Field(object): @property def _description_searchable(self): - return bool(self.store or self.search or (self.column and self.column._fnct_search)) + return bool(self.store or self.search) @property def _description_sortable(self): @@ -753,52 +748,6 @@ class Field(object): return field_help.get(self.name) or self.help return self.help - ############################################################################ - # - # Conversion to column instance - # - - def to_column(self): - """ Return a column object corresponding to ``self``, or ``None``. """ - if self.column: - return self.column - - if not self.store and (self.compute or not self.origin): - # non-stored computed fields do not have a corresponding column - return None - - # determine column parameters - #_logger.debug("Create fields._column for Field %s", self) - args = {} - for attr, prop in self.column_attrs: - args[attr] = getattr(self, prop) - for attr, value in self._attrs.iteritems(): - args[attr] = value - - if self.origin: - # let the origin provide a valid column for the given parameters - self.column = self.origin.new(_computed_field=bool(self.compute), **args) - else: - # create a fresh new column of the right type - self.column = getattr(fields, self.type)(**args) - - return self.column - - # properties used by to_column() to create a column instance - _column__module = property(attrgetter('_module')) - _column_copy = property(attrgetter('copy')) - _column_select = property(attrgetter('index')) - _column_manual = property(attrgetter('manual')) - _column_string = property(attrgetter('string')) - _column_help = property(attrgetter('help')) - _column_readonly = property(attrgetter('readonly')) - _column_required = property(attrgetter('required')) - _column_states = property(attrgetter('states')) - _column_groups = property(attrgetter('groups')) - _column_change_default = property(attrgetter('change_default')) - _column_deprecated = property(attrgetter('deprecated')) - _column_group_operator = property(attrgetter('group_operator')) - ############################################################################ # # Conversion of values @@ -984,7 +933,7 @@ class Field(object): """ Determine the value of ``self`` for ``record``. """ env = record.env - if self.column and not (self.compute and env.in_onchange): + if self.store and not (self.compute and env.in_onchange): # this is a stored field or an old-style function field if self.compute: # this is a stored computed field, check for recomputation @@ -1209,12 +1158,8 @@ class Float(Field): return self._digits _related__digits = property(attrgetter('_digits')) - _description_digits = property(attrgetter('digits')) - _column_digits = property(lambda self: not callable(self._digits) and self._digits) - _column_digits_compute = property(lambda self: callable(self._digits) and self._digits) - def convert_to_column(self, value, record): result = float(value or 0.0) digits = self.digits @@ -1254,11 +1199,8 @@ class Monetary(Field): super(Monetary, self).__init__(string=string, currency_field=currency_field, **kwargs) _related_currency_field = property(attrgetter('currency_field')) - _description_currency_field = property(attrgetter('currency_field')) - _column_currency_field = property(attrgetter('currency_field')) - def _setup_regular_base(self, model): super(Monetary, self)._setup_regular_base(model) if not self.currency_field: @@ -1298,7 +1240,6 @@ class _String(Field): kwargs['translate'] = bool(kwargs['translate']) super(_String, self).__init__(string=string, **kwargs) - _column_translate = property(attrgetter('translate')) _related_translate = property(attrgetter('translate')) def _description_translate(self, env): @@ -1364,7 +1305,6 @@ class Char(_String): def column_type(self): return ('varchar', pg_varchar(self.size)) - _column_size = property(attrgetter('size')) _related_size = property(attrgetter('size')) _description_size = property(attrgetter('size')) @@ -1427,28 +1367,18 @@ class Html(_String): if self.translate is True and self.sanitize: self.translate = html_translate - _column_sanitize = property(attrgetter('sanitize')) _related_sanitize = property(attrgetter('sanitize')) - _description_sanitize = property(attrgetter('sanitize')) - - _column_sanitize_tags = property(attrgetter('sanitize_tags')) _related_sanitize_tags = property(attrgetter('sanitize_tags')) - _description_sanitize_tags = property(attrgetter('sanitize_tags')) - - _column_sanitize_attributes = property(attrgetter('sanitize_attributes')) _related_sanitize_attributes = property(attrgetter('sanitize_attributes')) - _description_sanitize_attributes = property(attrgetter('sanitize_attributes')) - - _column_sanitize_style = property(attrgetter('sanitize_style')) _related_sanitize_style = property(attrgetter('sanitize_style')) - _description_sanitize_style = property(attrgetter('sanitize_style')) - - _column_strip_style = property(attrgetter('strip_style')) _related_strip_style = property(attrgetter('strip_style')) - _description_strip_style = property(attrgetter('strip_style')) - - _column_strip_classes = property(attrgetter('strip_classes')) _related_strip_classes = property(attrgetter('strip_classes')) + + _description_sanitize = property(attrgetter('sanitize')) + _description_sanitize_tags = property(attrgetter('sanitize_tags')) + _description_sanitize_attributes = property(attrgetter('sanitize_attributes')) + _description_sanitize_style = property(attrgetter('sanitize_style')) + _description_strip_style = property(attrgetter('strip_style')) _description_strip_classes = property(attrgetter('strip_classes')) def convert_to_column(self, value, record): @@ -1627,7 +1557,6 @@ class Binary(Field): def column_type(self): return None if self.attachment else ('bytea', 'bytea') - _column_attachment = property(attrgetter('attachment')) _description_attachment = property(attrgetter('attachment')) def convert_to_column(self, value, record): @@ -1766,14 +1695,6 @@ class Selection(Field): else: return selection - @property - def _column_selection(self): - if isinstance(self.selection, basestring): - method = self.selection - return lambda self, *a, **kw: getattr(self, method)(*a, **kw) - else: - return self.selection - def get_values(self, env): """ return a list of the possible values """ selection = self.selection @@ -1872,10 +1793,6 @@ class _Relational(Field): def _description_domain(self, env): return self.domain(env[self.model_name]) if callable(self.domain) else self.domain - _column_obj = property(attrgetter('comodel_name')) - _column_domain = property(attrgetter('domain')) - _column_context = property(attrgetter('context')) - def null(self, record): return record.env[self.comodel_name] @@ -1930,9 +1847,6 @@ class Many2one(_Relational): if not self.delegate: self.delegate = name in model._inherits.values() - _column_ondelete = property(attrgetter('ondelete')) - _column_auto_join = property(attrgetter('auto_join')) - def _update(self, records, value): """ Update the cached value of ``self`` for ``records`` with ``value``. This is used to reflect the assignment ``value[name] = records``, where @@ -2181,10 +2095,6 @@ class One2many(_RelationalMulti): _description_relation_field = property(attrgetter('inverse_name')) - _column_fields_id = property(attrgetter('inverse_name')) - _column_auto_join = property(attrgetter('auto_join')) - _column_limit = property(attrgetter('limit')) - def convert_to_onchange(self, value, record, fnames=()): if fnames: # do not serialize self's inverse field @@ -2342,12 +2252,6 @@ class Many2many(_RelationalMulti): # add self in m2m, so that its inverse field can find it m2m[(self.relation, self.column1, self.column2)] = self - _column_rel = property(attrgetter('relation')) - _column_id1 = property(attrgetter('column1')) - _column_id2 = property(attrgetter('column2')) - _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 @@ -2481,10 +2385,6 @@ class Id(Field): 'readonly': True, } - def to_column(self): - self.column = fields.integer(self.string) - return self.column - def __get__(self, record, owner): if record is None: return self # the field is accessed through the class owner @@ -2499,4 +2399,3 @@ class Id(Field): from openerp import SUPERUSER_ID 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 ae82b875af5..9c7edc15f86 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -46,7 +46,6 @@ from . import api from . import tools from .api import Environment from .exceptions import AccessError, MissingError, ValidationError, UserError -from .osv import fields from .osv.query import Query from .tools import frozendict, lazy_property, ormcache, Collector, LastOrderedSet, OrderedSet from .tools.config import config @@ -170,13 +169,6 @@ class MetaModel(api.Meta): if isinstance(val, Field): val.args = dict(val.args, _module=self._module) - # transform columns into new-style fields (enables field inheritance) - for name, column in self._columns.iteritems(): - if name in self.__dict__: - _logger.warning("In class %s, field %r overriding an existing value", self, name) - column._module = self._module - setattr(self, name, column.to_field()) - def _get_addon_name(self, full_name): # The (OpenERP) module name can be in the ``openerp.addons`` namespace # or not. For instance, module ``sale`` can be imported as @@ -246,7 +238,6 @@ class BaseModel(object): _inherit = None # Python-inherited models ('model' or ['model']) _inherits = {} # inherited models {'parent_model': 'm2o_field'} - _columns = {} # field definitions (old API) _constraints = [] # Python constraints (old API) _table = None # SQL table name used by model @@ -388,8 +379,6 @@ class BaseModel(object): # basic setup of field field.setup_base(self, name) - # cls._columns will be updated once fields are fully set up - @api.model def _pop_field(self, name): """ Remove the field with the given ``name`` from the model. @@ -397,7 +386,6 @@ class BaseModel(object): """ cls = type(self) field = cls._fields.pop(name) - cls._columns.pop(name, None) if hasattr(cls, name): delattr(cls, name) return field @@ -743,7 +731,6 @@ class BaseModel(object): - copy the stored fields' functions in the registry, - retrieve custom fields and add them in the model, - ensure there is a many2one for each _inherits'd parent, - - update the children's _columns, - give a chance to each field to initialize itself. """ @@ -1997,19 +1984,17 @@ class BaseModel(object): for gb in groupby_fields: assert gb in fields, "Fields in 'groupby' must appear in the list of fields to read (perhaps it's missing in the list view?)" assert gb in self._fields, "Unknown field %r in 'groupby'" % gb - groupby_def = self._fields[gb].base_field.column - assert groupby_def and groupby_def._classic_write, "Fields in 'groupby' must be regular database-persisted fields (no function or related fields), or function fields with store=True" - if not (gb in self._fields): - # Don't allow arbitrary values, as this would be a SQL injection vector! - raise UserError(_('Invalid group_by specification: "%s".\nA group_by specification must be a list of valid fields.') % (gb,)) + gb_field = self._fields[gb].base_field + assert gb_field.store and gb_field.column_type, "Fields in 'groupby' must be regular database-persisted fields (no function or related fields), or function fields with store=True" aggregated_fields = [ f for f in fields if f != 'sequence' if f not in groupby_fields - if f in self._fields - if self._fields[f].group_operator - if getattr(self._fields[f].base_field.column, '_classic_write', False) + for field in [self._fields.get(f)] + if field + if field.group_operator + if field.base_field.store and field.base_field.column_type ] field_formatter = lambda f: ( @@ -2871,8 +2856,7 @@ class BaseModel(object): """ Setup the fields, except for recomputation triggers. """ cls = type(self) - # set up fields, and determine their corresponding column - cls._columns = {} + # set up fields bad_fields = [] for name, field in cls._fields.iteritems(): try: @@ -2886,9 +2870,6 @@ class BaseModel(object): bad_fields.append(name) continue raise - column = field.to_column() - if column: - cls._columns[name] = column for name in bad_fields: del cls._fields[name] @@ -3137,7 +3118,7 @@ class BaseModel(object): fields_pre = [ field for field in fields - if field.base_field.column._classic_write + if field.base_field.store and field.base_field.column_type if not (field.inherited and callable(field.base_field.translate)) ] @@ -3197,9 +3178,9 @@ class BaseModel(object): # Warn about deprecated fields now that fields_pre and fields_post are computed for f in field_names: - column = self._columns[f] - if column.deprecated: - _logger.warning('Field %s.%s is deprecated: %s', self._name, f, column.deprecated) + field = self._fields[f] + if field.deprecated: + _logger.warning('Field %s is deprecated: %s', field, field.deprecated) # store failed values in cache for the records that could not be read missing = self - fetched @@ -3580,7 +3561,7 @@ class BaseModel(object): for key, val in vals.iteritems(): field = self._fields.get(key) if field: - if field.column or field.inherited: + if field.store or field.inherited: old_vals[key] = val if field.inverse and not field.inherited: new_vals[key] = val @@ -3896,7 +3877,7 @@ class BaseModel(object): elif field.inherited: tocreate[field.related_field.model_name][name] = val del vals[name] - elif not field.column: + elif not field.store: del vals[name] elif field.inverse: protected_fields.append(field) @@ -4143,12 +4124,10 @@ class BaseModel(object): # also add missing joins for reaching the table containing the m2o field qualified_field = self._inherits_join_calc(alias, order_field, query) alias, order_field = qualified_field.replace('"', '').split('.', 1) - column = field.base_field.column - else: - column = field.column + field = field.base_field assert field.type == 'many2one', 'Invalid field passed to _generate_m2o_order_by()' - if not (column and column._classic_write): + if not field.store: _logger.debug("Many2one function/related fields must be stored " "to be used as ordering fields! Ignoring sorting for %s.%s", self._name, order_field) @@ -4192,17 +4171,16 @@ class BaseModel(object): else: if field.inherited: field = field.base_field - column = field.column - if column and column._classic_read: - qualifield_name = self._inherits_join_calc(alias, order_field, query, implicit=False, outer=True) - if field.type == 'boolean': - qualifield_name = "COALESCE(%s, false)" % qualifield_name - order_by_elements.append("%s %s" % (qualifield_name, order_direction)) - elif column and column._type == 'many2one': + if field.store and field.type == 'many2one': key = (field.model_name, field.comodel_name, order_field) if key not in seen: seen.add(key) order_by_elements += self._generate_m2o_order_by(alias, order_field, query, do_reverse, seen) + elif field.store and field.column_type: + qualifield_name = self._inherits_join_calc(alias, order_field, query, implicit=False, outer=True) + if field.type == 'boolean': + qualifield_name = "COALESCE(%s, false)" % qualifield_name + order_by_elements.append("%s %s" % (qualifield_name, order_direction)) else: continue # ignore non-readable or "non-joinable" fields @@ -4273,20 +4251,6 @@ class BaseModel(object): return _uniquify_list([x[0] for x in res]) - @api.model - def distinct_field_get(self, field, value, args=None, offset=0, limit=None): - """ Returns the different values ever entered for one field. This is used, - for example, in the client when the user hits enter on a char field. - - Deprecated. - """ - ffield = self._fields[field] - if ffield.inherited: - parent_model = ffield.related_field.model_name - return self.env[parent_model].distinct_field_get(field, value, args, offset, limit) - else: - return ffield.column.search(self._cr, self._model, args or [], field, value, offset, limit, self._uid) - @api.multi @api.returns(None, lambda value: value[0]) def copy_data(self, default=None): diff --git a/openerp/osv/__init__.py b/openerp/osv/__init__.py index f15a0ef9fbd..80a43a61ec4 100644 --- a/openerp/osv/__init__.py +++ b/openerp/osv/__init__.py @@ -2,4 +2,3 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import osv -import fields diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index b5d6b7a13ad..be3d340be83 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -121,8 +121,6 @@ from functools import partial from zlib import crc32 import openerp.modules -from . import fields -from .. import SUPERUSER_ID from ..models import MAGIC_COLUMNS, BaseModel import openerp.tools as tools @@ -796,7 +794,6 @@ class expression(object): model = leaf.model field = model._fields.get(path[0]) - column = getattr(field, 'column', None) comodel = model.env.get(getattr(field, 'comodel_name', None)) # ---------------------------------------- @@ -839,7 +836,7 @@ class expression(object): new_leaf = create_substitution_leaf(leaf, dom_leaf, model) push(new_leaf) - elif not column and path[0] in MAGIC_COLUMNS: + elif path[0] in MAGIC_COLUMNS: push_result(leaf) # ---------------------------------------- @@ -855,14 +852,14 @@ class expression(object): # as after transforming the column, it will go through this loop once again # ---------------------------------------- - elif len(path) > 1 and column and column._type == 'many2one' and column._auto_join: + elif len(path) > 1 and field.store and field.type == 'many2one' and field.auto_join: # res_partner.state_id = res_partner__state_id.id leaf.add_join_context(comodel, path[0], 'id', path[0]) push(create_substitution_leaf(leaf, (path[1], operator, right), comodel)) - elif len(path) > 1 and column and column._type == 'one2many' and column._auto_join: + elif len(path) > 1 and field.store and field.type == 'one2many' and field.auto_join: # res_partner.id = res_partner__bank_ids.partner_id - leaf.add_join_context(comodel, 'id', column._fields_id, path[0]) + leaf.add_join_context(comodel, 'id', field.inverse_name, path[0]) domain = field.domain(model) if callable(field.domain) else field.domain push(create_substitution_leaf(leaf, (path[1], operator, right), comodel)) if domain: @@ -871,22 +868,22 @@ class expression(object): push(create_substitution_leaf(leaf, elem, comodel)) push(create_substitution_leaf(leaf, AND_OPERATOR, comodel)) - elif len(path) > 1 and column and column._auto_join: - raise NotImplementedError('_auto_join attribute not supported on many2many column %s' % left) + elif len(path) > 1 and field.store and field.auto_join: + raise NotImplementedError('auto_join attribute not supported on field %s' % field) - elif len(path) > 1 and column and column._type == 'many2one': + elif len(path) > 1 and field.store and field.type == 'many2one': right_ids = comodel.with_context(active_test=False).search([('.'.join(path[1:]), operator, right)]).ids leaf.leaf = (path[0], 'in', right_ids) push(leaf) - # Making search easier when there is a left operand as column.o2m or column.m2m - elif len(path) > 1 and column and column._type in ['many2many', 'one2many']: + # Making search easier when there is a left operand as one2many or many2many + elif len(path) > 1 and field.store and field.type in ('many2many', 'one2many'): right_ids = comodel.search([('.'.join(path[1:]), operator, right)]).ids table_ids = model.with_context(active_test=False).search([(path[0], 'in', right_ids)]).ids leaf.leaf = ('id', 'in', table_ids) push(leaf) - elif not column: + elif not field.store: # Non-stored field should provide an implementation of search. if not field.search: # field does not support search! @@ -914,22 +911,22 @@ class expression(object): # ------------------------------------------------- # Applying recursivity on field(one2many) - elif column._type == 'one2many' and operator in HIERARCHY_FUNCS: + elif field.type == 'one2many' and operator in HIERARCHY_FUNCS: ids2 = to_ids(right, comodel) - if column._obj != model._name: - dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=column._obj) + if field.comodel_name != model._name: + dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=field.comodel_name) else: dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left) for dom_leaf in reversed(dom): push(create_substitution_leaf(leaf, dom_leaf, model)) - elif column._type == 'one2many': + elif field.type == 'one2many': call_null = True if right is not False: if isinstance(right, basestring): op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator) - domain = column._domain + domain = field.domain if callable(domain): domain = domain(model) ids2 = [x[0] for x in comodel.name_search(right, domain or [], op, limit=None)] @@ -946,12 +943,12 @@ class expression(object): call_null = False push(create_substitution_leaf(leaf, FALSE_LEAF, model)) else: - # determine ids1 <-- column._fields_id --- ids2 - if comodel._fields[column._fields_id].store: - ids1 = select_from_where(cr, column._fields_id, comodel._table, 'id', ids2, operator) + # determine ids1 <-- field.inverse_name --- ids2 + if comodel._fields[field.inverse_name].store: + ids1 = select_from_where(cr, field.inverse_name, comodel._table, 'id', ids2, operator) else: recs = comodel.browse(ids2).sudo().with_context(prefetch_fields=False) - ids1 = recs.mapped(column._fields_id).ids + ids1 = recs.mapped(field.inverse_name).ids if ids1: call_null = False o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' @@ -963,9 +960,9 @@ class expression(object): if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, column._fields_id, comodel._table)), model)) + push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field.inverse_name, comodel._table)), model)) - elif column._type == 'many2many': + elif field.type == 'many2many': rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2 if operator in HIERARCHY_FUNCS: @@ -983,7 +980,7 @@ class expression(object): if right is not False: if isinstance(right, basestring): op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator) - domain = column._domain + domain = field.domain if callable(domain): domain = domain(model) res_ids = [x[0] for x in comodel.name_search(right, domain or [], op, limit=None)] @@ -1010,11 +1007,11 @@ class expression(object): m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' push(create_substitution_leaf(leaf, ('id', m2m_op, select_distinct_from_where_not_null(cr, rel_id1, rel_table)), model)) - elif column._type == 'many2one': + elif field.type == 'many2one': if operator in HIERARCHY_FUNCS: ids2 = to_ids(right, comodel) - if column._obj != model._name: - dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=column._obj) + if field.comodel_name != model._name: + dom = HIERARCHY_FUNCS[operator](left, ids2, comodel, prefix=field.comodel_name) else: dom = HIERARCHY_FUNCS[operator]('id', ids2, model, parent=left) for dom_leaf in reversed(dom): @@ -1048,7 +1045,7 @@ class expression(object): # -> check for null only # ------------------------------------------------- - elif column._type == 'binary' and column.attachment: + elif field.type == 'binary' and field.attachment: if operator in ('=', '!=') and not right: inselect_operator = 'inselect' if operator in NEGATIVE_TERM_OPERATORS else 'not inselect' subselect = "SELECT res_id FROM ir_attachment WHERE res_model=%s AND res_field=%s" @@ -1056,7 +1053,7 @@ class expression(object): push(create_substitution_leaf(leaf, ('id', inselect_operator, (subselect, params)), model, internal=True)) else: _logger.error("Binary field '%s' stored in attachment: ignore %s %s %s", - column.string, left, operator, right) + field.string, left, operator, right) leaf.leaf = TRUE_LEAF push(leaf) @@ -1068,7 +1065,7 @@ class expression(object): # ------------------------------------------------- else: - if column._type == 'datetime' and right and len(right) == 10: + if field.type == 'datetime' and right and len(right) == 10: if operator in ('>', '<='): right += ' 23:59:59' else: @@ -1200,7 +1197,7 @@ class expression(object): else: # Must not happen raise ValueError("Invalid domain term %r" % (leaf,)) - elif (left in model._columns) and model._columns[left]._type == "boolean" and ((operator == '=' and right is False) or (operator == '!=' and right is True)): + elif left in model and model._fields[left].type == "boolean" and ((operator == '=' and right is False) or (operator == '!=' and right is True)): query = '(%s."%s" IS NULL or %s."%s" = false )' % (table_alias, left, table_alias, left) params = [] @@ -1208,7 +1205,7 @@ class expression(object): query = '%s."%s" IS NULL ' % (table_alias, left) params = [] - elif (left in model._columns) and model._columns[left]._type == "boolean" and ((operator == '!=' and right is False) or (operator == '==' and right is True)): + elif left in model and model._fields[left].type == "boolean" and ((operator == '!=' and right is False) or (operator == '==' and right is True)): query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table_alias, left, table_alias, left) params = [] @@ -1235,7 +1232,7 @@ class expression(object): sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) cast = '::text' if sql_operator.endswith('like') else '' - if left in model._columns: + if left in model: format = need_wildcard and '%s' or model._fields[left].column_format unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x column = '%s.%s' % (table_alias, _quote(left)) @@ -1256,7 +1253,7 @@ class expression(object): str_utf8 = str(right) params = '%%%s%%' % str_utf8 add_null = not str_utf8 - elif left in model._columns: + elif left in model: params = model._fields[left].convert_to_column(right, model) if add_null: diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py deleted file mode 100644 index f65ae38ce4d..00000000000 --- a/openerp/osv/fields.py +++ /dev/null @@ -1,1186 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -""" Fields: - - simple - - relations (one2many, many2one, many2many) - - function - - Fields Attributes: - * _classic_read: is a classic sql fields - * _type : field type - * _auto_join: for one2many and many2one fields, tells whether select - queries will join the relational table instead of replacing the - field condition by an equivalent-one based on a search. - * readonly - * required - * size -""" - -import base64 -import datetime as DT -import functools -import logging -import pytz -import re -import xmlrpclib -from operator import itemgetter -from psycopg2 import Binary - -import openerp -import openerp.tools as tools -from openerp.sql_db import LazyCursor -from openerp.tools.translate import _ -from openerp.tools import float_repr, float_round, frozendict, html_sanitize -import json -from openerp import SUPERUSER_ID - -# deprecated; kept for backward compatibility only -_get_cursor = LazyCursor - -EMPTY_DICT = frozendict() - -_logger = logging.getLogger(__name__) - -def _symbol_set(symb): - if symb is None or symb == False: - return None - elif isinstance(symb, unicode): - return symb.encode('utf-8') - return str(symb) - - -class _column(object): - """ Base of all fields, a database column - - An instance of this object is a *description* of a database column. It will - not hold any data, but only provide the methods to manipulate data of an - ORM record or even prepare/update the database to hold such a field of data. - """ - _classic_read = True - _classic_write = True - _auto_join = False - _properties = False - _type = 'unknown' - _obj = None - _multi = False - _symbol_c = '%s' - _symbol_f = _symbol_set - _symbol_set = (_symbol_c, _symbol_f) - _symbol_get = None - _deprecated = False - - __slots__ = [ - 'copy', # whether value is copied by BaseModel.copy() - 'string', - 'help', - 'required', - 'readonly', - '_domain', - '_context', - 'states', - 'priority', - 'change_default', - 'size', - 'ondelete', - 'translate', - 'select', - 'manual', - 'selectable', - 'group_operator', - 'groups', # CSV list of ext IDs of groups - 'deprecated', # Optional deprecation warning - '_args', - '_prefetch', - '_module', # the column's module name - ] - - def __init__(self, string='unknown', required=False, readonly=False, domain=[], context={}, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args): - """ - - The 'manual' keyword argument specifies if the field is a custom one. - It corresponds to the 'state' column in ir_model_fields. - - """ - # add parameters and default values - args['copy'] = args.get('copy', True) - args['string'] = string - args['help'] = args.get('help', '') - args['required'] = required - args['readonly'] = readonly - args['_domain'] = domain - args['_context'] = context - args['states'] = states - args['priority'] = priority - args['change_default'] = change_default - args['size'] = size - args['ondelete'] = ondelete.lower() if ondelete else None - args['translate'] = translate - args['select'] = select - args['manual'] = manual - args['selectable'] = args.get('selectable', True) - args['group_operator'] = args.get('group_operator', None) - args['groups'] = args.get('groups', None) - args['deprecated'] = args.get('deprecated', None) - args['_prefetch'] = args.get('_prefetch', True) - args['_module'] = args.get('_module', None) - - self._args = EMPTY_DICT - for key, val in args.iteritems(): - setattr(self, key, val) - - # prefetch only if _classic_write, not deprecated and not manual - if not self._classic_write or self.deprecated or self.manual: - self._prefetch = False - - def __getattr__(self, name): - """ Access a non-slot attribute. """ - if name == '_args': - raise AttributeError(name) - try: - return self._args[name] - except KeyError: - raise AttributeError(name) - - def __setattr__(self, name, value): - """ Set a slot or non-slot attribute. """ - try: - object.__setattr__(self, name, value) - except AttributeError: - if self._args: - self._args[name] = value - else: - self._args = {name: value} # replace EMPTY_DICT - - def __delattr__(self, name): - """ Remove a non-slot attribute. """ - try: - del self._args[name] - except KeyError: - raise AttributeError(name) - - def new(self, _computed_field=False, **args): - """ Return a column like `self` with the given parameters; the parameter - `_computed_field` tells whether the corresponding field is computed. - """ - # memory optimization: reuse self whenever possible; you can reduce the - # average memory usage per registry by 10 megabytes! - column = type(self)(**args) - return self if self.to_field_args() == column.to_field_args() else column - - def to_field(self): - """ convert column `self` to a new-style field """ - from openerp.fields import Field - return Field.by_type[self._type](origin=self, **self.to_field_args()) - - def to_field_args(self): - """ return a dictionary with all the arguments to pass to the field """ - base_items = [ - ('_module', self._module), - ('automatic', False), - ('inherited', False), - ('store', True), - ('index', self.select), - ('manual', self.manual), - ('copy', self.copy), - ('compute', None), - ('inverse', None), - ('search', None), - ('related', None), - ('string', self.string), - ('help', self.help), - ('readonly', self.readonly), - ('required', self.required), - ('states', self.states), - ('groups', self.groups), - ('change_default', self.change_default), - ('deprecated', self.deprecated), - ] - truthy_items = filter(itemgetter(1), [ - ('group_operator', self.group_operator), - ('size', self.size), - ('ondelete', self.ondelete), - ('translate', self.translate), - ('domain', self._domain), - ('context', self._context), - ]) - return dict(base_items + truthy_items + self._args.items()) - - def restart(self): - pass - - def set(self, cr, obj, id, name, value, user=None, context=None): - cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id)) - - def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): - raise Exception(_('undefined get method !')) - - def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None): - ids = obj.search(cr, uid, args+self._domain+[(name, 'ilike', value)], offset, limit, context=context) - res = obj.read(cr, uid, ids, [name], context=context) - return [x[name] for x in res] - - def as_display_name(self, cr, uid, obj, value, context=None): - """Converts a field value to a suitable string representation for a record, - e.g. when this field is used as ``rec_name``. - - :param obj: the ``BaseModel`` instance this column belongs to - :param value: a proper value as returned by :py:meth:`~openerp.orm.osv.BaseModel.read` - for this column - """ - # delegated to class method, so a column type A can delegate - # to a column type B. - return self._as_display_name(self, cr, uid, obj, value, context=None) - - @classmethod - def _as_display_name(cls, field, cr, uid, obj, value, context=None): - # This needs to be a class method, in case a column type A as to delegate - # to a column type B. - return tools.ustr(value) - -# --------------------------------------------------------- -# Simple fields -# --------------------------------------------------------- -class boolean(_column): - _type = 'boolean' - _symbol_c = '%s' - _symbol_f = bool - _symbol_set = (_symbol_c, _symbol_f) - __slots__ = [] - - def __init__(self, string='unknown', required=False, **args): - super(boolean, self).__init__(string=string, required=required, **args) - if required: - _logger.debug( - "required=True is deprecated: making a boolean field" - " `required` has no effect, as NULL values are " - "automatically turned into False. args: %r",args) - -class integer(_column): - _type = 'integer' - _symbol_c = '%s' - _symbol_f = lambda x: int(x or 0) - _symbol_set = (_symbol_c, _symbol_f) - _symbol_get = lambda self,x: x or 0 - __slots__ = [] - - def __init__(self, string='unknown', required=False, **args): - super(integer, self).__init__(string=string, required=required, **args) - -class reference(_column): - _type = 'reference' - _classic_read = False # post-process to handle missing target - __slots__ = ['selection'] - - def __init__(self, string, selection, size=None, **args): - if callable(selection): - from openerp import api - selection = api.expected(api.cr_uid_context, selection) - _column.__init__(self, string=string, size=size, selection=selection, **args) - - def to_field_args(self): - args = super(reference, self).to_field_args() - args['selection'] = self.selection - return args - - def get(self, cr, obj, ids, name, uid=None, context=None, values=None): - result = {} - # copy initial values fetched previously. - for value in values: - result[value['id']] = value[name] - if value[name]: - model, res_id = value[name].split(',') - if not obj.pool[model].exists(cr, uid, [int(res_id)], context=context): - result[value['id']] = False - return result - - @classmethod - def _as_display_name(cls, field, cr, uid, obj, value, context=None): - if value: - # reference fields have a 'model,id'-like value, that we need to convert - # to a real name - model_name, res_id = value.split(',') - if model_name in obj.pool and res_id: - model = obj.pool[model_name] - names = model.name_get(cr, uid, [int(res_id)], context=context) - return names[0][1] if names else False - return tools.ustr(value) - -# takes a string (encoded in utf8) and returns a string (encoded in utf8) -def _symbol_set_char(self, symb): - - #TODO: - # * we need to remove the "symb==False" from the next line BUT - # for now too many things rely on this broken behavior - # * the symb==None test should be common to all data types - if symb is None or symb == False: - return None - - # we need to convert the string to a unicode object to be able - # to evaluate its length (and possibly truncate it) reliably - u_symb = tools.ustr(symb) - return u_symb[:self.size].encode('utf8') - -class char(_column): - _type = 'char' - __slots__ = ['_symbol_f', '_symbol_set', '_symbol_set_char'] - - def __init__(self, string="unknown", size=None, **args): - _column.__init__(self, string=string, size=size or None, **args) - # self._symbol_set_char defined to keep the backward compatibility - self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x) - self._symbol_set = (self._symbol_c, self._symbol_f) - -class text(_column): - _type = 'text' - __slots__ = [] - - -class html(text): - _type = 'html' - _symbol_c = '%s' - __slots__ = ['_sanitize', '_sanitize_tags', '_sanitize_attributes', '_sanitize_style', '_strip_style', '_strip_classes', '_symbol_f', '_symbol_set'] - - def _symbol_set_html(self, value): - if value is None or value is False: - return None - if not self._sanitize: - return value - return html_sanitize( - value, silent=True, - sanitize_attributes=self._sanitize_attributes, - sanitize_style=self._sanitize_style, - strip_style=self._strip_style, - strip_classes=self._strip_classes) - - def __init__(self, string='unknown', sanitize=True, sanitize_tags=True, sanitize_attributes=True, sanitize_style=False, strip_style=False, strip_classes=False, **args): - super(html, self).__init__(string=string, **args) - self._sanitize = sanitize - self._sanitize_tags = sanitize_tags - self._sanitize_attributes = sanitize_attributes - self._sanitize_style = sanitize_style - self._strip_style = strip_style - self._strip_classes = strip_classes - # symbol_set redefinition because of sanitize specific behavior - self._symbol_f = self._symbol_set_html - self._symbol_set = (self._symbol_c, self._symbol_f) - - def to_field_args(self): - args = super(html, self).to_field_args() - args['sanitize'] = self._sanitize - args['sanitize_tags'] = self._sanitize_tags - args['sanitize_attributes'] = self._sanitize_attributes - args['sanitize_style'] = self._sanitize_style - args['strip_style'] = self._strip_style - args['strip_classes'] = self._strip_classes - return args - -import __builtin__ - -def _symbol_set_float(self, x): - result = __builtin__.float(x or 0.0) - digits = self.digits - if digits: - precision, scale = digits - result = float_repr(float_round(result, precision_digits=scale), precision_digits=scale) - return result - -class float(_column): - _type = 'float' - _symbol_c = '%s' - _symbol_get = lambda self,x: x or 0.0 - __slots__ = ['_digits', '_digits_compute', '_symbol_f', '_symbol_set'] - - @property - def digits(self): - if self._digits_compute: - with LazyCursor() as cr: - return self._digits_compute(cr) - else: - return self._digits - - def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args): - _column.__init__(self, string=string, required=required, **args) - # synopsis: digits_compute(cr) -> (precision, scale) - self._digits = digits - self._digits_compute = digits_compute - self._symbol_f = lambda x: _symbol_set_float(self, x) - self._symbol_set = (self._symbol_c, self._symbol_f) - - def to_field_args(self): - args = super(float, self).to_field_args() - args['digits'] = self._digits_compute or self._digits - return args - - def digits_change(self, cr): - pass - -def _symbol_set_monetary(val): - try: - return val.float_repr() # see float_precision.float_repr() - except Exception: - return __builtin__.float(val or 0.0) - -class monetary(_column): - _type = 'monetary' - _symbol_set = ('%s', _symbol_set_monetary) - _symbol_get = lambda self,x: x or 0.0 - - def to_field_args(self): - raise NotImplementedError("fields.monetary is only supported in the new API, " - "but you can use widget='monetary' in client-side views") - -class date(_column): - _type = 'date' - __slots__ = [] - - MONTHS = [ - ('01', 'January'), - ('02', 'February'), - ('03', 'March'), - ('04', 'April'), - ('05', 'May'), - ('06', 'June'), - ('07', 'July'), - ('08', 'August'), - ('09', 'September'), - ('10', 'October'), - ('11', 'November'), - ('12', 'December') - ] - - @staticmethod - def today(*args): - """ Returns the current date in a format fit for being a - default value to a ``date`` field. - - This method should be provided as is to the _defaults dict, it - should not be called. - """ - return DT.date.today().strftime( - tools.DEFAULT_SERVER_DATE_FORMAT) - - @staticmethod - def context_today(model, cr, uid, context=None, timestamp=None): - """Returns the current date as seen in the client's timezone - in a format fit for date fields. - This method may be passed as value to initialize _defaults. - - :param Model model: model (osv) for which the date value is being - computed - automatically passed when used in - _defaults. - :param datetime timestamp: optional datetime value to use instead of - the current date and time (must be a - datetime, regular dates can't be converted - between timezones.) - :param dict context: the 'tz' key in the context should give the - name of the User/Client timezone (otherwise - UTC is used) - :rtype: str - """ - today = timestamp or DT.datetime.now() - context_today = None - if context and context.get('tz'): - tz_name = context['tz'] - else: - user = model.pool['res.users'].browse(cr, SUPERUSER_ID, uid) - tz_name = user.tz - if tz_name: - try: - utc = pytz.timezone('UTC') - context_tz = pytz.timezone(tz_name) - utc_today = utc.localize(today, is_dst=False) # UTC = no DST - context_today = utc_today.astimezone(context_tz) - except Exception: - _logger.debug("failed to compute context/client-specific today date, " - "using the UTC value for `today`", - exc_info=True) - return (context_today or today).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - - @staticmethod - def date_to_datetime(model, cr, uid, userdate, context=None): - """ Convert date values expressed in user's timezone to - server-side UTC timestamp, assuming a default arbitrary - time of 12:00 AM - because a time is needed. - - :param str userdate: date string in in user time zone - :return: UTC datetime string for server-side use - """ - user_date = DT.datetime.strptime(userdate, tools.DEFAULT_SERVER_DATE_FORMAT) - if context and context.get('tz'): - tz_name = context['tz'] - else: - tz_name = model.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz'] - if tz_name: - utc = pytz.timezone('UTC') - context_tz = pytz.timezone(tz_name) - user_datetime = user_date + DT.timedelta(hours=12.0) - local_timestamp = context_tz.localize(user_datetime, is_dst=False) - user_datetime = local_timestamp.astimezone(utc) - return user_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) - return user_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) - - -class datetime(_column): - _type = 'datetime' - __slots__ = [] - - MONTHS = [ - ('01', 'January'), - ('02', 'February'), - ('03', 'March'), - ('04', 'April'), - ('05', 'May'), - ('06', 'June'), - ('07', 'July'), - ('08', 'August'), - ('09', 'September'), - ('10', 'October'), - ('11', 'November'), - ('12', 'December') - ] - - @staticmethod - def now(*args): - """ Returns the current datetime in a format fit for being a - default value to a ``datetime`` field. - - This method should be provided as is to the _defaults dict, it - should not be called. - """ - return DT.datetime.now().strftime( - tools.DEFAULT_SERVER_DATETIME_FORMAT) - - @staticmethod - def context_timestamp(cr, uid, timestamp, context=None): - """Returns the given timestamp converted to the client's timezone. - This method is *not* meant for use as a _defaults initializer, - because datetime fields are automatically converted upon - display on client side. For _defaults you :meth:`fields.datetime.now` - should be used instead. - - :param datetime timestamp: naive datetime value (expressed in UTC) - to be converted to the client timezone - :param dict context: the 'tz' key in the context should give the - name of the User/Client timezone (otherwise - UTC is used) - :rtype: datetime - :return: timestamp converted to timezone-aware datetime in context - timezone - """ - assert isinstance(timestamp, DT.datetime), 'Datetime instance expected' - if context and context.get('tz'): - tz_name = context['tz'] - else: - registry = openerp.modules.registry.RegistryManager.get(cr.dbname) - user = registry['res.users'].browse(cr, SUPERUSER_ID, uid) - tz_name = user.tz - utc_timestamp = pytz.utc.localize(timestamp, is_dst=False) # UTC = no DST - if tz_name: - try: - context_tz = pytz.timezone(tz_name) - return utc_timestamp.astimezone(context_tz) - except Exception: - _logger.debug("failed to compute context/client-specific timestamp, " - "using the UTC value", - exc_info=True) - return utc_timestamp - - @classmethod - def _as_display_name(cls, field, cr, uid, obj, value, context=None): - value = datetime.context_timestamp(cr, uid, DT.datetime.strptime(value, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context) - return tools.ustr(value.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)) - -class binary(_column): - _type = 'binary' - _classic_read = False - _classic_write = property(lambda self: not self.attachment) - - # Binary values may be byte strings (python 2.6 byte array), but - # the legacy OpenERP convention is to transfer and store binaries - # as base64-encoded strings. The base64 string may be provided as a - # unicode in some circumstances, hence the str() cast in symbol_f. - # This str coercion will only work for pure ASCII unicode strings, - # on purpose - non base64 data must be passed as a 8bit byte strings. - _symbol_c = '%s' - _symbol_f = lambda symb: symb and Binary(str(symb)) or None - _symbol_set = (_symbol_c, _symbol_f) - _symbol_get = lambda self, x: x and str(x) - - __slots__ = ['attachment', 'filters'] - - def __init__(self, string='unknown', filters=None, **args): - args['_prefetch'] = args.get('_prefetch', False) - args['attachment'] = args.get('attachment', False) - _column.__init__(self, string=string, filters=filters, **args) - - def to_field_args(self): - args = super(binary, self).to_field_args() - args['attachment'] = self.attachment - return args - - def get(self, cr, obj, ids, name, user=None, context=None, values=None): - result = dict.fromkeys(ids, False) - - if self.attachment: - # values are stored in attachments, retrieve them - atts = obj.pool['ir.attachment'].browse(cr, SUPERUSER_ID, [], context) - domain = [ - ('res_model', '=', obj._name), - ('res_field', '=', name), - ('res_id', 'in', ids), - ] - for att in atts.search(domain): - # the 'bin_size' flag is handled by the field 'datas' itself - result[att.res_id] = att.datas - else: - # If client is requesting only the size of the field, we return it - # instead of the content. Presumably a separate request will be done - # to read the actual content if it's needed at some point. - context = context or {} - if context.get('bin_size') or context.get('bin_size_%s' % name): - postprocess = lambda val: tools.human_size(long(val)) - else: - postprocess = lambda val: val - for val in (values or []): - result[val['id']] = postprocess(val[name]) - - return result - - def set(self, cr, obj, id, name, value, user=None, context=None): - assert self.attachment - # retrieve the attachment that stores the value, and adapt it - att = obj.pool['ir.attachment'].browse(cr, SUPERUSER_ID, [], context).search([ - ('res_model', '=', obj._name), - ('res_field', '=', name), - ('res_id', '=', id), - ]) - with att.env.norecompute(): - if value: - if att: - att.write({'datas': value}) - else: - att.create({ - 'name': name, - 'res_model': obj._name, - 'res_field': name, - 'res_id': id, - 'type': 'binary', - 'datas': value, - }) - else: - att.unlink() - return [] - -class selection(_column): - _type = 'selection' - __slots__ = ['selection'] - - def __init__(self, selection, string='unknown', **args): - if callable(selection): - from openerp import api - selection = api.expected(api.cr_uid_context, selection) - _column.__init__(self, string=string, selection=selection, **args) - - def to_field_args(self): - args = super(selection, self).to_field_args() - args['selection'] = self.selection - return args - - @classmethod - def reify(cls, cr, uid, model, field, context=None): - """ Munges the field's ``selection`` attribute as necessary to get - something useable out of it: calls it if it's a function, applies - translations to labels if it's not. - - A callable ``selection`` is considered translated on its own. - - :param orm.Model model: - :param _column field: - """ - if callable(field.selection): - return field.selection(model, cr, uid, context) - - if not (context and 'lang' in context): - return field.selection - - # field_to_dict isn't given a field name, only a field object, we - # need to get the name back in order to perform the translation lookup - field_name = next( - name for name, column in model._columns.iteritems() - if column == field) - - translation_filter = "%s,%s" % (model._name, field_name) - translate = functools.partial( - model.pool['ir.translation']._get_source, - cr, uid, translation_filter, 'selection', context['lang']) - - return [ - (value, translate(label)) - for value, label in field.selection - ] - -# --------------------------------------------------------- -# Relationals fields -# --------------------------------------------------------- - -# -# Values: (0, 0, { fields }) create -# (1, ID, { fields }) update -# (2, ID) remove (delete) -# (3, ID) unlink one (target id or target of relation) -# (4, ID) link -# (5) unlink all (only valid for one2many) -# - -class many2one(_column): - _classic_read = False - _classic_write = True - _type = 'many2one' - _symbol_c = '%s' - _symbol_f = lambda x: x or None - _symbol_set = (_symbol_c, _symbol_f) - - __slots__ = ['_obj', '_auto_join'] - - def __init__(self, obj, string='unknown', auto_join=False, **args): - args['ondelete'] = args.get('ondelete', 'set null') - _column.__init__(self, string=string, **args) - self._obj = obj - self._auto_join = auto_join - - def to_field_args(self): - args = super(many2one, self).to_field_args() - args['comodel_name'] = self._obj - args['auto_join'] = self._auto_join - return args - - def set(self, cr, obj_src, id, field, values, user=None, context=None): - if not context: - context = {} - obj = obj_src.pool[self._obj] - self._table = obj._table - if type(values) == type([]): - for act in values: - if act[0] == 0: - id_new = obj.create(cr, act[2]) - cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (id_new, id)) - elif act[0] == 1: - obj.write(cr, [act[1]], act[2], context=context) - elif act[0] == 2: - cr.execute('delete from '+self._table+' where id=%s', (act[1],)) - elif act[0] == 3 or act[0] == 5: - cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,)) - elif act[0] == 4: - cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (act[1], id)) - else: - if values: - cr.execute('update '+obj_src._table+' set '+field+'=%s where id=%s', (values, id)) - else: - cr.execute('update '+obj_src._table+' set '+field+'=null where id=%s', (id,)) - - def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None): - return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', 'like', value)], offset, limit, context=context) - - @classmethod - def _as_display_name(cls, field, cr, uid, obj, value, context=None): - return value[1] if isinstance(value, tuple) else tools.ustr(value) - - -class one2many(_column): - _classic_read = False - _classic_write = False - _type = 'one2many' - - __slots__ = ['_obj', '_fields_id', '_limit', '_auto_join'] - - def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args): - # one2many columns are not copied by default - args['copy'] = args.get('copy', False) - args['_prefetch'] = args.get('_prefetch', False) - _column.__init__(self, string=string, **args) - self._obj = obj - self._fields_id = fields_id - self._limit = limit - self._auto_join = auto_join - #one2many can't be used as condition for defaults - assert(self.change_default != True) - - def to_field_args(self): - args = super(one2many, self).to_field_args() - args['comodel_name'] = self._obj - args['inverse_name'] = self._fields_id - args['auto_join'] = self._auto_join - args['limit'] = self._limit - return args - - def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None): - if self._context: - context = dict(context or {}) - context.update(self._context) - - # retrieve the records in the comodel - comodel = obj.pool[self._obj].browse(cr, user, [], context) - inverse = self._fields_id - domain = self._domain(obj) if callable(self._domain) else self._domain - domain = domain + [(inverse, 'in', ids)] - records = comodel.search(domain, limit=self._limit) - - result = {id: [] for id in ids} - # read the inverse of records without prefetching other fields on them - for record in records.with_context(prefetch_fields=False): - # record[inverse] may be a record or an integer - result[int(record[inverse])].append(record.id) - - return result - - def set(self, cr, obj, id, field, values, user=None, context=None): - result = [] - context = dict(context or {}) - context.update(self._context) - if not values: - return - obj = obj.pool[self._obj] - rec = obj.browse(cr, user, [], context=context) - with rec.env.norecompute(): - _table = obj._table - for act in values: - if act[0] == 0: - act[2][self._fields_id] = id - id_new = obj.create(cr, user, act[2], context=context) - elif act[0] == 1: - obj.write(cr, user, [act[1]], act[2], context=context) - elif act[0] == 2: - obj.unlink(cr, user, [act[1]], context=context) - elif act[0] == 3: - inverse_field = obj._fields.get(self._fields_id) - assert inverse_field, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o' - # if the model has on delete cascade, just delete the row - if inverse_field.ondelete == "cascade": - obj.unlink(cr, user, [act[1]], context=context) - else: - cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],)) - elif act[0] == 4: - # check whether the given record is already linked - rec = obj.browse(cr, SUPERUSER_ID, act[1], {'prefetch_fields': False}) - if int(rec[self._fields_id]) != id: - # Must use write() to recompute parent_store structure if needed and check access rules - obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {}) - elif act[0] == 5: - inverse_field = obj._fields.get(self._fields_id) - assert inverse_field, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o' - # if the o2m has a static domain we must respect it when unlinking - domain = self._domain(obj) if callable(self._domain) else self._domain - extra_domain = domain or [] - ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context) - # If the model has cascade deletion, we delete the rows because it is the intended behavior, - # otherwise we only nullify the reverse foreign key column. - if inverse_field.ondelete == "cascade": - obj.unlink(cr, user, ids_to_unlink, context=context) - else: - obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context) - elif act[0] == 6: - # Must use write() to recompute parent_store structure if needed - obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {}) - ids2 = act[2] or [0] - cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2)) - ids3 = map(lambda x:x[0], cr.fetchall()) - obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {}) - return result - - def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None): - domain = self._domain(obj) if callable(self._domain) else self._domain - return obj.pool[self._obj].name_search(cr, uid, value, domain, operator, context=context,limit=limit) - - @classmethod - def _as_display_name(cls, field, cr, uid, obj, value, context=None): - raise NotImplementedError('One2Many columns should not be used as record name (_rec_name)') - -# -# Values: (0, 0, { fields }) create -# (1, ID, { fields }) update (write fields to ID) -# (2, ID) remove (calls unlink on ID, that will also delete the relationship because of the ondelete) -# (3, ID) unlink (delete the relationship between the two objects but does not delete ID) -# (4, ID) link (add a relationship) -# (5, ID) unlink all -# (6, ?, ids) set a list of links -# -class many2many(_column): - """Encapsulates the logic of a many-to-many bidirectional relationship, handling the - low-level details of the intermediary relationship table transparently. - A many-to-many relationship is always symmetrical, and can be declared and accessed - from either endpoint model. - If ``rel`` (relationship table name), ``id1`` (source foreign key column name) - or id2 (destination foreign key column name) are not specified, the system will - provide default values. This will by default only allow one single symmetrical - many-to-many relationship between the source and destination model. - For multiple many-to-many relationship between the same models and for - relationships where source and destination models are the same, ``rel``, ``id1`` - and ``id2`` should be specified explicitly. - - :param str obj: destination model - :param str rel: optional name of the intermediary relationship table. If not specified, - a canonical name will be derived based on the alphabetically-ordered - model names of the source and destination (in the form: ``amodel_bmodel_rel``). - Automatic naming is not possible when the source and destination are - the same, for obvious ambiguity reasons. - :param str id1: optional name for the column holding the foreign key to the current - model in the relationship table. If not specified, a canonical name - will be derived based on the model name (in the form: `src_model_id`). - :param str id2: optional name for the column holding the foreign key to the destination - model in the relationship table. If not specified, a canonical name - will be derived based on the model name (in the form: `dest_model_id`) - :param str string: field label - """ - _classic_read = False - _classic_write = False - _type = 'many2many' - - __slots__ = ['_obj', '_rel', '_id1', '_id2', '_limit', '_auto_join'] - - def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args): - """ - """ - args['_prefetch'] = args.get('_prefetch', False) - _column.__init__(self, string=string, **args) - self._obj = obj - if rel and '.' in rel: - raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\ - 'You used %s, which is not a valid SQL table name.')% (string,rel)) - self._rel = rel - self._id1 = id1 - self._id2 = id2 - self._limit = limit - self._auto_join = False - - def to_field_args(self): - args = super(many2many, self).to_field_args() - args['comodel_name'] = self._obj - args['relation'] = self._rel - args['column1'] = self._id1 - args['column2'] = self._id2 - args['auto_join'] = self._auto_join - args['limit'] = self._limit - return args - - def _sql_names(self, source_model): - """Return the SQL names defining the structure of the m2m relationship table - - :return: (m2m_table, local_col, dest_col) where m2m_table is the table name, - local_col is the name of the column holding the current model's FK, and - dest_col is the name of the column holding the destination model's FK, and - """ - tbl, col1, col2 = self._rel, self._id1, self._id2 - if not all((tbl, col1, col2)): - # the default table name is based on the stable alphabetical order of tables - dest_model = source_model.pool[self._obj] - tables = tuple(sorted([source_model._table, dest_model._table])) - if not tbl: - assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\ - 'is not possible when source and destination models are '\ - 'the same' - tbl = '%s_%s_rel' % tables - openerp.models.check_pg_name(tbl) - if not col1: - col1 = '%s_id' % source_model._table - if not col2: - col2 = '%s_id' % dest_model._table - return tbl, col1, col2 - - def _get_query_and_where_params(self, cr, model, ids, values, where_params): - """ Extracted from ``get`` to facilitate fine-tuning of the generated - query. """ - query = """SELECT %(rel)s.%(id2)s, %(rel)s.%(id1)s - FROM %(rel)s, %(from_c)s - WHERE %(where_c)s - AND %(rel)s.%(id1)s IN %%s - AND %(rel)s.%(id2)s = %(tbl)s.id - %(order_by)s - %(limit)s - OFFSET %(offset)d - """ % values - return query, where_params + [tuple(ids)] - - def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None): - if not context: - context = {} - if not values: - values = {} - res = {} - if not ids: - return res - for id in ids: - res[id] = [] - if offset: - _logger.warning( - "Specifying offset at a many2many.get() is deprecated and may" - " produce unpredictable results.") - obj = model.pool[self._obj] - rel, id1, id2 = self._sql_names(model) - - # static domains are lists, and are evaluated both here and on client-side, while string - # domains supposed by dynamic and evaluated on client-side only (thus ignored here) - # FIXME: make this distinction explicit in API! - domain = isinstance(self._domain, list) and self._domain or [] - - wquery = obj._where_calc(cr, user, domain, context=context) - obj._apply_ir_rules(cr, user, wquery, 'read', context=context) - order_by = obj._generate_order_by(cr, user, None, wquery, context=context) - from_c, where_c, where_params = wquery.get_sql() - if not where_c: - where_c = '1=1' - - limit_str = '' - if self._limit is not None: - limit_str = ' LIMIT %d' % self._limit - - query_parts = { - 'rel': rel, - 'from_c': from_c, - 'tbl': obj._table, - 'id1': id1, - 'id2': id2, - 'where_c': where_c, - 'limit': limit_str, - 'order_by': order_by, - 'offset': offset, - } - query, where_params = self._get_query_and_where_params(cr, model, ids, - query_parts, - where_params) - - cr.execute(query, where_params) - for r in cr.fetchall(): - res[r[1]].append(r[0]) - return res - - def set(self, cr, model, id, name, values, user=None, context=None): - if not context: - context = {} - if not values: - return - rel, id1, id2 = self._sql_names(model) - obj = model.pool[self._obj] - - def link(ids): - # beware of duplicates when inserting - query = """ INSERT INTO {rel} ({id1}, {id2}) - (SELECT %s, unnest(%s)) EXCEPT (SELECT {id1}, {id2} FROM {rel} WHERE {id1}=%s) - """.format(rel=rel, id1=id1, id2=id2) - for sub_ids in cr.split_for_in_conditions(ids): - cr.execute(query, (id, list(sub_ids), id)) - - def unlink_all(): - # remove all records for which user has access rights - clauses, params, tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context) - cond = " AND ".join(clauses) if clauses else "1=1" - query = """ DELETE FROM {rel} USING {tables} - WHERE {rel}.{id1}=%s AND {rel}.{id2}={table}.id AND {cond} - """.format(rel=rel, id1=id1, id2=id2, - table=obj._table, tables=','.join(tables), cond=cond) - cr.execute(query, [id] + params) - - for act in values: - if not (isinstance(act, list) or isinstance(act, tuple)) or not act: - continue - if act[0] == 0: - idnew = obj.create(cr, user, act[2], context=context) - cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew)) - elif act[0] == 1: - obj.write(cr, user, [act[1]], act[2], context=context) - elif act[0] == 2: - obj.unlink(cr, user, [act[1]], context=context) - elif act[0] == 3: - cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1])) - elif act[0] == 4: - link([act[1]]) - elif act[0] == 5: - unlink_all() - elif act[0] == 6: - unlink_all() - link(act[2]) - - # - # TODO: use a name_search - # - def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None): - return obj.pool[self._obj].search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context) - - @classmethod - def _as_display_name(cls, field, cr, uid, obj, value, context=None): - raise NotImplementedError('Many2Many columns should not be used as record name (_rec_name)') - - -def get_nice_size(value): - size = 0 - if isinstance(value, (int,long)): - size = value - elif value: # this is supposed to be a string - size = len(value) - if size < 12: # suppose human size - return value - return tools.human_size(size) - -# See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char -# and http://bugs.python.org/issue10066 -invalid_xml_low_bytes = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f]') - -def sanitize_binary_value(value): - # binary fields should be 7-bit ASCII base64-encoded data, - # but we do additional sanity checks to make sure the values - # are not something else that won't pass via XML-RPC - if isinstance(value, (xmlrpclib.Binary, tuple, list, dict)): - # these builtin types are meant to pass untouched - return value - - # Handle invalid bytes values that will cause problems - # for XML-RPC. See for more info: - # - http://bugs.python.org/issue10066 - # - http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char - - # Coercing to unicode would normally allow it to properly pass via - # XML-RPC, transparently encoded as UTF-8 by xmlrpclib. - # (this works for _any_ byte values, thanks to the fallback - # to latin-1 passthrough encoding when decoding to unicode) - value = tools.ustr(value) - - # Due to Python bug #10066 this could still yield invalid XML - # bytes, specifically in the low byte range, that will crash - # the decoding side: [\x00-\x08\x0b-\x0c\x0e-\x1f] - # So check for low bytes values, and if any, perform - # base64 encoding - not very smart or useful, but this is - # our last resort to avoid crashing the request. - if invalid_xml_low_bytes.search(value): - # b64-encode after restoring the pure bytes with latin-1 - # passthrough encoding - value = base64.b64encode(value.encode('latin-1')) - - return value - - -# --------------------------------------------------------- -# Serialized fields -# --------------------------------------------------------- - -class serialized(_column): - """ A field able to store an arbitrary python data structure. - - Note: only plain components allowed. - """ - _type = 'serialized' - __slots__ = [] - - def _symbol_set_struct(val): - return json.dumps(val) - - def _symbol_get_struct(self, val): - return json.loads(val or '{}') - - _symbol_c = '%s' - _symbol_f = _symbol_set_struct - _symbol_set = (_symbol_c, _symbol_f) - _symbol_get = _symbol_get_struct - - def __init__(self, *args, **kwargs): - kwargs['_prefetch'] = kwargs.get('_prefetch', False) - super(serialized, self).__init__(*args, **kwargs) diff --git a/openerp/report/report_sxw.py b/openerp/report/report_sxw.py index 6d2b2434d36..552e7437518 100644 --- a/openerp/report/report_sxw.py +++ b/openerp/report/report_sxw.py @@ -18,8 +18,8 @@ import openerp.tools as tools from . import common from . import preprocess from .interface import report_rml +from openerp import fields from openerp.exceptions import AccessError -from openerp.osv.fields import float as float_field, datetime as datetime_field from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT from openerp.tools.safe_eval import safe_eval from openerp.tools.translate import _ @@ -177,7 +177,7 @@ class rml_parse(object): else: d = res_digits(self.cr)[1] elif (hasattr(obj, '_field') and\ - isinstance(obj._field, float_field) and\ + obj._field.type == 'float' and\ obj._field.digits): d = obj._field.digits[1] if not d and d is not 0: @@ -220,9 +220,8 @@ class rml_parse(object): date = datetime(*value.timetuple()[:6]) if date_time: # Convert datetime values to the expected client/context timezone - date = datetime_field.context_timestamp(self.cr, self.uid, - timestamp=date, - context=self.localcontext) + record = self.env['base'].with_context(self.localcontext) + date = fields.Datetime.context_timestamp(record, date) return date.strftime(date_format.encode('utf-8')) res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary) @@ -332,7 +331,6 @@ class report_sxw(report_rml, preprocess.report): if self.internal_header: context.update(internal_header=self.internal_header) - # skip osv.fields.sanitize_binary_value() because we want the raw bytes in all cases context.update(bin_raw=True) env = openerp.api.Environment(cr, uid, context) env['res.font'].sudo().font_scan(lazy=True) @@ -471,7 +469,6 @@ class report_sxw(report_rml, preprocess.report): # if binary content was passed as unicode, we must # re-encode it as a 8-bit string using the pass-through # 'latin1' encoding, to restore the original byte values. - # See also osv.fields.sanitize_binary_value() binary_report_content = report_xml.report_sxw_content.encode("latin1") sxw_io = StringIO.StringIO(binary_report_content) diff --git a/openerp/tests/addons/test_translation_import/models.py b/openerp/tests/addons/test_translation_import/models.py index 5ea9846b584..bcffd1c2f2c 100644 --- a/openerp/tests/addons/test_translation_import/models.py +++ b/openerp/tests/addons/test_translation_import/models.py @@ -1,17 +1,13 @@ # -*- coding: utf-8 -*- -import openerp -from openerp.tools.translate import _ -class m(openerp.osv.orm.TransientModel): - """ A model to provide source strings. - """ +from openerp import models, fields, _ + +class m(models.TransientModel): + """ A model to provide source strings. """ _name = 'test.translation.import' - _columns = { - 'name': openerp.osv.fields.char( - '1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB', - size=32, help='Efgh'), - } + name = fields.Char('1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB', + size=32, help='Efgh') _('Ijkl') diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index efea03d9cf0..0c2101fd191 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -102,8 +102,8 @@ def _eval_xml(self, node, env): ids = env[f_model].search(q).ids if f_use != 'id': ids = map(lambda x: x[f_use], env[f_model].browse(ids).read([f_use])) - _cols = env[f_model]._columns - if (f_name in _cols) and _cols[f_name]._type=='many2many': + _fields = env[f_model]._fields + if (f_name in _fields) and _fields[f_name].type == 'many2many': return ids f_val = False if len(ids):