tryton 7.0.7__py3-none-any.whl → 7.4.4__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 +149 -73
- tryton/common/completion.py +2 -2
- tryton/common/datetime_.py +3 -1
- tryton/common/domain_inversion.py +2 -1
- tryton/common/domain_parser.py +22 -11
- tryton/common/popup_menu.py +1 -1
- tryton/common/selection.py +6 -3
- tryton/common/tempfile.py +34 -0
- tryton/config.py +4 -5
- tryton/data/locale/bg/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/bg/LC_MESSAGES/tryton.po +69 -20
- tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ca/LC_MESSAGES/tryton.po +70 -25
- tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/cs/LC_MESSAGES/tryton.po +68 -21
- tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/de/LC_MESSAGES/tryton.po +71 -26
- tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/es/LC_MESSAGES/tryton.po +68 -23
- tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/es_419/LC_MESSAGES/tryton.po +72 -22
- tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/et/LC_MESSAGES/tryton.po +73 -23
- tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fa/LC_MESSAGES/tryton.po +74 -25
- tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fi/LC_MESSAGES/tryton.po +63 -20
- tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fr/LC_MESSAGES/tryton.po +73 -28
- tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/hu/LC_MESSAGES/tryton.po +75 -23
- tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/id/LC_MESSAGES/tryton.po +72 -23
- tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/it/LC_MESSAGES/tryton.po +73 -24
- 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 +74 -25
- tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/lt/LC_MESSAGES/tryton.po +73 -23
- tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/nl/LC_MESSAGES/tryton.po +72 -27
- tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/pl/LC_MESSAGES/tryton.po +113 -78
- tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/pt/LC_MESSAGES/tryton.po +73 -24
- tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ro/LC_MESSAGES/tryton.po +87 -36
- tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ru/LC_MESSAGES/tryton.po +72 -25
- tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/sl/LC_MESSAGES/tryton.po +77 -26
- tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/tr/LC_MESSAGES/tryton.po +64 -20
- tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/uk/LC_MESSAGES/tryton.po +75 -23
- tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +76 -24
- tryton/device_cookie.py +1 -1
- tryton/gui/main.py +14 -12
- tryton/gui/window/about.py +1 -1
- tryton/gui/window/dblogin.py +2 -2
- tryton/gui/window/email_.py +2 -2
- tryton/gui/window/form.py +10 -5
- tryton/gui/window/log.py +24 -2
- tryton/gui/window/tabcontent.py +2 -2
- tryton/gui/window/view_form/model/field.py +84 -34
- tryton/gui/window/view_form/model/group.py +7 -2
- tryton/gui/window/view_form/model/record.py +70 -31
- tryton/gui/window/view_form/screen/screen.py +98 -47
- tryton/gui/window/view_form/view/calendar_gtk/calendar_.py +15 -9
- tryton/gui/window/view_form/view/form.py +6 -12
- tryton/gui/window/view_form/view/form_gtk/char.py +5 -6
- tryton/gui/window/view_form/view/form_gtk/dictionary.py +49 -29
- tryton/gui/window/view_form/view/form_gtk/document.py +15 -10
- tryton/gui/window/view_form/view/form_gtk/many2many.py +49 -7
- tryton/gui/window/view_form/view/form_gtk/many2one.py +21 -13
- tryton/gui/window/view_form/view/form_gtk/multiselection.py +15 -5
- tryton/gui/window/view_form/view/form_gtk/one2many.py +42 -10
- tryton/gui/window/view_form/view/form_gtk/state_widget.py +6 -2
- tryton/gui/window/view_form/view/form_gtk/url.py +8 -4
- tryton/gui/window/view_form/view/graph_gtk/graph.py +3 -1
- tryton/gui/window/view_form/view/list.py +116 -48
- tryton/gui/window/view_form/view/list_gtk/editabletree.py +2 -1
- tryton/gui/window/view_form/view/list_gtk/widget.py +58 -23
- tryton/gui/window/view_form/view/screen_container.py +3 -5
- tryton/gui/window/win_csv.py +6 -12
- tryton/gui/window/win_export.py +49 -26
- tryton/gui/window/win_form.py +9 -7
- tryton/gui/window/win_import.py +45 -15
- tryton/gui/window/wizard.py +13 -10
- tryton/jsonrpc.py +75 -34
- tryton/plugins/__init__.py +5 -3
- tryton/pyson.py +57 -6
- tryton/rpc.py +18 -0
- tryton/tests/test_common_domain_parser.py +31 -2
- tryton/translate.py +5 -2
- {tryton-7.0.7.data → tryton-7.4.4.data}/scripts/tryton +8 -7
- {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/METADATA +6 -6
- {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/RECORD +105 -103
- {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/WHEEL +1 -1
- {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/LICENSE +0 -0
- {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
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
|
|
4
5
|
import decimal
|
|
5
6
|
import gettext
|
|
6
7
|
import locale
|
|
@@ -64,7 +65,7 @@ class DictEntry(object):
|
|
|
64
65
|
return self.widget.get_text()
|
|
65
66
|
|
|
66
67
|
def set_value(self, value):
|
|
67
|
-
self.widget.set_text(value or '')
|
|
68
|
+
self.widget.set_text(str(value or ''))
|
|
68
69
|
reset_position(self.widget)
|
|
69
70
|
|
|
70
71
|
def set_readonly(self, readonly):
|
|
@@ -223,17 +224,24 @@ class DictMultiSelectionEntry(DictEntry):
|
|
|
223
224
|
name = str(name)
|
|
224
225
|
model.append((value, name))
|
|
225
226
|
|
|
226
|
-
|
|
227
|
+
column = Gtk.TreeViewColumn()
|
|
228
|
+
select_cell = Gtk.CellRendererToggle()
|
|
229
|
+
select_cell.set_sensitive(False)
|
|
230
|
+
column.pack_start(select_cell, expand=False)
|
|
231
|
+
column.set_cell_data_func(select_cell, self._select_data_func)
|
|
227
232
|
name_cell = Gtk.CellRendererText()
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
self.tree.append_column(
|
|
233
|
+
column.pack_start(name_cell, expand=True)
|
|
234
|
+
column.add_attribute(name_cell, 'text', 1)
|
|
235
|
+
self.tree.append_column(column)
|
|
231
236
|
|
|
232
237
|
return widget
|
|
233
238
|
|
|
239
|
+
def _select_data_func(self, column, cell, model, iter_, selection):
|
|
240
|
+
cell.set_property('active', selection.iter_is_selected(iter_))
|
|
241
|
+
|
|
234
242
|
def get_value(self):
|
|
235
243
|
model, paths = self.tree.get_selection().get_selected_rows()
|
|
236
|
-
return [model[path][0] for path in paths]
|
|
244
|
+
return [model[path][0] for path in paths] or None
|
|
237
245
|
|
|
238
246
|
def set_value(self, value):
|
|
239
247
|
value2path = {v: idx for idx, (v, _) in enumerate(self.selection)}
|
|
@@ -241,9 +249,10 @@ class DictMultiSelectionEntry(DictEntry):
|
|
|
241
249
|
selection.handler_block_by_func(self._changed)
|
|
242
250
|
try:
|
|
243
251
|
selection.unselect_all()
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
252
|
+
if value:
|
|
253
|
+
for v in value:
|
|
254
|
+
if v in value2path:
|
|
255
|
+
selection.select_path(value2path[v])
|
|
247
256
|
finally:
|
|
248
257
|
selection.handler_unblock_by_func(self._changed)
|
|
249
258
|
|
|
@@ -278,7 +287,7 @@ class DictIntegerEntry(DictEntry):
|
|
|
278
287
|
return None
|
|
279
288
|
|
|
280
289
|
def set_value(self, value):
|
|
281
|
-
if value
|
|
290
|
+
if isinstance(value, (int, float, Decimal)):
|
|
282
291
|
txt_val = locale.format_string('%d', value, True)
|
|
283
292
|
else:
|
|
284
293
|
txt_val = ''
|
|
@@ -315,7 +324,7 @@ class DictFloatEntry(DictIntegerEntry):
|
|
|
315
324
|
else:
|
|
316
325
|
self.widget.digits = None
|
|
317
326
|
self.widget.set_width_chars(self.width)
|
|
318
|
-
if value
|
|
327
|
+
if isinstance(value, (int, float, Decimal)):
|
|
319
328
|
txt_val = locale.localize(
|
|
320
329
|
'{0:.{1}f}'.format(value, digits[1]), True)
|
|
321
330
|
else:
|
|
@@ -361,7 +370,8 @@ class DictDateTimeEntry(DictEntry):
|
|
|
361
370
|
return untimezoned_date(self.widget.props.value)
|
|
362
371
|
|
|
363
372
|
def set_value(self, value):
|
|
364
|
-
self.widget.props.value =
|
|
373
|
+
self.widget.props.value = (
|
|
374
|
+
timezoned_date(value) if isinstance(value, dt.datetime) else None)
|
|
365
375
|
|
|
366
376
|
def set_readonly(self, readonly):
|
|
367
377
|
for child in self.widget.get_children():
|
|
@@ -396,7 +406,7 @@ class DictDateEntry(DictEntry):
|
|
|
396
406
|
return self.widget.props.value
|
|
397
407
|
|
|
398
408
|
def set_value(self, value):
|
|
399
|
-
self.widget.props.value = value
|
|
409
|
+
self.widget.props.value = value if isinstance(value, dt.date) else None
|
|
400
410
|
|
|
401
411
|
def set_readonly(self, readonly):
|
|
402
412
|
super().set_readonly(readonly)
|
|
@@ -470,6 +480,7 @@ class DictWidget(Widget):
|
|
|
470
480
|
|
|
471
481
|
self._readonly = False
|
|
472
482
|
self._record_id = None
|
|
483
|
+
self._popup = False
|
|
473
484
|
|
|
474
485
|
@property
|
|
475
486
|
def _invalid_widget(self):
|
|
@@ -491,10 +502,16 @@ class DictWidget(Widget):
|
|
|
491
502
|
value = self.wid_text.get_text()
|
|
492
503
|
domain = self.field.domain_get(self.record)
|
|
493
504
|
|
|
505
|
+
if self._popup:
|
|
506
|
+
return
|
|
507
|
+
else:
|
|
508
|
+
self._popup = True
|
|
509
|
+
|
|
494
510
|
def callback(result):
|
|
495
511
|
if result:
|
|
496
512
|
self.add_new_keys([r[0] for r in result])
|
|
497
513
|
self.wid_text.set_text('')
|
|
514
|
+
self._popup = False
|
|
498
515
|
|
|
499
516
|
win = WinSearch(self.schema_model, callback, sel_multi=True,
|
|
500
517
|
context=context, domain=domain, new=False)
|
|
@@ -504,15 +521,14 @@ class DictWidget(Widget):
|
|
|
504
521
|
def add_new_keys(self, ids):
|
|
505
522
|
new_keys = self.field.add_new_keys(ids, self.record)
|
|
506
523
|
self.send_modified()
|
|
507
|
-
|
|
508
|
-
for
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
focus = True
|
|
524
|
+
value = self.field.get_client(self.record)
|
|
525
|
+
value.update({k: None for k in new_keys})
|
|
526
|
+
self.field.set_client(self.record, value)
|
|
527
|
+
self.display()
|
|
528
|
+
|
|
529
|
+
# Use idle add because it can be called from the callback
|
|
530
|
+
# of WinSearch while the popup is still there
|
|
531
|
+
GLib.idle_add(self.fields[new_keys[0]].widget.grab_focus)
|
|
516
532
|
|
|
517
533
|
def _sig_remove(self, button, key, modified=True):
|
|
518
534
|
self.fields[key].disconnect_signals()
|
|
@@ -558,7 +574,7 @@ class DictWidget(Widget):
|
|
|
558
574
|
not self._readonly
|
|
559
575
|
and self.attrs.get('delete', True)))
|
|
560
576
|
|
|
561
|
-
def add_line(self, key):
|
|
577
|
+
def add_line(self, key, position):
|
|
562
578
|
key_schema = self.field.keys[key]
|
|
563
579
|
self.fields[key] = DICT_ENTRIES[key_schema['type']](key, self)
|
|
564
580
|
field = self.fields[key]
|
|
@@ -566,8 +582,8 @@ class DictWidget(Widget):
|
|
|
566
582
|
label = Gtk.Label(
|
|
567
583
|
label=set_underline(text),
|
|
568
584
|
use_underline=True, halign=Gtk.Align.END)
|
|
569
|
-
self.grid.
|
|
570
|
-
|
|
585
|
+
self.grid.insert_row(position)
|
|
586
|
+
self.grid.attach(label, 0, position, 1, 1)
|
|
571
587
|
label.set_mnemonic_widget(field.widget)
|
|
572
588
|
label.show()
|
|
573
589
|
hbox = Gtk.HBox(hexpand=True)
|
|
@@ -604,6 +620,11 @@ class DictWidget(Widget):
|
|
|
604
620
|
self.field.add_keys(list(new_key_names), self.record)
|
|
605
621
|
decoder = PYSONDecoder()
|
|
606
622
|
|
|
623
|
+
# We remove first the old keys in order to keep the order when
|
|
624
|
+
# inserting the new ones
|
|
625
|
+
for key in set(self.fields.keys()) - set(value.keys()):
|
|
626
|
+
self._sig_remove(None, key, modified=False)
|
|
627
|
+
|
|
607
628
|
def filter_func(item):
|
|
608
629
|
key, value = item
|
|
609
630
|
return key in self.field.keys
|
|
@@ -612,9 +633,10 @@ class DictWidget(Widget):
|
|
|
612
633
|
key, value = item
|
|
613
634
|
return self.field.keys[key]['sequence'] or 0
|
|
614
635
|
|
|
615
|
-
for key, val in
|
|
636
|
+
for position, (key, val) in enumerate(
|
|
637
|
+
sorted(filter(filter_func, value.items()), key=key)):
|
|
616
638
|
if key not in self.fields:
|
|
617
|
-
self.add_line(key)
|
|
639
|
+
self.add_line(key, position)
|
|
618
640
|
widget = self.fields[key]
|
|
619
641
|
widget.set_value(val)
|
|
620
642
|
widget.set_readonly(self._readonly)
|
|
@@ -622,8 +644,6 @@ class DictWidget(Widget):
|
|
|
622
644
|
self.field.keys[key].get('domain') or '[]')
|
|
623
645
|
widget_class(
|
|
624
646
|
widget.widget, 'invalid', not eval_domain(key_domain, value))
|
|
625
|
-
for key in set(self.fields.keys()) - set(value.keys()):
|
|
626
|
-
self._sig_remove(None, key, modified=False)
|
|
627
647
|
|
|
628
648
|
self._set_button_sensitive()
|
|
629
649
|
|
|
@@ -1,7 +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
|
+
import logging
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from tempfile import NamedTemporaryFile
|
|
5
5
|
|
|
6
6
|
from gi.repository import Gdk, GLib, Gtk
|
|
7
7
|
|
|
@@ -16,6 +16,8 @@ from tryton.common import data2pixbuf, resize_pixbuf
|
|
|
16
16
|
from .binary import BinaryMixin
|
|
17
17
|
from .widget import Widget
|
|
18
18
|
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class Document(BinaryMixin, Widget):
|
|
21
23
|
expand = True
|
|
@@ -76,20 +78,23 @@ class Document(BinaryMixin, Widget):
|
|
|
76
78
|
self.image.hide()
|
|
77
79
|
if self.evince_view:
|
|
78
80
|
self.evince_scroll.show()
|
|
81
|
+
suffix = None
|
|
79
82
|
if self.filename_field:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
filename = self.filename_field.get(self.record)
|
|
84
|
+
if filename:
|
|
85
|
+
suffix = Path(filename).suffix
|
|
86
|
+
filename = Path(self.field.get_filename(self.record, suffix))
|
|
83
87
|
try:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
EvinceDocument.Document.factory_get_document(
|
|
89
|
-
path.as_uri()))
|
|
88
|
+
document = (
|
|
89
|
+
EvinceDocument.Document.factory_get_document_full(
|
|
90
|
+
filename.as_uri(),
|
|
91
|
+
EvinceDocument.DocumentLoadFlags.NONE))
|
|
90
92
|
model = EvinceView.DocumentModel()
|
|
91
93
|
model.set_document(document)
|
|
92
94
|
self.evince_view.set_model(model)
|
|
93
95
|
except GLib.GError:
|
|
96
|
+
logger.warning(
|
|
97
|
+
f"Could not open document {filename}",
|
|
98
|
+
exc_info=True)
|
|
94
99
|
self.evince_view.set_model(EvinceView.DocumentModel())
|
|
95
100
|
self.evince_scroll.hide()
|
|
@@ -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)):
|
|
@@ -83,6 +82,14 @@ class Many2Many(Widget):
|
|
|
83
82
|
self.but_remove.set_relief(Gtk.ReliefStyle.NONE)
|
|
84
83
|
hbox.pack_start(self.but_remove, expand=False, fill=False, padding=0)
|
|
85
84
|
|
|
85
|
+
self.but_unremove = Gtk.Button(can_focus=False)
|
|
86
|
+
tooltips.set_tip(self.but_unremove, _("Restore selected record"))
|
|
87
|
+
self.but_unremove.connect('clicked', self._sig_unremove)
|
|
88
|
+
self.but_unremove.add(common.IconFactory.get_image(
|
|
89
|
+
'tryton-undo', Gtk.IconSize.SMALL_TOOLBAR))
|
|
90
|
+
self.but_unremove.set_relief(Gtk.ReliefStyle.NONE)
|
|
91
|
+
hbox.pack_start(self.but_unremove, expand=False, fill=False, padding=0)
|
|
92
|
+
|
|
86
93
|
tooltips.enable()
|
|
87
94
|
|
|
88
95
|
frame = Gtk.Frame()
|
|
@@ -113,6 +120,8 @@ class Many2Many(Widget):
|
|
|
113
120
|
self.screen.widget.connect('key_press_event', self.on_keypress)
|
|
114
121
|
self.wid_text.connect('key_press_event', self.on_keypress)
|
|
115
122
|
|
|
123
|
+
self._popup = False
|
|
124
|
+
|
|
116
125
|
def on_keypress(self, widget, event):
|
|
117
126
|
editable = self.wid_text.get_editable()
|
|
118
127
|
activate_keys = [Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab]
|
|
@@ -129,6 +138,9 @@ class Many2Many(Widget):
|
|
|
129
138
|
elif event.keyval in remove_keys and editable:
|
|
130
139
|
self._sig_remove()
|
|
131
140
|
return True
|
|
141
|
+
elif event.keyval == Gdk.KEY_Insert:
|
|
142
|
+
self._sig_unremove()
|
|
143
|
+
return True
|
|
132
144
|
elif widget == self.wid_text:
|
|
133
145
|
if event.keyval == Gdk.KEY_F3:
|
|
134
146
|
self._sig_new()
|
|
@@ -161,25 +173,29 @@ class Many2Many(Widget):
|
|
|
161
173
|
return int(self.attrs.get('create', 1)) and self.get_access('create')
|
|
162
174
|
|
|
163
175
|
def _sig_add(self, *args):
|
|
164
|
-
if not self.focus_out:
|
|
165
|
-
return
|
|
166
176
|
domain = self.field.domain_get(self.record)
|
|
167
177
|
add_remove = self.record.expr_eval(self.attrs.get('add_remove'))
|
|
168
178
|
if add_remove:
|
|
169
179
|
domain = [domain, add_remove]
|
|
180
|
+
existing_ids = self.field.get_eval(self.record)
|
|
181
|
+
if existing_ids:
|
|
182
|
+
domain = [domain, ('id', 'not in', existing_ids)]
|
|
170
183
|
context = self.field.get_search_context(self.record)
|
|
171
184
|
order = self.field.get_search_order(self.record)
|
|
172
185
|
value = self.wid_text.get_text()
|
|
173
186
|
|
|
174
|
-
self.
|
|
187
|
+
if self._popup:
|
|
188
|
+
return
|
|
189
|
+
else:
|
|
190
|
+
self._popup = True
|
|
175
191
|
|
|
176
192
|
def callback(result):
|
|
177
|
-
self.focus_out = True
|
|
178
193
|
if result:
|
|
179
194
|
ids = [x[0] for x in result]
|
|
180
195
|
self.screen.load(ids, modified=True)
|
|
181
196
|
self.screen.set_cursor()
|
|
182
197
|
self.wid_text.set_text('')
|
|
198
|
+
self._popup = False
|
|
183
199
|
win = WinSearch(self.attrs['relation'], callback, sel_multi=True,
|
|
184
200
|
context=context, domain=domain, order=order,
|
|
185
201
|
view_ids=self.attrs.get('view_ids', '').split(','),
|
|
@@ -192,6 +208,9 @@ class Many2Many(Widget):
|
|
|
192
208
|
def _sig_remove(self, *args):
|
|
193
209
|
self.screen.remove(remove=True)
|
|
194
210
|
|
|
211
|
+
def _sig_unremove(self, *args):
|
|
212
|
+
self.screen.unremove()
|
|
213
|
+
|
|
195
214
|
def _on_activate(self):
|
|
196
215
|
self._sig_edit()
|
|
197
216
|
|
|
@@ -215,6 +234,10 @@ class Many2Many(Widget):
|
|
|
215
234
|
def _sig_edit(self):
|
|
216
235
|
if not self.screen.current_record:
|
|
217
236
|
return
|
|
237
|
+
if self._popup:
|
|
238
|
+
return
|
|
239
|
+
else:
|
|
240
|
+
self._popup = True
|
|
218
241
|
# Create a new screen that is not linked to the parent otherwise on the
|
|
219
242
|
# save of the record will trigger the save of the parent
|
|
220
243
|
screen = self._get_screen_form()
|
|
@@ -231,22 +254,26 @@ class Many2Many(Widget):
|
|
|
231
254
|
self.screen.current_record.modified_fields.setdefault('id')
|
|
232
255
|
# Force a display to clear the CellCache
|
|
233
256
|
self.screen.display()
|
|
257
|
+
self._popup = False
|
|
234
258
|
WinForm(screen, callback)
|
|
235
259
|
|
|
236
260
|
def _sig_new(self, defaults=None):
|
|
261
|
+
if self._popup:
|
|
262
|
+
return
|
|
263
|
+
else:
|
|
264
|
+
self._popup = True
|
|
237
265
|
screen = self._get_screen_form()
|
|
238
266
|
defaults = defaults.copy() if defaults is not None else {}
|
|
239
267
|
defaults['rec_name'] = self.wid_text.get_text()
|
|
240
268
|
|
|
241
269
|
def callback(result):
|
|
242
|
-
self.focus_out = True
|
|
243
270
|
if result:
|
|
244
271
|
record = screen.current_record
|
|
245
272
|
self.screen.load([record.id], modified=True)
|
|
246
273
|
self.wid_text.set_text('')
|
|
247
274
|
self.wid_text.grab_focus()
|
|
275
|
+
self._popup = False
|
|
248
276
|
|
|
249
|
-
self.focus_out = False
|
|
250
277
|
WinForm(
|
|
251
278
|
screen, callback, new=True, save_current=True, defaults=defaults)
|
|
252
279
|
|
|
@@ -274,11 +301,23 @@ class Many2Many(Widget):
|
|
|
274
301
|
else:
|
|
275
302
|
size_limit = False
|
|
276
303
|
|
|
304
|
+
removable = any(
|
|
305
|
+
not r.deleted and not r.removed
|
|
306
|
+
for r in self.screen.selected_records)
|
|
307
|
+
unremovable = any(
|
|
308
|
+
r.deleted or r.removed for r in self.screen.selected_records)
|
|
309
|
+
|
|
277
310
|
self.but_add.set_sensitive(bool(
|
|
278
311
|
not self._readonly
|
|
279
312
|
and not size_limit))
|
|
280
313
|
self.but_remove.set_sensitive(bool(
|
|
281
314
|
not self._readonly
|
|
315
|
+
and removable
|
|
316
|
+
and self._position))
|
|
317
|
+
self.but_unremove.set_sensitive(bool(
|
|
318
|
+
not self._readonly
|
|
319
|
+
and not size_limit
|
|
320
|
+
and unremovable
|
|
282
321
|
and self._position))
|
|
283
322
|
|
|
284
323
|
def record_message(self, position, size, *args):
|
|
@@ -333,6 +372,9 @@ class Many2Many(Widget):
|
|
|
333
372
|
add_remove = self.record.expr_eval(self.attrs.get('add_remove'))
|
|
334
373
|
if add_remove:
|
|
335
374
|
domain = [domain, add_remove]
|
|
375
|
+
existing_ids = self.field.get_eval(self.record)
|
|
376
|
+
if existing_ids:
|
|
377
|
+
domain = [domain, ('id', 'not in', existing_ids)]
|
|
336
378
|
update_completion(
|
|
337
379
|
self.wid_text, self.record, self.field, model, domain)
|
|
338
380
|
|
|
@@ -37,7 +37,7 @@ class Many2One(Widget):
|
|
|
37
37
|
lambda x, y: self._focus_out())
|
|
38
38
|
self.wid_text.connect('changed', self.sig_changed)
|
|
39
39
|
self.changed = True
|
|
40
|
-
self.
|
|
40
|
+
self._popup = False
|
|
41
41
|
|
|
42
42
|
if int(self.attrs.get('completion', 1)):
|
|
43
43
|
self.wid_text.connect('changed', self._update_completion)
|
|
@@ -103,13 +103,16 @@ class Many2One(Widget):
|
|
|
103
103
|
model = self.get_model()
|
|
104
104
|
if not model or not common.MODELACCESS[model]['read']:
|
|
105
105
|
return
|
|
106
|
-
if not self.
|
|
106
|
+
if not self.field:
|
|
107
107
|
return
|
|
108
108
|
self.changed = False
|
|
109
109
|
value = self.field.get(self.record)
|
|
110
110
|
model = self.get_model()
|
|
111
111
|
|
|
112
|
-
self.
|
|
112
|
+
if self._popup:
|
|
113
|
+
return
|
|
114
|
+
else:
|
|
115
|
+
self._popup = True
|
|
113
116
|
if model and not self.has_target(value):
|
|
114
117
|
if (not self._readonly
|
|
115
118
|
and (self.wid_text.get_text()
|
|
@@ -126,7 +129,7 @@ class Many2One(Widget):
|
|
|
126
129
|
self.value_from_id(*result[0]), force_change=True)
|
|
127
130
|
else:
|
|
128
131
|
self.wid_text.set_text('')
|
|
129
|
-
self.
|
|
132
|
+
self._popup = False
|
|
130
133
|
self.changed = True
|
|
131
134
|
|
|
132
135
|
win = WinSearch(model, callback, sel_multi=False,
|
|
@@ -139,7 +142,7 @@ class Many2One(Widget):
|
|
|
139
142
|
win.screen.search_filter(quote(text))
|
|
140
143
|
win.show()
|
|
141
144
|
return
|
|
142
|
-
self.
|
|
145
|
+
self._popup = False
|
|
143
146
|
self.changed = True
|
|
144
147
|
return
|
|
145
148
|
|
|
@@ -164,7 +167,10 @@ class Many2One(Widget):
|
|
|
164
167
|
def sig_new(self, defaults=None):
|
|
165
168
|
if not self.create_access:
|
|
166
169
|
return
|
|
167
|
-
self.
|
|
170
|
+
if self._popup:
|
|
171
|
+
return
|
|
172
|
+
else:
|
|
173
|
+
self._popup = True
|
|
168
174
|
screen = self.get_screen(search=True)
|
|
169
175
|
defaults = defaults.copy() if defaults is not None else {}
|
|
170
176
|
defaults['rec_name'] = self.wid_text.get_text()
|
|
@@ -174,7 +180,7 @@ class Many2One(Widget):
|
|
|
174
180
|
self.field.set_client(self.record,
|
|
175
181
|
self.value_from_id(screen.current_record.id,
|
|
176
182
|
screen.current_record.rec_name()))
|
|
177
|
-
self.
|
|
183
|
+
self._popup = False
|
|
178
184
|
WinForm(
|
|
179
185
|
screen, callback, new=True, save_current=True, defaults=defaults)
|
|
180
186
|
|
|
@@ -184,10 +190,9 @@ class Many2One(Widget):
|
|
|
184
190
|
model = self.get_model()
|
|
185
191
|
if not model or not common.MODELACCESS[model]['read']:
|
|
186
192
|
return
|
|
187
|
-
if not self.
|
|
193
|
+
if not self.field:
|
|
188
194
|
return
|
|
189
195
|
self.changed = False
|
|
190
|
-
self.focus_out = False
|
|
191
196
|
value = self.field.get(self.record)
|
|
192
197
|
|
|
193
198
|
if (icon_pos == Gtk.EntryIconPosition.SECONDARY
|
|
@@ -196,9 +201,12 @@ class Many2One(Widget):
|
|
|
196
201
|
self.field.set_client(self.record, self.value_from_id(None, ''))
|
|
197
202
|
self.wid_text.set_text('')
|
|
198
203
|
self.changed = True
|
|
199
|
-
self.focus_out = True
|
|
200
204
|
return
|
|
201
205
|
|
|
206
|
+
if self._popup:
|
|
207
|
+
return
|
|
208
|
+
else:
|
|
209
|
+
self._popup = True
|
|
202
210
|
if self.has_target(value):
|
|
203
211
|
m2o_id = self.id_from_value(self.field.get(self.record))
|
|
204
212
|
screen = self.get_screen()
|
|
@@ -211,7 +219,7 @@ class Many2One(Widget):
|
|
|
211
219
|
self.value_from_id(screen.current_record.id,
|
|
212
220
|
screen.current_record.rec_name()),
|
|
213
221
|
force_change=True)
|
|
214
|
-
self.
|
|
222
|
+
self._popup = False
|
|
215
223
|
self.changed = True
|
|
216
224
|
WinForm(screen, callback, save_current=True)
|
|
217
225
|
return
|
|
@@ -225,7 +233,7 @@ class Many2One(Widget):
|
|
|
225
233
|
if result:
|
|
226
234
|
self.field.set_client(self.record,
|
|
227
235
|
self.value_from_id(*result[0]), force_change=True)
|
|
228
|
-
self.
|
|
236
|
+
self._popup = False
|
|
229
237
|
self.changed = True
|
|
230
238
|
win = WinSearch(model, callback, sel_multi=False,
|
|
231
239
|
context=context, domain=domain, order=order,
|
|
@@ -236,7 +244,7 @@ class Many2One(Widget):
|
|
|
236
244
|
win.screen.search_filter(quote(text))
|
|
237
245
|
win.show()
|
|
238
246
|
return
|
|
239
|
-
self.
|
|
247
|
+
self._popup = False
|
|
240
248
|
self.changed = True
|
|
241
249
|
|
|
242
250
|
def sig_key_press(self, widget, event, *args):
|
|
@@ -33,15 +33,23 @@ class MultiSelection(Widget, SelectionMixin):
|
|
|
33
33
|
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
|
|
34
34
|
selection.connect('changed', self.changed)
|
|
35
35
|
self.widget.add(self.tree)
|
|
36
|
-
|
|
36
|
+
column = Gtk.TreeViewColumn()
|
|
37
|
+
select_cell = Gtk.CellRendererToggle()
|
|
38
|
+
select_cell.set_sensitive(False)
|
|
39
|
+
column.pack_start(select_cell, expand=False)
|
|
40
|
+
column.set_cell_data_func(
|
|
41
|
+
select_cell, self._select_data_func, selection)
|
|
37
42
|
name_cell = Gtk.CellRendererText()
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
self.tree.append_column(
|
|
43
|
+
column.pack_start(name_cell, expand=True)
|
|
44
|
+
column.add_attribute(name_cell, 'text', 1)
|
|
45
|
+
self.tree.append_column(column)
|
|
41
46
|
|
|
42
47
|
self.nullable_widget = False
|
|
43
48
|
self.init_selection()
|
|
44
49
|
|
|
50
|
+
def _select_data_func(self, column, cell, model, iter_, selection):
|
|
51
|
+
cell.set_property('active', selection.iter_is_selected(iter_))
|
|
52
|
+
|
|
45
53
|
def _readonly_set(self, readonly):
|
|
46
54
|
super(MultiSelection, self)._readonly_set(readonly)
|
|
47
55
|
selection = self.tree.get_selection()
|
|
@@ -70,6 +78,8 @@ class MultiSelection(Widget, SelectionMixin):
|
|
|
70
78
|
self.field.set_client(self.record, self.get_value())
|
|
71
79
|
|
|
72
80
|
def display(self):
|
|
81
|
+
def freeze(iter):
|
|
82
|
+
return list(map(tuple, iter))
|
|
73
83
|
selection = self.tree.get_selection()
|
|
74
84
|
selection.handler_block_by_func(self.changed)
|
|
75
85
|
try:
|
|
@@ -77,7 +87,7 @@ class MultiSelection(Widget, SelectionMixin):
|
|
|
77
87
|
# it will be set back in the super call
|
|
78
88
|
selection.set_select_function(lambda *a: True)
|
|
79
89
|
self.update_selection(self.record, self.field)
|
|
80
|
-
new_model = self.selection !=
|
|
90
|
+
new_model = freeze(self.selection) != freeze(self.model)
|
|
81
91
|
if new_model:
|
|
82
92
|
self.model.clear()
|
|
83
93
|
if not self.field:
|