diff --git a/web_domain_field/README.rst b/web_domain_field/README.rst new file mode 100644 index 000000000..e70f68f28 --- /dev/null +++ b/web_domain_field/README.rst @@ -0,0 +1,104 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +================ +Web Domain Field +================ + +When you define a view you can specify on the relational fields a domain +attribute. This attribute is evaluated as filter to apply when displaying +existing records for selection. + +.. code-block:: xml + + <field name="product_id" domain="[('type','=','product')]"/> + +The value provided for the domain attribute must be a string representing a +valid Odoo domain. This string is evaluated on the client side in a +restricted context where we can reference as right operand the values of +fields present into the form and a limited set of functions. + +In this context it's hard to build complex domain and we are facing to some +limitations as: + + * The syntax to include in your domain a criteria involving values from a + x2many field is complex. + * Domains computed by an onchange on an other field are not recomputed when + you modify the form and don't modify the field triggering the onchange. + * It's not possible to extend an existing domain. You must completely redefine + the domain in your specialized addon + * ... + +In order to mitigate these limitations this new addon allows you to use the +value of a field as domain of an other field in the xml definition of your +view. + +.. code-block:: xml + + <field name="product_id_domain" invisible="1"/> + <field name="product_id" domain="product_id_domain"/> + +The field used as domain must provide the domain as a JSON encoded string. + +.. code-block:: python + + product_id_domain = fields.Char( + compute="_compute_product_id_domain", + readonly=True, + store=False, + ) + + @api.multi + @api.depends('name') + def _compute_product_id_domain(self): + for rec in self: + rec.product_id_domain = json.dumps( + [('type', '=', 'product'), ('name', 'like', rec.name)] + ) + + +Usage +===== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/162/9.0 + + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +<https://github.com/OCA/web/issues>`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_. + +Contributors +------------ + +* Laurent Mignon <laurent.mignon@acsone.eu> + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/web_domain_field/__init__.py b/web_domain_field/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web_domain_field/__openerp__.py b/web_domain_field/__openerp__.py new file mode 100644 index 000000000..4ff66a1c7 --- /dev/null +++ b/web_domain_field/__openerp__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Web Domain Field', + 'summary': """ + Use computed field as domain""", + 'version': '9.0.1.0.0', + 'license': 'AGPL-3', + 'author': 'ACSONE SA/NV,Odoo Community Association (OCA)', + 'website': 'https://acsone.eu/', + 'depends': [ + 'web' + ], + 'data': [ + 'views/web_domain_field.xml', + ], + 'demo': [ + ], +} diff --git a/web_domain_field/static/description/icon.png b/web_domain_field/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/web_domain_field/static/description/icon.png differ diff --git a/web_domain_field/static/src/js/pyeval.js b/web_domain_field/static/src/js/pyeval.js new file mode 100644 index 000000000..f4b75e579 --- /dev/null +++ b/web_domain_field/static/src/js/pyeval.js @@ -0,0 +1,233 @@ +odoo.define('web.domain_field', function (require) { +"use strict"; + +var pyeval = require('web.pyeval'); +var session = require('web.session'); + + +var original_pyeval = pyeval.eval; +var original_ensure_evaluated = pyeval.ensure_evaluated; +var py = window.py; + +/** copied from pyeval and not modified but required since not publicly +exposed by web.pyeval**/ + +// recursively wraps JS objects passed into the context to attributedicts +// which jsonify back to JS objects +function wrap(value) { + if (value === null) { return py.None; } + + switch (typeof value) { + case 'undefined': throw new Error("No conversion for undefined"); + case 'boolean': return py.bool.fromJSON(value); + case 'number': return py.float.fromJSON(value); + case 'string': return py.str.fromJSON(value); + } + + switch(value.constructor) { + case Object: return wrapping_dict.fromJSON(value); + case Array: return wrapping_list.fromJSON(value); + } + + throw new Error("ValueError: unable to wrap " + value); +} + +var wrapping_dict = py.type('wrapping_dict', null, { + __init__: function () { + this._store = {}; + }, + __getitem__: function (key) { + var k = key.toJSON(); + if (!(k in this._store)) { + throw new Error("KeyError: '" + k + "'"); + } + return wrap(this._store[k]); + }, + __getattr__: function (key) { + return this.__getitem__(py.str.fromJSON(key)); + }, + __len__: function () { + return Object.keys(this._store).length + }, + __nonzero__: function () { + return py.PY_size(this) > 0 ? py.True : py.False; + }, + get: function () { + var args = py.PY_parseArgs(arguments, ['k', ['d', py.None]]); + + if (!(args.k.toJSON() in this._store)) { return args.d; } + return this.__getitem__(args.k); + }, + fromJSON: function (d) { + var instance = py.PY_call(wrapping_dict); + instance._store = d; + return instance; + }, + toJSON: function () { + return this._store; + }, +}); + +var wrapping_list = py.type('wrapping_list', null, { + __init__: function () { + this._store = []; + }, + __getitem__: function (index) { + return wrap(this._store[index.toJSON()]); + }, + __len__: function () { + return this._store.length; + }, + __nonzero__: function () { + return py.PY_size(this) > 0 ? py.True : py.False; + }, + fromJSON: function (ar) { + var instance = py.PY_call(wrapping_list); + instance._store = ar; + return instance; + }, + toJSON: function () { + return this._store; + }, +}); + +function wrap_context (context) { + for (var k in context) { + if (!context.hasOwnProperty(k)) { continue; } + var val = context[k]; + + if (val === null) { continue; } + if (val.constructor === Array) { + context[k] = wrapping_list.fromJSON(val); + } else if (val.constructor === Object + && !py.PY_isInstance(val, py.object)) { + context[k] = wrapping_dict.fromJSON(val); + } + } + return context; +} + +function ensure_evaluated (args, kwargs) { + for (var i=0; i<args.length; ++i) { + args[i] = eval_arg(args[i]); + } + for (var k in kwargs) { + if (!kwargs.hasOwnProperty(k)) { continue; } + kwargs[k] = eval_arg(kwargs[k]); + } +} +function eval_contexts (contexts, evaluation_context) { + evaluation_context = _.extend(pyeval.context(), evaluation_context || {}); + return _(contexts).reduce(function (result_context, ctx) { + // __eval_context evaluations can lead to some of `contexts`'s + // values being null, skip them as well as empty contexts + if (_.isEmpty(ctx)) { return result_context; } + if (_.isString(ctx)) { + // wrap raw strings in context + ctx = { __ref: 'context', __debug: ctx }; + } + var evaluated = ctx; + switch(ctx.__ref) { + case 'context': + evaluation_context.context = evaluation_context; + evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context)); + break; + case 'compound_context': + var eval_context = eval_contexts([ctx.__eval_context]); + evaluated = eval_contexts( + ctx.__contexts, _.extend({}, evaluation_context, eval_context)); + break; + } + // add newly evaluated context to evaluation context for following + // siblings + _.extend(evaluation_context, evaluated); + return _.extend(result_context, evaluated); + }, {}); +} + +/** end of unmodified methods copied from pyeval **/ + +// We need to override the original method to be able to call our +//specialized version of pyeval for domain fields +function eval_arg (arg) { + if (typeof arg !== 'object' || !arg.__ref) { return arg; } + switch(arg.__ref) { + case 'domain': case 'compound_domain': + return domain_field_pyeval('domains', [arg]); + case 'context': case 'compound_context': + return original_pyeval('contexts', [arg]); + default: + throw new Error(_t("Unknown nonliteral type ") + ' ' + arg.__ref); + } +} + +// override eval_domains to add 3 lines in order to be able to use a field +//value as domain +function eval_domains (domains, evaluation_context) { + evaluation_context = _.extend(pyeval.context(), evaluation_context || + {}); + var result_domain = []; + _(domains).each(function (domain) { + if (_.isString(domain)) { + // Modified part or the original method + if(domain in evaluation_context) { + result_domain.push.apply( + result_domain, $.parseJSON(evaluation_context[domain])); + return; + } + // end of modifications + + // wrap raw strings in domain + domain = { __ref: 'domain', __debug: domain }; + } + switch(domain.__ref) { + case 'domain': + evaluation_context.context = evaluation_context; + result_domain.push.apply( + result_domain, py.eval(domain.__debug, wrap_context(evaluation_context))); + break; + case 'compound_domain': + var eval_context = eval_contexts([domain.__eval_context]); + result_domain.push.apply( + result_domain, eval_domains( + domain.__domains, _.extend( + {}, evaluation_context, eval_context))); + break; + default: + result_domain.push.apply(result_domain, domain); + } + }); + return result_domain; +} + +// override pyeval in order to call our specialized implementation of +// eval_domains +function domain_field_pyeval (type, object, context, options) { + switch(type) { + case 'domain': + case 'domains': + if (type === 'domain') + object = [object]; + return eval_domains(object, context); + default: + return original_pyeval(type, object, context, options); + } +} + +// override sync_eval in order to call our specialized implementation of +// eval_domains +function sync_eval_domains_and_contexts (source) { + var contexts = ([session.user_context] || []).concat(source.contexts); + // see Session.eval_context in Python + return { + context: domain_field_pyeval('contexts', contexts), + domain: domain_field_pyeval('domains', source.domains), + group_by: domain_field_pyeval('groupbys', source.group_by_seq || []) + }; +} + +pyeval.eval = domain_field_pyeval; +pyeval.ensure_evaluated = ensure_evaluated; +pyeval.sync_eval_domains_and_contexts = sync_eval_domains_and_contexts; + +}); \ No newline at end of file diff --git a/web_domain_field/views/web_domain_field.xml b/web_domain_field/views/web_domain_field.xml new file mode 100644 index 000000000..537d2f874 --- /dev/null +++ b/web_domain_field/views/web_domain_field.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="assets_backend" name="web_domain_field assets" inherit_id="web.assets_backend"> + <xpath expr="script[last()]" position="after"> + <script type="text/javascript" src="/web_domain_field/static/src/js/pyeval.js" /> + </xpath> + </template> +</odoo> \ No newline at end of file