From f65b367b64b29323ddc44db91b15d9438a54ce82 Mon Sep 17 00:00:00 2001 From: abraham-mplus Date: Wed, 12 Nov 2025 17:44:58 +0700 Subject: [PATCH 1/2] [ADD] upgrade_analysis_patch: add patch module to handle older version of odoo --- upgrade_analysis_patch/__init__.py | 1 + upgrade_analysis_patch/__manifest__.py | 16 + upgrade_analysis_patch/compare.py | 541 ++++++++++++++++++ upgrade_analysis_patch/models/__init__.py | 1 + .../models/upgrade_analysis.py | 249 ++++++++ .../views/view_upgrade_analysis.xml | 12 + 6 files changed, 820 insertions(+) create mode 100644 upgrade_analysis_patch/__init__.py create mode 100644 upgrade_analysis_patch/__manifest__.py create mode 100644 upgrade_analysis_patch/compare.py create mode 100644 upgrade_analysis_patch/models/__init__.py create mode 100644 upgrade_analysis_patch/models/upgrade_analysis.py create mode 100644 upgrade_analysis_patch/views/view_upgrade_analysis.xml diff --git a/upgrade_analysis_patch/__init__.py b/upgrade_analysis_patch/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/upgrade_analysis_patch/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/upgrade_analysis_patch/__manifest__.py b/upgrade_analysis_patch/__manifest__.py new file mode 100644 index 00000000000..0fcd37d656f --- /dev/null +++ b/upgrade_analysis_patch/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Upgrade Analysis Patch", + "summary": "Patch code of Upgrade analysis", + "version": "17.0.1.0.0", + "category": "Migration", + "author": "Yotech", + "data": [ + "views/view_upgrade_analysis.xml", + ], + "installable": True, + "depends": ["upgrade_analysis"], + "external_dependencies": { + "python": ["mako", "dataclasses", "odoorpc", "openupgradelib"], + }, + "license": "AGPL-3", +} diff --git a/upgrade_analysis_patch/compare.py b/upgrade_analysis_patch/compare.py new file mode 100644 index 00000000000..939a91aba7b --- /dev/null +++ b/upgrade_analysis_patch/compare.py @@ -0,0 +1,541 @@ +import collections +import copy + +try: + from odoo.addons.openupgrade_scripts import apriori +except ImportError: + from dataclasses import dataclass + from dataclasses import field as dc_field + + @dataclass + class NullApriori: + renamed_modules: dict = dc_field(default_factory=dict) + merged_modules: dict = dc_field(default_factory=dict) + renamed_models: dict = dc_field(default_factory=dict) + merged_models: dict = dc_field(default_factory=dict) + + apriori = NullApriori() + +def module_map(module): + return apriori.renamed_modules.get( + module, apriori.merged_modules.get(module, module) + ) + + +def model_rename_map(model): + return apriori.renamed_models.get(model, model) + + +def model_map(model): + return apriori.renamed_models.get(model, apriori.merged_models.get(model, model)) + + +def inv_model_map(model): + inv_model_map_dict = {v: k for k, v in apriori.renamed_models.items()} + return inv_model_map_dict.get(model, model) + + +IGNORE_FIELDS = [ + "create_date", + "create_uid", + "id", + "write_date", + "write_uid", +] + + +def compare_records(dict_old, dict_new, fields): + """ + Check equivalence of two OpenUpgrade field representations + with respect to the keys in the 'fields' arguments. + Take apriori knowledge into account for mapped modules or + model names. + Return True of False. + """ + for field in fields: + if field == "module": + if module_map(dict_old["module"]) != dict_new["module"]: + return False + elif field == "model": + if model_rename_map(dict_old["model"]) != dict_new["model"]: + return False + elif field == "other_prefix": + if ( + dict_old["module"] != dict_old["prefix"] + or dict_new["module"] != dict_new["prefix"] + ): + return False + if dict_old["model"] == "ir.ui.view": + # basically, to avoid the assets_backend case + return False + elif dict_old[field] != dict_new[field]: + return False + return True + + +def search(item, item_list, fields, get_all=None): + """ + Find a match of a dictionary in a list of similar dictionaries + with respect to the keys in the 'fields' arguments. + Return the item if found or None. + """ + all_found = [] + for other in item_list: + if not compare_records(item, other, fields): + continue + if not get_all: + return other + if other["module"] != other["prefix"]: + all_found.append(other) + if get_all: + return all_found + # search for renamed fields + if "field" in fields: + for other in item_list: + if not item["field"] or item["field"] is not None or item["isproperty"]: + continue + if compare_records(dict(item, field=other["field"]), other, fields): + return other + return None + + +def fieldprint(old, new, field, text, reprs): + fieldrepr = "{}".format(old["field"]) + if old["field"] not in ("_inherits", "_order"): + fieldrepr += " ({})".format(old["type"]) + fullrepr = "{:<12} / {:<24} / {:<30}".format(old["module"], old["model"], fieldrepr) + if not text: + text = f"{field} is now '{new[field]}' ('{old[field]}')" + if field in ("column1", "column2"): + text += f" [{old['table']}]" + if field == "relation": + text += " [nothing to do]" + reprs[module_map(old["module"])].append(f"{fullrepr}: {text}") + if field == "module": + text = f"previously in module {old[field]}" + fullrepr = "{:<12} / {:<24} / {:<30}".format( + new["module"], old["model"], fieldrepr + ) + reprs[module_map(new["module"])].append(f"{fullrepr}: {text}") + + +def report_generic(new, old, attrs, reprs): + for attr in attrs: + try: + old[attr] + except KeyError: + continue + + if attr == "required": + if old[attr] != new["required"] and new["required"]: + text = "now required" + fieldprint(old, new, "", text, reprs) + elif attr == "stored": + if old[attr] != new[attr]: + if new["stored"]: + text = "is now stored" + else: + text = "not stored anymore" + fieldprint(old, new, "", text, reprs) + elif attr == "isfunction": + if old[attr] != new[attr]: + if new["isfunction"]: + text = "now a function" + else: + text = "not a function anymore" + fieldprint(old, new, "", text, reprs) + elif attr == "isproperty": + if old[attr] != new[attr]: + if new[attr]: + text = "now a property" + else: + text = "not a property anymore" + fieldprint(old, new, "", text, reprs) + elif attr == "isrelated": + if old[attr] != new[attr]: + if new[attr]: + text = "now related" + else: + text = "not related anymore" + fieldprint(old, new, "", text, reprs) + elif attr == "table": + if old[attr] != new[attr]: + fieldprint(old, new, attr, "", reprs) + if old[attr] and new[attr]: + if old["column1"] != new["column1"]: + fieldprint(old, new, "column1", "", reprs) + if old["column2"] != new["column2"]: + fieldprint(old, new, "column2", "", reprs) + elif old[attr] != new[attr]: + fieldprint(old, new, attr, "", reprs) + + +def compare_sets(old_records, new_records): + """ + Compare a set of OpenUpgrade field representations. + Try to match the equivalent fields in both sets. + Return a textual representation of changes in a dictionary with + module names as keys. Special case is the 'general' key + which contains overall remarks and matching statistics. + """ + reprs = collections.defaultdict(list) + + def clean_records(records): + result = [] + for record in records: + if record["field"] not in IGNORE_FIELDS: + result.append(record) + return result + + old_records = clean_records(old_records) + new_records = clean_records(new_records) + + origlen = len(old_records) + new_models = {column["model"] for column in new_records} + old_models = {column["model"] for column in old_records} + + matched_direct = 0 + matched_other_module = 0 + matched_other_type = 0 + in_obsolete_models = 0 + + obsolete_models = [] + for model in old_models: + if model not in new_models: + if model_map(model) not in new_models: + obsolete_models.append(model) + + non_obsolete_old_records = [] + for column in copy.copy(old_records): + if column["model"] in obsolete_models: + in_obsolete_models += 1 + else: + non_obsolete_old_records.append(column) + + def match(match_fields, report_fields, warn=False): + count = 0 + for column in copy.copy(non_obsolete_old_records): + found = search(column, new_records, match_fields) + if found: + if warn: + pass + # print "Tentatively" + report_generic(found, column, report_fields, reprs) + old_records.remove(column) + non_obsolete_old_records.remove(column) + new_records.remove(found) + count += 1 + return count + + matched_direct = match( + ["module", "mode", "model", "field"], + [ + "relation", + "type", + "selection_keys", + "_inherits", + "stored", + "isfunction", + "isrelated", + "required", + "table", + "_order", + ], + ) + + # other module, same type and operation + matched_other_module = match( + ["mode", "model", "field", "type"], + [ + "module", + "relation", + "selection_keys", + "_inherits", + "stored", + "isfunction", + "isrelated", + "required", + "table", + "_order", + ], + ) + + # other module, same operation, other type + matched_other_type = match( + ["module", "mode", "model", "field"], + [ + "relation", + "type", + "selection_keys", + "_inherits", + "stored", + "isfunction", + "isrelated", + "required", + "table", + "_order", + ], + ) + + # Info that is displayed for deleted fields + printkeys_old = [ + "relation", + "required", + "selection_keys", + "_inherits", + "mode", + "attachment", + ] + # Info that is displayed for new fields + printkeys_new = printkeys_old + [ + "hasdefault", + ] + for column in old_records: + if column["field"] == "_order": + continue + # we do not care about removed non stored function fields + if not column["stored"] and (column["isfunction"] or column["isrelated"]): + continue + if column["mode"] == "create": + column["mode"] = "" + + extra_message = ", ".join( + [ + k + ": " + str(column[k]) if k != str(column[k]) else k + for k in printkeys_old + if k in column + ] + ) + if extra_message: + extra_message = " " + extra_message + fieldprint(column, "", "", "DEL" + extra_message, reprs) + + for column in new_records: + if column["field"] == "_order": + continue + # we do not care about newly added non stored function fields + if not column["stored"] and (column["isfunction"] or column["isrelated"]): + continue + if column["mode"] == "create": + column["mode"] = "" + printkeys = printkeys_new.copy() + if column["isfunction"] or column["isrelated"]: + printkeys.extend(["isfunction", "isrelated", "stored"]) + extra_message = ", ".join( + [ + k + ": " + str(column[k]) if k != str(column[k]) else k + for k in printkeys + if column[k] + ] + ) + if extra_message: + extra_message = " " + extra_message + fieldprint(column, "", "", "NEW" + extra_message, reprs) + + for line in [ + "# %d fields matched," % (origlen - len(old_records)), + "# Direct match: %d" % matched_direct, + "# Found in other module: %d" % matched_other_module, + "# Found with different type: %d" % matched_other_type, + "# In obsolete models: %d" % in_obsolete_models, + "# Not matched: %d" % len(old_records), + "# New columns: %d" % len(new_records), + ]: + reprs["general"].append(line) + return reprs + + +def compare_xml_sets(old_records, new_records): + reprs = collections.defaultdict(list) + + def match_updates(match_fields): + old_updated, new_updated = {}, {} + for column in copy.copy(old_records): + found_all = search(column, old_records, match_fields, True) + for found in found_all: + old_records.remove(found) + for column in copy.copy(new_records): + found_all = search(column, new_records, match_fields, True) + for found in found_all: + new_records.remove(found) + matched_records = list(old_updated.values()) + list(new_updated.values()) + matched_records = [y for x in matched_records for y in x] + return matched_records + + def match(match_fields, match_type="direct"): + matched_records = [] + for column in copy.copy(old_records): + found = search(column, new_records, match_fields) + if found: + old_records.remove(column) + new_records.remove(found) + if match_type != "direct": + column["old"] = True + found["new"] = True + column[match_type] = found["module"] + found[match_type] = column["module"] + found["domain"] = ( + column["domain"] != found["domain"] + and column["domain"] != "[]" + and found["domain"] is False + ) + column["domain"] = False + found["definition"] = ( + column["definition"] + and column["definition"] != found["definition"] + and "is now '{}' ('{}')".format( + found["definition"], column["definition"] + ) + ) + column["definition"] = False + column["noupdate_switched"] = False + found["noupdate_switched"] = column["noupdate"] != found["noupdate"] + if match_type != "direct": + matched_records.append(column) + matched_records.append(found) + elif ( + match_type == "direct" and (found["domain"] or found["definition"]) + ) or found["noupdate_switched"]: + matched_records.append(found) + return matched_records + + # direct match + modified_records = match(["module", "model", "name"]) + + # updated records (will be excluded) + match_updates(["model", "name"]) + + # other module, same full xmlid + moved_records = match(["model", "name"], "moved") + + # other module, same suffix, other prefix + renamed_records = match(["model", "suffix", "other_prefix"], "renamed") + + for record in old_records: + record["old"] = True + record["domain"] = False + record["definition"] = False + record["noupdate_switched"] = False + for record in new_records: + record["new"] = True + record["domain"] = False + record["definition"] = False + record["noupdate_switched"] = False + + sorted_records = sorted( + old_records + new_records + moved_records + renamed_records + modified_records, + key=lambda k: (k["model"], "old" in k, k["name"]), + ) + for entry in sorted_records: + content = "" + if "old" in entry: + content = f"DEL {entry['model']}: {entry['name']}" + if "moved" in entry: + content += f" [moved to {entry['moved']} module]" + elif "renamed" in entry: + content += f" [renamed to {entry['renamed']} module]" + elif "new" in entry: + content = f"NEW {entry['model']}: {entry['name']}" + if "moved" in entry: + content += f" [moved from {entry['moved']} module]" + elif "renamed" in entry: + content += f" [renamed from {entry['renamed']} module]" + if "old" not in entry and "new" not in entry: + content = f"{entry['model']}: {entry['name']}" + if entry["domain"]: + content += " (deleted domain)" + if entry["definition"]: + content += f" (changed definition: {entry['definition']})" + if entry["noupdate"]: + content += " (noupdate)" + if entry["noupdate_switched"]: + content += " (noupdate switched)" + reprs[module_map(entry["module"])].append(content) + return reprs + + +def compare_model_sets(old_records, new_records): + """ + Compare a set of OpenUpgrade model representations. + """ + reprs = collections.defaultdict(list) + + new_models = {column["model"]: column["module"] for column in new_records} + old_models = {column["model"]: column["module"] for column in old_records} + + obsolete_models = [] + for column in copy.copy(old_records): + model = column["model"] + if model in old_models: + if model not in new_models: + if model_map(model) not in new_models: + obsolete_models.append(model) + text = f"obsolete model {model}" + if column["model_type"]: + text += f" [{column['model_type']}]" + reprs[module_map(column["module"])].append(text) + reprs["general"].append( + f"obsolete model {model} " + f"[module {module_map(column['module'])}]" + ) + else: + moved_module = "" + if module_map(column["module"]) != new_models[model_map(model)]: + moved_module = f" in module {new_models[model_map(model)]}" + text = "obsolete model {} (renamed to {}{})".format( + model, + model_map(model), + moved_module, + ) + if column["model_type"]: + text += f" [{column['model_type']}]" + reprs[module_map(column["module"])].append(text) + reprs["general"].append( + f"obsolete model {model} (renamed to {model_map(model)}) " + f"[module {module_map(column['module'])}]" + ) + else: + if module_map(column["module"]) != new_models[model]: + text = f"model {model} (moved to {new_models[model]})" + if column["model_type"]: + text += f" [{column['model_type']}]" + reprs[module_map(column["module"])].append(text) + text = f"model {model} (moved from {old_models[model]})" + if column["model_type"]: + text += f" [{column['model_type']}]" + + for column in copy.copy(new_records): + model = column["model"] + if model in new_models: + if model not in old_models: + if inv_model_map(model) not in old_models: + text = f"new model {model}" + if column["model_type"]: + text += f" [{column['model_type']}]" + reprs[column["module"]].append(text) + reprs["general"].append( + "new model {} [module {}]".format(model, column["module"]) + ) + else: + moved_module = "" + if column["module"] != module_map(old_models[inv_model_map(model)]): + moved_module = f" in module {old_models[inv_model_map(model)]}" + text = "new model {} (renamed from {}{})".format( + model, + inv_model_map(model), + moved_module, + ) + if column["model_type"]: + text += f" [{column['model_type']}]" + reprs[column["module"]].append(text) + reprs["general"].append( + f"new model {model} (renamed from {inv_model_map(model)}) " + f"[module {column['module']}]" + ) + else: + if column["module"] != module_map(old_models[model]): + text = f"model {model} (moved from {old_models[model]})" + if column["model_type"]: + text += f" [{column['model_type']}]" + reprs[column["module"]].append(text) + return reprs diff --git a/upgrade_analysis_patch/models/__init__.py b/upgrade_analysis_patch/models/__init__.py new file mode 100644 index 00000000000..47c9ca3197f --- /dev/null +++ b/upgrade_analysis_patch/models/__init__.py @@ -0,0 +1 @@ +from . import upgrade_analysis \ No newline at end of file diff --git a/upgrade_analysis_patch/models/upgrade_analysis.py b/upgrade_analysis_patch/models/upgrade_analysis.py new file mode 100644 index 00000000000..0bf6dcdf8cd --- /dev/null +++ b/upgrade_analysis_patch/models/upgrade_analysis.py @@ -0,0 +1,249 @@ +from odoo import fields, models, release +import logging + +from .. import compare + +try: + from odoo.addons.openupgrade_scripts.apriori import merged_modules, renamed_modules +except ImportError: + renamed_modules = {} + merged_modules = {} + +_logger = logging.getLogger(__name__) +_IGNORE_MODULES = ["openupgrade_records", "upgrade_analysis"] + +class UpgradeAnalysis(models.Model): + _inherit = "upgrade.analysis" + + def _get_remote_modules(self, remote_record_obj): + """Get list of modules from remote Odoo, even if list_modules() is missing.""" + + # Try calling remote list_modules() if available + if hasattr(remote_record_obj, "list_modules"): + try: + return remote_record_obj.list_modules() + except Exception: + pass # fallback if it exists but fails + + # Fallback: safe ORM call via search_read + records = remote_record_obj.search_read([], ["module"]) + return sorted({r["module"] for r in records}) + + def generate_noupdate_changes(self): + """Communicate with the remote server to fetch all xml data records + per module, and generate a diff in XML format that can be imported + from the module's migration script using openupgrade.load_data() + """ + self.ensure_one() + connection = self.config_id.get_connection() + remote_record_obj = self._get_remote_model(connection, "record") + local_record_obj = self.env["upgrade.record"] + local_modules = local_record_obj.list_modules() + all_remote_modules = self._get_remote_modules(remote_record_obj) + for local_module in local_modules: + remote_files = [] + remote_modules = [] + remote_update, remote_noupdate = {}, {} + for remote_module in all_remote_modules: + if local_module == renamed_modules.get( + remote_module, merged_modules.get(remote_module, remote_module) + ): + remote_files.extend( + remote_record_obj.get_xml_records(remote_module) + ) + remote_modules.append(remote_module) + add_remote_update, add_remote_noupdate = self._parse_files( + remote_files, remote_module + ) + remote_update.update(add_remote_update) + remote_noupdate.update(add_remote_noupdate) + if not remote_modules: + continue + local_files = local_record_obj.get_xml_records(local_module) + local_update, local_noupdate = self._parse_files(local_files, local_module) + diff = self._get_xml_diff( + remote_update, remote_noupdate, local_update, local_noupdate + ) + if diff: + module = self.env["ir.module.module"].search( + [("name", "=", local_module)] + ) + self._write_file( + local_module, + module.installed_version, + diff, + filename="noupdate_changes.xml", + ) + return True + + def analyze(self): + """ + Retrieve both sets of database representations, + perform the comparison and register the resulting + change set + """ + self.ensure_one() + self.write( + { + "analysis_date": fields.Datetime.now(), + } + ) + + connection = self.config_id.get_connection() + RemoteRecord = self._get_remote_model(connection, "record") + LocalRecord = self.env["upgrade.record"] + + # Retrieve field representations and compare + remote_records = RemoteRecord.field_dump() + + local_records = LocalRecord.field_dump() + res = compare.compare_sets(remote_records, local_records) + + # Retrieve xml id representations and compare + flds = [ + "module", + "model", + "name", + "noupdate", + "prefix", + "suffix", + "domain", + "definition", + ] + local_xml_records = [ + {field: record[field] for field in flds if field in record} + for record in LocalRecord.search([("type", "=", "xmlid")]) + ] + remote_xml_record_ids = RemoteRecord.search([("type", "=", "xmlid")]) + remote_xml_records = [ + {field: record[field] for field in flds if field in record} + for record in RemoteRecord.read(remote_xml_record_ids, flds) + ] + res_xml = compare.compare_xml_sets(remote_xml_records, local_xml_records) + + # Retrieve model representations and compare + flds = [ + "module", + "model", + "name", + "model_original_module", + "model_type", + ] + local_model_records = [ + {field: record[field] for field in flds} + for record in LocalRecord.search([("type", "=", "model")]) + ] + remote_model_record_ids = RemoteRecord.search([("type", "=", "model")]) + remote_model_records = [ + {field: record[field] for field in flds} + for record in RemoteRecord.read(remote_model_record_ids, flds) + ] + res_model = compare.compare_model_sets( + remote_model_records, local_model_records + ) + remote_modules = set([record["module"] for record in remote_records]) + + affected_modules = sorted( + { + record["module"] + for record in remote_records + + local_records + + remote_xml_records + + local_xml_records + + remote_model_records + + local_model_records + } + ) + if "base" in affected_modules: + try: + pass + except ImportError: + _logger.error( + "You are using upgrade_analysis on core modules without " + " having openupgrade_scripts module available." + " The analysis process will not work properly," + " if you are generating analysis for the odoo modules" + " in an openupgrade context." + ) + + # reorder and output the result + keys = ["general"] + affected_modules + modules = { + module["name"]: module + for module in self.env["ir.module.module"].search( + [("state", "=", "installed")] + ) + } + general_log = "" + + no_changes_modules = [] + + for ignore_module in _IGNORE_MODULES: + if ignore_module in keys: + keys.remove(ignore_module) + + for key in keys: + contents = "---Models in module '%s'---\n" % key + if key in res_model: + contents += "\n".join([str(line) for line in res_model[key]]) + if res_model[key]: + contents += "\n" + contents += "---Fields in module '%s'---\n" % key + if key in res: + contents += "\n".join([str(line) for line in sorted(res[key])]) + if res[key]: + contents += "\n" + contents += "---XML records in module '%s'---\n" % key + if key in res_xml: + contents += "\n".join([str(line) for line in res_xml[key]]) + if res_xml[key]: + contents += "\n" + if key not in res and key not in res_xml and key not in res_model: + contents += "---nothing has changed in this module--\n" + no_changes_modules.append(key) + if key == "general": + general_log += contents + continue + if compare.module_map(key) not in modules: + general_log += ( + "ERROR: module not in list of installed modules:\n" + contents + ) + continue + if key not in modules: + # no need to log in full log the merged/renamed modules + continue + if self.write_files: + error = self._write_file(key, modules[key].installed_version, contents) + if error: + general_log += error + general_log += contents + else: + general_log += contents + + # Store the full log + if self.write_files and "base" in modules: + self._write_file( + "base", + modules["base"].installed_version, + general_log, + "upgrade_general_log.txt", + ) + + try: + self.generate_noupdate_changes() + except Exception as e: + _logger.exception("Error generating noupdate changes: %s" % e) + general_log += "ERROR: error when generating noupdate changes: %s\n" % e + try: + self.generate_module_coverage_file(no_changes_modules) + except Exception as e: + _logger.exception("Error generating module coverage file: %s" % e) + general_log += "ERROR: error when generating module coverage file: %s\n" % e + + self.write( + { + "state": "done", + "log": general_log, + } + ) + return True \ No newline at end of file diff --git a/upgrade_analysis_patch/views/view_upgrade_analysis.xml b/upgrade_analysis_patch/views/view_upgrade_analysis.xml new file mode 100644 index 00000000000..65afe7606f3 --- /dev/null +++ b/upgrade_analysis_patch/views/view_upgrade_analysis.xml @@ -0,0 +1,12 @@ + + + + upgrade.analysis + + + + "" + + + + From 12fec0f116e3d3a3a0da31cee930894627e8a7c7 Mon Sep 17 00:00:00 2001 From: abraham-mplus Date: Thu, 13 Nov 2025 16:43:47 +0700 Subject: [PATCH 2/2] [FIX] upgrade_analysis_patch: fix code --- upgrade_analysis_patch/compare.py | 317 +----------------- .../models/upgrade_analysis.py | 23 +- 2 files changed, 16 insertions(+), 324 deletions(-) diff --git a/upgrade_analysis_patch/compare.py b/upgrade_analysis_patch/compare.py index 939a91aba7b..ff2c6898d8c 100644 --- a/upgrade_analysis_patch/compare.py +++ b/upgrade_analysis_patch/compare.py @@ -1,123 +1,6 @@ import collections import copy - -try: - from odoo.addons.openupgrade_scripts import apriori -except ImportError: - from dataclasses import dataclass - from dataclasses import field as dc_field - - @dataclass - class NullApriori: - renamed_modules: dict = dc_field(default_factory=dict) - merged_modules: dict = dc_field(default_factory=dict) - renamed_models: dict = dc_field(default_factory=dict) - merged_models: dict = dc_field(default_factory=dict) - - apriori = NullApriori() - -def module_map(module): - return apriori.renamed_modules.get( - module, apriori.merged_modules.get(module, module) - ) - - -def model_rename_map(model): - return apriori.renamed_models.get(model, model) - - -def model_map(model): - return apriori.renamed_models.get(model, apriori.merged_models.get(model, model)) - - -def inv_model_map(model): - inv_model_map_dict = {v: k for k, v in apriori.renamed_models.items()} - return inv_model_map_dict.get(model, model) - - -IGNORE_FIELDS = [ - "create_date", - "create_uid", - "id", - "write_date", - "write_uid", -] - - -def compare_records(dict_old, dict_new, fields): - """ - Check equivalence of two OpenUpgrade field representations - with respect to the keys in the 'fields' arguments. - Take apriori knowledge into account for mapped modules or - model names. - Return True of False. - """ - for field in fields: - if field == "module": - if module_map(dict_old["module"]) != dict_new["module"]: - return False - elif field == "model": - if model_rename_map(dict_old["model"]) != dict_new["model"]: - return False - elif field == "other_prefix": - if ( - dict_old["module"] != dict_old["prefix"] - or dict_new["module"] != dict_new["prefix"] - ): - return False - if dict_old["model"] == "ir.ui.view": - # basically, to avoid the assets_backend case - return False - elif dict_old[field] != dict_new[field]: - return False - return True - - -def search(item, item_list, fields, get_all=None): - """ - Find a match of a dictionary in a list of similar dictionaries - with respect to the keys in the 'fields' arguments. - Return the item if found or None. - """ - all_found = [] - for other in item_list: - if not compare_records(item, other, fields): - continue - if not get_all: - return other - if other["module"] != other["prefix"]: - all_found.append(other) - if get_all: - return all_found - # search for renamed fields - if "field" in fields: - for other in item_list: - if not item["field"] or item["field"] is not None or item["isproperty"]: - continue - if compare_records(dict(item, field=other["field"]), other, fields): - return other - return None - - -def fieldprint(old, new, field, text, reprs): - fieldrepr = "{}".format(old["field"]) - if old["field"] not in ("_inherits", "_order"): - fieldrepr += " ({})".format(old["type"]) - fullrepr = "{:<12} / {:<24} / {:<30}".format(old["module"], old["model"], fieldrepr) - if not text: - text = f"{field} is now '{new[field]}' ('{old[field]}')" - if field in ("column1", "column2"): - text += f" [{old['table']}]" - if field == "relation": - text += " [nothing to do]" - reprs[module_map(old["module"])].append(f"{fullrepr}: {text}") - if field == "module": - text = f"previously in module {old[field]}" - fullrepr = "{:<12} / {:<24} / {:<30}".format( - new["module"], old["model"], fieldrepr - ) - reprs[module_map(new["module"])].append(f"{fullrepr}: {text}") - +from odoo.addons.upgrade_analysis.compare import fieldprint, search, model_map, IGNORE_FIELDS def report_generic(new, old, attrs, reprs): for attr in attrs: @@ -303,7 +186,7 @@ def match(match_fields, report_fields, warn=False): [ k + ": " + str(column[k]) if k != str(column[k]) else k for k in printkeys_old - if k in column + if k in column # This is patched ] ) if extra_message: @@ -343,199 +226,3 @@ def match(match_fields, report_fields, warn=False): ]: reprs["general"].append(line) return reprs - - -def compare_xml_sets(old_records, new_records): - reprs = collections.defaultdict(list) - - def match_updates(match_fields): - old_updated, new_updated = {}, {} - for column in copy.copy(old_records): - found_all = search(column, old_records, match_fields, True) - for found in found_all: - old_records.remove(found) - for column in copy.copy(new_records): - found_all = search(column, new_records, match_fields, True) - for found in found_all: - new_records.remove(found) - matched_records = list(old_updated.values()) + list(new_updated.values()) - matched_records = [y for x in matched_records for y in x] - return matched_records - - def match(match_fields, match_type="direct"): - matched_records = [] - for column in copy.copy(old_records): - found = search(column, new_records, match_fields) - if found: - old_records.remove(column) - new_records.remove(found) - if match_type != "direct": - column["old"] = True - found["new"] = True - column[match_type] = found["module"] - found[match_type] = column["module"] - found["domain"] = ( - column["domain"] != found["domain"] - and column["domain"] != "[]" - and found["domain"] is False - ) - column["domain"] = False - found["definition"] = ( - column["definition"] - and column["definition"] != found["definition"] - and "is now '{}' ('{}')".format( - found["definition"], column["definition"] - ) - ) - column["definition"] = False - column["noupdate_switched"] = False - found["noupdate_switched"] = column["noupdate"] != found["noupdate"] - if match_type != "direct": - matched_records.append(column) - matched_records.append(found) - elif ( - match_type == "direct" and (found["domain"] or found["definition"]) - ) or found["noupdate_switched"]: - matched_records.append(found) - return matched_records - - # direct match - modified_records = match(["module", "model", "name"]) - - # updated records (will be excluded) - match_updates(["model", "name"]) - - # other module, same full xmlid - moved_records = match(["model", "name"], "moved") - - # other module, same suffix, other prefix - renamed_records = match(["model", "suffix", "other_prefix"], "renamed") - - for record in old_records: - record["old"] = True - record["domain"] = False - record["definition"] = False - record["noupdate_switched"] = False - for record in new_records: - record["new"] = True - record["domain"] = False - record["definition"] = False - record["noupdate_switched"] = False - - sorted_records = sorted( - old_records + new_records + moved_records + renamed_records + modified_records, - key=lambda k: (k["model"], "old" in k, k["name"]), - ) - for entry in sorted_records: - content = "" - if "old" in entry: - content = f"DEL {entry['model']}: {entry['name']}" - if "moved" in entry: - content += f" [moved to {entry['moved']} module]" - elif "renamed" in entry: - content += f" [renamed to {entry['renamed']} module]" - elif "new" in entry: - content = f"NEW {entry['model']}: {entry['name']}" - if "moved" in entry: - content += f" [moved from {entry['moved']} module]" - elif "renamed" in entry: - content += f" [renamed from {entry['renamed']} module]" - if "old" not in entry and "new" not in entry: - content = f"{entry['model']}: {entry['name']}" - if entry["domain"]: - content += " (deleted domain)" - if entry["definition"]: - content += f" (changed definition: {entry['definition']})" - if entry["noupdate"]: - content += " (noupdate)" - if entry["noupdate_switched"]: - content += " (noupdate switched)" - reprs[module_map(entry["module"])].append(content) - return reprs - - -def compare_model_sets(old_records, new_records): - """ - Compare a set of OpenUpgrade model representations. - """ - reprs = collections.defaultdict(list) - - new_models = {column["model"]: column["module"] for column in new_records} - old_models = {column["model"]: column["module"] for column in old_records} - - obsolete_models = [] - for column in copy.copy(old_records): - model = column["model"] - if model in old_models: - if model not in new_models: - if model_map(model) not in new_models: - obsolete_models.append(model) - text = f"obsolete model {model}" - if column["model_type"]: - text += f" [{column['model_type']}]" - reprs[module_map(column["module"])].append(text) - reprs["general"].append( - f"obsolete model {model} " - f"[module {module_map(column['module'])}]" - ) - else: - moved_module = "" - if module_map(column["module"]) != new_models[model_map(model)]: - moved_module = f" in module {new_models[model_map(model)]}" - text = "obsolete model {} (renamed to {}{})".format( - model, - model_map(model), - moved_module, - ) - if column["model_type"]: - text += f" [{column['model_type']}]" - reprs[module_map(column["module"])].append(text) - reprs["general"].append( - f"obsolete model {model} (renamed to {model_map(model)}) " - f"[module {module_map(column['module'])}]" - ) - else: - if module_map(column["module"]) != new_models[model]: - text = f"model {model} (moved to {new_models[model]})" - if column["model_type"]: - text += f" [{column['model_type']}]" - reprs[module_map(column["module"])].append(text) - text = f"model {model} (moved from {old_models[model]})" - if column["model_type"]: - text += f" [{column['model_type']}]" - - for column in copy.copy(new_records): - model = column["model"] - if model in new_models: - if model not in old_models: - if inv_model_map(model) not in old_models: - text = f"new model {model}" - if column["model_type"]: - text += f" [{column['model_type']}]" - reprs[column["module"]].append(text) - reprs["general"].append( - "new model {} [module {}]".format(model, column["module"]) - ) - else: - moved_module = "" - if column["module"] != module_map(old_models[inv_model_map(model)]): - moved_module = f" in module {old_models[inv_model_map(model)]}" - text = "new model {} (renamed from {}{})".format( - model, - inv_model_map(model), - moved_module, - ) - if column["model_type"]: - text += f" [{column['model_type']}]" - reprs[column["module"]].append(text) - reprs["general"].append( - f"new model {model} (renamed from {inv_model_map(model)}) " - f"[module {column['module']}]" - ) - else: - if column["module"] != module_map(old_models[model]): - text = f"model {model} (moved from {old_models[model]})" - if column["model_type"]: - text += f" [{column['model_type']}]" - reprs[column["module"]].append(text) - return reprs diff --git a/upgrade_analysis_patch/models/upgrade_analysis.py b/upgrade_analysis_patch/models/upgrade_analysis.py index 0bf6dcdf8cd..34a01e952a6 100644 --- a/upgrade_analysis_patch/models/upgrade_analysis.py +++ b/upgrade_analysis_patch/models/upgrade_analysis.py @@ -1,10 +1,11 @@ -from odoo import fields, models, release +from odoo import fields, models import logging from .. import compare +from odoo.addons.upgrade_analysis import compare as original_compare try: - from odoo.addons.openupgrade_scripts.apriori import merged_modules, renamed_modules + from odoo.addons.openupgrade_scripts.apriori import merged_modules, renamed_modules # type: ignore except ImportError: renamed_modules = {} merged_modules = {} @@ -39,7 +40,7 @@ def generate_noupdate_changes(self): remote_record_obj = self._get_remote_model(connection, "record") local_record_obj = self.env["upgrade.record"] local_modules = local_record_obj.list_modules() - all_remote_modules = self._get_remote_modules(remote_record_obj) + all_remote_modules = self._get_remote_modules(remote_record_obj) # This is changed for local_module in local_modules: remote_files = [] remote_modules = [] @@ -111,15 +112,15 @@ def analyze(self): "definition", ] local_xml_records = [ - {field: record[field] for field in flds if field in record} + {field: record[field] for field in flds if field in record} # This is patched for record in LocalRecord.search([("type", "=", "xmlid")]) ] - remote_xml_record_ids = RemoteRecord.search([("type", "=", "xmlid")]) + remote_xml_record_ids = RemoteRecord.search([("type", "=", "xmlid")]) # This is patched remote_xml_records = [ {field: record[field] for field in flds if field in record} for record in RemoteRecord.read(remote_xml_record_ids, flds) ] - res_xml = compare.compare_xml_sets(remote_xml_records, local_xml_records) + res_xml = original_compare.compare_xml_sets(remote_xml_records, local_xml_records) # Retrieve model representations and compare flds = [ @@ -134,14 +135,18 @@ def analyze(self): for record in LocalRecord.search([("type", "=", "model")]) ] remote_model_record_ids = RemoteRecord.search([("type", "=", "model")]) + _logger.info("DEBUG -> remote_model_record_ids = %s", remote_model_record_ids) + _logger.info("DEBUG -> flds = %s", flds) + for record in RemoteRecord.read(remote_model_record_ids, flds): + _logger.info("DEBUG -> record = %s", record) + remote_model_records = [ {field: record[field] for field in flds} for record in RemoteRecord.read(remote_model_record_ids, flds) ] - res_model = compare.compare_model_sets( + res_model = original_compare.compare_model_sets( remote_model_records, local_model_records ) - remote_modules = set([record["module"] for record in remote_records]) affected_modules = sorted( { @@ -204,7 +209,7 @@ def analyze(self): if key == "general": general_log += contents continue - if compare.module_map(key) not in modules: + if original_compare.module_map(key) not in modules: general_log += ( "ERROR: module not in list of installed modules:\n" + contents )