diff --git a/odoo_repository/tests/odoo_repo_mixin.py b/odoo_repository/tests/odoo_repo_mixin.py index 3fad1991..160355c0 100644 --- a/odoo_repository/tests/odoo_repo_mixin.py +++ b/odoo_repository/tests/odoo_repo_mixin.py @@ -2,7 +2,6 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) import io -import os import shutil import tempfile import threading @@ -31,20 +30,10 @@ def setUpClass(cls): cls.addon = "my_module" cls.target_addon = "my_module_renamed" # Create a temporary Git repository - cls._apply_git_config() cls.repo_upstream_path = cls._get_upstream_repository_path() cls.addon_path = Path(cls.repo_upstream_path) / cls.addon cls.manifest_path = cls.addon_path / "__manifest__.py" - @classmethod - def _apply_git_config(cls): - """Configure git (~/.gitconfig) if no config file exists.""" - git_cfg = Path(os.path.expanduser("~/.gitconfig")) - if git_cfg.exists(): - return - os.system("git config --global user.email 'test@example.com'") - os.system("git config --global user.name 'test'") - @classmethod def _get_upstream_repository_path(cls) -> Path: """Returns the path of upstream repository. @@ -95,7 +84,9 @@ def _unarchive_upstream_repository(cls, archive_data: bytes) -> Path: def _create_tmp_git_repository(cls) -> Path: """Create a temporary Git repository to run tests.""" repo_path = tempfile.mkdtemp() - git.Repo.init(repo_path) + repo = git.Repo.init(repo_path) + repo.config_writer().set_value("user", "name", "test").release() + repo.config_writer().set_value("user", "email", "test@example.com").release() return Path(repo_path) @classmethod diff --git a/odoo_repository_migration/README.rst b/odoo_repository_migration/README.rst new file mode 100644 index 00000000..ec3192f0 --- /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:568479ec26f358ee26894cc3b2409f3337827426661abba55f0e3f3b8db19cc3 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.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-16-0/module-composition-analysis-16-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=16.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..f8dd27e1 --- /dev/null +++ b/odoo_repository_migration/__manifest__.py @@ -0,0 +1,30 @@ +# Copyright 2023 Camptocamp SA +# 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": "18.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..51d8784d --- /dev/null +++ b/odoo_repository_migration/i18n/it.po @@ -0,0 +1,484 @@ +# 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: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 +msgid "Active" +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 +msgid "Info" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path____last_update +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration____last_update +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline____last_update +msgid "Last Modified on" +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_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_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_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 +#, python-format +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: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 +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,field_description:odoo_repository_migration.field_odoo_module_branch_migration__module_id +msgid "Technical name" +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..4a8547ad --- /dev/null +++ b/odoo_repository_migration/i18n/odoo_repository_migration.pot @@ -0,0 +1,483 @@ +# 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: \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: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 +msgid "Active" +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 +msgid "Info" +msgstr "" + +#. module: odoo_repository_migration +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_migration_path____last_update +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_migration____last_update +#: model:ir.model.fields,field_description:odoo_repository_migration.field_odoo_module_branch_timeline____last_update +msgid "Last Modified on" +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_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_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_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 +#, python-format +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: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 +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,field_description:odoo_repository_migration.field_odoo_module_branch_migration__module_id +msgid "Technical name" +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..b3db3de6 --- /dev/null +++ b/odoo_repository_migration/models/odoo_migration_path.py @@ -0,0 +1,61 @@ +# 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, + ) + + _sql_constraints = [ + ( + "migration_path_uniq", + "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..683572af --- /dev/null +++ b/odoo_repository_migration/models/odoo_module_branch.py @@ -0,0 +1,210 @@ +# Copyright 2023 Camptocamp SA +# 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() + modules = self._get_next_versions(target_branch) + for module in modules.sorted( + key=lambda mod: mod.branch_id.sequence, reverse=True + ): + if module.timeline_ids.state == "replaced": + return module.timeline_ids.next_module_id + return self.env["odoo.module"] + + 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() + modules = self._get_next_versions(target_branch) + for module in modules.sorted( + key=lambda mod: mod.branch_id.sequence, reverse=True + ): + if module.timeline_ids.state == "renamed": + return module.timeline_ids.next_module_id + return self.env["odoo.module"] + + def _get_next_versions(self, target_branch): + self.ensure_one() + return self.env["odoo.module.branch"].search( + [ + ("module_id", "=", self.module_id.id), + ("branch_id.sequence", ">=", self.branch_id.sequence), + ("branch_id.sequence", "<", target_branch.sequence), + ] + ) + + 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_target_module_id() + 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 recompute the target + # module in migration data in such case. + if "pr_url" in vals: + self._update_migration_target_module_id() + return res + + def _update_migration_target_module_id(self): + """Update `target_module_id` field on relevant module migration records.""" + for rec in self: + migrations = self.env["odoo.module.branch.migration"].search( + [ + ("module_id", "=", rec.module_id.id), + ("target_branch_id", "=", rec.branch_id.id), + ] + ) + # Recompute 'target_module_id' field + migrations._compute_target_module_branch_id() + + 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"] = _("Next versions") + action["domain"] = [("id", "in", self._get_next_module_branches().ids)] + return action 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..24980f0e --- /dev/null +++ b/odoo_repository_migration/models/odoo_module_branch_migration.py @@ -0,0 +1,359 @@ +# Copyright 2023 Camptocamp SA +# 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.", + ) + + _sql_constraints = [ + ( + "module_migration_path_uniq", + "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 + ) + + @api.depends( + "module_branch_id.timeline_ids.state", + "module_branch_id.timeline_ids.next_module_id", + "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 + ) + ) + + @api.depends( + "module_branch_id.timeline_ids.state", + "module_branch_id.timeline_ids.next_module_id", + "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.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 + + @api.model + @api.returns("odoo.module.branch.migration") + 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..b924a991 --- /dev/null +++ b/odoo_repository_migration/models/odoo_module_branch_timeline.py @@ -0,0 +1,102 @@ +# Copyright 2025 Camptocamp SA +# 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, + ) + 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"), + ], + inverse="_inverse_next_fields", + 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", + inverse="_inverse_next_fields", + 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() + + @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}" + ) + + def _inverse_next_fields(self): + # When a module is renamed or replaced, we reset the + # last target scan commits on all impacted migration paths. + # E.g. + # if a module on 17.0 is set as renamed starting from 18.0, + # all migration paths of this module targetting versions >= 18.0 + # should re-trigger a migration scan. + for rec in self: + migrations = ( + self.env["odoo.module.branch.migration"] + .search( + [ + ("module_id", "=", rec.module_branch_id.module_id.id), + ( + "target_branch_id.sequence", + ">=", + rec.odoo_version_id.sequence, + ), + ] + ) + .sudo() + ) + migrations.last_target_scanned_commit = False + migrations._compute_renamed_to_module_id() + migrations._compute_replaced_by_module_id() + migrations._compute_state() diff --git a/odoo_repository_migration/models/odoo_repository.py b/odoo_repository_migration/models/odoo_repository.py new file mode 100644 index 00000000..ef01706d --- /dev/null +++ b/odoo_repository_migration/models/odoo_repository.py @@ -0,0 +1,339 @@ +# 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), + ] + migration_paths = self.env["odoo.migration.path"].search(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 " f"({' > '.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: + 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: + 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..a326915b --- /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/test_odoo_module_branch.py b/odoo_repository_migration/tests/test_odoo_module_branch.py new file mode 100644 index 00000000..b62ca072 --- /dev/null +++ b/odoo_repository_migration/tests/test_odoo_module_branch.py @@ -0,0 +1,411 @@ +# 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 TestOdooModuleBranch(common.Common): + def setUp(self): + super().setUp() + self.module = self._create_odoo_module("my_module") + self.repo_branch = self._create_odoo_repository_branch( + self.odoo_repository, self.branch + ) + self.repo_branch2 = self._create_odoo_repository_branch( + self.odoo_repository, self.branch2 + ) + self.module_branch = self._create_odoo_module_branch( + self.module, + self.branch, + specific=False, + repository_branch_id=self.repo_branch.id, + last_scanned_commit="sha", + ) + self.std_repository = self.env.ref("odoo_repository.odoo_repository_odoo_odoo") + oca_org = self.env.ref("odoo_repository.odoo_repository_org_oca") + self.oca_repository = self.env["odoo.repository"].create( + { + "org_id": oca_org.id, + "name": "test-repo", + "repo_url": "https://github.com/OCA/test-repo", + } + ) + self.gen_repository = self.env["odoo.repository"].create( + { + "name": "new_repo", + "org_id": self.odoo_repository.org_id.id, + "repo_url": "http://example.net/new_repo", + "specific": False, + "to_scan": False, + } + ) + self.gen_repository.addons_path_ids = self.odoo_repository.addons_path_ids + + def _simulate_migration_scan(self, target_commit, report=None): + """Helper method that pushes scanned migration data.""" + data = { + "module": self.module_branch.module_name, + "source_version": self.branch.name, + "source_branch": self.branch.name, + "target_version": self.branch2.name, + "target_branch": self.branch2.name, + "source_commit": self.module_branch.last_scanned_commit, + "target_commit": target_commit, + } + if report is not None: + data["report"] = report + return self.env["odoo.module.branch.migration"].push_scanned_data( + self.module_branch.id, + data, + ) + + 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", 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", 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_branch.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", + 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.module_branch.migration_ids.last_target_scanned_commit = ( + target_module_branch.last_scanned_commit + ) + 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", 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", 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", 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", 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_to_module_in_target_version(self): + self.odoo_repository.collect_migration_data = True + # Next version is 16.0 + 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", report={"process": "migrate", "results": {}} + ) + # Module has been renamed starting from 16.0 + self.module_branch.timeline_ids.create( + { + "module_branch_id": self.module_branch.id, + "state": "renamed", + "next_module_id": new_module.id, + } + ) + renamed_to_module = self.module_branch._renamed_to_module_in_target_version( + next_branch + ) + self.assertEqual(renamed_to_module, new_module) + # We target 17.0 to check if intermediate data in 16.0 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, "migrate") + self.assertTrue(mig.migration_scan) + + def test_replaced_by_module_in_target_version(self): + self.odoo_repository.collect_migration_data = True + # Next version is 16.0 + 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", report={"process": "migrate", "results": {}} + ) + # New module is replacing current one starting from 16.0 + self.module_branch.timeline_ids.create( + { + "module_branch_id": self.module_branch.id, + "state": "replaced", + "next_module_id": new_module.id, + } + ) + replaced_by_module = self.module_branch._replaced_by_module_in_target_version( + next_branch + ) + self.assertEqual(replaced_by_module, new_module) + # We target 17.0 to check if intermediate data in 16.0 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") + self.assertFalse(mig.migration_scan) 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..8a5b311d --- /dev/null +++ b/odoo_repository_migration/views/odoo_module_branch_migration.xml @@ -0,0 +1,153 @@ + + + + + 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..eb20d374 --- /dev/null +++ b/odoo_repository_migration/views/odoo_module_branch_timeline.xml @@ -0,0 +1,104 @@ + + + + + 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 index 1130c184..977fea36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ # generated from manifests external_dependencies gitpython +oca-port odoo-addons-parser pyyaml