Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ addon | version | maintainers | summary
[test_auditlog](test_auditlog/) | 19.0.1.0.0 | | Additional unit tests for Audit Log based on accounting models
[tracking_manager](tracking_manager/) | 19.0.1.0.0 | <a href='https://github.com/Kev-Roche'><img src='https://github.com/Kev-Roche.png' width='32' height='32' style='border-radius:50%;' alt='Kev-Roche'/></a> <a href='https://github.com/sebastienbeau'><img src='https://github.com/sebastienbeau.png' width='32' height='32' style='border-radius:50%;' alt='sebastienbeau'/></a> | This module tracks all fields of a model, including one2many and many2many ones.
[tracking_manager_domain](tracking_manager_domain/) | 19.0.1.0.0 | <a href='https://github.com/CRogos'><img src='https://github.com/CRogos.png' width='32' height='32' style='border-radius:50%;' alt='CRogos'/></a> | This module extends the tracking manager to allow to define a domain on fields to track changes only when certain conditions apply.
[upgrade_analysis](upgrade_analysis/) | 19.0.1.0.4 | <a href='https://github.com/StefanRijnhart'><img src='https://github.com/StefanRijnhart.png' width='32' height='32' style='border-radius:50%;' alt='StefanRijnhart'/></a> <a href='https://github.com/legalsylvain'><img src='https://github.com/legalsylvain.png' width='32' height='32' style='border-radius:50%;' alt='legalsylvain'/></a> | Performs a difference analysis between modules installed on two different Odoo instances
[upgrade_analysis](upgrade_analysis/) | 19.0.1.1.1 | <a href='https://github.com/StefanRijnhart'><img src='https://github.com/StefanRijnhart.png' width='32' height='32' style='border-radius:50%;' alt='StefanRijnhart'/></a> <a href='https://github.com/legalsylvain'><img src='https://github.com/legalsylvain.png' width='32' height='32' style='border-radius:50%;' alt='legalsylvain'/></a> | Performs a difference analysis between modules installed on two different Odoo instances

[//]: # (end addons)

Expand Down
2 changes: 1 addition & 1 deletion upgrade_analysis/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Upgrade Analysis
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:89a10d9f558bdf888f62fbc7c03e763b17b3ee1c836644ed5dc752d1fafe9dd9
!! source digest: sha256:2f7fa794d0292655d9bb5907cd6c1dacce384ed506e9b9c7f6b0ae19ef3a2c93
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
Expand Down
2 changes: 1 addition & 1 deletion upgrade_analysis/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"name": "Upgrade Analysis",
"summary": "Performs a difference analysis between modules"
" installed on two different Odoo instances",
"version": "19.0.1.0.4",
"version": "19.0.1.1.1",
"category": "Migration",
"author": "Therp BV, Opener B.V., GRAP, Odoo Community Association (OCA)",
"maintainers": ["StefanRijnhart", "legalsylvain"],
Expand Down
2 changes: 1 addition & 1 deletion upgrade_analysis/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ def match(match_fields, match_type="direct"):
if entry["noupdate_switched"]:
content += " (noupdate switched)"
reprs[module_map(entry["module"])].append(content)
return reprs
return reprs, moved_records, renamed_records, modified_records


def compare_model_sets(old_records, new_records):
Expand Down
67 changes: 61 additions & 6 deletions upgrade_analysis/models/upgrade_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ def analyze(self):
{field: record[field] for field in flds}
for record in RemoteRecord.read(remote_xml_record_ids, flds)
]
res_xml = compare.compare_xml_sets(remote_xml_records, local_xml_records)
res_xml, moved_xml_records, renamed_xml_records, modified_xml_records = (
compare.compare_xml_sets(remote_xml_records, local_xml_records)
)

# Retrieve model representations and compare
flds = [
Expand Down Expand Up @@ -270,7 +272,9 @@ def analyze(self):
)
noupdate_modules = []
try:
noupdate_modules = self.generate_noupdate_changes()
noupdate_modules = self.generate_noupdate_changes(
moved_xml_records, renamed_xml_records, modified_xml_records
)
except Exception as e:
_logger.exception(f"Error generating noupdate changes: {e}")
general_log += "ERROR: error when generating noupdate changes: {e}\n"
Expand Down Expand Up @@ -366,11 +370,23 @@ def _get_xml_diff(
"name"
}:
# if previous version has set a field but current version
# doesn't, set whatever the NULL value of the field is
# (usually None)
# doesn't, reset it to the value a fresh install would give
# it: the field's default. Only fall back to the field's
# falsy value when there is no default. Using falsy_value
# unconditionally is wrong for fields whose default is truthy
# (e.g. ir.rule.perm_* or active) and can even produce
# records that violate model constraints.
model = self.env[local_record.attrib["model"]]
field = model._fields[attribs["name"]]
eval_constant = ast.unparse(ast.Constant(field.falsy_value))
reset_value = model.default_get([field.name]).get(
field.name, field.falsy_value
)
try:
eval_constant = ast.unparse(ast.Constant(reset_value))
except ValueError:
# non-scalar default (e.g. x2many command list):
# keep the previous behaviour
eval_constant = ast.unparse(ast.Constant(field.falsy_value))
if eval_constant != "''":
attribs["eval"] = eval_constant
element.append(etree.Element(record_remote_dict[key].tag, attribs))
Expand Down Expand Up @@ -486,7 +502,9 @@ def _parse_files(self, xml_files, module_name):

return records_update, records_noupdate

def generate_noupdate_changes(self):
def generate_noupdate_changes(
self, moved_xml_records, renamed_xml_records, modified_xml_records
):
"""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()
Expand All @@ -498,6 +516,14 @@ def generate_noupdate_changes(self):
local_modules = local_record_obj.list_modules()
all_remote_modules = remote_record_obj.list_modules()
changed_modules = []
# {new_module: {name: previous_module}}
renamed_xmlids = {}
for renamed_xml_record in renamed_xml_records:
if renamed_xml_record.get("old") or not renamed_xml_record.get("new"):
continue
renamed_xmlids.setdefault(renamed_xml_record["module"], {}).update(
{renamed_xml_record["suffix"]: renamed_xml_record["renamed"]}
)
for local_module in local_modules:
remote_files = []
remote_modules = []
Expand All @@ -515,6 +541,35 @@ def generate_noupdate_changes(self):
)
remote_update.update(add_remote_update)
remote_noupdate.update(add_remote_noupdate)
if any(
renamed_from_module == remote_module
for renamed_from_module in renamed_xmlids.get(
local_module, {}
).values()
):
# if xmlids have been renamed (moved) to the current module, query
# their definition from the module that contained it previously
remote_modules.append(remote_module)
renamed_from_module_files = remote_record_obj.get_xml_records(
remote_module
)
renamed_from_module_update, renamed_from_module_noupdate = (
self._parse_files(renamed_from_module_files, remote_module)
)
remote_update.update(
{
name: xml
for name, xml in renamed_from_module_update.items()
if renamed_xmlids[local_module].get(name) == remote_module
}
)
remote_noupdate.update(
{
name: xml
for name, xml in renamed_from_module_noupdate.items()
if renamed_xmlids[local_module].get(name) == remote_module
}
)
if not remote_modules:
continue
local_files = local_record_obj.get_xml_records(local_module)
Expand Down
2 changes: 1 addition & 1 deletion upgrade_analysis/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ <h1>Upgrade Analysis</h1>
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:89a10d9f558bdf888f62fbc7c03e763b17b3ee1c836644ed5dc752d1fafe9dd9
!! source digest: sha256:2f7fa794d0292655d9bb5907cd6c1dacce384ed506e9b9c7f6b0ae19ef3a2c93
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-tools/tree/19.0/upgrade_analysis"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-tools-19-0/server-tools-19-0-upgrade_analysis"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-tools&amp;target_branch=19.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module provides the tool to generate the database analysis files
Expand Down
140 changes: 140 additions & 0 deletions upgrade_analysis/tests/test_module.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from copy import deepcopy
from unittest.mock import patch

from lxml import etree

Expand Down Expand Up @@ -152,3 +153,142 @@ def test_xml_comparison(self):
)
self.assertIn('<field name="module_ids" eval="None"/>', diff)
self.assertIn('<field name="display_name"/>', diff)

def test_analyze(self):
"""
Test a full analysis run.
For the time being, only xmlid related functionality is tested
"""
analysis = self.env["upgrade.analysis"].create(
{
"config_id": self.env["upgrade.comparison.config"]
.create(
{
"database": self.env.cr.dbname,
}
)
.id,
}
)
upgrade_analysis_version = (
self.env["ir.module.module"]
.search([("name", "=", "upgrade_analysis")])
.latest_version
)

self.env["upgrade.record"].create(
{
"name": "upgrade_analysis.test_noupdate_xmlid",
"mode": "create",
"type": "xmlid",
"module": "upgrade_analysis",
"model": "upgrade.comparison.config",
"noupdate": True,
}
)

class RemoteUpgradeRecord:
_records = {
1: {
"name": "other_module.test_noupdate_xmlid",
"mode": "create",
"type": "xmlid",
"module": "other_module",
"prefix": "other_module",
"model": "upgrade.comparison.config",
"noupdate": True,
"suffix": "test_noupdate_xmlid",
},
}

def search(self, domain):
if domain == [("type", "=", "xmlid")]:
return [
_id
for _id, vals in self._records.items()
if vals["type"] == "xmlid"
]
return []

def read(self, ids, fields): # pylint: disable=method-required-super
return [
{field: record.get(field) for field in fields}
for _id, record in self._records.items()
if _id in ids
]

def field_dump(self):
return []

def list_modules(self):
return set(record["module"] for record in self._records.values())

def get_xml_records(self, module):
if module == "other_module":
return [
"""
<odoo noupdate="1">
<record
id="test_noupdate_xmlid"
model="upgrade.comparison.config"
>
<field
name="name"
>Noupdate xmlid from other_module</field>
<field name="server">some_server</field>
</record>
</odoo>
"""
]

def local_get_xml_records(module):
if module == "upgrade_analysis":
return [
"""
<odoo noupdate="1">
<record
id="test_noupdate_xmlid"
model="upgrade.comparison.config"
>
<field
name="name"
>Noupdate xmlid from upgrade_analysis</field>
<field name="server">some_server</field>
</record>
</odoo>
"""
]

written_files = {}

def write_file(module_name, version, content, filename="upgrade_analysis.txt"):
written_files[f"{module_name}-{version}-{filename}"] = content

with (
patch.object(analysis.config_id.__class__, "get_connection"),
patch.object(
analysis.__class__, "_get_remote_model"
) as patched_get_remote_model,
patch.object(analysis.__class__, "_write_file") as patched_write_file,
patch.object(
self.env["upgrade.record"].__class__, "get_xml_records"
) as patched_get_xml_records,
):
patched_get_remote_model.side_effect = lambda *args: RemoteUpgradeRecord()
patched_get_xml_records.side_effect = local_get_xml_records
patched_write_file.side_effect = write_file
analysis.analyze()

expected_noupdate_content = """<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="test_noupdate_xmlid" model="upgrade.comparison.config">
<field name="name">Noupdate xmlid from upgrade_analysis</field>
</record>
</odoo>
"""
self.assertEqual(
written_files[
f"upgrade_analysis-{upgrade_analysis_version}-noupdate_changes.xml"
],
expected_noupdate_content,
)