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
@@ -141,7 +141,7 @@ class One2Many(Widget):
141
141
  hbox.pack_start(self.but_del, expand=False, fill=False, padding=0)
142
142
 
143
143
  self.but_undel = Gtk.Button(can_focus=False)
144
- tooltips.set_tip(self.but_undel, _('Undelete selected record <Ins>'))
144
+ tooltips.set_tip(self.but_undel, _("Undelete selected record"))
145
145
  self.but_undel.connect('clicked', self._sig_undelete)
146
146
  self.but_undel.add(common.IconFactory.get_image(
147
147
  'tryton-undo', Gtk.IconSize.SMALL_TOOLBAR))
@@ -295,7 +295,13 @@ class One2Many(Widget):
295
295
  if isinstance(self._position, int):
296
296
  first = self._position <= 1
297
297
  last = self._position >= self._length
298
- 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)
299
305
  view_type = self.screen.current_view.view_type
300
306
  has_views = self.screen.number_of_views > 1
301
307
 
@@ -313,15 +319,16 @@ class One2Many(Widget):
313
319
  self.but_undel.set_sensitive(bool(
314
320
  not self._readonly
315
321
  and not size_limit
322
+ and undeletable
316
323
  and self._position))
317
324
  self.but_open.set_sensitive(bool(
318
325
  self._position
319
326
  and self.read_access))
320
327
  self.but_next.set_sensitive(bool(
321
- self._position
328
+ self._length
322
329
  and not last))
323
330
  self.but_pre.set_sensitive(bool(
324
- self._position
331
+ self._length
325
332
  and not first))
326
333
  if self.attrs.get('add_remove'):
327
334
  self.but_add.set_sensitive(bool(
@@ -499,6 +506,9 @@ class One2Many(Widget):
499
506
  domain = self.field.domain_get(self.record)
500
507
  context = self.field.get_search_context(self.record)
501
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)]
502
512
  removed_ids = self.field.get_removed_ids(self.record)
503
513
  domain = ['OR', domain, ('id', 'in', removed_ids)]
504
514
  text = self.wid_text.get_text()
@@ -605,6 +615,9 @@ class One2Many(Widget):
605
615
  model = self.attrs['relation']
606
616
  domain = self.field.domain_get(self.record)
607
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)]
608
621
  removed_ids = self.field.get_removed_ids(self.record)
609
622
  domain = ['OR', domain, ('id', 'in', removed_ids)]
610
623
  update_completion(self.wid_text, self.record, self.field, model,
@@ -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)
@@ -5,6 +5,7 @@ import gettext
5
5
  import json
6
6
  import locale
7
7
  import sys
8
+ from collections import defaultdict
8
9
  from functools import wraps
9
10
  from io import StringIO
10
11
 
@@ -336,7 +337,7 @@ class TreeXMLViewParser(XMLViewParser):
336
337
  self.view.treeview.append_column(column)
337
338
 
338
339
  if 'optional' in attributes and name != self.exclude_field:
339
- self.view.optionals.append(column)
340
+ self.view.optionals[column.name].append(column)
340
341
 
341
342
  def _parse_button(self, node, attributes):
342
343
  button = Button(self.view, attributes)
@@ -437,7 +438,7 @@ class ViewTree(View):
437
438
 
438
439
  def __init__(self, view_id, screen, xml, children_field):
439
440
  self.children_field = children_field
440
- self.optionals = []
441
+ self.optionals = defaultdict(list)
441
442
  self.sum_widgets = []
442
443
  self.sum_box = Gtk.HBox()
443
444
  self.treeview = None
@@ -451,6 +452,8 @@ class ViewTree(View):
451
452
  grid_lines = Gtk.TreeViewGridLines.VERTICAL
452
453
 
453
454
  super().__init__(view_id, screen, xml)
455
+
456
+ self.set_selection_column()
454
457
  self.set_drag_and_drop()
455
458
 
456
459
  self.mnemonic_widget = self.treeview
@@ -519,21 +522,27 @@ class ViewTree(View):
519
522
  self.treeview.append_column(column)
520
523
 
521
524
  def optional_menu(self, column):
522
- def toggle(menuitem, column):
523
- 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)
524
529
  self.save_optional()
525
530
 
526
531
  widget = column.get_widget()
527
532
  menu = Gtk.Menu()
528
- for optional in self.optionals:
529
- menuitem = Gtk.CheckMenuItem(label=optional.get_title())
530
- menuitem.set_active(optional.get_visible())
531
- 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)
532
539
  menu.add(menuitem)
533
540
  popup(menu, widget)
534
541
 
535
542
  def save_optional(self):
536
- 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)
537
546
  try:
538
547
  RPCExecute(
539
548
  'model', 'ir.ui.view_tree_optional', 'set_optional',
@@ -616,6 +625,21 @@ class ViewTree(View):
616
625
  else:
617
626
  arrow.clear()
618
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
+
619
643
  def set_drag_and_drop(self):
620
644
  dnd = False
621
645
  if self.children_field:
@@ -741,7 +765,10 @@ class ViewTree(View):
741
765
  clipboard = self.treeview.get_clipboard(clipboard_type)
742
766
  selection = self.treeview.get_selection()
743
767
  data = StringIO()
744
- writer = csv.writer(data, delimiter='\t', lineterminator='\n')
768
+ lineterminator = (
769
+ '\n' if selection.count_selected_rows() > 1 else '')
770
+ writer = csv.writer(
771
+ data, delimiter='\t', lineterminator=lineterminator)
745
772
  selection.selected_foreach(copy_foreach, writer.writerow)
746
773
  clipboard.set_text(data.getvalue(), -1)
747
774
 
@@ -1067,12 +1094,9 @@ class ViewTree(View):
1067
1094
  elif tree_sel.get_mode() == Gtk.SelectionMode.MULTIPLE:
1068
1095
  model, paths = tree_sel.get_selected_rows()
1069
1096
  if model and paths:
1070
- records = []
1071
- for path in paths:
1072
- iter_ = model.get_iter(path)
1073
- records.append(model.get_value(iter_, 0))
1074
- if self.record not in records:
1075
- self.record = records[0]
1097
+ iter_ = model.get_iter(paths[0])
1098
+ record = model.get_value(iter_, 0)
1099
+ self.record = record
1076
1100
  else:
1077
1101
  self.record = None
1078
1102
 
@@ -1112,10 +1136,6 @@ class ViewTree(View):
1112
1136
  def display(self, force=False):
1113
1137
  self.treeview.display_counter += 1
1114
1138
  current_record = self.record
1115
- if current_record and current_record not in current_record.group:
1116
- # current record may have been removed by on_change calls without
1117
- # changing the current record of screen before the display
1118
- current_record = None
1119
1139
  if (force
1120
1140
  or not self.treeview.get_model()
1121
1141
  or self.group != self.treeview.get_model().group):
@@ -1129,9 +1149,14 @@ class ViewTree(View):
1129
1149
  selection.select_path(path)
1130
1150
  # The search column must be set each time the model is changed
1131
1151
  self.treeview.set_search_column(0)
1152
+ selection = self.treeview.get_selection()
1132
1153
  if not current_record:
1133
- selection = self.treeview.get_selection()
1134
1154
  selection.unselect_all()
1155
+ else:
1156
+ model = self.treeview.get_model()
1157
+ path = Gtk.TreePath(current_record.get_index_path(model.group))
1158
+ if not selection.path_is_selected(path):
1159
+ selection.select_path(path)
1135
1160
  self.treeview.queue_draw()
1136
1161
  if self.editable:
1137
1162
  self.set_state()
@@ -1155,7 +1180,8 @@ class ViewTree(View):
1155
1180
  continue
1156
1181
  widget = self.get_column_widget(column)
1157
1182
  widget.set_editable()
1158
- if column.name in tree_column_optional:
1183
+ if ('optional' in widget.attrs
1184
+ and column.name in tree_column_optional):
1159
1185
  optional = tree_column_optional[column.name]
1160
1186
  else:
1161
1187
  optional = bool(int(widget.attrs.get('optional', '0')))
@@ -1174,7 +1200,7 @@ class ViewTree(View):
1174
1200
  column.set_visible(not unique or bool(self.children_field))
1175
1201
  if self.children_field:
1176
1202
  for i, column in enumerate(self.treeview.get_columns()):
1177
- if (self.draggable or self.optionals) and not i:
1203
+ if column._type in {'selection', 'optional', 'drag'}:
1178
1204
  continue
1179
1205
  if column.get_visible():
1180
1206
  self.treeview.set_expander_column(column)
@@ -1241,9 +1267,9 @@ class ViewTree(View):
1241
1267
  label.set_text(text)
1242
1268
 
1243
1269
  def set_cursor(self, new=False, reset_view=True):
1244
- self.treeview.grab_focus()
1245
1270
  model = self.treeview.get_model()
1246
1271
  if self.record and model and self.treeview.get_realized():
1272
+ self.treeview.grab_focus()
1247
1273
  path = self.record.get_index_path(model.group)
1248
1274
  if model.get_flags() & Gtk.TreeModelFlags.LIST_ONLY:
1249
1275
  path = (path[0],)
@@ -1267,6 +1293,8 @@ class ViewTree(View):
1267
1293
  records = []
1268
1294
  sel = self.treeview.get_selection()
1269
1295
  sel.selected_foreach(_func_sel_get, records)
1296
+ if not records and self.record:
1297
+ records.append(self.record)
1270
1298
  return records
1271
1299
 
1272
1300
  def get_selected_paths(self):
@@ -742,7 +742,7 @@ class _BinarySelect(_BinaryIcon):
742
742
  invisible = field.get_state_attrs(record).get('invisible', False)
743
743
  readonly = self.attrs.get('readonly',
744
744
  field.get_state_attrs(record).get('readonly', False))
745
- if readonly or size:
745
+ if readonly and size:
746
746
  cell.set_property('visible', False)
747
747
  else:
748
748
  cell.set_property('visible', not invisible)
@@ -185,8 +185,7 @@ class Selection(Gtk.ScrolledWindow):
185
185
 
186
186
  class ScreenContainer(object):
187
187
 
188
- def __init__(self, screen, tab_domain):
189
- self.screen = screen
188
+ def __init__(self, tab_domain):
190
189
  self.viewport = Gtk.Viewport()
191
190
  self.viewport.set_shadow_type(Gtk.ShadowType.NONE)
192
191
  self.vbox = Gtk.VBox(spacing=3)
@@ -352,9 +351,12 @@ class ScreenContainer(object):
352
351
  def widget_get(self):
353
352
  return self.vbox
354
353
 
355
- def show_filter(self):
354
+ def set_screen(self, screen):
355
+ self.screen = screen
356
356
  self.but_bookmark.set_sensitive(bool(list(self.bookmarks())))
357
357
  self.bookmark_match()
358
+
359
+ def show_filter(self):
358
360
  if self.filter_vbox:
359
361
  self.filter_vbox.show()
360
362
  if self.notebook:
@@ -7,8 +7,7 @@ import sys
7
7
 
8
8
  from gi.repository import Gdk, GObject, Gtk
9
9
 
10
- from tryton.common import IconFactory
11
- from tryton.common.underline import set_underline
10
+ from tryton.common import IconFactory, RPCExecute
12
11
  from tryton.config import TRYTON_ICON
13
12
  from tryton.gui import Main
14
13
  from tryton.gui.window.nomodal import NoModal
@@ -36,6 +35,11 @@ class WinCSV(NoModal):
36
35
  def __init__(self, *args, **kwargs):
37
36
  super(WinCSV, self).__init__(*args, **kwargs)
38
37
 
38
+ self.languages = RPCExecute(
39
+ 'model', 'ir.lang', 'search_read', [
40
+ ('translatable', '=', True),
41
+ ], 0, None, [('name', 'DESC')], ['code', 'name'])
42
+
39
43
  self.dialog = Gtk.Dialog(
40
44
  transient_for=self.parent, destroy_with_parent=True)
41
45
  Main().add_window(self.dialog)
@@ -172,16 +176,6 @@ class WinCSV(NoModal):
172
176
 
173
177
  self.add_csv_header_param(box)
174
178
 
175
- button_cancel = self.dialog.add_button(
176
- set_underline(_("Cancel")), Gtk.ResponseType.CANCEL)
177
- button_cancel.set_image(IconFactory.get_image(
178
- 'tryton-cancel', Gtk.IconSize.BUTTON))
179
-
180
- button_ok = self.dialog.add_button(
181
- set_underline(_("OK")), Gtk.ResponseType.OK)
182
- button_ok.set_image(IconFactory.get_image(
183
- 'tryton-ok', Gtk.IconSize.BUTTON))
184
-
185
179
  self.dialog.vbox.pack_start(
186
180
  dialog_vbox, expand=True, fill=True, padding=0)
187
181
 
@@ -14,16 +14,19 @@ from numbers import Number
14
14
  from gi.repository import Gdk, GObject, Gtk
15
15
 
16
16
  import tryton.common as common
17
- from tryton.common import RPCException, RPCExecute, tempfile
17
+ from tryton.common import IconFactory, RPCException, RPCExecute, tempfile
18
+ from tryton.common.underline import set_underline
18
19
  from tryton.config import CONFIG
19
20
  from tryton.gui.window.win_csv import WinCSV
20
21
  from tryton.jsonrpc import JSONEncoder
21
22
  from tryton.rpc import CONNECTION, clear_cache
22
23
 
24
+ from .infobar import InfoBar
25
+
23
26
  _ = gettext.gettext
24
27
 
25
28
 
26
- class WinExport(WinCSV):
29
+ class WinExport(WinCSV, InfoBar):
27
30
  "Window export"
28
31
 
29
32
  def __init__(self, name, screen):
@@ -32,8 +35,23 @@ class WinExport(WinCSV):
32
35
  self.fields = {}
33
36
  super(WinExport, self).__init__()
34
37
  self.dialog.set_title(_('CSV Export: %s') % name)
35
- # Hide as selected record is the default
36
- self.ignore_search_limit.hide()
38
+ self.dialog.vbox.pack_start(
39
+ self.create_info_bar(), expand=False, fill=True, padding=0)
40
+ # Trigger changed event to update ignore search limit
41
+ active_index = self.selected_records.get_active()
42
+ self.selected_records.set_active(-1)
43
+ self.selected_records.set_active(active_index)
44
+
45
+ self.dialog.add_button(
46
+ set_underline(_("Close")), Gtk.ResponseType.CANCEL).set_image(
47
+ IconFactory.get_image('tryton-close', Gtk.IconSize.BUTTON))
48
+ self.dialog.add_button(
49
+ set_underline(_("Open")), Gtk.ResponseType.OK).set_image(
50
+ IconFactory.get_image('tryton-open', Gtk.IconSize.BUTTON))
51
+ self.dialog.add_button(
52
+ set_underline(_("Save As...")), Gtk.ResponseType.ACCEPT).set_image(
53
+ IconFactory.get_image('tryton-save', Gtk.IconSize.BUTTON))
54
+ self.dialog.set_default_response(Gtk.ResponseType.OK)
37
55
 
38
56
  @property
39
57
  def model(self):
@@ -50,6 +68,10 @@ class WinExport(WinCSV):
50
68
  and self.screen.current_view.view_type == 'tree'
51
69
  and self.screen.current_view.children_field)
52
70
 
71
+ @property
72
+ def screen_has_selected(self):
73
+ return bool(self.screen.selected_records)
74
+
53
75
  def add_buttons(self, box):
54
76
  button_save_export = Gtk.Button(
55
77
  label=_('_Save Export'), stock=None, use_underline=True)
@@ -113,19 +135,13 @@ class WinExport(WinCSV):
113
135
  def add_chooser(self, box):
114
136
  hbox_csv_export = Gtk.HBox()
115
137
  box.pack_start(hbox_csv_export, expand=False, fill=True, padding=0)
116
- self.saveas = Gtk.ComboBoxText()
117
- hbox_csv_export.pack_start(
118
- self.saveas, expand=True, fill=True, padding=3)
119
- self.saveas.append_text(_("Open"))
120
- self.saveas.append_text(_("Save"))
121
- self.saveas.set_active(0)
122
138
 
123
139
  self.selected_records = Gtk.ComboBoxText()
124
140
  hbox_csv_export.pack_start(
125
141
  self.selected_records, expand=True, fill=True, padding=3)
126
142
  self.selected_records.append_text(_("Listed Records"))
127
143
  self.selected_records.append_text(_("Selected Records"))
128
- if not self.screen_is_tree:
144
+ if not self.screen_is_tree and self.screen_has_selected:
129
145
  self.selected_records.set_active(1)
130
146
  else:
131
147
  self.selected_records.set_active(0)
@@ -176,6 +192,13 @@ class WinExport(WinCSV):
176
192
  if '.' not in name:
177
193
  if field.get('relation'):
178
194
  self.model1.insert(node, 0, [None, ''])
195
+ elif field.get('translate', False):
196
+ for language in self.languages:
197
+ l_path = f"{path}:lang={language['code']}"
198
+ l_string = f"{string_} ({language['name']})"
199
+ self.model1.insert(
200
+ node, 0, [language['name'], l_path])
201
+ self.fields[l_path] = (l_string, None)
179
202
 
180
203
  def _get_fields(self, model):
181
204
  try:
@@ -334,7 +357,8 @@ class WinExport(WinCSV):
334
357
  self.model2.append((string_, name))
335
358
 
336
359
  def response(self, dialog, response):
337
- if response == Gtk.ResponseType.OK:
360
+ self.info_bar_clear()
361
+ if response in {Gtk.ResponseType.OK, Gtk.ResponseType.ACCEPT}:
338
362
  fields = []
339
363
  iter = self.model2.get_iter_first()
340
364
  while iter:
@@ -378,7 +402,7 @@ class WinExport(WinCSV):
378
402
  except RPCException:
379
403
  data = []
380
404
 
381
- if self.saveas.get_active():
405
+ if response == Gtk.ResponseType.ACCEPT:
382
406
  fname = common.file_selection(_('Save As...'),
383
407
  action=Gtk.FileChooserAction.SAVE)
384
408
  if fname:
@@ -386,15 +410,15 @@ class WinExport(WinCSV):
386
410
  else:
387
411
  fileno, fname = tempfile.mkstemp(
388
412
  '.csv', common.slugify(self.name) + '_')
389
- if self.export_csv(
390
- fname, data, paths, popup=False, header=header):
413
+ if self.export_csv(fname, data, paths, header=header):
391
414
  os.close(fileno)
392
415
  common.file_open(fname, 'csv')
393
416
  else:
394
417
  os.close(fileno)
395
- self.destroy()
418
+ else:
419
+ self.destroy()
396
420
 
397
- def export_csv(self, fname, data, paths, popup=True, header=False):
421
+ def export_csv(self, fname, data, paths, header=False):
398
422
  encoding = self.csv_enc.get_active_text() or 'utf_8_sig'
399
423
  locale_format = self.csv_locale.get_active()
400
424
 
@@ -408,14 +432,14 @@ class WinExport(WinCSV):
408
432
  if row:
409
433
  writer.writerow(self.format_row(
410
434
  row, indent=indent, locale_format=locale_format))
411
- if popup:
412
- size = len(data)
413
- if header:
414
- size -= 1
415
- if size <= 1:
416
- common.message(_('%d record saved.') % size)
417
- else:
418
- common.message(_('%d records saved.') % size)
435
+ size = len(data)
436
+ if header:
437
+ size -= 1
438
+ if size <= 1:
439
+ message = _('%d record saved.') % size
440
+ else:
441
+ message = _('%d records saved.') % size
442
+ self.info_bar_add(message, Gtk.MessageType.INFO)
419
443
  return True
420
444
  except (IOError, UnicodeEncodeError, csv.Error) as exception:
421
445
  common.warning(str(exception), _('Export failed'))
@@ -10,7 +10,8 @@ from decimal import Decimal
10
10
  from gi.repository import Gtk
11
11
 
12
12
  import tryton.common as common
13
- from tryton.common import RPCException, RPCExecute
13
+ from tryton.common import IconFactory, RPCException, RPCExecute
14
+ from tryton.common.underline import set_underline
14
15
  from tryton.gui.window.win_csv import WinCSV
15
16
 
16
17
  _ = gettext.gettext
@@ -29,6 +30,16 @@ class WinImport(WinCSV):
29
30
  super(WinImport, self).__init__()
30
31
  self.dialog.set_title(_('CSV Import: %s') % name)
31
32
 
33
+ button_cancel = self.dialog.add_button(
34
+ set_underline(_("Cancel")), Gtk.ResponseType.CANCEL)
35
+ button_cancel.set_image(IconFactory.get_image(
36
+ 'tryton-cancel', Gtk.IconSize.BUTTON))
37
+
38
+ button_ok = self.dialog.add_button(
39
+ set_underline(_("Import")), Gtk.ResponseType.OK)
40
+ button_ok.set_image(IconFactory.get_image(
41
+ 'tryton-import', Gtk.IconSize.BUTTON))
42
+
32
43
  def add_buttons(self, box):
33
44
  button_autodetect = Gtk.Button(
34
45
  label=_('_Auto-Detect'), stock=None, use_underline=True)
@@ -67,22 +78,34 @@ class WinImport(WinCSV):
67
78
  fields_order = list(fields.keys())
68
79
  fields_order.sort(
69
80
  key=lambda x: fields[x].get('string', ''), reverse=True)
70
- for field in fields_order:
71
- if not fields[field].get('readonly', False) or field == 'id':
72
- self.fields_data[prefix_field + field] = fields[field]
73
- name = fields[field]['string'] or field
81
+ for field_name in fields_order:
82
+ field = fields[field_name]
83
+ if not field.get('readonly', False) or field_name == 'id':
84
+ self.fields_data[prefix_field + field_name] = field
85
+ name = field['string'] or field_name
74
86
  node = self.model1.insert(
75
- parent_node, 0, [name, prefix_field + field])
87
+ parent_node, 0, [name, prefix_field + field_name])
76
88
  name = prefix_name + name
77
89
  # Only One2Many can be nested for import
78
- if fields[field]['type'] == 'one2many':
79
- relation = fields[field].get('relation')
90
+ if field['type'] == 'one2many':
91
+ relation = field.get('relation')
80
92
  else:
81
93
  relation = None
82
- self.fields[prefix_field + field] = (name, relation)
83
- self.fields_invert[name] = prefix_field + field
94
+ self.fields[prefix_field + field_name] = (name, relation)
95
+ self.fields_invert[name] = prefix_field + field_name
84
96
  if relation:
85
97
  self.model1.insert(node, 0, [None, ''])
98
+ elif field.get('translate', False):
99
+ for language in self.languages:
100
+ l_field_name = f"{field_name}:lang={language['code']}"
101
+ self.model1.insert(
102
+ node, 0, [
103
+ language['name'], prefix_field + l_field_name])
104
+ l_name = prefix_name + name + f" ({language['name']})"
105
+ self.fields[prefix_field + l_field_name] = (
106
+ l_name, relation)
107
+ self.fields_invert[l_name] = (
108
+ prefix_field + l_field_name)
86
109
 
87
110
  def _get_fields(self, model):
88
111
  try:
@@ -185,7 +208,8 @@ class WinImport(WinCSV):
185
208
 
186
209
  fname = self.import_csv_file.get_filename()
187
210
  if fname:
188
- self.import_csv(fname, fields)
211
+ if not self.import_csv(fname, fields):
212
+ return
189
213
  self.destroy()
190
214
 
191
215
  def import_csv(self, fname, fields):
@@ -236,3 +260,4 @@ class WinImport(WinCSV):
236
260
  common.message(_('%d record imported.') % count)
237
261
  else:
238
262
  common.message(_('%d records imported.') % count)
263
+ return count
tryton/jsonrpc.py CHANGED
@@ -11,12 +11,18 @@ import logging
11
11
  import socket
12
12
  import ssl
13
13
  import threading
14
+ import time
14
15
  import xmlrpc.client
15
16
  from contextlib import contextmanager
16
17
  from decimal import Decimal
17
18
  from functools import partial, reduce
18
19
  from urllib.parse import quote, urljoin
19
20
 
21
+ try:
22
+ from http import HTTPStatus
23
+ except ImportError:
24
+ from http import client as HTTPStatus
25
+
20
26
  from .cache import CacheDict
21
27
  from .config import CONFIG
22
28
 
@@ -288,12 +294,28 @@ class ServerProxy(xmlrpc.client.ServerProxy):
288
294
 
289
295
  try:
290
296
  try:
291
- response = self.__transport.request(
292
- self.__host,
293
- self.__handler,
294
- request,
295
- verbose=self.__verbose
296
- )
297
+ for i in range(5):
298
+ try:
299
+ response = self.__transport.request(
300
+ self.__host,
301
+ self.__handler,
302
+ request,
303
+ verbose=self.__verbose
304
+ )
305
+ except xmlrpc.client.ProtocolError as e:
306
+ if e.errcode == HTTPStatus.SERVICE_UNAVAILABLE:
307
+ try:
308
+ delay = int(e.headers.get('Retry-After', i))
309
+ except ValueError:
310
+ if isinstance(
311
+ e.errcode, HTTPStatus.GATEWAY_TIMEOUT):
312
+ # Do not retry on timeout without delay
313
+ raise
314
+ delay = i
315
+ delay = min(delay, 10)
316
+ time.sleep(delay)
317
+ continue
318
+ break
297
319
  except (socket.error, http.client.HTTPException) as v:
298
320
  if (isinstance(v, socket.error)
299
321
  and v.args[0] == errno.EPIPE):
tryton/pyson.py CHANGED
@@ -712,7 +712,8 @@ class DateTime(Date):
712
712
  and not isinstance(now, datetime.datetime)):
713
713
  now = datetime.datetime.combine(now, datetime.time())
714
714
  if not isinstance(now, datetime.datetime):
715
- now = datetime.datetime.utcnow()
715
+ now = datetime.datetime.now(
716
+ datetime.timezone.utc).replace(tzinfo=None)
716
717
  return now + relativedelta(
717
718
  year=dct['y'],
718
719
  month=dct['M'],