diff --git a/odoo/addons/base/controllers/rpc.py b/odoo/addons/base/controllers/rpc.py index 405fd26c44d..930c3170e52 100644 --- a/odoo/addons/base/controllers/rpc.py +++ b/odoo/addons/base/controllers/rpc.py @@ -7,6 +7,7 @@ from werkzeug.wrappers import Response from odoo.http import Controller, dispatch_rpc, request, route from odoo.service import wsgi_server from odoo.fields import Date, Datetime +from odoo.tools import lazy class OdooMarshaller(xmlrpc.client.Marshaller): @@ -28,6 +29,11 @@ class OdooMarshaller(xmlrpc.client.Marshaller): self.dump_unicode(value, write) dispatch[date] = dump_date + def dump_lazy(self, value, write): + v = value._value + return self.dispatch[type(v)](self, v, write) + dispatch[lazy] = dump_lazy + # monkey-patch xmlrpc.client's marshaller xmlrpc.client.Marshaller = OdooMarshaller diff --git a/odoo/addons/base/tests/test_xmlrpc.py b/odoo/addons/base/tests/test_xmlrpc.py index 09ec01adfbb..aa4d9028d10 100644 --- a/odoo/addons/base/tests/test_xmlrpc.py +++ b/odoo/addons/base/tests/test_xmlrpc.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import odoo.tools from odoo.tests import common @@ -25,3 +26,41 @@ class TestXMLRPC(common.HttpCase): self.assertIsInstance(ids, list) ids = o.execute(db_name, self.admin_uid, 'admin', 'ir.model', 'search', [], {}) self.assertIsInstance(ids, list) + + def test_xmlrpc_read_group(self): + groups = self.xmlrpc_object.execute( + common.get_db_name(), self.admin_uid, 'admin', + 'res.partner', 'read_group', [], ['is_company', 'color'], ['parent_id'] + ) + + def test_xmlrpc_name_search(self): + self.xmlrpc_object.execute( + common.get_db_name(), self.admin_uid, 'admin', + 'res.partner', 'name_search', "admin" + ) + + def test_jsonrpc_read_group(self): + self._json_call( + common.get_db_name(), self.admin_uid, 'admin', + 'res.partner', 'read_group', [], ['is_company', 'color'], ['parent_id'] + ) + + def test_jsonrpc_name_search(self): + # well that's some sexy sexy call right there + self._json_call( + common.get_db_name(), + self.admin_uid, 'admin', + 'res.partner', 'name_search', 'admin' + ) + + def _json_call(self, *args): + self.opener.post("http://%s:%s/jsonrpc" % (common.HOST, odoo.tools.config['http_port']), json={ + 'jsonrpc': '2.0', + 'id': None, + 'method': 'call', + 'params': { + 'service': 'object', + 'method': 'execute', + 'args': args + } + }) diff --git a/odoo/addons/test_performance/tests/test_performance.py b/odoo/addons/test_performance/tests/test_performance.py index 014cfa60aca..d77ef7c1a21 100644 --- a/odoo/addons/test_performance/tests/test_performance.py +++ b/odoo/addons/test_performance/tests/test_performance.py @@ -6,7 +6,7 @@ import json from odoo.addons.base.tests.common import SavepointCaseWithUserDemo from odoo.tests.common import TransactionCase, users, warmup, tagged -from odoo.tools import mute_logger +from odoo.tools import mute_logger, json_default class TestPerformance(SavepointCaseWithUserDemo): @@ -447,7 +447,7 @@ class TestPerformance(SavepointCaseWithUserDemo): self.assertEqual(res['value'], exp['value']) # now serialize to json, which should force evaluation with self.assertQueryCount(__system__=1, demo=1): - json.dumps(result) + json.dumps(result, default=json_default) @tagged('bacon_and_eggs') diff --git a/odoo/service/model.py b/odoo/service/model.py index 731007abd23..6cc05476195 100644 --- a/odoo/service/model.py +++ b/odoo/service/model.py @@ -15,6 +15,7 @@ from odoo.tools.translate import translate, translate_sql_constraint from odoo.tools.translate import _ from . import security +from ..tools import traverse_containers, lazy _logger = logging.getLogger(__name__) @@ -155,7 +156,12 @@ def execute_cr(cr, uid, obj, method, *args, **kw): recs = odoo.api.Environment(cr, uid, {}).get(obj) if recs is None: raise UserError(_("Object %s doesn't exist") % obj) - return odoo.api.call_kw(recs, method, args, kw) + result = odoo.api.call_kw(recs, method, args, kw) + # force evaluation of lazy values before the cursor is closed, as it would + # error afterwards if the lazy isn't already evaluated (and cached) + for l in traverse_containers(result, lazy): + _0 = l._value + return result def execute_kw(db, uid, obj, method, args, kw=None): diff --git a/odoo/tools/date_utils.py b/odoo/tools/date_utils.py index c26817b3850..9d934448e30 100644 --- a/odoo/tools/date_utils.py +++ b/odoo/tools/date_utils.py @@ -4,8 +4,9 @@ import calendar from datetime import date, datetime, time import pytz from dateutil.relativedelta import relativedelta -from . import ustr +from . import ustr +from .func import lazy def get_month(date): ''' Compute the month dates range on which the 'date' parameter belongs to. @@ -198,16 +199,17 @@ def subtract(value, *args, **kwargs): """ return value - relativedelta(*args, **kwargs) - def json_default(obj): """ Properly serializes date and datetime objects. """ from odoo import fields + if isinstance(obj, datetime): + return fields.Datetime.to_string(obj) if isinstance(obj, date): - if isinstance(obj, datetime): - return fields.Datetime.to_string(obj) return fields.Date.to_string(obj) + if isinstance(obj, lazy): + return obj._value return ustr(obj) diff --git a/odoo/tools/func.py b/odoo/tools/func.py index 424b6879aba..deae2bdec04 100644 --- a/odoo/tools/func.py +++ b/odoo/tools/func.py @@ -242,14 +242,3 @@ class lazy(object): def __aenter__(self): return self._value.__aenter__() def __aexit__(self, exc_type, exc_value, traceback): return self._value.__aexit__(exc_type, exc_value, traceback) - - -# patch serialization of lazy -def default(self, o): - if isinstance(o, lazy): - return o._value - return json_encoder_default(self, o) - - -json_encoder_default = JSONEncoder.default -JSONEncoder.default = default diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index ad599a5d8e5..2e58ad9e012 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -1507,3 +1507,21 @@ def get_diff(data_from, data_to, custom_style=False): numlines=3, ) return handle_style(diff, custom_style) + + +def traverse_containers(val, type_): + """ Yields atoms filtered by specified type_ (or type tuple), traverses + through standard containers (non-string mappings or sequences) *unless* + they're selected by the type filter + """ + if isinstance(val, type_): + yield val + elif isinstance(val, (str, bytes)): + return + elif isinstance(val, Mapping): + for k, v in val.items(): + yield from traverse_containers(k, type_) + yield from traverse_containers(v, type_) + elif isinstance(val, collections.abc.Sequence): + for v in val: + yield from traverse_containers(v, type_)