diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index a82511bed55..936af12779a 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -2345,7 +2345,7 @@ class AccountMoveLine(models.Model): continue distribution_by_root_plan = {} for analytic_account_id, percentage in (line.analytic_distribution or {}).items(): - root_plan = self.env['account.analytic.account'].browse(analytic_account_id).root_plan_id + root_plan = self.env['account.analytic.account'].browse(int(analytic_account_id)).root_plan_id distribution_by_root_plan[root_plan.id] = distribution_by_root_plan.get(root_plan.id, 0) + percentage for plan_id in mandatory_plans_ids: diff --git a/odoo/addons/test_new_api/models/test_new_api.py b/odoo/addons/test_new_api/models/test_new_api.py index b984d7dd245..51b3bad97f8 100644 --- a/odoo/addons/test_new_api/models/test_new_api.py +++ b/odoo/addons/test_new_api/models/test_new_api.py @@ -97,6 +97,7 @@ class Discussion(models.Model): important_emails = fields.One2many('test_new_api.emailmessage', 'discussion', domain=[('important', '=', True)]) + history = fields.Json('History', default={'delete_messages': []}) attributes_definition = fields.PropertiesDefinition('Message Properties') # see message@attributes def _domain_very_important(self): diff --git a/odoo/addons/test_new_api/tests/__init__.py b/odoo/addons/test_new_api/tests/__init__.py index db0523d2483..2f560e969a2 100644 --- a/odoo/addons/test_new_api/tests/__init__.py +++ b/odoo/addons/test_new_api/tests/__init__.py @@ -6,6 +6,7 @@ from . import test_attributes from . import test_one2many from . import test_many2many from . import test_properties +from . import test_json_field from . import test_qweb_float from . import test_indexed_translation from . import test_related_translation diff --git a/odoo/addons/test_new_api/tests/test_json_field.py b/odoo/addons/test_new_api/tests/test_json_field.py new file mode 100644 index 00000000000..0085fa64a8d --- /dev/null +++ b/odoo/addons/test_new_api/tests/test_json_field.py @@ -0,0 +1,36 @@ +from odoo.tests.common import TransactionCase + + +class JsonFieldTest(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.discussion_1 = cls.env['test_new_api.discussion'].create({ + 'name': 'Test Discussion JSON field' + }) + + def test_json_field_read_write(self): + random_str = "tVpajMuSvaR94DwSRVtRSLGNkKViNbWHt2hq13" + random_str_2 = "Crypto-money base" + + self.assertEqual(self.discussion_1.history, {'delete_messages': []}) + + # Check that it is not the value of the cache return by convert_to_record + self.assertIsNot(self.discussion_1.history, self.env.cache.get(self.discussion_1, type(self.discussion_1).history)) + + self.assertEqual(self.discussion_1.history, {'delete_messages': []}) + + self.discussion_1.history = {'delete_messages': [random_str]} + self.discussion_1.flush_recordset() + self.assertEqual(self.discussion_1.history, {'delete_messages': [random_str,]}) + + self.discussion_1.history = {'delete_messages': [random_str, random_str_2]} + self.discussion_1.flush_recordset() + + self.assertEqual(self.discussion_1.history, {'delete_messages': [random_str, random_str_2]}) + + self.discussion_1.history = (random_str, random_str_2) + self.discussion_1.flush_recordset() + + self.assertEqual(self.discussion_1.history, [random_str, random_str_2]) diff --git a/odoo/addons/test_new_api/tests/test_properties.py b/odoo/addons/test_new_api/tests/test_properties.py index 913d59a3ae8..e3640c6e0af 100644 --- a/odoo/addons/test_new_api/tests/test_properties.py +++ b/odoo/addons/test_new_api/tests/test_properties.py @@ -177,7 +177,7 @@ class PropertiesCase(TransactionCase): 'SELECT "test_new_api_message"."id" AS "id", "test_new_api_message"."attributes" AS "attributes" FROM "test_new_api_message" WHERE "test_new_api_message".id IN %s', 'SELECT "test_new_api_message"."id" AS "id", "test_new_api_message"."discussion" AS "discussion", "test_new_api_message"."body" AS "body", "test_new_api_message"."author" AS "author", "test_new_api_message"."name" AS "name", "test_new_api_message"."important" AS "important", "test_new_api_message"."label"->>\'en_US\' AS "label", "test_new_api_message"."priority" AS "priority", "test_new_api_message"."create_uid" AS "create_uid", "test_new_api_message"."create_date" AS "create_date", "test_new_api_message"."write_uid" AS "write_uid", "test_new_api_message"."write_date" AS "write_date" FROM "test_new_api_message" WHERE "test_new_api_message".id IN %s', # read the definition on the definition record - 'SELECT "test_new_api_discussion"."id" AS "id", "test_new_api_discussion"."name" AS "name", "test_new_api_discussion"."moderator" AS "moderator", "test_new_api_discussion"."message_concat" AS "message_concat", "test_new_api_discussion"."attributes_definition" AS "attributes_definition", "test_new_api_discussion"."create_uid" AS "create_uid", "test_new_api_discussion"."create_date" AS "create_date", "test_new_api_discussion"."write_uid" AS "write_uid", "test_new_api_discussion"."write_date" AS "write_date" FROM "test_new_api_discussion" WHERE "test_new_api_discussion".id IN %s', + 'SELECT "test_new_api_discussion"."id" AS "id", "test_new_api_discussion"."name" AS "name", "test_new_api_discussion"."moderator" AS "moderator", "test_new_api_discussion"."message_concat" AS "message_concat", "test_new_api_discussion"."history" AS "history", "test_new_api_discussion"."attributes_definition" AS "attributes_definition", "test_new_api_discussion"."create_uid" AS "create_uid", "test_new_api_discussion"."create_date" AS "create_date", "test_new_api_discussion"."write_uid" AS "write_uid", "test_new_api_discussion"."write_date" AS "write_date" FROM "test_new_api_discussion" WHERE "test_new_api_discussion".id IN %s', # check the many2one existence 'SELECT "test_new_api_partner".id FROM "test_new_api_partner" WHERE "test_new_api_partner".id IN %s', 'SELECT "test_new_api_partner"."id" AS "id", "test_new_api_partner"."name" AS "name", "test_new_api_partner"."create_uid" AS "create_uid", "test_new_api_partner"."create_date" AS "create_date", "test_new_api_partner"."write_uid" AS "write_uid", "test_new_api_partner"."write_date" AS "write_date" FROM "test_new_api_partner" WHERE "test_new_api_partner".id IN %s', diff --git a/odoo/fields.py b/odoo/fields.py index 00b6dd99dba..c17768bf702 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -21,7 +21,7 @@ import warnings from markupsafe import Markup import psycopg2 -from psycopg2.extras import Json +from psycopg2.extras import Json as PsycopgJson import pytz from difflib import get_close_matches from hashlib import sha256 @@ -1655,7 +1655,7 @@ class _String(Field): """ Convert from cache_raw value to column value """ if value is None: return None - return Json(value) if self.translate else value + return PsycopgJson(value) if self.translate else value def convert_to_cache(self, value, record, validate=True): if value is None or value is False: @@ -3143,6 +3143,33 @@ class Many2oneReference(Integer): return model_ids +class Json(Field): + """ JSON Field that contain unstructured information in jsonb PostgreSQL column. + This field is still in beta + Some features have not been implemented and won't be implemented in stable versions, including: + * searching + * indexing + * mutating the values. + """ + + type = 'json' + column_type = ('jsonb', 'jsonb') + + def convert_to_record(self, value, record): + """ Return a copy of the value """ + return False if value is None else copy.deepcopy(value) + + def convert_to_cache(self, value, record, validate=True): + if not value: + return None + return json.loads(json.dumps(value)) + + def convert_to_column(self, value, record, values=None, validate=True): + if not value: + return None + return PsycopgJson(value) + + class Properties(Field): """ Field that contains a list of properties (aka "sub-field") based on a definition defined on a container. Properties are pseudo-fields, acting