diff --git a/src/base/tests/test_util.py b/src/base/tests/test_util.py
index 91ebc1bc3..2bd24c8c6 100644
--- a/src/base/tests/test_util.py
+++ b/src/base/tests/test_util.py
@@ -2742,3 +2742,104 @@ def test_assert_updated_combo(self):
):
p2.city = "Niflheim"
util.flush(p2)
+
+
+class TestReportUtils(UnitTestCase):
+ def test_report_simple(self):
+ self.assertEqual(
+ util.report_with_list(
+ summary="Simple test with minimal arguments and no data.",
+ data=[],
+ columns=("id", "name"),
+ row_format="Partner {name} has id {id}.",
+ ),
+ "Simple test with minimal arguments and no data.",
+ )
+
+ def test_report_links(self):
+ self.assertEqual(
+ util.report_with_list(
+ summary="Testing links.",
+ data=[],
+ columns=("id", "name"),
+ row_format="Partner {partner_link}.",
+ links={"partner_link": ("res.partner", "id", "name")},
+ ),
+ "Testing links.",
+ )
+
+ def test_report_data_minimal(self):
+ href = (
+ "/odoo/res.partner/1?debug=1"
+ if util.version_gte("18.0")
+ else "/web?debug=1#view_type=form&model=res.partner&action=&id=1"
+ )
+ expected = (
+ "Test with minimal data. "
+ ).format(href)
+ self.assertEqual(
+ util.report_with_list(
+ summary="Test with minimal data.",
+ data=[(1, "Partner One")],
+ columns=("id", "name"),
+ row_format="Partner {partner_link}.",
+ links={"partner_link": ("res.partner", "id", "name")},
+ ),
+ expected,
+ )
+
+ def test_report_data_limited(self):
+ if util.version_gte("18.0"):
+ href1 = "/odoo/res.partner/1?debug=1"
+ href2 = "/odoo/res.partner/2?debug=1"
+ else:
+ href1 = "/web?debug=1#view_type=form&model=res.partner&action=&id=1"
+ href2 = "/web?debug=1#view_type=form&model=res.partner&action=&id=2"
+ expected = (
+ "Test with limited data.The total number of affected records is 3. This list is showing the first 2 records. "
+ ).format(href1, href2)
+ self.assertEqual(
+ util.report_with_list(
+ summary="Test with limited data.",
+ data=[(1, "Partner One"), (2, "Partner Two"), (3, "Partner Three")],
+ columns=("id", "name"),
+ row_format="Partner {partner_link}.",
+ links={"partner_link": ("res.partner", "id", "name")},
+ total=3,
+ limit=2,
+ ),
+ expected,
+ )
+
+ def test_report_data_limitless(self):
+ if util.version_gte("18.0"):
+ href1 = "/odoo/res.partner/1?debug=1"
+ href2 = "/odoo/res.partner/2?debug=1"
+ href3 = "/odoo/res.partner/3?debug=1"
+ else:
+ href1 = "/web?debug=1#view_type=form&model=res.partner&action=&id=1"
+ href2 = "/web?debug=1#view_type=form&model=res.partner&action=&id=2"
+ href3 = "/web?debug=1#view_type=form&model=res.partner&action=&id=3"
+ expected = (
+ "Test with limitless data. "
+ ).format(href1, href2, href3)
+ self.assertEqual(
+ util.report_with_list(
+ summary="Test with limitless data.",
+ data=[(1, "Partner One"), (2, "Partner Two"), (3, "Partner Three")],
+ columns=("id", "name"),
+ row_format="Partner {partner_link}.",
+ links={"partner_link": ("res.partner", "id", "name")},
+ limit=None,
+ ),
+ expected,
+ )
diff --git a/src/util/fields.py b/src/util/fields.py
index 65cfcb507..3d64d2c79 100644
--- a/src/util/fields.py
+++ b/src/util/fields.py
@@ -72,7 +72,7 @@ def make_index_name(table_name, column_name):
target_of,
)
from .records import _remove_import_export_paths
-from .report import add_to_migration_reports, get_anchor_link_to_record
+from .report import add_to_migration_reports, report_with_list
# python3 shims
try:
@@ -1597,11 +1597,26 @@ def _update_field_usage_multi(cr, models, old, new, domain_adapter=None, skip_in
)
""".format(col_prefix=col_prefix, name=get_value_or_en_translation(cr, "ir_act_server", "name"))
cr.execute(q, {"old_pattern": p["old_pattern"], "old": p["old"], "standard_modules": tuple(standard_modules)})
- li = ""
- if cr.rowcount:
- li = "".join(
- "
{}".format(get_anchor_link_to_record("ir.actions.server", aid, aname))
- for aid, aname in cr.fetchall()
+
+ model_text = "All models"
+ if only_models:
+ model_text = "Models " + ", ".join("{}".format(m) for m in only_models)
+ total = cr.rowcount
+ if total:
+ summary = (
+ "{model_text}: the field {old} has been renamed to {new}. "
+ "The following server actions may need an update. "
+ "If a server action is a standard one and you haven't made any modifications, you may ignore it."
+ ).format(model_text=model_text, old=old, new=new)
+ report_with_list(
+ summary=summary,
+ data=cr.fetchall(),
+ columns=("id", "name"),
+ row_format="{action_link}",
+ links={"action_link": ("ir.actions.server", "id", "name")},
+ total=total,
+ limit=20,
+ category="Fields renamed",
)
if column_exists(cr, "ir_model_fields", "compute"):
@@ -1620,28 +1635,24 @@ def _update_field_usage_multi(cr, models, old, new, domain_adapter=None, skip_in
)
"""
cr.execute(q, {"old_pattern": p["old_pattern"], "standard_modules": tuple(standard_modules)})
- if cr.rowcount:
- li += "".join(
- "{}".format(get_anchor_link_to_record("ir.model.fields", fid, fname))
- for fid, fname in cr.fetchall()
+
+ total = cr.rowcount
+ if total:
+ summary = (
+ "{model_text}: the field {old} has been renamed to {new}. "
+ "The following compute methods of other fields may need an update. "
+ "If a field is a standard one and you haven't made any modifications, you may ignore it."
+ ).format(model_text=model_text, old=old, new=new)
+ report_with_list(
+ summary=summary,
+ data=cr.fetchall(),
+ columns=("id", "name"),
+ row_format="{field_link}",
+ links={"field_link": ("ir.model.fields", "id", "name")},
+ total=total,
+ limit=20,
+ category="Fields renamed",
)
- if li:
- model_text = "All models"
- if only_models:
- model_text = "Models " + ", ".join("{}".format(m) for m in only_models)
- add_to_migration_reports(
- """
-
-
- {model_text}: the field {old} has been renamed to {new}. The following server actions and compute methods of other fields may need an update.
- If a server action or a field is a standard one and you haven't made any modifications, you may ignore them.
-
-
-
- """.format(**locals()),
- category="Fields renamed",
- format="html",
- )
# if we stay on the same model. (no usage of dotted-path) (only works for domains and related)
if "." not in old and "." not in new:
diff --git a/src/util/report.py b/src/util/report.py
index b6de3e3f6..1ce372135 100644
--- a/src/util/report.py
+++ b/src/util/report.py
@@ -107,7 +107,7 @@ def html_escape(text):
}
-def add_to_migration_reports(message, category="Other", format="text"):
+def report(message, category="Other", format="text"):
assert format in {"text", "html", "md", "rst"}
if format == "md":
message = md2html(dedent(message))
@@ -127,6 +127,91 @@ def add_to_migration_reports(message, category="Other", format="text"):
_logger.warning("Upgrade report is growing suspiciously long: %s characters so far.", migration_reports_length)
+add_to_migration_reports = report
+
+
+def report_with_summary(summary, details, category="Other"):
+ """Append the upgrade report with a new entry.
+
+ :param str summary: Description of a report entry.
+ :param str details: Detailed description that is going to be folded by default.
+ :param str category: Title of a report entry.
+ """
+ msg = (
+ "{}{} ".format(summary, details)
+ if details
+ else "{}".format(summary)
+ )
+ report(message=msg, category=category, format="html")
+ return msg
+
+
+def report_with_list(summary, data, columns, row_format, links=None, total=None, limit=100, category="Other"):
+ """Append the upgrade report with a new entry that displays a list of records.
+
+ The entry consists of a category (title) and a summary (body).
+ The entry displays a list of records previously returned by SQL query, or any list.
+
+ .. example::
+
+ .. code-block:: python
+
+ total = cr.rowcount
+ data = cr.fetchmany(20)
+ util.report_with_list(
+ summary="The following records were altered.",
+ data=data,
+ columns=("id", "name", "city", "comment", "company_id", "company_name"),
+ row_format="Partner with id {partner_link} works at company {company_link} in {city}, ({comment})",
+ links={"company_link": ("res.company", "company_id", "company_name"), "partner_link": ("res.partner", "id", "name")},
+ total=total,
+ category="Accounting"
+ )
+
+ :param str summary: description of a report entry.
+ :param list(tuple) data: data to report, each entry would be a row in the report.
+ It could be empty, in which case only the summary is rendered.
+ :param tuple(str) columns: columns in `data`, can be referenced in `row_format`.
+ :param str row_format: format for rows, can use any name from `columns` or `links`, e.g.:
+ "Partner {partner_link} that lives in {city} works at company {company_link}."
+ :param dict(str, tuple(str, str, str)) links: optional model/record links spec,
+ the keys can be referenced in `row_format`.
+ :param int total: optional, total number of records.
+ Taken as `len(data)` when `None` is passed.
+ Useful when `data` was limited by the caller.
+ :param int limit: maximum number of records to list in the report.
+ If `data` contains more records than `limit`, the `total`
+ number would be included in the report as well.
+ Set `None` for no limit.
+ :param str category: title of a report entry.
+ """
+
+ def row_to_html(row):
+ row_dict = dict(zip(columns, row))
+ if links:
+ row_dict.update(
+ {
+ link: get_anchor_link_to_record(rec_model, row_dict[id_col], row_dict[name_col])
+ for link, (rec_model, id_col, name_col) in links.items()
+ }
+ )
+ row_dict = {col: html_escape(val) for col, val in row_dict.items()}
+ return "{}".format(row_format.format(**row_dict))
+
+ if not data:
+ row_to_html(columns) # Validate the format is correct, including links
+ return report_with_summary(summary=summary, details="", category=category)
+
+ disclaimer = "The total number of affected records is {}. ".format(total) if total else ""
+ total = len(data) if total is None else total
+ limit = min(limit, total) if limit is not None else total
+ if total > limit:
+ disclaimer += "This list is showing the first {} records.".format(limit)
+
+ rows = "\n" + "\n".join([row_to_html(row) for row in data[:limit]]) + "\n
"
+ return report_with_summary(summary, "{}{}".format(disclaimer, rows), category)
+
+
def announce_release_note(cr):
filepath = os.path.join(os.path.dirname(__file__), "release-note.xml")
with open(filepath, "rb") as fp: