proteus 7.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proteus/__init__.py +1421 -0
- proteus/config.py +423 -0
- proteus/pyson.py +771 -0
- proteus/tests/__init__.py +9 -0
- proteus/tests/common.py +17 -0
- proteus/tests/test_action.py +49 -0
- proteus/tests/test_config.py +40 -0
- proteus/tests/test_context.py +16 -0
- proteus/tests/test_model.py +406 -0
- proteus/tests/test_readme.py +23 -0
- proteus/tests/test_report.py +29 -0
- proteus/tests/test_wizard.py +54 -0
- proteus-7.8.0.dist-info/METADATA +223 -0
- proteus-7.8.0.dist-info/RECORD +18 -0
- proteus-7.8.0.dist-info/WHEEL +5 -0
- proteus-7.8.0.dist-info/licenses/LICENSE +841 -0
- proteus-7.8.0.dist-info/top_level.txt +1 -0
- proteus-7.8.0.dist-info/zip-safe +1 -0
proteus/__init__.py
ADDED
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
2
|
+
# this repository contains the full copyright notices and license terms.
|
|
3
|
+
'''
|
|
4
|
+
A library to access Tryton's models like a client.
|
|
5
|
+
'''
|
|
6
|
+
import datetime
|
|
7
|
+
import functools
|
|
8
|
+
import threading
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from functools import total_ordering
|
|
11
|
+
|
|
12
|
+
import proteus.config
|
|
13
|
+
|
|
14
|
+
__version__ = "7.8.0"
|
|
15
|
+
__all__ = ['Model', 'Wizard', 'Report']
|
|
16
|
+
|
|
17
|
+
_MODELS = threading.local()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _EvalEnvironment(dict):
|
|
21
|
+
'Dictionary for evaluation'
|
|
22
|
+
__slots__ = ('parent', 'eval_type')
|
|
23
|
+
|
|
24
|
+
def __init__(self, parent, eval_type='eval'):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.parent = parent
|
|
27
|
+
assert eval_type in ('eval', 'on_change')
|
|
28
|
+
self.eval_type = eval_type
|
|
29
|
+
|
|
30
|
+
def __getitem__(self, item):
|
|
31
|
+
if item == 'id':
|
|
32
|
+
return self.parent.id
|
|
33
|
+
if item == '_parent_' + self.parent._parent_name \
|
|
34
|
+
and self.parent._parent:
|
|
35
|
+
return _EvalEnvironment(self.parent._parent,
|
|
36
|
+
eval_type=self.eval_type)
|
|
37
|
+
if self.eval_type == 'eval':
|
|
38
|
+
return self.parent._get_eval()[item]
|
|
39
|
+
else:
|
|
40
|
+
return self.parent._get_on_change_values(fields=[item])[item]
|
|
41
|
+
|
|
42
|
+
def __getattr__(self, item):
|
|
43
|
+
try:
|
|
44
|
+
return self.__getitem__(item)
|
|
45
|
+
except KeyError:
|
|
46
|
+
raise AttributeError(item)
|
|
47
|
+
|
|
48
|
+
def get(self, item, default=None):
|
|
49
|
+
try:
|
|
50
|
+
return self.__getitem__(item)
|
|
51
|
+
except KeyError:
|
|
52
|
+
pass
|
|
53
|
+
return super().get(item, default)
|
|
54
|
+
|
|
55
|
+
def __bool__(self):
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def __str__(self):
|
|
59
|
+
return str(self.parent)
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return repr(self.parent)
|
|
63
|
+
|
|
64
|
+
def __contains__(self, item):
|
|
65
|
+
if item == 'id':
|
|
66
|
+
return True
|
|
67
|
+
if item == '_parent_' + self.parent._parent_name \
|
|
68
|
+
and self.parent._parent:
|
|
69
|
+
return True
|
|
70
|
+
if self.eval_type == 'eval':
|
|
71
|
+
return item in self.parent._get_eval()
|
|
72
|
+
else:
|
|
73
|
+
return item in self.parent._fields
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class dualmethod(object):
|
|
77
|
+
"""Descriptor implementing combination of class and instance method
|
|
78
|
+
|
|
79
|
+
When called on an instance, the class is passed as the first argument and a
|
|
80
|
+
list with the instance as the second.
|
|
81
|
+
When called on a class, the class itself is passed as the first argument.
|
|
82
|
+
|
|
83
|
+
>>> class Example(object):
|
|
84
|
+
... @dualmethod
|
|
85
|
+
... def method(cls, instances):
|
|
86
|
+
... print(len(instances))
|
|
87
|
+
...
|
|
88
|
+
>>> Example.method([Example()])
|
|
89
|
+
1
|
|
90
|
+
>>> Example().method()
|
|
91
|
+
1
|
|
92
|
+
"""
|
|
93
|
+
__slots__ = ('func')
|
|
94
|
+
|
|
95
|
+
def __init__(self, func):
|
|
96
|
+
self.func = func
|
|
97
|
+
|
|
98
|
+
def __get__(self, instance, owner):
|
|
99
|
+
|
|
100
|
+
@functools.wraps(self.func)
|
|
101
|
+
def newfunc(*args, **kwargs):
|
|
102
|
+
if instance:
|
|
103
|
+
return self.func(owner, [instance], *args, **kwargs)
|
|
104
|
+
else:
|
|
105
|
+
return self.func(owner, *args, **kwargs)
|
|
106
|
+
return newfunc
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class FieldDescriptor(object):
|
|
110
|
+
default = None
|
|
111
|
+
|
|
112
|
+
def __init__(self, name, definition):
|
|
113
|
+
super().__init__()
|
|
114
|
+
self.name = name
|
|
115
|
+
self.definition = definition
|
|
116
|
+
self.__doc__ = definition['string']
|
|
117
|
+
|
|
118
|
+
def __get__(self, instance, owner):
|
|
119
|
+
if instance.id >= 0:
|
|
120
|
+
instance._read(self.name)
|
|
121
|
+
return instance._values.get(self.name, self.default)
|
|
122
|
+
|
|
123
|
+
def __set__(self, instance, value):
|
|
124
|
+
if instance.id >= 0:
|
|
125
|
+
instance._read(self.name)
|
|
126
|
+
previous = getattr(instance, self.name)
|
|
127
|
+
instance._values[self.name] = value
|
|
128
|
+
if previous != getattr(instance, self.name):
|
|
129
|
+
instance._changed.add(self.name)
|
|
130
|
+
instance._on_change([self.name])
|
|
131
|
+
if instance._parent:
|
|
132
|
+
instance._parent._changed.add(instance._parent_field_name)
|
|
133
|
+
instance._parent._on_change([instance._parent_field_name])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class BooleanDescriptor(FieldDescriptor):
|
|
137
|
+
default = False
|
|
138
|
+
|
|
139
|
+
def __set__(self, instance, value):
|
|
140
|
+
assert isinstance(value, bool)
|
|
141
|
+
super().__set__(instance, value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class CharDescriptor(FieldDescriptor):
|
|
145
|
+
default = None
|
|
146
|
+
|
|
147
|
+
def __set__(self, instance, value):
|
|
148
|
+
assert isinstance(value, str) or value is None
|
|
149
|
+
if self.definition.get('strip') and value:
|
|
150
|
+
if self.definition['strip'] == 'leading':
|
|
151
|
+
value = value.lstrip()
|
|
152
|
+
elif self.definition['strip'] == 'trailing':
|
|
153
|
+
value = value.rstrip()
|
|
154
|
+
else:
|
|
155
|
+
value = value.strip()
|
|
156
|
+
super().__set__(instance, value or '')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class BinaryDescriptor(FieldDescriptor):
|
|
160
|
+
default = None
|
|
161
|
+
|
|
162
|
+
def __set__(self, instance, value):
|
|
163
|
+
assert isinstance(value, (bytes, bytearray)) or value is None
|
|
164
|
+
super().__set__(instance, value)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class IntegerDescriptor(FieldDescriptor):
|
|
168
|
+
|
|
169
|
+
def __set__(self, instance, value):
|
|
170
|
+
assert isinstance(value, (int, type(None)))
|
|
171
|
+
super().__set__(instance, value)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class FloatDescriptor(FieldDescriptor):
|
|
175
|
+
|
|
176
|
+
def __set__(self, instance, value):
|
|
177
|
+
assert isinstance(value, (int, float, Decimal, type(None)))
|
|
178
|
+
if value is not None:
|
|
179
|
+
value = float(value)
|
|
180
|
+
super().__set__(instance, value)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class NumericDescriptor(FieldDescriptor):
|
|
184
|
+
|
|
185
|
+
def __set__(self, instance, value):
|
|
186
|
+
assert isinstance(value, (type(None), Decimal))
|
|
187
|
+
# TODO add digits validation
|
|
188
|
+
super().__set__(instance, value)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class SelectionDescriptor(FieldDescriptor):
|
|
192
|
+
|
|
193
|
+
def __set__(self, instance, value):
|
|
194
|
+
assert isinstance(value, str) or value is None
|
|
195
|
+
# TODO add selection validation
|
|
196
|
+
super().__set__(instance, value)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class MultiSelectionDescriptor(FieldDescriptor):
|
|
200
|
+
|
|
201
|
+
def __set__(self, instance, value):
|
|
202
|
+
assert isinstance(value, (list, type(None)))
|
|
203
|
+
# TODO add selection validation
|
|
204
|
+
super().__set__(instance, value)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ReferenceDescriptor(FieldDescriptor):
|
|
208
|
+
def __get__(self, instance, owner):
|
|
209
|
+
value = super().__get__(instance, owner)
|
|
210
|
+
if isinstance(value, str):
|
|
211
|
+
model_name, id = value.split(',', 1)
|
|
212
|
+
if model_name:
|
|
213
|
+
Relation = Model.get(model_name, instance._config)
|
|
214
|
+
config = Relation._config
|
|
215
|
+
with config.reset_context(), \
|
|
216
|
+
config.set_context(instance._context):
|
|
217
|
+
value = Relation(int(id))
|
|
218
|
+
instance._values[self.name] = value
|
|
219
|
+
return value
|
|
220
|
+
|
|
221
|
+
def __set__(self, instance, value):
|
|
222
|
+
assert isinstance(value, (Model, type(None), str))
|
|
223
|
+
if isinstance(value, str):
|
|
224
|
+
assert value.startswith(',')
|
|
225
|
+
elif isinstance(value, Model):
|
|
226
|
+
assert value._config == instance._config
|
|
227
|
+
super().__set__(instance, value)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class DateDescriptor(FieldDescriptor):
|
|
231
|
+
def __get__(self, instance, owner):
|
|
232
|
+
value = super().__get__(instance, owner)
|
|
233
|
+
if isinstance(value, datetime.datetime):
|
|
234
|
+
value = value.date()
|
|
235
|
+
instance._values[self.name] = value
|
|
236
|
+
return value
|
|
237
|
+
|
|
238
|
+
def __set__(self, instance, value):
|
|
239
|
+
assert isinstance(value, datetime.date) or value is None
|
|
240
|
+
super().__set__(instance, value)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class DateTimeDescriptor(FieldDescriptor):
|
|
244
|
+
def __set__(self, instance, value):
|
|
245
|
+
assert isinstance(value, datetime.datetime) or value is None
|
|
246
|
+
super().__set__(instance, value)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class TimeDescriptor(FieldDescriptor):
|
|
250
|
+
def __set__(self, instance, value):
|
|
251
|
+
assert isinstance(value, datetime.time) or value is None
|
|
252
|
+
super().__set__(instance, value)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TimeDeltaDescriptor(FieldDescriptor):
|
|
256
|
+
def __set__(self, instance, value):
|
|
257
|
+
assert isinstance(value, datetime.timedelta) or value is None
|
|
258
|
+
super().__set__(instance, value)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class DictDescriptor(FieldDescriptor):
|
|
262
|
+
def __get__(self, instance, owner):
|
|
263
|
+
value = super().__get__(instance, owner)
|
|
264
|
+
if value:
|
|
265
|
+
value = value.copy()
|
|
266
|
+
return value
|
|
267
|
+
|
|
268
|
+
def __set__(self, instance, value):
|
|
269
|
+
assert isinstance(value, dict) or value is None
|
|
270
|
+
super().__set__(instance, value)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class Many2OneDescriptor(FieldDescriptor):
|
|
274
|
+
def __get__(self, instance, owner):
|
|
275
|
+
Relation = Model.get(self.definition['relation'], instance._config)
|
|
276
|
+
value = super().__get__(instance, owner)
|
|
277
|
+
if isinstance(value, int):
|
|
278
|
+
config = Relation._config
|
|
279
|
+
with config.reset_context(), config.set_context(instance._context):
|
|
280
|
+
value = Relation(value)
|
|
281
|
+
if self.name in instance._values:
|
|
282
|
+
instance._values[self.name] = value
|
|
283
|
+
return value
|
|
284
|
+
|
|
285
|
+
def __set__(self, instance, value):
|
|
286
|
+
assert isinstance(value, (Model, type(None)))
|
|
287
|
+
if value:
|
|
288
|
+
assert value._config == instance._config
|
|
289
|
+
super().__set__(instance, value)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class One2OneDescriptor(Many2OneDescriptor):
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class One2ManyDescriptor(FieldDescriptor):
|
|
297
|
+
default = []
|
|
298
|
+
|
|
299
|
+
def __get__(self, instance, owner):
|
|
300
|
+
from .pyson import PYSONDecoder
|
|
301
|
+
Relation = Model.get(self.definition['relation'], instance._config)
|
|
302
|
+
value = super().__get__(instance, owner)
|
|
303
|
+
if not isinstance(value, ModelList):
|
|
304
|
+
ctx = instance._context.copy() if instance._context else {}
|
|
305
|
+
if self.definition.get('context'):
|
|
306
|
+
decoder = PYSONDecoder(_EvalEnvironment(instance))
|
|
307
|
+
ctx.update(decoder.decode(self.definition.get('context')))
|
|
308
|
+
config = Relation._config
|
|
309
|
+
with config.reset_context(), config.set_context(ctx):
|
|
310
|
+
value = ModelList(self.definition, (Relation(id)
|
|
311
|
+
for id in value or []), instance, self.name)
|
|
312
|
+
instance._values[self.name] = value
|
|
313
|
+
return value
|
|
314
|
+
|
|
315
|
+
def __set__(self, instance, value):
|
|
316
|
+
raise AttributeError
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class Many2ManyDescriptor(One2ManyDescriptor):
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class ValueDescriptor(object):
|
|
324
|
+
def __init__(self, name, definition):
|
|
325
|
+
super().__init__()
|
|
326
|
+
self.name = name
|
|
327
|
+
self.definition = definition
|
|
328
|
+
|
|
329
|
+
def __get__(self, instance, owner):
|
|
330
|
+
return getattr(instance, self.name)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class ReferenceValueDescriptor(ValueDescriptor):
|
|
334
|
+
def __get__(self, instance, owner):
|
|
335
|
+
value = super().__get__(instance, owner)
|
|
336
|
+
if isinstance(value, Model):
|
|
337
|
+
value = '%s,%s' % (value.__class__.__name__, value.id)
|
|
338
|
+
return value or None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class Many2OneValueDescriptor(ValueDescriptor):
|
|
342
|
+
def __get__(self, instance, owner):
|
|
343
|
+
value = super().__get__(instance, owner)
|
|
344
|
+
return value and value.id or None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class One2OneValueDescriptor(Many2OneValueDescriptor):
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class One2ManyValueDescriptor(ValueDescriptor):
|
|
352
|
+
def __get__(self, instance, owner):
|
|
353
|
+
value = []
|
|
354
|
+
value_list = getattr(instance, self.name)
|
|
355
|
+
parent_name = self.definition.get('relation_field', '')
|
|
356
|
+
to_add = []
|
|
357
|
+
to_create = []
|
|
358
|
+
to_write = []
|
|
359
|
+
for record in value_list:
|
|
360
|
+
if record.id >= 0:
|
|
361
|
+
values = record._get_values(fields=record._changed)
|
|
362
|
+
values.pop(parent_name, None)
|
|
363
|
+
if record._changed and values:
|
|
364
|
+
to_write.extend(([record.id], values))
|
|
365
|
+
to_add.append(record.id)
|
|
366
|
+
else:
|
|
367
|
+
values = record._get_values()
|
|
368
|
+
values.pop(parent_name, None)
|
|
369
|
+
to_create.append(values)
|
|
370
|
+
if to_add:
|
|
371
|
+
value.append(('add', to_add))
|
|
372
|
+
if to_create:
|
|
373
|
+
value.append(('create', to_create))
|
|
374
|
+
if to_write:
|
|
375
|
+
value.append(('write',) + tuple(to_write))
|
|
376
|
+
if value_list.record_removed:
|
|
377
|
+
value.append(('remove', [x.id for x in value_list.record_removed]))
|
|
378
|
+
if value_list.record_deleted:
|
|
379
|
+
value.append(('delete', [x.id for x in value_list.record_deleted]))
|
|
380
|
+
return value
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class Many2ManyValueDescriptor(One2ManyValueDescriptor):
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class EvalDescriptor(object):
|
|
388
|
+
def __init__(self, name, definition):
|
|
389
|
+
super().__init__()
|
|
390
|
+
self.name = name
|
|
391
|
+
self.definition = definition
|
|
392
|
+
|
|
393
|
+
def __get__(self, instance, owner):
|
|
394
|
+
return getattr(instance, self.name)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class ReferenceEvalDescriptor(EvalDescriptor):
|
|
398
|
+
def __get__(self, instance, owner):
|
|
399
|
+
value = super().__get__(instance, owner)
|
|
400
|
+
if isinstance(value, Model):
|
|
401
|
+
value = '%s,%s' % (value.__class__.__name__, value.id)
|
|
402
|
+
return value or None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class Many2OneEvalDescriptor(EvalDescriptor):
|
|
406
|
+
def __get__(self, instance, owner):
|
|
407
|
+
value = super().__get__(instance, owner)
|
|
408
|
+
if value:
|
|
409
|
+
return value.id
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class One2OneEvalDescriptor(Many2OneEvalDescriptor):
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class One2ManyEvalDescriptor(EvalDescriptor):
|
|
418
|
+
def __get__(self, instance, owner):
|
|
419
|
+
# Directly use _values to prevent infinite recursion with
|
|
420
|
+
# One2ManyDescriptor which could evaluate this field to decode the
|
|
421
|
+
# context
|
|
422
|
+
value = instance._values.get(self.name, [])
|
|
423
|
+
if isinstance(value, ModelList):
|
|
424
|
+
return [x.id for x in value]
|
|
425
|
+
else:
|
|
426
|
+
return value
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class Many2ManyEvalDescriptor(One2ManyEvalDescriptor):
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class MetaModelFactory(object):
|
|
434
|
+
descriptors = {
|
|
435
|
+
'boolean': BooleanDescriptor,
|
|
436
|
+
'char': CharDescriptor,
|
|
437
|
+
'text': CharDescriptor,
|
|
438
|
+
'binary': BinaryDescriptor,
|
|
439
|
+
'selection': SelectionDescriptor,
|
|
440
|
+
'multiselection': MultiSelectionDescriptor,
|
|
441
|
+
'integer': IntegerDescriptor,
|
|
442
|
+
'biginteger': IntegerDescriptor,
|
|
443
|
+
'float': FloatDescriptor,
|
|
444
|
+
'numeric': NumericDescriptor,
|
|
445
|
+
'reference': ReferenceDescriptor,
|
|
446
|
+
'date': DateDescriptor,
|
|
447
|
+
'datetime': DateTimeDescriptor,
|
|
448
|
+
'timestamp': DateTimeDescriptor,
|
|
449
|
+
'time': TimeDescriptor,
|
|
450
|
+
'timedelta': TimeDeltaDescriptor,
|
|
451
|
+
'dict': DictDescriptor,
|
|
452
|
+
'many2one': Many2OneDescriptor,
|
|
453
|
+
'one2many': One2ManyDescriptor,
|
|
454
|
+
'many2many': Many2ManyDescriptor,
|
|
455
|
+
'one2one': One2OneDescriptor,
|
|
456
|
+
}
|
|
457
|
+
value_descriptors = {
|
|
458
|
+
'reference': ReferenceValueDescriptor,
|
|
459
|
+
'many2one': Many2OneValueDescriptor,
|
|
460
|
+
'one2many': One2ManyValueDescriptor,
|
|
461
|
+
'many2many': Many2ManyValueDescriptor,
|
|
462
|
+
'one2one': One2OneValueDescriptor,
|
|
463
|
+
}
|
|
464
|
+
eval_descriptors = {
|
|
465
|
+
'reference': ReferenceEvalDescriptor,
|
|
466
|
+
'many2one': Many2OneEvalDescriptor,
|
|
467
|
+
'one2many': One2ManyEvalDescriptor,
|
|
468
|
+
'many2many': Many2ManyEvalDescriptor,
|
|
469
|
+
'one2one': One2OneEvalDescriptor,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
def __init__(self, model_name, config=None):
|
|
473
|
+
super().__init__()
|
|
474
|
+
self.model_name = model_name
|
|
475
|
+
self.config = config or proteus.config.get_config()
|
|
476
|
+
|
|
477
|
+
def __call__(self):
|
|
478
|
+
models_key = 'c%su%s' % (id(self.config), self.config.user)
|
|
479
|
+
if not hasattr(_MODELS, models_key):
|
|
480
|
+
setattr(_MODELS, models_key, {})
|
|
481
|
+
|
|
482
|
+
class MetaModel(type):
|
|
483
|
+
'Meta class for Model'
|
|
484
|
+
__slots__ = ()
|
|
485
|
+
|
|
486
|
+
def __new__(mcs, name, bases, dict):
|
|
487
|
+
if self.model_name in getattr(_MODELS, models_key):
|
|
488
|
+
return getattr(_MODELS, models_key)[self.model_name]
|
|
489
|
+
proxy = self.config.get_proxy(self.model_name)
|
|
490
|
+
context = self.config.context
|
|
491
|
+
name = self.model_name
|
|
492
|
+
dict['_proxy'] = proxy
|
|
493
|
+
dict['_config'] = self.config
|
|
494
|
+
dict['_fields'] = proxy.fields_get(None, context)
|
|
495
|
+
for field_name, definition in dict['_fields'].items():
|
|
496
|
+
if field_name == 'id':
|
|
497
|
+
continue
|
|
498
|
+
Descriptor = self.descriptors.get(definition['type'],
|
|
499
|
+
FieldDescriptor)
|
|
500
|
+
dict[field_name] = Descriptor(field_name, definition)
|
|
501
|
+
VDescriptor = self.value_descriptors.get(
|
|
502
|
+
definition['type'], ValueDescriptor)
|
|
503
|
+
dict['__%s_value' % field_name] = VDescriptor(
|
|
504
|
+
field_name, definition)
|
|
505
|
+
EDescriptor = self.eval_descriptors.get(
|
|
506
|
+
definition['type'], EvalDescriptor)
|
|
507
|
+
dict['__%s_eval' % field_name] = EDescriptor(
|
|
508
|
+
field_name, definition)
|
|
509
|
+
for method in self.config.get_proxy_methods(self.model_name):
|
|
510
|
+
setattr(mcs, method, getattr(proxy, method))
|
|
511
|
+
res = type.__new__(mcs, name, bases, dict)
|
|
512
|
+
getattr(_MODELS, models_key)[self.model_name] = res
|
|
513
|
+
return res
|
|
514
|
+
__new__.__doc__ = type.__new__.__doc__
|
|
515
|
+
return MetaModel
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class ModelList(list):
|
|
519
|
+
'List for Model'
|
|
520
|
+
__slots__ = ('model_name', 'parent', 'parent_field_name', 'parent_name',
|
|
521
|
+
'domain', 'context', 'add_remove', 'search_order', 'search_context',
|
|
522
|
+
'record_removed', 'record_deleted')
|
|
523
|
+
|
|
524
|
+
def __init__(self, definition, sequence=None, parent=None,
|
|
525
|
+
parent_field_name=''):
|
|
526
|
+
self.model_name = definition['relation']
|
|
527
|
+
if sequence is None:
|
|
528
|
+
sequence = []
|
|
529
|
+
self.parent = parent
|
|
530
|
+
if parent:
|
|
531
|
+
assert parent_field_name
|
|
532
|
+
self.parent_field_name = parent_field_name
|
|
533
|
+
self.parent_name = definition.get('relation_field', '')
|
|
534
|
+
self.domain = definition.get('domain', [])
|
|
535
|
+
self.context = definition.get('context')
|
|
536
|
+
self.add_remove = definition.get('add_remove')
|
|
537
|
+
self.search_order = definition.get('search_order', 'null')
|
|
538
|
+
self.search_context = definition.get('search_context', '{}')
|
|
539
|
+
self.record_removed = set()
|
|
540
|
+
self.record_deleted = set()
|
|
541
|
+
result = super().__init__(sequence)
|
|
542
|
+
assert len(self) == len(set(self))
|
|
543
|
+
self.__check(self, on_change=False)
|
|
544
|
+
return result
|
|
545
|
+
__init__.__doc__ = list.__init__.__doc__
|
|
546
|
+
|
|
547
|
+
def _changed(self):
|
|
548
|
+
'Signal change to parent'
|
|
549
|
+
if self.parent:
|
|
550
|
+
self.parent._changed.add(self.parent_field_name)
|
|
551
|
+
self.parent._on_change([self.parent_field_name])
|
|
552
|
+
|
|
553
|
+
def _get_context(self):
|
|
554
|
+
from .pyson import PYSONDecoder
|
|
555
|
+
decoder = PYSONDecoder(_EvalEnvironment(self.parent))
|
|
556
|
+
ctx = self.parent._context.copy() if self.parent._context else {}
|
|
557
|
+
ctx.update(decoder.decode(self.context) if self.context else {})
|
|
558
|
+
return ctx
|
|
559
|
+
|
|
560
|
+
def __check(self, records, on_change=True):
|
|
561
|
+
config = None
|
|
562
|
+
for record in records:
|
|
563
|
+
assert isinstance(record, Model)
|
|
564
|
+
assert record.__class__.__name__ == self.model_name
|
|
565
|
+
if self.parent:
|
|
566
|
+
assert record._config == self.parent._config
|
|
567
|
+
elif self:
|
|
568
|
+
assert record._config == self[0]._config
|
|
569
|
+
elif config:
|
|
570
|
+
assert record._config == config
|
|
571
|
+
else:
|
|
572
|
+
config = record._config
|
|
573
|
+
if record._group is not self:
|
|
574
|
+
assert record._group is None
|
|
575
|
+
record._group = self
|
|
576
|
+
for record in records:
|
|
577
|
+
# Set parent field to trigger on_change
|
|
578
|
+
if (on_change
|
|
579
|
+
and self.parent
|
|
580
|
+
and self.parent_name in record._fields):
|
|
581
|
+
definition = record._fields[self.parent_name]
|
|
582
|
+
if definition['type'] in ('many2one', 'reference'):
|
|
583
|
+
setattr(record, self.parent_name, self.parent)
|
|
584
|
+
self.record_removed.difference_update(records)
|
|
585
|
+
self.record_deleted.difference_update(records)
|
|
586
|
+
|
|
587
|
+
def append(self, record):
|
|
588
|
+
self.__check([record])
|
|
589
|
+
if record not in self:
|
|
590
|
+
super().append(record)
|
|
591
|
+
self._changed()
|
|
592
|
+
append.__doc__ = list.append.__doc__
|
|
593
|
+
|
|
594
|
+
def extend(self, iterable):
|
|
595
|
+
iterable = list(iterable)
|
|
596
|
+
self.__check(iterable)
|
|
597
|
+
set_ = set(self)
|
|
598
|
+
super().extend(r for r in iterable if r not in set_)
|
|
599
|
+
self._changed()
|
|
600
|
+
extend.__doc__ = list.extend.__doc__
|
|
601
|
+
|
|
602
|
+
def insert(self, index, record):
|
|
603
|
+
raise NotImplementedError
|
|
604
|
+
insert.__doc__ = list.insert.__doc__
|
|
605
|
+
|
|
606
|
+
def pop(self, index=-1, _changed=True):
|
|
607
|
+
self.record_removed.add(self[index])
|
|
608
|
+
self[index]._group = None
|
|
609
|
+
res = super().pop(index)
|
|
610
|
+
if _changed:
|
|
611
|
+
self._changed()
|
|
612
|
+
return res
|
|
613
|
+
pop.__doc__ = list.pop.__doc__
|
|
614
|
+
|
|
615
|
+
def remove(self, record, _changed=True):
|
|
616
|
+
if record.id >= 0:
|
|
617
|
+
self.record_deleted.add(record)
|
|
618
|
+
record._group = None
|
|
619
|
+
res = super().remove(record)
|
|
620
|
+
if _changed:
|
|
621
|
+
self._changed()
|
|
622
|
+
return res
|
|
623
|
+
remove.__doc__ = list.remove.__doc__
|
|
624
|
+
|
|
625
|
+
def reverse(self):
|
|
626
|
+
raise NotImplementedError
|
|
627
|
+
reverse.__doc__ = list.reverse.__doc__
|
|
628
|
+
|
|
629
|
+
def sort(self):
|
|
630
|
+
raise NotImplementedError
|
|
631
|
+
sort.__doc__ = list.sort.__doc__
|
|
632
|
+
|
|
633
|
+
def new(self, **kwargs):
|
|
634
|
+
'Adds a new record to the ModelList and returns it'
|
|
635
|
+
Relation = Model.get(self.model_name, self.parent._config)
|
|
636
|
+
config = Relation._config
|
|
637
|
+
with config.reset_context(), config.set_context(self._get_context()):
|
|
638
|
+
# Set parent for on_change calls from default_get
|
|
639
|
+
new_record = Relation(_group=self, **kwargs)
|
|
640
|
+
self.append(new_record)
|
|
641
|
+
return new_record
|
|
642
|
+
|
|
643
|
+
def find(self, condition=None, offset=0, limit=None, order=None):
|
|
644
|
+
'Returns records matching condition taking into account list domain'
|
|
645
|
+
from .pyson import PYSONDecoder
|
|
646
|
+
decoder = PYSONDecoder(_EvalEnvironment(self.parent))
|
|
647
|
+
Relation = Model.get(self.model_name, self.parent._config)
|
|
648
|
+
if condition is None:
|
|
649
|
+
condition = []
|
|
650
|
+
field_domain = decoder.decode(self.domain)
|
|
651
|
+
add_remove_domain = (decoder.decode(self.add_remove)
|
|
652
|
+
if self.add_remove else [])
|
|
653
|
+
new_domain = [field_domain, add_remove_domain, condition]
|
|
654
|
+
context = self._get_context()
|
|
655
|
+
context.update(decoder.decode(self.search_context))
|
|
656
|
+
order = order if order else decoder.decode(self.search_order)
|
|
657
|
+
config = Relation._config
|
|
658
|
+
with config.reset_context(), config.set_context(context):
|
|
659
|
+
return Relation.find(new_domain, offset, limit, order)
|
|
660
|
+
|
|
661
|
+
def set_sequence(self, field='sequence'):
|
|
662
|
+
changed = False
|
|
663
|
+
prev = None
|
|
664
|
+
for record in self:
|
|
665
|
+
if prev:
|
|
666
|
+
index = getattr(prev, field)
|
|
667
|
+
else:
|
|
668
|
+
index = None
|
|
669
|
+
update = False
|
|
670
|
+
value = getattr(record, field)
|
|
671
|
+
if value is None:
|
|
672
|
+
if index:
|
|
673
|
+
update = True
|
|
674
|
+
elif prev and record.id >= 0:
|
|
675
|
+
update = record.id < prev.id
|
|
676
|
+
elif value == index:
|
|
677
|
+
if prev and record.id >= 0:
|
|
678
|
+
update = record.id < prev.id
|
|
679
|
+
elif value <= (index or 0):
|
|
680
|
+
update = True
|
|
681
|
+
if update:
|
|
682
|
+
if index is None:
|
|
683
|
+
index = 0
|
|
684
|
+
index += 1
|
|
685
|
+
setattr(record, field, index)
|
|
686
|
+
changed = record
|
|
687
|
+
prev = record
|
|
688
|
+
if changed:
|
|
689
|
+
self._changed()
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
@total_ordering
|
|
693
|
+
class Model(object):
|
|
694
|
+
'Model class for Tryton records'
|
|
695
|
+
__slots__ = ('__id', '_values', '_changed', '_group', '__context')
|
|
696
|
+
|
|
697
|
+
__counter = -1
|
|
698
|
+
_proxy = None
|
|
699
|
+
_config = None
|
|
700
|
+
_fields = None
|
|
701
|
+
|
|
702
|
+
def __init__(self, id=None, _default=True, _group=None, **kwargs):
|
|
703
|
+
super().__init__()
|
|
704
|
+
if id is not None:
|
|
705
|
+
assert not kwargs
|
|
706
|
+
self.__id = id if id is not None else Model.__counter
|
|
707
|
+
if self.__id < 0:
|
|
708
|
+
Model.__counter -= 1
|
|
709
|
+
self._values = {} # store the values of fields
|
|
710
|
+
self._changed = set() # store the changed fields
|
|
711
|
+
self._group = _group # store the parent group
|
|
712
|
+
self.__context = self._config.context # store the context
|
|
713
|
+
if self.id < 0 and _default:
|
|
714
|
+
self._default_get()
|
|
715
|
+
|
|
716
|
+
for field_name, value in kwargs.items():
|
|
717
|
+
definition = self._fields[field_name]
|
|
718
|
+
if definition['type'] in ('one2many', 'many2many'):
|
|
719
|
+
relation = Model.get(definition['relation'], self._config)
|
|
720
|
+
|
|
721
|
+
def instantiate(v):
|
|
722
|
+
if isinstance(v, int):
|
|
723
|
+
return relation(v)
|
|
724
|
+
elif isinstance(v, dict):
|
|
725
|
+
return relation(_default=_default, **v)
|
|
726
|
+
else:
|
|
727
|
+
return v
|
|
728
|
+
value = [instantiate(x) for x in value]
|
|
729
|
+
getattr(self, field_name).extend(value)
|
|
730
|
+
else:
|
|
731
|
+
if definition['type'] == 'many2one':
|
|
732
|
+
if isinstance(value, int):
|
|
733
|
+
relation = Model.get(
|
|
734
|
+
definition['relation'], self._config)
|
|
735
|
+
value = relation(value)
|
|
736
|
+
setattr(self, field_name, value)
|
|
737
|
+
__init__.__doc__ = object.__init__.__doc__
|
|
738
|
+
|
|
739
|
+
@property
|
|
740
|
+
def _parent(self):
|
|
741
|
+
if self._group is not None:
|
|
742
|
+
return self._group.parent
|
|
743
|
+
|
|
744
|
+
@property
|
|
745
|
+
def _parent_field_name(self):
|
|
746
|
+
if self._group is not None:
|
|
747
|
+
return self._group.parent_field_name
|
|
748
|
+
return ''
|
|
749
|
+
|
|
750
|
+
@property
|
|
751
|
+
def _parent_name(self):
|
|
752
|
+
if self._group is not None:
|
|
753
|
+
return self._group.parent_name
|
|
754
|
+
return ''
|
|
755
|
+
|
|
756
|
+
@property
|
|
757
|
+
def _context(self):
|
|
758
|
+
if self._group:
|
|
759
|
+
context = self._group._get_context()
|
|
760
|
+
else:
|
|
761
|
+
context = self.__context
|
|
762
|
+
return context
|
|
763
|
+
|
|
764
|
+
@classmethod
|
|
765
|
+
def get(cls, name, config=None):
|
|
766
|
+
'Get a class for the named Model'
|
|
767
|
+
if (bytes == str) and isinstance(name, str):
|
|
768
|
+
name = name.encode('utf-8')
|
|
769
|
+
|
|
770
|
+
class Spam(Model, metaclass=MetaModelFactory(name, config=config)()):
|
|
771
|
+
__slots__ = ()
|
|
772
|
+
return Spam
|
|
773
|
+
|
|
774
|
+
@classmethod
|
|
775
|
+
def reset(cls, config=None, *names):
|
|
776
|
+
'Reset class definition for Models named'
|
|
777
|
+
config = config or proteus.config.get_config()
|
|
778
|
+
models_key = 'c%su%s' % (id(config), config.user)
|
|
779
|
+
if not names:
|
|
780
|
+
setattr(_MODELS, models_key, {})
|
|
781
|
+
else:
|
|
782
|
+
models = getattr(_MODELS, models_key, {})
|
|
783
|
+
for name in names:
|
|
784
|
+
del models[name]
|
|
785
|
+
|
|
786
|
+
def __str__(self):
|
|
787
|
+
return '%s,%d' % (self.__class__.__name__, self.id)
|
|
788
|
+
|
|
789
|
+
def __repr__(self):
|
|
790
|
+
if self._config == proteus.config.get_config():
|
|
791
|
+
return "proteus.Model.get('%s')(%d)" % (self.__class__.__name__,
|
|
792
|
+
self.id)
|
|
793
|
+
return "proteus.Model.get('%s', %s)(%d)" % (self.__class__.__name__,
|
|
794
|
+
repr(self._config), self.id)
|
|
795
|
+
|
|
796
|
+
def __eq__(self, other):
|
|
797
|
+
if isinstance(other, Model):
|
|
798
|
+
return ((self.__class__.__name__, self.id)
|
|
799
|
+
== (other.__class__.__name__, other.id))
|
|
800
|
+
return NotImplemented
|
|
801
|
+
|
|
802
|
+
def __lt__(self, other):
|
|
803
|
+
if not isinstance(other, Model) or self.__class__ != other.__class__:
|
|
804
|
+
return NotImplemented
|
|
805
|
+
return self.id < other.id
|
|
806
|
+
|
|
807
|
+
def __hash__(self):
|
|
808
|
+
return hash(self.__class__.__name__) ^ hash(self.id)
|
|
809
|
+
|
|
810
|
+
def __int__(self):
|
|
811
|
+
return self.id
|
|
812
|
+
|
|
813
|
+
@property
|
|
814
|
+
def id(self):
|
|
815
|
+
'The unique ID'
|
|
816
|
+
return self.__id
|
|
817
|
+
|
|
818
|
+
@id.setter
|
|
819
|
+
def id(self, value):
|
|
820
|
+
assert self.__id < 0
|
|
821
|
+
self.__id = int(value)
|
|
822
|
+
|
|
823
|
+
@classmethod
|
|
824
|
+
def find(cls, condition=None, offset=0, limit=None, order=None):
|
|
825
|
+
'Return records matching condition'
|
|
826
|
+
if condition is None:
|
|
827
|
+
condition = []
|
|
828
|
+
ids = cls._proxy.search(condition, offset, limit, order,
|
|
829
|
+
cls._config.context)
|
|
830
|
+
return [cls(id) for id in ids]
|
|
831
|
+
|
|
832
|
+
@dualmethod
|
|
833
|
+
def reload(cls, records):
|
|
834
|
+
'Reload record'
|
|
835
|
+
for record in records:
|
|
836
|
+
record._values = {}
|
|
837
|
+
record._changed = set()
|
|
838
|
+
|
|
839
|
+
@dualmethod
|
|
840
|
+
def save(cls, records):
|
|
841
|
+
'Save records'
|
|
842
|
+
if not records:
|
|
843
|
+
return
|
|
844
|
+
proxy = records[0]._proxy
|
|
845
|
+
config = records[0]._config
|
|
846
|
+
context = records[0]._context.copy()
|
|
847
|
+
create, write = [], []
|
|
848
|
+
for record in records:
|
|
849
|
+
assert proxy == record._proxy
|
|
850
|
+
assert config == record._config
|
|
851
|
+
assert context == record._context
|
|
852
|
+
if record.id < 0:
|
|
853
|
+
create.append(record)
|
|
854
|
+
elif record._changed:
|
|
855
|
+
write.append(record)
|
|
856
|
+
|
|
857
|
+
if create:
|
|
858
|
+
values = [r._get_values() for r in create]
|
|
859
|
+
ids = proxy.create(values, context)
|
|
860
|
+
for record, id_ in zip(create, ids):
|
|
861
|
+
record.id = id_
|
|
862
|
+
if write:
|
|
863
|
+
values = []
|
|
864
|
+
context['_timestamp'] = {}
|
|
865
|
+
for record in write:
|
|
866
|
+
values.append([record.id])
|
|
867
|
+
values.append(record._get_values(fields=record._changed))
|
|
868
|
+
context['_timestamp'].update(record._get_timestamp())
|
|
869
|
+
values.append(context)
|
|
870
|
+
proxy.write(*values)
|
|
871
|
+
for record in records:
|
|
872
|
+
record.reload()
|
|
873
|
+
|
|
874
|
+
@dualmethod
|
|
875
|
+
def delete(cls, records):
|
|
876
|
+
'Delete records'
|
|
877
|
+
if not records:
|
|
878
|
+
return
|
|
879
|
+
proxy = records[0]._proxy
|
|
880
|
+
config = records[0]._config
|
|
881
|
+
context = records[0]._context.copy()
|
|
882
|
+
timestamp = {}
|
|
883
|
+
delete = []
|
|
884
|
+
for record in records:
|
|
885
|
+
assert proxy == record._proxy
|
|
886
|
+
assert config == record._config
|
|
887
|
+
assert context == record._context
|
|
888
|
+
if record.id >= 0:
|
|
889
|
+
timestamp.update(record._get_timestamp())
|
|
890
|
+
delete.append(record.id)
|
|
891
|
+
context['_timestamp'] = timestamp
|
|
892
|
+
if delete:
|
|
893
|
+
proxy.delete(delete, context)
|
|
894
|
+
cls.reload(records)
|
|
895
|
+
|
|
896
|
+
@dualmethod
|
|
897
|
+
def duplicate(cls, records, default=None):
|
|
898
|
+
'Duplicate the record'
|
|
899
|
+
ids = cls._proxy.copy([r.id for r in records], default,
|
|
900
|
+
cls._config.context)
|
|
901
|
+
return [cls(id) for id in ids]
|
|
902
|
+
|
|
903
|
+
@dualmethod
|
|
904
|
+
def click(cls, records, button, change=None):
|
|
905
|
+
'Click on button'
|
|
906
|
+
if not records:
|
|
907
|
+
return
|
|
908
|
+
|
|
909
|
+
proxy = records[0]._proxy
|
|
910
|
+
config = records[0]._config
|
|
911
|
+
context = records[0]._context.copy()
|
|
912
|
+
for record in records:
|
|
913
|
+
assert proxy == record._proxy
|
|
914
|
+
assert config == record._config
|
|
915
|
+
assert context == record._context
|
|
916
|
+
|
|
917
|
+
if change is None:
|
|
918
|
+
cls.save(records)
|
|
919
|
+
cls.reload(records) # Force reload because save doesn't always
|
|
920
|
+
record_ids = [r.id for r in records]
|
|
921
|
+
action = getattr(proxy, button)(record_ids, context)
|
|
922
|
+
if action and not isinstance(action, str):
|
|
923
|
+
if isinstance(action, int):
|
|
924
|
+
action = config.get_proxy('ir.action').get_action_value(
|
|
925
|
+
action, context)
|
|
926
|
+
return _convert_action(
|
|
927
|
+
action, records, context=context, config=config)
|
|
928
|
+
return action
|
|
929
|
+
else:
|
|
930
|
+
record, = records
|
|
931
|
+
values = record._on_change_args(change)
|
|
932
|
+
values['id'] = record.id
|
|
933
|
+
changes = getattr(proxy, button)(values, context)
|
|
934
|
+
record._set_on_change(changes)
|
|
935
|
+
|
|
936
|
+
def _get_values(self, fields=None):
|
|
937
|
+
'Return dictionary values'
|
|
938
|
+
if fields is None:
|
|
939
|
+
fields = self._values.keys()
|
|
940
|
+
values = {}
|
|
941
|
+
for name in fields:
|
|
942
|
+
if name in ['id', '_timestamp']:
|
|
943
|
+
continue
|
|
944
|
+
definition = self._fields[name]
|
|
945
|
+
if definition.get('readonly') and definition['type'] != 'one2many':
|
|
946
|
+
continue
|
|
947
|
+
values[name] = getattr(self, '__%s_value' % name)
|
|
948
|
+
# Sending an empty X2Many fields breaks ModelFieldAccess.check
|
|
949
|
+
if (definition['type'] in {'one2many', 'many2many'}
|
|
950
|
+
and not values[name]):
|
|
951
|
+
del values[name]
|
|
952
|
+
return values
|
|
953
|
+
|
|
954
|
+
@property
|
|
955
|
+
def _timestamp(self):
|
|
956
|
+
'Get _timestamp'
|
|
957
|
+
return self._values.get('_timestamp')
|
|
958
|
+
|
|
959
|
+
def _get_timestamp(self):
|
|
960
|
+
'Return dictionary with timestamps'
|
|
961
|
+
result = {'%s,%s' % (self.__class__.__name__, self.id):
|
|
962
|
+
self._timestamp}
|
|
963
|
+
for field, definition in self._fields.items():
|
|
964
|
+
if field not in self._values:
|
|
965
|
+
continue
|
|
966
|
+
if definition['type'] in ('one2many', 'many2many'):
|
|
967
|
+
for record in getattr(self, field):
|
|
968
|
+
result.update(record._get_timestamp())
|
|
969
|
+
return result
|
|
970
|
+
|
|
971
|
+
def _read(self, name):
|
|
972
|
+
'Read field'
|
|
973
|
+
fields = [name]
|
|
974
|
+
if name in self._values:
|
|
975
|
+
return
|
|
976
|
+
loading = self._fields[name]['loading']
|
|
977
|
+
if loading == 'eager':
|
|
978
|
+
fields = [x for x, y in self._fields.items()
|
|
979
|
+
if y['loading'] == 'eager']
|
|
980
|
+
fields.append('_timestamp')
|
|
981
|
+
self._set(self._proxy.read([self.id], fields, self._context)[0])
|
|
982
|
+
|
|
983
|
+
def _default_get(self):
|
|
984
|
+
'Set default values'
|
|
985
|
+
fields = list(self._fields.keys())
|
|
986
|
+
self._default_set(
|
|
987
|
+
self._proxy.default_get(fields, False, self._context))
|
|
988
|
+
|
|
989
|
+
def _set(self, values, _default=False):
|
|
990
|
+
fieldnames = []
|
|
991
|
+
for field, value in values.items():
|
|
992
|
+
if '.' in field:
|
|
993
|
+
continue
|
|
994
|
+
if ((definition := self._fields.get(field))
|
|
995
|
+
and definition['type'] in {'one2many', 'many2many'}):
|
|
996
|
+
if value and len(value) and isinstance(value[0], int):
|
|
997
|
+
self._values[field] = value
|
|
998
|
+
else:
|
|
999
|
+
Relation = Model.get(definition['relation'], self._config)
|
|
1000
|
+
self._values[field] = records = ModelList(
|
|
1001
|
+
definition, [], self, field)
|
|
1002
|
+
for vals in (value or []):
|
|
1003
|
+
record = Relation()
|
|
1004
|
+
record._set(vals, _default=_default)
|
|
1005
|
+
records.append(record)
|
|
1006
|
+
else:
|
|
1007
|
+
self._values[field] = value
|
|
1008
|
+
fieldnames.append(field)
|
|
1009
|
+
if _default:
|
|
1010
|
+
self._on_change(sorted(fieldnames))
|
|
1011
|
+
|
|
1012
|
+
def _default_set(self, values):
|
|
1013
|
+
return self._set(values, _default=True)
|
|
1014
|
+
|
|
1015
|
+
def _get_eval(self):
|
|
1016
|
+
values = dict((x, getattr(self, '__%s_eval' % x))
|
|
1017
|
+
for x in self._fields if x != 'id')
|
|
1018
|
+
values['id'] = self.id
|
|
1019
|
+
return values
|
|
1020
|
+
|
|
1021
|
+
def _get_on_change_values(self, skip=None, fields=None):
|
|
1022
|
+
values = {'id': self.id}
|
|
1023
|
+
if fields:
|
|
1024
|
+
definitions = ((f, self._fields[f]) for f in fields)
|
|
1025
|
+
else:
|
|
1026
|
+
definitions = self._fields.items()
|
|
1027
|
+
for field, definition in definitions:
|
|
1028
|
+
if field == 'id':
|
|
1029
|
+
continue
|
|
1030
|
+
if not fields:
|
|
1031
|
+
if skip and field in skip:
|
|
1032
|
+
continue
|
|
1033
|
+
if (self.id >= 0
|
|
1034
|
+
and (field not in self._values
|
|
1035
|
+
or field not in self._changed)):
|
|
1036
|
+
continue
|
|
1037
|
+
if definition['type'] == 'one2many':
|
|
1038
|
+
values[field] = [x._get_on_change_values(
|
|
1039
|
+
skip={definition.get('relation_field', '')})
|
|
1040
|
+
for x in getattr(self, field)]
|
|
1041
|
+
elif (definition['type'] in ('many2one', 'reference')
|
|
1042
|
+
and self._parent_name == definition['name']
|
|
1043
|
+
and self._parent):
|
|
1044
|
+
values[field] = self._parent._get_on_change_values(
|
|
1045
|
+
skip={self._parent_field_name})
|
|
1046
|
+
if definition['type'] == 'reference':
|
|
1047
|
+
values[field] = (
|
|
1048
|
+
self._parent.__class__.__name__, values[field])
|
|
1049
|
+
else:
|
|
1050
|
+
values[field] = getattr(self, '__%s_eval' % field)
|
|
1051
|
+
return values
|
|
1052
|
+
|
|
1053
|
+
def _on_change_args(self, args):
|
|
1054
|
+
# Ensure arguments has been read
|
|
1055
|
+
for arg in args:
|
|
1056
|
+
record = self
|
|
1057
|
+
for i in arg.split('.'):
|
|
1058
|
+
if i in record._fields:
|
|
1059
|
+
getattr(record, i)
|
|
1060
|
+
elif i == '_parent_' + record._parent_name:
|
|
1061
|
+
getattr(record, record._parent_name)
|
|
1062
|
+
record = record._parent
|
|
1063
|
+
|
|
1064
|
+
res = {}
|
|
1065
|
+
values = _EvalEnvironment(self, 'on_change')
|
|
1066
|
+
for arg in args:
|
|
1067
|
+
scope = values
|
|
1068
|
+
for i in arg.split('.'):
|
|
1069
|
+
if i not in scope:
|
|
1070
|
+
break
|
|
1071
|
+
scope = scope[i]
|
|
1072
|
+
else:
|
|
1073
|
+
res[arg] = scope
|
|
1074
|
+
return res
|
|
1075
|
+
|
|
1076
|
+
def _on_change_set(self, field, value):
|
|
1077
|
+
if (self._fields[field]['type'] in ('one2many', 'many2many')
|
|
1078
|
+
and not isinstance(value, (list, tuple))):
|
|
1079
|
+
if value and value.get('delete'):
|
|
1080
|
+
for record_id in value['delete']:
|
|
1081
|
+
for record in getattr(self, field):
|
|
1082
|
+
if record.id == record_id:
|
|
1083
|
+
getattr(self, field).remove(record, _changed=False)
|
|
1084
|
+
if value and value.get('remove'):
|
|
1085
|
+
for record_id in value['remove']:
|
|
1086
|
+
for i, record in enumerate(getattr(self, field)):
|
|
1087
|
+
if record.id == record_id:
|
|
1088
|
+
getattr(self, field).pop(i, _changed=False)
|
|
1089
|
+
if value and (value.get('add') or value.get('update')):
|
|
1090
|
+
for index, vals in value.get('add', []):
|
|
1091
|
+
group = getattr(self, field)
|
|
1092
|
+
Relation = Model.get(
|
|
1093
|
+
self._fields[field]['relation'], self._config)
|
|
1094
|
+
config = Relation._config
|
|
1095
|
+
id_ = vals.pop('id', None)
|
|
1096
|
+
with config.reset_context(), \
|
|
1097
|
+
config.set_context(self._context):
|
|
1098
|
+
record = Relation(id=id_, _group=group, _default=False)
|
|
1099
|
+
try:
|
|
1100
|
+
idx = group.index(record)
|
|
1101
|
+
except ValueError:
|
|
1102
|
+
# append without signal
|
|
1103
|
+
if index == -1:
|
|
1104
|
+
list.append(group, record)
|
|
1105
|
+
else:
|
|
1106
|
+
list.insert(group, index, record)
|
|
1107
|
+
else:
|
|
1108
|
+
record = group[idx]
|
|
1109
|
+
group.record_removed.discard(record)
|
|
1110
|
+
group.record_deleted.discard(record)
|
|
1111
|
+
record._set_on_change(vals)
|
|
1112
|
+
for vals in value.get('update', []):
|
|
1113
|
+
if 'id' not in vals:
|
|
1114
|
+
continue
|
|
1115
|
+
for record in getattr(self, field):
|
|
1116
|
+
if record.id == vals['id']:
|
|
1117
|
+
record._set_on_change(vals)
|
|
1118
|
+
elif (self._fields[field]['type'] in ('one2many', 'many2many')
|
|
1119
|
+
and len(value) and not isinstance(value[0], int)):
|
|
1120
|
+
self._values[field] = []
|
|
1121
|
+
for vals in value:
|
|
1122
|
+
Relation = Model.get(
|
|
1123
|
+
self._fields[field]['relation'], self._config)
|
|
1124
|
+
config = Relation._config
|
|
1125
|
+
records = getattr(self, field)
|
|
1126
|
+
with config.reset_context(), \
|
|
1127
|
+
config.set_context(records._get_context()):
|
|
1128
|
+
record = Relation(_default=False, **vals)
|
|
1129
|
+
records.append(record)
|
|
1130
|
+
else:
|
|
1131
|
+
self._values[field] = value
|
|
1132
|
+
self._changed.add(field)
|
|
1133
|
+
|
|
1134
|
+
def _set_on_change(self, values):
|
|
1135
|
+
later = {}
|
|
1136
|
+
for field, value in values.items():
|
|
1137
|
+
if field not in self._fields:
|
|
1138
|
+
continue
|
|
1139
|
+
if self._fields[field]['type'] in ('one2many', 'many2many'):
|
|
1140
|
+
later[field] = value
|
|
1141
|
+
continue
|
|
1142
|
+
self._on_change_set(field, value)
|
|
1143
|
+
for field, value in later.items():
|
|
1144
|
+
self._on_change_set(field, value)
|
|
1145
|
+
|
|
1146
|
+
def _on_change(self, names):
|
|
1147
|
+
'Call on_change for field'
|
|
1148
|
+
# Import locally to not break installation
|
|
1149
|
+
from proteus.pyson import PYSONDecoder
|
|
1150
|
+
|
|
1151
|
+
values = {}
|
|
1152
|
+
for name in names:
|
|
1153
|
+
definition = self._fields[name]
|
|
1154
|
+
on_change = definition.get('on_change')
|
|
1155
|
+
if not on_change:
|
|
1156
|
+
continue
|
|
1157
|
+
if isinstance(on_change, str):
|
|
1158
|
+
definition['on_change'] = on_change = PYSONDecoder().decode(
|
|
1159
|
+
on_change)
|
|
1160
|
+
values.update(self._on_change_args(on_change))
|
|
1161
|
+
if values:
|
|
1162
|
+
values['id'] = self.id
|
|
1163
|
+
context = self._context
|
|
1164
|
+
change = getattr(self._proxy, 'on_change')(values, names, context)
|
|
1165
|
+
self._set_on_change(change)
|
|
1166
|
+
|
|
1167
|
+
values = {}
|
|
1168
|
+
fieldnames = set(names)
|
|
1169
|
+
to_change = set()
|
|
1170
|
+
later = set()
|
|
1171
|
+
for field, definition in self._fields.items():
|
|
1172
|
+
on_change_with = definition.get('on_change_with')
|
|
1173
|
+
if not on_change_with:
|
|
1174
|
+
continue
|
|
1175
|
+
if not fieldnames & set(on_change_with):
|
|
1176
|
+
continue
|
|
1177
|
+
if to_change & set(on_change_with):
|
|
1178
|
+
later.add(field)
|
|
1179
|
+
continue
|
|
1180
|
+
to_change.add(field)
|
|
1181
|
+
values.update(self._on_change_args(on_change_with + [field]))
|
|
1182
|
+
if to_change:
|
|
1183
|
+
values['id'] = self.id
|
|
1184
|
+
context = self._context
|
|
1185
|
+
changes = getattr(self._proxy, 'on_change_with')(values,
|
|
1186
|
+
list(to_change), context)
|
|
1187
|
+
self._set_on_change(changes)
|
|
1188
|
+
if later:
|
|
1189
|
+
values = {}
|
|
1190
|
+
for field in later:
|
|
1191
|
+
on_change_with = self._fields[field]['on_change_with']
|
|
1192
|
+
values.update(self._on_change_args(on_change_with + [field]))
|
|
1193
|
+
values['id'] = self.id
|
|
1194
|
+
context = self._context
|
|
1195
|
+
changes = getattr(self._proxy, 'on_change_with')(
|
|
1196
|
+
values, list(later), context)
|
|
1197
|
+
self._set_on_change(changes)
|
|
1198
|
+
|
|
1199
|
+
if self._parent:
|
|
1200
|
+
self._parent._changed.add(self._parent_field_name)
|
|
1201
|
+
self._parent._on_change([self._parent_field_name])
|
|
1202
|
+
|
|
1203
|
+
def notifications(self):
|
|
1204
|
+
values = self._get_on_change_values()
|
|
1205
|
+
return getattr(self._proxy, 'on_change_notify')(values, self._context)
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
class Wizard(object):
|
|
1209
|
+
'Wizard class for Tryton wizards'
|
|
1210
|
+
__slots__ = ('name', 'form', 'form_state', 'actions', '_config',
|
|
1211
|
+
'_context', '_proxy', 'session_id', 'start_state', 'end_state',
|
|
1212
|
+
'states', 'state', 'models', 'action')
|
|
1213
|
+
|
|
1214
|
+
def __init__(self, name, models=None, action=None, config=None,
|
|
1215
|
+
context=None):
|
|
1216
|
+
if models:
|
|
1217
|
+
assert len(set(type(x) for x in models)) == 1
|
|
1218
|
+
super().__init__()
|
|
1219
|
+
self.name = name
|
|
1220
|
+
self.form = None
|
|
1221
|
+
self.form_state = None
|
|
1222
|
+
self.actions = []
|
|
1223
|
+
self._config = config or proteus.config.get_config()
|
|
1224
|
+
self._context = self._config.context
|
|
1225
|
+
if context:
|
|
1226
|
+
self._context.update(context)
|
|
1227
|
+
self._proxy = self._config.get_proxy(name, type='wizard')
|
|
1228
|
+
result = self._proxy.create(self._context)
|
|
1229
|
+
self.session_id, self.start_state, self.end_state = result
|
|
1230
|
+
self.states = [self.start_state]
|
|
1231
|
+
self.models = models
|
|
1232
|
+
self.action = action
|
|
1233
|
+
self.execute(self.start_state)
|
|
1234
|
+
|
|
1235
|
+
def execute(self, state):
|
|
1236
|
+
assert state in self.states
|
|
1237
|
+
|
|
1238
|
+
self.state = state
|
|
1239
|
+
while self.state != self.end_state:
|
|
1240
|
+
ctx = self._context.copy()
|
|
1241
|
+
if self.models:
|
|
1242
|
+
ctx['active_id'] = self.models[0].id
|
|
1243
|
+
ctx['active_ids'] = [model.id for model in self.models]
|
|
1244
|
+
ctx['active_model'] = self.models[0].__class__.__name__
|
|
1245
|
+
elif isinstance(self.models, ModelList):
|
|
1246
|
+
ctx['active_id'] = None
|
|
1247
|
+
ctx['active_ids'] = None
|
|
1248
|
+
ctx['active_model'] = self.models.model_name
|
|
1249
|
+
else:
|
|
1250
|
+
ctx['active_id'] = None
|
|
1251
|
+
ctx['active_ids'] = None
|
|
1252
|
+
ctx['active_model'] = None
|
|
1253
|
+
if self.action:
|
|
1254
|
+
ctx['action_id'] = self.action['id']
|
|
1255
|
+
else:
|
|
1256
|
+
ctx['action_id'] = None
|
|
1257
|
+
|
|
1258
|
+
if self.form:
|
|
1259
|
+
data = {self.form_state: self.form._get_on_change_values()}
|
|
1260
|
+
else:
|
|
1261
|
+
data = {}
|
|
1262
|
+
|
|
1263
|
+
result = self._proxy.execute(self.session_id, data, self.state,
|
|
1264
|
+
ctx)
|
|
1265
|
+
|
|
1266
|
+
if 'view' in result:
|
|
1267
|
+
view = result['view']
|
|
1268
|
+
self.form = Model.get(
|
|
1269
|
+
view['fields_view']['model'], self._config)(_default=False)
|
|
1270
|
+
if 'defaults' in view:
|
|
1271
|
+
self.form._default_set(view['defaults'])
|
|
1272
|
+
if 'values' in view:
|
|
1273
|
+
self.form._set(view['values'])
|
|
1274
|
+
self.states = [b['state'] for b in view['buttons']]
|
|
1275
|
+
self.form_state = view['state']
|
|
1276
|
+
else:
|
|
1277
|
+
self.state = self.end_state
|
|
1278
|
+
|
|
1279
|
+
self.actions = []
|
|
1280
|
+
for action in result.get('actions', []):
|
|
1281
|
+
proteus_action = _convert_action(
|
|
1282
|
+
*action, context=self._context, config=self._config)
|
|
1283
|
+
if proteus_action is not None:
|
|
1284
|
+
self.actions.append(proteus_action)
|
|
1285
|
+
|
|
1286
|
+
if 'view' in result:
|
|
1287
|
+
return
|
|
1288
|
+
|
|
1289
|
+
if self.state == self.end_state:
|
|
1290
|
+
self._proxy.delete(self.session_id, self._context)
|
|
1291
|
+
if self.models:
|
|
1292
|
+
for record in self.models:
|
|
1293
|
+
record.reload()
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
class Report(object):
|
|
1297
|
+
'Report class for Tryton reports'
|
|
1298
|
+
__slots__ = ('name', '_config', '_context', '_proxy')
|
|
1299
|
+
|
|
1300
|
+
def __init__(self, name, config=None, context=None):
|
|
1301
|
+
super().__init__()
|
|
1302
|
+
self.name = name
|
|
1303
|
+
self._config = config or proteus.config.get_config()
|
|
1304
|
+
self._context = self._config.context
|
|
1305
|
+
if context:
|
|
1306
|
+
self._context.update(context)
|
|
1307
|
+
self._proxy = self._config.get_proxy(name, type='report')
|
|
1308
|
+
|
|
1309
|
+
def execute(self, models=None, data=None):
|
|
1310
|
+
if models:
|
|
1311
|
+
ids = [m.id for m in models]
|
|
1312
|
+
elif data:
|
|
1313
|
+
ids = data.get('ids', [])
|
|
1314
|
+
else:
|
|
1315
|
+
ids = []
|
|
1316
|
+
if data is None:
|
|
1317
|
+
data = {
|
|
1318
|
+
'id': ids[0] if ids else None,
|
|
1319
|
+
'ids': ids,
|
|
1320
|
+
}
|
|
1321
|
+
if models:
|
|
1322
|
+
data['model'] = models[0].__class__.__name__
|
|
1323
|
+
return self._proxy.execute(ids, data, self._context)
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def launch_action(xml_id, records, context=None, config=None):
|
|
1327
|
+
if records:
|
|
1328
|
+
assert len({type(x) for x in records}) == 1
|
|
1329
|
+
if context is None:
|
|
1330
|
+
context = {}
|
|
1331
|
+
|
|
1332
|
+
if isinstance(xml_id, str):
|
|
1333
|
+
ModelData = Model.get('ir.model.data')
|
|
1334
|
+
|
|
1335
|
+
if not config:
|
|
1336
|
+
config = proteus.config.get_config()
|
|
1337
|
+
context = config.context
|
|
1338
|
+
|
|
1339
|
+
action_id = ModelData.get_id(*xml_id.split('.'), context)
|
|
1340
|
+
elif isinstance(xml_id, int):
|
|
1341
|
+
action_id = xml_id
|
|
1342
|
+
|
|
1343
|
+
Action = Model.get('ir.action')
|
|
1344
|
+
action = Action.get_action_value(action_id, context)
|
|
1345
|
+
return _convert_action(action, records, context=context, config=config)
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def _convert_action(action, data=None, *, context=None, config=None):
|
|
1349
|
+
if config is None:
|
|
1350
|
+
config = proteus.config.get_config()
|
|
1351
|
+
records = None
|
|
1352
|
+
if data is None:
|
|
1353
|
+
data = {}
|
|
1354
|
+
elif isinstance(data, (list, tuple)):
|
|
1355
|
+
records = data
|
|
1356
|
+
data = {
|
|
1357
|
+
'model': records[0].__class__.__name__,
|
|
1358
|
+
'id': records[0].id,
|
|
1359
|
+
'ids': [r.id for r in records],
|
|
1360
|
+
}
|
|
1361
|
+
else:
|
|
1362
|
+
data = data.copy()
|
|
1363
|
+
|
|
1364
|
+
if 'type' not in (action or {}):
|
|
1365
|
+
return None
|
|
1366
|
+
|
|
1367
|
+
data['action_id'] = action['id']
|
|
1368
|
+
if action['type'] == 'ir.action.act_window':
|
|
1369
|
+
from .pyson import PYSONDecoder
|
|
1370
|
+
|
|
1371
|
+
action.setdefault('pyson_domain', '[]')
|
|
1372
|
+
ctx = {
|
|
1373
|
+
'active_model': data.get('model'),
|
|
1374
|
+
'active_id': data.get('id'),
|
|
1375
|
+
'active_ids': data.get('ids', []),
|
|
1376
|
+
}
|
|
1377
|
+
ctx.update(config.context)
|
|
1378
|
+
ctx['_user'] = config.user
|
|
1379
|
+
decoder = PYSONDecoder(ctx)
|
|
1380
|
+
action_ctx = decoder.decode(action.get('pyson_context') or '{}')
|
|
1381
|
+
ctx.update(action_ctx)
|
|
1382
|
+
ctx.update(context)
|
|
1383
|
+
action_ctx.update(context)
|
|
1384
|
+
if 'date_format' not in action_ctx:
|
|
1385
|
+
action_ctx['date_format'] = config.context.get(
|
|
1386
|
+
'locale', {}).get('date', '%x')
|
|
1387
|
+
|
|
1388
|
+
ctx['context'] = ctx
|
|
1389
|
+
decoder = PYSONDecoder(ctx)
|
|
1390
|
+
domain = decoder.decode(action['pyson_domain'])
|
|
1391
|
+
|
|
1392
|
+
res_model = action.get('res_model', data.get('res_model'))
|
|
1393
|
+
res_id = action.get('res_id', data.get('res_id'))
|
|
1394
|
+
Model_ = Model.get(res_model, config)
|
|
1395
|
+
config = Model_._config
|
|
1396
|
+
with config.reset_context(), config.set_context(action_ctx):
|
|
1397
|
+
if res_id is None:
|
|
1398
|
+
return Model_.find(domain)
|
|
1399
|
+
elif isinstance(res_id, int):
|
|
1400
|
+
return [Model_(res_id)]
|
|
1401
|
+
else:
|
|
1402
|
+
return [Model_(id_) for id_ in res_id]
|
|
1403
|
+
elif action['type'] == 'ir.action.wizard':
|
|
1404
|
+
kwargs = {
|
|
1405
|
+
'action': action,
|
|
1406
|
+
'config': config,
|
|
1407
|
+
'context': context,
|
|
1408
|
+
}
|
|
1409
|
+
if records is not None:
|
|
1410
|
+
kwargs['models'] = records
|
|
1411
|
+
elif 'model' in data:
|
|
1412
|
+
Model_ = Model.get(data['model'], config)
|
|
1413
|
+
config = Model_._config
|
|
1414
|
+
with config.reset_context(), config.set_context(context):
|
|
1415
|
+
kwargs['models'] = [Model_(id_) for id_ in data.get('ids', [])]
|
|
1416
|
+
return Wizard(action['wiz_name'], **kwargs)
|
|
1417
|
+
elif action['type'] == 'ir.action.report':
|
|
1418
|
+
ActionReport = Report(action['report_name'], context=context)
|
|
1419
|
+
return ActionReport.execute(data=data)
|
|
1420
|
+
elif action['type'] == 'ir.action.url':
|
|
1421
|
+
return action.get('url')
|