From 4e14d34a13fcefa900e8c6c62bb3b7002129b14c Mon Sep 17 00:00:00 2001 From: Erwan Date: Wed, 21 Jan 2026 12:45:40 +0100 Subject: [PATCH 1/4] [IMP] start module migration --- jsonifier/README.rst | 284 +++++++++ jsonifier/__init__.py | 1 + jsonifier/__manifest__.py | 26 + jsonifier/demo/export_demo.xml | 7 + jsonifier/demo/ir.exports.line.csv | 16 + jsonifier/demo/resolver_demo.xml | 12 + jsonifier/exceptions.py | 7 + jsonifier/i18n/ca.po | 234 +++++++ jsonifier/i18n/es.po | 266 ++++++++ jsonifier/i18n/it.po | 277 +++++++++ jsonifier/i18n/jsonifier.pot | 233 +++++++ jsonifier/i18n/zh_CN.po | 260 ++++++++ jsonifier/models/__init__.py | 5 + jsonifier/models/ir_exports.py | 124 ++++ jsonifier/models/ir_exports_line.py | 58 ++ jsonifier/models/ir_exports_resolver.py | 58 ++ jsonifier/models/models.py | 269 +++++++++ jsonifier/models/utils.py | 35 ++ jsonifier/pyproject.toml | 3 + jsonifier/readme/CONTRIBUTORS.md | 8 + jsonifier/readme/CREDITS.md | 1 + jsonifier/readme/DESCRIPTION.md | 163 +++++ jsonifier/readme/USAGE.md | 26 + jsonifier/security/ir.model.access.csv | 2 + jsonifier/static/description/icon.png | Bin 0 -> 9455 bytes jsonifier/static/description/index.html | 604 +++++++++++++++++++ jsonifier/tests/__init__.py | 3 + jsonifier/tests/test_get_parser.py | 448 ++++++++++++++ jsonifier/tests/test_helpers.py | 45 ++ jsonifier/tests/test_ir_exports_line.py | 68 +++ jsonifier/views/ir_exports_resolver_view.xml | 26 + jsonifier/views/ir_exports_view.xml | 38 ++ 32 files changed, 3607 insertions(+) create mode 100644 jsonifier/README.rst create mode 100644 jsonifier/__init__.py create mode 100644 jsonifier/__manifest__.py create mode 100644 jsonifier/demo/export_demo.xml create mode 100644 jsonifier/demo/ir.exports.line.csv create mode 100644 jsonifier/demo/resolver_demo.xml create mode 100644 jsonifier/exceptions.py create mode 100644 jsonifier/i18n/ca.po create mode 100644 jsonifier/i18n/es.po create mode 100644 jsonifier/i18n/it.po create mode 100644 jsonifier/i18n/jsonifier.pot create mode 100644 jsonifier/i18n/zh_CN.po create mode 100644 jsonifier/models/__init__.py create mode 100644 jsonifier/models/ir_exports.py create mode 100644 jsonifier/models/ir_exports_line.py create mode 100644 jsonifier/models/ir_exports_resolver.py create mode 100644 jsonifier/models/models.py create mode 100644 jsonifier/models/utils.py create mode 100644 jsonifier/pyproject.toml create mode 100644 jsonifier/readme/CONTRIBUTORS.md create mode 100644 jsonifier/readme/CREDITS.md create mode 100644 jsonifier/readme/DESCRIPTION.md create mode 100644 jsonifier/readme/USAGE.md create mode 100644 jsonifier/security/ir.model.access.csv create mode 100644 jsonifier/static/description/icon.png create mode 100644 jsonifier/static/description/index.html create mode 100644 jsonifier/tests/__init__.py create mode 100644 jsonifier/tests/test_get_parser.py create mode 100644 jsonifier/tests/test_helpers.py create mode 100644 jsonifier/tests/test_ir_exports_line.py create mode 100644 jsonifier/views/ir_exports_resolver_view.xml create mode 100644 jsonifier/views/ir_exports_view.xml diff --git a/jsonifier/README.rst b/jsonifier/README.rst new file mode 100644 index 00000000000..c0661fd352d --- /dev/null +++ b/jsonifier/README.rst @@ -0,0 +1,284 @@ +========= +JSONifier +========= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:cbf418975463b6a5ff3ae89fdd5b77a2f9c2ad8c23f82869ce51846f94bff7ce + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/18.0/jsonifier + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-jsonifier + :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/server-tools&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a 'jsonify' method to every model of the ORM. It works +on the current recordset and requires a single argument 'parser' that +specify the field to extract. + +Example of a simple parser: + +.. code:: python + + parser = [ + 'name', + 'number', + 'create_date', + ('partner_id', ['id', 'display_name', 'ref']) + ('line_id', ['id', ('product_id', ['name']), 'price_unit']) + ] + +In order to be consistent with the Odoo API the jsonify method always +returns a list of objects even if there is only one element in the +recordset. + +By default the key into the JSON is the name of the field extracted from +the model. If you need to specify an alternate name to use as key, you +can define your mapping as follow into the parser definition: + +.. code:: python + + parser = [ + 'field_name:json_key' + ] + +.. code:: python + + parser = [ + 'name', + 'number', + 'create_date:creationDate', + ('partner_id:partners', ['id', 'display_name', 'ref']) + ('line_id:lines', ['id', ('product_id', ['name']), 'price_unit']) + ] + +If you need to parse the value of a field in a custom way, you can pass +a callable or the name of a method on the model: + +.. code:: python + + parser = [ + ('name', "jsonify_name") # method name + ('number', lambda rec, field_name: rec[field_name] * 2)) # callable + ] + +Also the module provide a method "get_json_parser" on the ir.exports +object that generate a parser from an ir.exports configuration. + +Further features are available for advanced uses. It defines a simple +"resolver" model that has a "python_code" field and a resolve function +so that arbitrary functions can be configured to transform fields, or +process the resulting dictionary. It is also to specify a lang to +extract the translation of any given field. + +To use these features, a full parser follows the following structure: + +.. code:: python + + parser = { + "resolver": 3, + "language_agnostic": True, + "langs": { + False: [ + {'name': 'description'}, + {'name': 'number', 'resolver': 5}, + ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'display_name'}]) + ], + 'fr_FR': [ + {'name': 'description', 'target': 'descriptions_fr'}, + ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'description', 'target': 'description_fr'}]) + ], + } + } + +One would get a result having this structure (note that the translated +fields are merged in the same dictionary): + +.. code:: python + + exported_json == { + "description": "English description", + "description_fr": "French description, voilà", + "number": 42, + "partner": { + "display_name": "partner name", + "description_fr": "French description of that partner", + }, + } + +Note that a resolver can be passed either as a recordset or as an id, so +as to be fully serializable. A slightly simpler version in case the +translation of fields is not needed, but other features like custom +resolvers are: + +.. code:: python + + parser = { + "resolver": 3, + "fields": [ + {'name': 'description'}, + {'name': 'number', 'resolver': 5}, + ({'name': 'partner_id', 'target': 'partners'}, [{'name': 'display_name'}]), + ], + } + +By passing the fields key instead of langs, we have essentially the same +behaviour as simple parsers, with the added benefit of being able to use +resolvers. + +Standard use-cases of resolvers are: - give field-specific defaults +(e.g. "" instead of None) - cast a field type (e.g. int()) - alias a +particular field for a specific export - ... + +A simple parser is simply translated into a full parser at export. + +If the global resolver is given, then the json_dict goes through: + +.. code:: python + + resolver.resolve(dict, record) + +Which allows to add external data from the context or transform the +dictionary if necessary. Similarly if given for a field the resolver +evaluates the result. + +It is possible for a target to have a marshaller by ending the target +with '=list': in that case the result is put into a list. + +.. code:: python + + parser = { + fields: [ + {'name': 'name'}, + {'name': 'field_1', 'target': 'customTags=list'}, + {'name': 'field_2', 'target': 'customTags=list'}, + ] + } + +Would result in the following JSON structure: + +.. code:: python + + { + 'name': 'record_name', + 'customTags': ['field_1_value', 'field_2_value'], + } + +The intended use-case is to be compatible with APIs that require all +translated parameters to be exported simultaneously, and ask for custom +properties to be put in a sub-dictionary. Since it is often the case +that some of these requirements are optional, new requirements could be +met without needing to add field or change any code. + +Note that the export values with the simple parser depends on the +record's lang; this is in contrast with full parsers which are designed +to be language agnostic. + +NOTE: this module was named base_jsonify till version 14.0.1.5.0. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +with_fieldname parameter +------------------------ + +The with_fieldname option of jsonify() method, when true, will inject on +the same level of the data "\_fieldname\_$field" keys that will contain +the field name, in the language of the current user. + + Examples of with_fieldname usage: + +.. code:: python + + # example 1 + parser = [('name')] + a.jsonify(parser=parser) + [{'name': 'SO3996'}] + >>> a.jsonify(parser=parser, with_fieldname=False) + [{'name': 'SO3996'}] + >>> a.jsonify(parser=parser, with_fieldname=True) + [{'fieldname_name': 'Order Reference', 'name': 'SO3996'}}] + + + # example 2 - with a subparser- + parser=['name', 'create_date', ('order_line', ['id' , 'product_uom', 'is_expense'])] + >>> a.jsonify(parser=parser, with_fieldname=False) + [{'name': 'SO3996', 'create_date': '2015-06-02T12:18:26.279909+00:00', 'order_line': [{'id': 16649, 'product_uom': 'stuks', 'is_expense': False}, {'id': 16651, 'product_uom': 'stuks', 'is_expense': False}, {'id': 16650, 'product_uom': 'stuks', 'is_expense': False}]}] + >>> a.jsonify(parser=parser, with_fieldname=True) + [{'fieldname_name': 'Order Reference', 'name': 'SO3996', 'fieldname_create_date': 'Creation Date', 'create_date': '2015-06-02T12:18:26.279909+00:00', 'fieldname_order_line': 'Order Lines', 'order_line': [{'fieldname_id': 'ID', 'id': 16649, 'fieldname_product_uom': 'Unit of Measure', 'product_uom': 'stuks', 'fieldname_is_expense': 'Is expense', 'is_expense': False}]}] + +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 +------- + +* Akretion +* ACSONE +* Camptocamp + +Contributors +------------ + +- BEAU Sébastien +- Raphaël Reverdy +- Laurent Mignon +- Nans Lefebvre +- Simone Orsi +- Iván Todorovich +- Nguyen Minh Chien +- Thien Vo + +Other credits +------------- + +The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp. + +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/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/jsonifier/__init__.py b/jsonifier/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/jsonifier/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/jsonifier/__manifest__.py b/jsonifier/__manifest__.py new file mode 100644 index 00000000000..69f296199d3 --- /dev/null +++ b/jsonifier/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2017-2018 Akretion (http://www.akretion.com) +# Sébastien BEAU +# Raphaël Reverdy +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +{ + "name": "JSONifier", + "summary": "JSON-ify data for all models", + "version": "19.0.0.1.0", + "category": "Uncategorized", + "website": "https://github.com/OCA/server-tools", + "author": "Akretion, ACSONE, Camptocamp, Odoo Community Association (OCA)", + "license": "LGPL-3", + "installable": True, + "depends": ["base"], + "data": [ + "security/ir.model.access.csv", + "views/ir_exports_view.xml", + "views/ir_exports_resolver_view.xml", + ], + "demo": [ + "demo/resolver_demo.xml", + "demo/export_demo.xml", + "demo/ir.exports.line.csv", + ], +} diff --git a/jsonifier/demo/export_demo.xml b/jsonifier/demo/export_demo.xml new file mode 100644 index 00000000000..a060d300266 --- /dev/null +++ b/jsonifier/demo/export_demo.xml @@ -0,0 +1,7 @@ + + + + Partner Export + res.partner + + diff --git a/jsonifier/demo/ir.exports.line.csv b/jsonifier/demo/ir.exports.line.csv new file mode 100644 index 00000000000..4744c11ea0c --- /dev/null +++ b/jsonifier/demo/ir.exports.line.csv @@ -0,0 +1,16 @@ +id,export_id/id,name +name,ir_exp_partner,name +active,ir_exp_partner,active +partner_latitude,ir_exp_partner,partner_latitude +color,ir_exp_partner,color +category_id_name,ir_exp_partner,category_id/name +country_id_name,ir_exp_partner,country_id/name +country_id_code,ir_exp_partner,country_id/code +child_ids_name,ir_exp_partner,child_ids/name +child_ids_id,ir_exp_partner,child_ids/id +child_ids_email,ir_exp_partner,child_ids/email +child_ids_country_id_name,ir_exp_partner,child_ids/country_id/name +child_ids_country_id_code,ir_exp_partner,child_ids/country_id/code +child_ids_child_ids_name,ir_exp_partner,child_ids/child_ids/name +lang,ir_exp_partner,lang +comment,ir_exp_partner,comment diff --git a/jsonifier/demo/resolver_demo.xml b/jsonifier/demo/resolver_demo.xml new file mode 100644 index 00000000000..540302be23d --- /dev/null +++ b/jsonifier/demo/resolver_demo.xml @@ -0,0 +1,12 @@ + + + + ExtraData dictionary (number/text) + +is_number = field_type in ('integer', 'float') +ftype = "NUMBER" if is_number else "TEXT" +value = value if is_number else str(value) +result = {"Key": name, "Value": value, "Type": ftype, "IsPublic": True} + + + diff --git a/jsonifier/exceptions.py b/jsonifier/exceptions.py new file mode 100644 index 00000000000..7fd3fd38484 --- /dev/null +++ b/jsonifier/exceptions.py @@ -0,0 +1,7 @@ +# Copyright 2022 Camptocamp SA (http://www.camptocamp.com) +# Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + + +class SwallableException(Exception): + """An exception that can be safely skipped.""" diff --git a/jsonifier/i18n/ca.po b/jsonifier/i18n/ca.po new file mode 100644 index 00000000000..1477230c616 --- /dev/null +++ b/jsonifier/i18n/ca.po @@ -0,0 +1,234 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * jsonifier +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ca\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" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__instance_method_name +msgid "A method defined on the model that takes a record and a field_name" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__active +msgid "Active" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_base +msgid "Base" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_resolver__python_code +msgid "" +"Compute the result from 'value' by setting the variable 'result'.\n" +"\n" +"For fields resolvers:\n" +":param record: the record\n" +":param name: name of the field\n" +":param value: value of the field\n" +":param field_type: type of the field\n" +"\n" +"For global resolvers:\n" +":param value: JSON dict\n" +":param record: the record\n" +"\n" +"In both types, you can override the final json key.\n" +"To achieve this, simply return a dict like: \n" +"{'result': {'_value': $value, '_json_key': $new_json_key}}" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Configuration" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_uid +msgid "Created by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_date +msgid "Created on" +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_resolver_view +#: model:ir.ui.menu,name:jsonifier.ui_exports_resolvers +msgid "Custom Export Resolvers" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__global_resolver_id +msgid "Custom global resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__resolver_id +msgid "Custom resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name +msgid "Display Name" +msgstr "" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Either set a function or a resolver, not both." +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_view +#: model:ir.ui.menu,name:jsonifier.ui_exports +msgid "Export Fields" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_resolver +msgid "Export Resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports +msgid "Exports" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_line +msgid "Exports Line" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__field +msgid "Field" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__instance_method_name +msgid "Function" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__global +msgid "Global" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id +msgid "ID" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__lang_id +msgid "If set, the language in which the field is exported" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__global_resolver_id +msgid "If set, will apply the global resolver to the result" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__resolver_id +msgid "If set, will apply the resolver on the field value" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__language_agnostic +msgid "" +"If set, will set the lang to False when exporting lines without lang, " +"otherwise it uses the lang in the given context to export these fields" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Index" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__lang_id +msgid "Language" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__language_agnostic +msgid "Language Agnostic" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_date +msgid "Last Updated on" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__name +msgid "Name" +msgstr "" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Name and Target must have the same hierarchy depth" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__python_code +msgid "Python Code" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target +msgid "Target" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__target +msgid "" +"The complete path to the field where you can specify a target on the step as " +"field:target" +msgstr "" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "" +"The target must reference the same field as in name '%(name)s' not in " +"'%(name_with_target)s'" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__type +msgid "Type" +msgstr "" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/models.py:0 +msgid "Wrong parser configuration for field: `%s`" +msgstr "" diff --git a/jsonifier/i18n/es.po b/jsonifier/i18n/es.po new file mode 100644 index 00000000000..4db956314ff --- /dev/null +++ b/jsonifier/i18n/es.po @@ -0,0 +1,266 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * jsonifier +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-11 15:39+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\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" +"X-Generator: Weblate 4.17\n" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__instance_method_name +msgid "A method defined on the model that takes a record and a field_name" +msgstr "Un método definido en el modelo que toma un registro y un field_name" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__active +msgid "Active" +msgstr "Activo" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_base +msgid "Base" +msgstr "Base" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_resolver__python_code +msgid "" +"Compute the result from 'value' by setting the variable 'result'.\n" +"\n" +"For fields resolvers:\n" +":param record: the record\n" +":param name: name of the field\n" +":param value: value of the field\n" +":param field_type: type of the field\n" +"\n" +"For global resolvers:\n" +":param value: JSON dict\n" +":param record: the record\n" +"\n" +"In both types, you can override the final json key.\n" +"To achieve this, simply return a dict like: \n" +"{'result': {'_value': $value, '_json_key': $new_json_key}}" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Configuration" +msgstr "Configuración" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_resolver_view +#: model:ir.ui.menu,name:jsonifier.ui_exports_resolvers +msgid "Custom Export Resolvers" +msgstr "Resolucionadores de Exportación Personalizados" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__global_resolver_id +msgid "Custom global resolver" +msgstr "Resolución global personalizada" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__resolver_id +msgid "Custom resolver" +msgstr "Resolución personalizada" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Either set a function or a resolver, not both." +msgstr "O bien establece una función o una resolución, no ambas." + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_view +#: model:ir.ui.menu,name:jsonifier.ui_exports +msgid "Export Fields" +msgstr "Exportar Campos" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_resolver +msgid "Export Resolver" +msgstr "Resolver Exportaciones" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports +msgid "Exports" +msgstr "Exportaciones" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_line +msgid "Exports Line" +msgstr "Línea de Exportación" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__field +msgid "Field" +msgstr "Campo" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__instance_method_name +msgid "Function" +msgstr "Función" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__global +msgid "Global" +msgstr "Global" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__lang_id +msgid "If set, the language in which the field is exported" +msgstr "Si se establece, el idioma en el que se exporta el campo" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__global_resolver_id +msgid "If set, will apply the global resolver to the result" +msgstr "Si se establece, se aplicará el resolver global al resultado" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__resolver_id +msgid "If set, will apply the resolver on the field value" +msgstr "Si se establece, se aplicará el resolver en el valor del campo" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__language_agnostic +msgid "" +"If set, will set the lang to False when exporting lines without lang, " +"otherwise it uses the lang in the given context to export these fields" +msgstr "" +"Si se establece, se establecerá el lang a False al exportar líneas sin lang, " +"de lo contrario utiliza el lang en el contexto dado para exportar estos " +"campos" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Index" +msgstr "Índice" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__lang_id +msgid "Language" +msgstr "Lenguaje" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__language_agnostic +msgid "Language Agnostic" +msgstr "Agnóstico lingüístico" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_uid +msgid "Last Updated by" +msgstr "Actualizado por Última vez por" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__name +msgid "Name" +msgstr "Nombre" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Name and Target must have the same hierarchy depth" +msgstr "Nombre y Destino deben tener la misma profundidad jerárquica" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__python_code +msgid "Python Code" +msgstr "Código Python" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "Búsqueda Inteligente" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target +msgid "Target" +msgstr "Objetivo" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__target +msgid "" +"The complete path to the field where you can specify a target on the step as " +"field:target" +msgstr "" +"La ruta completa al campo donde se puede especificar un objetivo en el paso " +"como field:target" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "" +"The target must reference the same field as in name '%(name)s' not in " +"'%(name_with_target)s'" +msgstr "" +"El objetivo debe hacer referencia al mismo campo que en nombre '%(name)s' no " +"en '%(name_with_target)s'" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__type +msgid "Type" +msgstr "Tipo" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/models.py:0 +msgid "Wrong parser configuration for field: `%s`" +msgstr "Configuración incorrecta del analizador sintáctico para el campo: `%s`" + +#~ msgid "" +#~ "Compute the result from 'value' by setting the variable 'result'.\n" +#~ "For fields resolvers:\n" +#~ ":param name: name of the field\n" +#~ ":param value: value of the field\n" +#~ ":param field_type: type of the field\n" +#~ "For global resolvers:\n" +#~ ":param value: JSON dict\n" +#~ ":param record: the record" +#~ msgstr "" +#~ "Calcule el resultado a partir de 'valor' estableciendo la variable " +#~ "'resultado'.\n" +#~ "Para resolvedores de campos:\n" +#~ ":param nombre: nombre del campo\n" +#~ ":param valor: valor del campo\n" +#~ ":param tipo_campo: tipo del campo\n" +#~ "Para resolvedores globales:\n" +#~ ":param valor: dict JSON\n" +#~ ":param registro: el registro" + +#~ msgid "Last Modified on" +#~ msgstr "Última Modifiación en" diff --git a/jsonifier/i18n/it.po b/jsonifier/i18n/it.po new file mode 100644 index 00000000000..5086baf89f4 --- /dev/null +++ b/jsonifier/i18n/it.po @@ -0,0 +1,277 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * jsonifier +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-03-18 10:38+0000\n" +"Last-Translator: mymage \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" +"X-Generator: Weblate 5.10.2\n" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__instance_method_name +msgid "A method defined on the model that takes a record and a field_name" +msgstr "Un metdo definito nel modello che usa un record e un field_name" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__active +msgid "Active" +msgstr "Attiva" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_base +msgid "Base" +msgstr "Base" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_resolver__python_code +msgid "" +"Compute the result from 'value' by setting the variable 'result'.\n" +"\n" +"For fields resolvers:\n" +":param record: the record\n" +":param name: name of the field\n" +":param value: value of the field\n" +":param field_type: type of the field\n" +"\n" +"For global resolvers:\n" +":param value: JSON dict\n" +":param record: the record\n" +"\n" +"In both types, you can override the final json key.\n" +"To achieve this, simply return a dict like: \n" +"{'result': {'_value': $value, '_json_key': $new_json_key}}" +msgstr "" +"Calcola il risultato dal 'value' impostando la variabile 'result'.\n" +"\n" +"Per risolutori campi:\n" +":param record: il record\n" +":param name: nome del campo\n" +":param value: valore del campo\n" +":param field_type: tipo del campo\n" +"\n" +"Per risolutori globali:\n" +":param value: dizionario JSON \n" +":param record: il record\n" +"\n" +"In entrambi i tipi, si può forzare la chiave JSON finale.\n" +"Per farlo, restituire un dizionario come: \n" +"{'result': {'_value': $value, '_json_key': $new_json_key}}" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Configuration" +msgstr "Configurazione" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_resolver_view +#: model:ir.ui.menu,name:jsonifier.ui_exports_resolvers +msgid "Custom Export Resolvers" +msgstr "Identificatori esportazione personalizzati" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__global_resolver_id +msgid "Custom global resolver" +msgstr "identificatore globale personalizzato" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__resolver_id +msgid "Custom resolver" +msgstr "Identificatore personalizzato" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Either set a function or a resolver, not both." +msgstr "Impostare una funzione o un identificatore, non entrambi." + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_view +#: model:ir.ui.menu,name:jsonifier.ui_exports +msgid "Export Fields" +msgstr "Esporta campi" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_resolver +msgid "Export Resolver" +msgstr "Identificatore esportazione" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports +msgid "Exports" +msgstr "Esportazioni" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_line +msgid "Exports Line" +msgstr "Riga esportazioni" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__field +msgid "Field" +msgstr "Campo" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__instance_method_name +msgid "Function" +msgstr "Funzione" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__global +msgid "Global" +msgstr "Globale" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id +msgid "ID" +msgstr "ID" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__lang_id +msgid "If set, the language in which the field is exported" +msgstr "Se impostato, la lingua in cui è esportato il campo" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__global_resolver_id +msgid "If set, will apply the global resolver to the result" +msgstr "Se impostata, applicherà il riferimento globale al risultato" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__resolver_id +msgid "If set, will apply the resolver on the field value" +msgstr "Se impostata, applicherà il riferimento al valore del campo" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__language_agnostic +msgid "" +"If set, will set the lang to False when exporting lines without lang, " +"otherwise it uses the lang in the given context to export these fields" +msgstr "" +"Se impostata, imposterà il linuaggio a False nell'esportazione di righe " +"senza linguaggio, altrimenti utilizzerà il linguaggio nel dato contesto per " +"esportare questi campi" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Index" +msgstr "Indice" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__lang_id +msgid "Language" +msgstr "Lingua" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__language_agnostic +msgid "Language Agnostic" +msgstr "Agnostico alla lingua" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__name +msgid "Name" +msgstr "Nome" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Name and Target must have the same hierarchy depth" +msgstr "Nome e obiettivo devno avere la stessa profondità gerarchica" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__python_code +msgid "Python Code" +msgstr "Codice Python" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "Ricerca intelligente" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target +msgid "Target" +msgstr "Obiettivo" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__target +msgid "" +"The complete path to the field where you can specify a target on the step as " +"field:target" +msgstr "" +"Il percorso completo al campo dove si può indicare un obiettivo sul " +"passaggio come field:target" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "" +"The target must reference the same field as in name '%(name)s' not in " +"'%(name_with_target)s'" +msgstr "" +"L'obiettivo deve far riferimento allo stesso campo come nel nome '%(name)s' " +"non in '%(name_with_target)s'" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__type +msgid "Type" +msgstr "Tipo" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/models.py:0 +msgid "Wrong parser configuration for field: `%s`" +msgstr "Errata configurazione parser per il campo: `%s`" + +#~ msgid "" +#~ "Compute the result from 'value' by setting the variable 'result'.\n" +#~ "For fields resolvers:\n" +#~ ":param name: name of the field\n" +#~ ":param value: value of the field\n" +#~ ":param field_type: type of the field\n" +#~ "For global resolvers:\n" +#~ ":param value: JSON dict\n" +#~ ":param record: the record" +#~ msgstr "" +#~ "Calcola il risultato da 'value'impostando la variabile 'result'.\n" +#~ "Identificatri campi:\n" +#~ ":param name: nome del campo\n" +#~ ":param value: valore del cmapo\n" +#~ ":param field_type: tipo del campo\n" +#~ "Identificatori globali:\n" +#~ ":param value: dizionario JSON\n" +#~ ":param record: il record" diff --git a/jsonifier/i18n/jsonifier.pot b/jsonifier/i18n/jsonifier.pot new file mode 100644 index 00000000000..4059b8e8662 --- /dev/null +++ b/jsonifier/i18n/jsonifier.pot @@ -0,0 +1,233 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * jsonifier +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__instance_method_name +msgid "A method defined on the model that takes a record and a field_name" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__active +msgid "Active" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_base +msgid "Base" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_resolver__python_code +msgid "" +"Compute the result from 'value' by setting the variable 'result'.\n" +"\n" +"For fields resolvers:\n" +":param record: the record\n" +":param name: name of the field\n" +":param value: value of the field\n" +":param field_type: type of the field\n" +"\n" +"For global resolvers:\n" +":param value: JSON dict\n" +":param record: the record\n" +"\n" +"In both types, you can override the final json key.\n" +"To achieve this, simply return a dict like: \n" +"{'result': {'_value': $value, '_json_key': $new_json_key}}" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Configuration" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_uid +msgid "Created by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_date +msgid "Created on" +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_resolver_view +#: model:ir.ui.menu,name:jsonifier.ui_exports_resolvers +msgid "Custom Export Resolvers" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__global_resolver_id +msgid "Custom global resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__resolver_id +msgid "Custom resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name +msgid "Display Name" +msgstr "" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Either set a function or a resolver, not both." +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_view +#: model:ir.ui.menu,name:jsonifier.ui_exports +msgid "Export Fields" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_resolver +msgid "Export Resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports +msgid "Exports" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_line +msgid "Exports Line" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__field +msgid "Field" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__instance_method_name +msgid "Function" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__global +msgid "Global" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id +msgid "ID" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__lang_id +msgid "If set, the language in which the field is exported" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__global_resolver_id +msgid "If set, will apply the global resolver to the result" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__resolver_id +msgid "If set, will apply the resolver on the field value" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__language_agnostic +msgid "" +"If set, will set the lang to False when exporting lines without lang, " +"otherwise it uses the lang in the given context to export these fields" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Index" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__lang_id +msgid "Language" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__language_agnostic +msgid "Language Agnostic" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_date +msgid "Last Updated on" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__name +msgid "Name" +msgstr "" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Name and Target must have the same hierarchy depth" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__python_code +msgid "Python Code" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target +msgid "Target" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__target +msgid "" +"The complete path to the field where you can specify a target on the step as" +" field:target" +msgstr "" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "" +"The target must reference the same field as in name '%(name)s' not in " +"'%(name_with_target)s'" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__type +msgid "Type" +msgstr "" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/models.py:0 +msgid "Wrong parser configuration for field: `%s`" +msgstr "" diff --git a/jsonifier/i18n/zh_CN.po b/jsonifier/i18n/zh_CN.po new file mode 100644 index 00000000000..2fafc089de7 --- /dev/null +++ b/jsonifier/i18n/zh_CN.po @@ -0,0 +1,260 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * jsonifier +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-06-16 11:07+0000\n" +"Last-Translator: xtanuiha \n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.17\n" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__instance_method_name +msgid "A method defined on the model that takes a record and a field_name" +msgstr "在模型上定义的一个方法,该方法接受一条记录和一个字段名作为参数" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__active +msgid "Active" +msgstr "激活" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_base +msgid "Base" +msgstr "基础" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_resolver__python_code +msgid "" +"Compute the result from 'value' by setting the variable 'result'.\n" +"\n" +"For fields resolvers:\n" +":param record: the record\n" +":param name: name of the field\n" +":param value: value of the field\n" +":param field_type: type of the field\n" +"\n" +"For global resolvers:\n" +":param value: JSON dict\n" +":param record: the record\n" +"\n" +"In both types, you can override the final json key.\n" +"To achieve this, simply return a dict like: \n" +"{'result': {'_value': $value, '_json_key': $new_json_key}}" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Configuration" +msgstr "配置" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_uid +msgid "Created by" +msgstr "创建者" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_date +msgid "Created on" +msgstr "创建于" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_resolver_view +#: model:ir.ui.menu,name:jsonifier.ui_exports_resolvers +msgid "Custom Export Resolvers" +msgstr "自定义导出解析器" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__global_resolver_id +msgid "Custom global resolver" +msgstr "自定义全局解析器" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__resolver_id +msgid "Custom resolver" +msgstr "自定义解析器" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name +msgid "Display Name" +msgstr "显示名称" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Either set a function or a resolver, not both." +msgstr "只能设置函数或解析器中的一个,不能同时设置。" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_view +#: model:ir.ui.menu,name:jsonifier.ui_exports +msgid "Export Fields" +msgstr "导出字段" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_resolver +msgid "Export Resolver" +msgstr "导出解析器" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports +msgid "Exports" +msgstr "导出" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_line +msgid "Exports Line" +msgstr "导出行" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__field +msgid "Field" +msgstr "字段" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__instance_method_name +msgid "Function" +msgstr "函数" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__global +msgid "Global" +msgstr "全局" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id +msgid "ID" +msgstr "ID" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__lang_id +msgid "If set, the language in which the field is exported" +msgstr "如果设置,表示字段将以哪种语言导出" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__global_resolver_id +msgid "If set, will apply the global resolver to the result" +msgstr "如果设置,将对结果应用全局解析器" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__resolver_id +msgid "If set, will apply the resolver on the field value" +msgstr "如果设置,将对字段值应用特定解析器" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__language_agnostic +msgid "" +"If set, will set the lang to False when exporting lines without lang, " +"otherwise it uses the lang in the given context to export these fields" +msgstr "" +"如果设置,在导出不包含语言信息的行时,会将语言(lang)设置为False;否则,它会使" +"用给定上下文中的语言来导出这些字段" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Index" +msgstr "索引" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__lang_id +msgid "Language" +msgstr "语言" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__language_agnostic +msgid "Language Agnostic" +msgstr "语言无关" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_uid +msgid "Last Updated by" +msgstr "最后更新者" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_date +msgid "Last Updated on" +msgstr "最后更新于" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__name +msgid "Name" +msgstr "名称" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "Name and Target must have the same hierarchy depth" +msgstr "名称和别名必须具有相同的层次结构深度" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__python_code +msgid "Python Code" +msgstr "Python代码" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target +msgid "Target" +msgstr "别名" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__target +msgid "" +"The complete path to the field where you can specify a target on the step as " +"field:target" +msgstr "字段的完整路径,您可以在其中指定步骤作为字段的别名:别名" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/ir_exports_line.py:0 +msgid "" +"The target must reference the same field as in name '%(name)s' not in " +"'%(name_with_target)s'" +msgstr "目标必须引用与名称 '%(name)s' 相同的字段,而非 '%(name_with_target)s'" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__type +msgid "Type" +msgstr "类型" + +#. module: jsonifier +#. odoo-python +#: code:addons/jsonifier/models/models.py:0 +msgid "Wrong parser configuration for field: `%s`" +msgstr "字段`%s`的解析器配置有误" + +#~ msgid "" +#~ "Compute the result from 'value' by setting the variable 'result'.\n" +#~ "For fields resolvers:\n" +#~ ":param name: name of the field\n" +#~ ":param value: value of the field\n" +#~ ":param field_type: type of the field\n" +#~ "For global resolvers:\n" +#~ ":param value: JSON dict\n" +#~ ":param record: the record" +#~ msgstr "" +#~ "通过设置变量 'result' 来计算来自 'value' 的结果。\n" +#~ "对于字段解析器:\n" +#~ ":param name: 字段名\n" +#~ ":param value: 字段值\n" +#~ ":param field_type: 对于全局解析器的字段类型\n" +#~ ":param value: JSON 字典\n" +#~ ":param record: 记录对象" + +#, python-format +#~ msgid "The target must reference the same field as in name '%s' not in '%s'" +#~ msgstr "别名必须引用与名称相同的字段'%s'不在'%s'" diff --git a/jsonifier/models/__init__.py b/jsonifier/models/__init__.py new file mode 100644 index 00000000000..cd8aff40994 --- /dev/null +++ b/jsonifier/models/__init__.py @@ -0,0 +1,5 @@ +from . import utils +from . import models +from . import ir_exports +from . import ir_exports_line +from . import ir_exports_resolver diff --git a/jsonifier/models/ir_exports.py b/jsonifier/models/ir_exports.py new file mode 100644 index 00000000000..6e621d651aa --- /dev/null +++ b/jsonifier/models/ir_exports.py @@ -0,0 +1,124 @@ +# © 2017 Akretion (http://www.akretion.com) +# Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from collections import OrderedDict + +from odoo import fields, models +from odoo.tools import ormcache + + +def partition(line, accessor): + """Partition a recordset according to an accessor (e.g. a lambda). + Returns a dictionary whose keys are the values obtained from accessor, + and values are the items that have this value. + Example: partition([{"name": "ax"}, {"name": "by"}], lambda x: "x" in x["name"]) + => {True: [{"name": "ax"}], False: [{"name": "by"}]} + """ + result = {} + for item in line: + key = accessor(item) + if key not in result: + result[key] = [] + result[key].append(item) + return result + + +def update_dict(data, fields, options): + """Contruct a tree of fields. + + Example: + + { + "name": True, + "resource": True, + } + + Order of keys is important. + """ + field = fields[0] + if len(fields) == 1: + if field == ".id": + field = "id" + data[field] = (True, options) + else: + if field not in data: + data[field] = (False, OrderedDict()) + update_dict(data[field][1], fields[1:], options) + + +def convert_dict(dict_parser): + """Convert dict returned by update_dict to list consistent w/ Odoo API. + + The list is composed of strings (field names or targets) or tuples. + """ + parser = [] + for field, value in dict_parser.items(): + if value[0] is True: # is a leaf + parser.append(field_dict(field, value[1])) + else: + parser.append((field_dict(field), convert_dict(value[1]))) + return parser + + +def field_dict(field, options=None): + """Create a parser dict for the field field.""" + result = {"name": field.split(":")[0]} + if len(field.split(":")) > 1: + result["target"] = field.split(":")[1] + for option in options or {}: + if options[option]: + result[option] = options[option] + return result + + +class IrExports(models.Model): + _inherit = "ir.exports" + + language_agnostic = fields.Boolean( + default=False, + help="If set, will set the lang to False when exporting lines without lang," + " otherwise it uses the lang in the given context to export these fields", + ) + + global_resolver_id = fields.Many2one( + comodel_name="ir.exports.resolver", + string="Custom global resolver", + domain="[('type', '=', 'global')]", + help="If set, will apply the global resolver to the result", + ) + + @ormcache( + "self.language_agnostic", + "self.global_resolver_id.id", + "tuple(self.export_fields.mapped('write_date'))", + ) + def get_json_parser(self): + """Creates a parser from ir.exports record and return it. + + The final parser can be used to "jsonify" records of ir.export's model. + """ + self.ensure_one() + parser = {} + lang_to_lines = partition(self.export_fields, lambda _l: _l.lang_id.code) + lang_parsers = {} + for lang in lang_to_lines: + dict_parser = OrderedDict() + for line in lang_to_lines[lang]: + names = line.name.split("/") + if line.target: + names = line.target.split("/") + function = line.instance_method_name + # resolver must be passed as ID to avoid cache issues + options = {"resolver": line.resolver_id.id, "function": function} + update_dict(dict_parser, names, options) + lang_parsers[lang] = convert_dict(dict_parser) + if list(lang_parsers.keys()) == [False]: + parser["fields"] = lang_parsers[False] + else: + parser["langs"] = lang_parsers + if self.global_resolver_id: + parser["resolver"] = self.global_resolver_id.id + if self.language_agnostic: + parser["language_agnostic"] = self.language_agnostic + return parser diff --git a/jsonifier/models/ir_exports_line.py b/jsonifier/models/ir_exports_line.py new file mode 100644 index 00000000000..325f4b2b20d --- /dev/null +++ b/jsonifier/models/ir_exports_line.py @@ -0,0 +1,58 @@ +# Copyright 2017 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class IrExportsLine(models.Model): + _inherit = "ir.exports.line" + + target = fields.Char( + help="The complete path to the field where you can specify a " + "target on the step as field:target", + ) + active = fields.Boolean(default=True) + lang_id = fields.Many2one( + comodel_name="res.lang", + string="Language", + help="If set, the language in which the field is exported", + ) + resolver_id = fields.Many2one( + comodel_name="ir.exports.resolver", + string="Custom resolver", + help="If set, will apply the resolver on the field value", + ) + instance_method_name = fields.Char( + string="Function", + help="A method defined on the model that takes a record and a field_name", + ) + + @api.constrains("resolver_id", "instance_method_name") + def _check_function_resolver(self): + for rec in self: + if rec.resolver_id and rec.instance_method_name: + msg = _("Either set a function or a resolver, not both.") + raise ValidationError(msg) + + @api.constrains("target", "name") + def _check_target(self): + for rec in self: + if not rec.target: + continue + names = rec.name.split("/") + names_with_target = rec.target.split("/") + if len(names) != len(names_with_target): + raise ValidationError( + _("Name and Target must have the same hierarchy depth") + ) + for name, name_with_target in zip(names, names_with_target, strict=True): + field_name = name_with_target.split(":")[0] + if name != field_name: + raise ValidationError( + _( + "The target must reference the same field as in " + "name '%(name)s' not in '%(name_with_target)s'" + ) + % dict(name=name, name_with_target=name_with_target) + ) diff --git a/jsonifier/models/ir_exports_resolver.py b/jsonifier/models/ir_exports_resolver.py new file mode 100644 index 00000000000..a22b6ea5e70 --- /dev/null +++ b/jsonifier/models/ir_exports_resolver.py @@ -0,0 +1,58 @@ +# Copyright 2020 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models +from odoo.tools.safe_eval import safe_eval + +help_message = [ + "Compute the result from 'value' by setting the variable 'result'.", + "\n" "For fields resolvers:", + ":param record: the record", + ":param name: name of the field", + ":param value: value of the field", + ":param field_type: type of the field", + "\n" "For global resolvers:", + ":param value: JSON dict", + ":param record: the record", + "\n" + "In both types, you can override the final json key." + "\nTo achieve this, simply return a dict like: " + "\n{'result': {'_value': $value, '_json_key': $new_json_key}}", +] + + +class FieldResolver(models.Model): + """Arbitrary function to process a field or a dict at export time.""" + + _name = "ir.exports.resolver" + _description = "Export Resolver" + + name = fields.Char() + type = fields.Selection([("field", "Field"), ("global", "Global")]) + python_code = fields.Text( + default="\n".join(["# " + h for h in help_message] + ["result = value"]), + help="\n".join(help_message), + ) + + def resolve(self, param, records): + self.ensure_one() + result = [] + context = records.env.context + if self.type == "global": + assert len(param) == len(records) + for value, record in zip(param, records, strict=True): + values = {"value": value, "record": record, "context": context} + safe_eval(self.python_code, values, mode="exec", nocopy=True) + result.append(values["result"]) + else: # param is a field + for record in records: + values = { + "record": record, + "value": record[param.name], + "name": param.name, + "field_type": param.type, + "context": context, + } + safe_eval(self.python_code, values, mode="exec", nocopy=True) + result.append(values["result"]) + return result diff --git a/jsonifier/models/models.py b/jsonifier/models/models.py new file mode 100644 index 00000000000..55ae11cdf7e --- /dev/null +++ b/jsonifier/models/models.py @@ -0,0 +1,269 @@ +# Copyright 2017 Akretion (http://www.akretion.com) +# Sébastien BEAU +# Raphaël Reverdy +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +import logging + +from odoo import api, fields, models, tools +from odoo.exceptions import UserError +from odoo.tools.misc import format_duration +from odoo.tools.translate import _ + +from ..exceptions import SwallableException +from .utils import convert_simple_to_full_parser + +_logger = logging.getLogger(__name__) + + +class Base(models.AbstractModel): + _inherit = "base" + + @api.model + def __parse_field(self, parser_field): + """Deduct how to handle a field from its parser.""" + return parser_field if isinstance(parser_field, tuple) else (parser_field, None) + + @api.model + def _jsonify_bad_parser_error(self, field_name): + raise UserError(_("Wrong parser configuration for field: `%s`") % field_name) + + def _function_value(self, record, function, field_name): + if function in dir(record): + method = getattr(record, function, None) + return method(field_name) + elif callable(function): + return function(record, field_name) + else: + return self._jsonify_bad_parser_error(field_name) + + @api.model + def _jsonify_value(self, field, value): + """Override this function to support new field types.""" + if value is False and field.type != "boolean": + value = None + elif field.type == "date": + value = fields.Date.to_date(value).isoformat() + elif field.type == "datetime": + # Ensures value is a datetime + value = fields.Datetime.to_datetime(value) + value = value.isoformat() + elif field.type in ("many2one", "reference"): + value = value.display_name if value else None + elif field.type in ("one2many", "many2many"): + value = [v.display_name for v in value] + return value + + @api.model + def _add_json_key(self, values, json_key, value): + """To manage defaults, you can use a specific resolver.""" + key, sep, marshaller = json_key.partition("=") + if marshaller == "list": # sublist field + if not values.get(key): + values[key] = [] + values[key].append(value) + else: + values[key] = value + + @api.model + def _jsonify_record(self, parser, rec, root): + """JSONify one record (rec). Private function called by jsonify.""" + strict = self.env.context.get("jsonify_record_strict", False) + for field_key in parser: + field_dict, subparser = rec.__parse_field(field_key) + function = field_dict.get("function") + try: + self._jsonify_record_validate_field(rec, field_dict, strict) + except SwallableException: + if not function: + # If we have a function we can use it to get the value + # even if the field is not available. + # If not, well there's nothing we can do. + continue + json_key = field_dict.get("target", field_dict["name"]) + if function: + try: + value = self._jsonify_record_handle_function( + rec, field_dict, strict + ) + except SwallableException: + continue + elif subparser: + try: + value = self._jsonify_record_handle_subparser( + rec, field_dict, strict, subparser + ) + except SwallableException: + continue + else: + field = rec._fields[field_dict["name"]] + value = rec._jsonify_value(field, rec[field.name]) + resolver = field_dict.get("resolver") + if resolver: + if isinstance(resolver, int): + # cached versions of the parser are stored as integer + resolver = self.env["ir.exports.resolver"].browse(resolver) + value, json_key = self._jsonify_record_handle_resolver( + rec, field, resolver, json_key + ) + # whatever json value we have found in subparser or not ass a sister key + # on the same level _fieldname_{json_key} + if rec.env.context.get("with_fieldname"): + json_key_fieldname = "_fieldname_" + json_key + # check if we are in a subparser has already the fieldname sister keys + fieldname_value = rec._fields[field_dict["name"]].string + self._add_json_key(root, json_key_fieldname, fieldname_value) + self._add_json_key(root, json_key, value) + return root + + def _jsonify_record_validate_field(self, rec, field_dict, strict): + field_name = field_dict["name"] + if field_name not in rec._fields: + if strict: + # let it fail + rec._fields[field_name] # pylint: disable=pointless-statement + else: + if not tools.config["test_enable"]: + # If running live, log proper error + # so that techies can track it down + _logger.warning( + "%(model)s.%(fname)s not available", + {"model": self._name, "fname": field_name}, + ) + raise SwallableException() + return True + + def _jsonify_record_handle_function(self, rec, field_dict, strict): + field_name = field_dict["name"] + function = field_dict["function"] + try: + return self._function_value(rec, function, field_name) + except UserError as err: + if strict: + raise + if not tools.config["test_enable"]: + _logger.error( + "%(model)s.%(func)s not available", + {"model": self._name, "func": str(function)}, + ) + raise SwallableException() from err + + def _jsonify_record_handle_subparser(self, rec, field_dict, strict, subparser): + field_name = field_dict["name"] + field = rec._fields[field_name] + if not (field.relational or field.type == "reference"): + if strict: + self._jsonify_bad_parser_error(field_name) + if not tools.config["test_enable"]: + _logger.error( + "%(model)s.%(fname)s not relational", + {"model": self._name, "fname": field_name}, + ) + raise SwallableException() + value = [self._jsonify_record(subparser, r, {}) for r in rec[field_name]] + + if field.type in ("many2one", "reference"): + value = value[0] if value else None + + return value + + def _jsonify_record_handle_resolver(self, rec, field, resolver, json_key): + value = rec._jsonify_value(field, rec[field.name]) + value = resolver.resolve(field, rec)[0] if resolver else value + if isinstance(value, dict) and "_json_key" in value and "_value" in value: + # Allow override of json_key. + # In this case, + # the final value must be encapsulated into _value key + value, json_key = value["_value"], value["_json_key"] + return value, json_key + + def jsonify(self, parser, one=False, with_fieldname=False): + """Convert the record according to the given parser. + + Example of (simple) parser: + parser = [ + 'name', + 'number', + 'create_date', + ('partner_id', ['id', 'display_name', 'ref']) + ('shipping_id', callable) + ('delivery_id', "record_method") + ('line_id', ['id', ('product_id', ['name']), 'price_unit']) + ] + + In order to be consistent with the Odoo API the jsonify method always + returns a list of objects even if there is only one element in input. + You can change this behavior by passing `one=True` to get only one element. + + By default the key into the JSON is the name of the field extracted + from the model. If you need to specify an alternate name to use as + key, you can define your mapping as follow into the parser definition: + + parser = [ + 'field_name:json_key' + ] + + """ + if one: + self.ensure_one() + if isinstance(parser, list): + parser = convert_simple_to_full_parser(parser) + resolver = parser.get("resolver") + if isinstance(resolver, int): + # cached versions of the parser are stored as integer + resolver = self.env["ir.exports.resolver"].browse(resolver) + results = [{} for record in self] + parsers = {False: parser["fields"]} if "fields" in parser else parser["langs"] + for lang in parsers: + translate = lang or parser.get("language_agnostic") + new_ctx = {} + if translate: + new_ctx["lang"] = lang + if with_fieldname: + new_ctx["with_fieldname"] = True + records = self.with_context(**new_ctx) if new_ctx else self + for record, json in zip(records, results, strict=False): + self._jsonify_record(parsers[lang], record, json) + + if resolver: + results = resolver.resolve(results, self) + return results[0] if one else results + + # HELPERS + + def _jsonify_m2o_to_id(self, fname): + """Helper to get an ID only from a m2o field. + + Example: + + m2o_id + m2o_id:rel_id + _jsonify_m2o_to_id + + """ + return self[fname].id + + def _jsonify_x2m_to_ids(self, fname): + """Helper to get a list of IDs only from a o2m or m2m field. + + Example: + + m2m_ids + m2m_ids:rel_ids + _jsonify_x2m_to_ids + + """ + return self[fname].ids + + def _jsonify_format_duration(self, fname): + """Helper to format a Float-like duration to string 00:00. + + Example: + + duration + _jsonify_format_duration + + """ + return format_duration(self[fname]) diff --git a/jsonifier/models/utils.py b/jsonifier/models/utils.py new file mode 100644 index 00000000000..dba45cc06c7 --- /dev/null +++ b/jsonifier/models/utils.py @@ -0,0 +1,35 @@ +def convert_simple_to_full_parser(parser): + """Convert a simple API style parser to a full parser""" + assert isinstance(parser, list) + return {"fields": _convert_parser(parser)} + + +def _convert_field(fld, function=None): + """Return a dict from the string encoding a field to export. + The : is used as a separator to specify a target, if any. + """ + name, sep, target = fld.partition(":") + field_dict = {"name": name} + if target: + field_dict["target"] = target + if function: + field_dict["function"] = function + return field_dict + + +def _convert_parser(parser): + """Recursively process each list to replace encoded fields as string + by dicts specifying each attribute by its relevant key. + """ + result = [] + for line in parser: + if isinstance(line, str): + field_def = _convert_field(line) + else: + fld, sub = line + if callable(sub) or isinstance(sub, str): + field_def = _convert_field(fld, sub) + else: + field_def = (_convert_field(fld), _convert_parser(sub)) + result.append(field_def) + return result diff --git a/jsonifier/pyproject.toml b/jsonifier/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/jsonifier/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/jsonifier/readme/CONTRIBUTORS.md b/jsonifier/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..4323549dad8 --- /dev/null +++ b/jsonifier/readme/CONTRIBUTORS.md @@ -0,0 +1,8 @@ +- BEAU Sébastien \<\> +- Raphaël Reverdy \<\> +- Laurent Mignon \<\> +- Nans Lefebvre \<\> +- Simone Orsi \<\> +- Iván Todorovich \<\> +- Nguyen Minh Chien \<\> +- Thien Vo \<\> diff --git a/jsonifier/readme/CREDITS.md b/jsonifier/readme/CREDITS.md new file mode 100644 index 00000000000..83b3ec91f7d --- /dev/null +++ b/jsonifier/readme/CREDITS.md @@ -0,0 +1 @@ +The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp. diff --git a/jsonifier/readme/DESCRIPTION.md b/jsonifier/readme/DESCRIPTION.md new file mode 100644 index 00000000000..374a8ea1fc5 --- /dev/null +++ b/jsonifier/readme/DESCRIPTION.md @@ -0,0 +1,163 @@ +This module adds a 'jsonify' method to every model of the ORM. It works +on the current recordset and requires a single argument 'parser' that +specify the field to extract. + +Example of a simple parser: + +``` python +parser = [ + 'name', + 'number', + 'create_date', + ('partner_id', ['id', 'display_name', 'ref']) + ('line_id', ['id', ('product_id', ['name']), 'price_unit']) +] +``` + +In order to be consistent with the Odoo API the jsonify method always +returns a list of objects even if there is only one element in the +recordset. + +By default the key into the JSON is the name of the field extracted from +the model. If you need to specify an alternate name to use as key, you +can define your mapping as follow into the parser definition: + +``` python +parser = [ + 'field_name:json_key' +] +``` + +``` python +parser = [ + 'name', + 'number', + 'create_date:creationDate', + ('partner_id:partners', ['id', 'display_name', 'ref']) + ('line_id:lines', ['id', ('product_id', ['name']), 'price_unit']) +] +``` + +If you need to parse the value of a field in a custom way, you can pass +a callable or the name of a method on the model: + +``` python +parser = [ + ('name', "jsonify_name") # method name + ('number', lambda rec, field_name: rec[field_name] * 2)) # callable +] +``` + +Also the module provide a method "get_json_parser" on the ir.exports +object that generate a parser from an ir.exports configuration. + +Further features are available for advanced uses. It defines a simple +"resolver" model that has a "python_code" field and a resolve function +so that arbitrary functions can be configured to transform fields, or +process the resulting dictionary. It is also to specify a lang to +extract the translation of any given field. + +To use these features, a full parser follows the following structure: + +``` python +parser = { + "resolver": 3, + "language_agnostic": True, + "langs": { + False: [ + {'name': 'description'}, + {'name': 'number', 'resolver': 5}, + ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'display_name'}]) + ], + 'fr_FR': [ + {'name': 'description', 'target': 'descriptions_fr'}, + ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'description', 'target': 'description_fr'}]) + ], + } +} +``` + +One would get a result having this structure (note that the translated +fields are merged in the same dictionary): + +``` python +exported_json == { + "description": "English description", + "description_fr": "French description, voilà", + "number": 42, + "partner": { + "display_name": "partner name", + "description_fr": "French description of that partner", + }, +} +``` + +Note that a resolver can be passed either as a recordset or as an id, so +as to be fully serializable. A slightly simpler version in case the +translation of fields is not needed, but other features like custom +resolvers are: + +``` python +parser = { + "resolver": 3, + "fields": [ + {'name': 'description'}, + {'name': 'number', 'resolver': 5}, + ({'name': 'partner_id', 'target': 'partners'}, [{'name': 'display_name'}]), + ], +} +``` + +By passing the fields key instead of langs, we have essentially the same +behaviour as simple parsers, with the added benefit of being able to use +resolvers. + +Standard use-cases of resolvers are: - give field-specific defaults +(e.g. "" instead of None) - cast a field type (e.g. int()) - alias a +particular field for a specific export - ... + +A simple parser is simply translated into a full parser at export. + +If the global resolver is given, then the json_dict goes through: + +``` python +resolver.resolve(dict, record) +``` + +Which allows to add external data from the context or transform the +dictionary if necessary. Similarly if given for a field the resolver +evaluates the result. + +It is possible for a target to have a marshaller by ending the target +with '=list': in that case the result is put into a list. + +``` python +parser = { + fields: [ + {'name': 'name'}, + {'name': 'field_1', 'target': 'customTags=list'}, + {'name': 'field_2', 'target': 'customTags=list'}, + ] +} +``` + +Would result in the following JSON structure: + +``` python +{ + 'name': 'record_name', + 'customTags': ['field_1_value', 'field_2_value'], +} +``` + +The intended use-case is to be compatible with APIs that require all +translated parameters to be exported simultaneously, and ask for custom +properties to be put in a sub-dictionary. Since it is often the case +that some of these requirements are optional, new requirements could be +met without needing to add field or change any code. + +Note that the export values with the simple parser depends on the +record's lang; this is in contrast with full parsers which are designed +to be language agnostic. + +NOTE: this module was named base_jsonify till version 14.0.1.5.0. diff --git a/jsonifier/readme/USAGE.md b/jsonifier/readme/USAGE.md new file mode 100644 index 00000000000..0d566cb52c9 --- /dev/null +++ b/jsonifier/readme/USAGE.md @@ -0,0 +1,26 @@ +## with_fieldname parameter + +The with_fieldname option of jsonify() method, when true, will inject on +the same level of the data "\_fieldname_\$field" keys that will +contain the field name, in the language of the current user. + +> Examples of with_fieldname usage: + +``` python +# example 1 +parser = [('name')] +a.jsonify(parser=parser) +[{'name': 'SO3996'}] +>>> a.jsonify(parser=parser, with_fieldname=False) +[{'name': 'SO3996'}] +>>> a.jsonify(parser=parser, with_fieldname=True) +[{'fieldname_name': 'Order Reference', 'name': 'SO3996'}}] + + +# example 2 - with a subparser- +parser=['name', 'create_date', ('order_line', ['id' , 'product_uom', 'is_expense'])] +>>> a.jsonify(parser=parser, with_fieldname=False) +[{'name': 'SO3996', 'create_date': '2015-06-02T12:18:26.279909+00:00', 'order_line': [{'id': 16649, 'product_uom': 'stuks', 'is_expense': False}, {'id': 16651, 'product_uom': 'stuks', 'is_expense': False}, {'id': 16650, 'product_uom': 'stuks', 'is_expense': False}]}] +>>> a.jsonify(parser=parser, with_fieldname=True) +[{'fieldname_name': 'Order Reference', 'name': 'SO3996', 'fieldname_create_date': 'Creation Date', 'create_date': '2015-06-02T12:18:26.279909+00:00', 'fieldname_order_line': 'Order Lines', 'order_line': [{'fieldname_id': 'ID', 'id': 16649, 'fieldname_product_uom': 'Unit of Measure', 'product_uom': 'stuks', 'fieldname_is_expense': 'Is expense', 'is_expense': False}]}] +``` diff --git a/jsonifier/security/ir.model.access.csv b/jsonifier/security/ir.model.access.csv new file mode 100644 index 00000000000..dcc0a4adfe7 --- /dev/null +++ b/jsonifier/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_ir_exports_resolver,ir.exports.resolver,model_ir_exports_resolver,base.group_system,1,1,1,1 diff --git a/jsonifier/static/description/icon.png b/jsonifier/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/jsonifier/static/description/index.html b/jsonifier/static/description/index.html new file mode 100644 index 00000000000..9044815608d --- /dev/null +++ b/jsonifier/static/description/index.html @@ -0,0 +1,604 @@ + + + + + +JSONifier + + + +
+

JSONifier

+ + +

Beta License: LGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

This module adds a ‘jsonify’ method to every model of the ORM. It works +on the current recordset and requires a single argument ‘parser’ that +specify the field to extract.

+

Example of a simple parser:

+
+parser = [
+    'name',
+    'number',
+    'create_date',
+    ('partner_id', ['id', 'display_name', 'ref'])
+    ('line_id', ['id', ('product_id', ['name']), 'price_unit'])
+]
+
+

In order to be consistent with the Odoo API the jsonify method always +returns a list of objects even if there is only one element in the +recordset.

+

By default the key into the JSON is the name of the field extracted from +the model. If you need to specify an alternate name to use as key, you +can define your mapping as follow into the parser definition:

+
+parser = [
+    'field_name:json_key'
+]
+
+
+parser = [
+    'name',
+    'number',
+    'create_date:creationDate',
+    ('partner_id:partners', ['id', 'display_name', 'ref'])
+    ('line_id:lines', ['id', ('product_id', ['name']), 'price_unit'])
+]
+
+

If you need to parse the value of a field in a custom way, you can pass +a callable or the name of a method on the model:

+
+parser = [
+    ('name', "jsonify_name")  # method name
+    ('number', lambda rec, field_name: rec[field_name] * 2))  # callable
+]
+
+

Also the module provide a method “get_json_parser” on the ir.exports +object that generate a parser from an ir.exports configuration.

+

Further features are available for advanced uses. It defines a simple +“resolver” model that has a “python_code” field and a resolve function +so that arbitrary functions can be configured to transform fields, or +process the resulting dictionary. It is also to specify a lang to +extract the translation of any given field.

+

To use these features, a full parser follows the following structure:

+
+parser = {
+    "resolver": 3,
+    "language_agnostic": True,
+    "langs": {
+        False: [
+            {'name': 'description'},
+            {'name': 'number', 'resolver': 5},
+            ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'display_name'}])
+        ],
+        'fr_FR': [
+            {'name': 'description', 'target': 'descriptions_fr'},
+            ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'description', 'target': 'description_fr'}])
+        ],
+    }
+}
+
+

One would get a result having this structure (note that the translated +fields are merged in the same dictionary):

+
+exported_json == {
+    "description": "English description",
+    "description_fr": "French description, voilà",
+    "number": 42,
+    "partner": {
+        "display_name": "partner name",
+        "description_fr": "French description of that partner",
+    },
+}
+
+

Note that a resolver can be passed either as a recordset or as an id, so +as to be fully serializable. A slightly simpler version in case the +translation of fields is not needed, but other features like custom +resolvers are:

+
+parser = {
+    "resolver": 3,
+    "fields": [
+            {'name': 'description'},
+            {'name': 'number', 'resolver': 5},
+            ({'name': 'partner_id', 'target': 'partners'}, [{'name': 'display_name'}]),
+    ],
+}
+
+

By passing the fields key instead of langs, we have essentially the same +behaviour as simple parsers, with the added benefit of being able to use +resolvers.

+

Standard use-cases of resolvers are: - give field-specific defaults +(e.g. “” instead of None) - cast a field type (e.g. int()) - alias a +particular field for a specific export - …

+

A simple parser is simply translated into a full parser at export.

+

If the global resolver is given, then the json_dict goes through:

+
+resolver.resolve(dict, record)
+
+

Which allows to add external data from the context or transform the +dictionary if necessary. Similarly if given for a field the resolver +evaluates the result.

+

It is possible for a target to have a marshaller by ending the target +with ‘=list’: in that case the result is put into a list.

+
+parser = {
+    fields: [
+        {'name': 'name'},
+        {'name': 'field_1', 'target': 'customTags=list'},
+        {'name': 'field_2', 'target': 'customTags=list'},
+    ]
+}
+
+

Would result in the following JSON structure:

+
+{
+    'name': 'record_name',
+    'customTags': ['field_1_value', 'field_2_value'],
+}
+
+

The intended use-case is to be compatible with APIs that require all +translated parameters to be exported simultaneously, and ask for custom +properties to be put in a sub-dictionary. Since it is often the case +that some of these requirements are optional, new requirements could be +met without needing to add field or change any code.

+

Note that the export values with the simple parser depends on the +record’s lang; this is in contrast with full parsers which are designed +to be language agnostic.

+

NOTE: this module was named base_jsonify till version 14.0.1.5.0.

+

Table of contents

+ +
+

Usage

+
+

with_fieldname parameter

+

The with_fieldname option of jsonify() method, when true, will inject on +the same level of the data “_fieldname_$field” keys that will contain +the field name, in the language of the current user.

+
+Examples of with_fieldname usage:
+
+# example 1
+parser = [('name')]
+a.jsonify(parser=parser)
+[{'name': 'SO3996'}]
+>>> a.jsonify(parser=parser, with_fieldname=False)
+[{'name': 'SO3996'}]
+>>> a.jsonify(parser=parser, with_fieldname=True)
+[{'fieldname_name': 'Order Reference', 'name': 'SO3996'}}]
+
+
+# example 2 - with a subparser-
+parser=['name', 'create_date', ('order_line', ['id' , 'product_uom', 'is_expense'])]
+>>> a.jsonify(parser=parser, with_fieldname=False)
+[{'name': 'SO3996', 'create_date': '2015-06-02T12:18:26.279909+00:00', 'order_line': [{'id': 16649, 'product_uom': 'stuks', 'is_expense': False}, {'id': 16651, 'product_uom': 'stuks', 'is_expense': False}, {'id': 16650, 'product_uom': 'stuks', 'is_expense': False}]}]
+>>> a.jsonify(parser=parser, with_fieldname=True)
+[{'fieldname_name': 'Order Reference', 'name': 'SO3996', 'fieldname_create_date': 'Creation Date', 'create_date': '2015-06-02T12:18:26.279909+00:00', 'fieldname_order_line': 'Order Lines', 'order_line': [{'fieldname_id': 'ID', 'id': 16649, 'fieldname_product_uom': 'Unit of Measure', 'product_uom': 'stuks', 'fieldname_is_expense': 'Is expense', 'is_expense': False}]}]
+
+
+
+
+

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

+
    +
  • Akretion
  • +
  • ACSONE
  • +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp.

+
+
+

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/server-tools project on GitHub.

+

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

+
+
+
+ + diff --git a/jsonifier/tests/__init__.py b/jsonifier/tests/__init__.py new file mode 100644 index 00000000000..3402bb15298 --- /dev/null +++ b/jsonifier/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_get_parser +from . import test_helpers +from . import test_ir_exports_line diff --git a/jsonifier/tests/test_get_parser.py b/jsonifier/tests/test_get_parser.py new file mode 100644 index 00000000000..0f587ec5a48 --- /dev/null +++ b/jsonifier/tests/test_get_parser.py @@ -0,0 +1,448 @@ +# Copyright 2017 ACSONE SA/NV +# Copyright 2022 Camptocamp SA (http://www.camptocamp.com) +# Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from odoo import tools +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase + +from ..models.utils import convert_simple_to_full_parser + + +def jsonify_custom(self, field_name): + return "yeah!" + + +class TestParser(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # disable tracking test suite wise + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.env.user.tz = "Europe/Brussels" + cls.partner = cls.env["res.partner"].create( + { + "name": "Akretion", + "country_id": cls.env.ref("base.fr").id, + "lang": "en_US", # default + "category_id": [(0, 0, {"name": "Inovator"})], + "child_ids": [ + ( + 0, + 0, + { + "name": "Sebatien Beau", + "country_id": cls.env.ref("base.fr").id, + }, + ) + ], + } + ) + Langs = cls.env["res.lang"].with_context(active_test=False) + cls.lang = Langs.search([("code", "=", "fr_FR")]) + cls.lang.active = True + category = cls.env["res.partner.category"].create({"name": "name"}) + cls.translated_target = f"name_{cls.lang.code}" + category.with_context(lang=cls.lang.code).write({"name": cls.translated_target}) + cls.global_resolver = cls.env["ir.exports.resolver"].create( + {"python_code": "value['X'] = 'X'; result = value", "type": "global"} + ) + cls.resolver = cls.env["ir.exports.resolver"].create( + {"python_code": "result = value + '_pidgin'", "type": "field"} + ) + cls.category_export = cls.env["ir.exports"].create( + { + "global_resolver_id": cls.global_resolver.id, + "language_agnostic": True, + "export_fields": [ + (0, 0, {"name": "name"}), + ( + 0, + 0, + { + "name": "name", + "target": f"name:{cls.translated_target}", + "lang_id": cls.lang.id, + }, + ), + ( + 0, + 0, + { + "name": "name", + "target": "name:name_resolved", + "resolver_id": cls.resolver.id, + }, + ), + ], + } + ) + cls.category = category.with_context(lang=None) + cls.category_lang = category.with_context(lang=cls.lang.code) + + def test_getting_parser(self): + expected_parser = [ + "name", + "active", + "partner_latitude", + "color", + ("category_id", ["name"]), + ("country_id", ["name", "code"]), + ( + "child_ids", + [ + "name", + "id", + "email", + ("country_id", ["name", "code"]), + ("child_ids", ["name"]), + ], + ), + "lang", + "comment", + ] + + exporter = self.env.ref("jsonifier.ir_exp_partner") + parser = exporter.get_json_parser() + expected_full_parser = convert_simple_to_full_parser(expected_parser) + self.assertEqual(parser, expected_full_parser) + + # modify an ir.exports_line to put a target for a field + self.env.ref("jsonifier.category_id_name").write( + {"target": "category_id:category/name"} + ) + expected_parser[4] = ("category_id:category", ["name"]) + parser = exporter.get_json_parser() + expected_full_parser = convert_simple_to_full_parser(expected_parser) + self.assertEqual(parser, expected_full_parser) + + def test_json_export(self): + # will allow to view large dict diff in case of regression + self.maxDiff = None + # Enforces TZ to validate the serialization result of a Datetime + parser = [ + "lang", + "comment", + "partner_latitude", + "name", + "color", + ( + "child_ids:children", + [ + ("child_ids:children", ["name"]), + "email", + ("country_id:country", ["code", "name"]), + "name", + "id", + ], + ), + ("country_id:country", ["code", "name"]), + "active", + ("category_id", ["name"]), + "create_date", + ] + # put our own create date to ease tests + self.env.cr.execute( + "update res_partner set create_date=%s where id=%s", + ("2019-10-31 14:39:49", self.partner.id), + ) + expected_json = { + "lang": "en_US", + "comment": None, + "partner_latitude": 0.0, + "name": "Akretion", + "color": 0, + "country": {"code": "FR", "name": "France"}, + "active": True, + "category_id": [{"name": "Inovator"}], + "children": [ + { + "id": self.partner.child_ids.id, + "country": {"code": "FR", "name": "France"}, + "children": [], + "name": "Sebatien Beau", + "email": None, + } + ], + "create_date": "2019-10-31T14:39:49", + } + expected_json_with_fieldname = { + "_fieldname_lang": "Language", + "lang": "en_US", + "_fieldname_comment": "Notes", + "comment": None, + "partner_latitude": 0.0, + "_fieldname_name": "Name", + "name": "Akretion", + "_fieldname_color": "Color Index", + "color": 0, + "_fieldname_children": "Contact", + "children": [ + { + "_fieldname_children": "Contact", + "children": [], + "_fieldname_email": "Email", + "email": None, + "_fieldname_country": "Country", + "country": { + "_fieldname_code": "Country Code", + "code": "FR", + "_fieldname_name": "Country Name", + "name": "France", + }, + "_fieldname_name": "Name", + "name": "Sebatien Beau", + "_fieldname_id": "ID", + "id": self.partner.child_ids.id, + } + ], + "_fieldname_country": "Country", + "country": { + "_fieldname_code": "Country Code", + "code": "FR", + "_fieldname_name": "Country Name", + "name": "France", + }, + "_fieldname_active": "Active", + "active": True, + "_fieldname_category_id": "Tags", + "category_id": [{"_fieldname_name": "Name", "name": "Inovator"}], + "_fieldname_create_date": "Created on", + "_fieldname_partner_latitude": "Geo Latitude", + "create_date": "2019-10-31T14:39:49", + } + expected_json_with_fieldname = { + "_fieldname_lang": "Language", + "lang": "en_US", + "_fieldname_comment": "Notes", + "comment": None, + "_fieldname_partner_latitude": "Geo Latitude", + "_fieldname_name": "Name", + "name": "Akretion", + "_fieldname_color": "Color Index", + "color": 0, + "_fieldname_children": "Contact", + "children": [ + { + "_fieldname_children": "Contact", + "children": [], + "_fieldname_email": "Email", + "email": None, + "_fieldname_country": "Country", + "country": { + "_fieldname_code": "Country Code", + "code": "FR", + "_fieldname_name": "Country Name", + "name": "France", + }, + "_fieldname_name": "Name", + "name": "Sebatien Beau", + "_fieldname_id": "ID", + "id": self.partner.child_ids.id, + } + ], + "_fieldname_country": "Country", + "country": { + "_fieldname_code": "Country Code", + "code": "FR", + "_fieldname_name": "Country Name", + "name": "France", + }, + "_fieldname_active": "Active", + "active": True, + "_fieldname_category_id": "Tags", + "category_id": [{"_fieldname_name": "Name", "name": "Inovator"}], + "_fieldname_create_date": "Created on", + "create_date": "2019-10-31T14:39:49", + "partner_latitude": 0.0, + } + json_partner = self.partner.jsonify(parser) + self.assertDictEqual(json_partner[0], expected_json) + json_partner_with_fieldname = self.partner.jsonify( + parser=parser, with_fieldname=True + ) + self.assertDictEqual( + json_partner_with_fieldname[0], expected_json_with_fieldname + ) + # Check that only boolean fields have boolean values into json + # By default if a field is not set into Odoo, the value is always False + # This value is not the expected one into the json + self.partner.write({"child_ids": [(6, 0, [])], "active": False, "lang": False}) + json_partner = self.partner.jsonify(parser) + expected_json["active"] = False + expected_json["lang"] = None + expected_json["children"] = [] + self.assertDictEqual(json_partner[0], expected_json) + + def test_one(self): + parser = [ + "name", + ] + expected_json = { + "name": "Akretion", + } + json_partner = self.partner.jsonify(parser, one=True) + self.assertDictEqual(json_partner, expected_json) + # cannot call on multiple records + with self.assertRaises(ValueError) as err: + self.env["res.partner"].search([]).jsonify(parser, one=True) + self.assertIn("Expected singleton", str(err.exception)) + + def test_json_export_callable_parser(self): + self.partner.__class__.jsonify_custom = jsonify_custom + parser = [ + # callable subparser + ("name", lambda rec, fname: rec[fname] + " rocks!"), + ("name:custom", "jsonify_custom"), + ("unknown_field", lambda rec, fname: "yeah again!"), + ] + expected_json = { + "name": "Akretion rocks!", + "custom": "yeah!", + "unknown_field": "yeah again!", + } + json_partner = self.partner.jsonify(parser) + self.assertDictEqual(json_partner[0], expected_json) + del self.partner.__class__.jsonify_custom + + def test_full_parser(self): + parser = self.category_export.get_json_parser() + json = self.category.jsonify(parser)[0] + json_fr = self.category_lang.jsonify(parser)[0] + + self.assertEqual( + json, json_fr + ) # starting from different languages should not change anything + self.assertEqual(json[self.translated_target], self.translated_target) + self.assertEqual(json["name_resolved"], "name_pidgin") # field resolver + self.assertEqual(json["X"], "X") # added by global resolver + + def test_full_parser_resolver_json_key_override(self): + self.resolver.write( + {"python_code": """result = {"_json_key": "foo", "_value": record.id}"""} + ) + parser = self.category_export.get_json_parser() + json = self.category.jsonify(parser)[0] + self.assertNotIn("name_resolved", json) + self.assertEqual(json["foo"], self.category.id) # field resolver + self.assertEqual(json["X"], "X") # added by global resolver + + def test_simple_parser_translations(self): + """The simple parser result should depend on the context language.""" + parser = ["name"] + json = self.category.jsonify(parser)[0] + json_fr = self.category_lang.jsonify(parser)[0] + + self.assertEqual(json["name"], "name") + self.assertEqual(json_fr["name"], self.translated_target) + + def test_simple_star_target_and_field_resolver(self): + """The simple parser result should depend on the context language.""" + code = ( + "is_number = field_type in ('integer', 'float');" + "ftype = 'NUMBER' if is_number else 'TEXT';" + "value = value if is_number else str(value);" + "result = {'Key': name, 'Value': value, 'Type': ftype, 'IsPublic': True}" + ) + resolver = self.env["ir.exports.resolver"].create({"python_code": code}) + lang_parser = [ + {"target": "customTags=list", "name": "name", "resolver": resolver}, + {"target": "customTags=list", "name": "id", "resolver": resolver}, + ] + parser = {"language_agnostic": True, "langs": {False: lang_parser}} + expected_json = { + "customTags": [ + {"Value": "name", "Key": "name", "Type": "TEXT", "IsPublic": True}, + { + "Value": self.category.id, + "Key": "id", + "Type": "NUMBER", + "IsPublic": True, + }, + ] + } + + json = self.category.jsonify(parser)[0] + self.assertEqual(json, expected_json) + + def test_simple_export_with_function(self): + self.category.__class__.jsonify_custom = jsonify_custom + export = self.env["ir.exports"].create( + { + "export_fields": [ + (0, 0, {"name": "name", "instance_method_name": "jsonify_custom"}), + ], + } + ) + + json = self.category.jsonify(export.get_json_parser())[0] + self.assertEqual(json, {"name": "yeah!"}) + + def test_export_relational_display_names(self): + """If we export a relational, we get its display_name in the json.""" + parser = [ + "state_id", + "country_id", + "category_id", + "user_ids", + ] + expected_json = { + "state_id": None, + "country_id": "France", + "category_id": ["Inovator"], + "user_ids": [], + } + + json_partner = self.partner.jsonify(parser, one=True) + + self.assertDictEqual(json_partner, expected_json) + + def test_export_reference_display_names(self): + """Reference work the same as relational""" + menu = self.env.ref("base.menu_action_res_users") + + json_menu = menu.jsonify(["action"], one=True) + + self.assertDictEqual(json_menu, {"action": "Users"}) + + def test_bad_parsers_strict(self): + rec = self.category.with_context(jsonify_record_strict=True) + bad_field_name = ["Name"] + with self.assertRaises(KeyError): + rec.jsonify(bad_field_name, one=True) + + bad_function_name = {"fields": [{"name": "name", "function": "notafunction"}]} + with self.assertRaises(UserError): + rec.jsonify(bad_function_name, one=True) + + bad_subparser = {"fields": [({"name": "name"}, [{"name": "subparser_name"}])]} + with self.assertRaises(UserError): + rec.jsonify(bad_subparser, one=True) + + def test_bad_parsers_fail_gracefully(self): + rec = self.category + + # logging is disabled when testing as it makes too much noise + tools.config["test_enable"] = False + + logger_name = "odoo.addons.jsonifier.models.models" + bad_field_name = ["Name"] + with self.assertLogs(logger=logger_name, level="WARNING") as capt: + rec.jsonify(bad_field_name, one=True) + self.assertIn("res.partner.category.Name not availabl", capt.output[0]) + + bad_function_name = {"fields": [{"name": "name", "function": "notafunction"}]} + with self.assertLogs(logger=logger_name, level="WARNING") as capt: + rec.jsonify(bad_function_name, one=True) + self.assertIn( + "res.partner.category.notafunction not available", capt.output[0] + ) + + bad_subparser = {"fields": [({"name": "name"}, [{"name": "subparser_name"}])]} + with self.assertLogs(logger=logger_name, level="WARNING") as capt: + rec.jsonify(bad_subparser, one=True) + self.assertIn("res.partner.category.name not relational", capt.output[0]) + + tools.config["test_enable"] = True diff --git a/jsonifier/tests/test_helpers.py b/jsonifier/tests/test_helpers.py new file mode 100644 index 00000000000..6c57a4a23aa --- /dev/null +++ b/jsonifier/tests/test_helpers.py @@ -0,0 +1,45 @@ +# Copyright 2021 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.common import TransactionCase + + +class TestJsonifyHelpers(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.partner = cls.env["res.partner"].create( + { + "name": "My Partner", + } + ) + cls.children = cls.env["res.partner"].create( + [ + {"parent_id": cls.partner.id, "name": "Child 1"}, + {"parent_id": cls.partner.id, "name": "Child 2"}, + ] + ) + + def test_helper_m2o_to_id(self): + child = self.children[0] + self.assertEqual( + child._jsonify_m2o_to_id("parent_id"), + child.parent_id.id, + ) + + def test_helper_m2m_to_ids(self): + self.assertEqual( + self.partner._jsonify_x2m_to_ids("child_ids"), + self.partner.child_ids.ids, + ) + + def test_helper_format_duration(self): + # partner_latitude is not intended for this, but it's a float field in core + # any float field does the trick here + self.partner.partner_latitude = 15.5 + self.assertEqual( + self.partner._jsonify_format_duration("partner_latitude"), + "15:30", + ) diff --git a/jsonifier/tests/test_ir_exports_line.py b/jsonifier/tests/test_ir_exports_line.py new file mode 100644 index 00000000000..c83e1f96592 --- /dev/null +++ b/jsonifier/tests/test_ir_exports_line.py @@ -0,0 +1,68 @@ +# Copyright 2017 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestIrExportsLine(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ir_export = cls.env.ref("jsonifier.ir_exp_partner") + + def test_target_constrains(self): + ir_export_lines_model = self.env["ir.exports.line"] + with self.assertRaises(ValidationError): + # The field into the name must be also into the target + ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "name", + "target": "toto:my_target", + } + ) + with self.assertRaises(ValidationError): + # The hierarchy into the target must be the same as the one into + # the name + ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "child_ids/child_ids/name", + "target": "child_ids:children/name", + } + ) + with self.assertRaises(ValidationError): + # The hierarchy into the target must be the same as the one into + # the name and must contains the same fields as into the name + ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "child_ids/child_ids/name", + "target": "child_ids:children/category_id:category/name", + } + ) + line = ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "child_ids/child_ids/name", + "target": "child_ids:children/child_ids:children/name", + } + ) + self.assertTrue(line) + + def test_resolver_function_constrains(self): + resolver = self.env["ir.exports.resolver"].create( + {"python_code": "result = value", "type": "field"} + ) + ir_export_lines_model = self.env["ir.exports.line"] + with self.assertRaises(ValidationError): + # the callable should be an existing model function, but it's not checked + ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "name", + "resolver_id": resolver.id, + "instance_method_name": "function_name", + } + ) diff --git a/jsonifier/views/ir_exports_resolver_view.xml b/jsonifier/views/ir_exports_resolver_view.xml new file mode 100644 index 00000000000..f48124b7a86 --- /dev/null +++ b/jsonifier/views/ir_exports_resolver_view.xml @@ -0,0 +1,26 @@ + + + + ir.exports.resolver + 50 + +
+ + + + + +
+
+
+ + Custom Export Resolvers + ir.exports.resolver + list,form + + +
diff --git a/jsonifier/views/ir_exports_view.xml b/jsonifier/views/ir_exports_view.xml new file mode 100644 index 00000000000..cd0ec26c47e --- /dev/null +++ b/jsonifier/views/ir_exports_view.xml @@ -0,0 +1,38 @@ + + + + ir.exports + 50 + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Export Fields + ir.exports + list,form + + +
From 83e13c82e41bf4f6d054a4acf93358665507acd9 Mon Sep 17 00:00:00 2001 From: Erwan Date: Wed, 21 Jan 2026 15:55:05 +0100 Subject: [PATCH 2/4] [IMP] pre-commit and fix unit test --- jsonifier/README.rst | 16 +++++++----- jsonifier/models/ir_exports_line.py | 12 ++++----- jsonifier/models/ir_exports_resolver.py | 8 +++--- jsonifier/models/models.py | 5 ++-- jsonifier/static/description/index.html | 34 +++++++++++++++---------- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/jsonifier/README.rst b/jsonifier/README.rst index c0661fd352d..b5b971bfe24 100644 --- a/jsonifier/README.rst +++ b/jsonifier/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ========= JSONifier ========= @@ -13,17 +17,17 @@ JSONifier .. |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-LGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github - :target: https://github.com/OCA/server-tools/tree/18.0/jsonifier + :target: https://github.com/OCA/server-tools/tree/19.0/jsonifier :alt: OCA/server-tools .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-jsonifier + :target: https://translation.odoo-community.org/projects/server-tools-19-0/server-tools-19-0-jsonifier :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/server-tools&target_branch=18.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -234,7 +238,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -279,6 +283,6 @@ 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/server-tools `_ project on GitHub. +This module is part of the `OCA/server-tools `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/jsonifier/models/ir_exports_line.py b/jsonifier/models/ir_exports_line.py index 325f4b2b20d..b6dbf925683 100644 --- a/jsonifier/models/ir_exports_line.py +++ b/jsonifier/models/ir_exports_line.py @@ -1,7 +1,7 @@ # Copyright 2017 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import ValidationError @@ -32,7 +32,7 @@ class IrExportsLine(models.Model): def _check_function_resolver(self): for rec in self: if rec.resolver_id and rec.instance_method_name: - msg = _("Either set a function or a resolver, not both.") + msg = self.env._("Either set a function or a resolver, not both.") raise ValidationError(msg) @api.constrains("target", "name") @@ -44,15 +44,15 @@ def _check_target(self): names_with_target = rec.target.split("/") if len(names) != len(names_with_target): raise ValidationError( - _("Name and Target must have the same hierarchy depth") + self.env._("Name and Target must have the same hierarchy depth") ) for name, name_with_target in zip(names, names_with_target, strict=True): field_name = name_with_target.split(":")[0] if name != field_name: raise ValidationError( - _( + self.env._( "The target must reference the same field as in " "name '%(name)s' not in '%(name_with_target)s'" - ) - % dict(name=name, name_with_target=name_with_target) + ), + dict(name=name, name_with_target=name_with_target), ) diff --git a/jsonifier/models/ir_exports_resolver.py b/jsonifier/models/ir_exports_resolver.py index a22b6ea5e70..c535da3cd39 100644 --- a/jsonifier/models/ir_exports_resolver.py +++ b/jsonifier/models/ir_exports_resolver.py @@ -6,12 +6,12 @@ help_message = [ "Compute the result from 'value' by setting the variable 'result'.", - "\n" "For fields resolvers:", + "\nFor fields resolvers:", ":param record: the record", ":param name: name of the field", ":param value: value of the field", ":param field_type: type of the field", - "\n" "For global resolvers:", + "\nFor global resolvers:", ":param value: JSON dict", ":param record: the record", "\n" @@ -42,7 +42,7 @@ def resolve(self, param, records): assert len(param) == len(records) for value, record in zip(param, records, strict=True): values = {"value": value, "record": record, "context": context} - safe_eval(self.python_code, values, mode="exec", nocopy=True) + safe_eval(self.python_code, values, mode="exec") result.append(values["result"]) else: # param is a field for record in records: @@ -53,6 +53,6 @@ def resolve(self, param, records): "field_type": param.type, "context": context, } - safe_eval(self.python_code, values, mode="exec", nocopy=True) + safe_eval(self.python_code, values, mode="exec") result.append(values["result"]) return result diff --git a/jsonifier/models/models.py b/jsonifier/models/models.py index 55ae11cdf7e..29f0cb56468 100644 --- a/jsonifier/models/models.py +++ b/jsonifier/models/models.py @@ -10,7 +10,6 @@ from odoo import api, fields, models, tools from odoo.exceptions import UserError from odoo.tools.misc import format_duration -from odoo.tools.translate import _ from ..exceptions import SwallableException from .utils import convert_simple_to_full_parser @@ -28,7 +27,9 @@ def __parse_field(self, parser_field): @api.model def _jsonify_bad_parser_error(self, field_name): - raise UserError(_("Wrong parser configuration for field: `%s`") % field_name) + raise UserError( + self.env._("Wrong parser configuration for field: `%s`", field_name) + ) def _function_value(self, record, function, field_name): if function in dir(record): diff --git a/jsonifier/static/description/index.html b/jsonifier/static/description/index.html index 9044815608d..ef88f8cf774 100644 --- a/jsonifier/static/description/index.html +++ b/jsonifier/static/description/index.html @@ -3,7 +3,7 @@ -JSONifier +README.rst -
-

JSONifier

+
+ + +Odoo Community Association + +
+

JSONifier

-

Beta License: LGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

Beta License: LGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

This module adds a ‘jsonify’ method to every model of the ORM. It works on the current recordset and requires a single argument ‘parser’ that specify the field to extract.

@@ -522,9 +527,9 @@

JSONifier

-

Usage

+

Usage

-

with_fieldname parameter

+

with_fieldname parameter

The with_fieldname option of jsonify() method, when true, will inject on the same level of the data “_fieldname_$field” keys that will contain the field name, in the language of the current user.

@@ -551,17 +556,17 @@

with_fieldname parameter

-

Bug Tracker

+

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.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Akretion
  • ACSONE
  • @@ -569,7 +574,7 @@

    Authors

-

Contributors

+

Contributors

-

Other credits

+

Other credits

The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp.

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -595,10 +600,11 @@

Maintainers

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/server-tools project on GitHub.

+

This module is part of the OCA/server-tools project on GitHub.

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

+
From ad47fd4c6ad95efb159932c6ffb6b4c0d6dfda37 Mon Sep 17 00:00:00 2001 From: Erwan Date: Fri, 6 Feb 2026 10:55:43 +0100 Subject: [PATCH 3/4] [IMP] remove demo data xml for unit test --- jsonifier/__manifest__.py | 5 --- jsonifier/demo/export_demo.xml | 7 --- jsonifier/demo/ir.exports.line.csv | 16 ------- jsonifier/demo/resolver_demo.xml | 12 ----- jsonifier/models/ir_exports_line.py | 7 +-- jsonifier/tests/__init__.py | 1 + jsonifier/tests/test_data_setup.py | 60 +++++++++++++++++++++++++ jsonifier/tests/test_get_parser.py | 14 ++---- jsonifier/tests/test_helpers.py | 5 ++- jsonifier/tests/test_ir_exports_line.py | 16 ++++--- 10 files changed, 82 insertions(+), 61 deletions(-) delete mode 100644 jsonifier/demo/export_demo.xml delete mode 100644 jsonifier/demo/ir.exports.line.csv delete mode 100644 jsonifier/demo/resolver_demo.xml create mode 100644 jsonifier/tests/test_data_setup.py diff --git a/jsonifier/__manifest__.py b/jsonifier/__manifest__.py index 69f296199d3..d6fccb8370f 100644 --- a/jsonifier/__manifest__.py +++ b/jsonifier/__manifest__.py @@ -18,9 +18,4 @@ "views/ir_exports_view.xml", "views/ir_exports_resolver_view.xml", ], - "demo": [ - "demo/resolver_demo.xml", - "demo/export_demo.xml", - "demo/ir.exports.line.csv", - ], } diff --git a/jsonifier/demo/export_demo.xml b/jsonifier/demo/export_demo.xml deleted file mode 100644 index a060d300266..00000000000 --- a/jsonifier/demo/export_demo.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - Partner Export - res.partner - - diff --git a/jsonifier/demo/ir.exports.line.csv b/jsonifier/demo/ir.exports.line.csv deleted file mode 100644 index 4744c11ea0c..00000000000 --- a/jsonifier/demo/ir.exports.line.csv +++ /dev/null @@ -1,16 +0,0 @@ -id,export_id/id,name -name,ir_exp_partner,name -active,ir_exp_partner,active -partner_latitude,ir_exp_partner,partner_latitude -color,ir_exp_partner,color -category_id_name,ir_exp_partner,category_id/name -country_id_name,ir_exp_partner,country_id/name -country_id_code,ir_exp_partner,country_id/code -child_ids_name,ir_exp_partner,child_ids/name -child_ids_id,ir_exp_partner,child_ids/id -child_ids_email,ir_exp_partner,child_ids/email -child_ids_country_id_name,ir_exp_partner,child_ids/country_id/name -child_ids_country_id_code,ir_exp_partner,child_ids/country_id/code -child_ids_child_ids_name,ir_exp_partner,child_ids/child_ids/name -lang,ir_exp_partner,lang -comment,ir_exp_partner,comment diff --git a/jsonifier/demo/resolver_demo.xml b/jsonifier/demo/resolver_demo.xml deleted file mode 100644 index 540302be23d..00000000000 --- a/jsonifier/demo/resolver_demo.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - ExtraData dictionary (number/text) - -is_number = field_type in ('integer', 'float') -ftype = "NUMBER" if is_number else "TEXT" -value = value if is_number else str(value) -result = {"Key": name, "Value": value, "Type": ftype, "IsPublic": True} - - - diff --git a/jsonifier/models/ir_exports_line.py b/jsonifier/models/ir_exports_line.py index b6dbf925683..049a0312175 100644 --- a/jsonifier/models/ir_exports_line.py +++ b/jsonifier/models/ir_exports_line.py @@ -52,7 +52,8 @@ def _check_target(self): raise ValidationError( self.env._( "The target must reference the same field as in " - "name '%(name)s' not in '%(name_with_target)s'" - ), - dict(name=name, name_with_target=name_with_target), + "name '%(name)s' not in '%(name_with_target)s'", + name=name, + name_with_target=name_with_target, + ) ) diff --git a/jsonifier/tests/__init__.py b/jsonifier/tests/__init__.py index 3402bb15298..abc56bc947c 100644 --- a/jsonifier/tests/__init__.py +++ b/jsonifier/tests/__init__.py @@ -1,3 +1,4 @@ +from . import test_data_setup from . import test_get_parser from . import test_helpers from . import test_ir_exports_line diff --git a/jsonifier/tests/test_data_setup.py b/jsonifier/tests/test_data_setup.py new file mode 100644 index 00000000000..ff0bb4fb328 --- /dev/null +++ b/jsonifier/tests/test_data_setup.py @@ -0,0 +1,60 @@ +class JsonifierTestDataMixin: + @classmethod + def _create_demo_export_class(cls): + cls.ir_exp_partner = cls.env["ir.exports"].create( + { + "name": "Partner Export", + "resource": "res.partner", + } + ) + return cls.ir_exp_partner + + @classmethod + def _create_demo_export_lines_class(cls): + export_lines_data = [ + {"name": "name"}, + {"name": "active"}, + {"name": "partner_latitude"}, + {"name": "color"}, + {"name": "category_id/name"}, + {"name": "country_id/name"}, + {"name": "country_id/code"}, + {"name": "child_ids/name"}, + {"name": "child_ids/id"}, + {"name": "child_ids/email"}, + {"name": "child_ids/country_id/name"}, + {"name": "child_ids/country_id/code"}, + {"name": "child_ids/child_ids/name"}, + {"name": "lang"}, + {"name": "comment"}, + ] + + export_lines = cls.env["ir.exports.line"] + + for line_data in export_lines_data: + line_data["export_id"] = cls.ir_exp_partner.id + line = cls.env["ir.exports.line"].create(line_data) + export_lines |= line + + return export_lines + + @classmethod + def _create_demo_resolver_class(cls): + python_code = """is_number = field_type in ('integer', 'float') +ftype = "NUMBER" if is_number else "TEXT" +value = value if is_number else str(value) +result = {"Key": name, "Value": value, "Type": ftype, "IsPublic": True}""" + + cls.ir_exports_resolver_dict = cls.env["ir.exports.resolver"].create( + { + "name": "ExtraData dictionary (number/text)", + "python_code": python_code, + } + ) + return cls.ir_exports_resolver_dict + + @classmethod + def setUpClass_demo_data(cls): + cls._create_demo_export_class() + cls._create_demo_export_lines_class() + cls._create_demo_resolver_class() diff --git a/jsonifier/tests/test_get_parser.py b/jsonifier/tests/test_get_parser.py index 0f587ec5a48..0d33dc3262a 100644 --- a/jsonifier/tests/test_get_parser.py +++ b/jsonifier/tests/test_get_parser.py @@ -9,16 +9,18 @@ from odoo.tests.common import TransactionCase from ..models.utils import convert_simple_to_full_parser +from .test_data_setup import JsonifierTestDataMixin def jsonify_custom(self, field_name): return "yeah!" -class TestParser(TransactionCase): +class TestParser(TransactionCase, JsonifierTestDataMixin): @classmethod def setUpClass(cls): super().setUpClass() + cls.setUpClass_demo_data() # disable tracking test suite wise cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls.env.user.tz = "Europe/Brussels" @@ -104,16 +106,8 @@ def test_getting_parser(self): "comment", ] - exporter = self.env.ref("jsonifier.ir_exp_partner") - parser = exporter.get_json_parser() - expected_full_parser = convert_simple_to_full_parser(expected_parser) - self.assertEqual(parser, expected_full_parser) + exporter = self.ir_exp_partner - # modify an ir.exports_line to put a target for a field - self.env.ref("jsonifier.category_id_name").write( - {"target": "category_id:category/name"} - ) - expected_parser[4] = ("category_id:category", ["name"]) parser = exporter.get_json_parser() expected_full_parser = convert_simple_to_full_parser(expected_parser) self.assertEqual(parser, expected_full_parser) diff --git a/jsonifier/tests/test_helpers.py b/jsonifier/tests/test_helpers.py index 6c57a4a23aa..2c60b4db790 100644 --- a/jsonifier/tests/test_helpers.py +++ b/jsonifier/tests/test_helpers.py @@ -4,11 +4,14 @@ from odoo.tests.common import TransactionCase +from .test_data_setup import JsonifierTestDataMixin -class TestJsonifyHelpers(TransactionCase): + +class TestJsonifyHelpers(TransactionCase, JsonifierTestDataMixin): @classmethod def setUpClass(cls): super().setUpClass() + cls.setUpClass_demo_data() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls.partner = cls.env["res.partner"].create( { diff --git a/jsonifier/tests/test_ir_exports_line.py b/jsonifier/tests/test_ir_exports_line.py index c83e1f96592..b4660f10808 100644 --- a/jsonifier/tests/test_ir_exports_line.py +++ b/jsonifier/tests/test_ir_exports_line.py @@ -4,12 +4,14 @@ from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase +from .test_data_setup import JsonifierTestDataMixin -class TestIrExportsLine(TransactionCase): + +class TestIrExportsLine(TransactionCase, JsonifierTestDataMixin): @classmethod def setUpClass(cls): super().setUpClass() - cls.ir_export = cls.env.ref("jsonifier.ir_exp_partner") + cls.setUpClass_demo_data() def test_target_constrains(self): ir_export_lines_model = self.env["ir.exports.line"] @@ -17,7 +19,7 @@ def test_target_constrains(self): # The field into the name must be also into the target ir_export_lines_model.create( { - "export_id": self.ir_export.id, + "export_id": self.ir_exp_partner.id, "name": "name", "target": "toto:my_target", } @@ -27,7 +29,7 @@ def test_target_constrains(self): # the name ir_export_lines_model.create( { - "export_id": self.ir_export.id, + "export_id": self.ir_exp_partner.id, "name": "child_ids/child_ids/name", "target": "child_ids:children/name", } @@ -37,14 +39,14 @@ def test_target_constrains(self): # the name and must contains the same fields as into the name ir_export_lines_model.create( { - "export_id": self.ir_export.id, + "export_id": self.ir_exp_partner.id, "name": "child_ids/child_ids/name", "target": "child_ids:children/category_id:category/name", } ) line = ir_export_lines_model.create( { - "export_id": self.ir_export.id, + "export_id": self.ir_exp_partner.id, "name": "child_ids/child_ids/name", "target": "child_ids:children/child_ids:children/name", } @@ -60,7 +62,7 @@ def test_resolver_function_constrains(self): # the callable should be an existing model function, but it's not checked ir_export_lines_model.create( { - "export_id": self.ir_export.id, + "export_id": self.ir_exp_partner.id, "name": "name", "resolver_id": resolver.id, "instance_method_name": "function_name", From 73b03542d05c300fa69682d048780d3e866aa30f Mon Sep 17 00:00:00 2001 From: Erwan Date: Mon, 9 Feb 2026 09:44:40 +0100 Subject: [PATCH 4/4] [MIG] jsonifier : Migration to 19.0 --- jsonifier/README.rst | 5 +++-- jsonifier/readme/CONTRIBUTORS.md | 1 + jsonifier/readme/CREDITS.md | 2 +- jsonifier/static/description/index.html | 5 +++-- jsonifier/tests/test_ir_exports_line.py | 1 + 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/jsonifier/README.rst b/jsonifier/README.rst index b5b971bfe24..60633f54567 100644 --- a/jsonifier/README.rst +++ b/jsonifier/README.rst @@ -263,12 +263,13 @@ Contributors - Iván Todorovich - Nguyen Minh Chien - Thien Vo +- Mangono team Other credits ------------- -The migration of this module from 17.0 to 18.0 was financially supported -by Camptocamp. +The migration of this module from 18.0 to 19.0 was financially supported +by Mangono. Maintainers ----------- diff --git a/jsonifier/readme/CONTRIBUTORS.md b/jsonifier/readme/CONTRIBUTORS.md index 4323549dad8..953d2f19f2d 100644 --- a/jsonifier/readme/CONTRIBUTORS.md +++ b/jsonifier/readme/CONTRIBUTORS.md @@ -6,3 +6,4 @@ - Iván Todorovich \<\> - Nguyen Minh Chien \<\> - Thien Vo \<\> +- Mangono team \<\> diff --git a/jsonifier/readme/CREDITS.md b/jsonifier/readme/CREDITS.md index 83b3ec91f7d..514b5a21b3b 100644 --- a/jsonifier/readme/CREDITS.md +++ b/jsonifier/readme/CREDITS.md @@ -1 +1 @@ -The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp. +The migration of this module from 18.0 to 19.0 was financially supported by Mangono. diff --git a/jsonifier/static/description/index.html b/jsonifier/static/description/index.html index ef88f8cf774..876c5f2324c 100644 --- a/jsonifier/static/description/index.html +++ b/jsonifier/static/description/index.html @@ -584,12 +584,13 @@

Contributors

  • Iván Todorovich <ivan.todorovich@camptocamp.com>
  • Nguyen Minh Chien <chien@trobz.com>
  • Thien Vo <thienvh@trobz.com>
  • +
  • Mangono team <opensource@mangono.fr>
  • Other credits

    -

    The migration of this module from 17.0 to 18.0 was financially supported -by Camptocamp.

    +

    The migration of this module from 18.0 to 19.0 was financially supported +by Mangono.

    Maintainers

    diff --git a/jsonifier/tests/test_ir_exports_line.py b/jsonifier/tests/test_ir_exports_line.py index b4660f10808..fbf680a5022 100644 --- a/jsonifier/tests/test_ir_exports_line.py +++ b/jsonifier/tests/test_ir_exports_line.py @@ -11,6 +11,7 @@ class TestIrExportsLine(TransactionCase, JsonifierTestDataMixin): @classmethod def setUpClass(cls): super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) cls.setUpClass_demo_data() def test_target_constrains(self):