Coverage for adhoc-cicd-odoo-odoo / odoo / orm / model_classes.py: 75%

251 statements  

« 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 

3from __future__ import annotations 

4 

5import logging 

6import typing 

7 

8from collections import defaultdict 

9from types import MappingProxyType 

10 

11from . import models 

12from . import fields # must be imported after models 

13from .utils import check_pg_name 

14from odoo.exceptions import ValidationError 

15from odoo.tools import ( 

16 OrderedSet, 

17 LastOrderedSet, 

18 discardattr, 

19 frozendict, 

20 sql, 

21) 

22from odoo.tools.translate import FIELD_TRANSLATE 

23 

24if typing.TYPE_CHECKING: 

25 from odoo.api import Environment 

26 from odoo.fields import Field 

27 from odoo.models import BaseModel 

28 from odoo.modules.registry import Registry 

29 

30_logger = logging.getLogger('odoo.registry') 

31 

32# THE MODEL DEFINITIONS, MODEL CLASSES, AND MODEL INSTANCES 

33# 

34# The framework deals with two kinds of classes for models: the "model 

35# definitions" and the "model classes". 

36# 

37# The "model definitions" are the classes defined in modules source code: they 

38# define models and extend them. Those classes are essentially "static", for 

39# whatever that means in Python. The only exception is custom models: their 

40# model definition is created dynamically. 

41# 

42# The "model classes" are the ones you find in the registry. The recordsets of 

43# a model actually are instances of its model class. The "model class" of a 

44# model is created dynamically when the registry is built. It inherits (in the 

45# Python sense) from all the model definitions of the model, and possibly other 

46# model classes (when the model inherits from another model). It also carries 

47# model metadata inferred from its parent classes. 

48# 

49# 

50# THE MODEL CLASSES 

51# 

52# In the simplest case, a model class inherits from all the classes that define 

53# the model in a flat hierarchy. Consider the definitions of model 'a' below. 

54# The model class of 'a' inherits from the model definitions A1, A2, A3, in 

55# reverse order, to match the expected overriding order. The model class 

56# carries inferred metadata that is shared between all the recordsets of that 

57# model for a given registry. 

58# 

59# class A(Model): # A1 Model 

60# _name = 'a' / | \ 

61# A3 A2 A1 <- model definitions 

62# class A(Model): # A2 \ | / 

63# _inherit = 'a' a <- model class: registry['a'] 

64# | 

65# class A(Model): # A3 records <- model instances, like env['a'] 

66# _inherit = 'a' 

67# 

68# Note that when the model inherits from another model, we actually make the 

69# model classes inherit from each other, so that extensions to an inherited 

70# model are visible in the model class of the child model, like in the 

71# following example. 

72# 

73# class A(Model): # A1 

74# _name = 'a' Model 

75# / / \ \ 

76# class B(Model): # B1 / / \ \ 

77# _name = 'b' / A2 A1 \ 

78# B2 \ / B1 

79# class B(Model): # B2 \ \ / / 

80# _inherit = ['a', 'b'] \ a / 

81# \ | / 

82# class A(Model): # A2 \ | / 

83# _inherit = 'a' b 

84# 

85# To be more explicit, the parent classes of model 'a' are (A2, A1), and the 

86# ones of model 'b' are (B2, a, B1). Consequently, the MRO of model 'a' is 

87# [a, A2, A1, Model] while the MRO of 'b' is [b, B2, a, A2, A1, B1, Model]. 

88# 

89# 

90# THE FIELDS OF A MODEL 

91# 

92# The fields of a model are given by the model's definitions, inherited models 

93# ('_inherit' and '_inherits') and other parties, like custom fields. Note that 

94# a field can be partially overridden when it appears on several definitions of 

95# its model. In that case, the field's final definition depends on the 

96# presence or absence of each model definition, which itself depends on the 

97# modules loaded in the registry. 

98# 

99# By design, the model class has access to all the fields on its model 

100# definitions. When possible, the field is used directly from its model 

101# definition. There are a number of cases where the field cannot be used 

102# directly: 

103# - the field is related (and bits may not be shared); 

104# - the field is overridden on model definitions; 

105# - the field is defined on another model (and accessible by mixin). 

106# 

107# The last case prevents sharing the field across registries, because the field 

108# object is specific to a model, and is used as a key in several key 

109# dictionaries, like the record cache and pending computations. 

110# 

111# Setting up a field on its model definition helps saving memory and time. 

112# Indeed, when sharing is possible, the field's setup is almost entirely done 

113# where the field was defined. It is thus done when the model definition was 

114# created, and it may be reused across registries. 

115# 

116# In the example below, the field 'foo' appears once on its model definition. 

117# Assuming that it is not related, that field can be set up directly on its 

118# model definition. If the model appears in several registries, the 

119# field 'foo' is effectively shared across registries. 

120# 

121# class A1(Model): Model 

122# _name = 'a' / \ 

123# foo = ... / \ 

124# bar = ... A2 A1 

125# bar foo, bar 

126# class A2(Model): \ / 

127# _inherit = 'a' \ / 

128# bar = ... a 

129# bar 

130# 

131# On the other hand, the field 'bar' is overridden in its model definitions. In 

132# that case, the framework recreates the field on the model class, which is 

133# never shared across registries. The field's setup will be based on its 

134# definitions, and will thus not be shared across registries. 

135# 

136# The so-called magic fields ('id', 'display_name', ...) used to be added on 

137# model classes. But doing so prevents them from being shared. So instead, 

138# we add them on definition classes that define a model without extending it. 

139# This increases the number of fields that are shared across registries. 

140 

141 

142def is_model_definition(cls: type) -> bool: 

143 """ Return whether ``cls`` is a model definition class. """ 

144 return isinstance(cls, models.MetaModel) and getattr(cls, 'pool', None) is None 

145 

146 

147def is_model_class(cls: type) -> bool: 

148 """ Return whether ``cls`` is a model registry class. """ 

149 return getattr(cls, 'pool', None) is not None 

150 

151 

152def add_to_registry(registry: Registry, model_def: type[BaseModel]) -> type[BaseModel]: 

153 """ Add a model definition to the given registry, and return its 

154 corresponding model class. This function creates or extends a model class 

155 for the given model definition. 

156 """ 

157 assert is_model_definition(model_def) 

158 

159 if hasattr(model_def, '_constraints'): 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true

160 _logger.warning("Model attribute '_constraints' is no longer supported, " 

161 "please use @api.constrains on methods instead.") 

162 if hasattr(model_def, '_sql_constraints'): 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true

163 _logger.warning("Model attribute '_sql_constraints' is no longer supported, " 

164 "please define model.Constraint on the model.") 

165 

166 # all models except 'base' implicitly inherit from 'base' 

167 name = model_def._name 

168 parent_names = list(model_def._inherit) 

169 if name != 'base': 

170 parent_names.append('base') 

171 

172 # create or retrieve the model's class 

173 if name in parent_names: 

174 if name not in registry: 174 ↛ 175line 174 didn't jump to line 175 because the condition on line 174 was never true

175 raise TypeError(f"Model {name!r} does not exist in registry.") 

176 model_cls = registry[name] 

177 _check_model_extension(model_cls, model_def) 

178 else: 

179 model_cls = type(name, (model_def,), { 

180 'pool': registry, # this makes it a model class 

181 '_name': name, 

182 '_register': False, 

183 '_original_module': model_def._module, 

184 '_inherit_module': {}, # map parent to introducing module 

185 '_inherit_children': OrderedSet(), # names of children models 

186 '_inherits_children': set(), # names of children models 

187 '_fields__': {}, # populated in _setup() 

188 '_table_objects': frozendict(), # populated in _setup() 

189 }) 

190 model_cls._fields = MappingProxyType(model_cls._fields__) 

191 

192 # determine all the classes the model should inherit from 

193 bases = LastOrderedSet([model_def]) 

194 for parent_name in parent_names: 

195 if parent_name not in registry: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true

196 raise TypeError(f"Model {name!r} inherits from non-existing model {parent_name!r}.") 

197 parent_cls = registry[parent_name] 

198 if parent_name == name: 

199 for base in parent_cls._base_classes__: 

200 bases.add(base) 

201 else: 

202 _check_model_parent_extension(model_cls, model_def, parent_cls) 

203 bases.add(parent_cls) 

204 model_cls._inherit_module[parent_name] = model_def._module 

205 parent_cls._inherit_children.add(name) 

206 

207 # model_cls.__bases__ must be assigned those classes; however, this 

208 # operation is quite slow, so we do it once in method _prepare_setup() 

209 model_cls._base_classes__ = tuple(bases) 

210 

211 # determine the attributes of the model's class 

212 _init_model_class_attributes(model_cls) 

213 

214 check_pg_name(model_cls._table) 

215 

216 # Transience 

217 if model_cls._transient and not model_cls._log_access: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 raise TypeError( 

219 "TransientModels must have log_access turned on, " 

220 "in order to implement their vacuum policy" 

221 ) 

222 

223 # update the registry after all checks have passed 

224 registry[name] = model_cls 

225 

226 # mark all impacted models for setup 

227 for model_name in registry.descendants([name], '_inherit', '_inherits'): 

228 registry[model_name]._setup_done__ = False 

229 

230 return model_cls 

231 

232 

233def _check_model_extension(model_cls: type[BaseModel], model_def: type[BaseModel]): 

234 """ Check whether ``model_cls`` can be extended with ``model_def``. """ 

235 if model_cls._abstract and not model_def._abstract: 235 ↛ 236line 235 didn't jump to line 236 because the condition on line 235 was never true

236 raise TypeError( 

237 f"{model_def} transforms the abstract model {model_cls._name!r} into a non-abstract model. " 

238 "That class should either inherit from AbstractModel, or set a different '_name'." 

239 ) 

240 if model_cls._transient != model_def._transient: 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true

241 if model_cls._transient: 

242 raise TypeError( 

243 f"{model_def} transforms the transient model {model_cls._name!r} into a non-transient model. " 

244 "That class should either inherit from TransientModel, or set a different '_name'." 

245 ) 

246 else: 

247 raise TypeError( 

248 f"{model_def} transforms the model {model_cls._name!r} into a transient model. " 

249 "That class should either inherit from Model, or set a different '_name'." 

250 ) 

251 

252 

253def _check_model_parent_extension(model_cls: type[BaseModel], model_def: type[BaseModel], parent_cls: type[BaseModel]): 

254 """ Check whether ``model_cls`` can inherit from ``parent_cls``. """ 

255 if model_cls._abstract and not parent_cls._abstract: 255 ↛ 256line 255 didn't jump to line 256 because the condition on line 255 was never true

256 raise TypeError( 

257 f"In {model_def}, abstract model {model_cls._name!r} cannot inherit from non-abstract model {parent_cls._name!r}." 

258 ) 

259 

260 

261def _init_model_class_attributes(model_cls: type[BaseModel]): 

262 """ Initialize model class attributes. """ 

263 assert is_model_class(model_cls) 

264 

265 model_cls._description = model_cls._name 

266 model_cls._table = model_cls._name.replace('.', '_') 

267 model_cls._log_access = model_cls._auto 

268 inherits = {} 

269 depends = {} 

270 

271 for base in reversed(model_cls._base_classes__): 

272 if is_model_definition(base): 

273 # the following attributes are not taken from registry classes 

274 if model_cls._name not in base._inherit and not base._description: 274 ↛ 275line 274 didn't jump to line 275 because the condition on line 274 was never true

275 _logger.warning("The model %s has no _description", model_cls._name) 

276 model_cls._description = base._description or model_cls._description 

277 model_cls._table = base._table or model_cls._table 

278 model_cls._log_access = getattr(base, '_log_access', model_cls._log_access) 

279 

280 inherits.update(base._inherits) 

281 

282 for mname, fnames in base._depends.items(): 

283 depends.setdefault(mname, []).extend(fnames) 

284 

285 # avoid assigning an empty dict to save memory 

286 if inherits: 

287 model_cls._inherits = inherits 

288 if depends: 

289 model_cls._depends = depends 

290 

291 # update _inherits_children of parent models 

292 registry = model_cls.pool 

293 for parent_name in model_cls._inherits: 

294 registry[parent_name]._inherits_children.add(model_cls._name) 

295 

296 # recompute attributes of _inherit_children models 

297 for child_name in model_cls._inherit_children: 

298 _init_model_class_attributes(registry[child_name]) 

299 

300 

301def setup_model_classes(env: Environment): 

302 registry = env.registry 

303 

304 # we must setup ir.model before adding manual fields because _add_manual_models may 

305 # depend on behavior that is implemented through overrides, such as is_mail_thread which 

306 # is implemented through an override to env['ir.model']._instanciate_attrs 

307 _prepare_setup(registry['ir.model']) 

308 

309 # add manual models 

310 if registry._init_modules: 

311 _add_manual_models(env) 

312 

313 # prepare the setup on all models 

314 models_classes = list(registry.values()) 

315 for model_cls in models_classes: 

316 _prepare_setup(model_cls) 

317 

318 # do the actual setup 

319 for model_cls in models_classes: 

320 _setup(model_cls, env) 

321 

322 for model_cls in models_classes: 

323 _setup_fields(model_cls, env) 

324 

325 for model_cls in models_classes: 

326 model_cls(env, (), ())._post_model_setup__() 

327 

328 

329def _prepare_setup(model_cls: type[BaseModel]): 

330 """ Prepare the setup of the model. """ 

331 if model_cls._setup_done__: 

332 assert model_cls.__bases__ == model_cls._base_classes__ 

333 return 

334 

335 # changing base classes is costly, do it only when necessary 

336 if model_cls.__bases__ != model_cls._base_classes__: 

337 model_cls.__bases__ = model_cls._base_classes__ 

338 

339 # reset those attributes on the model's class for _setup_fields() below 

340 for attr in ('_rec_name', '_active_name'): 

341 discardattr(model_cls, attr) 

342 

343 # reset properties memoized on model_cls 

344 model_cls._constraint_methods = models.BaseModel._constraint_methods 

345 model_cls._ondelete_methods = models.BaseModel._ondelete_methods 

346 model_cls._onchange_methods = models.BaseModel._onchange_methods 

347 

348 

349def _setup(model_cls: type[BaseModel], env: Environment): 

350 """ Determine all the fields of the model. """ 

351 if model_cls._setup_done__: 

352 return 

353 

354 # the classes that define this model, i.e., the ones that are not 

355 # registry classes; the purpose of this attribute is to behave as a 

356 # cache of [c for c in model_cls.mro() if not is_model_class(c))], which 

357 # is heavily used in function fields.resolve_mro() 

358 model_cls._model_classes__ = tuple(c for c in model_cls.mro() if getattr(c, 'pool', None) is None) 

359 

360 # 1. determine the proper fields of the model: the fields defined on the 

361 # class and magic fields, not the inherited or custom ones 

362 

363 # retrieve fields from parent classes, and duplicate them on model_cls to 

364 # avoid clashes with inheritance between different models 

365 for name in model_cls._fields: 

366 discardattr(model_cls, name) 

367 model_cls._fields__.clear() 

368 

369 # collect the definitions of each field (base definition + overrides) 

370 definitions = defaultdict(list) 

371 for cls in reversed(model_cls._model_classes__): 

372 # this condition is an optimization of is_model_definition(cls) 

373 if isinstance(cls, models.MetaModel): 

374 for field in cls._field_definitions: 

375 definitions[field.name].append(field) 

376 

377 for name, fields_ in definitions.items(): 

378 if f'{model_cls._name}.{name}' in model_cls.pool._database_translated_fields: 378 ↛ 382line 378 didn't jump to line 382 because the condition on line 378 was never true

379 # the field is currently translated in the database; ensure the 

380 # field is translated to avoid converting its column to varchar 

381 # and losing data 

382 translate = next(( 

383 field._args__['translate'] for field in reversed(fields_) if 'translate' in field._args__ 

384 ), False) 

385 if not translate: 

386 field_translate = FIELD_TRANSLATE.get( 

387 model_cls.pool._database_translated_fields[f'{model_cls._name}.{name}'], 

388 True 

389 ) 

390 # patch the field definition by adding an override 

391 _logger.debug("Patching %s.%s with translate=True", model_cls._name, name) 

392 fields_.append(type(fields_[0])(translate=field_translate)) 

393 if f'{model_cls._name}.{name}' in model_cls.pool._database_company_dependent_fields: 393 ↛ 397line 393 didn't jump to line 397 because the condition on line 393 was never true

394 # the field is currently company dependent in the database; ensure 

395 # the field is company dependent to avoid converting its column to 

396 # the base data type 

397 company_dependent = next(( 

398 field._args__['company_dependent'] for field in reversed(fields_) if 'company_dependent' in field._args__ 

399 ), False) 

400 if not company_dependent: 

401 # validate column type again in case the column type is changed by upgrade script 

402 rows = env.execute_query(sql.SQL( 

403 'SELECT data_type FROM information_schema.columns' 

404 ' WHERE table_name = %s AND column_name = %s AND table_schema = current_schema', 

405 model_cls._table, name, 

406 )) 

407 if rows and rows[0][0] == 'jsonb': 

408 # patch the field definition by adding an override 

409 _logger.warning("Patching %s.%s with company_dependent=True", model_cls._name, name) 

410 fields_.append(type(fields_[0])(company_dependent=True)) 

411 if len(fields_) == 1 and fields_[0]._direct and fields_[0].model_name == model_cls._name: 

412 model_cls._fields__[name] = fields_[0] 

413 else: 

414 Field = type(fields_[-1]) 

415 add_field(model_cls, name, Field(_base_fields__=tuple(fields_))) 

416 

417 # 2. add manual fields 

418 if model_cls.pool._init_modules: 

419 _add_manual_fields(model_cls, env) 

420 

421 # 3. make sure that parent models determine their own fields, then add 

422 # inherited fields to model_cls 

423 _check_inherits(model_cls) 

424 for parent_name in model_cls._inherits: 

425 _setup(model_cls.pool[parent_name], env) 

426 _add_inherited_fields(model_cls) 

427 

428 # 4. initialize more field metadata 

429 model_cls._setup_done__ = True 

430 

431 for field in model_cls._fields.values(): 

432 field.prepare_setup() 

433 

434 # 5. determine and validate rec_name 

435 if model_cls._rec_name: 

436 assert model_cls._rec_name in model_cls._fields, \ 

437 "Invalid _rec_name=%r for model %r" % (model_cls._rec_name, model_cls._name) 

438 elif 'name' in model_cls._fields: 

439 model_cls._rec_name = 'name' 

440 elif model_cls._custom and 'x_name' in model_cls._fields: 440 ↛ 441line 440 didn't jump to line 441 because the condition on line 440 was never true

441 model_cls._rec_name = 'x_name' 

442 

443 # 6. determine and validate active_name 

444 if model_cls._active_name: 

445 assert (model_cls._active_name in model_cls._fields 

446 and model_cls._active_name in ('active', 'x_active')), \ 

447 ("Invalid _active_name=%r for model %r; only 'active' and " 

448 "'x_active' are supported and the field must be present on " 

449 "the model") % (model_cls._active_name, model_cls._name) 

450 elif 'active' in model_cls._fields: 

451 model_cls._active_name = 'active' 

452 elif 'x_active' in model_cls._fields: 452 ↛ 453line 452 didn't jump to line 453 because the condition on line 452 was never true

453 model_cls._active_name = 'x_active' 

454 

455 # 7. determine table objects 

456 assert not model_cls._table_object_definitions, "model_cls is a registry model" 

457 model_cls._table_objects = frozendict({ 

458 cons.full_name(model_cls): cons 

459 for cls in reversed(model_cls._model_classes__) 

460 if isinstance(cls, models.MetaModel) 

461 for cons in cls._table_object_definitions 

462 }) 

463 

464 

465def _check_inherits(model_cls: type[BaseModel]): 

466 for comodel_name, field_name in model_cls._inherits.items(): 

467 field = model_cls._fields.get(field_name) 

468 if not field or field.type != 'many2one': 468 ↛ 469line 468 didn't jump to line 469 because the condition on line 468 was never true

469 raise TypeError( 

470 f"Missing many2one field definition for _inherits reference {field_name!r} in model {model_cls._name!r}. " 

471 f"Add a field like: {field_name} = fields.Many2one({comodel_name!r}, required=True, ondelete='cascade')" 

472 ) 

473 if not (field.delegate and field.required and (field.ondelete or "").lower() in ("cascade", "restrict")): 473 ↛ 474line 473 didn't jump to line 474 because the condition on line 473 was never true

474 raise TypeError( 

475 f"Field definition for _inherits reference {field_name!r} in {model_cls._name!r} " 

476 "must be marked as 'delegate', 'required' with ondelete='cascade' or 'restrict'" 

477 ) 

478 

479 

480def _add_inherited_fields(model_cls: type[BaseModel]): 

481 """ Determine inherited fields. """ 

482 if model_cls._abstract or not model_cls._inherits: 

483 return 

484 

485 # determine which fields can be inherited 

486 to_inherit = { 

487 name: (parent_fname, field) 

488 for parent_model_name, parent_fname in model_cls._inherits.items() 

489 for name, field in model_cls.pool[parent_model_name]._fields.items() 

490 } 

491 

492 # add inherited fields that are not redefined locally 

493 for name, (parent_fname, field) in to_inherit.items(): 

494 if name not in model_cls._fields: 

495 # inherited fields are implemented as related fields, with the 

496 # following specific properties: 

497 # - reading inherited fields should not bypass access rights 

498 # - copy inherited fields iff their original field is copied 

499 field_cls = type(field) 

500 add_field(model_cls, name, field_cls( 

501 inherited=True, 

502 inherited_field=field, 

503 related=f"{parent_fname}.{name}", 

504 related_sudo=False, 

505 copy=field.copy, 

506 readonly=field.readonly, 

507 export_string_translation=field.export_string_translation, 

508 )) 

509 

510 

511def _setup_fields(model_cls: type[BaseModel], env: Environment): 

512 """ Setup the fields, except for recomputation triggers. """ 

513 bad_fields = [] 

514 many2one_company_dependents = model_cls.pool.many2one_company_dependents 

515 model = model_cls(env, (), ()) 

516 for name, field in model_cls._fields.items(): 

517 try: 

518 field.setup(model) 

519 except Exception: 

520 if field.base_field.manual: 

521 # Something goes wrong when setup a manual field. 

522 # This can happen with related fields using another manual many2one field 

523 # that hasn't been loaded because the comodel does not exist yet. 

524 # This can also be a manual function field depending on not loaded fields yet. 

525 bad_fields.append(name) 

526 continue 

527 raise 

528 if field.type == 'many2one' and field.company_dependent: 

529 many2one_company_dependents.add(field.comodel_name, field) 

530 

531 for name in bad_fields: 531 ↛ 532line 531 didn't jump to line 532 because the loop on line 531 never started

532 pop_field(model_cls, name) 

533 

534 

535def _add_manual_models(env: Environment): 

536 """ Add extra models to the registry. """ 

537 # clean up registry first 

538 for name, model_cls in list(env.registry.items()): 

539 if model_cls._custom: 539 ↛ 540line 539 didn't jump to line 540 because the condition on line 539 was never true

540 del env.registry.models[name] 

541 # remove the model's name from its parents' _inherit_children 

542 for parent_cls in model_cls.__bases__: 

543 if hasattr(parent_cls, 'pool'): 

544 parent_cls._inherit_children.discard(name) 

545 

546 # we cannot use self._fields to determine translated fields, as it has not been set up yet 

547 env.cr.execute("SELECT *, name->>'en_US' AS name FROM ir_model WHERE state = 'manual'") 

548 for model_data in env.cr.dictfetchall(): 548 ↛ 549line 548 didn't jump to line 549 because the loop on line 548 never started

549 attrs = env['ir.model']._instanciate_attrs(model_data) 

550 

551 # adapt _auto and _log_access if necessary 

552 table_name = model_data["model"].replace(".", "_") 

553 table_kind = sql.table_kind(env.cr, table_name) 

554 if table_kind not in (sql.TableKind.Regular, None): 

555 _logger.info( 

556 "Model %r is backed by table %r which is not a regular table (%r), disabling automatic schema management", 

557 model_data["model"], table_name, table_kind, 

558 ) 

559 attrs['_auto'] = False 

560 env.cr.execute( 

561 """ SELECT a.attname 

562 FROM pg_attribute a 

563 JOIN pg_class t ON a.attrelid = t.oid AND t.relname = %s 

564 WHERE a.attnum > 0 -- skip system columns 

565 AND t.relnamespace = current_schema::regnamespace """, 

566 [table_name] 

567 ) 

568 columns = {colinfo[0] for colinfo in env.cr.fetchall()} 

569 attrs['_log_access'] = set(models.LOG_ACCESS_COLUMNS) <= columns 

570 

571 model_def = type('CustomDefinitionModel', (models.Model,), attrs) 

572 add_to_registry(env.registry, model_def) 

573 

574 

575def _add_manual_fields(model_cls: type[BaseModel], env: Environment): 

576 """ Add extra fields on model. """ 

577 IrModelFields = env['ir.model.fields'] 

578 

579 fields_data = IrModelFields._get_manual_field_data(model_cls._name) 

580 for name, field_data in fields_data.items(): 

581 if name not in model_cls._fields and field_data['state'] == 'manual': 581 ↛ 580line 581 didn't jump to line 580 because the condition on line 581 was always true

582 try: 

583 attrs = IrModelFields._instanciate_attrs(field_data) 

584 if attrs: 584 ↛ 580line 584 didn't jump to line 580 because the condition on line 584 was always true

585 field = fields.Field._by_type__[field_data['ttype']](**attrs) 

586 add_field(model_cls, name, field) 

587 except Exception: 

588 _logger.exception("Failed to load field %s.%s: skipped", model_cls._name, field_data['name']) 

589 

590 

591def add_field(model_cls: type[BaseModel], name: str, field: Field): 

592 """ Add the given ``field`` under the given ``name`` on the model class of the given ``model``. """ 

593 # Assert the name is an existing field in the model, or any model in the _inherits 

594 # or a custom field (starting by `x_`) 

595 is_class_field = any( 

596 isinstance(getattr(model, name, None), fields.Field) 

597 for model in [model_cls] + [model_cls.pool[inherit] for inherit in model_cls._inherits] 

598 ) 

599 if not (is_class_field or model_cls.pool['ir.model.fields']._is_manual_name(None, name)): 599 ↛ 600line 599 didn't jump to line 600 because the condition on line 599 was never true

600 raise ValidationError( # pylint: disable=missing-gettext 

601 f"The field `{name}` is not defined in the `{model_cls._name}` Python class and does not start with 'x_'" 

602 ) 

603 

604 # Assert the attribute to assign is a Field 

605 if not isinstance(field, fields.Field): 605 ↛ 606line 605 didn't jump to line 606 because the condition on line 605 was never true

606 raise ValidationError("You can only add `fields.Field` objects to a model fields") # pylint: disable=missing-gettext 

607 

608 if not isinstance(getattr(model_cls, name, field), fields.Field): 608 ↛ 609line 608 didn't jump to line 609 because the condition on line 608 was never true

609 _logger.warning("In model %r, field %r overriding existing value", model_cls._name, name) 

610 setattr(model_cls, name, field) 

611 field._toplevel = True 

612 field.__set_name__(model_cls, name) 

613 # add field as an attribute and in model_cls._fields__ (for reflection) 

614 model_cls._fields__[name] = field 

615 

616 

617def pop_field(model_cls: type[BaseModel], name: str) -> Field | None: 

618 """ Remove the field with the given ``name`` from the model class of ``model``. """ 

619 field = model_cls._fields__.pop(name, None) 

620 discardattr(model_cls, name) 

621 if model_cls._rec_name == name: 

622 # fixup _rec_name and display_name's dependencies 

623 model_cls._rec_name = None 

624 if model_cls.display_name in model_cls.pool.field_depends: 

625 model_cls.pool.field_depends[model_cls.display_name] = tuple( 

626 dep for dep in model_cls.pool.field_depends[model_cls.display_name] if dep != name 

627 ) 

628 return field