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"
/>
+
+