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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion odoo_project/models/odoo_project.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2023 Camptocamp SA
# Copyright 2026 ACSONE SA/NV (<https://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

import ast
Expand Down Expand Up @@ -91,7 +92,7 @@ def _compute_available_odoo_version_ids(self):
if rec.repository_id:
rec.available_odoo_version_ids = rec.repository_id.branch_ids.branch_id

@api.depends("repository_id", "odoo_version_id")
@api.depends("repository_id.branch_ids.branch_id", "odoo_version_id")
def _compute_repository_branch_id(self):
for rec in self:
rec.repository_branch_id = False
Expand Down Expand Up @@ -166,6 +167,61 @@ def action_find_unknown_modules(self):
for module in self.unknown_module_ids:
module.action_find_pr_url()

def _get_module_branch(self, module, repository=None):
"""Return the `odoo.module.branch` matching `module` for this project.

The module is looked up in `repository` first, defaulting to the
repository of the project. If it doesn't exist it'll be automatically
created as an orphaned module.
"""
self.ensure_one()
if repository is None:
repository = self.repository_id
module_branch = self.env["odoo.module.branch"]._find_or_create(
self.odoo_version_id, module, repository
)
if not module_branch.repository_branch_id and not module_branch.specific:
# If the module hasn't been found in existing repositories content,
# it could be available somewhere on GitHub as a PR that could help
# to identity its repository
module_branch.with_delay().action_find_pr_url()
return module_branch

def _get_project_module(self, module_branch, version=False):
"""Return the `odoo.project.module` of this project for `module_branch`.

If it doesn't exist it'll be automatically created, otherwise its
installed version is updated.
"""
self.ensure_one()
project_module_model = self.env["odoo.project.module"]
domain = [
("module_branch_id", "=", module_branch.id),
("odoo_project_id", "=", self.id),
]
project_module = project_module_model.search(domain)
values = {
"module_branch_id": module_branch.id,
"odoo_project_id": self.id,
"installed_version": version,
}
if project_module:
project_module.sudo().write(values)
else:
# Create the module to make it available for the project
project_module = project_module_model.sudo().create(values)
return project_module

def _import_missing_dependencies(self, project_modules):
"""Complete the project with the dependencies of `project_modules`."""
self.ensure_one()
branch_modules = project_modules.module_branch_id
all_dependencies = branch_modules._get_recursive_dependencies()
missing_dependencies = all_dependencies - branch_modules
for missing_dependency in missing_dependencies:
self._get_project_module(missing_dependency, missing_dependency.version)
return True

def _get_repositories_to_scan(self):
"""Return the repositories to scan."""
domain = self.env["odoo.repository"]._cron_scanner_domain()
Expand Down
56 changes: 52 additions & 4 deletions odoo_project/tests/test_import_modules.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2024 Camptocamp SA
# Copyright 2026 ACSONE SA/NV (<https://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

from .common import ProjectCommon
Expand Down Expand Up @@ -62,7 +63,7 @@ def test_import_modules_names_versions(self):
def test_match_blacklisted_module(self):
mod1 = "test1"
mod2 = "test2"
mod1_blacklisted = self.wiz_import_modules_model._get_module(mod1)
mod1_blacklisted = self.module_branch_model._get_module(mod1)
mod1_blacklisted.blacklisted = True
# Import them through the wizard
modules_list_text = f"{mod1}\n{mod2}"
Expand All @@ -82,7 +83,7 @@ def test_match_blacklisted_module(self):
def test_match_orphaned_module(self):
mod1 = "test1"
mod2 = "test2"
mod1_orphaned = self.wiz_import_modules_model._get_module(mod1)
mod1_orphaned = self.module_branch_model._get_module(mod1)
mod1_branch_orphaned = self.module_branch_model._create_orphaned_module_branch(
self.branch, mod1_orphaned
)
Expand All @@ -106,7 +107,7 @@ def test_match_orphaned_module(self):
def test_match_generic_module(self):
mod1 = "test1"
mod2 = "test2"
mod1_generic = self.wiz_import_modules_model._get_module(mod1)
mod1_generic = self.module_branch_model._get_module(mod1)
repo_branch = self._create_odoo_repository_branch(
self.odoo_repository, self.branch
)
Expand Down Expand Up @@ -138,7 +139,7 @@ def test_match_project_repo_module(self):
self.project.odoo_version_id = self.branch
mod1 = "test1"
mod2 = "test2"
mod1_in_repo = self.wiz_import_modules_model._get_module(mod1)
mod1_in_repo = self.module_branch_model._get_module(mod1)
repo_branch = self._create_odoo_repository_branch(
self.odoo_repository, self.branch
)
Expand All @@ -163,3 +164,50 @@ def test_match_project_repo_module(self):
self.assertIn(mod1_branch_in_repo, existing_mods)
# Project modules are also created
self.assertEqual(len(existing_mods.odoo_project_module_ids), 2)

def test_get_module_branch_in_given_repository(self):
"""A module can be looked up in a repository other than the project one."""
module = self.module_branch_model._get_module("test1")
# The very same module lives in two scanned repositories
other_org = self.env["odoo.repository.org"].create({"name": "other-org"})
other_repository = self.env["odoo.repository"].create(
{
"org_id": other_org.id,
"name": self.odoo_repository.name,
"repo_url": "https://github.com/other-org/repo",
"repo_type": "github",
}
)
module_branches = {}
for repository in (self.odoo_repository, other_repository):
repo_branch = self._create_odoo_repository_branch(repository, self.branch)
module_branches[repository] = self._create_odoo_module_branch(
module,
self.branch,
specific=False,
repository_branch_id=repo_branch.id,
)
for repository, module_branch in module_branches.items():
with self.subTest(repository=repository.display_name):
self.assertEqual(
self.project._get_module_branch(module, repository=repository),
module_branch,
)

def test_repository_branch_id_follows_the_repository_branches(self):
"""The branch of a project is found whenever its repository gets one.

A project is commonly created before its repository has been scanned,
so the matching branch does not exist yet at that point.
"""
self.project.write(
{
"repository_id": self.odoo_repository.id,
"odoo_version_id": self.branch.id,
}
)
self.assertFalse(self.project.repository_branch_id)
repository_branch = self._create_odoo_repository_branch(
self.odoo_repository, self.branch
)
self.assertEqual(self.project.repository_branch_id, repository_branch)
21 changes: 21 additions & 0 deletions odoo_project/views/odoo_project_module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,28 @@
<field name="model">odoo.project.module</field>
<field name="inherit_id" ref="odoo_repository.odoo_module_branch_view_form" />
<field name="mode">primary</field>
<!-- Dropping what other modules add to the button box of the upstream
form is deliberate, hence the priority allowing 'replace'. -->
<field name="priority">99</field>
<field name="arch" type="xml">
<!-- Keep only the buttons this model can honour. It delegates to
'odoo.module.branch' through '_inherits', which carries its
fields but not its methods, so a button any module adds to the
form of the upstream module would break this one. Adding a
button here is therefore up to the module providing its
method. -->
<div name="button_box" position="replace">
<div class="oe_button_box" name="button_box">
<button
name="open_recursive_dependencies"
type="object"
class="oe_stat_button"
icon="fa-sitemap icon"
>
<span>Dependencies</span>
</button>
</div>
</div>
<field name="version" position="before">
<field name="installed_version" />
</field>
Expand Down
81 changes: 12 additions & 69 deletions odoo_project/wizards/odoo_project_import_modules.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2023 Camptocamp SA
# Copyright 2026 ACSONE SA/NV (<https://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

import re
Expand Down Expand Up @@ -49,11 +50,13 @@ def action_import(self):
project_module_ids = self._action_import_modules_list()
project_module_ids.extend(self._action_import_additional_modules())
if self.import_missing_dependencies:
self._action_import_missing_dependencies(project_module_ids)
project_modules = self.env["odoo.project.module"].browse(project_module_ids)
self.odoo_project_id._import_missing_dependencies(project_modules)

def _action_import_modules_list(self):
"""Import a fresh list of installed modules into the project."""
self.odoo_project_id.sudo().project_module_ids = False
project = self.odoo_project_id
project.sudo().project_module_ids = False
module_lines = list(filter(None, self.modules_list.split("\n")))
project_module_ids = []
for line in module_lines:
Expand All @@ -66,81 +69,21 @@ def _action_import_modules_list(self):
else:
module_name, version = data[0], False
# for module_name in module_names:
module = self._get_module(module_name)
module = self.env["odoo.module.branch"]._get_module(module_name)
if module.blacklisted:
continue
module_branch = self._get_module_branch(module)
project_module = self._get_project_module(module_branch, version)
module_branch = project._get_module_branch(module)
project_module = project._get_project_module(module_branch, version)
project_module_ids.append(project_module.id)
self.odoo_project_id.sudo().project_module_ids = project_module_ids
project.sudo().project_module_ids = project_module_ids
return project_module_ids

def _action_import_additional_modules(self):
"""Import additional modules into the project."""
project_module_ids = []
for module_branch in self.additional_module_ids:
project_module = self._get_project_module(module_branch, version=False)
project_module = self.odoo_project_id._get_project_module(
module_branch, version=False
)
project_module_ids.append(project_module.id)
return project_module_ids

def _action_import_missing_dependencies(self, project_module_ids):
"""Complete list of modules by adding all dependencies."""
project_modules = self.env["odoo.project.module"].browse(project_module_ids)
branch_modules = project_modules.module_branch_id
all_dependencies = branch_modules._get_recursive_dependencies()
missing_dependencies = all_dependencies - branch_modules
for missing_dependency in missing_dependencies:
self._get_project_module(missing_dependency, missing_dependency.version)
return True

def _get_module(self, module_name):
"""Return a `odoo.module` record.

If it doesn't exist it'll be automatically created.
"""
module_model = self.env["odoo.module"]
module = module_model.search([("name", "=", module_name)])
if not module:
module = module_model.sudo().create({"name": module_name})
return module

def _get_module_branch(self, module):
"""Return a `odoo.module.branch` record.

If it doesn't exist it'll be automatically created.
"""
module_branch_model = self.env["odoo.module.branch"]
module_branch = False
branch = self.odoo_project_id.odoo_version_id
module_branch = module_branch_model._find_or_create(
branch, module, self.odoo_project_id.repository_id
)
if not module_branch.repository_branch_id and not module_branch.specific:
# If the module hasn't been found in existing repositories content,
# it could be available somewhere on GitHub as a PR that could help
# to identity its repository
module_branch.with_delay().action_find_pr_url()
return module_branch

def _get_project_module(self, module_branch, version):
"""Return a `odoo.project.module` record for the project.

If it doesn't exist it'll be automatically created.
"""
project_module_model = self.env["odoo.project.module"]
domain = [
("module_branch_id", "=", module_branch.id),
("odoo_project_id", "=", self.odoo_project_id.id),
]
project_module = project_module_model.search(domain)
values = {
"module_branch_id": module_branch.id,
"odoo_project_id": self.odoo_project_id.id,
"installed_version": version,
}
if project_module:
project_module.sudo().write(values)
else:
# Create the module to make it available for the project
project_module = project_module_model.sudo().create(values)
return project_module
Loading
Loading