[ADD] report_carbone, jsonifier, export_json : carbone is an alternative to Py3o
1
export_json/README.md
Normal file
@@ -0,0 +1 @@
|
||||
JSON Export system for Odoo Community edition.
|
||||
47
export_json/README.rst
Normal 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
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import controller
|
||||
20
export_json/__manifest__.py
Normal 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"],
|
||||
}
|
||||
1
export_json/controller/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
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
|
||||
25
export_json/docs/export_json_userguide.adoc
Normal 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é.
|
||||
BIN
export_json/docs/images/export_json2.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
export_json/docs/images/export_json3_popup_export.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
export_json/docs/images/export_json_popup_export2.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
export_json/docs/images/process_export1.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
1
export_json/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import file_json
|
||||
20
export_json/models/file_json.py
Normal 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]
|
||||
8
export_json/pyproject.toml
Normal 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",
|
||||
]
|
||||
|
After Width: | Height: | Size: 892 KiB |
|
After Width: | Height: | Size: 799 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
BIN
export_json/static/description/assets/gifs/report-translated.gif
Normal file
|
After Width: | Height: | Size: 736 KiB |
BIN
export_json/static/description/assets/logo/mangono-logo-bleu.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
export_json/static/description/assets/logo/mangono-logo.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 248 KiB |
BIN
export_json/static/description/assets/screenshots/new_button.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -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 |
3
export_json/static/description/assets/svg/green-dot.svg
Normal 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 |
BIN
export_json/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
export_json/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
153
export_json/static/description/index.html
Normal 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 Odoo’s 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 & 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 Odoo’s 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>
|
||||
187
export_json/static/src/css/description.css
Normal 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;
|
||||
}
|
||||
1
export_json/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_export_json
|
||||
821
export_json/tests/test_export_json.py
Normal 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")
|
||||