[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

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