diff --git a/addons/base_sparse_field/models/fields.py b/addons/base_sparse_field/models/fields.py index cb1e1981193..e2b8adc0e29 100644 --- a/addons/base_sparse_field/models/fields.py +++ b/addons/base_sparse_field/models/fields.py @@ -81,7 +81,7 @@ class Serialized(fields.Field): } column_type = ('text', 'text') - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): return json.dumps(value) def convert_to_cache(self, value, record, validate=True): diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 9edf4a16114..948470d60d6 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -5,7 +5,7 @@ from datetime import date, datetime from odoo.exceptions import AccessError, except_orm from odoo.tests import common -from odoo.tools import mute_logger +from odoo.tools import mute_logger, float_repr class TestFields(common.TransactionCase): @@ -305,21 +305,65 @@ class TestFields(common.TransactionCase): record.number = 2.4999999999999996 self.assertEqual(record.number, 2.50) + def check_monetary(self, record, amount, currency, msg=None): + # determine the possible roundings of amount + if currency: + ramount = currency.round(amount) + samount = float(float_repr(ramount, currency.decimal_places)) + else: + ramount = samount = amount + + # check the currency on record + self.assertEqual(record.currency_id, currency) + + # check the value on the record + self.assertIn(record.amount, [ramount, samount], msg) + + # check the value in the database + self.cr.execute('SELECT amount FROM test_new_api_mixed WHERE id=%s', [record.id]) + value = self.cr.fetchone()[0] + self.assertEqual(value, samount, msg) + def test_20_monetary(self): """ test monetary fields """ - record = self.env['test_new_api.mixed'].create({}) - self.assertTrue(record.currency_id) - self.assertEqual(record.currency_id.rounding, 0.01) + model = self.env['test_new_api.mixed'] + currency = self.env['res.currency'].with_context(active_test=False) + amount = 14.70126 - # the conversion to cache should round the value to 14.700000000000001 - record.amount = 14.7 - self.assertNotEqual(record.amount, 14.7) - self.assertEqual(record.amount, 14.700000000000001) + for rounding in [0.01, 0.0001, 1.0, 0]: + # first retrieve a currency corresponding to rounding + if rounding: + currency = currency.search([('rounding', '=', rounding)], limit=1) + self.assertTrue(currency, "No currency found for rounding %s" % rounding) + else: + # rounding=0 corresponds to currency=False + currency = currency.browse() - # however when stored to database, it should be serialized as 14.70 - self.cr.execute('SELECT amount FROM test_new_api_mixed WHERE id=%s', (record.id,)) - (amount,) = self.cr.fetchone() - self.assertEqual(amount, 14.7) + # case 1: create with amount and currency + record = model.create({'amount': amount, 'currency_id': currency.id}) + self.check_monetary(record, amount, currency, 'create(amount, currency)') + + # case 2: assign amount + record.amount = 0 + record.amount = amount + self.check_monetary(record, amount, currency, 'assign(amount)') + + # case 3: write with amount and currency + record.write({'amount': 0, 'currency_id': False}) + record.write({'amount': amount, 'currency_id': currency.id}) + self.check_monetary(record, amount, currency, 'write(amount, currency)') + + # case 4: write with amount only + record.write({'amount': 0}) + record.write({'amount': amount}) + self.check_monetary(record, amount, currency, 'write(amount)') + + # case 5: write with amount on several records + records = record + model.create({'currency_id': currency.id}) + records.write({'amount': 0}) + records.write({'amount': amount}) + for record in records: + self.check_monetary(record, amount, currency, 'multi write(amount)') def test_21_date(self): """ test date fields """ diff --git a/odoo/fields.py b/odoo/fields.py index a4199916b8c..fec4666e925 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -18,7 +18,7 @@ except ImportError: import psycopg2 from odoo.sql_db import LazyCursor -from odoo.tools import float_precision, float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, ustr, OrderedSet, pycompat +from odoo.tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, ustr, OrderedSet, pycompat from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT from odoo.tools.translate import html_translate, _ @@ -737,7 +737,7 @@ class Field(MetaField('DummyField', (object,), {})): """ Return the null value for this field in the record format. """ return False - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): """ Convert ``value`` from the ``write`` format to the SQL format. """ if value is None or value is False: return None @@ -1140,7 +1140,7 @@ class Boolean(Field): type = 'boolean' column_type = ('bool', 'bool') - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): return bool(value) def convert_to_cache(self, value, record, validate=True): @@ -1161,7 +1161,7 @@ class Integer(Field): _description_group_operator = property(attrgetter('group_operator')) - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): return int(value or 0) def convert_to_cache(self, value, record, validate=True): @@ -1225,7 +1225,7 @@ class Float(Field): _description_digits = property(attrgetter('digits')) _description_group_operator = property(attrgetter('group_operator')) - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): result = float(value or 0.0) digits = self.digits if digits: @@ -1279,25 +1279,33 @@ class Monetary(Field): assert self.currency_field in model._fields, \ "Field %s with unknown currency_field %r" % (self, self.currency_field) - def convert_to_column(self, value, record): - try: - return value.float_repr() # see float_precision.float_repr() - except Exception: - return float(value or 0.0) + def convert_to_column(self, value, record, values=None): + # retrieve currency from values or record + if values and self.currency_field in values: + field = record._fields[self.currency_field] + currency = field.convert_to_cache(values[self.currency_field], record) + currency = field.convert_to_record(currency, record) + else: + # Note: this is wrong if 'record' is several records with different + # currencies, which is functional nonsense and should not happen + currency = record[:1][self.currency_field] + + value = float(value or 0.0) + if currency: + return float_repr(currency.round(value), currency.decimal_places) + return value def convert_to_cache(self, value, record, validate=True): - if validate: - currency = record[self.currency_field] + # cache format: float + value = float(value or 0.0) + if validate and record[self.currency_field]: # FIXME @rco-odoo: currency may not be already initialized if it is # a function or related field! - if currency: - value = currency.round(float(value or 0.0)) - return float_precision(value, currency.decimal_places) - return float(value or 0.0) + value = record[self.currency_field].round(value) + return value def convert_to_read(self, value, record, use_name_get=True): - # float_precision values are not supported in pure XMLRPC - return float(value) + return value def convert_to_write(self, value, record): return value @@ -1398,7 +1406,7 @@ class Char(_String): assert self.size is None or isinstance(self.size, int), \ "Char field %s with non-integer size %r" % (self, self.size) - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): if value is None or value is False: return None # we need to convert the string to a unicode object to be able @@ -1463,7 +1471,7 @@ class Html(_String): _description_strip_style = property(attrgetter('strip_style')) _description_strip_classes = property(attrgetter('strip_classes')) - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): if value is None or value is False: return None if self.sanitize: @@ -1644,7 +1652,7 @@ class Binary(Field): _description_attachment = property(attrgetter('attachment')) - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): # 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 @@ -1804,7 +1812,7 @@ class Selection(Field): return item[1] return False - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): """ Convert ``value`` from the ``write`` format to the SQL format. """ if value is None or value is False: return None @@ -1971,7 +1979,7 @@ class Many2one(_Relational): """ records._cache[self] = self.convert_to_cache(value, records, validate=False) - def convert_to_column(self, value, record): + def convert_to_column(self, value, record, values=None): return value or None def convert_to_cache(self, value, record, validate=True): diff --git a/odoo/models.py b/odoo/models.py index 3a633d9826d..7e2f65c9312 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -3066,7 +3066,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): if field.column_type: if single_lang or not (has_trans and field.translate is True): # val is not a translation: update the table - val = field.convert_to_column(val, self) + val = field.convert_to_column(val, self, vals) updates.append((name, field.column_format, val)) direct.append(name) else: @@ -3106,7 +3106,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # Insert value to DB src_trans = vals[name] self.with_context(lang=None).write({name: src_trans}) - val = field.convert_to_column(vals[name], self) + val = field.convert_to_column(vals[name], self, vals) tname = "%s,%s" % (self._name, name) self.env['ir.translation']._set_ids( tname, 'model', self.env.lang, self.ids, val, src_trans) @@ -3330,7 +3330,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): parent_id = self.env[parent_model].create(parent_vals).id else: self.env[parent_model].browse(parent_id).write(parent_vals) - updates.append((self._inherits[parent_model], '%s', parent_id)) + vals[self._inherits[parent_model]] = parent_id # set boolean fields to False by default (to make search more powerful) for name, field in pycompat.items(self._fields): @@ -3338,10 +3338,12 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): vals[name] = False # determine SQL values + self = self.browse() for name, val in pycompat.items(vals): field = self._fields[name] if field.store and field.column_type: - updates.append((name, field.column_format, field.convert_to_column(val, self))) + column_val = field.convert_to_column(val, self, vals) + updates.append((name, field.column_format, column_val)) else: upd_todo.append(name) diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py index fa150a1d902..9c37da07fe9 100644 --- a/odoo/tools/float_utils.py +++ b/odoo/tools/float_utils.py @@ -180,24 +180,6 @@ def float_split(value, precision_digits): return int(units), int(cents) -class float_precision(float): - """ A class for float values that carry precision digits. This is a thin - layer on top of ``float``, and the precision digits are not propagated - to the result of arithmetic operations. This class is used when - converting monetary values to cache, and for serializing them to the - database. - """ - __slots__ = ['precision_digits'] - - def __new__(cls, value, precision_digits): - obj = super(float_precision, cls).__new__(cls, value) - obj.precision_digits = precision_digits - return obj - - def float_repr(self): - return _float_repr(self, self.precision_digits) - - if __name__ == "__main__": import time