tryton 7.0.6__py3-none-any.whl → 7.2.13__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 (100) hide show
  1. tryton/__init__.py +1 -1
  2. tryton/cache.py +34 -0
  3. tryton/common/common.py +137 -69
  4. tryton/common/completion.py +2 -2
  5. tryton/common/datetime_.py +3 -1
  6. tryton/common/domain_inversion.py +10 -7
  7. tryton/common/domain_parser.py +17 -7
  8. tryton/common/selection.py +6 -3
  9. tryton/common/tempfile.py +34 -0
  10. tryton/config.py +4 -5
  11. tryton/data/locale/bg/LC_MESSAGES/tryton.mo +0 -0
  12. tryton/data/locale/bg/LC_MESSAGES/tryton.po +42 -4
  13. tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
  14. tryton/data/locale/ca/LC_MESSAGES/tryton.po +47 -8
  15. tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
  16. tryton/data/locale/cs/LC_MESSAGES/tryton.po +42 -4
  17. tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
  18. tryton/data/locale/de/LC_MESSAGES/tryton.po +45 -6
  19. tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
  20. tryton/data/locale/es/LC_MESSAGES/tryton.po +46 -7
  21. tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
  22. tryton/data/locale/es_419/LC_MESSAGES/tryton.po +43 -5
  23. tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
  24. tryton/data/locale/et/LC_MESSAGES/tryton.po +46 -6
  25. tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
  26. tryton/data/locale/fa/LC_MESSAGES/tryton.po +46 -7
  27. tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
  28. tryton/data/locale/fi/LC_MESSAGES/tryton.po +41 -4
  29. tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
  30. tryton/data/locale/fr/LC_MESSAGES/tryton.po +46 -7
  31. tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
  32. tryton/data/locale/hu/LC_MESSAGES/tryton.po +46 -6
  33. tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
  34. tryton/data/locale/id/LC_MESSAGES/tryton.po +43 -4
  35. tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
  36. tryton/data/locale/it/LC_MESSAGES/tryton.po +45 -6
  37. tryton/data/locale/ja_JP/LC_MESSAGES/tryton.mo +0 -0
  38. tryton/data/locale/lo/LC_MESSAGES/tryton.mo +0 -0
  39. tryton/data/locale/lo/LC_MESSAGES/tryton.po +45 -6
  40. tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
  41. tryton/data/locale/lt/LC_MESSAGES/tryton.po +46 -6
  42. tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
  43. tryton/data/locale/nl/LC_MESSAGES/tryton.po +46 -7
  44. tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
  45. tryton/data/locale/pl/LC_MESSAGES/tryton.po +84 -60
  46. tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
  47. tryton/data/locale/pt/LC_MESSAGES/tryton.po +45 -6
  48. tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
  49. tryton/data/locale/ro/LC_MESSAGES/tryton.po +57 -17
  50. tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
  51. tryton/data/locale/ru/LC_MESSAGES/tryton.po +43 -6
  52. tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
  53. tryton/data/locale/sl/LC_MESSAGES/tryton.po +46 -5
  54. tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
  55. tryton/data/locale/tr/LC_MESSAGES/tryton.po +41 -4
  56. tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
  57. tryton/data/locale/uk/LC_MESSAGES/tryton.po +46 -6
  58. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
  59. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +46 -5
  60. tryton/device_cookie.py +1 -1
  61. tryton/gui/main.py +3 -2
  62. tryton/gui/window/about.py +1 -1
  63. tryton/gui/window/dblogin.py +2 -2
  64. tryton/gui/window/email_.py +1 -1
  65. tryton/gui/window/form.py +6 -4
  66. tryton/gui/window/log.py +24 -2
  67. tryton/gui/window/view_form/model/field.py +84 -34
  68. tryton/gui/window/view_form/model/group.py +3 -1
  69. tryton/gui/window/view_form/model/record.py +64 -15
  70. tryton/gui/window/view_form/screen/screen.py +83 -46
  71. tryton/gui/window/view_form/view/calendar_gtk/calendar_.py +15 -9
  72. tryton/gui/window/view_form/view/form.py +6 -12
  73. tryton/gui/window/view_form/view/form_gtk/char.py +5 -6
  74. tryton/gui/window/view_form/view/form_gtk/dictionary.py +37 -24
  75. tryton/gui/window/view_form/view/form_gtk/document.py +9 -10
  76. tryton/gui/window/view_form/view/form_gtk/many2many.py +17 -7
  77. tryton/gui/window/view_form/view/form_gtk/many2one.py +21 -13
  78. tryton/gui/window/view_form/view/form_gtk/one2many.py +25 -6
  79. tryton/gui/window/view_form/view/form_gtk/state_widget.py +6 -2
  80. tryton/gui/window/view_form/view/graph_gtk/graph.py +3 -1
  81. tryton/gui/window/view_form/view/list.py +68 -35
  82. tryton/gui/window/view_form/view/list_gtk/editabletree.py +2 -1
  83. tryton/gui/window/view_form/view/list_gtk/widget.py +58 -23
  84. tryton/gui/window/view_form/view/screen_container.py +3 -5
  85. tryton/gui/window/win_export.py +1 -2
  86. tryton/gui/window/win_form.py +9 -7
  87. tryton/gui/window/win_import.py +9 -4
  88. tryton/gui/window/wizard.py +13 -10
  89. tryton/jsonrpc.py +46 -28
  90. tryton/plugins/__init__.py +5 -3
  91. tryton/pyson.py +55 -5
  92. tryton/rpc.py +18 -0
  93. tryton/tests/test_common_domain_parser.py +8 -0
  94. tryton/translate.py +5 -2
  95. {tryton-7.0.6.data → tryton-7.2.13.data}/scripts/tryton +8 -7
  96. {tryton-7.0.6.dist-info → tryton-7.2.13.dist-info}/METADATA +6 -6
  97. {tryton-7.0.6.dist-info → tryton-7.2.13.dist-info}/RECORD +100 -98
  98. {tryton-7.0.6.dist-info → tryton-7.2.13.dist-info}/WHEEL +1 -1
  99. {tryton-7.0.6.dist-info → tryton-7.2.13.dist-info}/LICENSE +0 -0
  100. {tryton-7.0.6.dist-info → tryton-7.2.13.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__ = "7.0.6"
3
+ __version__ = "7.2.13"
4
4
  import locale
5
5
 
6
6
  import gi
tryton/cache.py ADDED
@@ -0,0 +1,34 @@
1
+ # This file is part of Tryton. The COPYRIGHT file at the top level of
2
+ # this repository contains the full copyright notices and license terms.
3
+
4
+ from collections import OrderedDict
5
+
6
+
7
+ class CacheDict(OrderedDict):
8
+
9
+ def __init__(self, *args, cache_len=10, default_factory=None, **kwargs):
10
+ assert cache_len > 0
11
+ self.cache_len = cache_len
12
+ self.default_factory = default_factory
13
+
14
+ super().__init__(*args, **kwargs)
15
+
16
+ def __setitem__(self, key, value):
17
+ super().__setitem__(key, value)
18
+ self.move_to_end(key)
19
+
20
+ while len(self) > self.cache_len:
21
+ oldkey = next(iter(self))
22
+ self.__delitem__(oldkey)
23
+
24
+ def __getitem__(self, key):
25
+ value = super().__getitem__(key)
26
+ self.move_to_end(key)
27
+ return value
28
+
29
+ def __missing__(self, key):
30
+ if self.default_factory is None:
31
+ raise KeyError(key)
32
+ value = self.default_factory()
33
+ self[key] = value
34
+ return value
tryton/common/common.py CHANGED
@@ -1,7 +1,9 @@
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
3
 
4
+ import base64
4
5
  import colorsys
6
+ import concurrent.futures
5
7
  import gettext
6
8
  import locale
7
9
  import logging
@@ -9,10 +11,9 @@ import os
9
11
  import platform
10
12
  import re
11
13
  import subprocess
12
- import tempfile
13
14
  import unicodedata
14
15
  import xml.etree.ElementTree as ET
15
- from collections import defaultdict
16
+ from collections import OrderedDict, defaultdict
16
17
  from decimal import Decimal
17
18
  from http.server import BaseHTTPRequestHandler, HTTPServer
18
19
  from pathlib import PurePath
@@ -31,11 +32,13 @@ import urllib.error
31
32
  import urllib.parse
32
33
  import urllib.request
33
34
  import webbrowser
34
- from functools import lru_cache, wraps
35
+ from functools import wraps
35
36
  from string import Template
36
37
  from threading import Lock, Thread
37
38
 
38
39
  import tryton.rpc as rpc
40
+ import tryton.translate as translate
41
+ from tryton.cache import CacheDict
39
42
  from tryton.config import CONFIG, PIXMAPS_DIR, SOUNDS_DIR, TRYTON_ICON
40
43
 
41
44
  try:
@@ -50,6 +53,7 @@ from tryton import __version__
50
53
  from tryton.exceptions import TrytonError, TrytonServerError
51
54
  from tryton.pyson import PYSONEncoder
52
55
 
56
+ from . import tempfile
53
57
  from .underline import set_underline
54
58
  from .widget_style import widget_class
55
59
 
@@ -60,11 +64,15 @@ logger = logging.getLogger(__name__)
60
64
  class IconFactory:
61
65
 
62
66
  batchnum = 10
63
- _tryton_icons = []
64
- _name2id = {}
67
+ _name2id = OrderedDict()
65
68
  _icons = {}
66
69
  _local_icons = {}
67
70
  _pixbufs = defaultdict(dict)
71
+ _url_pixbufs = CacheDict(cache_len=CONFIG['image.cache_size'])
72
+ _empty_pixbufs = {}
73
+ _empty_gif = base64.b64decode(
74
+ 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')
75
+ _executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
68
76
 
69
77
  @classmethod
70
78
  def load_local_icons(cls):
@@ -75,69 +83,61 @@ class IconFactory:
75
83
 
76
84
  @classmethod
77
85
  def load_icons(cls, refresh=False):
78
- if not refresh:
79
- cls._name2id.clear()
80
- cls._icons.clear()
81
- del cls._tryton_icons[:]
82
-
83
86
  try:
84
87
  icons = rpc.execute('model', 'ir.ui.icon', 'list_icons',
85
88
  rpc.CONTEXT)
86
89
  except TrytonServerError:
87
90
  icons = []
88
- for icon_id, icon_name in icons:
89
- if refresh and icon_name in cls._icons:
90
- continue
91
- cls._tryton_icons.append((icon_id, icon_name))
92
- cls._name2id[icon_name] = icon_id
91
+ cls._name2id = name2id = OrderedDict((n, i) for i, n in icons)
92
+ if not refresh:
93
+ cls._icons.clear()
94
+ return name2id
93
95
 
94
96
  @classmethod
95
- def register_icon(cls, iconname):
96
- # iconname might be '' when page do not define icon
97
- if (not iconname
98
- or iconname in cls._icons
99
- or iconname in cls._local_icons):
100
- return
101
- if iconname not in cls._name2id:
102
- cls.load_icons(refresh=True)
103
- try:
104
- icon_ref = (cls._name2id[iconname], iconname)
105
- except KeyError:
106
- logger.error(f"Unknown icon {iconname}")
107
- cls._icons[iconname] = None
108
- return
109
- idx = cls._tryton_icons.index(icon_ref)
110
- to_load = slice(max(0, idx - cls.batchnum // 2),
97
+ def _get_icon(cls, iconname):
98
+ data = cls._icons.get(iconname)
99
+ if data is not None:
100
+ return data
101
+ path = cls._local_icons.get(iconname)
102
+ if path is not None:
103
+ with open(path, 'rb') as fp:
104
+ return fp.read()
105
+
106
+ name2id = cls._name2id
107
+ if iconname not in name2id:
108
+ name2id = cls.load_icons(refresh=True)
109
+ if iconname not in name2id:
110
+ logger.error(f"Unknown icon {iconname}")
111
+ cls._icons[iconname] = None
112
+ return
113
+ names = [n for n in name2id if n not in cls._icons or n == iconname]
114
+ idx = names.index(iconname)
115
+ to_load = slice(
116
+ max(0, idx - cls.batchnum // 2),
111
117
  idx + cls.batchnum // 2)
112
- ids = [e[0] for e in cls._tryton_icons[to_load]]
118
+ ids = [name2id[n] for n in names[to_load]]
113
119
  try:
114
120
  icons = rpc.execute('model', 'ir.ui.icon', 'read', ids,
115
121
  ['name', 'icon'], rpc.CONTEXT)
116
122
  except TrytonServerError:
117
123
  icons = []
124
+ data = None
118
125
  for icon in icons:
119
126
  name = icon['name']
120
- data = icon['icon'].encode('utf-8')
121
- cls._icons[name] = data
122
- cls._tryton_icons.remove((icon['id'], icon['name']))
123
- del cls._name2id[icon['name']]
127
+ icondata = icon['icon'].encode('utf-8')
128
+ cls._icons[name] = icondata
129
+ if name == iconname:
130
+ data = icondata
131
+ return data
124
132
 
125
133
  @classmethod
126
134
  def get_pixbuf(cls, iconname, size=16, color=None, badge=None):
127
135
  if not iconname:
128
136
  return
129
137
  colors = CONFIG['icon.colors'].split(',')
130
- cls.register_icon(iconname)
131
138
  if iconname not in cls._pixbufs[(size, badge)]:
132
- data = None
133
- if iconname in cls._icons:
134
- data = cls._icons[iconname]
135
- elif iconname in cls._local_icons:
136
- path = cls._local_icons[iconname]
137
- with open(path, 'rb') as fp:
138
- data = fp.read()
139
+ data = cls._get_icon(iconname)
139
140
  if not data:
140
- logger.error("Unknown icon %s" % iconname)
141
141
  return
142
142
  if not color:
143
143
  color = colors[0]
@@ -199,16 +199,40 @@ class IconFactory:
199
199
  return urllib.parse.urlunsplit(parts)
200
200
 
201
201
  @classmethod
202
- @lru_cache(maxsize=CONFIG['image.cache_size'])
203
- def get_pixbuf_url(cls, url, size=16, size_param=None):
202
+ def _get_pixbuf_url(cls, url, size=16):
204
203
  if not url:
205
204
  return
206
- url = cls._convert_url(url, size, size_param=size_param)
205
+ pixbuf = None
206
+ logger.info(f'GET {url}')
207
207
  try:
208
208
  with urllib.request.urlopen(url) as response:
209
- return data2pixbuf(response.read(), size, size)
209
+ pixbuf = data2pixbuf(response.read(), size, size)
210
210
  except urllib.error.URLError:
211
211
  logger.info("Can not fetch %s", url, exc_info=True)
212
+ cls._url_pixbufs[url] = pixbuf
213
+ return pixbuf
214
+
215
+ @classmethod
216
+ def get_pixbuf_url(cls, url, size=16, size_param=None, callback=None):
217
+ if not url:
218
+ return
219
+
220
+ url = cls._convert_url(url, size, size_param=size_param)
221
+ pixbuf = cls._url_pixbufs.get(url)
222
+ if pixbuf is not None:
223
+ return pixbuf
224
+
225
+ if callback:
226
+ def fetch(url, size):
227
+ pixbuf = cls._get_pixbuf_url(url, size)
228
+ GLib.idle_add(lambda: callback(pixbuf))
229
+ cls._executor.submit(fetch, url, size)
230
+ if size not in cls._empty_pixbufs:
231
+ cls._empty_pixbufs[size] = _data2pixbuf(
232
+ cls._empty_gif, size, size)
233
+ return cls._empty_pixbufs[size]
234
+ else:
235
+ return cls._get_pixbuf_url(url, size, size_param)
212
236
 
213
237
 
214
238
  IconFactory.load_local_icons()
@@ -229,7 +253,7 @@ class ModelAccess(object):
229
253
  self._models = rpc.execute('model', 'ir.model', 'list_models',
230
254
  rpc.CONTEXT)
231
255
  except TrytonServerError:
232
- pass
256
+ logger.error("Unable to get model list.")
233
257
 
234
258
  def __getitem__(self, model):
235
259
  if model in self._access:
@@ -243,7 +267,14 @@ class ModelAccess(object):
243
267
  access = rpc.execute('model', 'ir.model.access', 'get_access',
244
268
  self._models[to_load], rpc.CONTEXT)
245
269
  except TrytonServerError:
246
- access = {}
270
+ logger.error("Unable to get access for %s.", model)
271
+ access = {
272
+ model: {
273
+ 'read': True,
274
+ 'write': False,
275
+ 'create': False,
276
+ 'delete': False},
277
+ }
247
278
  self._access.update(access)
248
279
  return self._access[model]
249
280
 
@@ -799,7 +830,7 @@ class ConcurrencyDialog(UniqueDialog):
799
830
  dialog = Gtk.MessageDialog(
800
831
  transient_for=parent, modal=True, destroy_with_parent=True,
801
832
  message_type=Gtk.MessageType.QUESTION,
802
- buttons=Gtk.ButtonsType.NONE, text=_('Concurrency Exception'))
833
+ buttons=Gtk.ButtonsType.NONE, text=_("Concurrency Warning"))
803
834
  dialog.format_secondary_text(
804
835
  _('This record has been modified while you were editing it.'))
805
836
  cancel_button = dialog.add_button(
@@ -844,7 +875,7 @@ class ErrorDialog(UniqueDialog):
844
875
  dialog = Gtk.MessageDialog(
845
876
  transient_for=parent, modal=True, destroy_with_parent=True,
846
877
  message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.NONE)
847
- dialog.set_default_size(600, 400)
878
+ dialog.set_default_size(600, 200)
848
879
  dialog.set_position(Gtk.WindowPosition.CENTER)
849
880
 
850
881
  dialog.add_button(set_underline(_("Close")), Gtk.ResponseType.CANCEL)
@@ -859,6 +890,7 @@ class ErrorDialog(UniqueDialog):
859
890
  scrolledwindow.set_policy(
860
891
  Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
861
892
  scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE)
893
+ scrolledwindow.set_min_content_height(300)
862
894
 
863
895
  viewport = Gtk.Viewport()
864
896
  viewport.set_shadow_type(Gtk.ShadowType.NONE)
@@ -870,8 +902,12 @@ class ErrorDialog(UniqueDialog):
870
902
 
871
903
  viewport.add(textview)
872
904
  scrolledwindow.add(viewport)
905
+ expander = Gtk.Expander()
906
+ expander.set_label(_("Details"))
907
+ expander.add(scrolledwindow)
908
+ expander.set_resize_toplevel(True)
873
909
  dialog.vbox.pack_start(
874
- scrolledwindow, expand=True, fill=True, padding=0)
910
+ expander, expand=False, fill=True, padding=0)
875
911
 
876
912
  button_roundup = Gtk.LinkButton.new_with_label(
877
913
  CONFIG['bug.url'], _("Report Bug"))
@@ -973,10 +1009,13 @@ def process_exception(exception, *args, **kwargs):
973
1009
  name, msg, description = exception.args
974
1010
  res = userwarning(description, msg)
975
1011
  if res in ('always', 'ok'):
976
- RPCExecute(
977
- 'model', 'res.user.warning', 'skip',
978
- name, (res == 'always'),
979
- process_exception=False)
1012
+ try:
1013
+ RPCExecute(
1014
+ 'model', 'res.user.warning', 'skip',
1015
+ name, (res == 'always'),
1016
+ process_exception=False)
1017
+ except RPCException:
1018
+ pass
980
1019
  return rpc_execute(*args)
981
1020
  elif exception.faultCode == 'UserError':
982
1021
  msg, description, domain = exception.args
@@ -1014,12 +1053,11 @@ def process_exception(exception, *args, **kwargs):
1014
1053
  PLOCK.release()
1015
1054
  if args:
1016
1055
  return rpc_execute(*args)
1017
- elif exception.faultCode == str(int(HTTPStatus.TOO_MANY_REQUESTS)):
1056
+ elif exception.faultCode in map(str, HTTPStatus):
1057
+ err_msg = '[%s] %s' % (exception.faultCode, exception.faultString)
1018
1058
  message(
1019
- _('Too many requests. Try again later.'),
1059
+ _('Error: "%s". Try again later.') % err_msg,
1020
1060
  msg_type=Gtk.MessageType.ERROR)
1021
- elif exception.faultCode == str(int(HTTPStatus.NOT_FOUND)):
1022
- message(_("Not found."), msg_type=Gtk.MessageType.ERROR)
1023
1061
  else:
1024
1062
  error(exception, exception.faultString)
1025
1063
  else:
@@ -1104,7 +1142,7 @@ class Login(object):
1104
1142
  if exception.faultCode != 'LoginException':
1105
1143
  raise
1106
1144
  name, msg, type = exception.args
1107
- value = getattr(self, 'get_%s' % type)(msg)
1145
+ value = getattr(self, 'get_%s' % type)(msg, name)
1108
1146
  if value is None:
1109
1147
  raise TrytonError('QueryCanceled')
1110
1148
  parameters[name] = value
@@ -1113,12 +1151,35 @@ class Login(object):
1113
1151
  return
1114
1152
 
1115
1153
  @classmethod
1116
- def get_char(cls, message):
1154
+ def get_char(cls, message, name):
1117
1155
  return ask(message)
1118
1156
 
1119
1157
  @classmethod
1120
- def get_password(cls, message):
1121
- return ask(message, visibility=False)
1158
+ def get_password(cls, message_, name):
1159
+ class AskPasswordDialog(AskDialog):
1160
+ def build_dialog(self, *args, **kwargs):
1161
+ tooltips = Tooltips()
1162
+ dialog = super().build_dialog(*args, **kwargs)
1163
+ box = dialog.get_message_area()
1164
+ button = Gtk.Button.new_with_label(
1165
+ _("Reset forgotten password"))
1166
+ button.set_alignment(0, 0.5)
1167
+ button.set_relief(Gtk.ReliefStyle.NONE)
1168
+ tooltips.set_tip(
1169
+ button, _("Send you an email to reset your password."))
1170
+ button.connect('clicked', self.reset_password)
1171
+ box.pack_start(button, False, False, 0)
1172
+ return dialog
1173
+
1174
+ def reset_password(self, button):
1175
+ rpc.reset_password()
1176
+ message(
1177
+ _("A request to reset your password has been sent.\n"
1178
+ "Please check your mailbox."))
1179
+ self.entry.grab_focus()
1180
+ if name == 'password':
1181
+ ask = AskPasswordDialog()
1182
+ return ask(message_, visibility=False)
1122
1183
 
1123
1184
 
1124
1185
  class Logout:
@@ -1254,6 +1315,8 @@ class RPCProgress(object):
1254
1315
 
1255
1316
  def return_():
1256
1317
  if self.exception:
1318
+ if not isinstance(self.exception, RPCException):
1319
+ raise RPCException(self.exception)
1257
1320
  raise self.exception
1258
1321
  else:
1259
1322
  return self.res
@@ -1275,10 +1338,15 @@ def RPCExecute(*args, **kwargs):
1275
1338
 
1276
1339
 
1277
1340
  def RPCContextReload(callback=None):
1341
+ def clean(context):
1342
+ return {
1343
+ k: v for k, v in context.items()
1344
+ if k != 'locale' and not k.endswith('.rec_name')}
1345
+
1278
1346
  def update(context):
1279
1347
  rpc.context_reset()
1280
1348
  try:
1281
- rpc.CONTEXT.update(context())
1349
+ rpc.CONTEXT.update(clean(context()))
1282
1350
  except RPCException:
1283
1351
  pass
1284
1352
  if callback:
@@ -1288,7 +1356,7 @@ def RPCContextReload(callback=None):
1288
1356
  callback=update if callback else None)
1289
1357
  if not callback:
1290
1358
  rpc.context_reset()
1291
- rpc.CONTEXT.update(context)
1359
+ rpc.CONTEXT.update(clean(context))
1292
1360
 
1293
1361
 
1294
1362
  class Tooltips(object):
@@ -1463,7 +1531,7 @@ def get_align(float_, expand=True):
1463
1531
 
1464
1532
 
1465
1533
  def date_format(format_=None):
1466
- return format_ or rpc.CONTEXT.get('locale', {}).get('date', '%x')
1534
+ return format_ or translate.DATE or '%x'
1467
1535
 
1468
1536
 
1469
1537
  def idle_add(func):
@@ -5,7 +5,7 @@ import logging
5
5
 
6
6
  from gi.repository import GLib, Gtk
7
7
 
8
- from tryton.common import RPCExecute
8
+ from tryton.common import RPCException, RPCExecute
9
9
  from tryton.config import CONFIG
10
10
  from tryton.exceptions import TrytonError, TrytonServerError
11
11
 
@@ -72,7 +72,7 @@ def update_completion(entry, record, field, model, domain=None):
72
72
  'model', model, 'autocomplete', search_text, domain,
73
73
  CONFIG['client.limit'], order, context=context,
74
74
  process_exception=False, callback=callback)
75
- except Exception:
75
+ except RPCException:
76
76
  logger.warning(
77
77
  "Unable to search for completion of %s", model,
78
78
  exc_info=True)
@@ -9,7 +9,9 @@ from gi.repository import Gdk, GObject, Gtk
9
9
 
10
10
  from .common import IconFactory
11
11
 
12
- __all__ = ['Date', 'CellRendererDate', 'Time', 'CellRendererTime', 'DateTime']
12
+ __all__ = [
13
+ 'Date', 'CellRendererDate', 'Time', 'CellRendererTime', 'DateTime',
14
+ 'date_parse']
13
15
 
14
16
  _ = gettext.gettext
15
17
 
@@ -426,16 +426,19 @@ def unique_value(domain, single_value=True):
426
426
  if (isinstance(domain, list)
427
427
  and len(domain) == 1):
428
428
  name, operator, value, *model = domain[0]
429
- if (operator == '='
430
- or (single_value and operator == 'in' and len(value) == 1)):
431
- value = value if operator == '=' and single_value else value[0]
432
- count = 0
429
+ count = name.count('.')
430
+ if (
431
+ (operator == '='
432
+ or (single_value
433
+ and operator == 'in' and len(value) == 1))
434
+ and (not count
435
+ or (count == 1 and model and name.endswith('.id')))):
436
+ if operator == 'in' and single_value:
437
+ value = value[0]
433
438
  if model and name.endswith('.id'):
434
- count = 1
435
439
  model = model[0]
436
440
  value = [model, value]
437
- if name.count('.') == count:
438
- return True, name, value
441
+ return True, name, value
439
442
  return False, None, None
440
443
 
441
444
 
@@ -128,10 +128,12 @@ def unescape(value, escape='\\'):
128
128
  return value.replace(escape + '%', '%').replace(escape + '_', '_')
129
129
 
130
130
 
131
- def quote(value):
131
+ def quote(value, empty=False):
132
132
  "Quote string if needed"
133
133
  if not isinstance(value, str):
134
134
  return value
135
+ if empty and value == '':
136
+ return '""'
135
137
  if '\\' in value:
136
138
  value = value.replace('\\', '\\\\')
137
139
  if '"' in value:
@@ -307,7 +309,7 @@ def convert_value(field, value, context=None):
307
309
  return converts.get(field['type'], lambda: value)()
308
310
 
309
311
 
310
- def format_value(field, value, target=None, context=None):
312
+ def format_value(field, value, target=None, context=None, _quote_empty=False):
311
313
  "Format value for field"
312
314
  if context is None:
313
315
  context = {}
@@ -410,9 +412,11 @@ def format_value(field, value, target=None, context=None):
410
412
  'many2one': format_many2one,
411
413
  }
412
414
  if isinstance(value, (list, tuple)):
413
- return ';'.join(format_value(field, x, context=context) for x in value)
415
+ return ';'.join(
416
+ format_value(field, x, context=context, _quote_empty=True)
417
+ for x in value)
414
418
  return quote(converts.get(field['type'],
415
- lambda: value if value is not None else '')())
419
+ lambda: value if value is not None else '')(), empty=_quote_empty)
416
420
 
417
421
 
418
422
  def complete_value(field, value):
@@ -647,10 +651,16 @@ class DomainParser(object):
647
651
  operator = operator.rstrip(def_operator
648
652
  ).replace('not', '!').strip()
649
653
  if operator.endswith('in'):
650
- if operator == 'not in':
651
- operator = '!'
654
+ if isinstance(value, (list, tuple)) and len(value) == 1:
655
+ if operator == 'not in':
656
+ operator = '!='
657
+ else:
658
+ operator = '='
652
659
  else:
653
- operator = ''
660
+ if operator == 'not in':
661
+ operator = '!'
662
+ else:
663
+ operator = ''
654
664
  formatted_value = format_value(field, value, target, self.context)
655
665
  if (operator in OPERATORS
656
666
  and field['type'] in ('char', 'text', 'selection')
@@ -30,10 +30,13 @@ class SelectionMixin(object):
30
30
  and key not in self._values2selection):
31
31
  try:
32
32
  if self.attrs.get('selection_change_with'):
33
- selection = RPCExecute('model', self.model_name, selection,
34
- value)
33
+ selection = RPCExecute(
34
+ 'model', self.model_name, selection, value,
35
+ process_exception=False)
35
36
  else:
36
- selection = RPCExecute('model', self.model_name, selection)
37
+ selection = RPCExecute(
38
+ 'model', self.model_name, selection,
39
+ process_exception=False)
37
40
  except RPCException:
38
41
  selection = []
39
42
  self._values2selection[key] = selection
@@ -0,0 +1,34 @@
1
+ # This file is part of Tryton. The COPYRIGHT file at the top level of
2
+ # this repository contains the full copyright notices and license terms.
3
+ import atexit
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+
8
+ _files, _directories = [], []
9
+
10
+
11
+ def mkstemp(*args, **kwargs):
12
+ fileno, fname = tempfile.mkstemp(*args, **kwargs)
13
+ _files.append(fname)
14
+ return fileno, fname
15
+
16
+
17
+ def mkdtemp(*args, **kwargs):
18
+ dname = tempfile.mkdtemp(*args, **kwargs)
19
+ _directories.append(dname)
20
+ return dname
21
+
22
+
23
+ @atexit.register
24
+ def clean():
25
+ for fname in _files:
26
+ try:
27
+ os.remove(fname)
28
+ except (FileNotFoundError, PermissionError):
29
+ pass
30
+ _files.clear()
31
+
32
+ for dname in _directories:
33
+ shutil.rmtree(dname, ignore_errors=True)
34
+ _directories.clear()
tryton/config.py CHANGED
@@ -8,7 +8,7 @@ import optparse
8
8
  import os
9
9
  import shutil
10
10
  import sys
11
- import tempfile
11
+ from tempfile import NamedTemporaryFile
12
12
 
13
13
  from gi.repository import GdkPixbuf
14
14
 
@@ -81,6 +81,7 @@ class ConfigManager(object):
81
81
  'login.db': demo_database,
82
82
  'login.expanded': False,
83
83
  'client.title': 'Tryton',
84
+ 'rpc.cache_size': 1024,
84
85
  'client.modepda': False,
85
86
  'client.toolbar': 'default',
86
87
  'client.save_tree_width': True,
@@ -99,7 +100,7 @@ class ConfigManager(object):
99
100
  'graph.color': '#3465a4',
100
101
  'image.max_size': 10 ** 6,
101
102
  'image.cache_size': 1024,
102
- 'doc.url': 'https://docs.tryton.org/en/%(version)s',
103
+ 'doc.url': 'https://docs.tryton.org/%(version)s',
103
104
  'bug.url': 'https://bugs.tryton.org/',
104
105
  'download.url': 'https://downloads.tryton.org/',
105
106
  'download.frequency': 60 * 60 * 8,
@@ -185,7 +186,7 @@ class ConfigManager(object):
185
186
  parser.read([self.rcfile])
186
187
  except configparser.Error:
187
188
  config_dir = os.path.dirname(self.rcfile)
188
- with tempfile.NamedTemporaryFile(
189
+ with NamedTemporaryFile(
189
190
  delete=False, prefix='tryton_', suffix='.conf',
190
191
  dir=config_dir) as temp_file:
191
192
  temp_name = temp_file.name
@@ -219,8 +220,6 @@ class ConfigManager(object):
219
220
 
220
221
  CONFIG = ConfigManager()
221
222
  CURRENT_DIR = os.path.dirname(__file__)
222
- if hasattr(sys, 'frozen'):
223
- CURRENT_DIR = os.path.dirname(sys.executable)
224
223
  if not isinstance(CURRENT_DIR, str):
225
224
  CURRENT_DIR = str(CURRENT_DIR, sys.getfilesystemencoding())
226
225
 
Binary file