Coverage for adhoc-cicd-odoo-odoo / odoo / modules / db.py: 90%
77 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:05 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:05 +0000
1# Part of Odoo. See LICENSE file for full copyright and licensing details.
2""" Initialize the database for module management and Odoo installation. """
3from __future__ import annotations
5import logging
6import typing
7from enum import IntEnum
9from psycopg2.extras import Json
11import odoo.modules
12import odoo.tools
14if typing.TYPE_CHECKING:
15 from odoo.sql_db import BaseCursor, Cursor
17_logger = logging.getLogger(__name__)
20def is_initialized(cr: Cursor) -> bool:
21 """ Check if a database has been initialized for the ORM.
23 The database can be initialized with the 'initialize' function below.
25 """
26 return odoo.tools.sql.table_exists(cr, 'ir_module_module')
29def initialize(cr: Cursor) -> None:
30 """ Initialize a database with for the ORM.
32 This executes base/data/base_data.sql, creates the ir_module_categories
33 (taken from each module descriptor file), and creates the ir_module_module
34 and ir_model_data entries.
36 """
37 try:
38 f = odoo.tools.misc.file_path('base/data/base_data.sql')
39 except FileNotFoundError:
40 m = "File not found: 'base.sql' (provided by module 'base')."
41 _logger.critical(m)
42 raise OSError(m)
44 with odoo.tools.misc.file_open(f) as base_sql_file:
45 cr.execute(base_sql_file.read()) # pylint: disable=sql-injection
47 for info in odoo.modules.Manifest.all_addon_manifests():
48 module_name = info.name
49 categories = info['category'].split('/')
50 category_id = create_categories(cr, categories)
52 if info['installable']:
53 state = 'uninstalled'
54 else:
55 state = 'uninstallable'
57 cr.execute('INSERT INTO ir_module_module \
58 (author, website, name, shortdesc, description, \
59 category_id, auto_install, state, web, license, application, icon, sequence, summary) \
60 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
61 info['author'],
62 info['website'], module_name, Json({'en_US': info['name']}),
63 Json({'en_US': info['description']}), category_id,
64 info['auto_install'] is not False, state,
65 info['web'],
66 info['license'],
67 info['application'], info['icon'],
68 info['sequence'], Json({'en_US': info['summary']})))
69 row = cr.fetchone()
70 assert row is not None # for typing
71 module_id = row[0]
72 cr.execute(
73 'INSERT INTO ir_model_data'
74 '(name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)',
75 ('module_' + module_name, 'ir.module.module', 'base', module_id, True),
76 )
77 dependencies = info['depends']
78 for d in dependencies:
79 cr.execute(
80 'INSERT INTO ir_module_module_dependency (module_id, name, auto_install_required)'
81 ' VALUES (%s, %s, %s)',
82 (module_id, d, d in (info['auto_install'] or ()))
83 )
85 from odoo.tools import config # noqa: PLC0415
86 if config.get('skip_auto_install'): 86 ↛ 88line 86 didn't jump to line 88 because the condition on line 86 was never true
87 # even if skip_auto_install is enabled we still want to have base
88 cr.execute("""UPDATE ir_module_module SET state='to install' WHERE name = 'base'""")
89 return
91 # Install recursively all auto-installing modules
92 while True:
93 # this selects all the auto_install modules whose auto_install_required
94 # deps are marked as to install
95 cr.execute("""
96 SELECT m.name FROM ir_module_module m
97 WHERE m.auto_install
98 AND state not in ('to install', 'uninstallable')
99 AND NOT EXISTS (
100 SELECT 1 FROM ir_module_module_dependency d
101 JOIN ir_module_module mdep ON (d.name = mdep.name)
102 WHERE d.module_id = m.id
103 AND d.auto_install_required
104 AND mdep.state != 'to install'
105 )""")
106 to_auto_install = [x[0] for x in cr.fetchall()]
107 # however if the module has non-required deps we need to install
108 # those, so merge-in the modules which have a dependen*t* which is
109 # *either* to_install or in to_auto_install and merge it in?
110 cr.execute("""
111 SELECT d.name FROM ir_module_module_dependency d
112 JOIN ir_module_module m ON (d.module_id = m.id)
113 JOIN ir_module_module mdep ON (d.name = mdep.name)
114 WHERE (m.state = 'to install' OR m.name = any(%s))
115 -- don't re-mark marked modules
116 AND NOT (mdep.state = 'to install' OR mdep.name = any(%s))
117 """, [to_auto_install, to_auto_install])
118 to_auto_install.extend(x[0] for x in cr.fetchall())
120 if not to_auto_install:
121 break
122 cr.execute("""UPDATE ir_module_module SET state='to install' WHERE name in %s""", (tuple(to_auto_install),))
125def create_categories(cr: Cursor, categories: list[str]) -> int | None:
126 """ Create the ir_module_category entries for some categories.
128 categories is a list of strings forming a single category with its
129 parent categories, like ['Grand Parent', 'Parent', 'Child'].
131 Return the database id of the (last) category.
133 """
134 p_id = None
135 category = []
136 while categories:
137 category.append(categories[0])
138 xml_id = 'module_category_' + ('_'.join(x.lower() for x in category)).replace('&', 'and').replace(' ', '_')
139 # search via xml_id (because some categories are renamed)
140 cr.execute("SELECT res_id FROM ir_model_data WHERE name=%s AND module=%s AND model=%s",
141 (xml_id, "base", "ir.module.category"))
143 row = cr.fetchone()
144 if not row:
145 cr.execute('INSERT INTO ir_module_category \
146 (name, parent_id) \
147 VALUES (%s, %s) RETURNING id', (Json({'en_US': categories[0]}), p_id))
148 row = cr.fetchone()
149 assert row is not None # for typing
150 p_id = row[0]
151 cr.execute('INSERT INTO ir_model_data (module, name, res_id, model, noupdate) \
152 VALUES (%s, %s, %s, %s, %s)', ('base', xml_id, p_id, 'ir.module.category', True))
153 else:
154 p_id = row[0]
155 assert isinstance(p_id, int)
156 categories = categories[1:]
157 return p_id
160class FunctionStatus(IntEnum):
161 MISSING = 0 # function is not present (falsy)
162 PRESENT = 1 # function is present but not indexable (not immutable)
163 INDEXABLE = 2 # function is present and indexable (immutable)
166def has_unaccent(cr: BaseCursor) -> FunctionStatus:
167 """ Test whether the database has function 'unaccent' and return its status.
169 The unaccent is supposed to be provided by the PostgreSQL unaccent contrib
170 module but any similar function will be picked by OpenERP.
172 :rtype: FunctionStatus
173 """
174 cr.execute("""
175 SELECT p.provolatile
176 FROM pg_proc p
177 WHERE p.proname = 'unaccent'
178 AND p.pronamespace = current_schema::regnamespace
179 AND p.pronargs = 1
180 """)
181 result = cr.fetchone()
182 if not result: 182 ↛ 187line 182 didn't jump to line 187 because the condition on line 182 was always true
183 return FunctionStatus.MISSING
184 # The `provolatile` of unaccent allows to know whether the unaccent function
185 # can be used to create index (it should be 'i' - means immutable), see
186 # https://www.postgresql.org/docs/current/catalog-pg-proc.html.
187 return FunctionStatus.INDEXABLE if result[0] == 'i' else FunctionStatus.PRESENT
190def has_trigram(cr: BaseCursor) -> bool:
191 """ Test if the database has the a word_similarity function.
193 The word_similarity is supposed to be provided by the PostgreSQL built-in
194 pg_trgm module but any similar function will be picked by Odoo.
196 """
197 cr.execute("SELECT proname FROM pg_proc WHERE proname='word_similarity'")
198 return len(cr.fetchall()) > 0