diff --git a/odoo_repository_migration/README.rst b/odoo_repository_migration/README.rst new file mode 100644 index 00000000..6948075e --- /dev/null +++ b/odoo_repository_migration/README.rst @@ -0,0 +1,150 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============================== +Odoo Repository Migration Data +============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:30958839b2e43d4e211d93f56d5ac4fa79f4bf2ff38f62c0c2c9b01dba613b60 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmodule--composition--analysis-lightgray.png?logo=github + :target: https://github.com/OCA/module-composition-analysis/tree/19.0/odoo_repository_migration + :alt: OCA/module-composition-analysis +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/module-composition-analysis-19-0/module-composition-analysis-19-0-odoo_repository_migration + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/module-composition-analysis&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module collects and records modules migration data from Odoo +repositories. + +Thanks to `oca-port `__ and data +collected by ``odoo_repository``, this module will generate migration +data on each module for declared migration paths (e.g. 16.0 -> 18.0). + +Also, for specific cases you can declare what a module became in a given +Odoo version, like OCA ``web_domain_field`` replaced by standard Odoo +module ``web`` starting from 17.0 (with an optional explanation that +could help users, like how to use this new module compared to the +previous one). + +Given a migration path, a module can get one of this migration status: + ++-----------------------------+----------------------------------------+ +| Status | Description | ++=============================+========================================+ +| *Fully Ported* | All commits from source version are | +| | present in target version | ++-----------------------------+----------------------------------------+ +| *To migrate* | The module doesn't exist on target | +| | version | ++-----------------------------+----------------------------------------+ +| *Ported (missing commits?)* | Some commits from source version are | +| | not ported in target version (could be | +| | false-positive) | ++-----------------------------+----------------------------------------+ +| *To review* | A migration PR has been detected | ++-----------------------------+----------------------------------------+ +| *Replaced* | The module has been replaced by | +| | another one (not sharing the same git | +| | history) | ++-----------------------------+----------------------------------------+ +| *Moved to standard?* | The module name has been detected in | +| | Odoo standard repositories for target | +| | version. High chance this module is or | +| | should be replaced by another one | +| | instead (by creating a timeline), so | +| | it mainly helps to detect such cases. | ++-----------------------------+----------------------------------------+ +| *Moved to OCA* | the module name is available in an OCA | +| | repository (could be a false-positive | +| | because sharing the same name, in such | +| | case a timeline has to be created) | ++-----------------------------+----------------------------------------+ +| *Moved to generic repo* | a specific module (only available in a | +| | project) in source version now exists | +| | in a generic repository (could be a | +| | false-positive if both modules have | +| | only their name in common) | ++-----------------------------+----------------------------------------+ + +It helps to build a consolidated knowledge database accross different +Odoo versions for everyone: functionals and developers. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To enable this feature, the option *Collect migration data* should be +enabled on repositories (opt-in). + +To record what a module became starting from a given Odoo version, you +should do it through the *Odoo Repositories / Data / Modules / +Timelines* menu. + +Once the scheduled action ran, the migration data are available on each +module in *Migration* tab, or through the *Odoo Repositories / Data / +Modules / Migrations* menu. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- Camptocamp + + - Sébastien Alix + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/module-composition-analysis `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo_repository_migration/__init__.py b/odoo_repository_migration/__init__.py new file mode 100644 index 00000000..4e97e1b8 --- /dev/null +++ b/odoo_repository_migration/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import update_oca_repositories diff --git a/odoo_repository_migration/__manifest__.py b/odoo_repository_migration/__manifest__.py new file mode 100644 index 00000000..5519b656 --- /dev/null +++ b/odoo_repository_migration/__manifest__.py @@ -0,0 +1,32 @@ +# Copyright 2023 Camptocamp SA +# Copyright 2026 Akretion (https://www.akretion.com). +# @author Sébastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +{ + "name": "Odoo Repository Migration Data", + "summary": "Collect modules migration data for Odoo Repositories.", + "version": "19.0.1.0.0", + "category": "Tools", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/module-composition-analysis", + "data": [ + "security/ir.model.access.csv", + "data/queue_job.xml", + "views/odoo_migration_path.xml", + "views/odoo_module_branch.xml", + "views/odoo_module_branch_migration.xml", + "views/odoo_module_branch_timeline.xml", + "views/odoo_repository.xml", + ], + "installable": True, + "depends": [ + "odoo_repository", + ], + "external_dependencies": { + "python": [ + "oca_port", + ], + }, + "license": "AGPL-3", + "post_init_hook": "update_oca_repositories", +} diff --git a/odoo_repository_migration/data/queue_job.xml b/odoo_repository_migration/data/queue_job.xml new file mode 100644 index 00000000..37a5d3ee --- /dev/null +++ b/odoo_repository_migration/data/queue_job.xml @@ -0,0 +1,29 @@ + + + + + odoo_repository_scan_migration + + + + + + _scan_migration_paths + + + + + + + _scan_migration_module + + + + diff --git a/odoo_repository_migration/hooks.py b/odoo_repository_migration/hooks.py new file mode 100644 index 00000000..203aa658 --- /dev/null +++ b/odoo_repository_migration/hooks.py @@ -0,0 +1,14 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + + +def update_oca_repositories(env): + """Configure OCA repositories to collect migration data.""" + org = env["odoo.repository.org"].search([("name", "=", "OCA")]) + if org: + repositories = ( + env["odoo.repository"] + .with_context(active_test=False) + .search([("org_id", "=", org.id)]) + ) + repositories.write({"collect_migration_data": True}) diff --git a/odoo_repository_migration/i18n/it.po b/odoo_repository_migration/i18n/it.po new file mode 100644 index 00000000..f56eb37c --- /dev/null +++ b/odoo_repository_migration/i18n/it.po @@ -0,0 +1,524 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * odoo_repository_migration +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_view_form +msgid "Next Versions" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "" +"A scan of related modules is required to update relevant migration data. " +"Wait for the next scheduled scan, or trigger one manually." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__active +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__active +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__active +msgid "Active" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Archived" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__author_ids +msgid "Authors" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_repository__collect_migration_data +msgid "Collect migration data" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_repository__collect_migration_data +msgid "Collect migration data based on the configured migration paths." +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_view_search +msgid "Commits to Port" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__create_uid +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__create_uid +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__create_uid +msgid "Created by" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__create_date +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__create_date +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__create_date +msgid "Created on" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_form +msgid "Data" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_migration_path +msgid "Define a migration path (from one branch to another)" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__display_name +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__display_name +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__display_name +msgid "Display Name" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__fully_ported +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Fully Ported" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Group By" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__id +msgid "ID" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_form +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "Info" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__last_known_module_branch_id +msgid "Last Known Module" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__last_source_scanned_commit +msgid "Last Source Scanned Commit" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__last_target_scanned_commit +msgid "Last Target Scanned Commit" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__write_uid +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__write_uid +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__write_date +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__write_date +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__write_date +msgid "Last Updated on" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__maintainer_ids +msgid "Maintainers" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__migration_path_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Migration Path" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.actions.act_window,name:odoo_repository_migration.odoo_migration_path_action +#: model:ir.ui.menu,name:odoo_repository_migration.odoo_migration_path_menu +msgid "Migration Paths" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch__migration_scan +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__migration_scan +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__migration_scan +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_project_module__migration_scan +msgid "Migration Scan" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__state +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Migration Status" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_module_branch_migration +msgid "Migration data for a module of a given branch." +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Migration to review" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.actions.act_window,name:odoo_repository_migration.odoo_module_branch_migration_action +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch__migration_ids +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__migration_ids +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_project_module__migration_ids +#: model:ir.ui.menu,name:odoo_repository_migration.odoo_module_branch_migration_menu +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_view_form +msgid "Migrations" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__module_branch_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Module" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_oca +msgid "" +"Module now available in OCA. This module is maybe not exactly the same, and " +"doesn't have the same scope so it deserves a check during a migration." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_standard +msgid "" +"Module now available in Odoo standard code. This module is maybe not exactly " +"the same, and doesn't have the same scope so it deserves a check during a " +"migration." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_oca +msgid "Moved To Oca" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_standard +msgid "Moved To Standard" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__moved_to_oca +msgid "Moved to OCA" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__moved_to_generic +msgid "Moved to generic repo" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__moved_to_standard +msgid "Moved to standard?" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__name +msgid "Name" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__next_module_branch_id +msgid "New module" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__next_module_id +msgid "New module name" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch__next_odoo_version_module_branch_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_project_module__next_odoo_version_module_branch_id +msgid "Next Odoo Version Module Branch" +msgstr "" + +#. module: odoo_repository_migration +#. odoo-python +#: code:addons/odoo_repository_migration/models/odoo_module_branch.py:0 +msgid "Next versions" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__note +msgid "Note" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "Notes" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_generic +msgid "Now generic" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_module_branch +msgid "Odoo Module Branch" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_module_branch_timeline +msgid "Odoo Module Timeline (renaming/replacement)" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_repository +msgid "Odoo Modules Repository" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Odoo Version" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__org_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__org_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Organization" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__pr_url +msgid "PR URL" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__port_commits +msgid "Ported (missing commits?)" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__process +msgid "Process" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Renamed" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__renamed_to_module_id +msgid "Renamed to" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_timeline__state +msgid "" +"Renamed: modules still share the same Git history (it allows to check " +"commits that could be ported)\n" +"Replaced: module has been replaced (or merged) by another one that fulfill " +"the same feature" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__replaced +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Replaced" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__replaced_by_module_id +msgid "Replaced by" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__repository_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__repository_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Repository" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "Restore" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__results +msgid "Results" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__results_text +msgid "Results Text" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_migration_path_view_form +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_form +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "Scan" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__odoo_version_sequence +msgid "Sequence" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__module_branch_id +msgid "Source" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__source_branch_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__source_branch_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Source Branch" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_generic +msgid "" +"Specific module now available in a generic repository. This module is maybe " +"not exactly the same, and doesn't have the same scope so it deserves a check " +"during a migration." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__odoo_version_id +msgid "Starting from version" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__state +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "State" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__target_module_branch_id +msgid "Target" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__target_branch_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__target_branch_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Target Branch" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_migration__migration_scan +msgid "Technical field telling if this migration path needs a migration scan." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch__migration_scan +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_project_module__migration_scan +msgid "" +"Technical field telling if this module is elligible for a migration scan." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_timeline__migration_scan +msgid "Technical field telling if this timeline needs a migration scan." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_timeline__last_known_module_branch_id +msgid "Technical field to check if current selected module is the last one." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__module_id +msgid "Technical name" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "" +"The selected module version is not the last one available.\n" +" Recording such timeline will ignore all newest " +"versions when computing related migration data." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.constraint,message:odoo_repository_migration.constraint_odoo_migration_path_migration_path_uniq +msgid "This migration path already exists." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.constraint,message:odoo_repository_migration.constraint_odoo_module_branch_migration_module_migration_path_uniq +msgid "This module migration path already exists." +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_migration_path_view_form +msgid "This operation will scan all relevant repositories. Are you sure?" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch__timeline_ids +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_project_module__timeline_ids +msgid "Timeline" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.actions.act_window,name:odoo_repository_migration.odoo_module_branch_timeline_action +#: model:ir.ui.menu,name:odoo_repository_migration.odoo_module_branch_timeline_menu +msgid "Timelines" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_view_search +msgid "To Migrate" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__migrate +msgid "To migrate" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__review_migration +msgid "To review" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_timeline__state__renamed +msgid "has been renamed to" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_timeline__state__replaced +msgid "has been replaced by" +msgstr "" diff --git a/odoo_repository_migration/i18n/odoo_repository_migration.pot b/odoo_repository_migration/i18n/odoo_repository_migration.pot new file mode 100644 index 00000000..ce1e38c1 --- /dev/null +++ b/odoo_repository_migration/i18n/odoo_repository_migration.pot @@ -0,0 +1,520 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * odoo_repository_migration +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_view_form +msgid "Next Versions" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "" +"A scan of related modules is required to update relevant migration data. " +"Wait for the next scheduled scan, or trigger one manually." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__active +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__active +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__active +msgid "Active" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Archived" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__author_ids +msgid "Authors" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_repository__collect_migration_data +msgid "Collect migration data" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_repository__collect_migration_data +msgid "Collect migration data based on the configured migration paths." +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_view_search +msgid "Commits to Port" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__create_uid +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__create_uid +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__create_uid +msgid "Created by" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__create_date +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__create_date +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__create_date +msgid "Created on" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_form +msgid "Data" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_migration_path +msgid "Define a migration path (from one branch to another)" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__display_name +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__display_name +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__display_name +msgid "Display Name" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__fully_ported +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Fully Ported" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Group By" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__id +msgid "ID" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_form +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "Info" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__last_known_module_branch_id +msgid "Last Known Module" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__last_source_scanned_commit +msgid "Last Source Scanned Commit" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__last_target_scanned_commit +msgid "Last Target Scanned Commit" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__write_uid +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__write_uid +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__write_date +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__write_date +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__write_date +msgid "Last Updated on" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__maintainer_ids +msgid "Maintainers" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__migration_path_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Migration Path" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.actions.act_window,name:odoo_repository_migration.odoo_migration_path_action +#: model:ir.ui.menu,name:odoo_repository_migration.odoo_migration_path_menu +msgid "Migration Paths" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch__migration_scan +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__migration_scan +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__migration_scan +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_project_module__migration_scan +msgid "Migration Scan" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__state +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Migration Status" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_module_branch_migration +msgid "Migration data for a module of a given branch." +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Migration to review" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.actions.act_window,name:odoo_repository_migration.odoo_module_branch_migration_action +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch__migration_ids +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__migration_ids +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_project_module__migration_ids +#: model:ir.ui.menu,name:odoo_repository_migration.odoo_module_branch_migration_menu +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_view_form +msgid "Migrations" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__module_branch_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Module" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_oca +msgid "" +"Module now available in OCA. This module is maybe not exactly the same, and " +"doesn't have the same scope so it deserves a check during a migration." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_standard +msgid "" +"Module now available in Odoo standard code. This module is maybe not exactly" +" the same, and doesn't have the same scope so it deserves a check during a " +"migration." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_oca +msgid "Moved To Oca" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_standard +msgid "Moved To Standard" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__moved_to_oca +msgid "Moved to OCA" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__moved_to_generic +msgid "Moved to generic repo" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__moved_to_standard +msgid "Moved to standard?" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__name +msgid "Name" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__next_module_branch_id +msgid "New module" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__next_module_id +msgid "New module name" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch__next_odoo_version_module_branch_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_project_module__next_odoo_version_module_branch_id +msgid "Next Odoo Version Module Branch" +msgstr "" + +#. module: odoo_repository_migration +#. odoo-python +#: code:addons/odoo_repository_migration/models/odoo_module_branch.py:0 +msgid "Next versions" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__note +msgid "Note" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "Notes" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_generic +msgid "Now generic" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_module_branch +msgid "Odoo Module Branch" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_module_branch_timeline +msgid "Odoo Module Timeline (renaming/replacement)" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model,name:odoo_repository_migration.model_odoo_repository +msgid "Odoo Modules Repository" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Odoo Version" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__org_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__org_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Organization" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__pr_url +msgid "PR URL" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__port_commits +msgid "Ported (missing commits?)" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__process +msgid "Process" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Renamed" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__renamed_to_module_id +msgid "Renamed to" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_timeline__state +msgid "" +"Renamed: modules still share the same Git history (it allows to check commits that could be ported)\n" +"Replaced: module has been replaced (or merged) by another one that fulfill the same feature" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__replaced +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Replaced" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__replaced_by_module_id +msgid "Replaced by" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__repository_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__repository_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "Repository" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "Restore" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__results +msgid "Results" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__results_text +msgid "Results Text" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_migration_path_view_form +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_form +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "Scan" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__odoo_version_sequence +msgid "Sequence" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__module_branch_id +msgid "Source" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__source_branch_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__source_branch_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Source Branch" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_migration__moved_to_generic +msgid "" +"Specific module now available in a generic repository. This module is maybe " +"not exactly the same, and doesn't have the same scope so it deserves a check" +" during a migration." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__odoo_version_id +msgid "Starting from version" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline__state +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_search +msgid "State" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__target_module_branch_id +msgid "Target" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path__target_branch_id +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__target_branch_id +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +msgid "Target Branch" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_migration__migration_scan +msgid "Technical field telling if this migration path needs a migration scan." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch__migration_scan +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_project_module__migration_scan +msgid "" +"Technical field telling if this module is elligible for a migration scan." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_timeline__migration_scan +msgid "Technical field telling if this timeline needs a migration scan." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,help:odoo_repository_migration.field_odoo_module_branch_timeline__last_known_module_branch_id +msgid "Technical field to check if current selected module is the last one." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration__module_id +msgid "Technical name" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_timeline_view_form +msgid "" +"The selected module version is not the last one available.\n" +" Recording such timeline will ignore all newest versions when computing related migration data." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.constraint,message:odoo_repository_migration.constraint_odoo_migration_path_migration_path_uniq +msgid "This migration path already exists." +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.constraint,message:odoo_repository_migration.constraint_odoo_module_branch_migration_module_migration_path_uniq +msgid "This module migration path already exists." +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_migration_path_view_form +msgid "This operation will scan all relevant repositories. Are you sure?" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch__timeline_ids +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_project_module__timeline_ids +msgid "Timeline" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.actions.act_window,name:odoo_repository_migration.odoo_module_branch_timeline_action +#: model:ir.ui.menu,name:odoo_repository_migration.odoo_module_branch_timeline_menu +msgid "Timelines" +msgstr "" + +#. module: odoo_repository_migration +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_migration_view_search +#: model_terms:ir.ui.view,arch_db:odoo_repository_migration.odoo_module_branch_view_search +msgid "To Migrate" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__migrate +msgid "To migrate" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_migration__state__review_migration +msgid "To review" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_timeline__state__renamed +msgid "has been renamed to" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields.selection,name:odoo_repository_migration.selection__odoo_module_branch_timeline__state__replaced +msgid "has been replaced by" +msgstr "" diff --git a/odoo_repository_migration/models/__init__.py b/odoo_repository_migration/models/__init__.py new file mode 100644 index 00000000..75bb5828 --- /dev/null +++ b/odoo_repository_migration/models/__init__.py @@ -0,0 +1,5 @@ +from . import odoo_migration_path +from . import odoo_module_branch_migration +from . import odoo_module_branch_timeline +from . import odoo_module_branch +from . import odoo_repository diff --git a/odoo_repository_migration/models/odoo_migration_path.py b/odoo_repository_migration/models/odoo_migration_path.py new file mode 100644 index 00000000..3468256a --- /dev/null +++ b/odoo_repository_migration/models/odoo_migration_path.py @@ -0,0 +1,58 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class OdooMigrationPath(models.Model): + _name = "odoo.migration.path" + _description = "Define a migration path (from one branch to another)" + _order = "name" + + name = fields.Char(compute="_compute_name", store=True) + active = fields.Boolean(default=True) + source_branch_id = fields.Many2one( + comodel_name="odoo.branch", + ondelete="cascade", + required=True, + ) + target_branch_id = fields.Many2one( + comodel_name="odoo.branch", + ondelete="cascade", + required=True, + ) + + _migration_path_uniq = models.Constraint( + "UNIQUE (source_branch_id, target_branch_id)", + "This migration path already exists.", + ) + + @api.depends("source_branch_id.name", "target_branch_id.name") + def _compute_name(self): + for rec in self: + rec.name = f"{rec.source_branch_id.name} -> {rec.target_branch_id.name}" + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + # Recompute 'migration_scan' flag on relevant modules + modules = self.env["odoo.module.branch"].search( + [ + "|", + ("branch_id", "in", records.source_branch_id.ids), + ("branch_id", "in", records.target_branch_id.ids), + ] + ) + modules.modified(["last_scanned_commit"]) + modules.flush_recordset(["migration_scan"]) + return records + + def action_scan(self): + """Scan the source+target branches. + + Scan is done on all related repositories configured to collect migration data. + """ + branches = self.source_branch_id | self.target_branch_id + return branches.repository_branch_ids.filtered( + lambda o: o.repository_id.collect_migration_data + ).action_scan() diff --git a/odoo_repository_migration/models/odoo_module_branch.py b/odoo_repository_migration/models/odoo_module_branch.py new file mode 100644 index 00000000..b77b7b4b --- /dev/null +++ b/odoo_repository_migration/models/odoo_module_branch.py @@ -0,0 +1,255 @@ +# Copyright 2023 Camptocamp SA +# Copyright 2026 Akretion (https://www.akretion.com). +# @author Sébastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class OdooModuleBranch(models.Model): + _inherit = "odoo.module.branch" + + timeline_ids = fields.One2many( + comodel_name="odoo.module.branch.timeline", + inverse_name="module_branch_id", + string="Timeline", + ) + next_odoo_version_module_branch_id = fields.Many2one( + comodel_name="odoo.module.branch", + compute="_compute_next_odoo_version_module_branch_id", + string="Next Odoo Version Module Branch", + ) + migration_ids = fields.One2many( + comodel_name="odoo.module.branch.migration", + inverse_name="module_branch_id", + string="Migrations", + ) + migration_scan = fields.Boolean( + compute="_compute_migration_scan", + store=True, + help=( + "Technical field telling if this module is elligible for a migration scan." + ), + ) + + @api.depends("branch_id.next_id", "timeline_ids") + def _compute_next_odoo_version_module_branch_id(self): + for rec in self: + rec.next_odoo_version_module_branch_id = False + # Stop there if no next version + if not rec.branch_id.next_id: + continue + # Look for the next available version for this module name + rec.next_odoo_version_module_branch_id = self.search( + [ + ("branch_sequence", ">=", rec.branch_id.next_id.sequence), + ("module_id", "=", rec.module_id.id), + ], + order="branch_sequence", + limit=1, + ) + # Stop there if no renaming/relacement + if not rec.timeline_ids: + continue + rec.next_odoo_version_module_branch_id = self.search( + [ + ("branch_sequence", ">=", rec.branch_id.next_id.sequence), + ("module_id", "=", rec.timeline_ids.next_module_id.id), + ], + order="branch_sequence", + limit=1, + ) + + def _replaced_by_module_in_target_version(self, target_branch): + """Return the module replacing current one in last module versions. + + Look for current + next modules as the migration scan could do a jump + 14.0 -> 18.0, while a module has been replaced starting from 18.0 (and + therefore flagged as replaced in 17.0 module data). + + We give the priority to last modules while checking them. + """ + self.ensure_one() + replaced_by = self.env["odoo.module"] + next_ = self + while next_ and next_.branch_sequence < target_branch.sequence: + timeline = next_.timeline_ids + if timeline.state == "replaced": + replaced_by = timeline.next_module_id + next_ = next_.next_odoo_version_module_branch_id + # If a module has been renamed after been replaced, the module to + # return has to be the renamed one + if next_ and replaced_by: + replaced_by = next_.module_id + return replaced_by + + def _renamed_to_module_in_target_version(self, target_branch): + """Return the new module technical name in last module versions. + + Look for current + next modules as the migration scan could do a jump + 14.0 -> 18.0, while a module has been renamed starting from 18.0 (and + therefore flagged as renamed in 17.0 module data). + + We give the priority to last modules while checking them. + """ + self.ensure_one() + renamed_to = self.env["odoo.module"] + next_ = self + while next_ and next_.branch_sequence < target_branch.sequence: + timeline = next_.timeline_ids + # As soon as the module has been replaced in the chain of versions + # it cannot be considered as renamed anymore + if timeline.state == "replaced": + renamed_to = self.env["odoo.module"] + next_ = False + continue + if timeline.state == "renamed": + renamed_to = timeline.next_module_id + next_ = next_.next_odoo_version_module_branch_id + return renamed_to + + def _get_next_versions( + self, target_branch=None, order="branch_sequence", limit=None + ): + """Return all available upcoming versions. + + It doesn't take into account module renaming or replacement. Only versions + for the current module name are returned. + """ + self.ensure_one() + if not target_branch: + target_branch = self.env["odoo.branch"]._get_last_version() + return self.env["odoo.module.branch"].search( + [ + ("module_id", "=", self.module_id.id), + ("repository_id", "!=", False), + ("installable", "=", True), + ("branch_id.sequence", ">=", self.branch_id.sequence), + ("branch_id.sequence", "<", target_branch.sequence), + ], + order=order, + limit=limit, + ) + + def _get_next_module_branches(self, target_branch=None): + """Return all modules in the right version order starting from current one. + + This is taking into account module renamed or replaced in intermediate versions. + """ + if not self: + return self.browse() + self.ensure_one() + if target_branch: + assert self.branch_id.sequence < target_branch.sequence + next_module_branch = self.next_odoo_version_module_branch_id + next_module_branch_ids = [] + while next_module_branch: + if ( + target_branch + and next_module_branch.branch_id.sequence > target_branch.sequence + ): + break + next_module_branch_ids.append(next_module_branch.id) + next_module_branch = next_module_branch.next_odoo_version_module_branch_id + return self.browse(next_module_branch_ids) + + @api.depends( + "removed", + "pr_url", + "last_scanned_commit", + "migration_ids.migration_scan", + "repository_id.collect_migration_data", + ) + def _compute_migration_scan(self): + for rec in self: + # Do not scan removed or pending (in PR) modules + if rec.removed or rec.pr_url: + rec.migration_scan = False + continue + # Default repository migration scan policy + rec.migration_scan = rec.repository_id.collect_migration_data + if not rec.migration_scan: + continue + # Repository scan has to be performed first + if not rec.last_scanned_commit: + continue + # Migration scan to do as soon as a migration path is missing + # among existing scans. However, we remove migration path that doesn't + # match branches scanned in the repository (e.g. 18.0 branch could + # be missing in a repo while a migration path 16.0 -> 18.0 is + # configured, so no need to do a migration scan in this case). + available_repo_branches = rec.repository_id.branch_ids.branch_id + available_migration_paths = self.env["odoo.migration.path"].search( + [ + ("source_branch_id", "=", rec.branch_id.id), + ("target_branch_id", "in", available_repo_branches.ids), + ] + ) + scanned_migration_paths = rec.migration_ids.migration_path_id + if available_migration_paths != scanned_migration_paths: + rec.migration_scan = True + continue + # Migration scan to do if any of the migration path requires one + rec.migration_scan = any(rec.migration_ids.mapped("migration_scan")) + + def _to_dict(self): + # Add the migrations data + data = super()._to_dict() + data["migrations"] = [] + for migration in self.migration_ids: + data["migrations"].append(migration._to_dict()) + return data + + @api.model_create_multi + def create(self, vals_list): + recs = super().create(vals_list) + recs._update_migration_data() + return recs + + def write(self, vals): + res = super().write(vals) + # When 'pr_url' is set or unset, this means the module has been found + # in a PR or has been merged upstream. We want to refresh relevant + # migration data in such case. + if "pr_url" in vals: + self._update_migration_data() + return res + + def _update_migration_data(self): + """Update relevant module migration records.""" + for rec in self: + timelines = rec._get_related_timelines() + impacted_modules = rec.module_id | timelines._get_all_impacted_modules() + migrations = self.env["odoo.module.branch.migration"].search( + [ + ("module_id", "in", impacted_modules.ids), + ("source_branch_id.sequence", "<=", rec.branch_id.sequence), + ("target_branch_id.sequence", ">=", rec.branch_id.sequence), + ] + ) + migrations.force_update() + + def open_next_module_branches(self): + self.ensure_one() + xml_id = "odoo_repository.odoo_module_branch_action" + action = self.env["ir.actions.actions"]._for_xml_id(xml_id) + action["name"] = self.env._("Next versions") + action["domain"] = [("id", "in", self._get_next_module_branches().ids)] + return action + + def _get_related_timelines(self): + """Return timelines related to current module. + + A module A could be renamed to a module B in version X, then renamed + again to C in version X+1, etc. + When calling this method on any module, all related timelines are returned. + """ + self.ensure_one() + timeline = self.env["odoo.module.branch.timeline"].search( + [ + "|", + ("module_branch_id.module_id", "=", self.module_id.id), + ("next_module_id", "=", self.module_id.id), + ] + ) + return timeline._get_related_timelines() if timeline else timeline diff --git a/odoo_repository_migration/models/odoo_module_branch_migration.py b/odoo_repository_migration/models/odoo_module_branch_migration.py new file mode 100644 index 00000000..a90278c4 --- /dev/null +++ b/odoo_repository_migration/models/odoo_module_branch_migration.py @@ -0,0 +1,371 @@ +# Copyright 2023 Camptocamp SA +# Copyright 2026 Akretion (https://www.akretion.com). +# @author Sébastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import pprint + +from odoo import api, fields, models + + +class OdooModuleBranchMigration(models.Model): + _name = "odoo.module.branch.migration" + _description = "Migration data for a module of a given branch." + _order = "display_name" + + display_name = fields.Char(compute="_compute_display_name", store=True) + module_branch_id = fields.Many2one( + comodel_name="odoo.module.branch", + ondelete="cascade", + string="Source", + required=True, + index=True, + ) + module_id = fields.Many2one( + related="module_branch_id.module_id", + store=True, + index=True, + ) + org_id = fields.Many2one(related="module_branch_id.org_id", store=True) + repository_id = fields.Many2one( + related="module_branch_id.repository_id", + store=True, + ondelete="cascade", + ) + migration_path_id = fields.Many2one( + comodel_name="odoo.migration.path", + ondelete="cascade", + required=True, + index=True, + ) + source_branch_id = fields.Many2one( + related="migration_path_id.source_branch_id", + store=True, + index=True, + ) + target_branch_id = fields.Many2one( + related="migration_path_id.target_branch_id", + store=True, + index=True, + ) + target_module_branch_id = fields.Many2one( + comodel_name="odoo.module.branch", + ondelete="cascade", + string="Target", + compute="_compute_target_module_branch_id", + store=True, + index=True, + ) + author_ids = fields.Many2many(related="module_branch_id.author_ids") + maintainer_ids = fields.Many2many(related="module_branch_id.maintainer_ids") + process = fields.Char(index=True) + moved_to_standard = fields.Boolean( + compute="_compute_moved_to_standard", + store=True, + help=( + "Module now available in Odoo standard code. " + "This module is maybe not exactly the same, and doesn't have the " + "same scope so it deserves a check during a migration." + ), + ) + moved_to_oca = fields.Boolean( + compute="_compute_moved_to_oca", + store=True, + help=( + "Module now available in OCA. " + "This module is maybe not exactly the same, and doesn't have the " + "same scope so it deserves a check during a migration." + ), + ) + moved_to_generic = fields.Boolean( + compute="_compute_moved_to_generic", + store=True, + string="Now generic", + help=( + "Specific module now available in a generic repository. " + "This module is maybe not exactly the same, and doesn't have the " + "same scope so it deserves a check during a migration." + ), + ) + renamed_to_module_id = fields.Many2one( + comodel_name="odoo.module", + compute="_compute_renamed_to_module_id", + string="Renamed to", + store=True, + index=True, + ) + replaced_by_module_id = fields.Many2one( + comodel_name="odoo.module", + compute="_compute_replaced_by_module_id", + string="Replaced by", + store=True, + index=True, + ) + state = fields.Selection( + selection=[ + ("fully_ported", "Fully Ported"), + ("migrate", "To migrate"), + ("port_commits", "Ported (missing commits?)"), + ("review_migration", "To review"), + ("replaced", "Replaced"), + ("moved_to_standard", "Moved to standard?"), + ("moved_to_oca", "Moved to OCA"), + ("moved_to_generic", "Moved to generic repo"), + ], + string="Migration Status", + compute="_compute_state", + store=True, + index=True, + ) + pr_url = fields.Char( + string="PR URL", + compute="_compute_pr_url", + store=True, + ) + results = fields.Serialized() + results_text = fields.Text(compute="_compute_results_text") + last_source_scanned_commit = fields.Char() + last_target_scanned_commit = fields.Char() + active = fields.Boolean(related="migration_path_id.active", store=True) + migration_scan = fields.Boolean( + compute="_compute_migration_scan", + store=True, + help="Technical field telling if this migration path needs a migration scan.", + ) + + _module_migration_path_uniq = models.Constraint( + "UNIQUE (module_branch_id, migration_path_id)", + "This module migration path already exists.", + ) + + @api.depends( + "module_branch_id.module_id", + "source_branch_id.name", + "target_branch_id.name", + ) + def _compute_display_name(self): + for rec in self: + rec.display_name = ( + f"{rec.module_branch_id.module_id.name}: " + f"{rec.source_branch_id.name} -> {rec.target_branch_id.name}" + ) + + @api.depends( + "module_branch_id", + "migration_path_id", + "replaced_by_module_id", + "renamed_to_module_id", + ) + def _compute_target_module_branch_id(self): + module_branch_model = self.env["odoo.module.branch"] + for rec in self: + # Look for the right module technical name + module = ( + rec.replaced_by_module_id + or rec.renamed_to_module_id + or rec.module_branch_id.module_id + ) + rec.target_module_branch_id = module_branch_model._find( + rec.migration_path_id.target_branch_id, + module, + rec.module_branch_id.repository_id, + domain=[("installable", "=", True)], + ) + + @api.depends("module_branch_id.is_standard", "target_module_branch_id.is_standard") + def _compute_moved_to_standard(self): + for rec in self: + rec.moved_to_standard = ( + not rec.module_branch_id.is_standard + and rec.target_module_branch_id.is_standard + ) + + @api.depends("org_id", "target_module_branch_id.org_id") + def _compute_moved_to_oca(self): + org_oca = self.env.ref( + "odoo_repository.odoo_repository_org_oca", raise_if_not_found=False + ) + for rec in self: + rec.moved_to_oca = False + if not org_oca: + continue + rec.moved_to_oca = ( + rec.org_id != org_oca and rec.target_module_branch_id.org_id == org_oca + ) + + @api.depends( + "repository_id.specific", "target_module_branch_id.repository_id.specific" + ) + def _compute_moved_to_generic(self): + for rec in self: + rec.moved_to_generic = ( + rec.repository_id.specific + and rec.target_module_branch_id.repository_id + and not rec.target_module_branch_id.repository_id.specific + ) + + # NOTE: _renamed_to_module_in_target_version relies also on timelines, and + # this computed field will be recomputed automatically on timeline updates. + @api.depends("target_branch_id") + def _compute_renamed_to_module_id(self): + for rec in self: + rec.renamed_to_module_id = ( + rec.module_branch_id._renamed_to_module_in_target_version( + rec.target_branch_id + ) + ) + + # NOTE: _replaced_by_module_in_target_version relies also on timelines, and + # this computed field will be recomputed automatically on timeline updates. + @api.depends("target_branch_id") + def _compute_replaced_by_module_id(self): + for rec in self: + rec.replaced_by_module_id = ( + rec.module_branch_id._replaced_by_module_in_target_version( + rec.target_branch_id + ) + ) + + @api.depends( + "replaced_by_module_id", + "process", + "pr_url", + "moved_to_standard", + "moved_to_oca", + "moved_to_generic", + ) + def _compute_state(self): + for rec in self: + if rec.replaced_by_module_id: + # Module replaced by another one + rec.state = "replaced" + continue + if rec.moved_to_standard: + # Module moved to a standard repository (likely from OCA to + # odoo/odoo, like 'l10n_eu_oss', 'knowledge', ...). + # E.g. this could tell integrators that a module like + # 'l10n_eu_oss_oca' should now be used instead. + rec.state = "moved_to_standard" + continue + if rec.moved_to_oca: + # Module moved to an OCA repository + rec.state = "moved_to_oca" + continue + if rec.moved_to_generic: + # Specific module moved to a generic repository (public or private) + rec.state = "moved_to_generic" + continue + rec.state = rec.process or ( + "fully_ported" + if ( + rec.target_module_branch_id + and not rec.target_module_branch_id.pr_url + ) + else "migrate" + ) + if rec.process == "migrate" and rec.pr_url: + rec.state = "review_migration" + + @api.depends("results") + def _compute_pr_url(self): + for rec in self: + rec.pr_url = rec.results.get("existing_pr", {}).get("url") + + @api.depends("results") + def _compute_results_text(self): + for rec in self: + rec.results_text = pprint.pformat(rec.results) + + @api.depends( + "module_branch_id.last_scanned_commit", + "replaced_by_module_id", + "repository_id.collect_migration_data", + "last_source_scanned_commit", + "last_target_scanned_commit", + "pr_url", + "target_module_branch_id.pr_url", + "target_module_branch_id.last_scanned_commit", + "state", + ) + def _compute_migration_scan(self): + # Migration scan to do if last scanned commit doesn't match the last + # migration scan, both for source and target modules. + for rec in self: + rec.migration_scan = False + # No migration scan if repository is not configured to do it + if not rec.repository_id.collect_migration_data: + continue + # No migration scan for modules moved to Odoo/OCA/generic repo + if rec.state and rec.state.startswith("moved_to"): + continue + # No migration scan for modules replaced by another module + if rec.replaced_by_module_id: + continue + if ( + rec.last_source_scanned_commit + != rec.module_branch_id.last_scanned_commit + ): + rec.migration_scan = True + elif ( + rec.target_module_branch_id.last_scanned_commit + and rec.last_target_scanned_commit + != rec.target_module_branch_id.last_scanned_commit + ): + rec.migration_scan = True + elif rec.target_module_branch_id.pr_url != rec.pr_url: + rec.migration_scan = True + + def force_update(self): + """Ensure the migration data is updated or will be updated on next scan.""" + self_sudo = self.sudo() + self_sudo.last_target_scanned_commit = False + self_sudo.results = self_sudo.process = False + self_sudo._compute_replaced_by_module_id() + self_sudo._compute_renamed_to_module_id() + self_sudo._compute_target_module_branch_id() + self_sudo._compute_state() + self_sudo._compute_migration_scan() + + @api.model + def push_scanned_data(self, module_branch_id, data): + migration_path = self.env["odoo.migration.path"].search( + [ + ("source_branch_id", "=", data["source_version"]), + ("target_branch_id", "=", data["target_version"]), + ] + ) + values = { + "module_branch_id": module_branch_id, + "migration_path_id": migration_path.id, + "last_source_scanned_commit": data["source_commit"], + "last_target_scanned_commit": data["target_commit"], + } + # Update migration data only if a migration scan occured + if "report" in data: + values["process"] = data["report"].get("process", False) + values["results"] = data["report"].get("results", {}) + return self._create_or_update(module_branch_id, migration_path, values) + + def _create_or_update(self, module_branch_id, migration_path, values): + args = [ + ("module_branch_id", "=", module_branch_id), + ("source_branch_id", "=", migration_path.source_branch_id.id), + ("target_branch_id", "=", migration_path.target_branch_id.id), + ] + migration = self.search(args) + if migration: + migration.sudo().write(values) + else: + migration = self.sudo().create(values) + return migration + + def _to_dict(self): + self.ensure_one() + return { + "source_branch": self.source_branch_id.name, + "target_branch": self.target_branch_id.name, + "process": self.process, + "results": self.results, + "last_source_scanned_commit": self.last_source_scanned_commit, + "last_target_scanned_commit": self.last_target_scanned_commit, + } diff --git a/odoo_repository_migration/models/odoo_module_branch_timeline.py b/odoo_repository_migration/models/odoo_module_branch_timeline.py new file mode 100644 index 00000000..fae36ce6 --- /dev/null +++ b/odoo_repository_migration/models/odoo_module_branch_timeline.py @@ -0,0 +1,224 @@ +# Copyright 2025 Camptocamp SA +# Copyright 2026 Akretion (https://www.akretion.com). +# @author Sébastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class OdooModuleBranchTimeline(models.Model): + _name = "odoo.module.branch.timeline" + _description = "Odoo Module Timeline (renaming/replacement)" + _order = "odoo_version_sequence" + + module_branch_id = fields.Many2one( + string="Module", + comodel_name="odoo.module.branch", + required=True, + index=True, + ) + last_known_module_branch_id = fields.Many2one( + string="Last Known Module", + comodel_name="odoo.module.branch", + compute="_compute_last_known_module_branch_id", + help="Technical field to check if current selected module is the last one.", + ) + org_id = fields.Many2one( + string="Organization", + related="module_branch_id.org_id", + store=True, + index=True, + ) + repository_id = fields.Many2one( + string="Repository", + related="module_branch_id.repository_id", + store=True, + index=True, + ) + odoo_version_id = fields.Many2one( + string="Starting from version", + related="module_branch_id.branch_id.next_id", + store=True, + index=True, + ) + odoo_version_sequence = fields.Integer( + related="odoo_version_id.sequence", + store=True, + ) + state = fields.Selection( + selection=[ + ("renamed", "has been renamed to"), + ("replaced", "has been replaced by"), + ], + default="renamed", + required=True, + index=True, + help=( + "Renamed: modules still share the same Git history (it allows to " + "check commits that could be ported)\n" + "Replaced: module has been replaced (or merged) by another one that " + "fulfill the same feature" + ), + ) + next_module_id = fields.Many2one( + string="New module name", + comodel_name="odoo.module", + ondelete="restrict", + index=True, + ) + next_module_branch_id = fields.Many2one( + string="New module", + related="module_branch_id.next_odoo_version_module_branch_id", + ) + note = fields.Html() + migration_ids = fields.Many2many( + comodel_name="odoo.module.branch.migration", + string="Migrations", + compute="_compute_migration_ids", + ) + migration_scan = fields.Boolean( + compute="_compute_migration_scan", + help="Technical field telling if this timeline needs a migration scan.", + ) + active = fields.Boolean(default=True) + + @api.depends("odoo_version_id", "module_branch_id", "next_module_id") + def _compute_display_name(self): + for rec in self: + rec.display_name = ( + f"[{rec.odoo_version_id.name}] " + f"{rec.module_branch_id.module_id.name} > {rec.next_module_id.name}" + ) + + @api.depends("module_branch_id") + def _compute_last_known_module_branch_id(self): + for rec in self: + rec.last_known_module_branch_id = self.env["odoo.module.branch"].search( + [ + ("module_id", "=", rec.module_branch_id.module_id.id), + ("repository_id", "!=", False), + ("installable", "=", True), + ( + "branch_id.sequence", + ">=", + rec.module_branch_id.branch_id.sequence, + ), + ], + order="branch_sequence DESC", + limit=1, + ) + + @api.depends("module_branch_id", "next_module_id", "odoo_version_id") + def _compute_migration_ids(self): + for rec in self: + current_module = rec.module_branch_id.module_id + next_module = rec.next_module_id + impacted_modules = current_module + next_module + rec.migration_ids = ( + self.env["odoo.module.branch.migration"] + .search( + [ + ( + "target_branch_id.sequence", + ">=", + rec.odoo_version_id.sequence, + ), + "|", + "|", + ("module_id", "=", current_module.id), + ("renamed_to_module_id", "in", impacted_modules.ids), + ("replaced_by_module_id", "in", impacted_modules.ids), + ] + ) + .sudo() + ) + + def _compute_migration_scan(self): + for rec in self: + rec.migration_scan = any(rec.migration_ids.mapped("migration_scan")) + + # Update relevant migration records on timeline update. + # + # When a module is renamed or replaced, we refresh all impacted + # migration data records and reset their last scan commits to trigger + # a new migration scan. + # + # E.g. + # if a module on 17.0 is set as renamed starting from 18.0, existing + # migration data (17.0 -> 18.0, or 17.0 -> 19.0) will be aware of such + # change and all related modules will have to be scanned again. + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + records.migration_ids.force_update() + return records + + @api.model + def write(self, vals): + migrations = self.migration_ids + res = super().write(vals) + (self.migration_ids | migrations).force_update() + return res + + def unlink(self): + migrations = self.migration_ids + res = super().unlink() + migrations.force_update() + return res + + def open_migrations(self): + self.ensure_one() + xml_id = "odoo_repository_migration.odoo_module_branch_migration_action" + action = self.env["ir.actions.actions"]._for_xml_id(xml_id) + action["domain"] = [("id", "in", self.migration_ids.ids)] + return action + + def action_scan(self): + self.migration_ids.module_branch_id.repository_branch_id.action_scan() + return True + + def _get_related_timelines(self): + """Return timelines related to current ones regarding impacted modules. + + A module A could be renamed to a module B in version X, then renamed + again to C in version X+1, etc. + When calling this method on any timeline, the others are returned. + """ + impacted_modules = self._get_all_impacted_modules() + return self.search( + [ + "|", + ("module_branch_id.module_id", "in", impacted_modules.ids), + ("next_module_id", "in", impacted_modules.ids), + ] + ) + + # TODO: add ormcache? + def _get_all_impacted_modules(self, visited=None): + """Recursively collect all modules involved in the chain of timelines.""" + if visited is None: + visited = self.env["odoo.module.branch.timeline"].browse() + impacted_modules = self.env["odoo.module"].browse() + for rec in self: + if rec in visited: + continue + visited |= rec + impacted_modules |= rec.module_branch_id.module_id + rec.next_module_id + # Find previous timelines (current module is next_module_id) + previous_timelines = self.search( + [ + ("next_module_id", "=", rec.module_branch_id.module_id.id), + ] + ) + for timeline in previous_timelines: + impacted_modules |= timeline._get_all_impacted_modules(visited) + # Find next timelines (next_module_id is the module_branch_id) + next_timelines = self.search( + [ + ("module_branch_id.module_id", "=", rec.next_module_id.id), + ] + ) + for timeline in next_timelines: + impacted_modules |= timeline._get_all_impacted_modules(visited) + return impacted_modules diff --git a/odoo_repository_migration/models/odoo_repository.py b/odoo_repository_migration/models/odoo_repository.py new file mode 100644 index 00000000..71800f2e --- /dev/null +++ b/odoo_repository_migration/models/odoo_repository.py @@ -0,0 +1,361 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models, tools + +from odoo.addons.queue_job.delay import chain +from odoo.addons.queue_job.exception import RetryableJobError +from odoo.addons.queue_job.job import identity_exact + +from ..utils.scanner import MigrationScannerOdooEnv + + +class OdooRepository(models.Model): + _inherit = "odoo.repository" + + collect_migration_data = fields.Boolean( + string="Collect migration data", + help=("Collect migration data based on the configured migration paths."), + default=False, + ) + + def action_scan(self, branch_ids=None, force=False, raise_exc=True): + for rec in self: + # Scan only relevant branches regarding migration paths + rec_ctx = rec + if rec.specific: + rec_ctx = rec.with_context(strict_branches_scan=True) + super(OdooRepository, rec_ctx).action_scan( + branch_ids=branch_ids, force=force, raise_exc=raise_exc + ) + return True + + def _reset_scanned_commits(self, branch_ids=None): + res = super()._reset_scanned_commits(branch_ids=branch_ids) + if branch_ids is None: + branch_ids = self.branch_ids.branch_id.ids + repo_branches = self.branch_ids.filtered( + lambda rb: rb.branch_id.id in branch_ids + ) + repo_branches.module_ids.migration_ids.sudo().write( + { + "last_source_scanned_commit": False, + "last_target_scanned_commit": False, + } + ) + return res + + def _create_subsequent_jobs( + self, version_branch, next_versions_branches, all_versions_branches, data + ): + jobs = super()._create_subsequent_jobs( + version_branch, next_versions_branches, all_versions_branches, data + ) + # Prepare migration scan jobs when its the last repository scan + last_scan = not next_versions_branches + if not last_scan: + return jobs + # Check if the addons_paths are compatible with 'oca_port' + disable_collect = self.env.context.get("disable_collect_migration_data") + if not self.collect_migration_data or disable_collect: + return jobs + # Override to run the MigrationScanner once branches are scanned + args = [] + if all_versions_branches: + all_versions = [vb[0] for vb in all_versions_branches] + # A strict scan of branches avoids unwanted migration scans + # For instance if we are interested only by 14.0 and 17.0 branches, + # this avoids to scan other migration paths like 15.0 -> 17.0 + # NOTE: a strict scan always occurs on specific repositories + strict_scan = self.env.context.get("strict_branches_scan") or self.specific + args = [ + "&" if strict_scan else "|", + ("source_branch_id", "in", all_versions), + ("target_branch_id", "in", all_versions), + ] + # Without version filter all migration paths are wanted (small table) + migration_paths = self.env["odoo.migration.path"].search( # pylint: disable=no-search-all + args + ) + # Launch one job for all migration_paths + if migration_paths: + # Migration paths parameter containing the migration path ID + + # the Odoo versions and branches to scan. + # E.g. {MIG_PATH_ID: [('14.0', 'master'), ('18.0', '18.0-mig')], ...} + migration_paths_param = {} + for migration_path in migration_paths: + source_rb = self.branch_ids.filtered( + lambda rb, mp=migration_path: rb.branch_id == mp.source_branch_id + ) + target_rb = self.branch_ids.filtered( + lambda rb, mp=migration_path: rb.branch_id == mp.target_branch_id + ) + # Need the two Odoo versions of the migration path available + # in the scanned repository + if not source_rb or not target_rb: + continue + # Build list of tuples (Odoo version, branch name) corresponding + # to the migration path + versions_branches = [ + ( + source_rb.branch_id.name, + source_rb.cloned_branch or source_rb.branch_id.name, + ), + ( + target_rb.branch_id.name, + target_rb.cloned_branch or target_rb.branch_id.name, + ), + ] + migration_paths_param[migration_path.id] = versions_branches + + delayable = self.delayable( + description=f"Collect {self.display_name} migration data", + identity_key=identity_exact, + ) + job = delayable._scan_migration_paths(migration_paths_param) + jobs.append(job) + return jobs + + def _scan_migration_paths(self, migration_paths_param): + """Scan repository branches to collect modules migration data. + + Spawn one job per module to scan. + """ + self.ensure_one() + jobs = [] + for migration_path_id in migration_paths_param: + versions_branches = migration_paths_param[migration_path_id] + migration_path = ( + self.env["odoo.migration.path"] + .browse( + # Job encodes dict key as string => convert it to integer + int(migration_path_id) + ) + .exists() + ) + if not migration_path: + continue + modules_to_scan = self._migration_get_modules_to_scan(migration_path) + if modules_to_scan: + jobs.extend( + self._migration_create_jobs_scan_module( + migration_path, versions_branches, modules_to_scan + ) + ) + if jobs: + chain(*jobs).delay() + return True + + def _migration_create_jobs_scan_module( + self, migration_path, versions_branches, modules_to_scan + ): + jobs = [] + mig_path = ( + migration_path.source_branch_id.name, + migration_path.target_branch_id.name, + ) + for module in modules_to_scan: + delayable = self.delayable( + description=( + f"Collect {module.name} migration data ({' > '.join(mig_path)})" + ), + identity_key=identity_exact, + ) + job = delayable._scan_migration_module( + migration_path.id, versions_branches, module.id + ) + jobs.append(job) + return jobs + + def _scan_migration_module( + self, migration_path_id, versions_branches, module_branch_id + ): + """Scan migration path for `module_branch_id`. + + The migration scan can only occur if: + - target module doesn't exist (and can be migrated) + - source and target modules share the same commits histories (able to + collect migration data) + + Also, a target module could have been renamed while sharing the commits history. + + But a module that has been replaced (different name, different commits + history, but providing the same feature) in next versions cannot be scanned. + Such module will get a migration status "Replaced". + """ + module = self.env["odoo.module.branch"].browse(module_branch_id).exists() + module.ensure_one() + migration_path = ( + self.env["odoo.migration.path"].browse(migration_path_id).exists() + ) + # Skip migration scan if module is replaced in next versions + replaced_by_module = module._replaced_by_module_in_target_version( + migration_path.target_branch_id + ) + if replaced_by_module: + # Ensure a migration record exists before returning + values = { + "module_branch_id": module_branch_id, + "migration_path_id": migration_path_id, + "last_source_scanned_commit": module.last_scanned_commit, + } + self.env["odoo.module.branch.migration"]._create_or_update( + module_branch_id, migration_path, values + ) + return ( + f"{module.name} is now replaced by " + f"{replaced_by_module.name}, no need to collect " + "migration data." + ) + # Check if module has already been migrated on target version but in a + # different repository. If so, tune the scanner parameters to perform + # the scan from current repo to new one. + target_repository = None + mig = module.migration_ids.filtered( + lambda mig: mig.migration_path_id.id == migration_path_id + ) + target_module = mig.target_module_branch_id + if target_module.repository_branch_id: + target_repository = target_module.repository_id + if not target_repository.collect_migration_data: + return ( + "Cannot collect migration data on repository " + f"{target_repository.display_name}." + ) + params = self._prepare_migration_scanner_parameters( + versions_branches, target_repository + ) + module_names = [module.module_id.name] + if target_module: + if module.module_id != target_module.module_id: + module_names = [(module.module_id.name, target_module.module_id.name)] + # Run the migration scan + try: + scanner = MigrationScannerOdooEnv(**params) + return scanner.scan( + addons_path=module.addons_path, + target_addons_path=target_module.addons_path or module.addons_path, + module_names=module_names, + ) + except Exception as exc: + # oca-port requests the GitHub API on its own: when the rate + # limit is reached, postpone the job until the limit is reset + # (at worst 1h for an authenticated user) without increasing + # its retry counter. + if "rate limit" in str(exc).lower(): + raise RetryableJobError( + "GitHub API rate limit reached, waiting for reset", + seconds=3600, + ignore_retry=True, + ) from exc + raise RetryableJobError("Scanner error") from exc + + def _migration_get_modules_to_scan(self, migration_path): + """Return `odoo.module.branch` records that need a migration scan.""" + self.ensure_one() + mb_model = self.env["odoo.module.branch"] + modules = mb_model.search( + [ + ( + "repository_id", + "=", + self.id, + ), + ("branch_id", "=", migration_path.source_branch_id.id), + ("migration_scan", "=", True), + ] + ) + module_ids = [] + for module in modules: + migration = module.migration_ids.filtered( + lambda mig: mig.migration_path_id == migration_path + ) + if migration and not migration.migration_scan: + # Skip module that do not need a scan for the given migration path + continue + module_ids.append(module.id) + return mb_model.browse(module_ids) + + def _prepare_migration_scanner_parameters( + self, migration_path, target_repository=None + ): + ir_config = self.env["ir.config_parameter"] + repositories_path = ir_config.sudo().get_param(self._repositories_path_key) + params = { + "org": self.org_id.name, + "name": self.name, + "clone_url": self.clone_url, + "migration_path": migration_path, + "repositories_path": repositories_path, + "repo_type": self.repo_type, + "ssh_key": self.ssh_key_id.private_key, + "token": self._get_token(), + "workaround_fs_errors": ( + self.env.company.config_odoo_repository_workaround_fs_errors + ), + "clone_name": self.clone_name, + "env": self.env, + } + if target_repository and target_repository != self: + params["new_repo_name"] = target_repository.name + params["new_repo_url"] = target_repository.clone_url + return params + + def _pre_create_or_update_module_branch(self, rec, values, raw_data): + # Handle migration data + values = super()._pre_create_or_update_module_branch(rec, values, raw_data) + mig_model = self.env["odoo.module.branch.migration"] + migrations = raw_data.get("migrations", []) + values["migration_ids"] = [] + for mig in migrations: + source_branch = self.env["odoo.branch"].search( + [("name", "=", mig["source_branch"])] + ) + target_branch = self.env["odoo.branch"].search( + [("name", "=", mig["target_branch"])] + ) + if not source_branch or not target_branch: + # Such branches are not configured on this instance, skip + continue + migration_path = self._get_migration_path( + source_branch.id, target_branch.id + ) + mig_values = { + "migration_path_id": migration_path.id, + "process": mig["process"], + "results": mig["results"], + "last_source_scanned_commit": mig["last_source_scanned_commit"], + "last_target_scanned_commit": mig["last_target_scanned_commit"], + } + # Check if this migration data exists to update it, otherwise create it + mig_rec = None + if rec: + mig_rec = mig_model.search( + [ + ("migration_path_id", "=", migration_path.id), + ("module_branch_id", "=", rec.id), + ], + ) + if mig_rec: + mig_values_ = fields.Command.update(mig_rec.id, mig_values) + else: + mig_values_ = fields.Command.create(mig_values) + values["migration_ids"].append(mig_values_) + return values + + @tools.ormcache("source_branch_id", "target_branch_id") + def _get_migration_path(self, source_branch_id, target_branch_id): + rec = self.env["odoo.migration.path"].search( + [ + ("source_branch_id", "=", source_branch_id), + ("target_branch_id", "=", target_branch_id), + ], + limit=1, + ) + values = { + "source_branch_id": source_branch_id, + "target_branch_id": target_branch_id, + } + if not rec: + rec = self.env["odoo.migration.path"].sudo().create(values) + return rec diff --git a/odoo_repository_migration/pyproject.toml b/odoo_repository_migration/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/odoo_repository_migration/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/odoo_repository_migration/readme/CONTRIBUTORS.md b/odoo_repository_migration/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..17752927 --- /dev/null +++ b/odoo_repository_migration/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Camptocamp + - Sébastien Alix \ diff --git a/odoo_repository_migration/readme/DESCRIPTION.md b/odoo_repository_migration/readme/DESCRIPTION.md new file mode 100644 index 00000000..6bd474cf --- /dev/null +++ b/odoo_repository_migration/readme/DESCRIPTION.md @@ -0,0 +1,27 @@ +This module collects and records modules migration data from Odoo +repositories. + +Thanks to [oca-port](https://github.com/OCA/oca-port/) and data +collected by `odoo_repository`, this module will generate migration data +on each module for declared migration paths (e.g. 16.0 -> 18.0). + +Also, for specific cases you can declare what a module became in a given +Odoo version, like OCA `web_domain_field` replaced by standard Odoo module +`web` starting from 17.0 (with an optional explanation that could help +users, like how to use this new module compared to the previous one). + +Given a migration path, a module can get one of this migration status: + +| Status | Description | +| ------ | ----------- | +| *Fully Ported* | All commits from source version are present in target version | +| *To migrate* | The module doesn't exist on target version | +| *Ported (missing commits?)* | Some commits from source version are not ported in target version (could be false-positive) | +| *To review* | A migration PR has been detected | +| *Replaced* | The module has been replaced by another one (not sharing the same git history) | +| *Moved to standard?* | The module name has been detected in Odoo standard repositories for target version. High chance this module is or should be replaced by another one instead (by creating a timeline), so it mainly helps to detect such cases. | +| *Moved to OCA* | the module name is available in an OCA repository (could be a false-positive because sharing the same name, in such case a timeline has to be created) | +| *Moved to generic repo* | a specific module (only available in a project) in source version now exists in a generic repository (could be a false-positive if both modules have only their name in common) | + +It helps to build a consolidated knowledge database accross different +Odoo versions for everyone: functionals and developers. diff --git a/odoo_repository_migration/readme/USAGE.md b/odoo_repository_migration/readme/USAGE.md new file mode 100644 index 00000000..daae5c2e --- /dev/null +++ b/odoo_repository_migration/readme/USAGE.md @@ -0,0 +1,10 @@ +To enable this feature, the option *Collect migration data* should be +enabled on repositories (opt-in). + +To record what a module became starting from a given Odoo version, you +should do it through the *Odoo Repositories / Data / Modules / +Timelines* menu. + +Once the scheduled action ran, the migration data are available on each +module in *Migration* tab, or through the *Odoo Repositories / Data / +Modules / Migrations* menu. diff --git a/odoo_repository_migration/security/ir.model.access.csv b/odoo_repository_migration/security/ir.model.access.csv new file mode 100644 index 00000000..fd58e05f --- /dev/null +++ b/odoo_repository_migration/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_odoo_migration_path_user,odoo_migration_path_user,model_odoo_migration_path,odoo_repository.group_odoo_repository_user,1,0,0,0 +access_odoo_migration_path_manager,odoo_migration_path_manager,model_odoo_migration_path,odoo_repository.group_odoo_repository_manager,1,0,1,1 +access_odoo_module_branch_migration_user,odoo_module_branch_migration_user,model_odoo_module_branch_migration,odoo_repository.group_odoo_repository_user,1,0,0,0 +access_odoo_module_branch_timeline_user,odoo_module_branch_timeline_user,model_odoo_module_branch_timeline,odoo_repository.group_odoo_repository_user,1,0,0,0 +access_odoo_module_branch_timeline_manager,odoo_module_branch_timeline_manager,model_odoo_module_branch_timeline,odoo_repository.group_odoo_repository_manager,1,1,1,1 diff --git a/odoo_repository_migration/static/description/icon.png b/odoo_repository_migration/static/description/icon.png new file mode 100644 index 00000000..1dcc49c2 Binary files /dev/null and b/odoo_repository_migration/static/description/icon.png differ diff --git a/odoo_repository_migration/static/description/index.html b/odoo_repository_migration/static/description/index.html new file mode 100644 index 00000000..8cc02864 --- /dev/null +++ b/odoo_repository_migration/static/description/index.html @@ -0,0 +1,511 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Odoo Repository Migration Data

+ +

Beta License: AGPL-3 OCA/module-composition-analysis Translate me on Weblate Try me on Runboat

+

This module collects and records modules migration data from Odoo +repositories.

+

Thanks to oca-port and data +collected by odoo_repository, this module will generate migration +data on each module for declared migration paths (e.g. 16.0 -> 18.0).

+

Also, for specific cases you can declare what a module became in a given +Odoo version, like OCA web_domain_field replaced by standard Odoo +module web starting from 17.0 (with an optional explanation that +could help users, like how to use this new module compared to the +previous one).

+

Given a migration path, a module can get one of this migration status:

+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusDescription
Fully PortedAll commits from source version are +present in target version
To migrateThe module doesn’t exist on target +version
Ported (missing commits?)Some commits from source version are +not ported in target version (could be +false-positive)
To reviewA migration PR has been detected
ReplacedThe module has been replaced by +another one (not sharing the same git +history)
Moved to standard?The module name has been detected in +Odoo standard repositories for target +version. High chance this module is or +should be replaced by another one +instead (by creating a timeline), so +it mainly helps to detect such cases.
Moved to OCAthe module name is available in an OCA +repository (could be a false-positive +because sharing the same name, in such +case a timeline has to be created)
Moved to generic repoa specific module (only available in a +project) in source version now exists +in a generic repository (could be a +false-positive if both modules have +only their name in common)
+

It helps to build a consolidated knowledge database accross different +Odoo versions for everyone: functionals and developers.

+

Table of contents

+ +
+

Usage

+

To enable this feature, the option Collect migration data should be +enabled on repositories (opt-in).

+

To record what a module became starting from a given Odoo version, you +should do it through the Odoo Repositories / Data / Modules / +Timelines menu.

+

Once the scheduled action ran, the migration data are available on each +module in Migration tab, or through the Odoo Repositories / Data / +Modules / Migrations menu.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/module-composition-analysis project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/odoo_repository_migration/tests/__init__.py b/odoo_repository_migration/tests/__init__.py new file mode 100644 index 00000000..39bb08d7 --- /dev/null +++ b/odoo_repository_migration/tests/__init__.py @@ -0,0 +1 @@ +from . import test_odoo_module_branch diff --git a/odoo_repository_migration/tests/common.py b/odoo_repository_migration/tests/common.py new file mode 100644 index 00000000..ce18f620 --- /dev/null +++ b/odoo_repository_migration/tests/common.py @@ -0,0 +1,83 @@ +# Copyright 2024 Camptocamp SA +# Copyright 2026 Sébastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.odoo_repository.tests import common + + +class MigrationCommon(common.Common): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.module = cls._create_odoo_module("my_module") + cls.repo_branch = cls._create_odoo_repository_branch( + cls.odoo_repository, cls.branch + ) + cls.repo_branch2 = cls._create_odoo_repository_branch( + cls.odoo_repository, cls.branch2 + ) + cls.repo_branch3 = cls._create_odoo_repository_branch( + cls.odoo_repository, cls.branch3 + ) + cls.module_branch = cls._create_odoo_module_branch( + cls.module, + cls.branch, + specific=False, + repository_branch_id=cls.repo_branch.id, + last_scanned_commit="sha", + ) + cls.std_repository = cls.env.ref("odoo_repository.odoo_repository_odoo_odoo") + oca_org = cls.env.ref("odoo_repository.odoo_repository_org_oca") + cls.oca_repository = cls.env["odoo.repository"].create( + { + "org_id": oca_org.id, + "name": "test-repo", + "repo_url": "https://github.com/OCA/test-repo", + } + ) + cls.gen_repository = cls.env["odoo.repository"].create( + { + "name": "new_repo", + "org_id": cls.odoo_repository.org_id.id, + "repo_url": "http://example.net/new_repo", + "specific": False, + "to_scan": False, + } + ) + cls.gen_repository.addons_path_ids = cls.odoo_repository.addons_path_ids + + @classmethod + def _simulate_migration_scan(cls, target_commit, source, target, report=None): + """Helper method that pushes scanned migration data.""" + module_branch = cls.env["odoo.module.branch"].search( + [ + ("module_id", "=", cls.module.id), + ("branch_id", "=", source.id), + ] + ) + assert module_branch + data = { + "module": module_branch.module_name, + "source_version": source.name, + "source_branch": source.name, + "target_version": target.name, + "target_branch": target.name, + "source_commit": module_branch.last_scanned_commit, + "target_commit": target_commit, + } + if report is not None: + data["report"] = report + return cls.env["odoo.module.branch.migration"].push_scanned_data( + module_branch.id, + data, + ) + + @classmethod + def _create_timeline(cls, module_branch, next_module, state): + return cls.env["odoo.module.branch.timeline"].create( + { + "module_branch_id": module_branch.id, + "state": state, + "next_module_id": next_module.id, + } + ) diff --git a/odoo_repository_migration/tests/test_odoo_module_branch.py b/odoo_repository_migration/tests/test_odoo_module_branch.py new file mode 100644 index 00000000..322105f4 --- /dev/null +++ b/odoo_repository_migration/tests/test_odoo_module_branch.py @@ -0,0 +1,728 @@ +# Copyright 2024 Camptocamp SA +# Copyright 2026 Sébastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from .common import MigrationCommon + + +class TestOdooModuleBranch(MigrationCommon): + def test_next_odoo_version_module_branch_id(self): + """Test next_odoo_version_module_branch_id computed field.""" + # Create next branch module + next_module_branch = self._create_odoo_module_branch( + self.module, + self.branch2, + specific=False, + repository_branch_id=self.repo_branch2.id, + last_scanned_commit="sha", + ) + # Test next_odoo_version_module_branch_id + self.assertEqual( + self.module_branch.next_odoo_version_module_branch_id, next_module_branch + ) + + def test_next_odoo_version_module_branch_id_with_renamed_module(self): + """Test next_odoo_version_module_branch_id with renamed module.""" + # Create a new module name for the renamed module + next_module = self.module.copy({"name": "next_module"}) + # Create next branch module with new name + next_module_branch = self._create_odoo_module_branch( + next_module, + self.branch2, + specific=False, + repository_branch_id=self.repo_branch2.id, + last_scanned_commit="sha", + ) + # Add timeline entry for renaming + self._create_timeline(self.module_branch, next_module, "renamed") + # Test next_odoo_version_module_branch_id follows renaming + self.assertEqual( + self.module_branch.next_odoo_version_module_branch_id, next_module_branch + ) + + def test_migration_scan_removed(self): + self.module_branch.removed = True + self.assertFalse(self.module_branch.migration_scan) + + def test_migration_scan_pr_url(self): + self.module_branch.pr_url = "https://my/pr" + self.assertFalse(self.module_branch.migration_scan) + + def test_migration_scan_repo_collect_migration_data(self): + self.assertFalse(self.module_branch.migration_scan) + self.odoo_repository.collect_migration_data = True + # It's not enough to flag the module as there is no available + # migration path to scan + self.assertFalse(self.module_branch.migration_scan) + + def test_migration_scan_never_scanned(self): + self.module_branch.last_scanned_commit = False + self.assertFalse(self.module_branch.migration_ids) + self.assertFalse(self.module_branch.migration_scan) + self.odoo_repository.collect_migration_data = True + self.assertFalse(self.module_branch.migration_ids) + self.assertTrue(self.module_branch.migration_scan) + + def test_migration_scan_missing_migration_path(self): + self.odoo_repository.collect_migration_data = True + self.assertFalse(self.module_branch.migration_ids) + self.assertFalse(self.module_branch.migration_scan) + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": self.branch2.id, + } + ) + self.assertFalse(self.module_branch.migration_ids) + self.assertTrue(self.module_branch.migration_scan) + # Once we collected migration data for the expected branch+commit + # the module doesn't require a migration scan anymore + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=self.branch2, + report={"process": "migrate", "results": {}}, + ) + self.assertTrue(self.module_branch.migration_ids) + self.assertFalse(self.module_branch.migration_scan) + + def test_migration_scan_target_module_in_review_then_merged(self): + """Test full flow of the migration of a module. + + 1) At first, the module of the source branch needs a migration scan + because the migration data are missing for the target branch. + 2) Once the migration is done (and migration data available), the migration + scan is not needed anymore. + 3) Then the target module could be found in a PR to review, but this + doesn't + """ + self.odoo_repository.collect_migration_data = True + # Simulate a scan of a given migration path while the target module is + # not yet migrated/available in a repository + self.assertFalse(self.module_branch.migration_ids) + self.assertFalse(self.module_branch.migration_scan) + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": self.branch2.id, + } + ) + self.assertFalse(self.module_branch.migration_ids) + self.assertTrue(self.module_branch.migration_scan) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=self.branch2, + report={"process": "migrate", "results": {}}, + ) + self.assertTrue(self.module_branch.migration_ids) + self.assertFalse(self.module_branch.migration_ids.migration_scan) + self.assertFalse(self.module_branch.migration_scan) + self.assertEqual(self.module_branch.migration_ids.state, "migrate") + # Make the module available for targeted branch in review (available in a PR). + # The source module now needs a migration scan as the target module is + # available in a PR, the migration status has to be updated. + target_module_branch = self._create_odoo_module_branch( + self.module, + self.branch2, + specific=False, + repository_branch_id=self.repo_branch2.id, + # Module available in a PR + pr_url="https://my/pr", + ) + self.assertEqual( + self.module_branch.migration_ids.target_module_branch_id, + target_module_branch, + ) + self.assertEqual(self.module_branch.migration_ids.state, "migrate") + self.assertTrue(self.module_branch.migration_ids.migration_scan) + self.assertTrue(self.module_branch.migration_scan) + # Simulate the migration scan. + # The source module doesn't need a migration scan anymore. + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=self.branch2, + report={ + "process": "migrate", + "results": {"existing_pr": {"url": target_module_branch.pr_url}}, + }, + ) + self.assertEqual(self.module_branch.migration_ids.state, "review_migration") + self.assertFalse(self.module_branch.migration_ids.migration_scan) + self.assertFalse(self.module_branch.migration_scan) + # Merge the module in the upstream repository. + # The source module now needs a migration scan (to check if there is + # something to port, or to set the module as fully ported...). + target_module_branch.write( + { + "last_scanned_commit": "target_commit2", + # When 'pr_url' is unset, this means the module has been merged + "pr_url": False, + } + ) + self.assertEqual( + self.module_branch.migration_ids.target_module_branch_id, + target_module_branch, + ) + self.assertTrue(self.module_branch.migration_ids.migration_scan) + self.assertTrue(self.module_branch.migration_scan) + # Simulate the migration scan. + # The source module is fully ported and doesn't need a migration + # scan afterwards. + self._simulate_migration_scan( + "target_commit2", + source=self.branch, + target=self.branch2, + report={"results": {}}, + ) + self.assertEqual(self.module_branch.migration_ids.state, "fully_ported") + self.assertFalse(self.module_branch.migration_ids.migration_scan) + self.assertFalse(self.module_branch.migration_scan) + + def test_migration_scan_target_module_moved_to_standard(self): + """Module moved into a standard repository.""" + # Simulate a scan of a given migration path while the target module is + # not yet migrated/available in a repository + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": self.branch2.id, + } + ) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=self.branch2, + report={"process": "migrate", "results": {}}, + ) + self.assertTrue(self.module_branch.migration_ids) + mig = self.module_branch.migration_ids + self.assertFalse(mig.target_module_branch_id) + self.assertFalse(mig.migration_scan) + self.assertFalse(self.module_branch.migration_scan) + self.assertEqual(mig.state, "migrate") + # Then the module is discovered in a std repository + std_repo_branch = self._create_odoo_repository_branch( + self.std_repository, self.branch2 + ) + target_module_branch = self._create_odoo_module_branch( + self.module, + self.branch2, + specific=False, + is_standard=True, + repository_branch_id=std_repo_branch.id, + ) + self.assertEqual(mig.target_module_branch_id, target_module_branch) + self.assertTrue(mig.moved_to_standard) + self.assertFalse(mig.moved_to_oca) + self.assertFalse(mig.moved_to_generic) + self.assertEqual(mig.state, "moved_to_standard") + self.assertFalse(mig.migration_scan) + + def test_migration_scan_target_module_moved_to_oca(self): + """Module moved into an OCA repository.""" + # Simulate a scan of a given migration path while the target module is + # not yet migrated/available in a repository + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": self.branch2.id, + } + ) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=self.branch2, + report={"process": "migrate", "results": {}}, + ) + self.assertTrue(self.module_branch.migration_ids) + mig = self.module_branch.migration_ids + self.assertFalse(mig.target_module_branch_id) + self.assertFalse(mig.migration_scan) + self.assertFalse(self.module_branch.migration_scan) + self.assertEqual(mig.state, "migrate") + # Then the module is discovered in an OCA repository + oca_repo_branch = self._create_odoo_repository_branch( + self.oca_repository, self.branch2 + ) + target_module_branch = self._create_odoo_module_branch( + self.module, + self.branch2, + specific=False, + repository_branch_id=oca_repo_branch.id, + ) + self.assertEqual(mig.target_module_branch_id, target_module_branch) + self.assertFalse(mig.moved_to_standard) + self.assertTrue(mig.moved_to_oca) + self.assertFalse(mig.moved_to_generic) + self.assertEqual(mig.state, "moved_to_oca") + self.assertFalse(mig.migration_scan) + + def test_migration_scan_target_module_moved_to_generic(self): + """Specific module moved into a generic repository (that is not std or OCA).""" + self.odoo_repository.specific = True + # Simulate a scan of a given migration path while the target module is + # not yet migrated/available in a repository + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": self.branch2.id, + } + ) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=self.branch2, + report={"process": "migrate", "results": {}}, + ) + self.assertTrue(self.module_branch.migration_ids) + mig = self.module_branch.migration_ids + self.assertFalse(mig.target_module_branch_id) + self.assertFalse(mig.migration_scan) + self.assertFalse(self.module_branch.migration_scan) + self.assertEqual(mig.state, "migrate") + # Then the module is discovered in an OCA repository + gen_repo_branch = self._create_odoo_repository_branch( + self.gen_repository, self.branch2 + ) + target_module_branch = self._create_odoo_module_branch( + self.module, + self.branch2, + specific=False, + repository_branch_id=gen_repo_branch.id, + ) + self.assertEqual(mig.target_module_branch_id, target_module_branch) + self.assertFalse(mig.moved_to_standard) + self.assertFalse(mig.moved_to_oca) + self.assertTrue(mig.moved_to_generic) + self.assertEqual(mig.state, "moved_to_generic") + self.assertFalse(mig.migration_scan) + + def test_renamed_module(self): + """Test migration data of a module renamed. + + Data input: + - source version = X + - target version = X+1 + - module renamed in X+1 + + Expected ouput: + - 'renamed_to_module_id' should target module renamed in X+1 + """ + self.odoo_repository.collect_migration_data = True + # Next version is X+1 + next_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 1)] + ) + self.assertEqual(self.branch.next_id, next_branch) + # Create the target module + new_module = self.module.copy({"name": "new_module"}) + target_module_branch = self._create_odoo_module_branch( + new_module, + next_branch, + specific=False, + repository_branch_id=self.repo_branch2.id, + last_scanned_commit="sha", + ) + # Generate migration data records from X to X+1 + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": next_branch.id, + } + ) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=next_branch, + report={"process": "migrate", "results": {}}, + ) + # Module has been renamed starting from X+1 + self._create_timeline(self.module_branch, new_module, "renamed") + renamed_to_module = self.module_branch._renamed_to_module_in_target_version( + next_branch + ) + self.assertEqual(renamed_to_module, new_module) + # We target X+2 to check if intermediate data in X+1 is found + target_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 2)] + ) + renamed_to_module = self.module_branch._renamed_to_module_in_target_version( + target_branch + ) + self.assertEqual(renamed_to_module, new_module) + # Check migration data + mig = self.module_branch.migration_ids + self.assertEqual(mig.renamed_to_module_id, new_module) + self.assertFalse(mig.replaced_by_module_id) + self.assertEqual(mig.target_module_branch_id, target_module_branch) + self.assertFalse(mig.last_target_scanned_commit) + self.assertEqual(mig.state, "fully_ported") + self.assertTrue(mig.migration_scan) + + def test_renamed_module_twice(self): + """Test migration data of a module renamed twice. + + Data input: + - source version = X + - target version = X+2 + - module renamed in X+1 + - module renamed again in X+2 + + Expected ouput: + - 'renamed_to_module_id' should target module renamed in X+2 + """ + self.odoo_repository.collect_migration_data = True + # Next version is X+1 + next_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 1)] + ) + self.assertEqual(self.branch.next_id, next_branch) + # Target version is X+2 + target_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 2)] + ) + self.assertEqual(self.branch.next_id.next_id, target_branch) + # Create the next module + next_module = self.module.copy({"name": "next_module"}) + next_module_branch = self._create_odoo_module_branch( + next_module, + next_branch, + specific=False, + repository_branch_id=self.repo_branch2.id, + last_scanned_commit="sha_next", + ) + # Create the target module + target_module = self.module.copy({"name": "target_module"}) + target_module_branch = self._create_odoo_module_branch( + target_module, + target_branch, + specific=False, + repository_branch_id=self.repo_branch3.id, + last_scanned_commit="sha_target", + ) + # Generate migration data records from X to X+2 (with a gap) + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": target_branch.id, + } + ) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=target_branch, + report={"process": "migrate", "results": {}}, + ) + # Module has been renamed in X+1 + self._create_timeline(self.module_branch, next_module, "renamed") + renamed_to_module = self.module_branch._renamed_to_module_in_target_version( + next_branch + ) + self.assertEqual(renamed_to_module, next_module) + # Module has been renamed again in X+2 + self._create_timeline(next_module_branch, target_module, "renamed") + renamed_to_module = next_module_branch._renamed_to_module_in_target_version( + target_branch + ) + self.assertEqual(renamed_to_module, target_module) + # Check migration data + mig = self.module_branch.migration_ids + self.assertEqual(mig.renamed_to_module_id, target_module) + self.assertFalse(mig.replaced_by_module_id) + self.assertEqual(mig.target_module_branch_id, target_module_branch) + self.assertFalse(mig.last_target_scanned_commit) + self.assertEqual(mig.state, "fully_ported") + self.assertTrue(mig.migration_scan) + + def test_replaced_module(self): + """Test migration data of a module replaced. + + Data input: + - source version = X + - target version = X+1 + - module replaced in X+1 + + Expected ouput: + - 'replaced_by_module_id' should target module replaced in X+1 + """ + self.odoo_repository.collect_migration_data = True + # Next version is X+1 + next_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 1)] + ) + self.assertEqual(self.branch.next_id, next_branch) + # Create the target module + new_module = self.module.copy({"name": "new_module"}) + target_module_branch = self._create_odoo_module_branch( + new_module, + next_branch, + specific=False, + repository_branch_id=self.repo_branch.id, + last_scanned_commit="sha", + ) + # Generate migration data records + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": next_branch.id, + } + ) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=next_branch, + report={"process": "migrate", "results": {}}, + ) + # New module is replacing current one starting from X+1 + self._create_timeline(self.module_branch, new_module, "replaced") + replaced_by_module = self.module_branch._replaced_by_module_in_target_version( + next_branch + ) + self.assertEqual(replaced_by_module, new_module) + # We target X+2 to check if intermediate data in X+1 is found + target_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 2)] + ) + replaced_by_module = self.module_branch._replaced_by_module_in_target_version( + target_branch + ) + self.assertEqual(replaced_by_module, new_module) + # Check migration data + mig = self.module_branch.migration_ids + self.assertEqual(mig.replaced_by_module_id, new_module) + self.assertFalse(mig.renamed_to_module_id) + self.assertEqual(mig.target_module_branch_id, target_module_branch) + self.assertFalse(mig.last_target_scanned_commit) + self.assertEqual(mig.state, "replaced") + # No migration scan needed for replaced modules + self.assertFalse(mig.migration_scan) + + def test_replaced_module_twice(self): + """Test migration data of a module replaced twice. + + Data input: + - source version = X + - target version = X+2 + - module replaced in X+1 + - module replaced again in X+2 + + Expected ouput: + - 'replaced_by_module_id' should target module replaced in X+2 + """ + self.odoo_repository.collect_migration_data = True + # Next version is X+1 + next_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 1)] + ) + self.assertEqual(self.branch.next_id, next_branch) + # Target version is X+2 + target_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 2)] + ) + self.assertEqual(self.branch.next_id.next_id, target_branch) + # Create the next module + next_module = self.module.copy({"name": "next_module"}) + next_module_branch = self._create_odoo_module_branch( + next_module, + next_branch, + specific=False, + repository_branch_id=self.repo_branch2.id, + last_scanned_commit="sha_next", + ) + # Create the target module + target_module = self.module.copy({"name": "target_module"}) + target_module_branch = self._create_odoo_module_branch( + target_module, + target_branch, + specific=False, + repository_branch_id=self.repo_branch3.id, + last_scanned_commit="sha_target", + ) + # Generate migration data records from X to X+2 (with a gap) + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": target_branch.id, + } + ) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=target_branch, + report={"process": "migrate", "results": {}}, + ) + # Module has been replaced in X+1 + self._create_timeline(self.module_branch, next_module, "replaced") + replaced_by_module = self.module_branch._replaced_by_module_in_target_version( + next_branch + ) + self.assertEqual(replaced_by_module, next_module) + # Module has been replaced again in X+2 + self._create_timeline(next_module_branch, target_module, "replaced") + replaced_by_module = next_module_branch._replaced_by_module_in_target_version( + target_branch + ) + self.assertEqual(replaced_by_module, target_module) + # Check migration data + mig = self.module_branch.migration_ids + self.assertEqual(mig.replaced_by_module_id, target_module) + self.assertFalse(mig.renamed_to_module_id) + self.assertEqual(mig.target_module_branch_id, target_module_branch) + self.assertFalse(mig.last_target_scanned_commit) + self.assertEqual(mig.state, "replaced") + # No migration scan needed for replaced modules + self.assertFalse(mig.migration_scan) + + def test_renamed_then_replaced_module(self): + """Test migration data of a module renamed then replaced. + + Data input: + - source version = X + - target version = X+2 + - module renamed in X+1 + - module replaced in X+2 + + Expected ouput: + - 'renamed_to_module_id' should be empty + - 'replaced_by_module_id' should target module replaced in X+2 + """ + self.odoo_repository.collect_migration_data = True + # Next version is X+1 + next_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 1)] + ) + self.assertEqual(self.branch.next_id, next_branch) + # Target version is X+2 + target_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 2)] + ) + self.assertEqual(self.branch.next_id.next_id, target_branch) + # Create the next module + next_module = self.module.copy({"name": "next_module"}) + next_module_branch = self._create_odoo_module_branch( + next_module, + next_branch, + specific=False, + repository_branch_id=self.repo_branch2.id, + last_scanned_commit="sha_next", + ) + # Create the target module + target_module = self.module.copy({"name": "target_module"}) + target_module_branch = self._create_odoo_module_branch( + target_module, + target_branch, + specific=False, + repository_branch_id=self.repo_branch3.id, + last_scanned_commit="sha_target", + ) + # Generate migration data records from X to X+2 (with a gap) + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": target_branch.id, + } + ) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=target_branch, + report={"process": "migrate", "results": {}}, + ) + # Module has been renamed in X+1 + self._create_timeline(self.module_branch, next_module, "renamed") + renamed_to_module = self.module_branch._renamed_to_module_in_target_version( + next_branch + ) + self.assertEqual(renamed_to_module, next_module) + # Module has been replaced in X+2 + self._create_timeline(next_module_branch, target_module, "replaced") + replaced_by_module = next_module_branch._replaced_by_module_in_target_version( + target_branch + ) + self.assertEqual(replaced_by_module, target_module) + # Check migration data + mig = self.module_branch.migration_ids + self.assertFalse(mig.renamed_to_module_id) + self.assertEqual(mig.replaced_by_module_id, target_module) + self.assertEqual(mig.target_module_branch_id, target_module_branch) + self.assertFalse(mig.last_target_scanned_commit) + self.assertEqual(mig.state, "replaced") + self.assertFalse(mig.migration_scan) + + def test_replaced_then_renamed_module(self): + """Test migration data of a module replaced then renamed. + + Data input: + - source version = X + - target version = X+2 + - module replaced in X+1 + - module renamed in X+2 + + Expected ouput: + - 'renamed_to_module_id' should be empty + - 'replaced_by_module_id' should target module renamed in X+2 + """ + self.odoo_repository.collect_migration_data = True + # Next version is X+1 + next_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 1)] + ) + self.assertEqual(self.branch.next_id, next_branch) + # Target version is X+2 + target_branch = self.env["odoo.branch"].search( + [("sequence", "=", self.branch.sequence + 2)] + ) + self.assertEqual(self.branch.next_id.next_id, target_branch) + # Create the next module + next_module = self.module.copy({"name": "next_module"}) + next_module_branch = self._create_odoo_module_branch( + next_module, + next_branch, + specific=False, + repository_branch_id=self.repo_branch2.id, + last_scanned_commit="sha_next", + ) + # Create the target module + target_module = self.module.copy({"name": "target_module"}) + target_module_branch = self._create_odoo_module_branch( + target_module, + target_branch, + specific=False, + repository_branch_id=self.repo_branch3.id, + last_scanned_commit="sha_target", + ) + # Generate migration data records from X to X+2 (with a gap) + self.env["odoo.migration.path"].create( + { + "source_branch_id": self.branch.id, + "target_branch_id": target_branch.id, + } + ) + self._simulate_migration_scan( + "target_commit1", + source=self.branch, + target=target_branch, + report={"process": "migrate", "results": {}}, + ) + # Module has been replaced in X+1 + self._create_timeline(self.module_branch, next_module, "replaced") + replaced_by_module = self.module_branch._replaced_by_module_in_target_version( + next_branch + ) + self.assertEqual(replaced_by_module, next_module) + # Module has been renamed in X+2 + self._create_timeline(next_module_branch, target_module, "renamed") + renamed_to_module = next_module_branch._renamed_to_module_in_target_version( + target_branch + ) + self.assertEqual(renamed_to_module, target_module) + # Check migration data + mig = self.module_branch.migration_ids + self.assertFalse(mig.renamed_to_module_id) + self.assertEqual(mig.replaced_by_module_id, target_module) + self.assertEqual(mig.target_module_branch_id, target_module_branch) + self.assertFalse(mig.last_target_scanned_commit) + self.assertEqual(mig.state, "replaced") + self.assertFalse(mig.migration_scan) diff --git a/odoo_repository_migration/tests/test_odoo_module_branch_timeline.py b/odoo_repository_migration/tests/test_odoo_module_branch_timeline.py new file mode 100644 index 00000000..002e61aa --- /dev/null +++ b/odoo_repository_migration/tests/test_odoo_module_branch_timeline.py @@ -0,0 +1,99 @@ +# Copyright 2026 Akretion (https://www.akretion.com). +# @author Sébastien Alix +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from .common import MigrationCommon + + +class TestOdooModuleBranchTimeline(MigrationCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.branch4_name = str(float(cls.branch3.name) + 1) + cls.branch4 = ( + cls.env["odoo.branch"] + .with_context(active_test=False) + .search([("name", "=", cls.branch4_name)]) + ) + if not cls.branch4: + cls.branch4 = cls.env["odoo.branch"].create( + { + "name": cls.branch4_name, + } + ) + cls.repo_branch4 = cls._create_odoo_repository_branch( + cls.odoo_repository, cls.branch4 + ) + # Create modules A, B, C and D + cls.module_a = cls.module.copy({"name": "module_a"}) + cls.module_branch_a = cls._create_odoo_module_branch( + cls.module_a, + cls.branch, + specific=False, + repository_branch_id=cls.repo_branch.id, + last_scanned_commit="sha_a", + ) + cls.module_b = cls.module.copy({"name": "module_b"}) + cls.module_branch_b = cls._create_odoo_module_branch( + cls.module_b, + cls.branch2, + specific=False, + repository_branch_id=cls.repo_branch2.id, + last_scanned_commit="sha_b", + ) + cls.module_c = cls.module.copy({"name": "module_c"}) + cls.module_branch_c = cls._create_odoo_module_branch( + cls.module_c, + cls.branch3, + specific=False, + repository_branch_id=cls.repo_branch3.id, + last_scanned_commit="sha_c", + ) + cls.module_d = cls.module.copy({"name": "module_d"}) + cls.module_branch_d = cls._create_odoo_module_branch( + cls.module_d, + cls.branch4, + specific=False, + repository_branch_id=cls.repo_branch4.id, + last_scanned_commit="sha_d", + ) + + def test_get_related_timelines(self): + """Test _get_related_timelines returns all timelines in a chain. + + Data input: + - module A renamed/replaced to B in X+1 + - module B renamed/replaced to C in X+2 + - module C renamed/replaced to D in X+3 + + Expected output: + - Calling _get_related_timelines on any timeline should return all timelines + """ + # Create timelines + timeline_b = self._create_timeline( + self.module_branch_a, self.module_b, "renamed" + ) + timeline_c = self._create_timeline( + self.module_branch_b, self.module_c, "replaced" + ) + timeline_d = self._create_timeline( + self.module_branch_c, self.module_d, "renamed" + ) + # Test from first timeline (A -> B) + related_timelines = timeline_b._get_related_timelines() + self.assertIn(timeline_b, related_timelines) + self.assertIn(timeline_c, related_timelines) + self.assertIn(timeline_d, related_timelines) + self.assertEqual(len(related_timelines), 3) + # Test from second timeline (B -> C) + related_timelines = timeline_c._get_related_timelines() + self.assertIn(timeline_b, related_timelines) + self.assertIn(timeline_c, related_timelines) + self.assertIn(timeline_d, related_timelines) + self.assertEqual(len(related_timelines), 3) + # Test from third (and last) timeline (C -> D) + related_timelines = timeline_d._get_related_timelines() + self.assertIn(timeline_b, related_timelines) + self.assertIn(timeline_c, related_timelines) + self.assertIn(timeline_d, related_timelines) + self.assertEqual(len(related_timelines), 3) diff --git a/odoo_repository_migration/utils/__init__.py b/odoo_repository_migration/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/odoo_repository_migration/utils/scanner.py b/odoo_repository_migration/utils/scanner.py new file mode 100644 index 00000000..f377e181 --- /dev/null +++ b/odoo_repository_migration/utils/scanner.py @@ -0,0 +1,100 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.addons.odoo_repository.lib.scanner import MigrationScanner + + +class MigrationScannerOdooEnv(MigrationScanner): + """MigrationScanner running on the same server than Odoo. + + This class takes an additional `env` parameter (`odoo.api.Environment`) + used to request Odoo, and implement required methods to use it. + """ + + def __init__(self, *args, **kwargs): + if kwargs.get("env"): + self.env = kwargs.pop("env") + super().__init__(*args, **kwargs) + + def _get_odoo_repository_id(self) -> int: + return ( + self.env["odoo.repository"] + .search([("name", "=", self.name), ("org_id", "=", self.org)]) + .id + ) + + def _get_odoo_repository_branches(self, repo_id) -> list[str]: + args = [ + ("repository_id", "=", repo_id), + ("branch_id", "in", self.branches), + ] + repo_branches = self.env["odoo.repository.branch"].search(args) + return repo_branches.mapped("branch_id.name") + + def _get_odoo_migration_paths(self, branches: list[str]) -> list[tuple[str]]: + args = [ + ("source_branch_id", "in", branches), + ("target_branch_id", "in", branches), + ] + migration_paths = self.env["odoo.migration.path"].search(args) + return [ + (mp.source_branch_id.name, mp.target_branch_id.name) + for mp in migration_paths + ] + + def _is_module_blacklisted(self, module): + return bool( + self.env["odoo.module"].search_count( + [("name", "=", module), ("blacklisted", "=", True)] + ) + ) + + def _get_odoo_module_branch_id(self, repo_id: int, module: str, branch: str) -> int: + args = [ + ("repository_id", "=", repo_id), + ("module_id", "=", module), + ("branch_id", "=", branch), + ] + return self.env["odoo.module.branch"].search(args).id + + def _get_odoo_module_branch_migration_id( + self, module_branch_id: int, source_branch: str, target_branch: str + ) -> int: + args = [ + ("module_branch_id", "=", module_branch_id), + ("source_branch_id", "=", source_branch), + ("target_branch_id", "=", target_branch), + ] + migration = self.env["odoo.module.branch.migration"].search(args) + if migration: + return migration.id + + def _get_odoo_module_branch_migration_data( + self, repo_id: int, module: str, source_version: str, target_version: str + ) -> dict: + args = [ + ("module_branch_id.repository_id", "=", repo_id), + ("module_id", "=", module), + ("source_branch_id", "=", source_version), + ("target_branch_id", "=", target_version), + ] + migration = self.env["odoo.module.branch.migration"].search(args) + if migration: + data = { + "last_source_scanned_commit": ( + migration.module_branch_id.last_scanned_commit + ), + "last_target_scanned_commit": ( + migration.target_module_branch_id.last_scanned_commit + ), + "last_source_mig_scanned_commit": migration.last_source_scanned_commit, + "last_target_mig_scanned_commit": migration.last_target_scanned_commit, + } + return data + return {} + + def _push_scanned_data(self, module_branch_id: int, data: dict): + res = self.env["odoo.module.branch.migration"].push_scanned_data( + module_branch_id, data + ) + return res diff --git a/odoo_repository_migration/views/odoo_migration_path.xml b/odoo_repository_migration/views/odoo_migration_path.xml new file mode 100644 index 00000000..ad15565b --- /dev/null +++ b/odoo_repository_migration/views/odoo_migration_path.xml @@ -0,0 +1,53 @@ + + + + + odoo.migration.path.form + odoo.migration.path + +
+
+
+ + + + + + +
+
+
+ + + odoo.migration.path.list + odoo.migration.path + + + + + + + + + + Migration Paths + ir.actions.act_window + odoo.migration.path + + + + +
diff --git a/odoo_repository_migration/views/odoo_module_branch.xml b/odoo_repository_migration/views/odoo_module_branch.xml new file mode 100644 index 00000000..71b1df46 --- /dev/null +++ b/odoo_repository_migration/views/odoo_module_branch.xml @@ -0,0 +1,56 @@ + + + + + odoo.module.branch.form.inherit + odoo.module.branch + + +
+ + +
+ + + + + + + + + + + + +
+
+ + + odoo.module.branch.search.inherit + odoo.module.branch + + + + + + + + +
diff --git a/odoo_repository_migration/views/odoo_module_branch_migration.xml b/odoo_repository_migration/views/odoo_module_branch_migration.xml new file mode 100644 index 00000000..124551c1 --- /dev/null +++ b/odoo_repository_migration/views/odoo_module_branch_migration.xml @@ -0,0 +1,152 @@ + + + + + odoo.module.branch.migration.form + odoo.module.branch.migration + +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + odoo.module.branch.migration.list + odoo.module.branch.migration + + + + + + + + + + + + + + + odoo.module.branch.migration.search + odoo.module.branch.migration + search + + + + + + + + + + + + + + + + + + + + + + + + + + + + Migrations + ir.actions.act_window + odoo.module.branch.migration + + {'search_default_group_by_migration_path_id': 1, 'search_default_group_by_state': 2} + + + + diff --git a/odoo_repository_migration/views/odoo_module_branch_timeline.xml b/odoo_repository_migration/views/odoo_module_branch_timeline.xml new file mode 100644 index 00000000..fa8ef8e7 --- /dev/null +++ b/odoo_repository_migration/views/odoo_module_branch_timeline.xml @@ -0,0 +1,155 @@ + + + + + odoo.module.branch.timeline.form + odoo.module.branch.timeline + +
+
+
+ +
+
+ + + + + + + + + + + + + +
+
+
+
+ + + odoo.module.branch.timeline.list + odoo.module.branch.timeline + + + + + + + + + + + + odoo.module.branch.timeline.search + odoo.module.branch.timeline + search + + + + + + + + + + + + + + + + + + + + + + Timelines + ir.actions.act_window + odoo.module.branch.timeline + + {'search_default_group_by_org_id': 1} + + + +
diff --git a/odoo_repository_migration/views/odoo_repository.xml b/odoo_repository_migration/views/odoo_repository.xml new file mode 100644 index 00000000..67418efa --- /dev/null +++ b/odoo_repository_migration/views/odoo_repository.xml @@ -0,0 +1,15 @@ + + + + + odoo.repository.form.inherit + odoo.repository + + + + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..18a2e995 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +oca_port