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
@@ -75,7 +75,6 @@ class One2Many(Widget):
75
75
 
76
76
  hbox.pack_start(Gtk.VSeparator(), expand=False, fill=True, padding=0)
77
77
 
78
- self.focus_out = True
79
78
  self.wid_completion = None
80
79
  if attrs.get('add_remove'):
81
80
 
@@ -142,7 +141,7 @@ class One2Many(Widget):
142
141
  hbox.pack_start(self.but_del, expand=False, fill=False, padding=0)
143
142
 
144
143
  self.but_undel = Gtk.Button(can_focus=False)
145
- tooltips.set_tip(self.but_undel, _('Undelete selected record <Ins>'))
144
+ tooltips.set_tip(self.but_undel, _("Undelete selected record"))
146
145
  self.but_undel.connect('clicked', self._sig_undelete)
147
146
  self.but_undel.add(common.IconFactory.get_image(
148
147
  'tryton-undo', Gtk.IconSize.SMALL_TOOLBAR))
@@ -182,6 +181,8 @@ class One2Many(Widget):
182
181
  if self.attrs.get('add_remove'):
183
182
  self.wid_text.connect('key_press_event', self.on_keypress)
184
183
 
184
+ self._popup = False
185
+
185
186
  def get_access(self, type_):
186
187
  model = self.attrs['relation']
187
188
  if model:
@@ -294,7 +295,13 @@ class One2Many(Widget):
294
295
  if isinstance(self._position, int):
295
296
  first = self._position <= 1
296
297
  last = self._position >= self._length
297
- deletable = self.screen.deletable
298
+ deletable = (
299
+ self.screen.deletable
300
+ and any(
301
+ not r.deleted and not r.removed
302
+ for r in self.screen.selected_records))
303
+ undeletable = any(
304
+ r.deleted or r.removed for r in self.screen.selected_records)
298
305
  view_type = self.screen.current_view.view_type
299
306
  has_views = self.screen.number_of_views > 1
300
307
 
@@ -312,15 +319,16 @@ class One2Many(Widget):
312
319
  self.but_undel.set_sensitive(bool(
313
320
  not self._readonly
314
321
  and not size_limit
322
+ and undeletable
315
323
  and self._position))
316
324
  self.but_open.set_sensitive(bool(
317
325
  self._position
318
326
  and self.read_access))
319
327
  self.but_next.set_sensitive(bool(
320
- self._position
328
+ self._length
321
329
  and not last))
322
330
  self.but_pre.set_sensitive(bool(
323
- self._position
331
+ self._length
324
332
  and not first))
325
333
  if self.attrs.get('add_remove'):
326
334
  self.but_add.set_sensitive(bool(
@@ -370,12 +378,17 @@ class One2Many(Widget):
370
378
  self._new_single(defaults)
371
379
 
372
380
  def _new_single(self, defaults=None):
381
+ if self._popup:
382
+ return
383
+ else:
384
+ self._popup = True
373
385
  sequence = self._sequence()
374
386
 
375
387
  def update_sequence():
376
388
  if sequence:
377
389
  self.screen.group.set_sequence(
378
390
  field=sequence, position=self.screen.new_position)
391
+ self._popup = False
379
392
 
380
393
  if self.screen.current_view.creatable:
381
394
  self.screen.new()
@@ -392,6 +405,10 @@ class One2Many(Widget):
392
405
  fields = self.attrs['product'].split(',')
393
406
  product = {}
394
407
 
408
+ if self._popup:
409
+ return
410
+ else:
411
+ self._popup = True
395
412
  first = self.screen.new(default=False)
396
413
  default = first.default_get(defaults=defaults)
397
414
  first.set_default(default)
@@ -420,6 +437,7 @@ class One2Many(Widget):
420
437
  win_search.show()
421
438
 
422
439
  def make_product():
440
+ self._popup = False
423
441
  self.screen.group.remove(first, remove=True)
424
442
  if not product:
425
443
  return
@@ -448,7 +466,14 @@ class One2Many(Widget):
448
466
  return
449
467
  record = self.screen.current_record
450
468
  if record:
451
- WinForm(self.screen, lambda a: None)
469
+ if self._popup:
470
+ return
471
+ else:
472
+ self._popup = True
473
+
474
+ def callback(result):
475
+ self._popup = False
476
+ WinForm(self.screen, callback)
452
477
 
453
478
  def _sig_next(self, widget):
454
479
  if not self._validate():
@@ -475,24 +500,27 @@ class One2Many(Widget):
475
500
  self.screen.unremove()
476
501
 
477
502
  def _sig_add(self, *args):
478
- if not self.focus_out:
479
- return
480
503
  if not self.write_access or not self.read_access:
481
504
  return
482
505
  self.view.set_value()
483
506
  domain = self.field.domain_get(self.record)
484
507
  context = self.field.get_search_context(self.record)
485
508
  domain = [domain, self.record.expr_eval(self.attrs.get('add_remove'))]
509
+ existing_ids = self.field.get_eval(self.record)
510
+ if existing_ids:
511
+ domain = [domain, ('id', 'not in', existing_ids)]
486
512
  removed_ids = self.field.get_removed_ids(self.record)
487
513
  domain = ['OR', domain, ('id', 'in', removed_ids)]
488
514
  text = self.wid_text.get_text()
489
515
 
490
- self.focus_out = False
516
+ if self._popup:
517
+ return
518
+ else:
519
+ self._popup = True
491
520
 
492
521
  sequence = self._sequence()
493
522
 
494
523
  def callback(result):
495
- self.focus_out = True
496
524
  if result:
497
525
  ids = [x[0] for x in result]
498
526
  self.screen.load(ids, modified=True)
@@ -501,6 +529,7 @@ class One2Many(Widget):
501
529
  field=sequence, position=self.screen.new_position)
502
530
  self.screen.set_cursor()
503
531
  self.wid_text.set_text('')
532
+ self._popup = False
504
533
 
505
534
  order = self.field.get_search_order(self.record)
506
535
  win = WinSearch(self.attrs['relation'], callback, sel_multi=True,
@@ -586,6 +615,9 @@ class One2Many(Widget):
586
615
  model = self.attrs['relation']
587
616
  domain = self.field.domain_get(self.record)
588
617
  domain = [domain, self.record.expr_eval(self.attrs.get('add_remove'))]
618
+ existing_ids = self.field.get_eval(self.record)
619
+ if existing_ids:
620
+ domain = [domain, ('id', 'not in', existing_ids)]
589
621
  removed_ids = self.field.get_removed_ids(self.record)
590
622
  domain = ['OR', domain, ('id', 'in', removed_ids)]
591
623
  update_completion(self.wid_text, self.record, self.field, model,
@@ -50,6 +50,8 @@ class Label(StateMixin, Gtk.Label):
50
50
  readonly = ((field and field.attrs.get('readonly'))
51
51
  or state_changes.get('readonly', not bool(field)))
52
52
  common.apply_label_attributes(self, readonly, required)
53
+ self.set_max_width_chars(80)
54
+ self.set_line_wrap(True)
53
55
 
54
56
 
55
57
  class VBox(StateMixin, Gtk.VBox):
@@ -187,14 +189,16 @@ class Link(StateMixin, Gtk.Button):
187
189
  ['AND', domain, tab_domain], 0, 100, context=context,
188
190
  callback=functools.partial(
189
191
  self._set_count, idx=i, current=self._current,
190
- counter=counter, label=label))
192
+ counter=counter, label=label),
193
+ process_exception=False)
191
194
  else:
192
195
  common.RPCExecute(
193
196
  'model', action['res_model'], 'search_count',
194
197
  domain, 0, 100, context=context,
195
198
  callback=functools.partial(
196
199
  self._set_count, current=self._current,
197
- counter=counter, label=label))
200
+ counter=counter, label=label),
201
+ process_exception=False)
198
202
 
199
203
  def _set_count(self, value, idx=0, current=None, counter=None, label=''):
200
204
  if current != self._current or not self.get_parent():
@@ -130,8 +130,10 @@ class HTML(Widget, TranslateMixin):
130
130
  self.button, expand=False, fill=False, padding=0)
131
131
 
132
132
  if attrs.get('translate'):
133
- self.widget.pack_start(
134
- self.translate_button(), expand=False, fill=False, padding=0)
133
+ self.button.set_image(common.IconFactory.get_image(
134
+ 'tryton-translate', Gtk.IconSize.SMALL_TOOLBAR))
135
+ self.button.set_always_show_image(True)
136
+ self.button.connect('clicked', self.translate)
135
137
 
136
138
  def uri(self, language=None):
137
139
  if not self.record or self.record.id < 0 or CONNECTION.url is None:
@@ -149,7 +151,8 @@ class HTML(Widget, TranslateMixin):
149
151
 
150
152
  def display(self):
151
153
  super().display()
152
- self.button.set_uri(self.uri())
154
+ if self.attrs.get('translate'):
155
+ self.button.set_uri(self.uri())
153
156
 
154
157
  def _readonly_set(self, value):
155
158
  super()._readonly_set(value)
@@ -158,6 +161,7 @@ class HTML(Widget, TranslateMixin):
158
161
 
159
162
  def translate_dialog(self, languages):
160
163
  languages = {l['name']: l['code'] for l in languages}
161
- result = selection(_('Choose a language'), languages)
164
+ result = selection(
165
+ _('Choose a language'), languages, default=CONFIG['client.lang'])
162
166
  if result:
163
167
  webbrowser.open(self.uri(language=result[1]), new=2)
@@ -12,6 +12,7 @@ from dateutil.relativedelta import relativedelta
12
12
  from gi.repository import Gdk, Gtk
13
13
 
14
14
  import tryton.rpc as rpc
15
+ from tryton import common
15
16
  from tryton.action import Action
16
17
  from tryton.common import COLOR_SCHEMES, generateColorscheme, hex2rgb
17
18
  from tryton.config import CONFIG
@@ -386,7 +387,8 @@ class Graph(Gtk.DrawingArea):
386
387
  if isinstance(value, datetime.timedelta):
387
388
  value = value.total_seconds()
388
389
  self.datas[x][key] += float(value or 0)
389
- date_format = self.view.screen.context.get('date_format', '%x')
390
+ date_format = common.date_format(
391
+ self.view.screen.context.get('date_format'))
390
392
  datetime_format = date_format + ' %X'
391
393
  if isinstance(minx, datetime.datetime):
392
394
  date = minx
@@ -1,10 +1,13 @@
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 csv
3
4
  import gettext
4
5
  import json
5
6
  import locale
6
7
  import sys
8
+ from collections import defaultdict
7
9
  from functools import wraps
10
+ from io import StringIO
8
11
 
9
12
  from gi.repository import Gdk, GLib, GObject, Gtk
10
13
  from pygtkcompat.generictreemodel import GenericTreeModel
@@ -333,8 +336,8 @@ class TreeXMLViewParser(XMLViewParser):
333
336
 
334
337
  self.view.treeview.append_column(column)
335
338
 
336
- if 'optional' in attributes:
337
- self.view.optionals.append(column)
339
+ if 'optional' in attributes and name != self.exclude_field:
340
+ self.view.optionals[column.name].append(column)
338
341
 
339
342
  def _parse_button(self, node, attributes):
340
343
  button = Button(self.view, attributes)
@@ -435,7 +438,7 @@ class ViewTree(View):
435
438
 
436
439
  def __init__(self, view_id, screen, xml, children_field):
437
440
  self.children_field = children_field
438
- self.optionals = []
441
+ self.optionals = defaultdict(list)
439
442
  self.sum_widgets = []
440
443
  self.sum_box = Gtk.HBox()
441
444
  self.treeview = None
@@ -449,6 +452,8 @@ class ViewTree(View):
449
452
  grid_lines = Gtk.TreeViewGridLines.VERTICAL
450
453
 
451
454
  super().__init__(view_id, screen, xml)
455
+
456
+ self.set_selection_column()
452
457
  self.set_drag_and_drop()
453
458
 
454
459
  self.mnemonic_widget = self.treeview
@@ -517,21 +522,27 @@ class ViewTree(View):
517
522
  self.treeview.append_column(column)
518
523
 
519
524
  def optional_menu(self, column):
520
- def toggle(menuitem, column):
521
- column.set_visible(menuitem.get_active())
525
+ def toggle(menuitem, columns):
526
+ visible = menuitem.get_active()
527
+ for column in columns:
528
+ column.set_visible(visible)
522
529
  self.save_optional()
523
530
 
524
531
  widget = column.get_widget()
525
532
  menu = Gtk.Menu()
526
- for optional in self.optionals:
527
- menuitem = Gtk.CheckMenuItem(label=optional.get_title())
528
- menuitem.set_active(optional.get_visible())
529
- menuitem.connect('toggled', toggle, optional)
533
+ for name, columns in self.optionals.items():
534
+ visible = any(c.get_visible() for c in columns)
535
+ title = ' / '.join({c.get_title() for c in columns})
536
+ menuitem = Gtk.CheckMenuItem(label=title)
537
+ menuitem.set_active(visible)
538
+ menuitem.connect('toggled', toggle, columns)
530
539
  menu.add(menuitem)
531
540
  popup(menu, widget)
532
541
 
533
542
  def save_optional(self):
534
- fields = {c.name: not c.get_visible() for c in self.optionals}
543
+ fields = {}
544
+ for name, columns in self.optionals.items():
545
+ fields[name] = all(not c.get_visible() for c in columns)
535
546
  try:
536
547
  RPCExecute(
537
548
  'model', 'ir.ui.view_tree_optional', 'set_optional',
@@ -614,6 +625,21 @@ class ViewTree(View):
614
625
  else:
615
626
  arrow.clear()
616
627
 
628
+ def set_selection_column(self):
629
+ def select_data_func(column, cell, model, iter_, selection):
630
+ cell.set_property('active', selection.iter_is_selected(iter_))
631
+
632
+ selection = self.treeview.get_selection()
633
+ column = Gtk.TreeViewColumn()
634
+ column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
635
+ column.name = None
636
+ column._type = 'selection'
637
+ cell = Gtk.CellRendererToggle()
638
+ cell.set_sensitive(False)
639
+ column.pack_start(cell, expand=False)
640
+ column.set_cell_data_func(cell, select_data_func, selection)
641
+ self.treeview.insert_column(column, 0)
642
+
617
643
  def set_drag_and_drop(self):
618
644
  dnd = False
619
645
  if self.children_field:
@@ -720,46 +746,47 @@ class ViewTree(View):
720
746
  record.cancel()
721
747
  iter_ = model.iter_next(iter_)
722
748
 
723
- def on_copy(self):
749
+ def on_copy(self, columns=None):
750
+ if columns is None:
751
+ columns = self.treeview.get_columns()
752
+
753
+ def copy_foreach(treemodel, path, iter, add):
754
+ record = treemodel.get_value(iter, 0)
755
+ values = []
756
+ for col in columns:
757
+ if not col.get_visible() or not col.name:
758
+ continue
759
+ widget = self.get_column_widget(col)
760
+ values.append(widget.get_textual_value(record))
761
+ add(values)
762
+
724
763
  for clipboard_type in [
725
764
  Gdk.SELECTION_CLIPBOARD, Gdk.SELECTION_PRIMARY]:
726
765
  clipboard = self.treeview.get_clipboard(clipboard_type)
727
766
  selection = self.treeview.get_selection()
728
- data = []
729
- selection.selected_foreach(self.copy_foreach, data)
730
- clipboard.set_text('\n'.join(data), -1)
731
-
732
- def copy_foreach(self, treemodel, path, iter, data):
733
- record = treemodel.get_value(iter, 0)
734
- values = []
735
- for col in self.treeview.get_columns():
736
- if not col.get_visible() or not col.name:
737
- continue
738
- widget = self.get_column_widget(col)
739
- values.append('"'
740
- + str(widget.get_textual_value(record)).replace('"', '""')
741
- + '"')
742
- data.append('\t'.join(values))
743
- return
767
+ data = StringIO()
768
+ lineterminator = (
769
+ '\n' if selection.count_selected_rows() > 1 else '')
770
+ writer = csv.writer(
771
+ data, delimiter='\t', lineterminator=lineterminator)
772
+ selection.selected_foreach(copy_foreach, writer.writerow)
773
+ clipboard.set_text(data.getvalue(), -1)
744
774
 
745
775
  def on_paste(self):
746
776
  if not self.editable:
747
777
  return
748
778
 
749
- def unquote(value):
750
- if value[:1] == '"' and value[-1:] == '"':
751
- return value[1:-1]
752
- return value
753
- data = []
779
+ reader = None
754
780
  for clipboard_type in [
755
781
  Gdk.SELECTION_CLIPBOARD, Gdk.SELECTION_PRIMARY]:
756
782
  clipboard = self.treeview.get_clipboard(clipboard_type)
757
783
  text = clipboard.wait_for_text()
758
784
  if not text:
759
785
  continue
760
- data = [[unquote(v) for v in l.split('\t')]
761
- for l in text.splitlines()]
786
+ reader = csv.reader(StringIO(text), delimiter='\t')
762
787
  break
788
+ else:
789
+ return
763
790
  col = self.treeview.get_cursor()[1]
764
791
  columns = [c for c in self.treeview.get_columns()
765
792
  if c.get_visible() and c.name]
@@ -774,7 +801,7 @@ class ViewTree(View):
774
801
  group = self.group
775
802
  idx = len(group)
776
803
  default = None
777
- for line in data:
804
+ for line in reader:
778
805
  if idx >= len(group):
779
806
  record = group.new(default=False)
780
807
  if default is None:
@@ -785,13 +812,25 @@ class ViewTree(View):
785
812
  for col, value in zip(columns, line):
786
813
  widget = self.get_column_widget(col)
787
814
  if widget.get_textual_value(record) != value:
788
- widget.value_from_text(record, value)
815
+ if hasattr(widget, 'search_remote'):
816
+ field = record.group.fields[widget.attrs['name']]
817
+ win = widget.search_remote(record, field, value)
818
+ if len(win.screen.group) == 1:
819
+ target, = win.screen.group
820
+ field.set_client(
821
+ record, (target.id, target.rec_name()))
822
+ else:
823
+ field.set_client(record, (None, ''))
824
+ win.response(win, Gtk.ResponseType.CANCEL)
825
+ else:
826
+ widget.value_from_text(record, value)
789
827
  if value and not widget.get_textual_value(record):
790
828
  # Stop setting value if a value is correctly set
791
829
  idx = len(group)
792
830
  break
793
831
  if not record.validate():
794
832
  break
833
+ record.save()
795
834
  idx += 1
796
835
  self.record = record
797
836
  self.screen.display(set_cursor=True)
@@ -900,12 +939,27 @@ class ViewTree(View):
900
939
  except TypeError:
901
940
  # Outside row
902
941
  return False
942
+ selection = treeview.get_selection()
903
943
  menu = Gtk.Menu()
904
- copy_item = Gtk.MenuItem(label=_('Copy'))
905
- copy_item.connect('activate', lambda x: self.on_copy())
906
- menu.append(copy_item)
944
+ if selection.count_selected_rows():
945
+ if selection.count_selected_rows() == 1:
946
+ copy_item = Gtk.MenuItem(label=_("Copy"))
947
+ copy_item.connect(
948
+ 'activate', lambda x: self.on_copy([col]))
949
+ menu.append(copy_item)
950
+ copy_row_label = _("Copy Row")
951
+ else:
952
+ copy_row_label = _("Copy Rows")
953
+ if col:
954
+ copy_column_item = Gtk.MenuItem(label=_("Copy Column"))
955
+ copy_column_item.connect(
956
+ 'activate', lambda x: self.on_copy(columns=[col]))
957
+ menu.append(copy_column_item)
958
+ copy_row_item = Gtk.MenuItem(label=copy_row_label)
959
+ copy_row_item.connect('activate', lambda x: self.on_copy())
960
+ menu.append(copy_row_item)
907
961
  if self.editable:
908
- paste_item = Gtk.MenuItem(label=_('Paste'))
962
+ paste_item = Gtk.MenuItem(label=_("Paste Rows"))
909
963
  paste_item.connect('activate', lambda x: self.on_paste())
910
964
  menu.append(paste_item)
911
965
 
@@ -939,7 +993,6 @@ class ViewTree(View):
939
993
  menu, model, record_id, title=label, field=field,
940
994
  context=context)
941
995
 
942
- selection = treeview.get_selection()
943
996
  if selection.count_selected_rows() == 1:
944
997
  group = self.group
945
998
  if selection.get_mode() == Gtk.SelectionMode.SINGLE:
@@ -1041,9 +1094,12 @@ class ViewTree(View):
1041
1094
  elif tree_sel.get_mode() == Gtk.SelectionMode.MULTIPLE:
1042
1095
  model, paths = tree_sel.get_selected_rows()
1043
1096
  if model and paths:
1044
- iter_ = model.get_iter(paths[0])
1045
- record = model.get_value(iter_, 0)
1046
- self.record = record
1097
+ records = []
1098
+ for path in paths:
1099
+ iter_ = model.get_iter(path)
1100
+ records.append(model.get_value(iter_, 0))
1101
+ if self.record not in records:
1102
+ self.record = records[0]
1047
1103
  else:
1048
1104
  self.record = None
1049
1105
 
@@ -1083,6 +1139,10 @@ class ViewTree(View):
1083
1139
  def display(self, force=False):
1084
1140
  self.treeview.display_counter += 1
1085
1141
  current_record = self.record
1142
+ if current_record and current_record not in current_record.group:
1143
+ # current record may have been removed by on_change calls without
1144
+ # changing the current record of screen before the display
1145
+ current_record = None
1086
1146
  if (force
1087
1147
  or not self.treeview.get_model()
1088
1148
  or self.group != self.treeview.get_model().group):
@@ -1096,9 +1156,14 @@ class ViewTree(View):
1096
1156
  selection.select_path(path)
1097
1157
  # The search column must be set each time the model is changed
1098
1158
  self.treeview.set_search_column(0)
1159
+ selection = self.treeview.get_selection()
1099
1160
  if not current_record:
1100
- selection = self.treeview.get_selection()
1101
1161
  selection.unselect_all()
1162
+ else:
1163
+ model = self.treeview.get_model()
1164
+ path = Gtk.TreePath(current_record.get_index_path(model.group))
1165
+ if not selection.path_is_selected(path):
1166
+ selection.select_path(path)
1102
1167
  self.treeview.queue_draw()
1103
1168
  if self.editable:
1104
1169
  self.set_state()
@@ -1122,7 +1187,8 @@ class ViewTree(View):
1122
1187
  continue
1123
1188
  widget = self.get_column_widget(column)
1124
1189
  widget.set_editable()
1125
- if column.name in tree_column_optional:
1190
+ if ('optional' in widget.attrs
1191
+ and column.name in tree_column_optional):
1126
1192
  optional = tree_column_optional[column.name]
1127
1193
  else:
1128
1194
  optional = bool(int(widget.attrs.get('optional', '0')))
@@ -1141,7 +1207,7 @@ class ViewTree(View):
1141
1207
  column.set_visible(not unique or bool(self.children_field))
1142
1208
  if self.children_field:
1143
1209
  for i, column in enumerate(self.treeview.get_columns()):
1144
- if (self.draggable or self.optionals) and not i:
1210
+ if column._type in {'selection', 'optional', 'drag'}:
1145
1211
  continue
1146
1212
  if column.get_visible():
1147
1213
  self.treeview.set_expander_column(column)
@@ -1208,9 +1274,9 @@ class ViewTree(View):
1208
1274
  label.set_text(text)
1209
1275
 
1210
1276
  def set_cursor(self, new=False, reset_view=True):
1211
- self.treeview.grab_focus()
1212
1277
  model = self.treeview.get_model()
1213
1278
  if self.record and model and self.treeview.get_realized():
1279
+ self.treeview.grab_focus()
1214
1280
  path = self.record.get_index_path(model.group)
1215
1281
  if model.get_flags() & Gtk.TreeModelFlags.LIST_ONLY:
1216
1282
  path = (path[0],)
@@ -1234,6 +1300,8 @@ class ViewTree(View):
1234
1300
  records = []
1235
1301
  sel = self.treeview.get_selection()
1236
1302
  sel.selected_foreach(_func_sel_get, records)
1303
+ if not records and self.record:
1304
+ records.append(self.record)
1237
1305
  return records
1238
1306
 
1239
1307
  def get_selected_paths(self):
@@ -163,7 +163,8 @@ class EditableTreeView(TreeView):
163
163
  for renderer in column.get_cells():
164
164
  if renderer.props.editing:
165
165
  widget = self.view.get_column_widget(column)
166
- self.on_editing_done(widget.editable, renderer)
166
+ editable = widget.get_editable(renderer)
167
+ self.on_editing_done(editable, renderer)
167
168
  return True
168
169
 
169
170
  def on_keypressed(self, entry, event, renderer):