diff --git a/pandoc-3.1.11.1-x86_64-macOS.pkg b/pandoc-3.1.11.1-x86_64-macOS.pkg new file mode 100644 index 000000000000..b17e3bfd6245 Binary files /dev/null and b/pandoc-3.1.11.1-x86_64-macOS.pkg differ diff --git a/web_domain_field/README.rst b/web_domain_field/README.rst new file mode 100644 index 000000000000..e218d7de8ccf --- /dev/null +++ b/web_domain_field/README.rst @@ -0,0 +1,162 @@ +================ +Web Domain Field +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:38caddd3efd0bf3c90c5b3a84068ef6e757e1bb72698ee16451a62c210e7a154 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github + :target: https://github.com/OCA/web/tree/17.0/web_domain_field + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-17-0/web-17-0-web_domain_field + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +.. warning:: + + This module is deprecated. If you want to use this functionality you + can assign a unserialised domain to a fields.Binary, + `example `__ + +This technical addon allows developers to specify a field domain in a +view using the value of another field in that view, rather than as a +static XML attribute. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +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:: xml + + + +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. + - The right side of domain in case of x2many can involve huge amount + of ids (performance problem). + - 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 + - etc... + +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:: xml + + + + +The field used as domain must provide the domain as a JSON encoded +string. + +.. code:: python + + product_id_domain = fields.Char( + compute="_compute_product_id_domain", + readonly=True, + store=False, + ) + + @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)] + ) + +.. note:: + + You do not actually need this module to craft a dynamic domain. Odoo + comes with its own + `py.js `__ + web library to parse expressions such as domains. py.js supports more + complex expressions than just static lists. + + Here is an example of a conditional domain based on the value of + another field: + + .. code:: python + + (order_id or partner_id) and [('id', 'in', allowed_picking_ids)] + or [('state', '=', 'done'), ('picking_type_id.code', '=', 'outgoing')] + + For OCA modules, this method is preferred over adding this module as + an additional dependency. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Laurent Mignon +- Denis Roussel +- Raf Ven + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/web `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_domain_field/__init__.py b/web_domain_field/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/web_domain_field/__manifest__.py b/web_domain_field/__manifest__.py new file mode 100644 index 000000000000..c5cc9d50d6fa --- /dev/null +++ b/web_domain_field/__manifest__.py @@ -0,0 +1,23 @@ +# 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": "17.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/web", + "depends": ["web"], + "data": [], + "assets": { + "web.assets_backend": [ + "/web_domain_field/static/lib/js/*.js", + ], + "web.qunit_suite_tests": [ + "/web_domain_field/static/tests/**/*.js", + ], + }, + "installable": True, +} diff --git a/web_domain_field/i18n/it.po b/web_domain_field/i18n/it.po new file mode 100644 index 000000000000..73388557f6d5 --- /dev/null +++ b/web_domain_field/i18n/it.po @@ -0,0 +1,14 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" diff --git a/web_domain_field/i18n/web_domain_field.pot b/web_domain_field/i18n/web_domain_field.pot new file mode 100644 index 000000000000..78d58d53fe07 --- /dev/null +++ b/web_domain_field/i18n/web_domain_field.pot @@ -0,0 +1,13 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/web_domain_field/pyproject.toml b/web_domain_field/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/web_domain_field/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/web_domain_field/readme/CONTRIBUTORS.md b/web_domain_field/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..c40542a6ab35 --- /dev/null +++ b/web_domain_field/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Laurent Mignon \<\> +- Denis Roussel \<\> +- Raf Ven \<\> diff --git a/web_domain_field/readme/DESCRIPTION.md b/web_domain_field/readme/DESCRIPTION.md new file mode 100644 index 000000000000..93db18876744 --- /dev/null +++ b/web_domain_field/readme/DESCRIPTION.md @@ -0,0 +1,8 @@ +> [!WARNING] +> This module is deprecated. If you want to use this functionality you +> can assign a unserialised domain to a fields.Binary, +> [example](https://github.com/OCA/OCB/blob/16.0/addons/account/models/account_tax.py#L1308) + +This technical addon allows developers to specify a field domain in a +view using the value of another field in that view, rather than as a +static XML attribute. diff --git a/web_domain_field/readme/USAGE.md b/web_domain_field/readme/USAGE.md new file mode 100644 index 000000000000..579a95c4655f --- /dev/null +++ b/web_domain_field/readme/USAGE.md @@ -0,0 +1,71 @@ +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. + +``` xml + +``` + +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. +> - The right side of domain in case of x2many can involve huge amount +> of ids (performance problem). +> - 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 +> - etc... + +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. + +``` xml + + +``` + +The field used as domain must provide the domain as a JSON encoded +string. + +``` python +product_id_domain = fields.Char( + compute="_compute_product_id_domain", + readonly=True, + store=False, +) + +@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)] + ) +``` + +> [!NOTE] +> You do not actually need this module to craft a dynamic domain. Odoo +> comes with its own +> [py.js](https://github.com/odoo/odoo/tree/16.0/addons/web/static/lib/py.js) +> web library to parse expressions such as domains. py.js supports more +> complex expressions than just static lists. +> +> Here is an example of a conditional domain based on the value of +> another field: +> +> ``` python +> (order_id or partner_id) and [('id', 'in', allowed_picking_ids)] +> or [('state', '=', 'done'), ('picking_type_id.code', '=', 'outgoing')] +> ``` +> +> For OCA modules, this method is preferred over adding this module as +> an additional dependency. diff --git a/web_domain_field/static/description/icon.png b/web_domain_field/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/web_domain_field/static/description/icon.png differ diff --git a/web_domain_field/static/description/index.html b/web_domain_field/static/description/index.html new file mode 100644 index 000000000000..90447e6b88fb --- /dev/null +++ b/web_domain_field/static/description/index.html @@ -0,0 +1,500 @@ + + + + + + +Web Domain Field + + + +
+

Web Domain Field

+ + +

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runboat

+
+

Warning

+

This module is deprecated. If you want to use this functionality you +can assign a unserialised domain to a fields.Binary, +example

+
+

This technical addon allows developers to specify a field domain in a +view using the value of another field in that view, rather than as a +static XML attribute.

+

Table of contents

+ +
+

Usage

+

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.

+
+<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.
  • +
  • The right side of domain in case of x2many can involve huge amount +of ids (performance problem).
  • +
  • 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
  • +
  • etc…
  • +
+
+

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.

+
+<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.

+
+product_id_domain = fields.Char(
+    compute="_compute_product_id_domain",
+    readonly=True,
+    store=False,
+)
+
+@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)]
+        )
+
+
+

Note

+

You do not actually need this module to craft a dynamic domain. Odoo +comes with its own +py.js +web library to parse expressions such as domains. py.js supports more +complex expressions than just static lists.

+

Here is an example of a conditional domain based on the value of +another field:

+
+(order_id or partner_id) and [('id', 'in', allowed_picking_ids)]
+or [('state', '=', 'done'), ('picking_type_id.code', '=', 'outgoing')]
+
+

For OCA modules, this method is preferred over adding this module as +an additional dependency.

+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/web project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/web_domain_field/static/lib/js/pyeval.js b/web_domain_field/static/lib/js/pyeval.js new file mode 100644 index 000000000000..66566b8f2772 --- /dev/null +++ b/web_domain_field/static/lib/js/pyeval.js @@ -0,0 +1,220 @@ +odoo.define('web.domain_field', function (require) { + "use strict"; + + var py_utils = require('web.py_utils'); + var session = require('web.session'); + + + var original_pyeval = py_utils.eval; + var py = window.py; + + /** Copied from py_utils and not modified but required since not publicly + exposed by web.py_utils**/ + + // 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]; + // Don't add a test case like ``val === undefined`` + // this is intended to prevent letting crap pass + // on the context without even knowing it. + // If you face an issue from here, try to sanitize + // the context upstream instead + 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 0 && + domains[0].length === 1 && + (domains[0][0] === "|" || domains[0][0] === "!") + ); + _(domains).each(function (domain) { + if (_.isString(domain)) { + // Modified part or the original method + if (domain in evaluation_context) { + var fail_parse_domain = false; + try { + var domain_parse = $.parseJSON(evaluation_context[domain]); + console.warn("`web_domain_field is deprecated. If you want to use this functionality you can assign a unserialised domain to a fields.Binary"); + } catch (e) { + fail_parse_domain = true; + } + if (!fail_parse_domain) { + result_domain.push.apply(result_domain, domain_parse); + return; + } + } + // End of modifications + + // wrap raw strings in domain + domain = { __ref: 'domain', __debug: domain }; + } + var domain_array_to_combine; + switch(domain.__ref) { + case 'domain': + evaluation_context.context = evaluation_context; + domain_array_to_combine = py.eval(domain.__debug, wrap_context(evaluation_context)); + break; + default: + domain_array_to_combine = domain; + } + if (need_normalization) { + domain_array_to_combine = get_normalized_domain(domain_array_to_combine); + } + result_domain.push.apply(result_domain, domain_array_to_combine); + }); + 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); + } + } + + function eval_domains_and_contexts(source) { + // see Session.eval_context in Python + return { + context: domain_field_pyeval('contexts', source.contexts || [], source.eval_context), + domain: domain_field_pyeval('domains', source.domains, source.eval_context), + group_by: domain_field_pyeval('groupbys', source.group_by_seq || [], source.eval_context), + }; + } + + + py_utils.eval = domain_field_pyeval; + py_utils.ensure_evaluated = ensure_evaluated; + py_utils.eval_domains_and_contexts = eval_domains_and_contexts; + +}); diff --git a/web_domain_field/static/tests/test_qunit.js b/web_domain_field/static/tests/test_qunit.js new file mode 100644 index 000000000000..27a9fdf147ce --- /dev/null +++ b/web_domain_field/static/tests/test_qunit.js @@ -0,0 +1,129 @@ +odoo.define("web_domain_field.tests", function (require) { + "use strict"; + + const FormView = require("web.FormView"); + const testUtils = require("web.test_utils"); + const {createView} = testUtils; + const {QUnit} = window; + + QUnit.module( + "web_domain_field", + { + beforeEach: function () { + this.data = { + "res.partner": { + fields: { + name: { + string: "Name", + type: "char", + searchable: true, + }, + type: { + string: "Type", + type: "selection", + selection: [ + ["person", "Person"], + ["company", "Company"], + ], + searchable: true, + }, + parent_id: { + string: "Parent", + type: "many2one", + relation: "res.partner", + }, + parent_domain: { + string: "Parent Domain", + type: "char", + }, + }, + records: [ + { + id: 1, + name: "John Doe", + type: "person", + parent_id: 2, + parent_domain: "[]", + }, + { + id: 2, + name: "ACME inc.", + type: "company", + parent_id: false, + parent_domain: `[["type", "=", "company"]]`, + }, + ], + onchanges: {}, + }, + }; + }, + }, + function () { + QUnit.test( + "one2many: field as domain attribute value", + async function (assert) { + assert.expect(2); + + async function testPartnerFormDomain(data, resId, expectedDomain) { + const form = await createView({ + View: FormView, + model: "res.partner", + data: data, + arch: ` +
+ + + + `, + mockRPC: function (route, args) { + if (args.method === "name_search") { + assert.deepEqual(args.kwargs.args, expectedDomain); + } + return this._super.apply(this, arguments); + }, + res_id: resId, + viewOptions: {mode: "edit"}, + }); + form.$el.find(".o_field_widget[name=parent_id] input").click(); + form.destroy(); + } + + await testPartnerFormDomain(this.data, 1, []); + await testPartnerFormDomain(this.data, 2, [ + ["type", "=", "company"], + ]); + } + ); + + QUnit.test( + "one2many: field with default behaviour", + async function (assert) { + assert.expect(1); + const form = await createView({ + View: FormView, + model: "res.partner", + data: this.data, + arch: ` +
+ + + + `, + mockRPC: function (route, args) { + if (args.method === "name_search") { + assert.deepEqual(args.kwargs.args, [ + ["name", "=", "John"], + ]); + } + return this._super.apply(this, arguments); + }, + res_id: 1, + viewOptions: {mode: "edit"}, + }); + form.$el.find(".o_field_widget[name=parent_id] input").click(); + form.destroy(); + } + ); + } + ); +}); diff --git a/web_domain_field/tests/__init__.py b/web_domain_field/tests/__init__.py new file mode 100644 index 000000000000..d20d305b6294 --- /dev/null +++ b/web_domain_field/tests/__init__.py @@ -0,0 +1 @@ +from . import test_qunit diff --git a/web_domain_field/tests/test_qunit.py b/web_domain_field/tests/test_qunit.py new file mode 100644 index 000000000000..7e206ddda2d4 --- /dev/null +++ b/web_domain_field/tests/test_qunit.py @@ -0,0 +1,16 @@ +# Copyright 2022 Camptocamp SA (https://www.camptocamp.com). +# @author Iván Todorovich +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import HttpCase, tagged + + +@tagged("-at_install", "post_install") +class TestQunit(HttpCase): + def test_qunit(self): + self.browser_js( + "/web/tests?module=web_domain_field&failfast", + "", + "", + login="admin", + )