diff --git a/vcp_github/demo/demo_vcp_platform.xml b/vcp_github/demo/demo_vcp_platform.xml index a039cd5..984dc6c 100644 --- a/vcp_github/demo/demo_vcp_platform.xml +++ b/vcp_github/demo/demo_vcp_platform.xml @@ -4,5 +4,6 @@ OCA ^\d+\.\d+$ + diff --git a/vcp_github/models/vcp_repository.py b/vcp_github/models/vcp_repository.py index a44dc09..016e5e9 100644 --- a/vcp_github/models/vcp_repository.py +++ b/vcp_github/models/vcp_repository.py @@ -56,6 +56,7 @@ def _update_branches_github(self): raise ValidationError(self.env._(f"Reset on {reset}")) from e def _parse_github_pr(self, pr, client): + self.ensure_one() origin_data = pr.as_dict() comments_url = pr.comments_url comments_req = client.session.get(comments_url) @@ -71,12 +72,22 @@ def _parse_github_pr(self, pr, client): reviews_url = reviews_req.links["next"]["url"] reviews_req = client.session.get(reviews_url) reviews += reviews_req.json() + + branch_pattern = ( + self.fetch_branch_pattern + or self.platform_id.fetch_repository_branch_pattern + or False + ) + if branch_pattern and not re.match(branch_pattern, pr.base.ref): + branch_id = False + else: + branch_id = self.platform_id._get_branch(pr.base.ref) return ( str(pr.id), { "user_id": self.platform_id.host_id._get_user(pr.user.login), "repository_id": self.id, - "branch_id": self.platform_id._get_branch(pr.base.ref), + "branch_id": branch_id, "organization_id": self.platform_id.host_id._get_organization( pr.head.repo[0] ), @@ -85,6 +96,7 @@ def _parse_github_pr(self, pr, client): "name": pr.title, "is_merged": any(label["name"] == "merged 🎉" for label in pr.labels) or pr.is_merged(), + "is_draft": pr.draft, "created_at": self.platform_id._parse_github_date( origin_data["created_at"] ), diff --git a/vcp_github/tests/test_github.py b/vcp_github/tests/test_github.py index f1d3df4..e598de7 100644 --- a/vcp_github/tests/test_github.py +++ b/vcp_github/tests/test_github.py @@ -19,8 +19,8 @@ def setUpClass(cls): "key_ids": [ Command.create({"name": "ghp_exampletoken1234567890abcdef"}) ], - "default_update_repository_information": True, - "information_update": True, + "default_repository_scheduled_information_update": True, + "scheduled_information_update": True, } ) diff --git a/vcp_management/__manifest__.py b/vcp_management/__manifest__.py index ea5f64d..666ca84 100644 --- a/vcp_management/__manifest__.py +++ b/vcp_management/__manifest__.py @@ -17,7 +17,9 @@ "views/vcp_comment.xml", "views/vcp_review.xml", "views/vcp_request.xml", + "views/vcp_request_label.xml", "views/vcp_repository.xml", + "views/vcp_repository_branch.xml", "views/vcp_branch.xml", "views/vcp_platform.xml", "views/vcp_organization.xml", diff --git a/vcp_management/data/ir_cron.xml b/vcp_management/data/ir_cron.xml index bb3ba1c..7746dda 100644 --- a/vcp_management/data/ir_cron.xml +++ b/vcp_management/data/ir_cron.xml @@ -6,7 +6,7 @@ VCP: Repository Update code - model._cron_update_repositories() + model._cron_update_repositories(limit=1) 1 minutes False @@ -24,7 +24,7 @@ VCP: Branch Update code - model._cron_update_branches() + model._cron_update_branches(limit=1) 1 days False @@ -33,7 +33,7 @@ VCP: Branch Rule Process code - model._cron_process_branch_rules() + model._cron_process_branch_rules(limit=10) 1 days False diff --git a/vcp_management/models/__init__.py b/vcp_management/models/__init__.py index be3a675..6d1bd25 100644 --- a/vcp_management/models/__init__.py +++ b/vcp_management/models/__init__.py @@ -7,6 +7,7 @@ from . import vcp_repository from . import vcp_repository_branch from . import vcp_request +from . import vcp_request_label from . import vcp_review from . import vcp_comment from . import res_partner diff --git a/vcp_management/models/vcp_comment.py b/vcp_management/models/vcp_comment.py index 02970b8..938e701 100644 --- a/vcp_management/models/vcp_comment.py +++ b/vcp_management/models/vcp_comment.py @@ -38,6 +38,8 @@ class VcpComment(models.Model): comodel_name="vcp.request", string="Request", readonly=True, + required=True, + ondelete="cascade", ) _sql_constraints = [ ("external_id_uniq", "unique(external_id)", "External ID must be unique.") diff --git a/vcp_management/models/vcp_platform.py b/vcp_management/models/vcp_platform.py index 77c6e6d..23b025c 100644 --- a/vcp_management/models/vcp_platform.py +++ b/vcp_management/models/vcp_platform.py @@ -48,8 +48,19 @@ class VcpPlatform(models.Model): inverse_name="platform_id", ) repository_count = fields.Integer(compute="_compute_repository_count", store=True) - default_update_repository_information = fields.Boolean() - information_update = fields.Boolean() + default_repository_scheduled_information_update = fields.Boolean( + help="If checked, the cron that update repositories" + " will look for up to date information, for this repository.", + ) + default_repository_scheduled_branch_update = fields.Boolean( + help="If checked, the cron that update repository branches" + " will look for up to date branches, for this repository.", + ) + scheduled_information_update = fields.Boolean( + default=True, + help="If checked, the cron that update platform informations" + " will look for up to date information, for this platform.", + ) fetch_repository_fork = fields.Boolean( help="If checked, all repositories will be fetched (sources and forks)." " Otherwise, only sources repositories will be fetched" @@ -98,7 +109,7 @@ def _get_git_url(self, repository): return getattr(self, f"_get_git_url_{self.kind}")(repository) def _cron_update_platforms(self): - for platform in self.search([("information_update", "=", True)]): + for platform in self.search([("scheduled_information_update", "=", True)]): try: platform.update_information() except Exception as e: diff --git a/vcp_management/models/vcp_repository.py b/vcp_management/models/vcp_repository.py index 665ef56..5debe42 100644 --- a/vcp_management/models/vcp_repository.py +++ b/vcp_management/models/vcp_repository.py @@ -36,12 +36,21 @@ class VcpRepository(models.Model): request_count = fields.Integer(compute="_compute_request_count") test_field = fields.Char() # TODO remove after testing active = fields.Boolean(default=True, readonly=True) - information_update = fields.Boolean( - compute="_compute_information_update", + scheduled_information_update = fields.Boolean( + compute="_compute_scheduled_information_update", store=True, readonly=False, + help="If checked, the cron that update repository informations" + " will look for up to date information, for this repository." + " This update include the recovery of requests, comments and reviews.", + ) + scheduled_branch_update = fields.Boolean( + compute="_compute_scheduled_branch_update", + store=True, + readonly=False, + help="If checked, the cron that update repository branches" + " will look for up to date branches, for this repository.", ) - branch_update = fields.Boolean(default=False) branch_update_date = fields.Datetime( readonly=True, required=True, default=fields.Datetime.now ) @@ -73,10 +82,17 @@ def _get_git_url(self): return self.platform_id._get_git_url(self) @api.depends("platform_id") - def _compute_information_update(self): + def _compute_scheduled_information_update(self): + for record in self: + record.scheduled_information_update = ( + record.platform_id.default_repository_scheduled_information_update + ) + + @api.depends("platform_id") + def _compute_scheduled_branch_update(self): for record in self: - record.information_update = ( - record.platform_id.default_update_repository_information + record.scheduled_branch_update = ( + record.platform_id.default_repository_scheduled_branch_update ) @api.depends("request_ids") @@ -104,16 +120,20 @@ def update_information(self, update_interval_days=None): update_interval_days=update_interval_days ) - def _cron_update_repositories(self, limit=1): + def _cron_update_repositories(self, limit): repositories = self.search( - [("information_update", "=", True)], limit=limit, order="from_date ASC" + [("scheduled_information_update", "=", True)], + limit=limit, + order="from_date ASC", ) for repository in repositories: repository.update_information() - def _cron_update_branches(self, limit=1): + def _cron_update_branches(self, limit): repositories = self.search( - [("branch_update", "=", True)], limit=limit, order="branch_update_date ASC" + [("scheduled_branch_update", "=", True)], + limit=limit, + order="branch_update_date ASC", ) for repository in repositories: repository.update_branches() diff --git a/vcp_management/models/vcp_repository_branch.py b/vcp_management/models/vcp_repository_branch.py index 88f5407..df80536 100644 --- a/vcp_management/models/vcp_repository_branch.py +++ b/vcp_management/models/vcp_repository_branch.py @@ -18,11 +18,13 @@ class VcpRepositoryBranch(models.Model): "vcp.branch", string="Branch", required=True, + readonly=True, ondelete="cascade", ) repository_id = fields.Many2one( "vcp.repository", required=True, + readonly=True, ondelete="cascade", ) platform_id = fields.Many2one( @@ -40,7 +42,7 @@ class VcpRepositoryBranch(models.Model): required=True, ) - def _cron_process_branch_rules(self, limit=10): + def _cron_process_branch_rules(self, limit): branches = self.search([], limit=limit, order="update_rule_processing_date asc") for branch in branches: branch.process_rules() diff --git a/vcp_management/models/vcp_request.py b/vcp_management/models/vcp_request.py index 6f52b75..c75c473 100644 --- a/vcp_management/models/vcp_request.py +++ b/vcp_management/models/vcp_request.py @@ -1,7 +1,14 @@ # Copyright 2026 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models, tools +from odoo import api, fields, models + +_STATUS_SELECTION = [ + ("draft", "Draft"), + ("open", "Open"), + ("merged", "Merged"), + ("closed", "Closed"), +] class VcpRequest(models.Model): @@ -41,9 +48,27 @@ class VcpRequest(models.Model): related="organization_id.partner_id", string="Organization Partner", ) + review_ids = fields.One2many( + comodel_name="vcp.review", + string="Reviews", + readonly=True, + inverse_name="request_id", + ) + review_count = fields.Integer(compute="_compute_review_count", store=True) + comment_ids = fields.One2many( + comodel_name="vcp.comment", + string="Comments", + readonly=True, + inverse_name="request_id", + ) + comment_count = fields.Integer(compute="_compute_comment_count", store=True) url = fields.Char(readonly=True) state = fields.Char(readonly=True) + status = fields.Selection( + selection=_STATUS_SELECTION, compute="_compute_status", store=True + ) is_merged = fields.Boolean(readonly=True) + is_draft = fields.Boolean(readonly=True) created_at = fields.Datetime(readonly=True) updated_at = fields.Datetime(readonly=True) closed_at = fields.Datetime(readonly=True) @@ -63,18 +88,24 @@ class VcpRequest(models.Model): ("external_id_uniq", "unique(external_id)", "External ID must be unique.") ] + @api.depends("review_ids") + def _compute_review_count(self): + for record in self: + record.review_count = len(record.review_ids) -class VcpRequestLabel(models.Model): - _name = "vcp.request.label" - _description = "Vcp Request Label" - - name = fields.Char(required=True) - - _sql_constraints = [("name_uniq", "unique(name)", "Label name must be unique.")] + @api.depends("comment_ids") + def _compute_comment_count(self): + for record in self: + record.comment_count = len(record.comment_ids) - @tools.ormcache("name") - def _get_label(self, name): - label = self.search([("name", "=", name)], limit=1) - if not label: - label = self.sudo().create({"name": name}) - return label.id + @api.depends("is_draft", "is_merged", "state") + def _compute_status(self): + for record in self: + if record.is_merged: + record.status = "merged" + elif record.closed_at: + record.status = "closed" + elif record.is_draft: + record.status = "draft" + else: + record.status = "open" diff --git a/vcp_management/models/vcp_request_label.py b/vcp_management/models/vcp_request_label.py new file mode 100644 index 0000000..5058c16 --- /dev/null +++ b/vcp_management/models/vcp_request_label.py @@ -0,0 +1,44 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from random import randint + +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError + + +class VcpRequestLabel(models.Model): + _name = "vcp.request.label" + _description = "Vcp Request Label" + + name = fields.Char(required=True, readonly=True) + + color = fields.Char(default=lambda x: x._default_color()) + + request_ids = fields.Many2many( + comodel_name="vcp.request", + string="Requests", + readonly=True, + ) + + _sql_constraints = [("name_uniq", "unique(name)", "Label name must be unique.")] + + def _default_color(self): + return randint(1, 11) + + @tools.ormcache("name") + def _get_label(self, name): + label = self.search([("name", "=", name)], limit=1) + if not label: + label = self.sudo().create({"name": name}) + return label.id + + @api.ondelete(at_uninstall=False) + def _check_requests(self): + if self.mapped("request_ids"): + raise UserError( + _( + "You can not delete labels that are related to Requests. " + "You should first delete the related requests." + ) + ) diff --git a/vcp_management/models/vcp_review.py b/vcp_management/models/vcp_review.py index 4e3f6db..86371dd 100644 --- a/vcp_management/models/vcp_review.py +++ b/vcp_management/models/vcp_review.py @@ -29,6 +29,8 @@ class VcpReview(models.Model): request_id = fields.Many2one( "vcp.request", readonly=True, + required=True, + ondelete="cascade", ) organization_id = fields.Many2one( related="request_id.organization_id", diff --git a/vcp_management/security/ir.model.access.csv b/vcp_management/security/ir.model.access.csv index b6a5de5..b2b35bc 100644 --- a/vcp_management/security/ir.model.access.csv +++ b/vcp_management/security/ir.model.access.csv @@ -11,9 +11,13 @@ manage_repository_branch,Access Repository Branch,model_vcp_repository_branch,gr access_rule_information,Access Repository Branch Rule information,model_vcp_rule_information,group_vcp_user,1,0,0,0 manage_rule_information,Access Repository Branch Rule information,model_vcp_rule_information,group_vcp_manager,1,1,1,1 access_request,Access Pull Requests,model_vcp_request,group_vcp_user,1,0,0,0 +manage_request,Access Pull Requests,model_vcp_request,group_vcp_manager,1,1,1,1 access_request_label,Access Pull Requests Labels,model_vcp_request_label,group_vcp_user,1,0,0,0 +manage_request_label,Access Pull Requests Labels,model_vcp_request_label,group_vcp_manager,1,1,1,1 access_review,Access Reviews,model_vcp_review,group_vcp_user,1,0,0,0 +manage_review,Access Reviews,model_vcp_review,group_vcp_manager,1,1,1,1 access_comment,Access Comments,model_vcp_comment,group_vcp_user,1,0,0,0 +manage_comment,Access Comments,model_vcp_comment,group_vcp_manager,1,1,1,1 access_vcp_host_type,Access Host Type,model_vcp_host_type,group_vcp_user,1,0,0,0 access_vcp_host,Access Hosts,model_vcp_host,group_vcp_user,1,0,0,0 manage_vcp_host,Nabage Hosts,model_vcp_host,group_vcp_manager,1,1,1,1 diff --git a/vcp_management/tests/test_vcp_crons.py b/vcp_management/tests/test_vcp_crons.py index a1d0583..f24a514 100644 --- a/vcp_management/tests/test_vcp_crons.py +++ b/vcp_management/tests/test_vcp_crons.py @@ -32,9 +32,11 @@ def setUpClass(cls): } ) # disable updates to avoid unwanted side effects during tests - cls.env["vcp.platform"].search([]).write({"information_update": False}) + cls.env["vcp.platform"].search([]).write( + {"scheduled_information_update": False} + ) cls.env["vcp.repository"].search([]).write( - {"information_update": False, "branch_update": False} + {"scheduled_information_update": False, "scheduled_branch_update": False} ) # be sure some expected values are set otherwise homepage may fail cls.platform = cls.env["vcp.platform"].create( @@ -43,7 +45,7 @@ def setUpClass(cls): "short_description": "OCA", "description": "OCA", "host_id": cls.host.id, - "information_update": True, + "scheduled_information_update": True, } ) @@ -53,7 +55,7 @@ def test_repository_update_no_definition(self): "name": "test_repo", "description": "Test Repository", "platform_id": self.platform.id, - "information_update": True, + "scheduled_information_update": True, "from_date": Date.today(), } ) @@ -69,7 +71,7 @@ def test_platform_branch_update_no_definition(self): "name": "test_repo", "description": "Test Repository", "platform_id": self.platform.id, - "branch_update": True, + "scheduled_branch_update": True, "from_date": Date.today(), } ) @@ -77,7 +79,7 @@ def test_platform_branch_update_no_definition(self): self.assertRaises(AttributeError), mute_logger("odoo.addons.vcp_management.models.vcp_platform"), ): - self.env["vcp.repository"]._cron_update_branches() + self.env["vcp.repository"]._cron_update_branches(limit=1) def test_platform_update_no_definition(self): with ( @@ -95,8 +97,8 @@ def dummy_update_information(oself, *args, **kwargs): "name": "test_repo", "description": "Test Repository", "platform_id": oself.id, - "information_update": True, - "branch_update": True, + "scheduled_information_update": True, + "scheduled_branch_update": True, "from_date": Date.today(), } ) @@ -140,7 +142,7 @@ def dummy_repository_update_information(oself, *args, **kwargs): dummy_repository_update_information, create=True, ): - self.env["vcp.repository"]._cron_update_repositories() + self.env["vcp.repository"]._cron_update_repositories(limit=1) repository.invalidate_recordset() self.assertTrue(repository.request_ids) self.assertFalse(repository.branch_ids) @@ -160,7 +162,7 @@ def dummy_repository_update_branches(oself, *args, **kwargs): dummy_repository_update_branches, create=True, ): - self.env["vcp.repository"]._cron_update_branches() + self.env["vcp.repository"]._cron_update_branches(limit=1) self.assertTrue(repository.branch_ids) self.assertEqual(repository.branch_ids.branch_id.name, "main") diff --git a/vcp_management/views/menu.xml b/vcp_management/views/menu.xml index 91d7b02..19f03c4 100644 --- a/vcp_management/views/menu.xml +++ b/vcp_management/views/menu.xml @@ -36,6 +36,12 @@ action="vcp_branch_act_window" sequence="10" /> + + diff --git a/vcp_management/views/vcp_host.xml b/vcp_management/views/vcp_host.xml index a231261..df18045 100644 --- a/vcp_management/views/vcp_host.xml +++ b/vcp_management/views/vcp_host.xml @@ -41,6 +41,7 @@ Platform Types + vcp-host vcp.host list,form [] diff --git a/vcp_management/views/vcp_organization.xml b/vcp_management/views/vcp_organization.xml index 142a44a..8c09fd8 100644 --- a/vcp_management/views/vcp_organization.xml +++ b/vcp_management/views/vcp_organization.xml @@ -39,6 +39,7 @@ Organizations + vcp-organization vcp.organization list,form [] diff --git a/vcp_management/views/vcp_platform.xml b/vcp_management/views/vcp_platform.xml index 4798e72..328c3de 100644 --- a/vcp_management/views/vcp_platform.xml +++ b/vcp_management/views/vcp_platform.xml @@ -2,6 +2,14 @@ + + Repositories + vcp.repository + vcp-platform-to-repository + list,form + [("platform_id", "=", active_id)] + + vcp.platform @@ -44,8 +52,18 @@ - - + + + @@ -85,13 +103,18 @@ + Platforms - vcp-platforms + vcp-platform vcp.platform list,form [] diff --git a/vcp_management/views/vcp_repository.xml b/vcp_management/views/vcp_repository.xml index 75ba13f..5230343 100644 --- a/vcp_management/views/vcp_repository.xml +++ b/vcp_management/views/vcp_repository.xml @@ -2,6 +2,15 @@ + + Requests + vcp-repository-to-request + vcp.request + list,form + [("repository_id", "=", active_id)] + {'search_default_filter_active': 1} + + vcp.repository @@ -49,7 +58,7 @@ class="oe_stat_button" icon="fa-code-fork fa-flip-vertical" type="action" - name="%(vcp_request_repository_act_window)d" + name="%(vcp_repository_2_request_act_window)d" > - - + + @@ -129,46 +141,23 @@ + + - - vcp.repository.branch - -
- - - - - - - - - - - - - - - -
-
-
- - - Repositories - vcp.repository - list,form - [("platform_id", "=", active_id)] - - Repositories - vcp-repositories + vcp-repository vcp.repository list,form [] diff --git a/vcp_management/views/vcp_repository_branch.xml b/vcp_management/views/vcp_repository_branch.xml new file mode 100644 index 0000000..3bbd321 --- /dev/null +++ b/vcp_management/views/vcp_repository_branch.xml @@ -0,0 +1,58 @@ + + + + + vcp.repository.branch + +
+ + + + + + + + + + + + + + + +
+
+
+ + + vcp.repository.branch + + + + + + + + + + + vcp.repository.branch + + + + + + + + + + + Repository Branches + vcp-repository-branch + vcp.repository.branch + list,form + [] + {} + +
diff --git a/vcp_management/views/vcp_request.xml b/vcp_management/views/vcp_request.xml index d2a203a..1f166b2 100644 --- a/vcp_management/views/vcp_request.xml +++ b/vcp_management/views/vcp_request.xml @@ -2,28 +2,86 @@ + + Reviews + vcp.review + list,form + [("request_id", "=", active_id)] + + + + Comments + vcp.comment + list,form + [("request_id", "=", active_id)] + + vcp.request
+
+ + +
+ + + + @@ -32,6 +90,7 @@ +
@@ -47,6 +106,11 @@ + @@ -54,32 +118,39 @@ vcp.request - + - + + + + + + Requests - vcp-requests + vcp-request vcp.request list,form [] - {} - - - - Requests - vcp-platform-requests - vcp.request - list,form - [("repository_id", "=", active_id)] - {} + {'search_default_filter_active': 1} diff --git a/vcp_management/views/vcp_request_label.xml b/vcp_management/views/vcp_request_label.xml new file mode 100644 index 0000000..2ba3b29 --- /dev/null +++ b/vcp_management/views/vcp_request_label.xml @@ -0,0 +1,47 @@ + + + + + vcp.request.label + +
+
+ + + + + + + + + + + + vcp.request.label + + + + + + + + + vcp.request.label + + + + + + + + + + Request Labels + vcp.request.label + vcp-request-label + list,form + [] + {} + + diff --git a/vcp_management/views/vcp_rule.xml b/vcp_management/views/vcp_rule.xml index e621272..13cbf2e 100644 --- a/vcp_management/views/vcp_rule.xml +++ b/vcp_management/views/vcp_rule.xml @@ -44,6 +44,7 @@ Processing Rules + vcp-rule vcp.rule list,form [] diff --git a/vcp_management/views/vcp_user.xml b/vcp_management/views/vcp_user.xml index 18e17f9..8d328ad 100644 --- a/vcp_management/views/vcp_user.xml +++ b/vcp_management/views/vcp_user.xml @@ -42,6 +42,7 @@ Users + vcp-user vcp.user list,form [] diff --git a/vcp_odoo/__manifest__.py b/vcp_odoo/__manifest__.py index 91db677..db9c82d 100644 --- a/vcp_odoo/__manifest__.py +++ b/vcp_odoo/__manifest__.py @@ -11,9 +11,12 @@ "depends": ["vcp_management"], "data": [ "security/ir.model.access.csv", - "views/vcp_odoo_module_version.xml", "views/vcp_odoo_module.xml", + "views/vcp_odoo_module_version.xml", + "views/vcp_odoo_bin_package.xml", + "views/vcp_odoo_python_library.xml", "views/vcp_rule.xml", + "views/menu.xml", "data/vcp_rule.xml", ], "demo": [], diff --git a/vcp_odoo/models/__init__.py b/vcp_odoo/models/__init__.py index be6d37f..dd294a2 100644 --- a/vcp_odoo/models/__init__.py +++ b/vcp_odoo/models/__init__.py @@ -2,4 +2,4 @@ from . import vcp_odoo_module from . import vcp_odoo_module_version from . import vcp_odoo_bin_package -from . import vcp_odoo_lib_python +from . import vcp_odoo_python_library diff --git a/vcp_odoo/models/vcp_odoo_bin_package.py b/vcp_odoo/models/vcp_odoo_bin_package.py index 2625e28..41d7fdf 100644 --- a/vcp_odoo/models/vcp_odoo_bin_package.py +++ b/vcp_odoo/models/vcp_odoo_bin_package.py @@ -1,18 +1,35 @@ # Copyright 2026 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields, models, tools +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError class VcpOdooBinPackage(models.Model): _name = "vcp.odoo.bin.package" _description = "Binary Package required by an Odoo Module" - name = fields.Char(required=True) + name = fields.Char(required=True, readonly=True) + + module_version_ids = fields.Many2many( + "vcp.odoo.module.version", + string="Odoo Module Versions", + readonly=True, + ) @tools.ormcache("name") - def _get_bin(self, name): + def _get_bin_package(self, name): bin_src = self.search([("name", "=", name)], limit=1) if not bin_src: bin_src = self.create({"name": name}) return bin_src.id + + @api.ondelete(at_uninstall=False) + def _check_module_versions(self): + if self.mapped("module_version_ids"): + raise UserError( + _( + "You can not delete packages that are related to Odoo Modules. " + "You should first delete the related odoo modules." + ) + ) diff --git a/vcp_odoo/models/vcp_odoo_lib_python.py b/vcp_odoo/models/vcp_odoo_lib_python.py deleted file mode 100644 index 2759f3c..0000000 --- a/vcp_odoo/models/vcp_odoo_lib_python.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2026 Dixmit -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import fields, models, tools - - -class VcpOdooLibPython(models.Model): - _name = "vcp.odoo.lib.python" - _description = "Python Library required by an Odoo Module" - - name = fields.Char(required=True) - - @tools.ormcache("name") - def _get_lib_python(self, name): - lib = self.search([("name", "=", name)], limit=1) - if not lib: - lib = self.create({"name": name}) - return lib.id diff --git a/vcp_odoo/models/vcp_odoo_module.py b/vcp_odoo/models/vcp_odoo_module.py index 479107e..7f6ee71 100644 --- a/vcp_odoo/models/vcp_odoo_module.py +++ b/vcp_odoo/models/vcp_odoo_module.py @@ -8,8 +8,10 @@ class VcpOdooModule(models.Model): _name = "vcp.odoo.module" _description = "Odoo Module" - name = fields.Char(required=True) - version_ids = fields.One2many("vcp.odoo.module.version", inverse_name="module_id") + name = fields.Char(required=True, readonly=True) + version_ids = fields.One2many( + "vcp.odoo.module.version", inverse_name="module_id", readonly=True + ) version_count = fields.Integer( compute="_compute_version_count", help="number of versions in which the module is available", diff --git a/vcp_odoo/models/vcp_odoo_module_version.py b/vcp_odoo/models/vcp_odoo_module_version.py index 363f860..eb1b10f 100644 --- a/vcp_odoo/models/vcp_odoo_module_version.py +++ b/vcp_odoo/models/vcp_odoo_module_version.py @@ -9,34 +9,43 @@ class VcpOdooModuleVersion(models.Model): _description = "Odoo Module on an specific repository branch" _inherit = ["vcp.rule.information.mixin", "image.mixin"] - name = fields.Char(required=True) - path = fields.Char(required=True) + name = fields.Char(required=True, readonly=True) + path = fields.Char(required=True, readonly=True) module_id = fields.Many2one( "vcp.odoo.module", + readonly=True, required=True, ondelete="cascade", ) - version = fields.Char(required=True) + version = fields.Char( + required=True, + readonly=True, + ) repository_branch_id = fields.Many2one( "vcp.repository.branch", + readonly=True, required=True, ondelete="cascade", ) depends_on_module_ids = fields.Many2many( "vcp.odoo.module", + readonly=True, ) - auto_install = fields.Boolean() + auto_install = fields.Boolean(readonly=True) license = fields.Char(string="License (Manifest)", readonly=True) summary = fields.Char(string="Summary (Manifest)", readonly=True) website = fields.Char(string="Website (Manifest)", readonly=True) - lib_python_ids = fields.Many2many( - "vcp.odoo.lib.python", + python_library_ids = fields.Many2many( + "vcp.odoo.python.library", string="Python Libraries", + readonly=True, ) bin_package_ids = fields.Many2many( "vcp.odoo.bin.package", string="Python Binaries", + readonly=True, ) + description = fields.Html(readonly=True) def _get_local_path(self): return f"{self.repository_branch_id.local_path}/{self.path}" diff --git a/vcp_odoo/models/vcp_odoo_python_library.py b/vcp_odoo/models/vcp_odoo_python_library.py new file mode 100644 index 0000000..d8f01ad --- /dev/null +++ b/vcp_odoo/models/vcp_odoo_python_library.py @@ -0,0 +1,35 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError + + +class VcpOdooPythonLibrary(models.Model): + _name = "vcp.odoo.python.library" + _description = "Python Library required by an Odoo Module" + + name = fields.Char(required=True, readonly=True) + + module_version_ids = fields.Many2many( + "vcp.odoo.module.version", + string="Odoo Module Versions", + readonly=True, + ) + + @tools.ormcache("name") + def _get_python_library(self, name): + lib = self.search([("name", "=", name)], limit=1) + if not lib: + lib = self.create({"name": name}) + return lib.id + + @api.ondelete(at_uninstall=False) + def _check_module_versions(self): + if self.mapped("module_version_ids"): + raise UserError( + _( + "You can not delete librairies that are related to Odoo Modules. " + "You should first delete the related odoo modules." + ) + ) diff --git a/vcp_odoo/models/vcp_rule.py b/vcp_odoo/models/vcp_rule.py index a9309ea..255754f 100644 --- a/vcp_odoo/models/vcp_rule.py +++ b/vcp_odoo/models/vcp_rule.py @@ -5,6 +5,7 @@ import copy import logging import os +from pathlib import Path from odoo import fields, models from odoo.fields import Command @@ -75,6 +76,11 @@ def _get_odoo_icon_path(self): "static/description/icon.png", ] + def _get_html_description_path(self): + return [ + "static/description/index.html", + ] + def _process_rule_odoo_module_prepare_vals( self, repository_branch, module_id, manifest_path ): @@ -92,10 +98,20 @@ def _process_rule_odoo_module_prepare_vals( break python_libs = [] for lib in manifest.get("external_dependencies", {}).get("python", []): - python_libs.append(self.env["vcp.odoo.lib.python"]._get_lib_python(lib)) + python_libs.append( + self.env["vcp.odoo.python.library"]._get_python_library(lib) + ) package_bins = [] for package_bin in manifest.get("external_dependencies", {}).get("bin", []): - package_bins.append(self.env["vcp.odoo.bin.package"]._get_bin(package_bin)) + package_bins.append( + self.env["vcp.odoo.bin.package"]._get_bin_package(package_bin) + ) + description = False + for html_description_path in self._get_html_description_path(): + path = Path(os.path.dirname(manifest_path)) / html_description_path + if path.exists(): + description = path.read_text() + break return { "name": manifest.get("name"), "module_id": module_id, @@ -112,6 +128,7 @@ def _process_rule_odoo_module_prepare_vals( "repository_branch_id": repository_branch.id, "depends_on_module_ids": [Command.set(depends)], "image_1920": icon, - "lib_python_ids": [Command.set(python_libs)], + "python_library_ids": [Command.set(python_libs)], "bin_package_ids": [Command.set(package_bins)], + "description": description, } diff --git a/vcp_odoo/security/ir.model.access.csv b/vcp_odoo/security/ir.model.access.csv index bad4a4f..2fe5323 100644 --- a/vcp_odoo/security/ir.model.access.csv +++ b/vcp_odoo/security/ir.model.access.csv @@ -1,9 +1,9 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" access_vcp_odoo_module,Access Odoo Module,model_vcp_odoo_module,vcp_management.group_vcp_user,1,0,0,0 -manage_vcp_odoo_module,Manage Odoo Module,model_vcp_odoo_module,vcp_management.group_vcp_manager,1,1,1,0 +manage_vcp_odoo_module,Manage Odoo Module,model_vcp_odoo_module,vcp_management.group_vcp_manager,1,1,1,1 access_vcp_odoo_module_version,Access Odoo Module Version,model_vcp_odoo_module_version,vcp_management.group_vcp_user,1,0,0,0 -manage_vcp_odoo_module_version,Manage Odoo Module Version,model_vcp_odoo_module_version,vcp_management.group_vcp_manager,1,1,1,0 +manage_vcp_odoo_module_version,Manage Odoo Module Version,model_vcp_odoo_module_version,vcp_management.group_vcp_manager,1,1,1,1 access_vcp_odoo_bin_package,Access Odoo Bin Package,model_vcp_odoo_bin_package,vcp_management.group_vcp_user,1,0,0,0 -manage_vcp_odoo_bin_package,Manage Odoo Bin Package,model_vcp_odoo_bin_package,vcp_management.group_vcp_manager,1,1,1,0 -access_vcp_odoo_lib_python,Access Odoo Python Library,model_vcp_odoo_lib_python,vcp_management.group_vcp_user,1,0,0,0 -manage_vcp_odoo_lib_python,Manage Odoo Python Library,model_vcp_odoo_lib_python,vcp_management.group_vcp_manager,1,1,1,0 +manage_vcp_odoo_bin_package,Manage Odoo Bin Package,model_vcp_odoo_bin_package,vcp_management.group_vcp_manager,1,1,1,1 +access_vcp_odoo_python_library,Access Odoo Python Library,model_vcp_odoo_python_library,vcp_management.group_vcp_user,1,0,0,0 +manage_vcp_odoo_python_library,Manage Odoo Python Library,model_vcp_odoo_python_library,vcp_management.group_vcp_manager,1,1,1,1 diff --git a/vcp_odoo/views/menu.xml b/vcp_odoo/views/menu.xml new file mode 100644 index 0000000..befbdcf --- /dev/null +++ b/vcp_odoo/views/menu.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/vcp_odoo/views/vcp_odoo_bin_package.xml b/vcp_odoo/views/vcp_odoo_bin_package.xml new file mode 100644 index 0000000..ff91bd2 --- /dev/null +++ b/vcp_odoo/views/vcp_odoo_bin_package.xml @@ -0,0 +1,50 @@ + + + + + vcp.odoo.bin.package + +
+
+ +
+

+ +

+
+ + + +
+ + + + + + vcp.odoo.bin.package + + + + + + + + + vcp.odoo.bin.package + + + + + + + + + Bin Packages + vcp-odoo-bin-package + vcp.odoo.bin.package + list,form + [] + {} + + diff --git a/vcp_odoo/views/vcp_odoo_module.xml b/vcp_odoo/views/vcp_odoo_module.xml index 105d54a..d7d0166 100644 --- a/vcp_odoo/views/vcp_odoo_module.xml +++ b/vcp_odoo/views/vcp_odoo_module.xml @@ -51,17 +51,11 @@ - Odoo Modules + Modules + vcp-odoo-module vcp.odoo.module list,form [] {'search_default_filter_with_version': 1} - - - Odoo Modules - - - - diff --git a/vcp_odoo/views/vcp_odoo_module_version.xml b/vcp_odoo/views/vcp_odoo_module_version.xml index 8e1a514..3484034 100644 --- a/vcp_odoo/views/vcp_odoo_module_version.xml +++ b/vcp_odoo/views/vcp_odoo_module_version.xml @@ -26,12 +26,13 @@ - + + - - + + @@ -64,4 +65,13 @@ + + + Module Versions + vcp-odoo-module-version + vcp.odoo.module.version + list,form + [] + {} + diff --git a/vcp_odoo/views/vcp_odoo_python_library.xml b/vcp_odoo/views/vcp_odoo_python_library.xml new file mode 100644 index 0000000..0b5580c --- /dev/null +++ b/vcp_odoo/views/vcp_odoo_python_library.xml @@ -0,0 +1,50 @@ + + + + + vcp.odoo.python.library + +
+
+ +
+

+ +

+
+ + + +
+ + + + + + vcp.odoo.python.library + + + + + + + + + vcp.odoo.python.library + + + + + + + + + Bin Packages + vcp-odoo-python-library + vcp.odoo.python.library + list,form + [] + {} + +