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.

Files changed (105) hide show
  1. tryton/__init__.py +1 -1
  2. tryton/cache.py +34 -0
  3. tryton/common/common.py +149 -73
  4. tryton/common/completion.py +2 -2
  5. tryton/common/datetime_.py +3 -1
  6. tryton/common/domain_inversion.py +2 -1
  7. tryton/common/domain_parser.py +22 -11
  8. tryton/common/popup_menu.py +1 -1
  9. tryton/common/selection.py +6 -3
  10. tryton/common/tempfile.py +34 -0
  11. tryton/config.py +4 -5
  12. tryton/data/locale/bg/LC_MESSAGES/tryton.mo +0 -0
  13. tryton/data/locale/bg/LC_MESSAGES/tryton.po +69 -20
  14. tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
  15. tryton/data/locale/ca/LC_MESSAGES/tryton.po +70 -25
  16. tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
  17. tryton/data/locale/cs/LC_MESSAGES/tryton.po +68 -21
  18. tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
  19. tryton/data/locale/de/LC_MESSAGES/tryton.po +71 -26
  20. tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
  21. tryton/data/locale/es/LC_MESSAGES/tryton.po +68 -23
  22. tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
  23. tryton/data/locale/es_419/LC_MESSAGES/tryton.po +72 -22
  24. tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
  25. tryton/data/locale/et/LC_MESSAGES/tryton.po +73 -23
  26. tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
  27. tryton/data/locale/fa/LC_MESSAGES/tryton.po +74 -25
  28. tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
  29. tryton/data/locale/fi/LC_MESSAGES/tryton.po +63 -20
  30. tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
  31. tryton/data/locale/fr/LC_MESSAGES/tryton.po +73 -28
  32. tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
  33. tryton/data/locale/hu/LC_MESSAGES/tryton.po +75 -23
  34. tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
  35. tryton/data/locale/id/LC_MESSAGES/tryton.po +72 -23
  36. tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
  37. tryton/data/locale/it/LC_MESSAGES/tryton.po +73 -24
  38. tryton/data/locale/ja_JP/LC_MESSAGES/tryton.mo +0 -0
  39. tryton/data/locale/lo/LC_MESSAGES/tryton.mo +0 -0
  40. tryton/data/locale/lo/LC_MESSAGES/tryton.po +74 -25
  41. tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
  42. tryton/data/locale/lt/LC_MESSAGES/tryton.po +73 -23
  43. tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
  44. tryton/data/locale/nl/LC_MESSAGES/tryton.po +72 -27
  45. tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
  46. tryton/data/locale/pl/LC_MESSAGES/tryton.po +113 -78
  47. tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
  48. tryton/data/locale/pt/LC_MESSAGES/tryton.po +73 -24
  49. tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
  50. tryton/data/locale/ro/LC_MESSAGES/tryton.po +87 -36
  51. tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
  52. tryton/data/locale/ru/LC_MESSAGES/tryton.po +72 -25
  53. tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
  54. tryton/data/locale/sl/LC_MESSAGES/tryton.po +77 -26
  55. tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
  56. tryton/data/locale/tr/LC_MESSAGES/tryton.po +64 -20
  57. tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
  58. tryton/data/locale/uk/LC_MESSAGES/tryton.po +75 -23
  59. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
  60. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +76 -24
  61. tryton/device_cookie.py +1 -1
  62. tryton/gui/main.py +14 -12
  63. tryton/gui/window/about.py +1 -1
  64. tryton/gui/window/dblogin.py +2 -2
  65. tryton/gui/window/email_.py +2 -2
  66. tryton/gui/window/form.py +10 -5
  67. tryton/gui/window/log.py +24 -2
  68. tryton/gui/window/tabcontent.py +2 -2
  69. tryton/gui/window/view_form/model/field.py +84 -34
  70. tryton/gui/window/view_form/model/group.py +7 -2
  71. tryton/gui/window/view_form/model/record.py +70 -31
  72. tryton/gui/window/view_form/screen/screen.py +98 -47
  73. tryton/gui/window/view_form/view/calendar_gtk/calendar_.py +15 -9
  74. tryton/gui/window/view_form/view/form.py +6 -12
  75. tryton/gui/window/view_form/view/form_gtk/char.py +5 -6
  76. tryton/gui/window/view_form/view/form_gtk/dictionary.py +49 -29
  77. tryton/gui/window/view_form/view/form_gtk/document.py +15 -10
  78. tryton/gui/window/view_form/view/form_gtk/many2many.py +49 -7
  79. tryton/gui/window/view_form/view/form_gtk/many2one.py +21 -13
  80. tryton/gui/window/view_form/view/form_gtk/multiselection.py +15 -5
  81. tryton/gui/window/view_form/view/form_gtk/one2many.py +42 -10
  82. tryton/gui/window/view_form/view/form_gtk/state_widget.py +6 -2
  83. tryton/gui/window/view_form/view/form_gtk/url.py +8 -4
  84. tryton/gui/window/view_form/view/graph_gtk/graph.py +3 -1
  85. tryton/gui/window/view_form/view/list.py +116 -48
  86. tryton/gui/window/view_form/view/list_gtk/editabletree.py +2 -1
  87. tryton/gui/window/view_form/view/list_gtk/widget.py +58 -23
  88. tryton/gui/window/view_form/view/screen_container.py +3 -5
  89. tryton/gui/window/win_csv.py +6 -12
  90. tryton/gui/window/win_export.py +49 -26
  91. tryton/gui/window/win_form.py +9 -7
  92. tryton/gui/window/win_import.py +45 -15
  93. tryton/gui/window/wizard.py +13 -10
  94. tryton/jsonrpc.py +75 -34
  95. tryton/plugins/__init__.py +5 -3
  96. tryton/pyson.py +57 -6
  97. tryton/rpc.py +18 -0
  98. tryton/tests/test_common_domain_parser.py +31 -2
  99. tryton/translate.py +5 -2
  100. {tryton-7.0.7.data → tryton-7.4.4.data}/scripts/tryton +8 -7
  101. {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/METADATA +6 -6
  102. {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/RECORD +105 -103
  103. {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/WHEEL +1 -1
  104. {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/LICENSE +0 -0
  105. {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
- name_column = Gtk.TreeViewColumn()
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
- name_column.pack_start(name_cell, expand=True)
229
- name_column.add_attribute(name_cell, 'text', 1)
230
- self.tree.append_column(name_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
- for v in value:
245
- if v in value2path:
246
- selection.select_path(value2path[v])
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 is not None:
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 is not None:
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 = timezoned_date(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
- focus = False
508
- for key_name in new_keys:
509
- if key_name not in self.fields:
510
- self.add_line(key_name)
511
- if not focus:
512
- # Use idle add because it can be called from the callback
513
- # of WinSearch while the popup is still there
514
- GLib.idle_add(self.fields[key_name].widget.grab_focus)
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.attach_next_to(
570
- label, None, Gtk.PositionType.BOTTOM, 1, 1)
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 sorted(filter(filter_func, value.items()), key=key):
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
- suffix = self.filename_field.get(self.record)
81
- else:
82
- suffix = None
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
- 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()))
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.focus_out = False
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.focus_out = True
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.focus_out or not self.field:
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.focus_out = False
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.focus_out = True
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.focus_out = True
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.focus_out = False
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.focus_out = True
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.focus_out or not self.field:
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.focus_out = True
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.focus_out = True
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.focus_out = True
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
- name_column = Gtk.TreeViewColumn()
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
- name_column.pack_start(name_cell, expand=True)
39
- name_column.add_attribute(name_cell, 'text', 1)
40
- self.tree.append_column(name_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 != [list(row) for row in self.model]
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: