tryton 6.6.8__py3-none-any.whl → 6.8.1__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 (92) hide show
  1. tryton/__init__.py +1 -1
  2. tryton/action/main.py +32 -43
  3. tryton/bus.py +2 -0
  4. tryton/client.py +3 -0
  5. tryton/common/button.py +3 -1
  6. tryton/common/common.py +55 -43
  7. tryton/common/datetime_.py +14 -2
  8. tryton/common/domain_inversion.py +10 -10
  9. tryton/common/domain_parser.py +5 -2
  10. tryton/common/popup_menu.py +7 -0
  11. tryton/common/selection.py +3 -1
  12. tryton/config.py +22 -5
  13. tryton/data/locale/bg/LC_MESSAGES/tryton.mo +0 -0
  14. tryton/data/locale/bg/LC_MESSAGES/tryton.po +45 -39
  15. tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
  16. tryton/data/locale/ca/LC_MESSAGES/tryton.po +41 -35
  17. tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
  18. tryton/data/locale/cs/LC_MESSAGES/tryton.po +46 -39
  19. tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
  20. tryton/data/locale/de/LC_MESSAGES/tryton.po +41 -35
  21. tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
  22. tryton/data/locale/es/LC_MESSAGES/tryton.po +41 -35
  23. tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
  24. tryton/data/locale/es_419/LC_MESSAGES/tryton.po +171 -167
  25. tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
  26. tryton/data/locale/et/LC_MESSAGES/tryton.po +47 -39
  27. tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
  28. tryton/data/locale/fa/LC_MESSAGES/tryton.po +46 -38
  29. tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
  30. tryton/data/locale/fi/LC_MESSAGES/tryton.po +38 -32
  31. tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
  32. tryton/data/locale/fr/LC_MESSAGES/tryton.po +42 -36
  33. tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
  34. tryton/data/locale/hu/LC_MESSAGES/tryton.po +44 -34
  35. tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
  36. tryton/data/locale/id/LC_MESSAGES/tryton.po +40 -34
  37. tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
  38. tryton/data/locale/it/LC_MESSAGES/tryton.po +44 -34
  39. tryton/data/locale/ja_JP/LC_MESSAGES/tryton.mo +0 -0
  40. tryton/data/locale/lo/LC_MESSAGES/tryton.mo +0 -0
  41. tryton/data/locale/lo/LC_MESSAGES/tryton.po +46 -38
  42. tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
  43. tryton/data/locale/lt/LC_MESSAGES/tryton.po +47 -37
  44. tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
  45. tryton/data/locale/nl/LC_MESSAGES/tryton.po +41 -35
  46. tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
  47. tryton/data/locale/pl/LC_MESSAGES/tryton.po +45 -35
  48. tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
  49. tryton/data/locale/pt/LC_MESSAGES/tryton.po +46 -38
  50. tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
  51. tryton/data/locale/ro/LC_MESSAGES/tryton.po +50 -48
  52. tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
  53. tryton/data/locale/ru/LC_MESSAGES/tryton.po +45 -39
  54. tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
  55. tryton/data/locale/sl/LC_MESSAGES/tryton.po +47 -38
  56. tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
  57. tryton/data/locale/tr/LC_MESSAGES/tryton.po +39 -33
  58. tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
  59. tryton/data/locale/uk/LC_MESSAGES/tryton.po +43 -35
  60. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
  61. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +45 -35
  62. tryton/data/pixmaps/tryton/tryton-icon.svg +1 -0
  63. tryton/gui/main.py +54 -61
  64. tryton/gui/window/dblogin.py +27 -10
  65. tryton/gui/window/form.py +21 -53
  66. tryton/gui/window/infobar.py +9 -4
  67. tryton/gui/window/log.py +95 -0
  68. tryton/gui/window/view_board/action.py +0 -4
  69. tryton/gui/window/view_form/model/field.py +36 -14
  70. tryton/gui/window/view_form/model/record.py +22 -9
  71. tryton/gui/window/view_form/screen/screen.py +45 -76
  72. tryton/gui/window/view_form/view/calendar_.py +24 -11
  73. tryton/gui/window/view_form/view/calendar_gtk/toolbar.py +6 -5
  74. tryton/gui/window/view_form/view/form.py +14 -5
  75. tryton/gui/window/view_form/view/form_gtk/many2many.py +10 -1
  76. tryton/gui/window/view_form/view/form_gtk/many2one.py +1 -0
  77. tryton/gui/window/view_form/view/form_gtk/one2many.py +7 -7
  78. tryton/gui/window/view_form/view/form_gtk/textbox.py +0 -2
  79. tryton/gui/window/view_form/view/form_gtk/widget.py +8 -10
  80. tryton/gui/window/view_form/view/list_form.py +61 -5
  81. tryton/gui/window/view_form/view/list_gtk/editabletree.py +13 -3
  82. tryton/gui/window/view_form/view/list_gtk/widget.py +97 -27
  83. tryton/gui/window/win_form.py +6 -5
  84. tryton/rpc.py +13 -15
  85. tryton/tests/test_common.py +46 -0
  86. tryton/tests/test_common_domain_parser.py +24 -24
  87. {tryton-6.6.8.dist-info → tryton-6.8.1.dist-info}/METADATA +6 -6
  88. {tryton-6.6.8.dist-info → tryton-6.8.1.dist-info}/RECORD +92 -89
  89. {tryton-6.6.8.data → tryton-6.8.1.data}/scripts/tryton +0 -0
  90. {tryton-6.6.8.dist-info → tryton-6.8.1.dist-info}/LICENSE +0 -0
  91. {tryton-6.6.8.dist-info → tryton-6.8.1.dist-info}/WHEEL +0 -0
  92. {tryton-6.6.8.dist-info → tryton-6.8.1.dist-info}/top_level.txt +0 -0
tryton/__init__.py CHANGED
@@ -1,6 +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
- __version__ = "6.6.8"
3
+ __version__ = "6.8.1"
4
4
  import locale
5
5
 
6
6
  import gi
tryton/action/main.py CHANGED
@@ -6,7 +6,6 @@ import webbrowser
6
6
  import tryton.rpc as rpc
7
7
  from tryton.common import (
8
8
  RPCException, RPCExecute, file_open, file_write, message, selection)
9
- from tryton.config import CONFIG
10
9
  from tryton.pyson import PYSONDecoder
11
10
 
12
11
  _ = gettext.gettext
@@ -91,14 +90,15 @@ class Action(object):
91
90
  return name
92
91
 
93
92
  data['action_id'] = action['id']
93
+ params = {
94
+ 'icon': action.get('icon.rec_name') or '',
95
+ }
94
96
  if action['type'] == 'ir.action.act_window':
95
- view_ids = []
96
- view_mode = None
97
97
  if action.get('views', []):
98
- view_ids = [x[0] for x in action['views']]
99
- view_mode = [x[1] for x in action['views']]
98
+ params['view_ids'] = [x[0] for x in action['views']]
99
+ params['view_mode'] = [x[1] for x in action['views']]
100
100
  elif action.get('view_id', False):
101
- view_ids = [action['view_id'][0]]
101
+ params['view_ids'] = [action['view_id'][0]]
102
102
 
103
103
  action.setdefault('pyson_domain', '[]')
104
104
  ctx = {
@@ -109,58 +109,47 @@ class Action(object):
109
109
  ctx.update(rpc.CONTEXT)
110
110
  ctx['_user'] = rpc._USER
111
111
  decoder = PYSONDecoder(ctx)
112
- action_ctx = context.copy()
113
- action_ctx.update(
112
+ params['context'] = context.copy()
113
+ params['context'].update(
114
114
  decoder.decode(action.get('pyson_context') or '{}'))
115
- ctx.update(action_ctx)
115
+ ctx.update(params['context'])
116
116
 
117
117
  ctx['context'] = ctx
118
118
  decoder = PYSONDecoder(ctx)
119
- domain = decoder.decode(action['pyson_domain'])
120
- order = decoder.decode(action['pyson_order'])
121
- search_value = decoder.decode(action['pyson_search_value'] or '[]')
122
- tab_domain = [(n, decoder.decode(d), c)
123
- for n, d, c in action['domains']]
119
+ params['domain'] = decoder.decode(action['pyson_domain'])
120
+ params['order'] = decoder.decode(action['pyson_order'])
121
+ params['search_value'] = decoder.decode(
122
+ action['pyson_search_value'] or '[]')
123
+ params['tab_domain'] = [
124
+ (n, decoder.decode(d), c) for n, d, c in action['domains']]
124
125
 
125
126
  name = action.get('name', '')
126
127
  if action.get('keyword', ''):
127
- name = add_name_suffix(name, action_ctx)
128
+ name = add_name_suffix(name, params['context'])
129
+ params['name'] = name
128
130
 
129
131
  res_model = action.get('res_model', data.get('res_model'))
130
- res_id = action.get('res_id', data.get('res_id'))
132
+ params['res_id'] = action.get('res_id', data.get('res_id'))
133
+ params['context_model'] = action.get('context_model')
134
+ params['context_domain'] = action.get('context_domain')
131
135
  limit = action.get('limit')
132
- if limit is None:
133
- limit = CONFIG['client.limit']
134
-
135
- Window.create(res_model,
136
- view_ids=view_ids,
137
- res_id=res_id,
138
- domain=domain,
139
- context=action_ctx,
140
- order=order,
141
- mode=view_mode,
142
- name=name,
143
- limit=limit,
144
- search_value=search_value,
145
- icon=(action.get('icon.rec_name') or ''),
146
- tab_domain=tab_domain,
147
- context_model=action['context_model'],
148
- context_domain=action['context_domain'])
136
+ if limit is not None:
137
+ params['limit'] = limit
138
+
139
+ Window.create(res_model, **params)
149
140
  elif action['type'] == 'ir.action.wizard':
141
+ params['context'] = context
142
+ params['window'] = action.get('window')
150
143
  name = action.get('name', '')
151
144
  if action.get('keyword', 'form_action') == 'form_action':
152
145
  name = add_name_suffix(name, context)
153
- Window.create_wizard(action['wiz_name'], data,
154
- direct_print=action.get('direct_print', False),
155
- name=name,
156
- context=context, icon=(action.get('icon.rec_name') or ''),
157
- window=action.get('window', False))
158
-
146
+ params['name'] = name
147
+ Window.create_wizard(action['wiz_name'], data, **params)
159
148
  elif action['type'] == 'ir.action.report':
160
- Action.exec_report(
161
- action['report_name'], data,
162
- direct_print=action.get('direct_print', False),
163
- context=context)
149
+ params['direct_print'] = action.get('direct_print', False)
150
+ params['context'] = context
151
+ del params['icon']
152
+ Action.exec_report(action['report_name'], data, **params)
164
153
 
165
154
  elif action['type'] == 'ir.action.url':
166
155
  if action['url']:
tryton/bus.py CHANGED
@@ -25,6 +25,8 @@ CHANNELS = [
25
25
 
26
26
 
27
27
  def listen(connection):
28
+ if not CONFIG['thread']:
29
+ return
28
30
  listener = threading.Thread(
29
31
  target=_listen, args=(connection,), daemon=True)
30
32
  listener.start()
tryton/client.py CHANGED
@@ -30,6 +30,9 @@ def main():
30
30
  label.editable {
31
31
  font-style: italic;
32
32
  }
33
+ label.warning {
34
+ color: @warning_color;
35
+ }
33
36
  .window-title, .wizard-title {
34
37
  font-size: large;
35
38
  font-weight: bold;
tryton/common/button.py CHANGED
@@ -37,7 +37,9 @@ class Button(Gtk.Button):
37
37
  self.hide()
38
38
  else:
39
39
  self.show()
40
- self.set_sensitive(not states.get('readonly', False))
40
+ self.set_sensitive(
41
+ not (record.readonly if record else False)
42
+ and not states.get('readonly', False))
41
43
  self._set_icon(states.get('icon', self.attrs.get('icon')))
42
44
 
43
45
  if self.attrs.get('rule'):
tryton/common/common.py CHANGED
@@ -23,7 +23,6 @@ try:
23
23
  except ImportError:
24
24
  from http import client as HTTPStatus
25
25
 
26
- import _thread
27
26
  import shlex
28
27
  import socket
29
28
  import sys
@@ -34,6 +33,7 @@ import urllib.request
34
33
  import webbrowser
35
34
  from functools import lru_cache, wraps
36
35
  from string import Template
36
+ from threading import Lock, Thread
37
37
 
38
38
  import tryton.rpc as rpc
39
39
  from tryton.config import CONFIG, PIXMAPS_DIR, TRYTON_ICON
@@ -43,9 +43,8 @@ try:
43
43
  except ImportError:
44
44
  ssl = None
45
45
  import zipfile
46
- from threading import Lock
47
46
 
48
- from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk
47
+ from gi.repository import Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk
49
48
 
50
49
  from tryton import __version__
51
50
  from tryton.exceptions import TrytonError, TrytonServerError
@@ -104,6 +103,8 @@ class IconFactory:
104
103
  try:
105
104
  icon_ref = (cls._name2id[iconname], iconname)
106
105
  except KeyError:
106
+ logger.error(f"Unknown icon {iconname}")
107
+ cls._icons[iconname] = None
107
108
  return
108
109
  idx = cls._tryton_icons.index(icon_ref)
109
110
  to_load = slice(max(0, idx - cls.batchnum // 2),
@@ -128,13 +129,14 @@ class IconFactory:
128
129
  colors = CONFIG['icon.colors'].split(',')
129
130
  cls.register_icon(iconname)
130
131
  if iconname not in cls._pixbufs[(size, badge)]:
132
+ data = None
131
133
  if iconname in cls._icons:
132
134
  data = cls._icons[iconname]
133
135
  elif iconname in cls._local_icons:
134
136
  path = cls._local_icons[iconname]
135
137
  with open(path, 'rb') as fp:
136
138
  data = fp.read()
137
- else:
139
+ if not data:
138
140
  logger.error("Unknown icon %s" % iconname)
139
141
  return
140
142
  if not color:
@@ -439,16 +441,8 @@ def file_selection(title, filename='',
439
441
  action=Gtk.FileChooserAction.OPEN, preview=True, multi=False,
440
442
  filters=None):
441
443
  parent = get_toplevel_window()
442
- if action == Gtk.FileChooserAction.OPEN:
443
- buttons = (set_underline(_("Cancel")), Gtk.ResponseType.CANCEL,
444
- set_underline(_("Select")), Gtk.ResponseType.OK)
445
- else:
446
- buttons = (set_underline(_("Cancel")), Gtk.ResponseType.CANCEL,
447
- set_underline(_("Save")), Gtk.ResponseType.OK)
448
- win = Gtk.FileChooserDialog(
444
+ win = Gtk.FileChooserNative(
449
445
  title=title, transient_for=parent, action=action)
450
- win.add_buttons(*buttons)
451
- win.set_icon(TRYTON_ICON)
452
446
  if filename:
453
447
  if action in (Gtk.FileChooserAction.SAVE,
454
448
  Gtk.FileChooserAction.CREATE_FOLDER):
@@ -459,7 +453,6 @@ def file_selection(title, filename='',
459
453
  if hasattr(win, 'set_do_overwrite_confirmation'):
460
454
  win.set_do_overwrite_confirmation(True)
461
455
  win.set_select_multiple(multi)
462
- win.set_default_response(Gtk.ResponseType.OK)
463
456
  if filters is not None:
464
457
  for filt in filters:
465
458
  win.add_filter(filt)
@@ -484,7 +477,7 @@ def file_selection(title, filename='',
484
477
  win.connect('update-preview', update_preview_cb, img_preview)
485
478
 
486
479
  button = win.run()
487
- if button != Gtk.ResponseType.OK:
480
+ if button != Gtk.ResponseType.ACCEPT:
488
481
  result = None
489
482
  elif not multi:
490
483
  result = PurePath(win.get_filename())
@@ -546,10 +539,8 @@ def file_open(filename, type=None, print_p=False):
546
539
  file_open(zfilename, type=ztype, print_p=True)
547
540
  return
548
541
 
549
- if os.name == 'nt':
550
- operation = 'open'
551
- if print_p:
552
- operation = 'print'
542
+ if hasattr(os, 'startfile'):
543
+ operation = 'print' if print_p else 'open'
553
544
  try:
554
545
  os.startfile(os.path.normpath(filename), operation)
555
546
  except WindowsError:
@@ -574,12 +565,20 @@ def file_open(filename, type=None, print_p=False):
574
565
  except OSError:
575
566
  save()
576
567
  else:
568
+ uri = GLib.filename_to_uri(filename)
577
569
  try:
578
- subprocess.Popen(['xdg-open', filename])
579
- except OSError:
570
+ Gio.AppInfo.launch_default_for_uri(uri)
571
+ except GLib.Error:
580
572
  save()
581
573
 
582
574
 
575
+ def webbrowser_open(url):
576
+ try:
577
+ Gio.AppInfo.launch_default_for_uri(url)
578
+ except GLib.Error:
579
+ webbrowser.open(url)
580
+
581
+
583
582
  def url_open(uri):
584
583
  try:
585
584
  return urllib.request.urlopen(uri)
@@ -640,7 +639,7 @@ def mailto(to=None, cc=None, subject=None, body=None, attachment=None):
640
639
  url += "&body=" + urllib.parse.quote(body, "")
641
640
  if attachment:
642
641
  url += "&attachment=" + urllib.parse.quote(attachment, "")
643
- webbrowser.open(url, new=1)
642
+ webbrowser_open(url, new=1)
644
643
 
645
644
 
646
645
  class UniqueDialog(object):
@@ -882,7 +881,7 @@ class ErrorDialog(UniqueDialog):
882
881
  CONFIG['bug.url'], _("Report Bug"))
883
882
  button_roundup.get_child().set_halign(Gtk.Align.START)
884
883
  button_roundup.connect('activate-link',
885
- lambda widget: webbrowser.open(CONFIG['bug.url'], new=2))
884
+ lambda widget: webbrowser_open(CONFIG['bug.url'], new=2))
886
885
  dialog.vbox.pack_start(
887
886
  button_roundup, expand=False, fill=False, padding=0)
888
887
 
@@ -892,8 +891,7 @@ class ErrorDialog(UniqueDialog):
892
891
  if isinstance(title, Exception):
893
892
  title = "%s: %s" % (title.__class__.__name__, title)
894
893
  details += '\n' + title
895
- log = logging.getLogger(__name__)
896
- log.error(details)
894
+ logger.error(details)
897
895
  return super(ErrorDialog, self).__call__(title, details)
898
896
 
899
897
 
@@ -903,7 +901,7 @@ error = ErrorDialog()
903
901
  def check_version(box, version=__version__):
904
902
  def info_bar_response(info_bar, response, box, url):
905
903
  if response == Gtk.ResponseType.ACCEPT:
906
- webbrowser.open(url)
904
+ webbrowser_open(url)
907
905
  box.remove(info_bar)
908
906
 
909
907
  class HeadRequest(urllib.request.Request):
@@ -955,7 +953,7 @@ def open_documentation():
955
953
  version = 'latest'
956
954
  else:
957
955
  version = '.'.join(version)
958
- webbrowser.open(CONFIG['doc.url'] % {
956
+ webbrowser_open(CONFIG['doc.url'] % {
959
957
  'lang': CONFIG['client.lang'],
960
958
  'version': version,
961
959
  })
@@ -1049,7 +1047,7 @@ def get_credentials(user_id=None):
1049
1047
  query['renew'] = user_id
1050
1048
  url_parts[4] = urlencode(query)
1051
1049
  url = urlunparse(url_parts)
1052
- webbrowser.open(url)
1050
+ webbrowser_open(url)
1053
1051
 
1054
1052
  class RequestHandler(BaseHTTPRequestHandler):
1055
1053
  def do_GET(self):
@@ -1213,7 +1211,7 @@ class RPCProgress(object):
1213
1211
  else:
1214
1212
  if not self.res:
1215
1213
  self.error = True
1216
- if self.callback:
1214
+ if self.callback and CONFIG['thread']:
1217
1215
  # Post to GTK queue to be run by the main thread
1218
1216
  GLib.idle_add(self.process)
1219
1217
  return True
@@ -1222,12 +1220,12 @@ class RPCProgress(object):
1222
1220
  self.process_exception_p = process_exception_p
1223
1221
  self.callback = callback
1224
1222
 
1225
- if callback:
1223
+ if callback and CONFIG['thread']:
1226
1224
  # Parent is only useful if it is asynchronous
1227
1225
  # otherwise the cursor is not updated.
1228
1226
  self.parent = get_toplevel_window()
1229
1227
  self._cursor_timeout = GLib.timeout_add(3000, self._set_cursor)
1230
- _thread.start_new_thread(self.start, ())
1228
+ Thread(target=self.start).start()
1231
1229
  return
1232
1230
  else:
1233
1231
  self.start()
@@ -1365,14 +1363,29 @@ def untimezoned_date(date):
1365
1363
 
1366
1364
 
1367
1365
  def humanize(size, suffix=''):
1368
- for u in ['', 'K', 'M', 'G', 'T', 'P']:
1369
- if size <= 1000:
1370
- if isinstance(size, int) or size.is_integer():
1371
- size = locale.localize('{0:.0f}'.format(size))
1372
- else:
1373
- size = locale.localize('{0:.{1}f}'.format(size, 2).rstrip('0'))
1374
- return ''.join([size, u, suffix])
1375
- size /= 1000.0
1366
+ if 0 < abs(size) < 1:
1367
+ for u in ['', 'm', 'µ', 'n', 'p', 'f', 'a', 'z', 'y', 'r', 'q']:
1368
+ if abs(size) >= 0.01:
1369
+ break
1370
+ size *= 1000.0
1371
+ else:
1372
+ size /= 1000.0
1373
+ else:
1374
+ for u in ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q']:
1375
+ if abs(size) <= 1000:
1376
+ break
1377
+ size /= 1000.0
1378
+ else:
1379
+ size *= 1000.0
1380
+ if isinstance(size, int) or size.is_integer():
1381
+ size = locale.localize(str(int(size)))
1382
+ elif abs(size) < 0.01:
1383
+ size = locale.localize(
1384
+ '{0:f}'.format(size).rstrip('0').rstrip('.'))
1385
+ else:
1386
+ size = locale.localize(
1387
+ '{0:.{1}f}'.format(size, 2).rstrip('0').rstrip('.'))
1388
+ return ''.join([size, u, suffix])
1376
1389
 
1377
1390
 
1378
1391
  def get_hostname(netloc):
@@ -1445,13 +1458,12 @@ def ellipsize(string, length):
1445
1458
  def get_align(float_, expand=True):
1446
1459
  "Convert float align into Gtk.Align"
1447
1460
  value = float(float_)
1461
+ if expand:
1462
+ return Gtk.Align.FILL
1448
1463
  if value < 0.5:
1449
1464
  return Gtk.Align.START
1450
1465
  elif value == 0.5:
1451
- if expand:
1452
- return Gtk.Align.FILL
1453
- else:
1454
- return Gtk.Align.CENTER
1466
+ return Gtk.Align.CENTER
1455
1467
  else:
1456
1468
  return Gtk.Align.END
1457
1469
 
@@ -129,8 +129,9 @@ class Date(Gtk.Entry):
129
129
  self.__date.month - 1, self.__date.year)
130
130
  self.__calendar.select_day(self.__date.day)
131
131
  self.__cal_popup.set_transient_for(self.get_toplevel())
132
- popup_position(self, self.__cal_popup)
132
+ # Show popup before because position needs the popup allocation
133
133
  popup_show(self.__cal_popup)
134
+ popup_position(self, self.__cal_popup)
134
135
 
135
136
  def cal_popup_changed(self, calendar):
136
137
  year, month, day = self.__calendar.get_date()
@@ -532,8 +533,19 @@ GObject.type_register(DateTime)
532
533
 
533
534
  def popup_position(widget, popup):
534
535
  allocation = widget.get_allocation()
536
+ popup_allocation = popup.get_allocation()
535
537
  x, y = widget.get_window().get_root_coords(allocation.x, allocation.y)
536
- popup.move(x, y + allocation.height)
538
+ display = widget.get_display()
539
+ monitor = display.get_monitor_at_window(widget.get_window())
540
+ monitor_geometry = monitor.get_geometry()
541
+ if (monitor_geometry.height
542
+ < y + allocation.height + popup_allocation.height):
543
+ y -= popup_allocation.height
544
+ else:
545
+ y += allocation.height
546
+ if monitor_geometry.width < x + popup_allocation.width:
547
+ x -= popup_allocation.width
548
+ popup.move(x, y)
537
549
 
538
550
 
539
551
  def popup_show(popup):
@@ -381,16 +381,16 @@ def unique_value(domain):
381
381
  "Return if unique, the field and the value"
382
382
  if (isinstance(domain, list)
383
383
  and len(domain) == 1):
384
- domain, = domain
385
- name = domain[0]
386
- value = domain[2]
387
- count = 0
388
- if len(domain) == 4 and name[-3:] == '.id':
389
- count = 1
390
- model = domain[3]
391
- value = [model, value]
392
- if name.count('.') == count and domain[1] == '=':
393
- return True, name, value
384
+ name, operator, value, *model = domain[0]
385
+ if operator == '=' or (operator == 'in' and len(value) == 1):
386
+ value = value if operator == '=' else value[0]
387
+ count = 0
388
+ if model and name.endswith('.id'):
389
+ count = 1
390
+ model = model[0]
391
+ value = [model, value]
392
+ if name.count('.') == count:
393
+ return True, name, value
394
394
  return False, None, None
395
395
 
396
396
 
@@ -484,7 +484,10 @@ def parenthesize(tokens):
484
484
 
485
485
  def operatorize(tokens, operator='or'):
486
486
  "Convert operators"
487
- test = (operator, (operator,))
487
+ test = {
488
+ 'or': ('|', ('|',)),
489
+ 'and': ('&', ('&',)),
490
+ }[operator]
488
491
  try:
489
492
  cur = next(tokens)
490
493
  while cur in test:
@@ -656,7 +659,7 @@ class DomainParser(object):
656
659
  if not domain:
657
660
  return ''
658
661
  if domain[0] in ('AND', 'OR'):
659
- nary = ' ' if domain[0] == 'AND' else ' or '
662
+ nary = ' ' if domain[0] == 'AND' else ' | '
660
663
  domain = domain[1:]
661
664
  else:
662
665
  nary = ' '
@@ -10,6 +10,7 @@ from tryton.common.common import selection
10
10
  from tryton.gui.window import Window
11
11
  from tryton.gui.window.attachment import Attachment
12
12
  from tryton.gui.window.email_ import Email
13
+ from tryton.gui.window.log import Log
13
14
  from tryton.gui.window.note import Note
14
15
  from tryton.gui.window.view_form.screen import Screen
15
16
 
@@ -56,6 +57,9 @@ def populate(menu, model, record, title='', field=None, context=None):
56
57
  with Window(hide_current=True, allow_similar=allow_similar):
57
58
  Action.execute(action, data, context=rec.get_context())
58
59
 
60
+ def log(menuitem):
61
+ Log(load(record))
62
+
59
63
  def attachment(menuitem):
60
64
  Attachment(load(record), None)
61
65
 
@@ -103,6 +107,9 @@ def populate(menu, model, record, title='', field=None, context=None):
103
107
  edit_item.connect('activate', edit)
104
108
  action_menu.append(edit_item)
105
109
  action_menu.append(Gtk.SeparatorMenuItem())
110
+ log_item = Gtk.MenuItem(label=_("View Logs..."))
111
+ action_menu.append(log_item)
112
+ log_item.connect('activate', log)
106
113
  attachment_item = Gtk.MenuItem(label=_('Attachments...'))
107
114
  action_menu.append(attachment_item)
108
115
  attachment_item.connect('activate', attachment)
@@ -48,6 +48,8 @@ class SelectionMixin(object):
48
48
  def update_selection(self, record, field):
49
49
  if not field:
50
50
  return
51
+ if not self.selection:
52
+ self.init_selection()
51
53
 
52
54
  domain = field.domain_get(record)
53
55
  if 'relation' not in self.attrs:
@@ -71,7 +73,7 @@ class SelectionMixin(object):
71
73
  try:
72
74
  result = RPCExecute('model', self.attrs['relation'],
73
75
  'search_read', domain, 0, None, None, fields,
74
- context=context)
76
+ context=context, process_exception=False)
75
77
  except RPCException:
76
78
  result = False
77
79
  if isinstance(result, list):
tryton/config.py CHANGED
@@ -6,12 +6,15 @@ import locale
6
6
  import logging
7
7
  import optparse
8
8
  import os
9
+ import shutil
9
10
  import sys
11
+ import tempfile
10
12
 
11
13
  from gi.repository import GdkPixbuf
12
14
 
13
15
  from tryton import __version__
14
16
 
17
+ logger = logging.getLogger(__name__)
15
18
  _ = gettext.gettext
16
19
 
17
20
 
@@ -68,7 +71,7 @@ class ConfigManager(object):
68
71
  'bug.url': 'https://bugs.tryton.org/',
69
72
  'download.url': 'https://downloads.tryton.org/',
70
73
  'download.frequency': 60 * 60 * 8,
71
- 'menu.pane': 200,
74
+ 'menu.pane': 320,
72
75
  }
73
76
  self.config = {}
74
77
  self.options = {}
@@ -94,6 +97,9 @@ class ConfigManager(object):
94
97
  help=_("specify the login user"))
95
98
  parser.add_option("-s", "--server", dest="host",
96
99
  help=_("specify the server hostname:port"))
100
+ parser.add_option(
101
+ '--no-thread', default=True, action='store_false', dest='thread',
102
+ help=_("disable thread usage"))
97
103
  opt, self.arguments = parser.parse_args()
98
104
  self.rcfile = opt.config or os.path.join(
99
105
  get_config_dir(), 'tryton.conf')
@@ -122,6 +128,7 @@ class ConfigManager(object):
122
128
  for arg in ['login', 'host']:
123
129
  if getattr(opt, arg):
124
130
  self.options['login.' + arg] = getattr(opt, arg)
131
+ self.options['thread'] = opt.thread
125
132
 
126
133
  def save(self):
127
134
  try:
@@ -136,15 +143,25 @@ class ConfigManager(object):
136
143
  with open(self.rcfile, 'w') as fp:
137
144
  parser.write(fp)
138
145
  except IOError:
139
- logging.getLogger(__name__).warn(
140
- _('Unable to write config file %s.')
141
- % (self.rcfile,))
146
+ logger.warn("Unable to write config file %s", self.rcfile)
142
147
  return False
143
148
  return True
144
149
 
145
150
  def load(self):
146
151
  parser = configparser.ConfigParser()
147
- parser.read([self.rcfile])
152
+ try:
153
+ parser.read([self.rcfile])
154
+ except configparser.Error:
155
+ config_dir = os.path.dirname(self.rcfile)
156
+ with tempfile.NamedTemporaryFile(
157
+ delete=False, prefix='tryton_', suffix='.conf',
158
+ dir=config_dir) as temp_file:
159
+ temp_name = temp_file.name
160
+ shutil.copy(self.rcfile, temp_name)
161
+ logger.error(
162
+ f"Failed to parse {self.rcfile}. "
163
+ f"A backup can be found at {temp_name}", exc_info=True)
164
+ return
148
165
  for section in parser.sections():
149
166
  for (name, value) in parser.items(section):
150
167
  if value.lower() == 'true':
Binary file