[IMP] report_carbone : bump to version 18.0.1.1.4
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import inspect
|
||||
import json
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from requests import Response
|
||||
from unittest.mock import MagicMock, Mock, mock_open, patch
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo.tests import common, tagged
|
||||
|
||||
from odoo.addons.base.models.ir_actions_report import IrActionsReport
|
||||
from odoo.addons.export_json.controller import main as controller_module
|
||||
|
||||
from ..models.base.exceptions import MissingApiKeyError
|
||||
@@ -65,6 +65,8 @@ class TestIrActionsReport(common.TransactionCase):
|
||||
self.docids = f"{self.partner_demo.id},{self.partner_admin.id}"
|
||||
self.env["ir.config_parameter"].set_param("report-engine.stage_api_key", "mock_key")
|
||||
self._setup_patcher(IrActionsReportCarbone, "call_carbone_endpoint", Mock(return_value={}))
|
||||
# Force template_id on every record found (guards against stale DB data from previous runs)
|
||||
self.report.template_id = "mock_carbone_template_id"
|
||||
|
||||
def test_generate_report_carbone_without_api_keys(self):
|
||||
"""MissingApiKeyError has to be raised when we try to generate a carbone report without an API key."""
|
||||
@@ -91,6 +93,58 @@ class TestIrActionsReport(common.TransactionCase):
|
||||
ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name)
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
|
||||
def test_render_carbone_signature_matches_odoo_render_dispatch_contract(self):
|
||||
"""`ir.actions.report._render()` is Odoo's generic dispatcher: for a report of type
|
||||
"xxx" it looks up `_render_xxx` and calls it as `render_func(report_ref, res_ids,
|
||||
data=data)`. `_render_carbone` must stay callable that way, and its result must still
|
||||
be unpackable as a 2-element `(content, report_type)` tuple like every other
|
||||
`_render_<type>` method (see `_render_qweb_pdf`, used here as the reference contract).
|
||||
If Odoo ever changes that convention, this test breaks and flags the incompatibility."""
|
||||
dispatch_source = inspect.getsource(IrActionsReport._render)
|
||||
self.assertIn(
|
||||
"render_func(report_ref, res_ids, data=data)",
|
||||
dispatch_source,
|
||||
"odoo.addons.base.models.ir_actions_report.IrActionsReport._render no longer calls "
|
||||
"render_func(report_ref, res_ids, data=data): the assumed dispatch contract for "
|
||||
"_render_carbone has changed upstream.",
|
||||
)
|
||||
|
||||
carbone_signature = inspect.signature(IrActionsReportCarbone._render_carbone)
|
||||
reference_signature = inspect.signature(IrActionsReport._render_qweb_pdf)
|
||||
|
||||
for signature, label in ((carbone_signature, "_render_carbone"), (reference_signature, "_render_qweb_pdf")):
|
||||
params = list(signature.parameters.values())
|
||||
self.assertGreaterEqual(len(params), 3, f"{label} must accept (self, report_ref, res_ids/docids, ...)")
|
||||
self.assertEqual(params[0].name, "self")
|
||||
self.assertIn(
|
||||
params[1].kind,
|
||||
(inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY),
|
||||
f"{label}'s 2nd parameter (report_ref) must be positional",
|
||||
)
|
||||
self.assertIn(
|
||||
params[2].kind,
|
||||
(inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY),
|
||||
f"{label}'s 3rd parameter (res_ids/docids) must be positional",
|
||||
)
|
||||
self.assertIn("data", signature.parameters, f"{label} must accept a 'data' parameter")
|
||||
self.assertEqual(
|
||||
signature.parameters["data"].default,
|
||||
None,
|
||||
f"{label}'s 'data' parameter must default to None",
|
||||
)
|
||||
|
||||
# Simulate the exact call performed by the generic dispatcher, unbound (self is the
|
||||
# positional receiver): render_func(report_ref, res_ids, data=data).
|
||||
ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name)
|
||||
bound = carbone_signature.bind(
|
||||
ir_actions_report_carbone, ir_actions_report_carbone, "1,2", data={"some": "data"}
|
||||
)
|
||||
bound.apply_defaults()
|
||||
|
||||
with patch.object(IrActionsReportCarbone, "_render_carbone_full", return_value=(b"content", "file.pdf", "pdf")):
|
||||
content, report_type = IrActionsReportCarbone._render_carbone(*bound.args, **bound.kwargs)
|
||||
self.assertEqual((content, report_type), (b"content", "pdf"))
|
||||
|
||||
def test_get_report_from_name(self):
|
||||
# ir_actions_report found with name and report_type == "carbone':
|
||||
res = self.report_object._get_report_from_name(self.report_name)
|
||||
@@ -114,9 +168,9 @@ class TestIrActionsReport(common.TransactionCase):
|
||||
with (
|
||||
patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_render_report),
|
||||
patch("carbone_sdk.CarboneSDK.get_report", return_value=mock_get_report),
|
||||
self.assertRaises(exceptions.UserError) as error,
|
||||
):
|
||||
with self.assertRaises(exceptions.UserError) as error:
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
|
||||
self.assertEqual(
|
||||
str(error.exception),
|
||||
@@ -127,21 +181,12 @@ class TestIrActionsReport(common.TransactionCase):
|
||||
def test_failed_generate_report_carbone_status_code_400(self):
|
||||
ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name)
|
||||
|
||||
def post(url, **kwargs):
|
||||
response = Response()
|
||||
if url.startswith("https://api.carbone.io/render/mock_carbone_template_id"):
|
||||
response.status_code = 400 # NotFileError
|
||||
response._content = json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "NotFileError",
|
||||
}
|
||||
).encode("utf-8")
|
||||
return response
|
||||
|
||||
with patch("requests.post", post):
|
||||
with self.assertRaises(exceptions.UserError) as error:
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
mock_result = {"success": False, "error": "NotFileError"}
|
||||
with (
|
||||
patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result),
|
||||
self.assertRaises(exceptions.UserError) as error,
|
||||
):
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
self.assertEqual(
|
||||
str(error.exception),
|
||||
"An error occurred when generating the report via Carbone : Carbone SDK render error: NotFileError",
|
||||
@@ -150,21 +195,12 @@ class TestIrActionsReport(common.TransactionCase):
|
||||
def test_failed_generate_report_carbone_status_code_500(self):
|
||||
ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name)
|
||||
|
||||
def post(url, **kwargs):
|
||||
response = Response()
|
||||
if url.startswith("https://api.carbone.io/render/mock_carbone_template_id"):
|
||||
response.status_code = 500 # GenerateReportError
|
||||
response._content = json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "GenerateReportError",
|
||||
}
|
||||
).encode("utf-8")
|
||||
return response
|
||||
|
||||
with patch("requests.post", post):
|
||||
with self.assertRaises(exceptions.UserError) as error:
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
mock_result = {"success": False, "error": "GenerateReportError"}
|
||||
with (
|
||||
patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result),
|
||||
self.assertRaises(exceptions.UserError) as error,
|
||||
):
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
self.assertEqual(
|
||||
str(error.exception),
|
||||
"An error occurred when generating the report via Carbone : Carbone SDK render error: GenerateReportError",
|
||||
@@ -180,9 +216,9 @@ class TestIrActionsReport(common.TransactionCase):
|
||||
patch("carbone_sdk.CarboneSDK.generate_template_id", return_value="no_template_carbone_id"),
|
||||
patch("carbone_sdk.CarboneSDK.add_template", return_value=mock_result),
|
||||
patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result),
|
||||
self.assertRaises(exceptions.UserError) as error,
|
||||
):
|
||||
with self.assertRaises(exceptions.UserError) as error:
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
self.assertEqual(
|
||||
str(error.exception),
|
||||
"An error occurred when generating the report via Carbone : Carbone SDK render error:mock_error_message",
|
||||
@@ -192,9 +228,8 @@ class TestIrActionsReport(common.TransactionCase):
|
||||
# Error Carbone SDK render error: failled to generate the template id
|
||||
ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name)
|
||||
|
||||
with patch("os.path.exists", return_value=True):
|
||||
with self.assertRaises(exceptions.UserError) as error:
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
with patch("os.path.exists", return_value=True), self.assertRaises(exceptions.UserError) as error:
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
self.assertEqual(
|
||||
str(error.exception),
|
||||
"An error occurred when generating the report via Carbone :"
|
||||
@@ -225,9 +260,9 @@ class TestIrActionsReport(common.TransactionCase):
|
||||
with (
|
||||
patch("carbone_sdk.CarboneSDK.generate_template_id", return_value="no_template_carbone_id"),
|
||||
patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result),
|
||||
self.assertRaises(exceptions.UserError) as error,
|
||||
):
|
||||
with self.assertRaises(exceptions.UserError) as error:
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
self.assertEqual(
|
||||
str(error.exception),
|
||||
"An error occurred when generating the report via Carbone : Carbone SDK render error: render_id empty",
|
||||
@@ -237,14 +272,160 @@ class TestIrActionsReport(common.TransactionCase):
|
||||
# Default SDK's error
|
||||
ir_actions_report_carbone = self.report_object._get_report_from_name(self.report_name)
|
||||
mock_result = None
|
||||
with patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result):
|
||||
with self.assertRaises(exceptions.UserError) as error:
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
with (
|
||||
patch("carbone_sdk.CarboneSDK.render_report", return_value=mock_result),
|
||||
self.assertRaises(exceptions.UserError) as error,
|
||||
):
|
||||
ir_actions_report_carbone._render_carbone(ir_actions_report_carbone, self.docids)
|
||||
self.assertEqual(
|
||||
str(error.exception),
|
||||
"An error occurred when generating the report via Carbone : Carbone SDK render error: something went wrong",
|
||||
)
|
||||
|
||||
# region local_context
|
||||
|
||||
def test_is_json_serializable(self):
|
||||
"""_is_json_serializable accepts JSON primitives and rejects non-serialisable values
|
||||
(e.g. recordsets), so the Carbone payload never breaks csdk.render's JSON encoding."""
|
||||
report = self.report
|
||||
self.assertTrue(report._is_json_serializable("string"))
|
||||
self.assertTrue(report._is_json_serializable(42))
|
||||
self.assertTrue(report._is_json_serializable({"a": [1, 2, True, None]}))
|
||||
self.assertFalse(report._is_json_serializable(self.partner_demo)) # recordset
|
||||
self.assertFalse(report._is_json_serializable(object()))
|
||||
|
||||
def test_get_env_context_datas_to_export(self):
|
||||
"""_get_env_context_datas_to_export returns a JSON-safe `local_context` block: default_*
|
||||
keys and non-serialisable context values are filtered out, user/company info is added."""
|
||||
report = self.report.with_context(
|
||||
default_should_be_dropped=1,
|
||||
custom_key="kept",
|
||||
non_serialisable=self.partner_demo, # recordset -> not JSON serialisable
|
||||
)
|
||||
result = report._get_env_context_datas_to_export()
|
||||
|
||||
# Structure.
|
||||
self.assertIn("local_context", result)
|
||||
local_context = result["local_context"]
|
||||
self.assertEqual(set(local_context), {"context", "user", "company"})
|
||||
|
||||
# clean_context: default_* dropped, plain key kept.
|
||||
context = local_context["context"]
|
||||
self.assertNotIn("default_should_be_dropped", context)
|
||||
self.assertEqual(context.get("custom_key"), "kept")
|
||||
|
||||
# Non-serialisable values are filtered out.
|
||||
self.assertNotIn("non_serialisable", context)
|
||||
|
||||
# User / company info is populated from the environment.
|
||||
self.assertEqual(local_context["user"]["name"], self.env.user.name)
|
||||
self.assertEqual(local_context["user"]["login"], self.env.user.login)
|
||||
self.assertEqual(local_context["company"]["name"], self.env.user.company_id.name)
|
||||
|
||||
# The whole block must be JSON serialisable (it is sent to csdk.render, which JSON-encodes it).
|
||||
json.dumps(result)
|
||||
|
||||
# endregion local_context
|
||||
|
||||
# region deploy_carbone_template
|
||||
|
||||
def _clear_template_id(self):
|
||||
"""Return the demo report with its template_id reset (call_carbone_endpoint
|
||||
is mocked in setUp, so the write() override does not hit the network)."""
|
||||
report = self.report
|
||||
report.template_id = False
|
||||
return report
|
||||
|
||||
def test_carbone_template_exists(self):
|
||||
report = self.report
|
||||
# call_carbone_endpoint is mocked to {} in setUp -> no data -> not found.
|
||||
self.assertFalse(report._carbone_template_exists("whatever"))
|
||||
|
||||
with patch.object(IrActionsReportCarbone, "call_carbone_endpoint", return_value={"data": [{"id": "x"}]}):
|
||||
self.assertTrue(report._carbone_template_exists("x"))
|
||||
|
||||
def test_deploy_carbone_template_no_api_key(self):
|
||||
"""Garde A: without a configured API key, nothing happens."""
|
||||
self.env["ir.config_parameter"].set_param("report-engine.stage_api_key", False)
|
||||
self.env["ir.config_parameter"].set_param("report-engine.prod_api_key", False)
|
||||
report = self._clear_template_id()
|
||||
|
||||
with patch.object(IrActionsReportCarbone, "get_carbone_sdk") as mock_sdk:
|
||||
self.assertFalse(report.deploy_carbone_template("/tmp/template.docx"))
|
||||
mock_sdk.assert_not_called()
|
||||
self.assertFalse(report.template_id)
|
||||
|
||||
def test_deploy_carbone_template_set_once(self):
|
||||
"""Garde B: a report that already has a template_id is left untouched."""
|
||||
self.env["ir.config_parameter"].set_param("report-engine.is_stage_mode", True)
|
||||
report = self.report
|
||||
report.template_id = "mock_carbone_template_id"
|
||||
self.assertTrue(report.template_id)
|
||||
|
||||
with patch.object(IrActionsReportCarbone, "get_carbone_sdk") as mock_sdk:
|
||||
self.assertFalse(report.deploy_carbone_template("/tmp/template.docx"))
|
||||
mock_sdk.assert_not_called()
|
||||
|
||||
def test_deploy_carbone_template_file_not_found(self):
|
||||
self.env["ir.config_parameter"].set_param("report-engine.is_stage_mode", True)
|
||||
report = self._clear_template_id()
|
||||
|
||||
with patch("os.path.exists", return_value=False), self.assertRaises(exceptions.UserError):
|
||||
report.deploy_carbone_template("/tmp/missing.docx")
|
||||
|
||||
def test_deploy_carbone_template_already_on_carbone(self):
|
||||
"""Template already known by Carbone: stable template_id retrieved via GET, no upload."""
|
||||
self.env["ir.config_parameter"].set_param("report-engine.is_stage_mode", True)
|
||||
report = self._clear_template_id()
|
||||
|
||||
mock_csdk = MagicMock()
|
||||
mock_csdk.generate_template_id.return_value = "hash_id"
|
||||
|
||||
def mock_endpoint(endpoint, params=None, raise_exception=True, method="GET", **kwargs):
|
||||
return {"data": [{"id": "stable_template_id", "type": "docx"}]}
|
||||
|
||||
with (
|
||||
patch("os.path.exists", return_value=True),
|
||||
patch.object(IrActionsReportCarbone, "get_carbone_sdk", return_value=mock_csdk),
|
||||
patch.object(IrActionsReportCarbone, "_carbone_template_exists", return_value=True),
|
||||
patch.object(IrActionsReportCarbone, "call_carbone_endpoint", side_effect=mock_endpoint),
|
||||
):
|
||||
res = report.deploy_carbone_template("/tmp/template.docx")
|
||||
|
||||
self.assertEqual(res, "stable_template_id")
|
||||
# No upload: the template already existed on Carbone, SDK add_template never called.
|
||||
mock_csdk.add_template.assert_not_called()
|
||||
self.assertEqual(report.template_id, "stable_template_id")
|
||||
|
||||
def test_deploy_carbone_template_uploads_when_missing(self):
|
||||
"""Template unknown by Carbone: upload via call_carbone_endpoint with deployedAt."""
|
||||
self.env["ir.config_parameter"].set_param("report-engine.is_stage_mode", True)
|
||||
report = self._clear_template_id()
|
||||
|
||||
mock_csdk = MagicMock()
|
||||
mock_csdk.generate_template_id.return_value = "hash_id"
|
||||
|
||||
def mock_endpoint(endpoint, params=None, raise_exception=True, method="GET", **kwargs):
|
||||
if method == "POST" and endpoint == "template":
|
||||
return {"data": {"id": "new_template_id"}}
|
||||
return {}
|
||||
|
||||
with (
|
||||
patch("os.path.exists", return_value=True),
|
||||
patch("builtins.open", mock_open(read_data=b"template_content")),
|
||||
patch.object(IrActionsReportCarbone, "get_carbone_sdk", return_value=mock_csdk),
|
||||
patch.object(IrActionsReportCarbone, "_carbone_template_exists", return_value=False),
|
||||
patch.object(IrActionsReportCarbone, "call_carbone_endpoint", side_effect=mock_endpoint),
|
||||
):
|
||||
res = report.deploy_carbone_template("/tmp/template.docx")
|
||||
|
||||
self.assertEqual(res, "new_template_id")
|
||||
# SDK add_template must NOT be used — upload goes through call_carbone_endpoint.
|
||||
mock_csdk.add_template.assert_not_called()
|
||||
self.assertEqual(report.template_id, "new_template_id")
|
||||
|
||||
# endregion deploy_carbone_template
|
||||
|
||||
# region utils
|
||||
|
||||
def get_not_use_id(self, model_name: "str") -> int:
|
||||
|
||||
Reference in New Issue
Block a user