diff --git a/.editorconfig b/.editorconfig
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_LmVkaXRvcmNvbmZpZw==..81ae7213f0cd37e1e90235f732c751ad607d49e8_LmVkaXRvcmNvbmZpZw== 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -11,7 +11,7 @@
 indent_size = 2
 
 # Do not configure editor for libs
-[{*/static/{lib,src/lib}/**}]
+[*/static/{lib,src/lib}/**]
 charset = unset
 end_of_line = unset
 indent_size = unset
diff --git a/.flake8 b/.flake8
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_LmZsYWtlOA==..81ae7213f0cd37e1e90235f732c751ad607d49e8_LmZsYWtlOA== 100644
--- a/.flake8
+++ b/.flake8
@@ -1,3 +1,5 @@
 [flake8]
+# Some leniancy for docstrings black puts in single lines (add 3 ", 79+3=82).
+max-line-length = 82
 per-file-ignores=
     __init__.py:F401
@@ -2,2 +4,3 @@
 per-file-ignores=
     __init__.py:F401
+    __manifest__.py:B018
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_LmdpdGxhYi1jaS55bWw=..81ae7213f0cd37e1e90235f732c751ad607d49e8_LmdpdGxhYi1jaS55bWw= 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,3 +1,3 @@
 include:
   - project: xcg/ci-templates
-    file: /odoo/13.0/gitlab-ci.yaml
+    file: /odoo/15.0/gitlab-ci.yaml
diff --git a/.hgconf b/.hgconf
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_LmhnY29uZg==..81ae7213f0cd37e1e90235f732c751ad607d49e8_LmhnY29uZg== 100644
--- a/.hgconf
+++ b/.hgconf
@@ -1,5 +1,5 @@
 [converter]
 pulluri = https://orus.io/xcg/odoo-modules/converter
 layout = ../converter
-track = 13.0
+track = 15.0
 expand =
diff --git a/NEWS.rst b/NEWS.rst
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_TkVXUy5yc3Q=..81ae7213f0cd37e1e90235f732c751ad607d49e8_TkVXUy5yc3Q= 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -2,6 +2,11 @@
 Changelog
 =========
 
+15.0.1.0.0
+----------
+
+* Migrate to Odoo 15.0.
+
 13.0.3.4.0
 ----------
 
diff --git a/__manifest__.py b/__manifest__.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_X19tYW5pZmVzdF9fLnB5..81ae7213f0cd37e1e90235f732c751ad607d49e8_X19tYW5pZmVzdF9fLnB5 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -21,6 +21,6 @@
 {
     "name": "Redner",
     "license": "AGPL-3",
-    "version": "13.0.3.4.0",
+    "version": "15.0.1.0.0",
     "category": "Reporting",
     "author": "XCG Consulting",
@@ -25,6 +25,6 @@
     "category": "Reporting",
     "author": "XCG Consulting",
-    "website": "https://odoo.consulting/",
+    "website": "https://orbeet.io/",
     # converter: https://orus.io/xcg/odoo-modules/converter
     "depends": ["converter", "mail", "web"],
     "data": [
@@ -32,6 +32,5 @@
         "views/redner_template.xml",
         "views/mail_template.xml",
         "views/ir_actions_report.xml",
-        "views/assets.xml",
         "views/menu.xml",
     ],
@@ -36,5 +35,10 @@
         "views/menu.xml",
     ],
+    "assets": {
+        "web.assets_backend": [
+            "redner/static/src/js/redner_report_action.esm.js"
+        ],
+    },
     "installable": True,
     # These dependencies are in the "requirements" file.
     "external_dependencies": {"python": ["requests_unixsocket"]},
diff --git a/controllers/main.py b/controllers/main.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_Y29udHJvbGxlcnMvbWFpbi5weQ==..81ae7213f0cd37e1e90235f732c751ad607d49e8_Y29udHJvbGxlcnMvbWFpbi5weQ== 100644
--- a/controllers/main.py
+++ b/controllers/main.py
@@ -21,7 +21,8 @@
 import json
 import mimetypes
 
-from werkzeug import exceptions, url_decode
+from werkzeug import exceptions
+from werkzeug.urls import url_decode
 
 from odoo.http import request, route
 from odoo.tools import html_escape
@@ -34,6 +35,10 @@
 
 
 class ReportController(main.ReportController):
+    """Add redner report downloads within report controllers.
+    Much of this code comes from OCA modules, the latest one being report_xlsx.
+    """
+
     @route()
     def report_routes(self, reportname, docids=None, converter=None, **data):
         if converter != "redner":
@@ -67,7 +72,7 @@
                 description="Redner action report not found for report_name "
                 "%s" % reportname
             )
-        res, filetype = action_redner_report.render(docids, data)
+        res, filetype = action_redner_report._render(docids, data)
         filename = action_redner_report.gen_report_download_filename(
             docids, data
         )
@@ -82,7 +87,8 @@
         return request.make_response(res, headers=http_headers)
 
     @route()
-    def report_download(self, data, token):
-        """This function is used by 'redneractionmanager.js' in order to
-        trigger the download of a redner/controller report.
+    def report_download(self, data, context=None):
+        """This function is used by 'action_manager_report.js' in order to
+        trigger the download of a pdf/controller report.
+
         :param data: a javascript array JSON.stringified containg report
@@ -88,7 +94,7 @@
         :param data: a javascript array JSON.stringified containg report
-        internal url ([0]) and type [1]
-        :returns: Response with a filetoken cookie and an attachment header
+            internal url ([0]) and type [1]
+        :returns: Response with an attachment header
         """
         requestcontent = json.loads(data)
         url, report_type = requestcontent[0], requestcontent[1]
         if "redner" not in report_type:
@@ -91,8 +97,8 @@
         """
         requestcontent = json.loads(data)
         url, report_type = requestcontent[0], requestcontent[1]
         if "redner" not in report_type:
-            return super().report_download(data, token)
+            return super().report_download(data, context=context)
         try:
             reportname = url.split("/report/redner/")[1].split("?")[0]
             docids = None
@@ -102,9 +108,12 @@
             if docids:
                 # Generic report:
                 response = self.report_routes(
-                    reportname, docids=docids, converter="redner"
+                    reportname,
+                    docids=docids,
+                    converter="redner",
+                    context=context,
                 )
             else:
                 # Particular report:
                 # decoding the args represented in JSON
                 data = list(url_decode(url.split("?")[1]).items())
@@ -106,6 +115,11 @@
                 )
             else:
                 # Particular report:
                 # decoding the args represented in JSON
                 data = list(url_decode(url.split("?")[1]).items())
+                if "context" in data:
+                    context, data_context = json.loads(
+                        context or "{}"
+                    ), json.loads(data.pop("context"))
+                    context = json.dumps({**context, **data_context})
                 response = self.report_routes(
@@ -111,3 +125,3 @@
                 response = self.report_routes(
-                    reportname, converter="redner", **dict(data)
+                    reportname, converter="redner", context=context, **data
                 )
@@ -113,5 +127,4 @@
                 )
-            response.set_cookie("fileToken", token)
             return response
         except Exception as e:
             se = _serialize_exception(e)
diff --git a/doc/autotodo.py b/doc/autotodo.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_ZG9jL2F1dG90b2RvLnB5..81ae7213f0cd37e1e90235f732c751ad607d49e8_ZG9jL2F1dG90b2RvLnB5 100755
--- a/doc/autotodo.py
+++ b/doc/autotodo.py
@@ -22,6 +22,7 @@
 import os
 import os.path
 import sys
+from collections.abc import Mapping
 
 
 def main():
@@ -33,5 +34,6 @@
     exts = sys.argv[2].split(",")
     tags = sys.argv[3].split(",")
     todolist = {tag: [] for tag in tags}
+    path_file_length: Mapping[str, int] = {}
 
     for root, _dirs, files in os.walk(folder):
@@ -36,6 +38,6 @@
 
     for root, _dirs, files in os.walk(folder):
-        scan_folder((exts, tags, todolist), root, files)
-    create_autotodo(folder, todolist)
+        scan_folder((exts, tags, todolist, path_file_length), root, files)
+    create_autotodo(folder, todolist, path_file_length)
 
 
@@ -40,6 +42,6 @@
 
 
-def write_info(f, infos, folder):
+def write_info(f, infos, folder, path_file_length: Mapping[str, int]):
     # Check sphinx version for lineno-start support
 
     import sphinx
@@ -52,7 +54,7 @@
     for i in infos:
         path = i[0]
         line = i[1]
-        lines = (line - 3, line + 4)
+        lines = (line - 3, min(line + 4, path_file_length[path]))
         class_name = ":class:`%s`" % os.path.basename(
             os.path.splitext(path)[0]
         )
@@ -71,7 +73,7 @@
                 path,
                 lines[0],
                 lines[1],
-                line,
+                4,
             )
         )
         if lineno_start:
@@ -79,7 +81,7 @@
         f.write("\n")
 
 
-def create_autotodo(folder, todolist):
+def create_autotodo(folder, todolist, path_file_length: Mapping[str, int]):
     with open("autotodo", "w+") as f:
         for tag, info in list(todolist.items()):
             f.write("%s\n%s\n\n" % (tag, "=" * len(tag)))
@@ -83,7 +85,7 @@
     with open("autotodo", "w+") as f:
         for tag, info in list(todolist.items()):
             f.write("%s\n%s\n\n" % (tag, "=" * len(tag)))
-            write_info(f, info, folder)
+            write_info(f, info, folder, path_file_length)
 
 
 def scan_folder(data_tuple, dirname, names):
@@ -87,8 +89,7 @@
 
 
 def scan_folder(data_tuple, dirname, names):
-    (exts, tags, res) = data_tuple
-    file_info = {}
+    (exts, tags, res, path_file_length) = data_tuple
     for name in names:
         (root, ext) = os.path.splitext(name)
         if ext in exts:
@@ -92,9 +93,11 @@
     for name in names:
         (root, ext) = os.path.splitext(name)
         if ext in exts:
-            file_info = scan_file(os.path.join(dirname, name), tags)
+            path = os.path.join(dirname, name)
+            file_info, length = scan_file(path, tags)
+            path_file_length[path] = length
             for tag, info in list(file_info.items()):
                 if info:
                     res[tag].extend(info)
 
 
@@ -96,7 +99,7 @@
             for tag, info in list(file_info.items()):
                 if info:
                     res[tag].extend(info)
 
 
-def scan_file(filename, tags):
+def scan_file(filename, tags) -> tuple[dict[str, tuple[str, int, str]], int]:
     res = {tag: [] for tag in tags}
@@ -102,6 +105,7 @@
     res = {tag: [] for tag in tags}
+    line_num: int = 0
     with open(filename, "r") as f:
         for line_num, line in enumerate(f):
             for tag in tags:
                 if tag in line:
                     res[tag].append((filename, line_num, line[:-1].strip()))
@@ -103,9 +107,9 @@
     with open(filename, "r") as f:
         for line_num, line in enumerate(f):
             for tag in tags:
                 if tag in line:
                     res[tag].append((filename, line_num, line[:-1].strip()))
-    return res
+    return res, line_num
 
 
 if __name__ == "__main__":
diff --git a/doc/conf.py b/doc/conf.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_ZG9jL2NvbmYucHk=..81ae7213f0cd37e1e90235f732c751ad607d49e8_ZG9jL2NvbmYucHk= 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -12,5 +12,5 @@
 import os
 import sys
 
-import odoo
+from odoo_scripts.config import Configuration
 
@@ -16,5 +16,5 @@
 
-from odoo_scripts.config import Configuration
+import odoo
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
@@ -33,6 +33,7 @@
     "sphinx.ext.todo",
     "sphinx.ext.coverage",
     "sphinx.ext.graphviz",
+    "sphinx.ext.viewcode",
     "sphinxodoo.ext.autodoc",
 ]
 
@@ -60,7 +61,7 @@
 
 # General information about the project.
 project = d["name"]
-copyright = "2021 XCG Consulting"
+copyright = "2021, 2022 XCG Consulting"
 author = d["author"]
 module_nospace = project.replace(" ", "")
 module_description = d.get("summary", "")
@@ -150,7 +151,7 @@
 # odoo-sphinx-autodoc
 #
 
-# sphinxodoo_addons: List of addons name to load (if empty, no addon will be
+# sphinxodoo_addons : List of addons name to load (if empty, no addon will be
 # loaded)
 this_module = os.path.basename(
     os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -171,7 +172,7 @@
         c.read(setup_path)
         if c.has_section("odoo_scripts"):
             # reload with odoo_scripts
-            c = Configuration(setup_path)
+            c = Configuration(os.path.dirname(setup_path))
         else:
             c = None
     if not c:
@@ -180,7 +181,7 @@
         else:
             directory = None
 
-sphinxodoo_addons_path = []
+sphinxodoo_addons_path = list()
 
 if c:
     addon_dirs = set(os.path.dirname(path) for path in c.modules)
diff --git a/doc/index.rst b/doc/index.rst
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_ZG9jL2luZGV4LnJzdA==..81ae7213f0cd37e1e90235f732c751ad607d49e8_ZG9jL2luZGV4LnJzdA== 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -3,7 +3,7 @@
 Contents:
 
 .. toctree::
-   :maxdepth: 2
+   :maxdepth: 4
 
    modules
    NEWS
diff --git a/doc/modules.rst b/doc/modules.rst
new file mode 100644
index 0000000000000000000000000000000000000000..81ae7213f0cd37e1e90235f732c751ad607d49e8_ZG9jL21vZHVsZXMucnN0
--- /dev/null
+++ b/doc/modules.rst
@@ -0,0 +1,7 @@
+odoo_module
+===========
+
+.. toctree::
+   :maxdepth: 4
+
+   odoo_module
diff --git a/models/ir_actions_report.py b/models/ir_actions_report.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_bW9kZWxzL2lyX2FjdGlvbnNfcmVwb3J0LnB5..81ae7213f0cd37e1e90235f732c751ad607d49e8_bW9kZWxzL2lyX2FjdGlvbnNfcmVwb3J0LnB5 100644
--- a/models/ir_actions_report.py
+++ b/models/ir_actions_report.py
@@ -20,7 +20,6 @@
 
 import base64
 import logging
-import time
 
 from odoo import _, api, fields, models
 from odoo.exceptions import AccessError, ValidationError
@@ -24,7 +23,7 @@
 
 from odoo import _, api, fields, models
 from odoo.exceptions import AccessError, ValidationError
-from odoo.tools.safe_eval import safe_eval
+from odoo.tools.safe_eval import safe_eval, time
 
 from ..utils.formats import Formats
 
@@ -72,7 +71,9 @@
             selections.append((name, description))
         return selections
 
-    report_type = fields.Selection(selection_add=[("redner", "redner")])
+    report_type = fields.Selection(
+        selection_add=[("redner", "redner")], ondelete={"redner": "cascade"}
+    )
 
     redner_multi_in_one = fields.Boolean(
         string="Multiple Records in a Single Redner Report",
@@ -173,7 +174,8 @@
             ]
         )
 
-    def render_redner(self, res_ids, data):
+    def _render_redner(self, res_ids, data=None):
+        """Called by ``_render``, method name dynamically built."""
         self.ensure_one()
         if self.report_type != "redner":
             raise RuntimeError(
@@ -183,7 +185,7 @@
         return (
             self.env["redner.report"]
             .create({"ir_actions_report_id": self.id})
-            .create_report(res_ids, data)
+            .create_report(res_ids, data or {})
         )
 
     def gen_report_download_filename(self, res_ids, data):
@@ -187,8 +189,7 @@
         )
 
     def gen_report_download_filename(self, res_ids, data):
-        """Override this function to change the name of the downloaded report
-        """
+        """Override this function to change the name of the downloaded report"""
 
         self.ensure_one()
         report = self.get_from_report_name(self.report_name, self.report_type)
@@ -236,7 +237,7 @@
 
         attachment_vals = {
             "name": attachment_name,
-            "datas": base64.encodestring(buffer.getvalue()),
+            "datas": base64.b64encode(buffer.getvalue()),
             "res_model": self.model,
             "res_id": record.id,
             "type": "binary",
diff --git a/models/mail_template.py b/models/mail_template.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_bW9kZWxzL21haWxfdGVtcGxhdGUucHk=..81ae7213f0cd37e1e90235f732c751ad607d49e8_bW9kZWxzL21haWxfdGVtcGxhdGUucHk= 100644
--- a/models/mail_template.py
+++ b/models/mail_template.py
@@ -110,6 +110,6 @@
 
         return values
 
-    def generate_email(self, res_ids, fields=None):
+    def generate_email(self, res_ids, fields):
         self.ensure_one()
 
@@ -114,6 +114,6 @@
         self.ensure_one()
 
-        results = super().generate_email(res_ids, fields=fields)
+        results = super().generate_email(res_ids, fields)
         if not self.is_redner_template:
             return results
 
@@ -130,7 +130,6 @@
         return self._patch_email_values(results, res_ids[0])
 
     def render_variable_hook(self, variables):
-        """Override to add additional variables in mail "render template" func
-        """
+        """Override to add additional variables in mail "render template" func"""
         variables.update({"image": lambda value: image(value)})
         return super().render_variable_hook(variables)
diff --git a/models/redner_template.py b/models/redner_template.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_bW9kZWxzL3JlZG5lcl90ZW1wbGF0ZS5weQ==..81ae7213f0cd37e1e90235f732c751ad607d49e8_bW9kZWxzL3JlZG5lcl90ZW1wbGF0ZS5weQ== 100644
--- a/models/redner_template.py
+++ b/models/redner_template.py
@@ -27,6 +27,8 @@
 
 logger = logging.getLogger(__name__)
 
+_redner = None
+
 
 class RednerTemplate(models.Model):
 
@@ -91,8 +93,6 @@
 
     template_data = fields.Binary("Libreoffice Template")
 
-    _redner = None
-
     @property
     def redner(self):
         """Try to avoid Redner instance to be over created"""
@@ -96,9 +96,10 @@
     @property
     def redner(self):
         """Try to avoid Redner instance to be over created"""
-        if self._redner is None:
+        global _redner
+        if _redner is None:
 
             # Bypass security rules when reading these configuration params. By
             # default, only some administrators have access to that model.
             config_model = self.env["ir.config_parameter"].sudo()
 
@@ -100,12 +101,12 @@
 
             # Bypass security rules when reading these configuration params. By
             # default, only some administrators have access to that model.
             config_model = self.env["ir.config_parameter"].sudo()
 
-            self._redner = Redner(
+            _redner = Redner(
                 config_model.get_param("redner.api_key"),
                 config_model.get_param("redner.server_url"),
                 config_model.get_param("redner.account"),
                 int(config_model.get_param("redner.timeout", default="20")),
             )
 
@@ -106,10 +107,10 @@
                 config_model.get_param("redner.api_key"),
                 config_model.get_param("redner.server_url"),
                 config_model.get_param("redner.account"),
                 int(config_model.get_param("redner.timeout", default="20")),
             )
 
-        return self._redner
+        return _redner
 
     @api.model
     def create(self, vals):
diff --git a/pyproject.toml b/pyproject.toml
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_cHlwcm9qZWN0LnRvbWw=..81ae7213f0cd37e1e90235f732c751ad607d49e8_cHlwcm9qZWN0LnRvbWw= 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,2 +1,18 @@
+[tool.isort]
+line_length = 79
+py_version = 310
+profile = "black"
+known_odoo = ['odoo']
+known_odoo_addons = ['odoo.addons']
+sections = [
+  'FUTURE',
+  'STDLIB',
+  'THIRDPARTY',
+  'ODOO',
+  'ODOO_ADDONS',
+  'FIRSTPARTY',
+  'LOCALFOLDER'
+]
+
 [tool.black]
 line-length = 79
@@ -1,3 +17,3 @@
 [tool.black]
 line-length = 79
-target = 3.8
+target = 3.10
diff --git a/redner.py b/redner.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_cmVkbmVyLnB5..81ae7213f0cd37e1e90235f732c751ad607d49e8_cmVkbmVyLnB5 100644
--- a/redner.py
+++ b/redner.py
@@ -23,7 +23,8 @@
 from urllib.parse import quote
 
 import requests
+import requests_unixsocket
 
 from odoo import _
 from odoo.exceptions import ValidationError
 
@@ -26,9 +27,7 @@
 
 from odoo import _
 from odoo.exceptions import ValidationError
 
-import requests_unixsocket
-
 _logger = logging.getLogger(__name__)
 
 REDNER_API_PATH = "api/v1/"
diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_c2VjdXJpdHkvaXIubW9kZWwuYWNjZXNzLmNzdg==..81ae7213f0cd37e1e90235f732c751ad607d49e8_c2VjdXJpdHkvaXIubW9kZWwuYWNjZXNzLmNzdg== 100644
--- a/security/ir.model.access.csv
+++ b/security/ir.model.access.csv
@@ -1,2 +1,3 @@
 id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_redner_report,access_redner_report,model_redner_report,,1,1,1,1
 access_redner_template,access_redner_template,model_redner_template,,1,1,1,1
@@ -2,2 +3,2 @@
 access_redner_template,access_redner_template,model_redner_template,,1,1,1,1
-access_redner_substitution,access_redner_substitution,model_redner_substitution,,1,1,1,1
\ No newline at end of file
+access_redner_substitution,access_redner_substitution,model_redner_substitution,,1,1,1,1
diff --git a/static/src/js/redneractionmanager.js b/static/src/js/redner_report_action.esm.js
similarity index 27%
rename from static/src/js/redneractionmanager.js
rename to static/src/js/redner_report_action.esm.js
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_c3RhdGljL3NyYy9qcy9yZWRuZXJhY3Rpb25tYW5hZ2VyLmpz..81ae7213f0cd37e1e90235f732c751ad607d49e8_c3RhdGljL3NyYy9qcy9yZWRuZXJfcmVwb3J0X2FjdGlvbi5lc20uanM= 100644
--- a/static/src/js/redneractionmanager.js
+++ b/static/src/js/redner_report_action.esm.js
@@ -1,3 +1,4 @@
+/** @odoo-module **/
 /*
 Redner Odoo module
 Copyright (C) 2016 XCG Consulting <https://xcg-consulting.fr>
@@ -16,6 +17,8 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-odoo.define("redner.report", function (require) {
-    "use strict";
+/* Add a report handler to download redner reports.
+ * Adapted from OCA's report_xlsx module:
+ * https://github.com/OCA/reporting-engine/blob/15.0/report_xlsx/static/src/js/report/action_manager_report.esm.js
+ */
 
@@ -21,3 +24,4 @@
 
-    var ActionManager = require("web.ActionManager");
+import {download} from "@web/core/network/download";
+import {registry} from "@web/core/registry";
 
@@ -23,8 +27,25 @@
 
-    ActionManager.include({
-        _executeReportAction: function (action, options) {
-            // Redner reports
-            if (action.report_type === "redner") {
-                console.log(action.report_type);
-                return this._triggerDownload(action, options, "redner");
+registry
+    .category("ir.actions.report handlers")
+    .add("redner_handler", async function (action, options, env) {
+        if (action.report_type === "redner") {
+            const type = action.report_type;
+            let url = `/report/${type}/${action.report_name}`;
+            const actionContext = action.context || {};
+            if (action.data && JSON.stringify(action.data) !== "{}") {
+                // Build a query string with `action.data` (it's the place where reports
+                // using a wizard to customize the output traditionally put their options)
+                const action_options = encodeURIComponent(JSON.stringify(action.data));
+                const context = encodeURIComponent(JSON.stringify(actionContext));
+                url += `?options=${action_options}&context=${context}`;
+            } else {
+                if (actionContext.active_ids) {
+                    url += `/${actionContext.active_ids.join(",")}`;
+                }
+                if (type === "redner") {
+                    const context = encodeURIComponent(
+                        JSON.stringify(env.services.user.context)
+                    );
+                    url += `?context=${context}`;
+                }
             }
@@ -30,25 +51,13 @@
             }
-            return this._super.apply(this, arguments);
-        },
-        _makeReportUrls: function (action) {
-            var reportUrls = this._super.apply(this, arguments);
-            reportUrls.redner = "/report/redner/" + action.report_name;
-            // We may have to build a query string with `action.data`. It's the place
-            // were report's using a wizard to customize the output traditionally put
-            // their options.
-            if (
-                _.isUndefined(action.data) ||
-                _.isNull(action.data) ||
-                (_.isObject(action.data) && _.isEmpty(action.data))
-            ) {
-                if (action.context.active_ids) {
-                    var activeIDsPath = "/" + action.context.active_ids.join(",");
-                    reportUrls.redner += activeIDsPath;
-                }
-            } else {
-                var serializedOptionsPath =
-                    "?options=" + encodeURIComponent(JSON.stringify(action.data));
-                serializedOptionsPath +=
-                    "&context=" + encodeURIComponent(JSON.stringify(action.context));
-                reportUrls.redner += serializedOptionsPath;
+            env.services.ui.block();
+            try {
+                await download({
+                    url: "/report/download",
+                    data: {
+                        data: JSON.stringify([url, action.report_type]),
+                        context: JSON.stringify(env.services.user.context),
+                    },
+                });
+            } finally {
+                env.services.ui.unblock();
             }
@@ -54,4 +63,14 @@
             }
-            return reportUrls;
-        },
+            const onClose = options.onClose;
+            if (action.close_on_report_download) {
+                return env.services.action.doAction(
+                    {type: "ir.actions.act_window_close"},
+                    {onClose}
+                );
+            } else if (onClose) {
+                onClose();
+            }
+            return Promise.resolve(true);
+        }
+        return Promise.resolve(false);
     });
@@ -57,2 +76,1 @@
     });
-});
diff --git a/tests/test_ir_actions_report.py b/tests/test_ir_actions_report.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_dGVzdHMvdGVzdF9pcl9hY3Rpb25zX3JlcG9ydC5weQ==..81ae7213f0cd37e1e90235f732c751ad607d49e8_dGVzdHMvdGVzdF9pcl9hY3Rpb25zX3JlcG9ydC5weQ== 100644
--- a/tests/test_ir_actions_report.py
+++ b/tests/test_ir_actions_report.py
@@ -72,7 +72,7 @@
             json=lambda: [{"body": base64.b64encode(b"test-rendered-report")}],
         )
         demo_user = self.env.ref("base.user_demo")
-        render_ret = self.report.render([demo_user.id])
+        render_ret = self.report._render([demo_user.id])
         requests_post_mock.assert_called_once_with(
             "https://test-redner-url/api/v1/render",
             json={
diff --git a/utils/formats.py b/utils/formats.py
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_dXRpbHMvZm9ybWF0cy5weQ==..81ae7213f0cd37e1e90235f732c751ad607d49e8_dXRpbHMvZm9ybWF0cy5weQ== 100644
--- a/utils/formats.py
+++ b/utils/formats.py
@@ -37,14 +37,14 @@
 
 class Format:
     """A format representation that contains:
-        a name we use in our applications
-        an ODF name (like: 'MS Word 2003 XML') which is the name you must
-        use as a filter to call a renderserver or a LibreOffice server
-        a mimetype that corresponds to the mimetype of the produced file
-        if you ask LibreOffice to convert to the corresponding format
-        and a simple flag that indicates if the format is Native or if it is
-        produced by calling a LibreOffice filter to convert the native
-        document to an "external format"
+    a name we use in our applications
+    an ODF name (like: 'MS Word 2003 XML') which is the name you must
+    use as a filter to call a renderserver or a LibreOffice server
+    a mimetype that corresponds to the mimetype of the produced file
+    if you ask LibreOffice to convert to the corresponding format
+    and a simple flag that indicates if the format is Native or if it is
+    produced by calling a LibreOffice filter to convert the native
+    document to an "external format"
     """
 
     def __init__(self, name, odfname, mimetype=DEFAULT_MIMETYPE, native=False):
diff --git a/views/assets.xml b/views/assets.xml
deleted file mode 100644
index 9b1520b0e1c3da9efe39cb1ee94a9e6b954ccf40_dmlld3MvYXNzZXRzLnhtbA==..0000000000000000000000000000000000000000
--- a/views/assets.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8" ?>
-<odoo>
-    <template id="assets_backend" name="redner assets" inherit_id="web.assets_backend">
-        <xpath expr="." position="inside">
-            <script
-                type="text/javascript"
-                src="/redner/static/src/js/redneractionmanager.js"
-            />
-        </xpath>
-    </template>
-</odoo>