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
1 change: 1 addition & 0 deletions odoo_repository/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from . import authentication_token
from . import ssh_key
from . import odoo_ref_data_mixin
from . import odoo_author
from . import odoo_branch
from . import odoo_license
Expand Down
67 changes: 38 additions & 29 deletions odoo_repository/models/odoo_module_branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

class OdooModuleBranch(models.Model):
_name = "odoo.module.branch"
_inherit = "odoo.ref.data.mixin"
_description = "Odoo Module Branch"
_order = "repository_sequence, module_name, branch_name"

Expand Down Expand Up @@ -644,13 +645,13 @@ def _get_module_category_id(self, category_name):
rec = self.env["odoo.module.category"].search(
[("name", "=", category_name)], limit=1
)
if not rec:
rec = (
self.env["odoo.module.category"]
.sudo()
.create({"name": category_name})
)
return rec.id
if rec:
return rec.id
return self._create_ref_data(
"odoo.module.category",
[("name", "=", category_name)],
{"name": category_name},
)
return False

@tools.ormcache("names")
Expand All @@ -661,27 +662,29 @@ def _get_author_ids(self, names):
names = [name.strip() for name in names.split(",")]
authors = self.env["odoo.author"].search([("name", "in", names)])
missing_author_names = set(names) - set(authors.mapped("name"))
missing_authors = self.env["odoo.author"]
created_ids = []
if missing_author_names:
missing_authors = (
self.env["odoo.author"]
.sudo()
.create([{"name": name} for name in missing_author_names])
created_ids = self._create_ref_data_multi(
"odoo.author",
"name",
[{"name": name} for name in missing_author_names],
)
return (authors | missing_authors).ids
return authors.ids + created_ids
return []

@tools.ormcache("names")
def _get_maintainer_ids(self, names):
if names:
maintainers = self.env["odoo.maintainer"].search([("name", "in", names)])
missing_maintainer_names = set(names) - set(maintainers.mapped("name"))
created = self.env["odoo.maintainer"]
created_ids = []
if missing_maintainer_names:
created = created.sudo().create(
[{"name": name} for name in missing_maintainer_names]
created_ids = self._create_ref_data_multi(
"odoo.maintainer",
"name",
[{"name": name} for name in missing_maintainer_names],
)
return (maintainers | created).ids
return maintainers.ids + created_ids
return []

@tools.ormcache("name")
Expand All @@ -690,9 +693,11 @@ def _get_dev_status_id(self, name):
rec = self.env["odoo.module.dev.status"].search(
[("name", "=", name)], limit=1
)
if not rec:
rec = self.env["odoo.module.dev.status"].sudo().create({"name": name})
return rec.id
if rec:
return rec.id
return self._create_ref_data(
"odoo.module.dev.status", [("name", "=", name)], {"name": name}
)
return False

@api.model
Expand Down Expand Up @@ -759,23 +764,27 @@ def _get_python_dependency_ids(self, packages):
dependencies = self.env["odoo.python.dependency"].search(
[("name", "in", packages)]
)
missing_dependencies = set(packages) - set(dependencies.mapped("name"))
created = self.env["odoo.python.dependency"]
if missing_dependencies:
created = created.sudo().create(
[{"name": package} for package in missing_dependencies]
missing_names = set(packages) - set(dependencies.mapped("name"))
created_ids = []
if missing_names:
created_ids = self._create_ref_data_multi(
"odoo.python.dependency",
"name",
[{"name": name} for name in missing_names],
)
return (dependencies | created).ids
return dependencies.ids + created_ids
return []

@tools.ormcache("license_name")
def _get_license_id(self, license_name):
if license_name:
license_model = self.env["odoo.license"]
rec = license_model.search([("name", "=", license_name)], limit=1)
if not rec:
rec = license_model.sudo().create({"name": license_name})
return rec.id
if rec:
return rec.id
return self._create_ref_data(
"odoo.license", [("name", "=", license_name)], {"name": license_name}
)
return False

def _get_module(self, name):
Expand Down
84 changes: 84 additions & 0 deletions odoo_repository/models/odoo_ref_data_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2026 ACSONE SA/NV (<https://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

import threading
from contextlib import contextmanager

import psycopg2.errors

from odoo import api, models


class OdooRefDataMixin(models.AbstractModel):
"""Shared helpers to safely create reference data cached by ``@tools.ormcache``."""

_name = "odoo.ref.data.mixin"
_description = "Odoo Ref Data Mixin"

@contextmanager
def _ref_data_cursor(self):
"""Yield the cursor to use for an isolated reference-data creation.

This method is a context manager that yields a cursor to use for creating
reference data in a separate transaction. If the current environment is in
test mode, it yields the current cursor instead, since in test mode we
don't want to create a new transaction.
"""
in_test_mode = self.env.registry.in_test_mode() or getattr(
threading.current_thread(), "testing", False
)
if in_test_mode:
yield self.env.cr
else:
with self.pool.cursor() as new_cr:
yield new_cr

def _create_ref_data(self, model_name, domain, values):
"""Create a single get-or-create reference record in its own
dedicated, immediately-committed transaction, and return its id.

``@tools.ormcache`` is a process-wide cache that is never invalidated
by a SQL ROLLBACK. If a newly created record's id were cached from
within the caller's (still uncommitted) transaction, and that
transaction was later rolled back for an unrelated reason (e.g. a
conflicting concurrent queue job), the cache would keep returning an
id that no longer exists in the database. Creating and committing the
record here, independently of the caller's transaction, guarantees
the returned id always stays valid, whatever happens to the caller's
job afterwards.

A concurrent job may create the same record at the same time: the
unique SQL constraint on the target model then raises
``UniqueViolation`` for the loser, which simply looks up the row the
winner just committed.
"""
with self._ref_data_cursor() as new_cr:
env = api.Environment(new_cr, self.env.uid, self.env.context)
model = env[model_name].sudo()
try:
with new_cr.savepoint():
return model.create(values).id
except psycopg2.errors.UniqueViolation:
return model.search(domain, limit=1).id

def _create_ref_data_multi(self, model_name, search_field, values_list):
"""Same as ``_create_ref_data``, but for a batch of records sharing
the same natural-key field (e.g. several new authors discovered at
once). Each record is created individually, in its own savepoint, so
that a conflict on one of them does not abort the creation of the
others.
"""
ids = []
with self._ref_data_cursor() as new_cr:
env = api.Environment(new_cr, self.env.uid, self.env.context)
model = env[model_name].sudo()
for values in values_list:
try:
with new_cr.savepoint():
ids.append(model.create(values).id)
except psycopg2.errors.UniqueViolation:
existing = model.search(
[(search_field, "=", values[search_field])], limit=1
)
ids.append(existing.id)
return ids
62 changes: 31 additions & 31 deletions odoo_repository/models/odoo_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

class OdooRepository(models.Model):
_name = "odoo.repository"
_inherit = "odoo.ref.data.mixin"
_description = "Odoo Modules Repository"
_order = "sequence, display_name"

Expand Down Expand Up @@ -501,12 +502,18 @@ def _import_data(self, data):
def _prepare_module_branch_values(self, data):
# Get branch, repository and technical module
branch = self.env["odoo.branch"].search([("name", "=", data["branch"])])
org = self._get_repository_org(data["repository"]["org"])
repository = self._get_repository(
org.id, data["repository"]["name"], data["repository"]
org_id = self._get_repository_org_id(data["repository"]["org"])
repository_id = self._get_repository_id(
org_id, data["repository"]["name"], data["repository"]
)
repository_branch = self._get_repository_branch(
org.id, repository.id, branch.id, data["repository"]
repository_branch_id = self._get_repository_branch_id(
org_id, repository_id, branch.id, data["repository"]
)
# `_get_repository_branch_id` returns a plain id (see `_create_ref_data`
# docstring), so browse it here to get a recordset bound to this
# transaction's cursor.
repository_branch = self.env["odoo.repository.branch"].browse(
repository_branch_id
)

mb_model = self.env["odoo.module.branch"]
Expand Down Expand Up @@ -605,21 +612,18 @@ def _post_create_or_update_module_branch(self, rec, values, raw_data):
"""Hook executed after the creation or update of `rec`."""

@tools.ormcache("name")
def _get_repository_org(self, name):
def _get_repository_org_id(self, name):
rec = self.env["odoo.repository.org"].search([("name", "=", name)], limit=1)
if not rec:
rec = self.env["odoo.repository.org"].sudo().create({"name": name})
return rec
if rec:
return rec.id
return self._create_ref_data(
"odoo.repository.org", [("name", "=", name)], {"name": name}
)

@tools.ormcache("org_id", "name")
def _get_repository(self, org_id, name, data):
rec = self.env["odoo.repository"].search(
[
("org_id", "=", org_id),
("name", "=", name),
],
limit=1,
)
def _get_repository_id(self, org_id, name, data):
domain = [("org_id", "=", org_id), ("name", "=", name)]
rec = self.env["odoo.repository"].search(domain, limit=1)
values = {
"org_id": org_id,
"name": name,
Expand All @@ -629,29 +633,25 @@ def _get_repository(self, org_id, name, data):
}
if rec:
rec.sudo().write(values)
else:
rec = self.env["odoo.repository"].sudo().create(values)
return rec
return rec.id
return self._create_ref_data("odoo.repository", domain, values)

@tools.ormcache("org_id", "repository_id", "branch_id")
def _get_repository_branch(self, org_id, repository_id, branch_id, data):
rec = self.env["odoo.repository.branch"].search(
[
("repository_id", "=", repository_id),
("branch_id", "=", branch_id),
],
limit=1,
)
def _get_repository_branch_id(self, org_id, repository_id, branch_id, data):
domain = [
("repository_id", "=", repository_id),
("branch_id", "=", branch_id),
]
rec = self.env["odoo.repository.branch"].search(domain, limit=1)
values = {
"repository_id": repository_id,
"branch_id": branch_id,
"last_scanned_commit": data["last_scanned_commit"],
}
if rec:
rec.sudo().write(values)
else:
rec = self.env["odoo.repository.branch"].sudo().create(values)
return rec
return rec.id
return self._create_ref_data("odoo.repository.branch", domain, values)

def _get_resource_url(self, branch, path):
self.ensure_one()
Expand Down
2 changes: 2 additions & 0 deletions odoo_repository/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
- Camptocamp
- Sébastien Alix \<seb@usr-src.org\>
- ACSONE SA/NV
- Laurent Mignon \<laurent.mignon@acsone.eu\>
Loading