tryton 7.2.13__py3-none-any.whl → 7.4.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.

Files changed (85) hide show
  1. tryton/__init__.py +1 -1
  2. tryton/common/common.py +12 -4
  3. tryton/common/domain_inversion.py +1 -2
  4. tryton/common/domain_parser.py +5 -4
  5. tryton/common/popup_menu.py +1 -1
  6. tryton/data/locale/bg/LC_MESSAGES/tryton.mo +0 -0
  7. tryton/data/locale/bg/LC_MESSAGES/tryton.po +27 -16
  8. tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
  9. tryton/data/locale/ca/LC_MESSAGES/tryton.po +23 -17
  10. tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
  11. tryton/data/locale/cs/LC_MESSAGES/tryton.po +26 -17
  12. tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
  13. tryton/data/locale/de/LC_MESSAGES/tryton.po +27 -21
  14. tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
  15. tryton/data/locale/es/LC_MESSAGES/tryton.po +22 -16
  16. tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
  17. tryton/data/locale/es_419/LC_MESSAGES/tryton.po +31 -19
  18. tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
  19. tryton/data/locale/et/LC_MESSAGES/tryton.po +27 -17
  20. tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
  21. tryton/data/locale/fa/LC_MESSAGES/tryton.po +28 -18
  22. tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
  23. tryton/data/locale/fi/LC_MESSAGES/tryton.po +22 -16
  24. tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
  25. tryton/data/locale/fr/LC_MESSAGES/tryton.po +28 -22
  26. tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
  27. tryton/data/locale/hu/LC_MESSAGES/tryton.po +29 -17
  28. tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
  29. tryton/data/locale/id/LC_MESSAGES/tryton.po +31 -21
  30. tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
  31. tryton/data/locale/it/LC_MESSAGES/tryton.po +30 -20
  32. tryton/data/locale/ja_JP/LC_MESSAGES/tryton.mo +0 -0
  33. tryton/data/locale/lo/LC_MESSAGES/tryton.mo +0 -0
  34. tryton/data/locale/lo/LC_MESSAGES/tryton.po +29 -19
  35. tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
  36. tryton/data/locale/lt/LC_MESSAGES/tryton.po +27 -17
  37. tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
  38. tryton/data/locale/nl/LC_MESSAGES/tryton.po +26 -20
  39. tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
  40. tryton/data/locale/pl/LC_MESSAGES/tryton.po +29 -18
  41. tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
  42. tryton/data/locale/pt/LC_MESSAGES/tryton.po +28 -18
  43. tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
  44. tryton/data/locale/ro/LC_MESSAGES/tryton.po +37 -26
  45. tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
  46. tryton/data/locale/ru/LC_MESSAGES/tryton.po +29 -19
  47. tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
  48. tryton/data/locale/sl/LC_MESSAGES/tryton.po +45 -35
  49. tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
  50. tryton/data/locale/tr/LC_MESSAGES/tryton.po +23 -16
  51. tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
  52. tryton/data/locale/uk/LC_MESSAGES/tryton.po +31 -19
  53. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
  54. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +32 -21
  55. tryton/gui/main.py +11 -10
  56. tryton/gui/window/about.py +1 -1
  57. tryton/gui/window/email_.py +1 -1
  58. tryton/gui/window/form.py +4 -1
  59. tryton/gui/window/tabcontent.py +2 -2
  60. tryton/gui/window/view_form/model/field.py +2 -0
  61. tryton/gui/window/view_form/model/group.py +4 -1
  62. tryton/gui/window/view_form/model/record.py +11 -26
  63. tryton/gui/window/view_form/screen/screen.py +17 -7
  64. tryton/gui/window/view_form/view/form.py +3 -5
  65. tryton/gui/window/view_form/view/form_gtk/dictionary.py +12 -5
  66. tryton/gui/window/view_form/view/form_gtk/document.py +6 -0
  67. tryton/gui/window/view_form/view/form_gtk/many2many.py +32 -0
  68. tryton/gui/window/view_form/view/form_gtk/multiselection.py +15 -5
  69. tryton/gui/window/view_form/view/form_gtk/one2many.py +17 -4
  70. tryton/gui/window/view_form/view/form_gtk/url.py +8 -4
  71. tryton/gui/window/view_form/view/list.py +52 -24
  72. tryton/gui/window/view_form/view/list_gtk/widget.py +1 -1
  73. tryton/gui/window/view_form/view/screen_container.py +5 -3
  74. tryton/gui/window/win_csv.py +6 -12
  75. tryton/gui/window/win_export.py +49 -25
  76. tryton/gui/window/win_import.py +36 -11
  77. tryton/jsonrpc.py +28 -6
  78. tryton/pyson.py +2 -1
  79. tryton/tests/test_common_domain_parser.py +23 -2
  80. {tryton-7.2.13.dist-info → tryton-7.4.0.dist-info}/METADATA +2 -2
  81. {tryton-7.2.13.dist-info → tryton-7.4.0.dist-info}/RECORD +85 -85
  82. {tryton-7.2.13.dist-info → tryton-7.4.0.dist-info}/WHEEL +1 -1
  83. {tryton-7.2.13.data → tryton-7.4.0.data}/scripts/tryton +0 -0
  84. {tryton-7.2.13.dist-info → tryton-7.4.0.dist-info}/LICENSE +0 -0
  85. {tryton-7.2.13.dist-info → tryton-7.4.0.dist-info}/top_level.txt +0 -0
@@ -140,9 +140,9 @@ msgstr "并行操作异常"
140
140
  msgid "Could not get a session."
141
141
  msgstr "无法获取会话。"
142
142
 
143
- #, python-format
143
+ #, fuzzy, python-format
144
144
  msgid "Error: \"%s\". Try again later."
145
- msgstr ""
145
+ msgstr "请求太多,请稍后重试."
146
146
 
147
147
  msgid "Too many requests. Try again later."
148
148
  msgstr "请求太多,请稍后重试."
@@ -240,8 +240,8 @@ msgstr "报告..."
240
240
  msgid "Print..."
241
241
  msgstr "打印..."
242
242
 
243
- msgid "E-Mail..."
244
- msgstr "用电子邮件发送..."
243
+ msgid "Email..."
244
+ msgstr "电子邮件(_E)..."
245
245
 
246
246
  msgid "Bold"
247
247
  msgstr "粗体"
@@ -351,12 +351,6 @@ msgstr "帮助"
351
351
  msgid "No result found."
352
352
  msgstr "无符合条件的数据."
353
353
 
354
- msgid "Favorites"
355
- msgstr "收藏(_V)"
356
-
357
- msgid "Manage..."
358
- msgstr "管理..."
359
-
360
354
  msgid "Action"
361
355
  msgstr "操作"
362
356
 
@@ -587,6 +581,9 @@ msgstr "连接 Tryton 服务器"
587
581
  msgid "Profile:"
588
582
  msgstr "配置:"
589
583
 
584
+ msgid "Manage..."
585
+ msgstr "管理..."
586
+
590
587
  msgid "Host / Database information"
591
588
  msgstr "主机/数据库信息"
592
589
 
@@ -596,8 +593,8 @@ msgstr "用户名:"
596
593
  msgid "Unable to complete email entry"
597
594
  msgstr "无法完成电子邮件输入"
598
595
 
599
- #, python-format
600
- msgid "E-mail %s"
596
+ #, fuzzy, python-format
597
+ msgid "Email %s"
601
598
  msgstr "电子邮件 %s"
602
599
 
603
600
  msgid "To:"
@@ -845,10 +842,12 @@ msgstr "报告(_R)..."
845
842
  msgid "_Print..."
846
843
  msgstr "打印(_P)..."
847
844
 
848
- msgid "_E-Mail..."
849
- msgstr "电子邮件发送(_E)..."
845
+ #, fuzzy
846
+ msgid "_Email..."
847
+ msgstr "电子邮件(_E)..."
850
848
 
851
- msgid "Send an e-mail using the record"
849
+ #, fuzzy
850
+ msgid "Send an email using the record"
852
851
  msgstr "使用当前记录发送电子邮件"
853
852
 
854
853
  msgid "_Export Data..."
@@ -900,6 +899,9 @@ msgstr "字段名"
900
899
  msgid "CSV Export: %s"
901
900
  msgstr "CSV导出:%s"
902
901
 
902
+ msgid "Open"
903
+ msgstr "打开"
904
+
903
905
  msgid "_Save Export"
904
906
  msgstr "保存导出设置(_S)"
905
907
 
@@ -915,12 +917,6 @@ msgstr "<b>预定义导出设置</b>"
915
917
  msgid "Name"
916
918
  msgstr "名称"
917
919
 
918
- msgid "Open"
919
- msgstr "打开"
920
-
921
- msgid "Save"
922
- msgstr "保存"
923
-
924
920
  msgid "Listed Records"
925
921
  msgstr "已列出记录"
926
922
 
@@ -978,6 +974,9 @@ msgstr "保存和新建"
978
974
  msgid "Add and New"
979
975
  msgstr "添加和新建"
980
976
 
977
+ msgid "Save"
978
+ msgstr "保存"
979
+
981
980
  msgid "Add"
982
981
  msgstr "添加"
983
982
 
@@ -1003,6 +1002,10 @@ msgstr "已选记录取消删除 <Ins>"
1003
1002
  msgid "CSV Import: %s"
1004
1003
  msgstr "CSV导入: %s"
1005
1004
 
1005
+ #, fuzzy
1006
+ msgid "Import"
1007
+ msgstr "报告(_R)"
1008
+
1006
1009
  msgid "_Auto-Detect"
1007
1010
  msgstr "自动设置(_A)"
1008
1011
 
@@ -1206,6 +1209,10 @@ msgstr "添加已存在记录"
1206
1209
  msgid "Remove selected record"
1207
1210
  msgstr "删除所选记录"
1208
1211
 
1212
+ #, fuzzy
1213
+ msgid "Restore selected record"
1214
+ msgstr "删除所选记录"
1215
+
1209
1216
  msgid "Open the record <F2>"
1210
1217
  msgstr "打开记录 <F2>"
1211
1218
 
@@ -1221,6 +1228,10 @@ msgstr "编辑所选记录<F2>"
1221
1228
  msgid "Delete selected record"
1222
1229
  msgstr "删除所选记录"
1223
1230
 
1231
+ #, fuzzy
1232
+ msgid "Undelete selected record"
1233
+ msgstr "删除所选记录"
1234
+
1224
1235
  #, python-format
1225
1236
  msgid "%s%%"
1226
1237
  msgstr "%s%%"
tryton/gui/main.py CHANGED
@@ -463,10 +463,6 @@ class Main(Gtk.Application):
463
463
  'id': id_,
464
464
  })
465
465
 
466
- def _manage_favorites(widget):
467
- Window.create(self.menu_screen.model_name + '.favorite',
468
- mode=['tree', 'form'],
469
- name=_("Favorites"))
470
466
  try:
471
467
  favorites = RPCExecute('model',
472
468
  self.menu_screen.model_name + '.favorite', 'get',
@@ -477,11 +473,6 @@ class Main(Gtk.Application):
477
473
  menuitem = Gtk.MenuItem(label=name)
478
474
  menuitem.connect('activate', _action_favorite, id_)
479
475
  self.menu_favorite.add(menuitem)
480
- self.menu_favorite.add(Gtk.SeparatorMenuItem())
481
- manage_favorites = Gtk.MenuItem(label=_("Manage..."),
482
- use_underline=True)
483
- manage_favorites.connect('activate', _manage_favorites)
484
- self.menu_favorite.add(manage_favorites)
485
476
  self.menu_favorite.show_all()
486
477
 
487
478
  def favorite_unset(self):
@@ -772,6 +763,9 @@ class Main(Gtk.Application):
772
763
  expand=True, fill=True, padding=0)
773
764
  treeview = screen.current_view.treeview
774
765
  treeview.set_headers_visible(False)
766
+ for col in treeview.get_columns():
767
+ if col._type in {'selection', 'optional', 'drag'}:
768
+ col.set_visible(False)
775
769
 
776
770
  # Favorite column
777
771
  column = Gtk.TreeViewColumn()
@@ -864,7 +858,14 @@ class Main(Gtk.Application):
864
858
  halign=Gtk.Align.START)
865
859
  self.tooltips.set_tip(label, page.name)
866
860
  self.tooltips.enable()
867
- hbox.pack_start(label, expand=True, fill=True, padding=0)
861
+
862
+ def remove_book_middle_click(widget, event):
863
+ if event.button == 2:
864
+ return self._sig_remove_book(widget, page.widget)
865
+ eventbox = Gtk.EventBox()
866
+ eventbox.connect('button-press-event', remove_book_middle_click)
867
+ eventbox.add(label)
868
+ hbox.pack_start(eventbox, expand=True, fill=True, padding=0)
868
869
 
869
870
  button = Gtk.Button()
870
871
  button.set_relief(Gtk.ReliefStyle.NONE)
@@ -11,7 +11,7 @@ from tryton.common import get_toplevel_window
11
11
  from tryton.config import CONFIG, PIXMAPS_DIR
12
12
 
13
13
  COPYRIGHT = '''\
14
- Copyright (C) 2004-2025 Tryton.
14
+ Copyright (C) 2004-2024 Tryton.
15
15
  '''
16
16
  AUTHORS = [
17
17
  'Bertrand Chenal <bertrand.chenal@b2ck.com>',
@@ -106,7 +106,7 @@ class Email(NoModal):
106
106
  self.dialog.set_default_size(*self.default_size())
107
107
  self.dialog.connect('response', self.response)
108
108
 
109
- self.dialog.set_title(_('E-mail %s') % name)
109
+ self.dialog.set_title(_('Email %s') % name)
110
110
 
111
111
  grid = Gtk.Grid(
112
112
  column_spacing=3, row_spacing=3,
tryton/gui/window/form.py CHANGED
@@ -579,6 +579,7 @@ class Form(TabContent):
579
579
  name = str(position) if position else '_'
580
580
  selected = len(self.screen.selected_records)
581
581
  view_type = self.screen.current_view.view_type
582
+ next_view_type = self.screen.next_view_type
582
583
  has_views = self.screen.number_of_views > 1
583
584
  if selected > 1:
584
585
  name += '#%i' % selected
@@ -594,7 +595,9 @@ class Form(TabContent):
594
595
  for b in self.screen.get_buttons())
595
596
  set_sensitive(button_id, bool(position) and can_be_sensitive)
596
597
  set_sensitive(
597
- 'switch', (position or view_type == 'form') and has_views)
598
+ 'switch',
599
+ (position or view_type == 'form' or next_view_type != 'form')
600
+ and has_views)
598
601
  set_sensitive('remove', self.screen.deletable)
599
602
  set_sensitive('previous', self.screen.has_prev())
600
603
  set_sensitive('next', self.screen.has_next())
@@ -141,8 +141,8 @@ class TabContent(InfoBar):
141
141
  accel_path='<tryton>/Form/Print'),
142
142
  ToolbarItem(
143
143
  id='email',
144
- label=_("_E-Mail..."),
145
- tooltip=_("Send an e-mail using the record"),
144
+ label=_("_Email..."),
145
+ tooltip=_("Send an email using the record"),
146
146
  icon_name='tryton-email',
147
147
  accel_path='<tryton>/Form/Email'),
148
148
  None,
@@ -621,10 +621,12 @@ class O2MField(Field):
621
621
  from .group import Group
622
622
  parent_name = self.attrs.get('relation_field', '')
623
623
  fields = fields or {}
624
+ context = record.expr_eval(self.attrs.get('context', {}))
624
625
  group = Group(self.attrs['relation'], fields,
625
626
  parent=record,
626
627
  parent_name=parent_name,
627
628
  child_name=self.name,
629
+ context=context,
628
630
  parent_datetime_field=self.attrs.get('datetime_field'))
629
631
  if not fields and record.model_name == self.attrs['relation']:
630
632
  group.fields = record.group.fields
@@ -128,6 +128,7 @@ class Group(list):
128
128
  self._group_list_changed('record-removed', record, idx)
129
129
  super(Group, self).remove(record)
130
130
  del self.__id2record[record.id]
131
+ record.destroy()
131
132
 
132
133
  def clear(self):
133
134
  # Use reversed order to minimize the cursor reposition as the cursor
@@ -408,7 +409,9 @@ class Group(list):
408
409
  if record not in self.record_deleted:
409
410
  self.record_deleted.append(record)
410
411
  record.modified_fields.setdefault('id')
411
- if record.id < 0 or force_remove:
412
+ if (record.id < 0
413
+ or (self.parent and self.parent.id < 0)
414
+ or force_remove):
412
415
  self._remove(record)
413
416
 
414
417
  if len(self):
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
17
  def get_x2m_sub_fields(f_attrs, prefix):
18
- if f_attrs.get('visible') and f_attrs.get('views'):
18
+ if f_attrs['loading'] == 'eager' and f_attrs.get('views'):
19
19
  sub_fields = functools.reduce(
20
20
  operator.or_,
21
21
  (v.get('fields', {}) for v in f_attrs['views'].values()),
@@ -177,10 +177,10 @@ class Record:
177
177
  for record in id2record.values():
178
178
  record.exception = True
179
179
  if process_exception:
180
- values = [{'id': x} for x in id2record]
180
+ values = []
181
181
  default_values = {f: None for f in fnames if f != 'id'}
182
- for value in values:
183
- value.update(default_values)
182
+ for id in id2record:
183
+ values.append({'id': id, **default_values})
184
184
  else:
185
185
  raise
186
186
  id2value = dict((value['id'], value) for value in values)
@@ -299,20 +299,13 @@ class Record:
299
299
  return self.group.fields
300
300
 
301
301
  def _check_load(self, fields=None):
302
- if fields is not None:
303
- if not self.get_loaded(fields):
304
- self.reload(fields)
305
- return True
306
- return False
307
- if not self.loaded:
308
- self.reload()
309
- return True
310
- return False
302
+ if not self.get_loaded(fields):
303
+ self.reload(fields)
311
304
 
312
305
  def get_loaded(self, fields=None):
313
- if fields:
314
- return set(fields) <= (self._loaded | set(self.modified_fields))
315
- return set(self.group.fields.keys()) == self._loaded
306
+ if fields is None:
307
+ fields = self.group.fields.keys()
308
+ return set(fields) <= (self._loaded | set(self.modified_fields))
316
309
 
317
310
  loaded = property(get_loaded)
318
311
 
@@ -413,7 +406,6 @@ class Record:
413
406
  return self.id
414
407
 
415
408
  def default_get(self, defaults=None):
416
- vals = {}
417
409
  if len(self.group.fields):
418
410
  context = self.get_context()
419
411
  if defaults is not None:
@@ -423,7 +415,7 @@ class Record:
423
415
  vals = RPCExecute('model', self.model_name, 'default_get',
424
416
  list(self.group.fields.keys()), context=context)
425
417
  except RPCException:
426
- return vals
418
+ return
427
419
  if (self.parent
428
420
  and self.parent_name in self.group.fields):
429
421
  parent_field = self.group.fields[self.parent_name]
@@ -444,10 +436,7 @@ class Record:
444
436
  return ''
445
437
 
446
438
  def validate(self, fields=None, softvalidation=False, pre_validate=None):
447
- if isinstance(fields, list) and fields:
448
- self._check_load(fields)
449
- elif fields is None:
450
- self._check_load()
439
+ self._check_load(fields)
451
440
  res = True
452
441
  for field_name, field in list(self.group.fields.items()):
453
442
  if fields is not None and field_name not in fields:
@@ -617,7 +606,6 @@ class Record:
617
606
  values.update(self._get_on_change_args(on_change))
618
607
 
619
608
  if values:
620
- values['id'] = self.id
621
609
  try:
622
610
  if len(fieldnames) == 1 or 'id' not in values:
623
611
  changes = []
@@ -679,7 +667,6 @@ class Record:
679
667
  'on_change_with_' + fieldname,
680
668
  values, context=self.get_context()))
681
669
  else:
682
- values['id'] = self.id
683
670
  changed = RPCExecute(
684
671
  'model', self.model_name, 'on_change_with',
685
672
  values, list(fieldnames), context=self.get_context())
@@ -702,7 +689,6 @@ class Record:
702
689
  'on_change_with_' + fieldname,
703
690
  values, context=self.get_context()))
704
691
  else:
705
- values['id'] = self.id
706
692
  changed = RPCExecute(
707
693
  'model', self.model_name, 'on_change_with',
708
694
  values, list(later), context=self.get_context())
@@ -734,7 +720,6 @@ class Record:
734
720
  def on_scan_code(self, code, depends):
735
721
  depends = self.expr_eval(depends)
736
722
  values = self._get_on_change_args(depends)
737
- values['id'] = self.id
738
723
  try:
739
724
  changes = RPCExecute(
740
725
  'model', self.model_name, 'on_scan_code', values, code,
@@ -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()
@@ -323,6 +323,7 @@ class Screen:
323
323
  if only_ids:
324
324
  return ids
325
325
  self.clear()
326
+ GLib.idle_add(self.screen_container.search_entry.grab_focus)
326
327
  self.load(ids)
327
328
  self.count_tab_domain()
328
329
  return bool(ids)
@@ -541,6 +542,15 @@ class Screen:
541
542
  def view_index(self):
542
543
  return self.__current_view
543
544
 
545
+ @property
546
+ def next_view_type(self):
547
+ views = self.views + self.view_to_load
548
+ next_view_index = (self.view_index + 1) % len(views)
549
+ next_view = views[next_view_index]
550
+ if not isinstance(next_view, str):
551
+ next_view = next_view.view_type
552
+ return next_view
553
+
544
554
  def switch_view(
545
555
  self, view_type=None, view_id=None, creatable=None, display=True):
546
556
  if view_id is not None:
@@ -792,6 +802,7 @@ class Screen:
792
802
  records = records or self.selected_records
793
803
  if not records:
794
804
  return
805
+ current_record = self.current_record
795
806
  if delete:
796
807
  # Must delete children records before parent
797
808
  records.sort(key=lambda r: r.depth, reverse=True)
@@ -805,9 +816,6 @@ class Screen:
805
816
  record.group.remove(
806
817
  record, remove=remove, modified=False,
807
818
  force_remove=force_remove)
808
- # set current_record to None to prevent __select_changed
809
- # to set deleted record as current_record
810
- self.current_record = None
811
819
  # call only once
812
820
  record.set_modified()
813
821
 
@@ -822,7 +830,10 @@ class Screen:
822
830
  record.parent.save(force_reload=False)
823
831
  record.destroy()
824
832
 
825
- self.current_record = None
833
+ if current_record and not current_record.destroyed:
834
+ self.current_record = current_record
835
+ else:
836
+ self.current_record = None
826
837
  self.set_cursor()
827
838
  self.display()
828
839
  return True
@@ -1249,7 +1260,6 @@ class Screen:
1249
1260
  record = self.current_record
1250
1261
  args = record.expr_eval(button.get('change', []))
1251
1262
  values = record._get_on_change_args(args)
1252
- values['id'] = record.id
1253
1263
  try:
1254
1264
  changes = RPCExecute('model', self.model_name, button['name'],
1255
1265
  values, context=self.context)
@@ -230,8 +230,8 @@ class FormXMLViewParser(XMLViewParser):
230
230
  self.container.add(None, attributes)
231
231
  return
232
232
 
233
- if int(attributes.get('visible', 0)):
234
- self.field_attrs[name]['visible'] = True
233
+ if attributes.get('loading') == 'eager':
234
+ self.field_attrs[name]['loading'] = 'eager'
235
235
 
236
236
  widget = self.WIDGETS[attributes['widget']](self.view, attributes)
237
237
  self.view.widgets[name].append(widget)
@@ -382,6 +382,7 @@ class FormXMLViewParser(XMLViewParser):
382
382
  group = Container.constructor(
383
383
  int(attributes.get('col', 4)),
384
384
  attributes.get('homogeneous', False))
385
+ self.parse_child(node, group)
385
386
 
386
387
  if 'name' in attributes and attributes['name'] == self.exclude_field:
387
388
  self.container.add(None, attributes)
@@ -405,9 +406,6 @@ class FormXMLViewParser(XMLViewParser):
405
406
  bool(attributes.get('yexpand'))))
406
407
  self.view.state_widgets.append(widget)
407
408
  self.container.add(widget, attributes)
408
- # Parse the children at the end to preserve the order of the state
409
- # widgets
410
- self.parse_child(node, group)
411
409
 
412
410
  def _parse_hpaned(self, node, attributes):
413
411
  self._parse_paned(node, attributes, Gtk.HPaned)
@@ -224,17 +224,24 @@ class DictMultiSelectionEntry(DictEntry):
224
224
  name = str(name)
225
225
  model.append((value, name))
226
226
 
227
- 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)
228
232
  name_cell = Gtk.CellRendererText()
229
- name_column.pack_start(name_cell, expand=True)
230
- name_column.add_attribute(name_cell, 'text', 1)
231
- 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)
232
236
 
233
237
  return widget
234
238
 
239
+ def _select_data_func(self, column, cell, model, iter_, selection):
240
+ cell.set_property('active', selection.iter_is_selected(iter_))
241
+
235
242
  def get_value(self):
236
243
  model, paths = self.tree.get_selection().get_selected_rows()
237
- return [model[path][0] for path in paths]
244
+ return [model[path][0] for path in paths] or None
238
245
 
239
246
  def set_value(self, value):
240
247
  value2path = {v: idx for idx, (v, _) in enumerate(self.selection)}
@@ -1,5 +1,6 @@
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
5
 
5
6
  from gi.repository import Gdk, GLib, Gtk
@@ -15,6 +16,8 @@ from tryton.common import data2pixbuf, resize_pixbuf
15
16
  from .binary import BinaryMixin
16
17
  from .widget import Widget
17
18
 
19
+ logger = logging.getLogger(__name__)
20
+
18
21
 
19
22
  class Document(BinaryMixin, Widget):
20
23
  expand = True
@@ -90,5 +93,8 @@ class Document(BinaryMixin, Widget):
90
93
  model.set_document(document)
91
94
  self.evince_view.set_model(model)
92
95
  except GLib.GError:
96
+ logger.warning(
97
+ f"Could not open document {filename}",
98
+ exc_info=True)
93
99
  self.evince_view.set_model(EvinceView.DocumentModel())
94
100
  self.evince_scroll.hide()
@@ -82,6 +82,14 @@ class Many2Many(Widget):
82
82
  self.but_remove.set_relief(Gtk.ReliefStyle.NONE)
83
83
  hbox.pack_start(self.but_remove, expand=False, fill=False, padding=0)
84
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
+
85
93
  tooltips.enable()
86
94
 
87
95
  frame = Gtk.Frame()
@@ -130,6 +138,9 @@ class Many2Many(Widget):
130
138
  elif event.keyval in remove_keys and editable:
131
139
  self._sig_remove()
132
140
  return True
141
+ elif event.keyval == Gdk.KEY_Insert:
142
+ self._sig_unremove()
143
+ return True
133
144
  elif widget == self.wid_text:
134
145
  if event.keyval == Gdk.KEY_F3:
135
146
  self._sig_new()
@@ -166,6 +177,9 @@ class Many2Many(Widget):
166
177
  add_remove = self.record.expr_eval(self.attrs.get('add_remove'))
167
178
  if add_remove:
168
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)]
169
183
  context = self.field.get_search_context(self.record)
170
184
  order = self.field.get_search_order(self.record)
171
185
  value = self.wid_text.get_text()
@@ -194,6 +208,9 @@ class Many2Many(Widget):
194
208
  def _sig_remove(self, *args):
195
209
  self.screen.remove(remove=True)
196
210
 
211
+ def _sig_unremove(self, *args):
212
+ self.screen.unremove()
213
+
197
214
  def _on_activate(self):
198
215
  self._sig_edit()
199
216
 
@@ -284,11 +301,23 @@ class Many2Many(Widget):
284
301
  else:
285
302
  size_limit = False
286
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
+
287
310
  self.but_add.set_sensitive(bool(
288
311
  not self._readonly
289
312
  and not size_limit))
290
313
  self.but_remove.set_sensitive(bool(
291
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
292
321
  and self._position))
293
322
 
294
323
  def record_message(self, position, size, *args):
@@ -343,6 +372,9 @@ class Many2Many(Widget):
343
372
  add_remove = self.record.expr_eval(self.attrs.get('add_remove'))
344
373
  if add_remove:
345
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)]
346
378
  update_completion(
347
379
  self.wid_text, self.record, self.field, model, domain)
348
380
 
@@ -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: