tryton 7.0.21__py3-none-any.whl → 7.2.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.
Potentially problematic release.
This version of tryton might be problematic. Click here for more details.
- tryton/__init__.py +1 -1
- tryton/cache.py +34 -0
- tryton/common/common.py +125 -69
- tryton/common/completion.py +2 -2
- tryton/common/domain_inversion.py +1 -2
- tryton/common/domain_parser.py +7 -17
- tryton/common/selection.py +6 -3
- tryton/common/tempfile.py +34 -0
- tryton/config.py +3 -2
- tryton/data/locale/bg/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/bg/LC_MESSAGES/tryton.po +28 -3
- tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ca/LC_MESSAGES/tryton.po +33 -5
- tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/cs/LC_MESSAGES/tryton.po +28 -3
- tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/de/LC_MESSAGES/tryton.po +32 -4
- tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/es/LC_MESSAGES/tryton.po +32 -4
- tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/es_419/LC_MESSAGES/tryton.po +29 -4
- tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/et/LC_MESSAGES/tryton.po +32 -5
- tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fa/LC_MESSAGES/tryton.po +32 -6
- tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fi/LC_MESSAGES/tryton.po +28 -3
- tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fr/LC_MESSAGES/tryton.po +32 -4
- tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/hu/LC_MESSAGES/tryton.po +32 -5
- tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/id/LC_MESSAGES/tryton.po +30 -3
- tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/it/LC_MESSAGES/tryton.po +31 -5
- tryton/data/locale/ja_JP/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/lo/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/lo/LC_MESSAGES/tryton.po +31 -5
- tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/lt/LC_MESSAGES/tryton.po +32 -5
- tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/nl/LC_MESSAGES/tryton.po +32 -4
- tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/pl/LC_MESSAGES/tryton.po +32 -5
- tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/pt/LC_MESSAGES/tryton.po +31 -5
- tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ro/LC_MESSAGES/tryton.po +43 -16
- tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ru/LC_MESSAGES/tryton.po +29 -5
- tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/sl/LC_MESSAGES/tryton.po +32 -4
- tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/tr/LC_MESSAGES/tryton.po +28 -3
- tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/uk/LC_MESSAGES/tryton.po +32 -5
- tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +32 -4
- tryton/device_cookie.py +1 -1
- tryton/gui/main.py +3 -2
- tryton/gui/window/about.py +1 -1
- tryton/gui/window/dblogin.py +2 -2
- tryton/gui/window/email_.py +1 -1
- tryton/gui/window/form.py +4 -3
- tryton/gui/window/log.py +24 -2
- tryton/gui/window/view_form/model/field.py +56 -62
- tryton/gui/window/view_form/model/group.py +3 -1
- tryton/gui/window/view_form/model/record.py +55 -16
- tryton/gui/window/view_form/screen/screen.py +22 -22
- tryton/gui/window/view_form/view/calendar_gtk/calendar_.py +7 -12
- tryton/gui/window/view_form/view/form.py +4 -14
- tryton/gui/window/view_form/view/form_gtk/binary.py +3 -3
- tryton/gui/window/view_form/view/form_gtk/dictionary.py +33 -27
- tryton/gui/window/view_form/view/form_gtk/document.py +10 -9
- tryton/gui/window/view_form/view/form_gtk/many2many.py +17 -7
- tryton/gui/window/view_form/view/form_gtk/many2one.py +21 -13
- tryton/gui/window/view_form/view/form_gtk/one2many.py +25 -6
- tryton/gui/window/view_form/view/form_gtk/state_widget.py +6 -2
- tryton/gui/window/view_form/view/list.py +47 -56
- tryton/gui/window/view_form/view/list_gtk/widget.py +36 -23
- tryton/gui/window/view_form/view/screen_container.py +5 -3
- tryton/gui/window/win_export.py +1 -2
- tryton/gui/window/win_form.py +6 -8
- tryton/gui/window/wizard.py +11 -10
- tryton/jsonrpc.py +41 -27
- tryton/pyson.py +54 -4
- tryton/rpc.py +18 -0
- tryton/tests/test_common_domain_parser.py +0 -8
- {tryton-7.0.21.data → tryton-7.2.0.data}/scripts/tryton +1 -2
- {tryton-7.0.21.dist-info → tryton-7.2.0.dist-info}/METADATA +6 -20
- {tryton-7.0.21.dist-info → tryton-7.2.0.dist-info}/RECORD +94 -92
- {tryton-7.0.21.dist-info → tryton-7.2.0.dist-info}/WHEEL +1 -1
- {tryton-7.0.21.dist-info → tryton-7.2.0.dist-info}/LICENSE +0 -0
- {tryton-7.0.21.dist-info → tryton-7.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
2
2
|
# this repository contains the full copyright notices and license terms.
|
|
3
|
+
import functools
|
|
3
4
|
import logging
|
|
5
|
+
import operator
|
|
4
6
|
|
|
5
7
|
import tryton.common as common
|
|
6
8
|
from tryton.common import RPCException, RPCExecute
|
|
@@ -12,6 +14,30 @@ from . import field as fields
|
|
|
12
14
|
logger = logging.getLogger(__name__)
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
def get_x2m_sub_fields(f_attrs, prefix):
|
|
18
|
+
if f_attrs['loading'] == 'eager' and f_attrs.get('views'):
|
|
19
|
+
sub_fields = functools.reduce(
|
|
20
|
+
operator.or_,
|
|
21
|
+
(v.get('fields', {}) for v in f_attrs['views'].values()),
|
|
22
|
+
{})
|
|
23
|
+
x2m_sub_fields = []
|
|
24
|
+
for s_field, f_def in sub_fields.items():
|
|
25
|
+
x2m_sub_fields.append(f"{prefix}.{s_field}")
|
|
26
|
+
if f_def['type'] in {'many2one', 'one2one', 'reference'}:
|
|
27
|
+
x2m_sub_fields.append(f"{prefix}.{s_field}.rec_name")
|
|
28
|
+
elif f_def['type'] in {'selection', 'multiselection'}:
|
|
29
|
+
x2m_sub_fields.append(f"{prefix}.{s_field}:string")
|
|
30
|
+
elif f_def['type'] in {'one2many', 'many2many'}:
|
|
31
|
+
x2m_sub_fields.extend(
|
|
32
|
+
get_x2m_sub_fields(f_def, f"{prefix}.{s_field}"))
|
|
33
|
+
x2m_sub_fields.extend(
|
|
34
|
+
f"{prefix}.{f}"
|
|
35
|
+
for f in ['_timestamp', '_write', '_delete'])
|
|
36
|
+
return x2m_sub_fields
|
|
37
|
+
else:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
|
|
15
41
|
class Record:
|
|
16
42
|
|
|
17
43
|
id = -1
|
|
@@ -79,6 +105,7 @@ class Record:
|
|
|
79
105
|
fnames = [fname for fname, field in fields
|
|
80
106
|
if fname not in self._loaded
|
|
81
107
|
and (not views or (views & field.views))]
|
|
108
|
+
related_read_limit = 0
|
|
82
109
|
for fname in list(fnames):
|
|
83
110
|
f_attrs = self.group.fields[fname].attrs
|
|
84
111
|
if f_attrs['type'] in {'many2one', 'one2one', 'reference'}:
|
|
@@ -86,6 +113,11 @@ class Record:
|
|
|
86
113
|
elif (f_attrs['type'] in {'selection', 'multiselection'}
|
|
87
114
|
and f_attrs.get('loading', 'lazy') == 'eager'):
|
|
88
115
|
fnames.append('%s:string' % fname)
|
|
116
|
+
elif f_attrs['type'] in {'many2many', 'one2many'}:
|
|
117
|
+
sub_fields = get_x2m_sub_fields(f_attrs, fname)
|
|
118
|
+
fnames.extend(sub_fields)
|
|
119
|
+
if sub_fields:
|
|
120
|
+
related_read_limit += len(sub_fields)
|
|
89
121
|
if 'rec_name' not in fnames:
|
|
90
122
|
fnames.append('rec_name')
|
|
91
123
|
fnames.extend(['_timestamp', '_write', '_delete'])
|
|
@@ -132,22 +164,24 @@ class Record:
|
|
|
132
164
|
ctx.update(dict(('%s.%s' % (self.model_name, fname), 'size')
|
|
133
165
|
for fname, field in self.group.fields.items()
|
|
134
166
|
if field.attrs['type'] == 'binary' and fname in fnames))
|
|
167
|
+
if related_read_limit:
|
|
168
|
+
ctx['related_read_limit'] = (
|
|
169
|
+
CONFIG['client.limit'] // min(related_read_limit, 10))
|
|
170
|
+
exception = False
|
|
135
171
|
try:
|
|
136
172
|
values = RPCExecute('model', self.model_name, 'read',
|
|
137
173
|
list(id2record.keys()), fnames, context=ctx,
|
|
138
174
|
process_exception=process_exception)
|
|
139
175
|
except RPCException:
|
|
140
|
-
for
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
for value in values:
|
|
146
|
-
value.update(default_values)
|
|
147
|
-
else:
|
|
148
|
-
raise
|
|
176
|
+
values = [{'id': x} for x in id2record]
|
|
177
|
+
default_values = dict((f, None) for f in fnames)
|
|
178
|
+
for value in values:
|
|
179
|
+
value.update(default_values)
|
|
180
|
+
self.exception = exception = True
|
|
149
181
|
id2value = dict((value['id'], value) for value in values)
|
|
150
182
|
for id, record in id2record.items():
|
|
183
|
+
if not record.exception:
|
|
184
|
+
record.exception = exception
|
|
151
185
|
value = id2value.get(id)
|
|
152
186
|
if record and not record.destroyed and value:
|
|
153
187
|
for key in record.modified_fields:
|
|
@@ -376,7 +410,6 @@ class Record:
|
|
|
376
410
|
return self.id
|
|
377
411
|
|
|
378
412
|
def default_get(self, defaults=None):
|
|
379
|
-
vals = {}
|
|
380
413
|
if len(self.group.fields):
|
|
381
414
|
context = self.get_context()
|
|
382
415
|
if defaults is not None:
|
|
@@ -386,7 +419,7 @@ class Record:
|
|
|
386
419
|
vals = RPCExecute('model', self.model_name, 'default_get',
|
|
387
420
|
list(self.group.fields.keys()), context=context)
|
|
388
421
|
except RPCException:
|
|
389
|
-
return
|
|
422
|
+
return
|
|
390
423
|
if (self.parent
|
|
391
424
|
and self.parent_name in self.group.fields):
|
|
392
425
|
parent_field = self.group.fields[self.parent_name]
|
|
@@ -498,7 +531,9 @@ class Record:
|
|
|
498
531
|
self._loaded.add(fieldname)
|
|
499
532
|
fieldnames.append(fieldname)
|
|
500
533
|
for fieldname, value in later.items():
|
|
501
|
-
|
|
534
|
+
if isinstance(
|
|
535
|
+
field := self.group.fields[fieldname], fields.O2MField):
|
|
536
|
+
field.set(self, value, preloaded=val.get(f"{fieldname}."))
|
|
502
537
|
self._loaded.add(fieldname)
|
|
503
538
|
fieldnames.append(fieldname)
|
|
504
539
|
if validate:
|
|
@@ -682,7 +717,8 @@ class Record:
|
|
|
682
717
|
try:
|
|
683
718
|
res = RPCExecute(
|
|
684
719
|
'model', self.model_name,
|
|
685
|
-
'autocomplete_' + fieldname, args, context=self.get_context()
|
|
720
|
+
'autocomplete_' + fieldname, args, context=self.get_context(),
|
|
721
|
+
process_exception=False)
|
|
686
722
|
except RPCException:
|
|
687
723
|
# ensure res is a list
|
|
688
724
|
res = []
|
|
@@ -691,9 +727,12 @@ class Record:
|
|
|
691
727
|
def on_scan_code(self, code, depends):
|
|
692
728
|
depends = self.expr_eval(depends)
|
|
693
729
|
values = self._get_on_change_args(depends)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
730
|
+
try:
|
|
731
|
+
changes = RPCExecute(
|
|
732
|
+
'model', self.model_name, 'on_scan_code', values, code,
|
|
733
|
+
context=self.get_context(), process_exception=False)
|
|
734
|
+
except RPCException:
|
|
735
|
+
changes = []
|
|
697
736
|
self.set_on_change(changes)
|
|
698
737
|
self.set_modified()
|
|
699
738
|
return bool(changes)
|
|
@@ -75,8 +75,7 @@ class Screen:
|
|
|
75
75
|
self.__current_record = None
|
|
76
76
|
self.new_group(context or {})
|
|
77
77
|
self.current_record = None
|
|
78
|
-
self.screen_container = ScreenContainer(
|
|
79
|
-
self, attributes.get('tab_domain'))
|
|
78
|
+
self.screen_container = ScreenContainer(attributes.get('tab_domain'))
|
|
80
79
|
self.screen_container.alternate_view = attributes.get(
|
|
81
80
|
'alternate_view', False)
|
|
82
81
|
self.widget = self.screen_container.widget_get()
|
|
@@ -164,6 +163,7 @@ class Screen:
|
|
|
164
163
|
|
|
165
164
|
def search_active(self, active=True):
|
|
166
165
|
if active and not self.parent:
|
|
166
|
+
self.screen_container.set_screen(self)
|
|
167
167
|
self.screen_container.show_filter()
|
|
168
168
|
else:
|
|
169
169
|
self.screen_container.hide_filter()
|
|
@@ -307,7 +307,8 @@ class Screen:
|
|
|
307
307
|
try:
|
|
308
308
|
self.search_count = RPCExecute(
|
|
309
309
|
'model', self.model_name, 'search_count',
|
|
310
|
-
domain, 0, self.count_limit, context=context
|
|
310
|
+
domain, 0, self.count_limit, context=context,
|
|
311
|
+
process_exception=False)
|
|
311
312
|
except RPCException:
|
|
312
313
|
self.search_count = 0
|
|
313
314
|
else:
|
|
@@ -536,6 +537,10 @@ class Screen:
|
|
|
536
537
|
def number_of_views(self):
|
|
537
538
|
return len(self.views) + len(self.view_to_load)
|
|
538
539
|
|
|
540
|
+
@property
|
|
541
|
+
def view_index(self):
|
|
542
|
+
return self.__current_view
|
|
543
|
+
|
|
539
544
|
def switch_view(
|
|
540
545
|
self, view_type=None, view_id=None, creatable=None, display=True):
|
|
541
546
|
if view_id is not None:
|
|
@@ -800,9 +805,6 @@ class Screen:
|
|
|
800
805
|
record.group.remove(
|
|
801
806
|
record, remove=remove, modified=False,
|
|
802
807
|
force_remove=force_remove)
|
|
803
|
-
# set current_record to None to prevent __select_changed
|
|
804
|
-
# to set deleted record as current_record
|
|
805
|
-
self.current_record = None
|
|
806
808
|
# call only once
|
|
807
809
|
record.set_modified()
|
|
808
810
|
|
|
@@ -852,12 +854,11 @@ class Screen:
|
|
|
852
854
|
parent = self.parent.id if self.parent else None
|
|
853
855
|
if parent is not None and parent < 0:
|
|
854
856
|
return
|
|
855
|
-
|
|
856
|
-
if state:
|
|
857
|
-
expanded_nodes, selected_nodes = state
|
|
858
|
-
else:
|
|
859
|
-
expanded_nodes, selected_nodes = [], []
|
|
857
|
+
expanded_nodes, selected_nodes = [], []
|
|
860
858
|
if view.view_type in {'tree', 'list-form'}:
|
|
859
|
+
state = self.tree_states[parent][view.children_field]
|
|
860
|
+
if state:
|
|
861
|
+
expanded_nodes, selected_nodes = state
|
|
861
862
|
if (state is None
|
|
862
863
|
and CONFIG['client.save_tree_state']
|
|
863
864
|
and int(view.attributes.get('tree_state', False))):
|
|
@@ -905,8 +906,7 @@ class Screen:
|
|
|
905
906
|
for widget in widgets:
|
|
906
907
|
if hasattr(widget, 'screen'):
|
|
907
908
|
widget.screen.save_tree_state(store)
|
|
908
|
-
|
|
909
|
-
if self.current_record:
|
|
909
|
+
if len(self.views) == 1 and self.current_record:
|
|
910
910
|
path = self.current_record.id
|
|
911
911
|
if path < 0:
|
|
912
912
|
path = -self.current_record.group.index(
|
|
@@ -920,9 +920,8 @@ class Screen:
|
|
|
920
920
|
else:
|
|
921
921
|
paths = []
|
|
922
922
|
selected_paths = view.get_selected_paths()
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
paths, selected_paths)
|
|
923
|
+
self.tree_states[parent][view.children_field] = (
|
|
924
|
+
paths, selected_paths)
|
|
926
925
|
if (store
|
|
927
926
|
and int(view.attributes.get('tree_state', False))
|
|
928
927
|
and CONFIG['client.save_tree_state']):
|
|
@@ -936,7 +935,7 @@ class Screen:
|
|
|
936
935
|
json_paths, json_selected_path,
|
|
937
936
|
process_exception=False)
|
|
938
937
|
clear_cache('model.ir.ui.view_tree_state.get')
|
|
939
|
-
except
|
|
938
|
+
except RPCException:
|
|
940
939
|
logger.warn(
|
|
941
940
|
_('Unable to set view tree state'), exc_info=True)
|
|
942
941
|
|
|
@@ -1169,6 +1168,8 @@ class Screen:
|
|
|
1169
1168
|
def selected_paths(self):
|
|
1170
1169
|
if self.current_view and self.current_view.view_type == 'tree':
|
|
1171
1170
|
return self.current_view.get_selected_paths()
|
|
1171
|
+
else:
|
|
1172
|
+
return []
|
|
1172
1173
|
|
|
1173
1174
|
@property
|
|
1174
1175
|
def listed_records(self):
|
|
@@ -1237,7 +1238,6 @@ class Screen:
|
|
|
1237
1238
|
record_id = self.current_record.save(force_reload=False)
|
|
1238
1239
|
if record_id is False or record_id < 0:
|
|
1239
1240
|
return
|
|
1240
|
-
if button.get('type', 'class') == 'class':
|
|
1241
1241
|
self._button_class(button)
|
|
1242
1242
|
else:
|
|
1243
1243
|
self._button_instance(button)
|
|
@@ -1329,13 +1329,13 @@ class Screen:
|
|
|
1329
1329
|
if name:
|
|
1330
1330
|
query_string.append(
|
|
1331
1331
|
('name', json.dumps(name, separators=(',', ':'))))
|
|
1332
|
-
if self.screen_container.tab_domain:
|
|
1333
|
-
query_string.append(('tab_domain', json.dumps(
|
|
1334
|
-
self.screen_container.tab_domain,
|
|
1335
|
-
cls=JSONEncoder, separators=(',', ':'))))
|
|
1336
1332
|
path = [CONFIG['login.db'], 'model', self.model_name]
|
|
1337
1333
|
view_ids = [v.view_id for v in self.views] + self.view_ids
|
|
1338
1334
|
if self.current_view and self.current_view.view_type != 'form':
|
|
1335
|
+
if self.screen_container.tab_domain:
|
|
1336
|
+
query_string.append(('tab_domain', json.dumps(
|
|
1337
|
+
self.screen_container.tab_domain,
|
|
1338
|
+
cls=JSONEncoder, separators=(',', ':'))))
|
|
1339
1339
|
if self.search_value:
|
|
1340
1340
|
search_value = self.search_value
|
|
1341
1341
|
else:
|
|
@@ -67,18 +67,13 @@ class Calendar_(goocalendar.Calendar):
|
|
|
67
67
|
self.current_domain_period.get_dates(True)
|
|
68
68
|
dtstart = self.attrs['dtstart']
|
|
69
69
|
dtend = self.attrs.get('dtend') or dtstart
|
|
70
|
-
domain = [
|
|
71
|
-
(dtstart, '
|
|
72
|
-
|
|
73
|
-
['
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
(dtend, '<', last_datetime)],
|
|
78
|
-
['AND', (dtstart, '<', first_datetime),
|
|
79
|
-
(dtend, '>', last_datetime)],
|
|
80
|
-
],
|
|
81
|
-
]
|
|
70
|
+
domain = ['OR',
|
|
71
|
+
['AND', (dtstart, '>=', first_datetime),
|
|
72
|
+
(dtstart, '<', last_datetime)],
|
|
73
|
+
['AND', (dtend, '>=', first_datetime),
|
|
74
|
+
(dtend, '<', last_datetime)],
|
|
75
|
+
['AND', (dtstart, '<', first_datetime),
|
|
76
|
+
(dtend, '>', last_datetime)]]
|
|
82
77
|
return domain
|
|
83
78
|
|
|
84
79
|
def get_colors(self, record):
|
|
@@ -230,6 +230,9 @@ class FormXMLViewParser(XMLViewParser):
|
|
|
230
230
|
self.container.add(None, attributes)
|
|
231
231
|
return
|
|
232
232
|
|
|
233
|
+
if attributes.get('loading') == 'eager':
|
|
234
|
+
self.field_attrs[name]['loading'] = 'eager'
|
|
235
|
+
|
|
233
236
|
widget = self.WIDGETS[attributes['widget']](self.view, attributes)
|
|
234
237
|
self.view.widgets[name].append(widget)
|
|
235
238
|
|
|
@@ -340,18 +343,7 @@ class FormXMLViewParser(XMLViewParser):
|
|
|
340
343
|
int(attributes.get('width', -1)),
|
|
341
344
|
int(attributes.get('height', -1)))
|
|
342
345
|
|
|
343
|
-
# Force to display the first time it switches on a page
|
|
344
|
-
# This avoids glitch in position of widgets
|
|
345
|
-
def switch(notebook, page, page_num):
|
|
346
|
-
if not self.view.widget:
|
|
347
|
-
# Not yet finish to parse
|
|
348
|
-
return
|
|
349
|
-
notebook.grab_focus()
|
|
350
|
-
self.view.display()
|
|
351
|
-
notebook.disconnect(handler_id)
|
|
352
|
-
handler_id = notebook.connect('switch-page', switch)
|
|
353
346
|
self.view.state_widgets.append(notebook)
|
|
354
|
-
|
|
355
347
|
self.view.notebooks.append(notebook)
|
|
356
348
|
self.container.add(notebook, attributes)
|
|
357
349
|
self.parse_child(node, notebook)
|
|
@@ -390,6 +382,7 @@ class FormXMLViewParser(XMLViewParser):
|
|
|
390
382
|
group = Container.constructor(
|
|
391
383
|
int(attributes.get('col', 4)),
|
|
392
384
|
attributes.get('homogeneous', False))
|
|
385
|
+
self.parse_child(node, group)
|
|
393
386
|
|
|
394
387
|
if 'name' in attributes and attributes['name'] == self.exclude_field:
|
|
395
388
|
self.container.add(None, attributes)
|
|
@@ -413,9 +406,6 @@ class FormXMLViewParser(XMLViewParser):
|
|
|
413
406
|
bool(attributes.get('yexpand'))))
|
|
414
407
|
self.view.state_widgets.append(widget)
|
|
415
408
|
self.container.add(widget, attributes)
|
|
416
|
-
# Parse the children at the end to preserve the order of the state
|
|
417
|
-
# widgets
|
|
418
|
-
self.parse_child(node, group)
|
|
419
409
|
|
|
420
410
|
def _parse_hpaned(self, node, attributes):
|
|
421
411
|
self._parse_paned(node, attributes, Gtk.HPaned)
|
|
@@ -182,13 +182,13 @@ class Binary(BinaryMixin, Widget):
|
|
|
182
182
|
def sig_key_press(self, widget, event, *args):
|
|
183
183
|
editable = self.wid_text and self.wid_text.get_editable()
|
|
184
184
|
if event.keyval == Gdk.KEY_F3 and editable:
|
|
185
|
-
self.
|
|
185
|
+
self.sig_new(widget)
|
|
186
186
|
return True
|
|
187
187
|
elif event.keyval == Gdk.KEY_F2:
|
|
188
188
|
if self.filename:
|
|
189
|
-
self.
|
|
189
|
+
self.sig_open(widget)
|
|
190
190
|
else:
|
|
191
|
-
self.
|
|
191
|
+
self.sig_save_as(widget)
|
|
192
192
|
return True
|
|
193
193
|
return False
|
|
194
194
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# This file is part of Tryton. The COPYRIGHT file at the top level of this
|
|
2
2
|
# repository contains the full copyright notices and license terms.
|
|
3
3
|
|
|
4
|
-
import datetime as dt
|
|
5
4
|
import decimal
|
|
6
5
|
import gettext
|
|
7
6
|
import locale
|
|
@@ -65,7 +64,7 @@ class DictEntry(object):
|
|
|
65
64
|
return self.widget.get_text()
|
|
66
65
|
|
|
67
66
|
def set_value(self, value):
|
|
68
|
-
self.widget.set_text(
|
|
67
|
+
self.widget.set_text(value or '')
|
|
69
68
|
reset_position(self.widget)
|
|
70
69
|
|
|
71
70
|
def set_readonly(self, readonly):
|
|
@@ -242,10 +241,9 @@ class DictMultiSelectionEntry(DictEntry):
|
|
|
242
241
|
selection.handler_block_by_func(self._changed)
|
|
243
242
|
try:
|
|
244
243
|
selection.unselect_all()
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
selection.select_path(value2path[v])
|
|
244
|
+
for v in value:
|
|
245
|
+
if v in value2path:
|
|
246
|
+
selection.select_path(value2path[v])
|
|
249
247
|
finally:
|
|
250
248
|
selection.handler_unblock_by_func(self._changed)
|
|
251
249
|
|
|
@@ -280,7 +278,7 @@ class DictIntegerEntry(DictEntry):
|
|
|
280
278
|
return None
|
|
281
279
|
|
|
282
280
|
def set_value(self, value):
|
|
283
|
-
if
|
|
281
|
+
if value is not None:
|
|
284
282
|
txt_val = locale.format_string('%d', value, True)
|
|
285
283
|
else:
|
|
286
284
|
txt_val = ''
|
|
@@ -317,7 +315,7 @@ class DictFloatEntry(DictIntegerEntry):
|
|
|
317
315
|
else:
|
|
318
316
|
self.widget.digits = None
|
|
319
317
|
self.widget.set_width_chars(self.width)
|
|
320
|
-
if
|
|
318
|
+
if value is not None:
|
|
321
319
|
txt_val = locale.localize(
|
|
322
320
|
'{0:.{1}f}'.format(value, digits[1]), True)
|
|
323
321
|
else:
|
|
@@ -363,8 +361,7 @@ class DictDateTimeEntry(DictEntry):
|
|
|
363
361
|
return untimezoned_date(self.widget.props.value)
|
|
364
362
|
|
|
365
363
|
def set_value(self, value):
|
|
366
|
-
self.widget.props.value = (
|
|
367
|
-
timezoned_date(value) if isinstance(value, dt.datetime) else None)
|
|
364
|
+
self.widget.props.value = timezoned_date(value)
|
|
368
365
|
|
|
369
366
|
def set_readonly(self, readonly):
|
|
370
367
|
for child in self.widget.get_children():
|
|
@@ -399,7 +396,7 @@ class DictDateEntry(DictEntry):
|
|
|
399
396
|
return self.widget.props.value
|
|
400
397
|
|
|
401
398
|
def set_value(self, value):
|
|
402
|
-
self.widget.props.value = value
|
|
399
|
+
self.widget.props.value = value
|
|
403
400
|
|
|
404
401
|
def set_readonly(self, readonly):
|
|
405
402
|
super().set_readonly(readonly)
|
|
@@ -473,6 +470,7 @@ class DictWidget(Widget):
|
|
|
473
470
|
|
|
474
471
|
self._readonly = False
|
|
475
472
|
self._record_id = None
|
|
473
|
+
self._popup = False
|
|
476
474
|
|
|
477
475
|
@property
|
|
478
476
|
def _invalid_widget(self):
|
|
@@ -494,10 +492,16 @@ class DictWidget(Widget):
|
|
|
494
492
|
value = self.wid_text.get_text()
|
|
495
493
|
domain = self.field.domain_get(self.record)
|
|
496
494
|
|
|
495
|
+
if self._popup:
|
|
496
|
+
return
|
|
497
|
+
else:
|
|
498
|
+
self._popup = True
|
|
499
|
+
|
|
497
500
|
def callback(result):
|
|
498
501
|
if result:
|
|
499
502
|
self.add_new_keys([r[0] for r in result])
|
|
500
503
|
self.wid_text.set_text('')
|
|
504
|
+
self._popup = False
|
|
501
505
|
|
|
502
506
|
win = WinSearch(self.schema_model, callback, sel_multi=True,
|
|
503
507
|
context=context, domain=domain, new=False)
|
|
@@ -507,15 +511,13 @@ class DictWidget(Widget):
|
|
|
507
511
|
def add_new_keys(self, ids):
|
|
508
512
|
new_keys = self.field.add_new_keys(ids, self.record)
|
|
509
513
|
self.send_modified()
|
|
510
|
-
|
|
511
|
-
for
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
GLib.idle_add(self.fields[key_name].widget.grab_focus)
|
|
518
|
-
focus = True
|
|
514
|
+
value = self.field.get_client(self.record)
|
|
515
|
+
value.update({k: None for k in new_keys})
|
|
516
|
+
self.display()
|
|
517
|
+
|
|
518
|
+
# Use idle add because it can be called from the callback
|
|
519
|
+
# of WinSearch while the popup is still there
|
|
520
|
+
GLib.idle_add(self.fields[new_keys[0]].widget.grab_focus)
|
|
519
521
|
|
|
520
522
|
def _sig_remove(self, button, key, modified=True):
|
|
521
523
|
self.fields[key].disconnect_signals()
|
|
@@ -561,7 +563,7 @@ class DictWidget(Widget):
|
|
|
561
563
|
not self._readonly
|
|
562
564
|
and self.attrs.get('delete', True)))
|
|
563
565
|
|
|
564
|
-
def add_line(self, key):
|
|
566
|
+
def add_line(self, key, position):
|
|
565
567
|
key_schema = self.field.keys[key]
|
|
566
568
|
self.fields[key] = DICT_ENTRIES[key_schema['type']](key, self)
|
|
567
569
|
field = self.fields[key]
|
|
@@ -569,8 +571,8 @@ class DictWidget(Widget):
|
|
|
569
571
|
label = Gtk.Label(
|
|
570
572
|
label=set_underline(text),
|
|
571
573
|
use_underline=True, halign=Gtk.Align.END)
|
|
572
|
-
self.grid.
|
|
573
|
-
|
|
574
|
+
self.grid.insert_row(position)
|
|
575
|
+
self.grid.attach(label, 0, position, 1, 1)
|
|
574
576
|
label.set_mnemonic_widget(field.widget)
|
|
575
577
|
label.show()
|
|
576
578
|
hbox = Gtk.HBox(hexpand=True)
|
|
@@ -607,6 +609,11 @@ class DictWidget(Widget):
|
|
|
607
609
|
self.field.add_keys(list(new_key_names), self.record)
|
|
608
610
|
decoder = PYSONDecoder()
|
|
609
611
|
|
|
612
|
+
# We remove first the old keys in order to keep the order when
|
|
613
|
+
# inserting the new ones
|
|
614
|
+
for key in set(self.fields.keys()) - set(value.keys()):
|
|
615
|
+
self._sig_remove(None, key, modified=False)
|
|
616
|
+
|
|
610
617
|
def filter_func(item):
|
|
611
618
|
key, value = item
|
|
612
619
|
return key in self.field.keys
|
|
@@ -615,9 +622,10 @@ class DictWidget(Widget):
|
|
|
615
622
|
key, value = item
|
|
616
623
|
return self.field.keys[key]['sequence'] or 0
|
|
617
624
|
|
|
618
|
-
for key, val in
|
|
625
|
+
for position, (key, val) in enumerate(
|
|
626
|
+
sorted(filter(filter_func, value.items()), key=key)):
|
|
619
627
|
if key not in self.fields:
|
|
620
|
-
self.add_line(key)
|
|
628
|
+
self.add_line(key, position)
|
|
621
629
|
widget = self.fields[key]
|
|
622
630
|
widget.set_value(val)
|
|
623
631
|
widget.set_readonly(self._readonly)
|
|
@@ -625,8 +633,6 @@ class DictWidget(Widget):
|
|
|
625
633
|
self.field.keys[key].get('domain') or '[]')
|
|
626
634
|
widget_class(
|
|
627
635
|
widget.widget, 'invalid', not eval_domain(key_domain, value))
|
|
628
|
-
for key in set(self.fields.keys()) - set(value.keys()):
|
|
629
|
-
self._sig_remove(None, key, modified=False)
|
|
630
636
|
|
|
631
637
|
self._set_button_sensitive()
|
|
632
638
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
2
2
|
# this repository contains the full copyright notices and license terms.
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from tempfile import NamedTemporaryFile
|
|
4
5
|
|
|
5
6
|
from gi.repository import Gdk, GLib, Gtk
|
|
6
7
|
|
|
@@ -75,17 +76,17 @@ class Document(BinaryMixin, Widget):
|
|
|
75
76
|
self.image.hide()
|
|
76
77
|
if self.evince_view:
|
|
77
78
|
self.evince_scroll.show()
|
|
78
|
-
suffix = None
|
|
79
79
|
if self.filename_field:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
filename = Path(self.field.get_filename(self.record, suffix))
|
|
80
|
+
suffix = self.filename_field.get(self.record)
|
|
81
|
+
else:
|
|
82
|
+
suffix = None
|
|
84
83
|
try:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
with NamedTemporaryFile(suffix=suffix) as fp:
|
|
85
|
+
fp.write(data)
|
|
86
|
+
path = Path(fp.name)
|
|
87
|
+
document = (
|
|
88
|
+
EvinceDocument.Document.factory_get_document(
|
|
89
|
+
path.as_uri()))
|
|
89
90
|
model = EvinceView.DocumentModel()
|
|
90
91
|
model.set_document(document)
|
|
91
92
|
self.evince_view.set_model(model)
|
|
@@ -48,7 +48,6 @@ class Many2Many(Widget):
|
|
|
48
48
|
self.wid_text.set_placeholder_text(_('Search'))
|
|
49
49
|
self.wid_text.set_property('width_chars', 13)
|
|
50
50
|
self.wid_text.connect('focus-out-event', self._focus_out)
|
|
51
|
-
self.focus_out = True
|
|
52
51
|
hbox.pack_start(self.wid_text, expand=True, fill=True, padding=0)
|
|
53
52
|
|
|
54
53
|
if int(self.attrs.get('completion', 1)):
|
|
@@ -113,6 +112,8 @@ class Many2Many(Widget):
|
|
|
113
112
|
self.screen.widget.connect('key_press_event', self.on_keypress)
|
|
114
113
|
self.wid_text.connect('key_press_event', self.on_keypress)
|
|
115
114
|
|
|
115
|
+
self._popup = False
|
|
116
|
+
|
|
116
117
|
def on_keypress(self, widget, event):
|
|
117
118
|
editable = self.wid_text.get_editable()
|
|
118
119
|
activate_keys = [Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab]
|
|
@@ -161,8 +162,6 @@ class Many2Many(Widget):
|
|
|
161
162
|
return int(self.attrs.get('create', 1)) and self.get_access('create')
|
|
162
163
|
|
|
163
164
|
def _sig_add(self, *args):
|
|
164
|
-
if not self.focus_out:
|
|
165
|
-
return
|
|
166
165
|
domain = self.field.domain_get(self.record)
|
|
167
166
|
add_remove = self.record.expr_eval(self.attrs.get('add_remove'))
|
|
168
167
|
if add_remove:
|
|
@@ -171,15 +170,18 @@ class Many2Many(Widget):
|
|
|
171
170
|
order = self.field.get_search_order(self.record)
|
|
172
171
|
value = self.wid_text.get_text()
|
|
173
172
|
|
|
174
|
-
self.
|
|
173
|
+
if self._popup:
|
|
174
|
+
return
|
|
175
|
+
else:
|
|
176
|
+
self._popup = True
|
|
175
177
|
|
|
176
178
|
def callback(result):
|
|
177
|
-
self.focus_out = True
|
|
178
179
|
if result:
|
|
179
180
|
ids = [x[0] for x in result]
|
|
180
181
|
self.screen.load(ids, modified=True)
|
|
181
182
|
self.screen.set_cursor()
|
|
182
183
|
self.wid_text.set_text('')
|
|
184
|
+
self._popup = False
|
|
183
185
|
win = WinSearch(self.attrs['relation'], callback, sel_multi=True,
|
|
184
186
|
context=context, domain=domain, order=order,
|
|
185
187
|
view_ids=self.attrs.get('view_ids', '').split(','),
|
|
@@ -215,6 +217,10 @@ class Many2Many(Widget):
|
|
|
215
217
|
def _sig_edit(self):
|
|
216
218
|
if not self.screen.current_record:
|
|
217
219
|
return
|
|
220
|
+
if self._popup:
|
|
221
|
+
return
|
|
222
|
+
else:
|
|
223
|
+
self._popup = True
|
|
218
224
|
# Create a new screen that is not linked to the parent otherwise on the
|
|
219
225
|
# save of the record will trigger the save of the parent
|
|
220
226
|
screen = self._get_screen_form()
|
|
@@ -231,22 +237,26 @@ class Many2Many(Widget):
|
|
|
231
237
|
self.screen.current_record.modified_fields.setdefault('id')
|
|
232
238
|
# Force a display to clear the CellCache
|
|
233
239
|
self.screen.display()
|
|
240
|
+
self._popup = False
|
|
234
241
|
WinForm(screen, callback)
|
|
235
242
|
|
|
236
243
|
def _sig_new(self, defaults=None):
|
|
244
|
+
if self._popup:
|
|
245
|
+
return
|
|
246
|
+
else:
|
|
247
|
+
self._popup = True
|
|
237
248
|
screen = self._get_screen_form()
|
|
238
249
|
defaults = defaults.copy() if defaults is not None else {}
|
|
239
250
|
defaults['rec_name'] = self.wid_text.get_text()
|
|
240
251
|
|
|
241
252
|
def callback(result):
|
|
242
|
-
self.focus_out = True
|
|
243
253
|
if result:
|
|
244
254
|
record = screen.current_record
|
|
245
255
|
self.screen.load([record.id], modified=True)
|
|
246
256
|
self.wid_text.set_text('')
|
|
247
257
|
self.wid_text.grab_focus()
|
|
258
|
+
self._popup = False
|
|
248
259
|
|
|
249
|
-
self.focus_out = False
|
|
250
260
|
WinForm(
|
|
251
261
|
screen, callback, new=True, save_current=True, defaults=defaults)
|
|
252
262
|
|