[ADD] report_carbone, jsonifier, export_json : carbone is an alternative to Py3o

This commit is contained in:
2026-04-21 14:59:24 +02:00
parent ae3c34257f
commit c2061984d1
216 changed files with 29344 additions and 0 deletions

1
export_json/README.md Normal file
View File

@@ -0,0 +1 @@
JSON Export system for Odoo Community edition.

47
export_json/README.rst Normal file
View File

@@ -0,0 +1,47 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
JSON Export
===========
JSON Export system for Odoo 18 Community edition.
Configuration
=============
No configuration
Company
-------
* `Mangono <https://mangono.fr/>`__
License
-------
General Public License, Version 3 (LGPL v3).
(http://www.gnu.org/licenses/lgpl-3.0-standalone.html)
Credits
-------
Developer: (V18) Mangono , Contact: contact@mangono.fr
Contacts
--------
* Mail Contact : contact@mangono.fr
* Website : https://mangono.fr
Bug Tracker
-----------
In case of trouble, please contact us via our email address.
Maintainer
==========
.. image:: static/description/assets/logo/mangono-logo-bleu.png
:scale: 10 %
:target: https://mangono.fr
This module is maintained by Mangono.
For support and more information, please visit `Our Website <https://mangono.fr/>`__
Further information
===================
HTML Description: `<static/description/index.html>`__

2
export_json/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from . import models
from . import controller

View File

@@ -0,0 +1,20 @@
{
"name": "JSON Export",
"version": "18.0.1.0.5",
"category": "Extra Tools",
"license": "AGPL-3",
"summary": "Add JSON export to all models similar to XLSX or CSV exports.",
"author": "Mangono",
"maintainers": "Mangono",
"support": "contact@mangono.fr",
"description": """
JSON Export
===========
see README
""",
"website": "https://mangono.fr/",
"depends": ["base", "web", "jsonifier"],
"data": [],
"installable": True,
"images": ["static/description/banner.png"],
}

View File

@@ -0,0 +1 @@
from . import main

View File

@@ -0,0 +1,432 @@
import json
import logging
import operator
from typing import Any
import requests
from odoo import api, http, models
from odoo.http import content_disposition, request
from odoo.tools import json_default, osutil
from odoo.addons.web.controllers.export import (
Export,
ExportFormat,
)
_logger = logging.getLogger(__name__)
class JsonExport(Export):
@http.route("/web/export/formats", type="json", auth="user")
def formats(self):
res = super().formats()
res.append({"tag": "json", "label": "JSON"})
return res
class JsonExportFormat(ExportFormat, http.Controller):
@http.route("/web/export/json", type="http", auth="user")
def index(self, data: str) -> "requests.Response":
return self.base(data)
@property
def content_type(self) -> str:
"""
:return: str, json format
"""
return "text/json;charset=utf8"
@property
def extension(self) -> str:
"""
Return extension for file, that will be generated later.
:return: json extension
"""
return ".json"
@api.model
def format_data(self, fields) -> str:
for field in fields:
for key, value in field.items():
if type(value) is bytes:
field.update({key: "data:image/png;base64," + value.decode("utf-8")})
return json.dumps(fields, indent=4, default=json_default)
@api.model
def _insert_simple_field(self, fields_name: list, field_groups: dict, parser: list[dict]):
"""
This function is used to add "simple" field like "partner_id", "name", in the parser that will be export.
Field name like "partner_id/child_ids, partner_id/child_ids/name" will be processed in another method.
:param fields_name: list of field name to export, display like ["partner_id", "partner_id/child_ids", ...].
:param field_groups: The dict is not return
:param parser: list of key's name field, define in a specific way
(exemple in function "define_parser" docstring).
:return: None, but "field_groups" and "parser" parameters are modified by side effect.
"""
for field in fields_name:
name = field["name"]
if "/" in name:
parts = name.split("/")
main_field = parts[0]
subfield = "/".join(parts[1:])
if main_field not in field_groups:
field_groups[main_field] = []
field_groups[main_field].append({"name": subfield})
else:
parser.append(name)
@api.model
def _insert_nested_field(self, field_groups, parser):
for main_field, subfields in field_groups.items():
if any("/" in subfield["name"] for subfield in subfields):
nested_fields = [subfield for subfield in subfields]
nested_parser = self.define_parser(nested_fields)
parser.append((main_field, nested_parser))
else:
sub_names = [subfield["name"] for subfield in subfields]
parser.append((main_field, sub_names))
@api.model
def define_parser(self, fields_name: list[dict[str, str]]) -> tuple[str | dict[str, Any]]:
"""
Args:
:param fields_name: list of dictionaries with two or three keys: 'label', 'name' which are field names
and optionally 'type', that is not use.
:return: list of field names to export. Example of parser:
parser = [
'name',
'number',
'create_date',
('partner_id', ['id', 'display_name', 'ref'])
('line_id', ['id', ('product_id', ['name']), 'price_unit'])
]
"""
parser = []
field_groups = {}
self._insert_simple_field(fields_name, field_groups, parser)
self._insert_nested_field(field_groups, parser)
return parser
def base(self, data: str) -> "requests.Response":
params = json.loads(data)
model, fields, ids, domain, import_compat = operator.itemgetter(
"model", "fields", "ids", "domain", "import_compat"
)(params)
Model = request.env[model].with_context(import_compat=import_compat, **params.get("context", {}))
response_data = self.perform_json_export(domain, fields, ids, Model)
return request.make_response(
response_data,
headers=[
(
"Content-Disposition",
content_disposition(osutil.clean_filename(self.filename(model) + self.extension)),
),
("Content-Type", self.content_type),
],
)
@api.model
def _convert_simple_list(self, fields_name) -> list[dict[str, str]]:
"""
Use to convert a list of fields names that it is not a dict with 'name' key
:param fields_name: list of field names, either str or dict.
:return: converted list of field names in dict{'name' : 'field_name'}.
"""
converted_fields_name = []
for element in fields_name:
if type(element) is not dict:
element_to_append = {"name": element}
else:
element_to_append = element
converted_fields_name.append(element_to_append)
return converted_fields_name
# region merge_lang_dict
@api.model
def process_keys(
self,
all_keys: set,
dicts_at_idx: dict,
merged_dict: dict,
parent_key: str = False,
):
"""
Traite un ensemble de clés en regroupant leurs valeurs depuis plusieurs dictionnaires par langue.
La méthode itère sur toutes les clés fournies et collecte leurs valeurs correspondantes
depuis différents dictionnaires organisés par langue. Pour chaque clé, on rassemble
les valeurs disponibles dans chaque langue et les transmet à "process_key_values" pour
un traitement ultérieur.
"""
for key in all_keys:
key_values = {}
for lang, d in dicts_at_idx.items():
if key in d:
key_values[lang] = d[key]
self.process_key_values(key, key_values, merged_dict, parent_key)
@api.model
def process_key_values(
self,
current_key: str,
key_values: dict,
merged_item: dict,
parent_key: str = False,
):
if key_values:
first_val = list(key_values.values())[0]
item_added = self.add_list_or_dict_on_merge_item(
current_key, first_val, key_values, merged_item, parent_key
)
if not item_added:
self.add_lang_key(current_key, first_val, key_values, merged_item, parent_key)
@api.model
def add_list_or_dict_on_merge_item(
self,
current_key,
current_val: list | dict,
values_by_lang: dict,
merged_item: dict,
parent_key: str = False,
) -> bool:
"""Permets de différencier l'ajout de dict ou de list dans le dictionnaire final.
Le paramètre merged_item est modifié par référence.
Retourne un boolean qui permet de ne pas continuer le process d'ajouts de données,
si la valeur courante a déjà été ajoutée.
"""
# Si la clé courante et la clé parente sont définies, qu'elles ne sont pas identiques,
# et que la valeur courante est un dict (ou une list) ?
# Alors la clé parente devient la concaténation de la clé parente et courante,
# étant donnée que nous sommes dans des dicts imbriquées
if parent_key and current_key and parent_key != current_key and isinstance(current_val, dict):
parent_key = f"{parent_key}/{current_key}"
else:
parent_key = parent_key or current_key
# Gérer les listes de dictionnaires
if isinstance(current_val, list):
if current_val and isinstance(current_val[0], dict):
merged_item[current_key] = self._merge_list_of_dicts(values_by_lang, parent_key)
return True
# Gérer les dictionnaires imbriqués
elif isinstance(current_val, dict):
merged_item[current_key] = self._merge_nested_dict(values_by_lang, parent_key)
return True
return False
@api.model
def retrieve_all_keys(self, items: dict) -> set:
assert all(isinstance(v, dict) for v in items.values()), "All values must be dictionaries."
all_keys = set()
for vals in items.values():
all_keys.update(vals.keys())
return all_keys
@api.model
def add_unique_vals(self, key_values: dict) -> set:
"""Vérifier si toutes les valeurs sont identiques"""
unique_values = set()
for val in key_values.values():
if isinstance(val, list | dict):
unique_values.add(str(val))
else:
unique_values.add(val)
return unique_values
@api.model
def get_country_code(self, lang_code):
if "_" in lang_code:
return lang_code.split("_")[1]
else:
return lang_code
@api.model
def add_lang_key(
self,
current_key: str,
first_val: str | list,
key_values: dict,
merged_dict: dict,
parent_key: str = False,
):
"""Permets d'ajouter au dict final, soit une valeur avec une clé unique, s'il n'existe pas
de différence dans les dict de chaque langue, ou bien une clé par lang, s'il y a des différences.
"""
# Vérifier si toutes les valeurs sont identiques
unique_values = self.add_unique_vals(key_values)
if len(unique_values) <= 1:
# Valeurs identiques
merged_dict[current_key] = first_val
else:
# On ajoute un ensemble {key : val}, avec pour key la clé courante sans ajout, et pour valeur,
# le chemin de la key.
if parent_key:
merged_dict[current_key] = f"{parent_key}/{current_key}"
else:
merged_dict[current_key] = current_key
# Valeurs différentes, créer des clés avec suffixes, qui serviront provisoirement à stocker les traductions
# de la clé
for lang, val in key_values.items():
lang_suffix = self.get_country_code(lang)
merged_dict[f"{current_key}_{lang_suffix}"] = val
@api.model
def merge_multilingual_dicts(self, multilingual_data: dict) -> list:
"""
Fusionne les dictionnaires de différentes langues en un seul dictionnaire.
Pour chaque clé:
- Si toutes les valeurs sont identiques -> garde la clé originale
- Si les valeurs diffèrent -> crée des clés suffixées par le code langue
- Si la valeur est une liste de dicts -> fusionne récursivement
- Si la valeur est un dict -> fusionne récursivement
"""
languages = list(multilingual_data.keys())
if not languages:
return []
base_lang = languages[0]
base_data = multilingual_data[base_lang]
merged_data = []
for idx, base_item in enumerate(base_data):
merged_item = {}
# Récupérer tous les éléments correspondants dans les autres langues
lang_items = {base_lang: base_item}
for lang in languages[1:]:
if idx < len(multilingual_data[lang]):
lang_items[lang] = multilingual_data[lang][idx]
# Analyser chaque clé
all_keys = self.retrieve_all_keys(lang_items)
# Récupérer les valeurs pour cette clé dans toutes les langues
self.process_keys(all_keys, lang_items, merged_item)
merged_data.append(merged_item)
return merged_data
@api.model
def _merge_nested_dict(self, values_by_lang: dict, parent_key: str = False) -> dict:
"""
Fusionne des dictionnaires imbriqués provenant de différentes langues.
Args:
parent_key: str, key parent du dict imbriqué
values_by_lang: dict {lang_code: dict}
Returns:
dict: dictionnaire fusionné
"""
merged_dict = {}
# Récupérer toutes les clés possibles
all_keys = self.retrieve_all_keys(values_by_lang)
# Pour chaque clé, comparer les valeurs
for key in all_keys:
key_values = {}
for lang, d in values_by_lang.items():
if isinstance(d, dict) and key in d:
key_values[lang] = d[key]
if key_values:
# Modification du paramètre merged_dict par référence
first_val = list(key_values.values())[0]
self.add_lang_key(key, first_val, key_values, merged_dict, parent_key)
return merged_dict
@api.model
def _merge_list_of_dicts(self, values_by_lang: dict, parent_key: str = False) -> list[dict]:
"""
Fusionne des listes de dictionnaires provenant de différentes langues.
Args:
values_by_lang: dict {lang_code: [dict1, dict2, ...]}
parent_key : str
Returns:
list: liste fusionnée de dictionnaires
"""
languages = list(values_by_lang.keys())
if not languages:
return []
# Utiliser la première langue comme référence pour la longueur
base_lang = languages[0]
base_list = values_by_lang[base_lang]
merged_list = []
# Pour chaque index dans la liste
for idx in range(len(base_list)):
merged_dict = {}
# Récupérer tous les dictionnaires à cet index
dicts_at_idx = {}
for lang in languages:
if idx < len(values_by_lang[lang]):
dicts_at_idx[lang] = values_by_lang[lang][idx]
# Récupérer toutes les clés possibles
all_keys = self.retrieve_all_keys(dicts_at_idx)
self.process_keys(all_keys, dicts_at_idx, merged_dict, parent_key)
merged_list.append(merged_dict)
return merged_list
# endregion merge_lang_dict
def perform_json_export(
self,
domain: list[list[Any]],
field_names: list[str],
ids: list[int],
model: models.BaseModel,
langs_code: list[str] = False,
) -> str:
"""
Method to prepare and format json data for the export.
Args:
:param domain: condition (domain) needed for the search method
:param field_names: list of dictionaries with two keys : 'label' and 'name' which are field names
:param ids: list with ids of selected records
:param model: model on which the export is done
:param langs_code: languages code
:return: string with json_data
"""
if not model._is_an_ordinary_table():
field_names = [field for field in field_names if field != "id"]
field_names = self._convert_simple_list(field_names)
records = model.browse(ids) if ids else model.search(domain, offset=0, limit=False, order=False)
parser = self.define_parser(field_names)
if not langs_code:
langs_code = [request.env.context.get("lang")] or [request.env.user.lang]
tz = request.env.context.get("tz") or request.env.user.tz
if len(langs_code) == 1:
result = records.with_context(lang=langs_code[0], tz=tz).jsonify(parser)
return self.format_data(result)
result = {}
for lang in langs_code:
result[lang] = records.with_context(lang=lang, tz=tz).jsonify(parser)
merged_list = self.merge_multilingual_dicts(result)
response_data = self.format_data(merged_list)
return response_data

View File

@@ -0,0 +1,25 @@
= Guide d'utilisation de l'export JSON
== Présentation de la fonctionnalité
L'export JSON permet, comme son nom l'indique, d'exporter les données d'un record en format JSON.
Ce type d'export peut être utilisé par exemple si le connecteur Carbone.io est utilisé, car il a besoin de recevoir les données à intégrer au document sous le format JSON.
== Utilisation de la fonctionnalité
- Dans l'interface Odoo, aller sur une vue Liste > sélectionner au moins un record :
image::images/process_export1.png[]
- Ensuite, cliquer sur _Action_ -> _Exporter_ , cette action ouvre un popup :
image::images/export_json2.png[]
image::images/export_json3_popup_export.png[]
- Sélectionner le format d'exportation de type _JSON_ , insérer tous les champs à exporter, créer un modèle d'export et le sauvegarder :
image::images/export_json_popup_export2.png[]
- Cliquer sur _Exporter_ , le fichier en format JSON est téléchargé.

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -0,0 +1 @@
from . import file_json

View File

@@ -0,0 +1,20 @@
import json
from odoo import models
from odoo.addons.base_import import models as model_import
model_import.base_import.FILE_TYPE_DICT["application/json"] = ("json", True, None)
model_import.base_import.EXTENSIONS[".json"] = True
class BaseImportJSON(models.TransientModel):
_inherit = "base_import.import"
def _read_json(self, record, options):
items = json.loads(record.file)
if items:
headers = items[0].keys()
yield headers
for item in items:
yield [item[header] for header in headers]

View File

@@ -0,0 +1,8 @@
[build-system]
requires = ["addon-odoo-wheel>=0.4.2"]
build-backend = "addon_odoo_wheel.builder"
[tool.addon-odoo-wheel]
dependencies = [
"odoo-addon-jsonifier~=18.0",
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,3 @@
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle cx="12" cy="12" r="11" stroke="#01FFCC" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 193 B

View File

@@ -0,0 +1,3 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle cx="9.5" cy="9.5" r="9.5" fill="#01FFCC"/>
</svg>

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSON Export - Odoo v18</title>
<!-- <link rel="stylesheet" href="/export_json/static/src/css/description.css">-->
</head>
<body>
<div class="container" style="font-family: 'Inter', sans-serif; line-height: 1.7; color: #001E2B;
background-color: #ffffff; max-width: 1200px; margin: 0 auto; padding: 20px;">
<!-- Header -->
<div class="header"
style="background-color: #001e2b; color: #f5f3ed; padding: 80px 60px; margin-bottom: 60px; text-align: center;">
<table class="header-title-table" style="width: 100%; margin-bottom: 20px;">
<tr>
<td class="header-title-left"
style="text-align: right; vertical-align: middle; padding-right: 40px;">
<h1 class="header-title"
style="color: #01FFCC; font-size: 3em; font-weight: 700; letter-spacing: -0.02em; margin: 0;">
JSON Export
</h1>
</td>
<td class="header-title-right" style="text-align: left; vertical-align: middle;">
<img src="assets/logo/mangono-logo.png" alt="Mangono Logo" class="header-logo"
style="width: 300px; max-width: 100%; object-fit: contain;">
</td>
</tr>
</table>
<span class="header-badge"
style="display: inline-block; border: 1px solid rgba(245,243,237,0.3); padding: 10px 24px; margin: 10px 8px; font-size: 0.85em; letter-spacing: 0.05em; text-transform: uppercase; color: #f5f3ed;">
Free Module
</span>
<span class="header-badge"
style="display: inline-block; border: 1px solid rgba(245,243,237,0.3); padding: 10px 24px; margin: 10px 8px; font-size: 0.85em; letter-spacing: 0.05em; text-transform: uppercase; color: #f5f3ed;">
Odoo v18
</span>
</div>
<!-- Description -->
<div class="section" style="margin-bottom: 80px;">
<div class="section-title-wrapper" style="margin-bottom: 40px;">
<div class="section-title-line"
style="background-color: #01ffcc; height: 16px; width: 310px; margin-bottom: 12px;"></div>
<h2 class="section-title"
style="color: #001E2B; font-size: 2.2em; font-weight: 700; letter-spacing: -0.01em; margin: 0; border-bottom: 1px solid #001E2B; padding-bottom: 15px;">
Description
</h2>
<img src="assets/svg/green-dot.svg" alt="green-dot" class="section-title-dot"
style="display: block; margin-top: 8px;">
</div>
<p class="section-text" style="font-size: 1.1em; line-height: 1.8;">
<strong>JSON Export</strong>
is a module that adds the JSON option as an export format inside Odoos native export tool.
</p>
</div>
<!-- Screenshots Section -->
<div class="section" style="margin-bottom: 80px;">
<div class="section-title-wrapper" style="margin-bottom: 40px;">
<div class="section-title-line"
style="background-color: #01ffcc; height: 16px; width: 310px; margin-bottom: 12px;"></div>
<h2 class="section-title"
style="color: #001E2B; font-size: 2.2em; font-weight: 700; letter-spacing: -0.01em; margin: 0; border-bottom: 1px solid #001E2B; padding-bottom: 15px;">
Screenshot
</h2>
<img src="assets/svg/green-dot.svg" alt="green-dot" class="section-title-dot"
style="display: block; margin-top: 8px;">
</div>
<div class="screenshot-wrapper" style="text-align: center; margin: 30px 0;">
<img src="assets/screenshots/pop-up-export.png" class="screenshot-image"
style="width: 100%; max-width: 100%; object-fit: contain;">
</div>
</div>
<!-- Installation -->
<div class="section" style="margin-bottom: 80px;">
<div class="section-title-wrapper" style="margin-bottom: 40px;">
<div class="section-title-line"
style="background-color: #01ffcc; height: 16px; width: 310px; margin-bottom: 12px;"></div>
<h2 class="section-title"
style="color: #001E2B; font-size: 2.2em; font-weight: 700; letter-spacing: -0.01em; margin: 0; border-bottom: 1px solid #001E2B; padding-bottom: 15px;">
Installation &amp; Configuration
</h2>
<img src="assets/svg/green-dot.svg" alt="green-dot" class="section-title-dot"
style="display: block; margin-top: 8px;">
</div>
<h3 style="color: #001E2B; font-size: 1.4em; margin-top: 40px; margin-bottom: 20px; font-weight: 500;">
1. Installation
</h3>
<p>Install the module from the Odoo Store.</p>
<h3 style="color: #001E2B; font-size: 1.4em; margin-top: 40px; margin-bottom: 20px; font-weight: 500;">
2. Configuration
</h3>
<p>No specific configuration required.</p>
<h3 style="color: #001E2B; font-size: 1.4em; margin-top: 40px; margin-bottom: 20px; font-weight: 500;">
3. Usage
</h3>
<p>When using Odoos export feature, the “JSON” option will appear in “Export Format”.</p>
</div>
<!-- Technical Specifications -->
<div class="section" style="margin-bottom: 80px;">
<div class="section-title-wrapper" style="margin-bottom: 40px;">
<div class="section-title-line"
style="background-color: #01ffcc; height: 16px; width: 310px; margin-bottom: 12px;"></div>
<h2 class="section-title"
style="color: #001E2B; font-size: 2.2em; font-weight: 700; letter-spacing: -0.01em; margin: 0; border-bottom: 1px solid #001E2B; padding-bottom: 15px;">
Technical Specifications
</h2>
<img src="assets/svg/green-dot.svg" alt="green-dot" class="section-title-dot"
style="display: block; margin-top: 8px;">
</div>
<div class="tech-specs"
style="background-color: #ffffff; padding: 30px; margin-top: 30px; border: 1px solid #e8e6df;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 15px 0; border-bottom: 1px solid #e8e6df; font-weight: 500; width: 240px;">
Odoo Version
</td>
<td style="padding: 15px 0; border-bottom: 1px solid #e8e6df;">18.0</td>
</tr>
<tr>
<td style="padding: 15px 0; border-bottom: 1px solid #e8e6df; font-weight: 500;">
Dependencies
</td>
<td style="padding: 15px 0; border-bottom: 1px solid #e8e6df;">base, web, jsonifier</td>
</tr>
<tr>
<td style="padding: 15px 0; border-bottom: 1px solid #e8e6df; font-weight: 500;">Category
</td>
<td style="padding: 15px 0; border-bottom: 1px solid #e8e6df;">Extra Tools</td>
</tr>
<tr>
<td style="padding: 15px 0; font-weight: 500;">License</td>
<td style="padding: 15px 0;">AGPL-3</td>
</tr>
</table>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,187 @@
/* =========================
GLOBAL
========================= */
body {
font-family: "Inter", sans-serif;
line-height: 1.7;
color: #001e2b;
background-color: #ffffff;
}
a {
color: #01ffcc;
}
.container {
font-family: "Inter", sans-serif;
line-height: 1.7;
color: #001e2b;
background-color: #ffffff;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* =========================
HEADER
========================= */
.header {
background-color: #001e2b;
color: #f5f3ed;
padding: 80px 60px;
margin-bottom: 60px;
text-align: center;
}
.header-title-table {
width: 100%;
margin-bottom: 20px;
}
.header-title-left {
text-align: right;
vertical-align: middle;
padding-right: 40px;
}
.header-title-right {
text-align: left;
vertical-align: middle;
}
.header-title {
color: #01ffcc;
font-size: 3em;
font-weight: 700;
letter-spacing: -0.02em;
margin: 0;
}
.header-logo {
width: 300px;
max-width: 100%;
object-fit: contain;
}
/* =========================
BADGES
========================= */
.header-badge {
display: inline-block;
border: 1px solid rgba(245, 243, 237, 0.3);
padding: 10px 24px;
margin: 10px 8px;
font-size: 0.85em;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #f5f3ed;
}
/* =========================
SECTIONS
========================= */
.section {
margin-bottom: 80px;
}
/* =========================
SECTION TITLES PATTERN B
========================= */
.section-title-wrapper {
margin-bottom: 40px;
}
.section-title-line {
background-color: #01ffcc;
height: 16px;
width: 310px;
margin-bottom: 12px;
}
.section-title {
color: #001e2b;
font-size: 2.2em;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0;
border-bottom: 1px solid #001e2b;
padding-bottom: 15px;
}
.section-title-dot {
display: block;
margin-top: 8px;
}
/* =========================
TEXT
========================= */
.section-text {
font-size: 1.1em;
line-height: 1.8;
}
/* =========================
SCREENSHOT
========================= */
.screenshot-wrapper {
text-align: center;
margin: 30px 0;
}
.screenshot-image {
width: 100%;
max-width: 100%;
object-fit: contain;
}
/* =========================
SUBTITLES
========================= */
.section h3 {
color: #001e2b;
font-size: 1.4em;
margin-top: 40px;
margin-bottom: 20px;
font-weight: 500;
}
/* =========================
TECH SPECS
========================= */
.tech-specs {
background-color: #ffffff;
padding: 30px;
margin-top: 30px;
border: 1px solid #e8e6df;
}
.tech-specs table {
width: 100%;
border-collapse: collapse;
}
.tech-specs td {
padding: 15px 0;
border-bottom: 1px solid #e8e6df;
font-size: 0.95em;
}
.tech-specs td:first-child {
font-weight: 500;
color: #001e2b;
width: 240px;
}
.tech-specs td:last-child {
color: #5a5a5a;
}

View File

@@ -0,0 +1 @@
from . import test_export_json

View File

@@ -0,0 +1,821 @@
import json
import re
from unittest.mock import MagicMock, patch
from odoo.tests import common, tagged
from odoo.addons.export_json.controller import main as controller_module
from odoo.addons.export_json.controller.main import JsonExportFormat
@tagged("export_json")
class TestExportJson(common.HttpCase):
def setUp(self):
self.partner = self.env["res.partner"].create(
{
"name": "TEST",
"email": "test@test.fr",
"type": "contact",
"child_ids": [
(
0,
None,
{
"name": "test child 1",
"email": "test_child_1@test.fr",
"type": "delivery",
},
),
(
0,
None,
{
"name": "test child 2",
"email": "test_child_2@test.fr",
"type": "invoice",
},
),
],
}
)
self.data_dict = {
"model": "res.partner",
"fields": [
{"name": "id", "label": "External ID"},
{"name": "name", "label": "Name"},
{"name": "email", "label": "Email"},
{"name": "type", "label": "Type"},
{"name": "child_ids/name", "label": "Name"},
{"name": "child_ids/email", "label": "Email"},
{"name": "child_ids/type", "label": "Type"},
],
"ids": [self.partner.id],
"domain": [["type", "in", ("contact", "delivery")]],
"groupby": [],
"context": {
"lang": "en_US",
"tz": "Europe/Paris",
"uid": 2,
"allowed_company_ids": [1],
},
"import_compat": True,
}
self.basic_expect_dict = {
"id": self.partner.id,
"name": "TEST",
"email": "test@test.fr",
"type": "contact",
"child_ids": [
{
"name": "test child 1",
"email": "test_child_1@test.fr",
"type": "delivery",
},
{
"name": "test child 2",
"email": "test_child_2@test.fr",
"type": "invoice",
},
],
}
self.expected_list_dict_merge = [
{
"amount_total": 300.0,
"amount_untaxed": 250.0,
"company_id": "My Company",
"currency_id": "EUR",
"invoice_status": "invoice_status",
"invoice_status_FR": "Rien à facturer",
"invoice_status_US": "Nothing to Bill",
"order_line": [
{
"company_id": "My Company",
"currency_id": "EUR",
"display_type": "Section",
"name": "Exmple de section",
"state": "order_line/state",
"state_FR": "Bon de commande fournisseur",
"state_US": "Purchase Order",
"tax_calculation_rounding_method": "order_line/tax_calculation_rounding_method",
"tax_calculation_rounding_method_FR": "Arrondir à la ligne",
"tax_calculation_rounding_method_US": "Round per Line",
"taxes_id": [],
},
{
"company_id": "My Company",
"currency_id": "EUR",
"name": "Produit test 2",
"price_subtotal": 75.0,
"price_unit": 75.0,
"product_id": "order_line/product_id",
"product_id_FR": "Produit test 2",
"product_id_US": "ENGLISH Produit test 2",
"product_type": "order_line/product_type",
"product_type_FR": "Biens",
"product_type_US": "Goods",
"product_uom": "order_line/product_uom",
"product_uom_FR": "Unité(s)",
"product_uom_US": "Units",
"product_uom_category_id": "order_line/product_uom_category_id",
"product_uom_category_id_FR": "Unité",
"product_uom_category_id_US": "Unit",
"state": "order_line/state",
"state_FR": "Bon de commande fournisseur",
"state_US": "Purchase Order",
"tax_calculation_rounding_method": "order_line/tax_calculation_rounding_method",
"tax_calculation_rounding_method_FR": "Arrondir à la ligne",
"tax_calculation_rounding_method_US": "Round per Line",
"taxes_id": ["20%"],
},
],
"payment_term_id": "payment_term_id",
"payment_term_id_FR": "21 jours ",
"payment_term_id_US": "21 Days",
"tax_country_id": "tax_country_id",
"tax_country_id_FR": "États-Unis",
"tax_country_id_US": "United States",
}
]
self.data_str = str(self.data_dict)
self.data = json.dumps(self.data_dict)
self.json_export_format = JsonExportFormat()
self.mock_request = MagicMock()
self.mock_request.env = self.env
super().setUp()
def helper_get_csrf_token(self):
resp = self.url_open("/web/login")
html = resp.content.decode()
match = re.search(r'csrf_token:\s*"([^"]+)"', html)
return match and match.group(1) or False
def test_generate_export_json(self):
# Perform the json export
fields = [field["name"] for field in self.data_dict["fields"]]
model = self.env[self.data_dict["model"]]
with patch.object(controller_module, "request", self.mock_request):
result_json = self.json_export_format.perform_json_export(
self.data_dict["domain"], fields, self.data_dict["ids"], model
)
dict_result_json = json.loads(result_json)
self.assertDictEqual(
self.basic_expect_dict,
dict_result_json[0],
"Check if json export is equal to expected data",
)
def test_perform_json_export_with_lang(self):
self.env["res.lang"]._activate_lang("fr_FR")
self.data_dict["fields"].append({"name": "title/name", "label": "Title"})
self.data_dict["fields"].append({"name": "country_id/name", "label": "Country"})
fields = [field["name"] for field in self.data_dict["fields"]]
model = self.env[self.data_dict["model"]]
# Ajouts de traductions
title = (
self.env["res.partner.title"]
.with_context(lang="en_US")
.create(
{
"name": "Doctor",
}
)
)
title.with_context(lang="fr_FR").name = "Docteur"
self.partner.title = title
self.env.ref("base.es").with_context(lang="fr_FR").name = "Espagne"
self.partner.country_id = self.env.ref("base.es")
with patch.object(controller_module, "request", self.mock_request):
result_json = self.json_export_format.perform_json_export(
self.data_dict["domain"],
fields,
self.data_dict["ids"],
model,
langs_code=["fr_FR", "en_US"],
)
dict_result_json = json.loads(result_json)
expected_dict = {
"id": self.partner.id,
"name": "TEST",
"email": "test@test.fr",
"type": "contact",
"title": {
"name": "title/name",
"name_FR": "Docteur",
"name_US": "Doctor",
},
"country_id": {
"name": "country_id/name",
"name_FR": "Espagne",
"name_US": "Spain",
},
"child_ids": [
{
"name": "test child 1",
"email": "test_child_1@test.fr",
"type": "delivery",
},
{
"name": "test child 2",
"email": "test_child_2@test.fr",
"type": "invoice",
},
],
}
self.assertDictEqual(expected_dict, dict_result_json[0], "Le json doit contenir les traductions")
def test_convert_simple_list(self):
"""La fonction _convert_simple_list prend en entrée une liste de clée, correspondant
à des noms de champs, et retourne une liste de dict{"name": nom_de_champ}"""
res_partner_fields_names = list(self.partner._fields.keys())
res = self.json_export_format._convert_simple_list(res_partner_fields_names)
self.assertEqual(
len(res_partner_fields_names),
len(res),
"Tous les noms de champs du modèle res.partner doivent être retourné sous la forme {'name': nom_de_champ}",
)
# Si on ajoute un dict dans les noms de champ du res_partner, le dict doit être retourné tel quel, lors
# du passage de la fonction _convert_simple_list
mock_dict = {
"random_key": "random_value",
"random_key1": "random_value1",
"random_key2": "random_value2",
}
res_partner_fields_names.append(mock_dict)
res = self.json_export_format._convert_simple_list(res_partner_fields_names)
self.assertEqual(len(res_partner_fields_names), len(res))
self.assertIn(
mock_dict,
res,
"Le dict ajouté dans les champs de res.partner est bien retourner telquel dans la list",
)
def test_get_country_code(self):
"""La fonction prend un paramètre un string de lang, elle doit retourner la deuxième partie du code
(séparé par le premier '_' du string)"""
for test_val, expected in [
("fr_FR", "FR"),
("_fr_FR", "fr"),
("FR_", ""),
("", ""),
]:
self.assertEqual(self.json_export_format.get_country_code(test_val), expected)
def test_add_unique_vals(self):
"""La fonction prend en paramètre un dict, et retourne un set de valeur unique.
La fonction stringify les list et les dicts, si dans le dict d'entrée, des valeurs sont
des dicts ou des lists."""
mock_dict_same_value = {
"random_key": "random_value",
"random_key1": "random_value",
"random_key2": "random_value",
}
res = self.json_export_format.add_unique_vals(mock_dict_same_value)
self.assertEqual(
len(res),
1,
"Le dict d'entrée contenait 3 clés valeurs, mais les valeurs liées aux clés étaient identiques.",
)
self.assertIn("random_value", res)
mock_dict_different_value = {
"random_key": "random_value",
"random_key1": "random_value1",
"random_key2": "random_value",
}
res = self.json_export_format.add_unique_vals(mock_dict_different_value)
self.assertEqual(len(res), 2)
for expected in ["random_value", "random_value1"]:
self.assertIn(expected, res)
mock_dict_with_dict_list = {
"random_key": {"an_other": "one"},
"random_key1": [10, 20, 40],
"random_key2": [10, 20, 40],
}
res = self.json_export_format.add_unique_vals(mock_dict_with_dict_list)
self.assertEqual(len(res), 2)
for expected in ["{'an_other': 'one'}", "[10, 20, 40]"]:
self.assertIn(expected, res)
self.assertEqual(0, len(self.json_export_format.add_unique_vals({})))
def test_retrieve_all_keys(self):
test_dict = {
"fr_FR": {
"champ_1": "valeur",
"champ_2": "valeur",
"champ_3": "valeur",
"champ_4": "valeur",
},
"zn_CN": {
"champ_5": "valeur",
"champ_6": "valeur",
"champ_3": "valeur",
"champ_4": "valeur",
},
}
res = self.json_export_format.retrieve_all_keys(test_dict)
for expected in [
"champ_1",
"champ_2",
"champ_3",
"champ_4",
"champ_5",
"champ_6",
]:
self.assertIn(expected, res)
other_dict = {
"champ_1": "valeur",
"champ_2": "valeur",
"champ_3": "valeur",
}
with self.assertRaises(AssertionError):
self.json_export_format.retrieve_all_keys(other_dict)
def test_process_keys_without_parent_key(self):
test_dict = {
"fr_FR": {
"champ_1": "valeur",
"champ_2": "valeur",
"champ_3": "valeur",
"champ_4": "valeur",
},
"zn_CN": {
"champ_5": "valeur",
"champ_6": "valeur",
"champ_3": "valeur",
"champ_4": "valeur",
},
}
merged_dict = {}
all_keys = self.json_export_format.retrieve_all_keys(test_dict)
self.json_export_format.process_keys(all_keys, test_dict, merged_dict)
for expected in [
"champ_1",
"champ_2",
"champ_3",
"champ_4",
"champ_5",
"champ_6",
]:
self.assertIn(
expected,
merged_dict,
"Chaque champ doit être inclus dans le dictionnaire final, qui fusionne"
"l'ensemble des champs des dictionnaires d'entrées, par langue.",
)
self.json_export_format.process_keys(all_keys, test_dict, merged_dict)
# Si pour un même nom de champ donné, la valeur est différente entre des langues, les deux clés concaténées
# du suffixe du code la langue,
# doivent être présent dans le dict final.
test_dict_2 = {
"fr_FR": {
"champ_1": "valeur",
"champ_2": "valeur",
"champ_3": "VALEUR EN FRANÇAIS",
"champ_4": "valeur",
},
"zn_CN": {
"champ_5": "valeur",
"champ_6": "valeur",
"champ_3": "VALEUR PAS EN FRANÇAIS",
"champ_4": "valeur",
},
}
self.json_export_format.process_keys(all_keys, test_dict_2, merged_dict)
for expected in [
"champ_1",
"champ_2",
"champ_3",
"champ_4",
"champ_5",
"champ_6",
"champ_3_FR",
"champ_3_CN",
]:
self.assertIn(
expected,
merged_dict,
"Chaque champ doit être inclus dans le dictionnaire final, qui fusionne"
"l'ensemble des champs des dictionnaires d'entrées, par langue.",
)
self.assertEqual(merged_dict.get("champ_3_FR"), "VALEUR EN FRANÇAIS")
self.assertEqual(merged_dict.get("champ_3_CN"), "VALEUR PAS EN FRANÇAIS")
def test_process_keys_with_parent_key(self):
test_dict = {
"fr_FR": {
"champ_1": "valeur",
"champ_2": "valeur",
"champ_3": {
"cle_dict_imbrique_1": "valeur1",
"cle_dict_imbrique_2": "valeur2",
"cle_dict_imbrique_3": [
"element_liste_imbriqueFR",
"element_autre_listeFR",
],
},
},
"zn_CN": {
"champ_1": "valeur",
"champ_2": "valeur",
"champ_3": {
"cle_dict_imbrique_1": "valeur1489",
"cle_dict_imbrique_2": "valeur24996",
"cle_dict_imbrique_3": [
"element_liste_imbrique",
"element_autre_liste",
],
},
},
}
merged_dict = {}
all_keys = self.json_export_format.retrieve_all_keys(test_dict)
self.json_export_format.process_keys(all_keys, test_dict, merged_dict, "champ_3")
# Le dict attendu doit contenir toutes les valeurs de chaque langues, même si les valeurs sont eux mêmes dans
# des dict ou dans des listes. Un regroupement doit être effectué autour du champ_3 :
# le dict du champ_3 doit contenir la clé original de chaque dict (cle_dict_imbrique_1, cle_dict_imbrique_2,
# cle_dict_imbrique_3)
# avec pour valeurs le chemin absolu de la valeur (champ_3/cle_dict_imbrique_1, champ_3/cle_dict_imbrique_2,
# champ_3/cle_dict_imbrique_3)
expected_dict = {
"champ_1": "valeur",
"champ_2": "valeur",
"champ_3": {
"cle_dict_imbrique_1": "champ_3/cle_dict_imbrique_1",
"cle_dict_imbrique_1_CN": "valeur1489",
"cle_dict_imbrique_1_FR": "valeur1",
"cle_dict_imbrique_2": "champ_3/cle_dict_imbrique_2",
"cle_dict_imbrique_2_CN": "valeur24996",
"cle_dict_imbrique_2_FR": "valeur2",
"cle_dict_imbrique_3": "champ_3/cle_dict_imbrique_3",
"cle_dict_imbrique_3_CN": [
"element_liste_imbrique",
"element_autre_liste",
],
"cle_dict_imbrique_3_FR": [
"element_liste_imbriqueFR",
"element_autre_listeFR",
],
},
}
self.assertDictEqual(expected_dict, merged_dict)
def test_process_keys_nested_dict(self):
test_dict = {
"fr_FR": {
"champ_1": [
{
"cle_dict_imbrique_1": "valeur1",
"cle_dict_imbrique_2": "valeur2",
"cle_dict_imbrique_3": [
"element_liste_imbriqueFR",
"element_autre_listeFR",
],
},
{
"cle_dict_imbrique_1": "valeur44",
"cle_dict_imbrique_2": "valeur4858",
"cle_dict_imbrique_3": [
"element_liste_imbriqueFR",
"element_autre_liste",
],
},
],
"champ_2": "valeur",
},
"zn_CN": {
"champ_1": [
{
"cle_dict_imbrique_1": "valeur1",
"cle_dict_imbrique_2": "valeur2",
"cle_dict_imbrique_3": [
"element_liste_imbriqueFR",
"element_autre_listeFR",
],
},
{
"cle_dict_imbrique_1": "valeur44",
"cle_dict_imbrique_2": "valeur4858",
"cle_dict_imbrique_3": [
"element_liste_imbrique",
"element_autre_liste",
],
},
],
},
}
merged_dict = {}
all_keys = self.json_export_format.retrieve_all_keys(test_dict)
self.json_export_format.process_keys(all_keys, test_dict, merged_dict)
expected_dict = {
"champ_1": [
{
"cle_dict_imbrique_1": "valeur1",
"cle_dict_imbrique_2": "valeur2",
"cle_dict_imbrique_3": [
"element_liste_imbriqueFR",
"element_autre_listeFR",
],
},
{
"cle_dict_imbrique_1": "valeur44",
"cle_dict_imbrique_2": "valeur4858",
"cle_dict_imbrique_3": "champ_1/cle_dict_imbrique_3",
"cle_dict_imbrique_3_CN": [
"element_liste_imbrique",
"element_autre_liste",
],
"cle_dict_imbrique_3_FR": [
"element_liste_imbriqueFR",
"element_autre_liste",
],
},
],
"champ_2": "valeur",
}
self.assertDictEqual(expected_dict, merged_dict)
def test_merge_list_of_dicts_without_lang(self):
self.assertEqual(
[],
self.json_export_format._merge_list_of_dicts({}),
"Sans langue, cette fonction ne doit rien faire et retourner une liste vide",
)
self.assertEqual(
[],
self.json_export_format.merge_multilingual_dicts({}),
"Même chose, sans langue, pas de dict",
)
def test_merge_multilingual_dicts(self):
test_dict = {
"en_US": [
{
"amount_total": 300.0,
"amount_untaxed": 250.0,
"company_id": "My Company",
"currency_id": "EUR",
"invoice_status": "Nothing to Bill",
"order_line": [
{
"company_id": "My Company",
"currency_id": "EUR",
"display_type": "Section",
"name": "Exmple de section",
"state": "Purchase Order",
"tax_calculation_rounding_method": "Round per Line",
"taxes_id": [],
},
{
"company_id": "My Company",
"currency_id": "EUR",
"name": "Produit test 2",
"price_subtotal": 75.0,
"price_unit": 75.0,
"product_type": "Goods",
"product_id": "ENGLISH Produit test 2",
"product_uom": "Units",
"product_uom_category_id": "Unit",
"state": "Purchase Order",
"tax_calculation_rounding_method": "Round per Line",
"taxes_id": ["20%"],
},
],
"payment_term_id": "21 Days",
"tax_country_id": "United States",
}
],
"fr_FR": [
{
"amount_total": 300.0,
"amount_untaxed": 250.0,
"company_id": "My Company",
"currency_id": "EUR",
"invoice_status": "Rien à facturer",
"order_line": [
{
"company_id": "My Company",
"currency_id": "EUR",
"display_type": "Section",
"name": "Exmple de section",
"state": "Bon de commande fournisseur",
"tax_calculation_rounding_method": "Arrondir à la ligne",
"taxes_id": [],
},
{
"company_id": "My Company",
"currency_id": "EUR",
"name": "Produit test 2",
"price_subtotal": 75.0,
"price_unit": 75.0,
"product_id": "Produit test 2",
"product_type": "Biens",
"product_uom": "Unité(s)",
"product_uom_category_id": "Unité",
"state": "Bon de commande fournisseur",
"tax_calculation_rounding_method": "Arrondir à la ligne",
"taxes_id": ["20%"],
},
],
"payment_term_id": "21 jours ",
"tax_country_id": "États-Unis",
}
],
}
self.assertDictEqual(
self.expected_list_dict_merge[0],
self.json_export_format.merge_multilingual_dicts(test_dict)[0],
)
def test_format_data(self):
fields = self.expected_list_dict_merge
res_json = self.json_export_format.format_data(fields)
self.assertTrue(json.loads(res_json), "Le résultat de la fonction doit être au format .json")
fields[0].update({"mock_bytes_key": b"mock_bytes_value"})
res_json = self.json_export_format.format_data(fields)
list_dict_from_json = json.loads(res_json)
bytes_key = list_dict_from_json[0].get("mock_bytes_key")
self.assertEqual(
"data:image/png;base64,mock_bytes_value",
bytes_key,
"La présence d'une clé contenant une valeur en bytes, dans les champs d'entrée, doit obligatoirement"
"être modifié en sorti dans le json, pour y inclure un préfixe.",
)
def test_define_parser(self):
res_partner_fields_names = list(self.partner._fields.keys())
set_fields_names = set(res_partner_fields_names)
# Ajouts de faux nom de champs qui sont concaténés.
res_partner_fields_names.append("company_id/name")
res_partner_fields_names.append("company_id/street1")
res_partner_fields_names.append("company_id/siret")
res_partner_fields_names.append("country_id/currency_id/name")
res_partner_fields_names.append("country_id/currency_id/iso_numeric")
converted_fields_name = self.json_export_format._convert_simple_list(res_partner_fields_names)
parser = self.json_export_format.define_parser(converted_fields_name)
self.assertTrue(
set_fields_names.issubset(parser),
"Tous les champs du modèle res.partner doivent être présent dans le parser.",
)
expected_tuple_1 = ("company_id", ["name", "street1", "siret"])
expected_tuple_2 = ("country_id", [("currency_id", ["name", "iso_numeric"])])
for expected_tuple in [expected_tuple_1, expected_tuple_2]:
self.assertIn(
expected_tuple,
parser,
"Un tuple nom_de_champ et liste de nom de champ (qui peut lui même contenir un tuple avec "
"la même combinaison), doit être présent, pour les champs de type M2O, avec un accès à un "
"des champs du M2O",
)
def test_add_list_or_dict_on_merge_item(self):
# Faire la version avec un parent_key qui est un dict
test_dict = {
"fr_FR": {
"champ_2": "valeur",
"champ_3": {
"cle_dict_imbrique_1": "valeur1",
"cle_dict_imbrique_2": "valeur2",
"cle_dict_imbrique_3": [
"element_liste_imbriqueFR",
"element_autre_listeFR",
],
},
},
"zn_CN": {
"champ_2": "valeur",
"champ_3": {
"cle_dict_imbrique_1": "valeur1489",
"cle_dict_imbrique_2": "valeur24996",
"cle_dict_imbrique_3": [
"element_liste_imbrique",
"element_autre_liste",
],
},
},
}
merged_dict = {}
all_keys = self.json_export_format.retrieve_all_keys(test_dict)
# On simule le fait que le champ_2 soit le parent key de tout le dict d'entrée "test_dict".
# La clé doit apparaître dans l'arborescence de chaque champ imbriquée.
self.json_export_format.process_keys(all_keys, test_dict, merged_dict, "champ_2")
expected_dict = {
"champ_2": "valeur",
"champ_3": {
"cle_dict_imbrique_2": "champ_2/champ_3/cle_dict_imbrique_2",
"cle_dict_imbrique_2_FR": "valeur2",
"cle_dict_imbrique_2_CN": "valeur24996",
"cle_dict_imbrique_3": "champ_2/champ_3/cle_dict_imbrique_3",
"cle_dict_imbrique_3_FR": [
"element_liste_imbriqueFR",
"element_autre_listeFR",
],
"cle_dict_imbrique_3_CN": [
"element_liste_imbrique",
"element_autre_liste",
],
"cle_dict_imbrique_1": "champ_2/champ_3/cle_dict_imbrique_1",
"cle_dict_imbrique_1_FR": "valeur1",
"cle_dict_imbrique_1_CN": "valeur1489",
},
}
self.assertDictEqual(expected_dict, merged_dict)
def test_index_json_export_route(self):
# Fonction appelé par le controller qui gère l'export au format json (bouton "Action" > "Exporter")
self.authenticate("admin", "admin")
csrf = self.helper_get_csrf_token()
url = "/web/export/json"
payload = {
"csrf_token": csrf,
"data": json.dumps(self.data_dict),
}
response = self.url_open(url, payload)
self.assertEqual(response.status_code, 200)
self.assertDictEqual(self.basic_expect_dict, response.json()[0])
def test_formats_export_formats(self):
# On vérifie que la pop-up d'export ajoute bien la coche json dans les options de format de sortie.
self.authenticate("admin", "admin")
url = "/web/export/formats"
payload = {"jsonrpc": "2.0", "method": "call", "params": {}, "id": 1}
response = self.url_open(
url,
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
)
result_format = response.json().get("result")
self.assertIn(
{"label": "JSON", "tag": "json"},
result_format,
"Le nouveau format json doit être disponible enexport",
)
def test_json_export_on_none_ordinary_table(self):
fields = [field["name"] for field in self.data_dict["fields"]]
model = self.env[self.data_dict["model"]]
with patch("odoo.models.BaseModel._is_an_ordinary_table", return_value=False):
with patch.object(controller_module, "request", self.mock_request):
result_json = self.json_export_format.perform_json_export(
self.data_dict["domain"], fields, self.data_dict["ids"], model
)
res_dict = json.loads(result_json)
self.basic_expect_dict.pop("id")
self.assertDictEqual(
res_dict[0],
self.basic_expect_dict,
"Seul l'id ne doit pas être récupérer, "
"si la table associé au modèle ne correspond pas à une table classique."
"Toutes les autres informations doivent être récupérés normalement.",
)
def test_property_extension(self):
self.assertEqual(self.json_export_format.extension, ".json")
def test_property_content_type(self):
self.assertEqual(self.json_export_format.content_type, "text/json;charset=utf8")

288
jsonifier/README.rst Normal file
View File

@@ -0,0 +1,288 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
=========
JSONifier
=========
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:a064cec295d1e9072c772cfaaaeee217cc7da886fa33b319d2ebbae819ceffc9
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/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
: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 <https://github.com/OCA/server-tools/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 <https://github.com/OCA/server-tools/issues/new?body=module:%20jsonifier%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Akretion
* ACSONE
* Camptocamp
Contributors
------------
- BEAU Sébastien <sebastien.beau@akretion.com>
- Raphaël Reverdy <raphael.reverdy@akretion.com>
- Laurent Mignon <laurent.mignon@acsone.eu>
- Nans Lefebvre <nans.lefebvre@acsone.eu>
- Simone Orsi <simone.orsi@camptocamp.com>
- Iván Todorovich <ivan.todorovich@camptocamp.com>
- Nguyen Minh Chien <chien@trobz.com>
- Thien Vo <thienvh@trobz.com>
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 <https://github.com/OCA/server-tools/tree/18.0/jsonifier>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

1
jsonifier/__init__.py Normal file
View File

@@ -0,0 +1 @@
from . import models

26
jsonifier/__manifest__.py Normal file
View File

@@ -0,0 +1,26 @@
# Copyright 2017-2018 Akretion (http://www.akretion.com)
# Sébastien BEAU <sebastien.beau@akretion.com>
# Raphaël Reverdy <raphael.reverdy@akretion.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
"name": "JSONifier",
"summary": "JSON-ify data for all models",
"version": "18.0.1.1.1",
"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",
],
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="ir_exp_partner" model="ir.exports">
<field name="name">Partner Export</field>
<field name="resource">res.partner</field>
</record>
</odoo>

View File

@@ -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
1 id export_id/id name
2 name ir_exp_partner name
3 active ir_exp_partner active
4 partner_latitude ir_exp_partner partner_latitude
5 color ir_exp_partner color
6 category_id_name ir_exp_partner category_id/name
7 country_id_name ir_exp_partner country_id/name
8 country_id_code ir_exp_partner country_id/code
9 child_ids_name ir_exp_partner child_ids/name
10 child_ids_id ir_exp_partner child_ids/id
11 child_ids_email ir_exp_partner child_ids/email
12 child_ids_country_id_name ir_exp_partner child_ids/country_id/name
13 child_ids_country_id_code ir_exp_partner child_ids/country_id/code
14 child_ids_child_ids_name ir_exp_partner child_ids/child_ids/name
15 lang ir_exp_partner lang
16 comment ir_exp_partner comment

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="ir_exports_resolver_dict" model="ir.exports.resolver">
<field name="name">ExtraData dictionary (number/text)</field>
<field name="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}
</field>
</record>
</odoo>

7
jsonifier/exceptions.py Normal file
View File

@@ -0,0 +1,7 @@
# Copyright 2022 Camptocamp SA (http://www.camptocamp.com)
# Simone Orsi <simahawk@gmail.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
class SwallableException(Exception):
"""An exception that can be safely skipped."""

234
jsonifier/i18n/ca.po Normal file
View File

@@ -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 ""

266
jsonifier/i18n/es.po Normal file
View File

@@ -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 <informatica@totmaterial.es>\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"

277
jsonifier/i18n/it.po Normal file
View File

@@ -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 <stefano.consolaro@mymage.it>\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"

View File

@@ -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 ""

260
jsonifier/i18n/zh_CN.po Normal file
View File

@@ -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 <feihu.zhang@live.com>\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'"

View File

@@ -0,0 +1,5 @@
from . import utils
from . import models
from . import ir_exports
from . import ir_exports_line
from . import ir_exports_resolver

View File

@@ -0,0 +1,124 @@
# © 2017 Akretion (http://www.akretion.com)
# Sébastien BEAU <sebastien.beau@akretion.com>
# 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

View File

@@ -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)
)

View File

@@ -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

269
jsonifier/models/models.py Normal file
View File

@@ -0,0 +1,269 @@
# Copyright 2017 Akretion (http://www.akretion.com)
# Sébastien BEAU <sebastien.beau@akretion.com>
# Raphaël Reverdy <raphael.reverdy@akretion.com>
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# Simone Orsi <simahawk@gmail.com>
# 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:
<field name="name">m2o_id</field>
<field name="target">m2o_id:rel_id</field>
<field name="instance_method_name">_jsonify_m2o_to_id</field>
"""
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:
<field name="name">m2m_ids</field>
<field name="target">m2m_ids:rel_ids</field>
<field name="instance_method_name">_jsonify_x2m_to_ids</field>
"""
return self[fname].ids
def _jsonify_format_duration(self, fname):
"""Helper to format a Float-like duration to string 00:00.
Example:
<field name="name">duration</field>
<field name="instance_method_name">_jsonify_format_duration</field>
"""
return format_duration(self[fname])

35
jsonifier/models/utils.py Normal file
View File

@@ -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

3
jsonifier/pyproject.toml Normal file
View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,8 @@
- BEAU Sébastien \<<sebastien.beau@akretion.com>\>
- Raphaël Reverdy \<<raphael.reverdy@akretion.com>\>
- Laurent Mignon \<<laurent.mignon@acsone.eu>\>
- Nans Lefebvre \<<nans.lefebvre@acsone.eu>\>
- Simone Orsi \<<simone.orsi@camptocamp.com>\>
- Iván Todorovich \<<ivan.todorovich@camptocamp.com>\>
- Nguyen Minh Chien \<<chien@trobz.com>\>
- Thien Vo \<<thienvh@trobz.com>\>

View File

@@ -0,0 +1 @@
The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp.

View File

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

26
jsonifier/readme/USAGE.md Normal file
View File

@@ -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}]}]
```

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ir_exports_resolver ir.exports.resolver model_ir_exports_resolver base.group_system 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,610 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="jsonifier">
<h1>JSONifier</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:a064cec295d1e9072c772cfaaaeee217cc7da886fa33b319d2ebbae819ceffc9
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-tools/tree/18.0/jsonifier"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-jsonifier"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-tools&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>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.</p>
<p>Example of a simple parser:</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">[</span><span class="w">
</span> <span class="s1">'name'</span><span class="p">,</span><span class="w">
</span> <span class="s1">'number'</span><span class="p">,</span><span class="w">
</span> <span class="s1">'create_date'</span><span class="p">,</span><span class="w">
</span> <span class="p">(</span><span class="s1">'partner_id'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'id'</span><span class="p">,</span> <span class="s1">'display_name'</span><span class="p">,</span> <span class="s1">'ref'</span><span class="p">])</span><span class="w">
</span> <span class="p">(</span><span class="s1">'line_id'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'id'</span><span class="p">,</span> <span class="p">(</span><span class="s1">'product_id'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'name'</span><span class="p">]),</span> <span class="s1">'price_unit'</span><span class="p">])</span><span class="w">
</span><span class="p">]</span>
</pre>
<p>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.</p>
<p>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:</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">[</span><span class="w">
</span> <span class="s1">'field_name:json_key'</span><span class="w">
</span><span class="p">]</span>
</pre>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">[</span><span class="w">
</span> <span class="s1">'name'</span><span class="p">,</span><span class="w">
</span> <span class="s1">'number'</span><span class="p">,</span><span class="w">
</span> <span class="s1">'create_date:creationDate'</span><span class="p">,</span><span class="w">
</span> <span class="p">(</span><span class="s1">'partner_id:partners'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'id'</span><span class="p">,</span> <span class="s1">'display_name'</span><span class="p">,</span> <span class="s1">'ref'</span><span class="p">])</span><span class="w">
</span> <span class="p">(</span><span class="s1">'line_id:lines'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'id'</span><span class="p">,</span> <span class="p">(</span><span class="s1">'product_id'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'name'</span><span class="p">]),</span> <span class="s1">'price_unit'</span><span class="p">])</span><span class="w">
</span><span class="p">]</span>
</pre>
<p>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:</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">[</span><span class="w">
</span> <span class="p">(</span><span class="s1">'name'</span><span class="p">,</span> <span class="s2">&quot;jsonify_name&quot;</span><span class="p">)</span> <span class="c1"># method name</span><span class="w">
</span> <span class="p">(</span><span class="s1">'number'</span><span class="p">,</span> <span class="k">lambda</span> <span class="n">rec</span><span class="p">,</span> <span class="n">field_name</span><span class="p">:</span> <span class="n">rec</span><span class="p">[</span><span class="n">field_name</span><span class="p">]</span> <span class="o">*</span> <span class="mi">2</span><span class="p">))</span> <span class="c1"># callable</span><span class="w">
</span><span class="p">]</span>
</pre>
<p>Also the module provide a method “get_json_parser” on the ir.exports
object that generate a parser from an ir.exports configuration.</p>
<p>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.</p>
<p>To use these features, a full parser follows the following structure:</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">{</span><span class="w">
</span> <span class="s2">&quot;resolver&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;language_agnostic&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;langs&quot;</span><span class="p">:</span> <span class="p">{</span><span class="w">
</span> <span class="kc">False</span><span class="p">:</span> <span class="p">[</span><span class="w">
</span> <span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">},</span><span class="w">
</span> <span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'number'</span><span class="p">,</span> <span class="s1">'resolver'</span><span class="p">:</span> <span class="mi">5</span><span class="p">},</span><span class="w">
</span> <span class="p">({</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'partner_id'</span><span class="p">,</span> <span class="s1">'target'</span><span class="p">:</span> <span class="s1">'partner'</span><span class="p">},</span> <span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'display_name'</span><span class="p">}])</span><span class="w">
</span> <span class="p">],</span><span class="w">
</span> <span class="s1">'fr_FR'</span><span class="p">:</span> <span class="p">[</span><span class="w">
</span> <span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">,</span> <span class="s1">'target'</span><span class="p">:</span> <span class="s1">'descriptions_fr'</span><span class="p">},</span><span class="w">
</span> <span class="p">({</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'partner_id'</span><span class="p">,</span> <span class="s1">'target'</span><span class="p">:</span> <span class="s1">'partner'</span><span class="p">},</span> <span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">,</span> <span class="s1">'target'</span><span class="p">:</span> <span class="s1">'description_fr'</span><span class="p">}])</span><span class="w">
</span> <span class="p">],</span><span class="w">
</span> <span class="p">}</span><span class="w">
</span><span class="p">}</span>
</pre>
<p>One would get a result having this structure (note that the translated
fields are merged in the same dictionary):</p>
<pre class="code python literal-block">
<span class="n">exported_json</span> <span class="o">==</span> <span class="p">{</span><span class="w">
</span> <span class="s2">&quot;description&quot;</span><span class="p">:</span> <span class="s2">&quot;English description&quot;</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;description_fr&quot;</span><span class="p">:</span> <span class="s2">&quot;French description, voilà&quot;</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;number&quot;</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;partner&quot;</span><span class="p">:</span> <span class="p">{</span><span class="w">
</span> <span class="s2">&quot;display_name&quot;</span><span class="p">:</span> <span class="s2">&quot;partner name&quot;</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;description_fr&quot;</span><span class="p">:</span> <span class="s2">&quot;French description of that partner&quot;</span><span class="p">,</span><span class="w">
</span> <span class="p">},</span><span class="w">
</span><span class="p">}</span>
</pre>
<p>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:</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">{</span><span class="w">
</span> <span class="s2">&quot;resolver&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;fields&quot;</span><span class="p">:</span> <span class="p">[</span><span class="w">
</span> <span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">},</span><span class="w">
</span> <span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'number'</span><span class="p">,</span> <span class="s1">'resolver'</span><span class="p">:</span> <span class="mi">5</span><span class="p">},</span><span class="w">
</span> <span class="p">({</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'partner_id'</span><span class="p">,</span> <span class="s1">'target'</span><span class="p">:</span> <span class="s1">'partners'</span><span class="p">},</span> <span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'display_name'</span><span class="p">}]),</span><span class="w">
</span> <span class="p">],</span><span class="w">
</span><span class="p">}</span>
</pre>
<p>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.</p>
<p>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 - …</p>
<p>A simple parser is simply translated into a full parser at export.</p>
<p>If the global resolver is given, then the json_dict goes through:</p>
<pre class="code python literal-block">
<span class="n">resolver</span><span class="o">.</span><span class="n">resolve</span><span class="p">(</span><span class="nb">dict</span><span class="p">,</span> <span class="n">record</span><span class="p">)</span>
</pre>
<p>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.</p>
<p>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.</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">{</span><span class="w">
</span> <span class="n">fields</span><span class="p">:</span> <span class="p">[</span><span class="w">
</span> <span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'name'</span><span class="p">},</span><span class="w">
</span> <span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'field_1'</span><span class="p">,</span> <span class="s1">'target'</span><span class="p">:</span> <span class="s1">'customTags=list'</span><span class="p">},</span><span class="w">
</span> <span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'field_2'</span><span class="p">,</span> <span class="s1">'target'</span><span class="p">:</span> <span class="s1">'customTags=list'</span><span class="p">},</span><span class="w">
</span> <span class="p">]</span><span class="w">
</span><span class="p">}</span>
</pre>
<p>Would result in the following JSON structure:</p>
<pre class="code python literal-block">
<span class="p">{</span><span class="w">
</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'record_name'</span><span class="p">,</span><span class="w">
</span> <span class="s1">'customTags'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'field_1_value'</span><span class="p">,</span> <span class="s1">'field_2_value'</span><span class="p">],</span><span class="w">
</span><span class="p">}</span>
</pre>
<p>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.</p>
<p>Note that the export values with the simple parser depends on the
records lang; this is in contrast with full parsers which are designed
to be language agnostic.</p>
<p>NOTE: this module was named base_jsonify till version 14.0.1.5.0.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a><ul>
<li><a class="reference internal" href="#with-fieldname-parameter" id="toc-entry-2">with_fieldname parameter</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-7">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<div class="section" id="with-fieldname-parameter">
<h3><a class="toc-backref" href="#toc-entry-2">with_fieldname parameter</a></h3>
<p>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.</p>
<blockquote>
Examples of with_fieldname usage:</blockquote>
<pre class="code python literal-block">
<span class="c1"># example 1</span><span class="w">
</span><span class="n">parser</span> <span class="o">=</span> <span class="p">[(</span><span class="s1">'name'</span><span class="p">)]</span><span class="w">
</span><span class="n">a</span><span class="o">.</span><span class="n">jsonify</span><span class="p">(</span><span class="n">parser</span><span class="o">=</span><span class="n">parser</span><span class="p">)</span><span class="w">
</span><span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'SO3996'</span><span class="p">}]</span><span class="w">
</span><span class="o">&gt;&gt;&gt;</span> <span class="n">a</span><span class="o">.</span><span class="n">jsonify</span><span class="p">(</span><span class="n">parser</span><span class="o">=</span><span class="n">parser</span><span class="p">,</span> <span class="n">with_fieldname</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span><span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'SO3996'</span><span class="p">}]</span><span class="w">
</span><span class="o">&gt;&gt;&gt;</span> <span class="n">a</span><span class="o">.</span><span class="n">jsonify</span><span class="p">(</span><span class="n">parser</span><span class="o">=</span><span class="n">parser</span><span class="p">,</span> <span class="n">with_fieldname</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="w">
</span><span class="p">[{</span><span class="s1">'fieldname_name'</span><span class="p">:</span> <span class="s1">'Order Reference'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'SO3996'</span><span class="p">}}]</span><span class="w">
</span><span class="c1"># example 2 - with a subparser-</span><span class="w">
</span><span class="n">parser</span><span class="o">=</span><span class="p">[</span><span class="s1">'name'</span><span class="p">,</span> <span class="s1">'create_date'</span><span class="p">,</span> <span class="p">(</span><span class="s1">'order_line'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'id'</span> <span class="p">,</span> <span class="s1">'product_uom'</span><span class="p">,</span> <span class="s1">'is_expense'</span><span class="p">])]</span><span class="w">
</span><span class="o">&gt;&gt;&gt;</span> <span class="n">a</span><span class="o">.</span><span class="n">jsonify</span><span class="p">(</span><span class="n">parser</span><span class="o">=</span><span class="n">parser</span><span class="p">,</span> <span class="n">with_fieldname</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span><span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'SO3996'</span><span class="p">,</span> <span class="s1">'create_date'</span><span class="p">:</span> <span class="s1">'2015-06-02T12:18:26.279909+00:00'</span><span class="p">,</span> <span class="s1">'order_line'</span><span class="p">:</span> <span class="p">[{</span><span class="s1">'id'</span><span class="p">:</span> <span class="mi">16649</span><span class="p">,</span> <span class="s1">'product_uom'</span><span class="p">:</span> <span class="s1">'stuks'</span><span class="p">,</span> <span class="s1">'is_expense'</span><span class="p">:</span> <span class="kc">False</span><span class="p">},</span> <span class="p">{</span><span class="s1">'id'</span><span class="p">:</span> <span class="mi">16651</span><span class="p">,</span> <span class="s1">'product_uom'</span><span class="p">:</span> <span class="s1">'stuks'</span><span class="p">,</span> <span class="s1">'is_expense'</span><span class="p">:</span> <span class="kc">False</span><span class="p">},</span> <span class="p">{</span><span class="s1">'id'</span><span class="p">:</span> <span class="mi">16650</span><span class="p">,</span> <span class="s1">'product_uom'</span><span class="p">:</span> <span class="s1">'stuks'</span><span class="p">,</span> <span class="s1">'is_expense'</span><span class="p">:</span> <span class="kc">False</span><span class="p">}]}]</span><span class="w">
</span><span class="o">&gt;&gt;&gt;</span> <span class="n">a</span><span class="o">.</span><span class="n">jsonify</span><span class="p">(</span><span class="n">parser</span><span class="o">=</span><span class="n">parser</span><span class="p">,</span> <span class="n">with_fieldname</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span><span class="w">
</span><span class="p">[{</span><span class="s1">'fieldname_name'</span><span class="p">:</span> <span class="s1">'Order Reference'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'SO3996'</span><span class="p">,</span> <span class="s1">'fieldname_create_date'</span><span class="p">:</span> <span class="s1">'Creation Date'</span><span class="p">,</span> <span class="s1">'create_date'</span><span class="p">:</span> <span class="s1">'2015-06-02T12:18:26.279909+00:00'</span><span class="p">,</span> <span class="s1">'fieldname_order_line'</span><span class="p">:</span> <span class="s1">'Order Lines'</span><span class="p">,</span> <span class="s1">'order_line'</span><span class="p">:</span> <span class="p">[{</span><span class="s1">'fieldname_id'</span><span class="p">:</span> <span class="s1">'ID'</span><span class="p">,</span> <span class="s1">'id'</span><span class="p">:</span> <span class="mi">16649</span><span class="p">,</span> <span class="s1">'fieldname_product_uom'</span><span class="p">:</span> <span class="s1">'Unit of Measure'</span><span class="p">,</span> <span class="s1">'product_uom'</span><span class="p">:</span> <span class="s1">'stuks'</span><span class="p">,</span> <span class="s1">'fieldname_is_expense'</span><span class="p">:</span> <span class="s1">'Is expense'</span><span class="p">,</span> <span class="s1">'is_expense'</span><span class="p">:</span> <span class="kc">False</span><span class="p">}]}]</span>
</pre>
</div>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-tools/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/server-tools/issues/new?body=module:%20jsonifier%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-4">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-5">Authors</a></h3>
<ul class="simple">
<li>Akretion</li>
<li>ACSONE</li>
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-6">Contributors</a></h3>
<ul class="simple">
<li>BEAU Sébastien &lt;<a class="reference external" href="mailto:sebastien.beau&#64;akretion.com">sebastien.beau&#64;akretion.com</a>&gt;</li>
<li>Raphaël Reverdy &lt;<a class="reference external" href="mailto:raphael.reverdy&#64;akretion.com">raphael.reverdy&#64;akretion.com</a>&gt;</li>
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
<li>Nans Lefebvre &lt;<a class="reference external" href="mailto:nans.lefebvre&#64;acsone.eu">nans.lefebvre&#64;acsone.eu</a>&gt;</li>
<li>Simone Orsi &lt;<a class="reference external" href="mailto:simone.orsi&#64;camptocamp.com">simone.orsi&#64;camptocamp.com</a>&gt;</li>
<li>Iván Todorovich &lt;<a class="reference external" href="mailto:ivan.todorovich&#64;camptocamp.com">ivan.todorovich&#64;camptocamp.com</a>&gt;</li>
<li>Nguyen Minh Chien &lt;<a class="reference external" href="mailto:chien&#64;trobz.com">chien&#64;trobz.com</a>&gt;</li>
<li>Thien Vo &lt;<a class="reference external" href="mailto:thienvh&#64;trobz.com">thienvh&#64;trobz.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h3><a class="toc-backref" href="#toc-entry-7">Other credits</a></h3>
<p>The migration of this module from 17.0 to 18.0 was financially supported
by Camptocamp.</p>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/18.0/jsonifier">OCA/server-tools</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
from . import test_get_parser
from . import test_helpers
from . import test_ir_exports_line

View File

@@ -0,0 +1,452 @@
# Copyright 2017 ACSONE SA/NV
# Copyright 2022 Camptocamp SA (http://www.camptocamp.com)
# Simone Orsi <simahawk@gmail.com>
# 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 odoo.tools import mute_logger
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):
with mute_logger("odoo.tests.common"): # Mute patch detector
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):
with mute_logger("odoo.tests.common"): # Mute patch detector
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!"})
del self.category.__class__.jsonify_custom
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

View File

@@ -0,0 +1,45 @@
# Copyright 2021 Camptocamp SA (https://www.camptocamp.com).
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
# 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",
)

View File

@@ -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",
}
)

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="ir.ui.view" id="view_ir_exports_resolver">
<field name="model">ir.exports.resolver</field>
<field name="priority">50</field>
<field name="arch" type="xml">
<form>
<group>
<field name="name" />
<field name="type" />
<field name="python_code" />
</group>
</form>
</field>
</record>
<record id="act_ui_exports_resolver_view" model="ir.actions.act_window">
<field name="name">Custom Export Resolvers</field>
<field name="res_model">ir.exports.resolver</field>
<field name="view_mode">list,form</field>
</record>
<menuitem
id="ui_exports_resolvers"
action="act_ui_exports_resolver_view"
parent="base.next_id_2"
/>
</odoo>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="ir.ui.view" id="view_ir_exports">
<field name="model">ir.exports</field>
<field name="priority">50</field>
<field name="arch" type="xml">
<form>
<sheet>
<group name="se" string="Configuration">
<group colspan="4" col="4" name="se-main">
<field name="name" />
<field name="resource" />
<field name="language_agnostic" />
<field name="global_resolver_id" />
</group>
</group>
<group name="index" string="Index">
<field name="export_fields" nolabel="1" colspan="2">
<list editable="bottom">
<field name="name" />
<field name="target" />
<field name="lang_id" />
<field name="resolver_id" />
<field name="instance_method_name" />
</list>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="act_ui_exports_view" model="ir.actions.act_window">
<field name="name">Export Fields</field>
<field name="res_model">ir.exports</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="ui_exports" action="act_ui_exports_view" parent="base.next_id_2" />
</odoo>

File diff suppressed because it is too large Load Diff

9
report_carbone/README.md Normal file
View File

@@ -0,0 +1,9 @@
Carbone Odoo Connector
======================
Carbone Odoo Connector system for Odoo Community edition.
Configuration
=============
Add your Carbon API keys and define users who have access to the "Carbon Reports" menu with the "Carbon Report - Viewer" and "Carbon Report - Manager" groups.

55
report_carbone/README.rst Normal file
View File

@@ -0,0 +1,55 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
Carbone Odoo Connector
======================
Carbone Odoo Connector system for Odoo 18 Community edition.
Configuration
=============
Add your Carbon API keys and define users who have access to the "Carbon Reports" menu with the "Carbon Report - Viewer" and "Carbon Report - Manager" groups.
.. image:: static/description/assets/screenshots/add_api_keys.png
:scale: 36 %
.. image:: static/description/assets/screenshots/carbone_groups.png
:scale: 50 %
Company
-------
* `Mangono <https://mangono.fr/>`__
License
-------
General Public License, Version 3 (LGPL v3).
(http://www.gnu.org/licenses/lgpl-3.0-standalone.html)
Credits
-------
Developer: (V18) Mangono , Contact: contact@mangono.fr
Contacts
--------
* Mail Contact : contact@mangono.fr
* Website : https://mangono.fr
Bug Tracker
-----------
In case of trouble, please contact us via our email address.
Maintainer
==========
.. image:: static/description/assets/logo/mangono-logo-bleu.png
:scale: 10 %
:target: https://mangono.fr
This module is maintained by Mangono.
For support and more information, please visit `Our Website <https://mangono.fr/>`__
Further information
===================
HTML Description: `<static/description/index.html>`__

View File

@@ -0,0 +1,3 @@
from . import controllers
from . import models
from .post_install import set_res_config_settings

View File

@@ -0,0 +1,50 @@
{
"name": "Report Generator | Carbone.io",
"version": "18.0.1.0.8",
"author": "Mangono",
"maintainer": "Mangono",
"summary": """
Automate documents with Carbone.io the universal, most efficient no-code document generation solution. Generate
custom documents and reports from Odoo data using customizable templates in Word, Excel, PowerPoint, LibreOffice,
or Google Docs. Convert and export to 100+ formats, including PDF, DOCX, XLSX, PPTX, CSV, HTML, XML, JPG, and PNG,
with enterprise features like charts, barcodes, images, forms, e-signatures, E-reporting, Factur-X, ZUGFeRD,
i18n, multi-language support, HTML rendering, and PDF merging. Use cases: Generate invoices, contracts, quotes,
delivery notes, payslips, purchase orders, sales presentations, sales reports, brochures, datasheets, SLAs, NDAs,
meeting notes, daybook reports, sales analysis, inventory reports, and product category reports.
""",
"category": "Reporting",
"depends": ["base", "base_setup", "web", "export_json"],
"description": "Report Generator | Carbone.io",
"website": "https://carbone.io/integration/odoo.html",
"data": [
"security/groups.xml",
"security/ir.model.access.csv",
"data/carbone_guide_attachment.xml",
"data/carbone_demo_template_purchase_order.xml",
"views/base/ir_actions_report.xml",
"views/base/ir_exports.xml",
"views/res/res_config_settings_carbone.xml",
"views/carbone/carbone_print_by_action.xml",
"views/carbone/report_carbone_menu.xml",
"views/carbone/carbone_translate.xml",
"views/carbone/carbone_translate_line.xml",
"views/carbone/carbone_create_report_wizard.xml",
],
"assets": {
"web.assets_backend": [
"report_carbone/static/src/js/report/action_manager_report.js",
"report_carbone/static/src/js/report/action_create_ir_report.js",
"report_carbone/static/src/js/report/copy_export_json.js",
"report_carbone/static/src/js/report/initiate_studio.js",
"report_carbone/static/src/scss/carbone.scss",
"report_carbone/static/src/xml/list_template.xml",
],
},
"test": [],
"images": ["static/description/banner.png"],
"installable": True,
"auto_install": False,
"license": "AGPL-3",
"application": False,
"post_init_hook": "set_res_config_settings",
}

101
report_carbone/const.py Normal file
View File

@@ -0,0 +1,101 @@
ALLOWED_EXTENSIONS = {
# Document
"pdf",
"docx",
"odt",
"bib",
"doc",
"doc6",
"doc95",
"docbook",
"docx7",
"fodt",
"html",
"latex",
"ooxml",
"ott",
"psw",
"rtf",
"sdw",
"stw",
"sxw",
"text",
"txt",
"uot",
"vor",
"xhtml",
"jpg",
"jpeg",
"png",
"epub",
"md",
"svg",
"webp",
"pages",
# Spreadsheet
"xlsx",
"ods",
"csv",
"dbf",
"dif",
"fods",
"ots",
"pxl",
"slk",
"stc",
"sxc",
"uos",
"xls",
"xls5",
"xls95",
"xlt",
"xlt5",
"xlt95",
"numbers",
"parquet",
"json",
# Presentation
"pptx",
"odp",
"bmp",
"emf",
"eps",
"fodp",
"gif",
"met",
"odg",
"pbm",
"pct",
"pgm",
"ppm",
"pwp",
"ras",
"sti",
"svm",
"swf",
"sxi",
"tiff",
"uop",
"wmf",
"otp",
"potm",
"pot",
"pps",
"ppt",
# Web
"etext",
"html10",
"mediawiki",
"sdw3",
"sdw4",
"text10",
"vor4",
# Graphics
"fodg",
"odd",
"otg",
"std",
"sxd",
"pub",
"cdr",
}

View File

@@ -0,0 +1,2 @@
from . import main
from . import config

View File

@@ -0,0 +1,15 @@
from odoo import http
from odoo.http import request
class CarboneConfigParamController(http.Controller):
@http.route("/carbone_config/carbone_studio_params", type="json", auth="user")
def get_carbone_studio_params(self):
carbone_url = request.env["ir.config_parameter"].sudo().get_param("report-engine.carbone_studio_url")
carbone_js_url = request.env["ir.config_parameter"].sudo().get_param("report-engine.carbone_js_file_url")
return {"studio_url": carbone_url, "js_url": carbone_js_url}
@http.route("/carbone_config/carbone_api_key", type="json", auth="user")
def get_carbone_api_key(self):
value = request.env["res.config.settings"].sudo().retrieve_carbone_api_key(test_mode_key=True)
return {"token": value}

View File

@@ -0,0 +1,134 @@
import json
import logging
import mimetypes
import requests
import werkzeug.exceptions
from werkzeug.urls import url_decode
from odoo import _, api, exceptions, http
from odoo.http import content_disposition, request
from odoo.tools import html_escape
from odoo.addons.web.controllers.report import ReportController
_logger = logging.getLogger(__name__)
def _get_headers(extension, content, filename):
if extension == "zip":
mime_type = "application/zip"
else:
mime_type, _ = mimetypes.guess_type(f"file.{extension}")
if not mime_type:
mime_type = "application/octet-stream" # generic fallback
header = [
("Content-Type", mime_type),
("Content-Length", len(content)),
("X-Content-Type-Options", "nosniff"),
]
if filename:
header.append(("Content-Disposition", content_disposition(filename)))
return header
class CarboneReportController(ReportController):
@classmethod
def handle_response(cls, response: requests.Response, raise_exception=True) -> str | dict:
try:
response_json = response.json()
except ValueError:
response_json = {}
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
response_error_message = response_json.get("message", response.text) if response_json else response.text
error_message = f"Carbone API Error: {response.status_code}: {response_error_message}"
_logger.error("Carbone API Error %s: ", response.status_code, exc_info=e)
if raise_exception:
raise exceptions.UserError(error_message) from e
return error_message
return response_json
@api.model
def check_carbone_report(self, carbone_report: "odoo.model.ir_actions_report"):
if not carbone_report:
raise exceptions.UserError(_("No report with this name and Carbone type was found."))
if not carbone_report.name:
raise exceptions.UserError(_("Please add a name to the report in order to export it."))
@api.model
def handle_exception_error(self, e, reportname: str):
_logger.exception("Error while generating report %s", reportname)
se = http.serialize_exception(e)
error = {"code": 200, "message": "Odoo Server Error", "data": se}
res = request.make_response(html_escape(json.dumps(error)))
raise werkzeug.exceptions.InternalServerError(response=res) from e
@http.route(
[
"/report/<converter>/<reportname>",
"/report/<converter>/<reportname>/<docids>",
],
type="http",
auth="user",
website=True,
readonly=True,
)
def report_routes(self, reportname: str, docids: str | None = None, converter=None, **data):
if converter != "carbone":
return super().report_routes(reportname, docids, converter, **data)
context = dict(request.env.context)
context.update({"from_ir_report_controller": True})
if data.get("options"):
data.update(json.loads(data.pop("options")))
if data.get("context"):
context.update(json.loads(data["context"]))
request.update_context(**context)
# Retrieval of ir.actions.report
carbone_report = request.env["ir.actions.report"]._get_report_from_name(reportname)
self.check_carbone_report(carbone_report)
report_content, extension = request.env["ir.actions.report"]._render_carbone(carbone_report, docids)
filename = f"{carbone_report.report_name}.{extension}"
headers = _get_headers(extension, report_content, filename)
return request.make_response(report_content, headers)
def _call_carbone_converter(self, docids: str, reportname: str, context: str, url: "str"):
if docids:
# Generic report:
response = self.report_routes(reportname, docids=docids, converter="carbone", context=context)
else:
# Particular report:
data = dict(url_decode(url.split("?")[1]).items()) # decoding the args represented in JSON
if "context" in data:
context, data_context = json.loads(context or "{}"), json.loads(data.pop("context"))
context = json.dumps({**context, **data_context})
response = self.report_routes(reportname, converter="carbone", context=context, **data)
return response
@http.route(["/report/download"], type="http", auth="user")
def report_download(self, data, context=None):
"""
Overload of the function to handle ir.actions.reports of "Carbone" type
:param data: data: a javascript array JSON.stringified contain report internal url ([0]) and
type [1]
:param context:
:return: Response with an attachment header
"""
requestcontent = json.loads(data)
url, report_type = requestcontent[0], requestcontent[1]
if report_type != "carbone":
return super().report_download(data, context)
reportname = url
try:
reportname = url.split("/report/carbone/")[1].split("?")[0]
docids = None
if "/" in reportname:
reportname, docids = reportname.split("/")
response = self._call_carbone_converter(docids, reportname, context, url)
return response
except Exception as e:
self.handle_exception_error(e, reportname)

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="report_carbone_demo_purchase_order" model="ir.attachment">
<field name="name">Demo_template_purchase_order.odt</field>
<field name="datas" type="base64" file="report_carbone/data/demo_template_purchase_order.odt"/>
<field name="public">True</field>
</record>
</odoo>

Some files were not shown because too many files have changed in this diff Show More