[ADD] report_carbone, jsonifier, export_json : carbone is an alternative to Py3o
This commit is contained in:
1
export_json/controller/__init__.py
Normal file
1
export_json/controller/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
432
export_json/controller/main.py
Normal file
432
export_json/controller/main.py
Normal 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
|
||||
Reference in New Issue
Block a user