-
-
Notifications
You must be signed in to change notification settings - Fork 809
Expand file tree
/
Copy pathir_model.py
More file actions
2122 lines (1857 loc) · 95.1 KB
/
ir_model.py
File metadata and controls
2122 lines (1857 loc) · 95.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import itertools
import logging
import psycopg2
import time
from ast import literal_eval
from collections import defaultdict
from collections.abc import Mapping
from operator import itemgetter
import dateutil
from odoo import api, fields, models, tools, _
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.osv import expression
from odoo.tools import pycompat
from odoo.tools.safe_eval import safe_eval
from odoo.openupgrade import openupgrade_log, openupgrade_loading
from openupgradelib import openupgrade
_logger = logging.getLogger(__name__)
MODULE_UNINSTALL_FLAG = '_force_unlink'
# base environment for doing a safe_eval
SAFE_EVAL_BASE = {
'datetime': datetime,
'dateutil': dateutil,
'time': time,
}
def make_compute(text, deps):
""" Return a compute function from its code body and dependencies. """
func = lambda self: safe_eval(text, SAFE_EVAL_BASE, {'self': self}, mode="exec")
deps = [arg.strip() for arg in deps.split(",")] if deps else []
return api.depends(*deps)(func)
# generic INSERT and UPDATE queries
INSERT_QUERY = "INSERT INTO {table} ({cols}) VALUES {rows} RETURNING id"
UPDATE_QUERY = "UPDATE {table} SET {assignment} WHERE {condition} RETURNING id"
def query_insert(cr, table, rows):
""" Insert rows in a table. ``rows`` is a list of dicts, all with the same
set of keys. Return the ids of the new rows.
"""
if isinstance(rows, Mapping):
rows = [rows]
cols = list(rows[0])
query = INSERT_QUERY.format(
table=table,
cols=",".join(cols),
rows=",".join("%s" for row in rows),
)
params = [tuple(row[col] for col in cols) for row in rows]
cr.execute(query, params)
return [row[0] for row in cr.fetchall()]
def query_update(cr, table, values, selectors):
""" Update the table with the given values (dict), and use the columns in
``selectors`` to select the rows to update.
"""
setters = set(values) - set(selectors)
query = UPDATE_QUERY.format(
table=table,
assignment=",".join("{0}=%({0})s".format(s) for s in setters),
condition=" AND ".join("{0}=%({0})s".format(s) for s in selectors),
)
cr.execute(query, values)
return [row[0] for row in cr.fetchall()]
#
# IMPORTANT: this must be the first model declared in the module
#
class Base(models.AbstractModel):
""" The base model, which is implicitly inherited by all models. """
_name = 'base'
_description = 'Base'
class Unknown(models.AbstractModel):
"""
Abstract model used as a substitute for relational fields with an unknown
comodel.
"""
_name = '_unknown'
_description = 'Unknown'
class IrModel(models.Model):
_name = 'ir.model'
_description = "Models"
_order = 'model'
def _default_field_id(self):
if self.env.context.get('install_mode'):
return [] # no default field when importing
return [(0, 0, {'name': 'x_name', 'field_description': 'Name', 'ttype': 'char'})]
name = fields.Char(string='Model Description', translate=True, required=True)
model = fields.Char(default='x_', required=True, index=True)
info = fields.Text(string='Information')
field_id = fields.One2many('ir.model.fields', 'model_id', string='Fields', required=True, copy=True,
default=_default_field_id)
inherited_model_ids = fields.Many2many('ir.model', compute='_inherited_models', string="Inherited models",
help="The list of models that extends the current model.")
state = fields.Selection([('manual', 'Custom Object'), ('base', 'Base Object')], string='Type', default='manual', readonly=True)
access_ids = fields.One2many('ir.model.access', 'model_id', string='Access')
rule_ids = fields.One2many('ir.rule', 'model_id', string='Record Rules')
transient = fields.Boolean(string="Transient Model")
modules = fields.Char(compute='_in_modules', string='In Apps', help='List of modules in which the object is defined or inherited')
view_ids = fields.One2many('ir.ui.view', compute='_view_ids', string='Views')
count = fields.Integer(compute='_compute_count', string="Count (Incl. Archived)",
help="Total number of records in this model")
@api.depends()
def _inherited_models(self):
self.inherited_model_ids = False
for model in self:
parent_names = list(self.env[model.model]._inherits)
if parent_names:
model.inherited_model_ids = self.search([('model', 'in', parent_names)])
else:
model.inherited_model_ids = False
@api.depends()
def _in_modules(self):
installed_modules = self.env['ir.module.module'].search([('state', '=', 'installed')])
installed_names = set(installed_modules.mapped('name'))
xml_ids = models.Model._get_external_ids(self)
for model in self:
module_names = set(xml_id.split('.')[0] for xml_id in xml_ids[model.id])
model.modules = ", ".join(sorted(installed_names & module_names))
@api.depends()
def _view_ids(self):
for model in self:
model.view_ids = self.env['ir.ui.view'].search([('model', '=', model.model)])
@api.depends()
def _compute_count(self):
cr = self.env.cr
self.count = 0
for model in self:
records = self.env[model.model]
if not records._abstract:
cr.execute('SELECT COUNT(*) FROM "%s"' % records._table)
model.count = cr.fetchone()[0]
@api.constrains('model')
def _check_model_name(self):
for model in self:
if model.state == 'manual':
if not model.model.startswith('x_'):
raise ValidationError(_("The model name must start with 'x_'."))
if not models.check_object_name(model.model):
raise ValidationError(_("The model name can only contain lowercase characters, digits, underscores and dots."))
_sql_constraints = [
('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
]
def _get(self, name):
""" Return the (sudoed) `ir.model` record with the given name.
The result may be an empty recordset if the model is not found.
"""
model_id = self._get_id(name) if name else False
return self.sudo().browse(model_id)
@tools.ormcache('name')
def _get_id(self, name):
self.env.cr.execute("SELECT id FROM ir_model WHERE model=%s", (name,))
result = self.env.cr.fetchone()
return result and result[0]
# overridden to allow searching both on model name (field 'model') and model
# description (field 'name')
@api.model
def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None):
if args is None:
args = []
domain = args + ['|', ('model', operator, name), ('name', operator, name)]
model_ids = self._search(domain, limit=limit, access_rights_uid=name_get_uid)
return models.lazy_name_get(self.browse(model_ids).with_user(name_get_uid))
def _drop_table(self):
for model in self:
current_model = self.env.get(model.model)
if current_model is not None:
# OpenUpgrade: do not run the new table cleanup
openupgrade.message(
self._cr, 'Unknown', False, False,
"Not dropping the table or view of model %s", model.model)
continue
table = current_model._table
kind = tools.table_kind(self._cr, table)
if kind == 'v':
self._cr.execute('DROP VIEW "%s"' % table)
elif kind == 'r':
self._cr.execute('DROP TABLE "%s" CASCADE' % table)
# discard all translations for this model
self._cr.execute("""
DELETE FROM ir_translation
WHERE type IN ('model', 'model_terms') AND name LIKE %s
""", [model.model + ',%'])
else:
_logger.warning('The model %s could not be dropped because it did not exist in the registry.', model.model)
return True
def unlink(self):
# Prevent manual deletion of module tables
if not self._context.get(MODULE_UNINSTALL_FLAG):
for model in self:
if model.state != 'manual':
raise UserError(_("Model '%s' contains module data and cannot be removed.") % model.name)
# prevent screwing up fields that depend on these models' fields
model.field_id._prepare_update()
# OpenUpgrade: always remove relations to this model (fix issue #3649)
self.env['ir.model.relation'].search([('model', 'in', self.ids)]).unlink()
# delete fields whose comodel is being removed
self.env['ir.model.fields'].search([('relation', 'in', self.mapped('model'))]).unlink()
self._drop_table()
res = super(IrModel, self).unlink()
# Reload registry for normal unlink only. For module uninstall, the
# reload is done independently in odoo.modules.loading.
if not self._context.get(MODULE_UNINSTALL_FLAG):
# setup models; this automatically removes model from registry
self.flush()
self.pool.setup_models(self._cr)
return res
def write(self, vals):
if '__last_update' in self._context:
self = self.with_context({k: v for k, v in self._context.items() if k != '__last_update'})
if 'model' in vals and any(rec.model != vals['model'] for rec in self):
raise UserError(_('Field "Model" cannot be modified on models.'))
if 'state' in vals and any(rec.state != vals['state'] for rec in self):
raise UserError(_('Field "Type" cannot be modified on models.'))
if 'transient' in vals and any(rec.transient != vals['transient'] for rec in self):
raise UserError(_('Field "Transient Model" cannot be modified on models.'))
# Filter out operations 4 from field id, because the web client always
# writes (4,id,False) even for non dirty items.
if 'field_id' in vals:
vals['field_id'] = [op for op in vals['field_id'] if op[0] != 4]
return super(IrModel, self).write(vals)
@api.model
def create(self, vals):
res = super(IrModel, self).create(vals)
if vals.get('state', 'manual') == 'manual':
# setup models; this automatically adds model in registry
self.flush()
self.pool.setup_models(self._cr)
# update database schema
self.pool.init_models(self._cr, [vals['model']], dict(self._context, update_custom_fields=True))
return res
@api.model
def name_create(self, name):
""" Infer the model from the name. E.g.: 'My New Model' should become 'x_my_new_model'. """
vals = {
'name': name,
'model': 'x_' + '_'.join(name.lower().split(' ')),
}
return self.create(vals).name_get()[0]
def _reflect_model_params(self, model):
""" Return the values to write to the database for the given model. """
return {
'model': model._name,
'name': model._description,
'info': next(cls.__doc__ for cls in type(model).mro() if cls.__doc__),
'state': 'manual' if model._custom else 'base',
'transient': model._transient,
}
def _reflect_model(self, model):
""" Reflect the given model and return the corresponding record. Also
create entries in 'ir.model.data'.
"""
cr = self.env.cr
# create/update the entries in 'ir.model' and 'ir.model.data'
params = self._reflect_model_params(model)
ids = query_update(cr, self._table, params, ['model'])
if not ids:
ids = query_insert(cr, self._table, params)
record = self.browse(ids)
self.pool.post_init(record.modified, set(params) - {'model', 'state'})
if model._module == self._context.get('module'):
# self._module is the name of the module that last extended self
xmlid = '%s.model_%s' % (model._module, model._name.replace('.', '_'))
self.env['ir.model.data']._update_xmlids([{'xml_id': xmlid, 'record': record}])
return record
@api.model
def _instanciate(self, model_data):
""" Return a class for the custom model given by parameters ``model_data``. """
class CustomModel(models.Model):
_name = pycompat.to_text(model_data['model'])
_description = model_data['name']
_module = False
_custom = True
_transient = bool(model_data['transient'])
__doc__ = model_data['info']
return CustomModel
def _add_manual_models(self):
""" Add extra models to the registry. """
# clean up registry first
for name, Model in list(self.pool.items()):
if Model._custom:
del self.pool.models[name]
# remove the model's name from its parents' _inherit_children
for Parent in Model.__bases__:
if hasattr(Parent, 'pool'):
Parent._inherit_children.discard(name)
# add manual models
cr = self.env.cr
cr.execute('SELECT * FROM ir_model WHERE state=%s', ['manual'])
for model_data in cr.dictfetchall():
model_class = self._instanciate(model_data)
Model = model_class._build_model(self.pool, cr)
if tools.table_kind(cr, Model._table) not in ('r', None):
# not a regular table, so disable schema upgrades
Model._auto = False
cr.execute(
'''
SELECT a.attname
FROM pg_attribute a
JOIN pg_class t
ON a.attrelid = t.oid
AND t.relname = %s
WHERE a.attnum > 0 -- skip system columns
''',
[Model._table]
)
columns = {colinfo[0] for colinfo in cr.fetchall()}
Model._log_access = set(models.LOG_ACCESS_COLUMNS) <= columns
# retrieve field types defined by the framework only (not extensions)
FIELD_TYPES = [(key, key) for key in sorted(fields.Field.by_type)]
class IrModelFields(models.Model):
_name = 'ir.model.fields'
_description = "Fields"
_order = "name"
_rec_name = 'field_description'
name = fields.Char(string='Field Name', default='x_', required=True, index=True)
complete_name = fields.Char(index=True)
model = fields.Char(string='Object Name', required=True, index=True,
help="The technical name of the model this field belongs to")
relation = fields.Char(string='Object Relation',
help="For relationship fields, the technical name of the target model")
relation_field = fields.Char(help="For one2many fields, the field on the target model that implement the opposite many2one relationship")
relation_field_id = fields.Many2one('ir.model.fields', compute='_compute_relation_field_id',
store=True, ondelete='cascade', string='Relation field')
model_id = fields.Many2one('ir.model', string='Model', required=True, index=True, ondelete='cascade',
help="The model this field belongs to")
field_description = fields.Char(string='Field Label', default='', required=True, translate=True)
help = fields.Text(string='Field Help', translate=True)
ttype = fields.Selection(selection=FIELD_TYPES, string='Field Type', required=True)
selection = fields.Char(string="Selection Options (Deprecated)",
compute='_compute_selection', inverse='_inverse_selection')
selection_ids = fields.One2many("ir.model.fields.selection", "field_id",
string="Selection Options", copy=True)
copied = fields.Boolean(string='Copied',
help="Whether the value is copied when duplicating a record.")
related = fields.Char(string='Related Field', help="The corresponding related field, if any. This must be a dot-separated list of field names.")
related_field_id = fields.Many2one('ir.model.fields', compute='_compute_related_field_id',
store=True, string="Related field", ondelete='cascade')
required = fields.Boolean()
readonly = fields.Boolean()
index = fields.Boolean(string='Indexed')
translate = fields.Boolean(string='Translatable', help="Whether values for this field can be translated (enables the translation mechanism for that field)")
size = fields.Integer()
state = fields.Selection([('manual', 'Custom Field'), ('base', 'Base Field')], string='Type', default='manual', required=True, readonly=True, index=True)
on_delete = fields.Selection([('cascade', 'Cascade'), ('set null', 'Set NULL'), ('restrict', 'Restrict')],
string='On Delete', default='set null', help='On delete property for many2one fields')
domain = fields.Char(default="[]", help="The optional domain to restrict possible values for relationship fields, "
"specified as a Python expression defining a list of triplets. "
"For example: [('color','=','red')]")
groups = fields.Many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id') # CLEANME unimplemented field (empty table)
selectable = fields.Boolean(default=True)
modules = fields.Char(compute='_in_modules', string='In Apps', help='List of modules in which the field is defined')
relation_table = fields.Char(help="Used for custom many2many fields to define a custom relation table name")
column1 = fields.Char(string='Column 1', help="Column referring to the record in the model table")
column2 = fields.Char(string="Column 2", help="Column referring to the record in the comodel table")
compute = fields.Text(help="Code to compute the value of the field.\n"
"Iterate on the recordset 'self' and assign the field's value:\n\n"
" for record in self:\n"
" record['size'] = len(record.name)\n\n"
"Modules time, datetime, dateutil are available.")
depends = fields.Char(string='Dependencies', help="Dependencies of compute method; "
"a list of comma-separated field names, like\n\n"
" name, partner_id.name")
store = fields.Boolean(string='Stored', default=True, help="Whether the value is stored in the database.")
@api.depends('relation', 'relation_field')
def _compute_relation_field_id(self):
for rec in self:
if rec.state == 'manual' and rec.relation_field:
rec.relation_field_id = self._get(rec.relation, rec.relation_field)
else:
rec.relation_field_id = False
@api.depends('related')
def _compute_related_field_id(self):
for rec in self:
if rec.state == 'manual' and rec.related:
field = rec._related_field()
rec.related_field_id = self._get(field.model_name, field.name)
else:
rec.related_field_id = False
@api.depends('selection_ids')
def _compute_selection(self):
for rec in self:
if rec.ttype in ('selection', 'reference'):
rec.selection = str(self.env['ir.model.fields.selection']._get_selection(rec.id))
else:
rec.selection = False
def _inverse_selection(self):
for rec in self:
selection = literal_eval(rec.selection or "[]")
self.env['ir.model.fields.selection']._update_selection(rec.model, rec.name, selection)
@api.depends()
def _in_modules(self):
installed_modules = self.env['ir.module.module'].search([('state', '=', 'installed')])
installed_names = set(installed_modules.mapped('name'))
xml_ids = models.Model._get_external_ids(self)
for field in self:
module_names = set(xml_id.split('.')[0] for xml_id in xml_ids[field.id])
field.modules = ", ".join(sorted(installed_names & module_names))
@api.constrains('domain')
def _check_domain(self):
for field in self:
safe_eval(field.domain or '[]')
@api.constrains('name', 'state')
def _check_name(self):
for field in self:
if field.state == 'manual' and not field.name.startswith('x_'):
raise ValidationError(_("Custom fields must have a name that starts with 'x_' !"))
try:
models.check_pg_name(field.name)
except ValidationError:
msg = _("Field names can only contain characters, digits and underscores (up to 63).")
raise ValidationError(msg)
_sql_constraints = [
('name_unique', 'UNIQUE(model, name)', "Field names must be unique per model."),
('size_gt_zero', 'CHECK (size>=0)', 'Size of the field cannot be negative.'),
]
def _related_field(self):
""" Return the ``Field`` instance corresponding to ``self.related``. """
names = self.related.split(".")
last = len(names) - 1
model = self.env[self.model or self.model_id.model]
for index, name in enumerate(names):
field = model._fields.get(name)
if field is None:
raise UserError(_("Unknown field name '%s' in related field '%s'") % (name, self.related))
if index < last and not field.relational:
raise UserError(_("Non-relational field name '%s' in related field '%s'") % (name, self.related))
model = model[name]
return field
@api.constrains('related')
def _check_related(self):
for rec in self:
if rec.state == 'manual' and rec.related:
field = rec._related_field()
if field.type != rec.ttype:
raise ValidationError(_("Related field '%s' does not have type '%s'") % (rec.related, rec.ttype))
if field.relational and field.comodel_name != rec.relation:
raise ValidationError(_("Related field '%s' does not have comodel '%s'") % (rec.related, rec.relation))
@api.onchange('related')
def _onchange_related(self):
if self.related:
try:
field = self._related_field()
except UserError as e:
return {'warning': {'title': _("Warning"), 'message': e}}
self.ttype = field.type
self.relation = field.comodel_name
self.readonly = True
self.copied = False
@api.constrains('depends')
def _check_depends(self):
""" Check whether all fields in dependencies are valid. """
for record in self:
if not record.depends:
continue
for seq in record.depends.split(","):
if not seq.strip():
raise UserError(_("Empty dependency in %r") % (record.depends))
model = self.env[record.model]
names = seq.strip().split(".")
last = len(names) - 1
for index, name in enumerate(names):
field = model._fields.get(name)
if field is None:
raise UserError(_("Unknown field %r in dependency %r") % (name, seq.strip()))
if index < last and not field.relational:
raise UserError(_("Non-relational field %r in dependency %r") % (name, seq.strip()))
model = model[name]
@api.onchange('compute')
def _onchange_compute(self):
if self.compute:
self.readonly = True
self.copied = False
@api.constrains('relation_table')
def _check_relation_table(self):
for rec in self:
if rec.relation_table:
models.check_pg_name(rec.relation_table)
@api.model
def _custom_many2many_names(self, model_name, comodel_name):
""" Return default names for the table and columns of a custom many2many field. """
rel1 = self.env[model_name]._table
rel2 = self.env[comodel_name]._table
table = 'x_%s_%s_rel' % tuple(sorted([rel1, rel2]))
if rel1 == rel2:
return (table, 'id1', 'id2')
else:
return (table, '%s_id' % rel1, '%s_id' % rel2)
@api.onchange('ttype', 'model_id', 'relation')
def _onchange_ttype(self):
self.copied = (self.ttype != 'one2many')
if self.ttype == 'many2many' and self.model_id and self.relation:
if self.relation not in self.env:
return {
'warning': {
'title': _('Model %s does not exist') % self.relation,
'message': _('Please specify a valid model for the object relation'),
}
}
names = self._custom_many2many_names(self.model_id.model, self.relation)
self.relation_table, self.column1, self.column2 = names
else:
self.relation_table = False
self.column1 = False
self.column2 = False
@api.onchange('relation_table')
def _onchange_relation_table(self):
if self.relation_table:
# check whether other fields use the same table
others = self.search([('ttype', '=', 'many2many'),
('relation_table', '=', self.relation_table),
('id', 'not in', self.ids)])
if others:
for other in others:
if (other.model, other.relation) == (self.relation, self.model):
# other is a candidate inverse field
self.column1 = other.column2
self.column2 = other.column1
return
return {'warning': {
'title': _("Warning"),
'message': _("The table %r if used for other, possibly incompatible fields.") % self.relation_table,
}}
@api.onchange('required', 'ttype', 'on_delete')
def _onchange_required(self):
for rec in self:
if rec.ttype == 'many2one' and rec.required and rec.on_delete == 'set null':
return {'warning': {'title': _("Warning"), 'message': _(
"The m2o field %s is required but declares its ondelete policy "
"as being 'set null'. Only 'restrict' and 'cascade' make sense."
% (rec.name)
)}}
def _get(self, model_name, name):
""" Return the (sudoed) `ir.model.fields` record with the given model and name.
The result may be an empty recordset if the model is not found.
"""
field_id = self._get_id(model_name, name) if model_name and name else False
return self.sudo().browse(field_id)
@tools.ormcache('model_name', 'name')
def _get_id(self, model_name, name):
self.env.cr.execute("SELECT id FROM ir_model_fields WHERE model=%s AND name=%s",
(model_name, name))
result = self.env.cr.fetchone()
return result and result[0]
def _drop_column(self):
tables_to_drop = set()
for field in self:
if field.name in models.MAGIC_COLUMNS:
continue
# OpenUpgrade: do not drop columns
openupgrade.message(
self._cr, 'Unknown', False, False,
"Not dropping the column of field %s of model %s", field.name,
field.model,
)
continue
model = self.env.get(field.model)
is_model = model is not None
if field.store:
# TODO: Refactor this brol in master
if is_model and tools.column_exists(self._cr, model._table, field.name) and \
tools.table_kind(self._cr, model._table) == 'r':
self._cr.execute('ALTER TABLE "%s" DROP COLUMN "%s" CASCADE' % (
model._table, field.name,
))
if field.state == 'manual' and field.ttype == 'many2many':
rel_name = field.relation_table or (is_model and model._fields[field.name].relation)
tables_to_drop.add(rel_name)
if field.state == 'manual' and is_model:
model._pop_field(field.name)
if field.translate:
# discard all translations for this field
self._cr.execute("""
DELETE FROM ir_translation
WHERE type IN ('model', 'model_terms') AND name=%s
""", ['%s,%s' % (field.model, field.name)])
if tables_to_drop:
# drop the relation tables that are not used by other fields
self._cr.execute("""SELECT relation_table FROM ir_model_fields
WHERE relation_table IN %s AND id NOT IN %s""",
(tuple(tables_to_drop), tuple(self.ids)))
tables_to_keep = set(row[0] for row in self._cr.fetchall())
for rel_name in tables_to_drop - tables_to_keep:
self._cr.execute('DROP TABLE "%s"' % rel_name)
return True
def _prepare_update(self):
""" Check whether the fields in ``self`` may be modified or removed.
This method prevents the modification/deletion of many2one fields
that have an inverse one2many, for instance.
"""
failed_dependencies = []
for rec in self:
model = self.env.get(rec.model)
if model is not None:
if rec.name in model._fields:
field = model._fields[rec.name]
else:
# field hasn't been loaded (yet?)
continue
for dep in model._dependent_fields(field):
if dep.manual:
failed_dependencies.append((field, dep))
for inverse in model._field_inverses.get(field, ()):
if inverse.manual and inverse.type == 'one2many':
failed_dependencies.append((field, inverse))
if not self._context.get(MODULE_UNINSTALL_FLAG) and failed_dependencies:
msg = _("The field '%s' cannot be removed because the field '%s' depends on it.")
raise UserError(msg % failed_dependencies[0])
elif failed_dependencies:
dependants = {rel[1] for rel in failed_dependencies}
to_unlink = [self._get(field.model_name, field.name) for field in dependants]
self.browse().union(*to_unlink).unlink()
self = self.filtered(lambda record: record.state == 'manual')
if not self:
return
# remove pending write of this field
# DLE P16: if there are pending towrite of the field we currently try to unlink, pop them out from the towrite queue
# test `test_unlink_with_dependant`
for record in self:
for record_values in self.env.all.towrite[record.model].values():
record_values.pop(record.name, None)
# remove fields from registry, and check that views are not broken
fields = [self.env[record.model]._pop_field(record.name) for record in self]
domain = expression.OR([('arch_db', 'like', record.name)] for record in self)
views = self.env['ir.ui.view'].search(domain)
try:
for view in views:
view._check_xml()
except Exception:
if not self._context.get(MODULE_UNINSTALL_FLAG):
raise UserError("\n".join([
_("Cannot rename/delete fields that are still present in views:"),
_("Fields: %s") % ", ".join(str(f) for f in fields),
_("View: %s") % view.name,
]))
else:
# uninstall mode
_logger.warn("The following fields were force-deleted to prevent a registry crash "
+ ", ".join(str(f) for f in fields)
+ " the following view might be broken %s" % view.name)
finally:
# the registry has been modified, restore it
self.pool.setup_models(self._cr)
def unlink(self):
if not self:
return True
# Prevent manual deletion of module columns
if not self._context.get(MODULE_UNINSTALL_FLAG) and \
any(field.state != 'manual' for field in self):
raise UserError(_("This column contains module data and cannot be removed!"))
# prevent screwing up fields that depend on these fields
self._prepare_update()
# determine registry fields corresponding to self
fields = []
for record in self:
try:
fields.append(self.pool[record.model]._fields[record.name])
except KeyError:
pass
model_names = self.mapped('model')
self._drop_column()
res = super(IrModelFields, self).unlink()
# discard the removed fields from field triggers
def discard_fields(tree):
# discard fields from the tree's root node
tree.get(None, set()).difference_update(fields)
# discard subtrees labelled with any of the fields
for field in fields:
tree.pop(field, None)
# discard fields from remaining subtrees
for field, subtree in tree.items():
if field is not None:
discard_fields(subtree)
discard_fields(self.pool.field_triggers)
self.pool.registry_invalidated = True
# The field we just deleted might be inherited, and the registry is
# inconsistent in this case; therefore we reload the registry.
if not self._context.get(MODULE_UNINSTALL_FLAG):
# setup models; this re-initializes models in registry
self.flush()
self.pool.setup_models(self._cr)
# update database schema of model and its descendant models
models = self.pool.descendants(model_names, '_inherits')
self.pool.init_models(self._cr, models, dict(self._context, update_custom_fields=True))
return res
@api.model
def create(self, vals):
if 'model_id' in vals:
model_data = self.env['ir.model'].browse(vals['model_id'])
vals['model'] = model_data.model
res = super(IrModelFields, self).create(vals)
if vals.get('state', 'manual') == 'manual':
if vals.get('relation') and not self.env['ir.model'].search([('model', '=', vals['relation'])]):
raise UserError(_("Model %s does not exist!") % vals['relation'])
if vals.get('ttype') == 'one2many':
if not self.search([('model_id', '=', vals['relation']), ('name', '=', vals['relation_field']), ('ttype', '=', 'many2one')]):
raise UserError(_("Many2one %s on model %s does not exist!") % (vals['relation_field'], vals['relation']))
self.clear_caches() # for _existing_field_data()
if vals['model'] in self.pool:
# setup models; this re-initializes model in registry
self.flush()
self.pool.setup_models(self._cr)
# update database schema of model and its descendant models
models = self.pool.descendants([vals['model']], '_inherits')
self.pool.init_models(self._cr, models, dict(self._context, update_custom_fields=True))
return res
def write(self, vals):
# if set, *one* column can be renamed here
column_rename = None
# names of the models to patch
patched_models = set()
if vals and self:
for item in self:
if item.state != 'manual':
raise UserError(_('Properties of base fields cannot be altered in this manner! '
'Please modify them through Python code, '
'preferably through a custom addon!'))
if vals.get('model_id', item.model_id.id) != item.model_id.id:
raise UserError(_("Changing the model of a field is forbidden!"))
if vals.get('ttype', item.ttype) != item.ttype:
raise UserError(_("Changing the type of a field is not yet supported. "
"Please drop it and create it again!"))
obj = self.pool.get(item.model)
field = getattr(obj, '_fields', {}).get(item.name)
if vals.get('name', item.name) != item.name:
# We need to rename the field
item._prepare_update()
if item.ttype in ('one2many', 'many2many', 'binary'):
# those field names are not explicit in the database!
pass
else:
if column_rename:
raise UserError(_('Can only rename one field at a time!'))
column_rename = (obj._table, item.name, vals['name'], item.index, item.store)
# We don't check the 'state', because it might come from the context
# (thus be set for multiple fields) and will be ignored anyway.
if obj is not None and field is not None:
patched_models.add(obj._name)
# These shall never be written (modified)
for column_name in ('model_id', 'model', 'state'):
if column_name in vals:
del vals[column_name]
res = super(IrModelFields, self).write(vals)
self.flush()
self.clear_caches() # for _existing_field_data()
if column_rename:
# rename column in database, and its corresponding index if present
table, oldname, newname, index, stored = column_rename
if stored:
self._cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % (table, oldname, newname))
if index:
self._cr.execute('ALTER INDEX "%s_%s_index" RENAME TO "%s_%s_index"' % (table, oldname, table, newname))
if column_rename or patched_models:
# setup models, this will reload all manual fields in registry
self.flush()
self.pool.setup_models(self._cr)
if patched_models:
# update the database schema of the models to patch
models = self.pool.descendants(patched_models, '_inherits')
self.pool.init_models(self._cr, models, dict(self._context, update_custom_fields=True))
return res
def name_get(self):
res = []
for field in self:
res.append((field.id, '%s (%s)' % (field.field_description, field.model)))
return res
@tools.ormcache('model_name')
def _existing_field_data(self, model_name):
""" Return the given model's existing field data. """
cr = self._cr
cr.execute("SELECT * FROM ir_model_fields WHERE model=%s", [model_name])
return {row['name']: row for row in cr.dictfetchall()}
def _reflect_field_params(self, field):
""" Return the values to write to the database for the given field. """
model = self.env['ir.model']._get(field.model_name)
return {
'model_id': model.id,
'model': field.model_name,
'name': field.name,
'field_description': field.string,
'help': field.help or None,
'ttype': field.type,
'state': 'manual' if field.manual else 'base',
'relation': field.comodel_name or None,
'index': bool(field.index),
'store': bool(field.store),
'copied': bool(field.copy),
'on_delete': getattr(field, 'ondelete', None),
'related': ".".join(field.related) if field.related else None,
'readonly': bool(field.readonly),
'required': bool(field.required),
'selectable': bool(field.search or field.store),
'size': getattr(field, 'size', None),
'translate': bool(field.translate),
'relation_field': field.inverse_name if field.type == 'one2many' else None,
'relation_table': field.relation if field.type == 'many2many' else None,
'column1': field.column1 if field.type == 'many2many' else None,
'column2': field.column2 if field.type == 'many2many' else None,
}
def _reflect_model(self, model):
""" Reflect the given model's fields. """
self.clear_caches()
by_label = {}
for field in model._fields.values():
if field.string in by_label:
_logger.warning('Two fields (%s, %s) of %s have the same label: %s.',
field.name, by_label[field.string], model, field.string)
else:
by_label[field.string] = field.name
cr = self._cr
module = self._context.get('module')
fields_data = self._existing_field_data(model._name)
to_insert = []
to_xmlids = []
for name, field in model._fields.items():
old_vals = fields_data.get(name)
new_vals = self._reflect_field_params(field)
if old_vals is None:
to_insert.append(new_vals)
elif any(old_vals[key] != new_vals[key] for key in new_vals):
ids = query_update(cr, self._table, new_vals, ['model', 'name'])
record = self.browse(ids)
keys = [key for key in new_vals if old_vals[key] != new_vals[key]]
self.pool.post_init(record.modified, keys)
old_vals.update(new_vals)
if module and (module == model._original_module or module in field._modules):
# remove this and only keep the else clause if version >= saas-12.4
if field.manual:
self.pool.loaded_xmlids.add(
'%s.field_%s__%s' % (module, model._name.replace('.', '_'), name))
else:
to_xmlids.append(name)
if to_insert:
# insert missing fields
ids = query_insert(cr, self._table, to_insert)
records = self.browse(ids)
self.pool.post_init(records.modified, to_insert[0])
self.clear_caches()
if to_xmlids:
# create or update their corresponding xml ids
fields_data = self._existing_field_data(model._name)
prefix = '%s.field_%s__' % (module, model._name.replace('.', '_'))
self.env['ir.model.data']._update_xmlids([
dict(xml_id=prefix + name, record=self.browse(fields_data[name]['id']))
for name in to_xmlids
])
if not self.pool._init:
# remove ir.model.fields that are not in self._fields
fields_data = self._existing_field_data(model._name)
extra_names = set(fields_data) - set(model._fields)
if extra_names:
# add key MODULE_UNINSTALL_FLAG in context to (1) force the
# removal of the fields and (2) not reload the registry
records = self.browse([fields_data.pop(name)['id'] for name in extra_names])
records.with_context(**{MODULE_UNINSTALL_FLAG: True}).unlink()
@tools.ormcache()
def _all_manual_field_data(self):
cr = self._cr
cr.execute("SELECT * FROM ir_model_fields WHERE state='manual'")
result = defaultdict(dict)
for row in cr.dictfetchall():
result[row['model']][row['name']] = row
return result
def _get_manual_field_data(self, model_name):
""" Return the given model's manual field data. """
return self._all_manual_field_data().get(model_name, {})
def _instanciate_attrs(self, field_data):
""" Return the parameters for a field instance for ``field_data``. """
attrs = {
'manual': True,
'string': field_data['field_description'],
'help': field_data['help'],
'index': bool(field_data['index']),
'copy': bool(field_data['copied']),
'related': field_data['related'],
'required': bool(field_data['required']),
'readonly': bool(field_data['readonly']),
'store': bool(field_data['store']),
}
if field_data['ttype'] in ('char', 'text', 'html'):
attrs['translate'] = bool(field_data['translate'])
attrs['size'] = field_data['size'] or None