tryton 7.0.20__py3-none-any.whl → 7.2.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 (93) hide show
  1. tryton/__init__.py +1 -1
  2. tryton/cache.py +34 -0
  3. tryton/common/common.py +125 -69
  4. tryton/common/completion.py +2 -2
  5. tryton/common/domain_inversion.py +1 -2
  6. tryton/common/domain_parser.py +7 -17
  7. tryton/common/selection.py +6 -3
  8. tryton/common/tempfile.py +34 -0
  9. tryton/config.py +3 -2
  10. tryton/data/locale/bg/LC_MESSAGES/tryton.mo +0 -0
  11. tryton/data/locale/bg/LC_MESSAGES/tryton.po +28 -3
  12. tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
  13. tryton/data/locale/ca/LC_MESSAGES/tryton.po +33 -5
  14. tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
  15. tryton/data/locale/cs/LC_MESSAGES/tryton.po +28 -3
  16. tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
  17. tryton/data/locale/de/LC_MESSAGES/tryton.po +32 -4
  18. tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
  19. tryton/data/locale/es/LC_MESSAGES/tryton.po +32 -4
  20. tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
  21. tryton/data/locale/es_419/LC_MESSAGES/tryton.po +29 -4
  22. tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
  23. tryton/data/locale/et/LC_MESSAGES/tryton.po +32 -5
  24. tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
  25. tryton/data/locale/fa/LC_MESSAGES/tryton.po +32 -6
  26. tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
  27. tryton/data/locale/fi/LC_MESSAGES/tryton.po +28 -3
  28. tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
  29. tryton/data/locale/fr/LC_MESSAGES/tryton.po +32 -4
  30. tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
  31. tryton/data/locale/hu/LC_MESSAGES/tryton.po +32 -5
  32. tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
  33. tryton/data/locale/id/LC_MESSAGES/tryton.po +30 -3
  34. tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
  35. tryton/data/locale/it/LC_MESSAGES/tryton.po +31 -5
  36. tryton/data/locale/ja_JP/LC_MESSAGES/tryton.mo +0 -0
  37. tryton/data/locale/lo/LC_MESSAGES/tryton.mo +0 -0
  38. tryton/data/locale/lo/LC_MESSAGES/tryton.po +31 -5
  39. tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
  40. tryton/data/locale/lt/LC_MESSAGES/tryton.po +32 -5
  41. tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
  42. tryton/data/locale/nl/LC_MESSAGES/tryton.po +32 -4
  43. tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
  44. tryton/data/locale/pl/LC_MESSAGES/tryton.po +32 -5
  45. tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
  46. tryton/data/locale/pt/LC_MESSAGES/tryton.po +31 -5
  47. tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
  48. tryton/data/locale/ro/LC_MESSAGES/tryton.po +43 -16
  49. tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
  50. tryton/data/locale/ru/LC_MESSAGES/tryton.po +29 -5
  51. tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
  52. tryton/data/locale/sl/LC_MESSAGES/tryton.po +32 -4
  53. tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
  54. tryton/data/locale/tr/LC_MESSAGES/tryton.po +28 -3
  55. tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
  56. tryton/data/locale/uk/LC_MESSAGES/tryton.po +32 -5
  57. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
  58. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +32 -4
  59. tryton/device_cookie.py +1 -1
  60. tryton/gui/main.py +3 -2
  61. tryton/gui/window/about.py +1 -1
  62. tryton/gui/window/dblogin.py +2 -2
  63. tryton/gui/window/email_.py +1 -1
  64. tryton/gui/window/form.py +4 -3
  65. tryton/gui/window/log.py +24 -2
  66. tryton/gui/window/view_form/model/field.py +56 -62
  67. tryton/gui/window/view_form/model/group.py +3 -1
  68. tryton/gui/window/view_form/model/record.py +55 -16
  69. tryton/gui/window/view_form/screen/screen.py +15 -12
  70. tryton/gui/window/view_form/view/calendar_gtk/calendar_.py +7 -12
  71. tryton/gui/window/view_form/view/form.py +4 -14
  72. tryton/gui/window/view_form/view/form_gtk/dictionary.py +33 -27
  73. tryton/gui/window/view_form/view/form_gtk/document.py +10 -9
  74. tryton/gui/window/view_form/view/form_gtk/many2many.py +17 -7
  75. tryton/gui/window/view_form/view/form_gtk/many2one.py +21 -13
  76. tryton/gui/window/view_form/view/form_gtk/one2many.py +25 -6
  77. tryton/gui/window/view_form/view/form_gtk/state_widget.py +6 -2
  78. tryton/gui/window/view_form/view/list.py +47 -56
  79. tryton/gui/window/view_form/view/list_gtk/widget.py +36 -23
  80. tryton/gui/window/view_form/view/screen_container.py +5 -3
  81. tryton/gui/window/win_export.py +1 -2
  82. tryton/gui/window/win_form.py +6 -8
  83. tryton/gui/window/wizard.py +11 -10
  84. tryton/jsonrpc.py +41 -27
  85. tryton/pyson.py +54 -4
  86. tryton/rpc.py +18 -0
  87. tryton/tests/test_common_domain_parser.py +0 -8
  88. {tryton-7.0.20.data → tryton-7.2.0.data}/scripts/tryton +1 -2
  89. {tryton-7.0.20.dist-info → tryton-7.2.0.dist-info}/METADATA +5 -5
  90. {tryton-7.0.20.dist-info → tryton-7.2.0.dist-info}/RECORD +93 -91
  91. {tryton-7.0.20.dist-info → tryton-7.2.0.dist-info}/WHEEL +1 -1
  92. {tryton-7.0.20.dist-info → tryton-7.2.0.dist-info}/LICENSE +0 -0
  93. {tryton-7.0.20.dist-info → tryton-7.2.0.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.20"
3
+ __version__ = "7.2.0"
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,12 @@ 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
+ from tryton.cache import CacheDict
39
41
  from tryton.config import CONFIG, PIXMAPS_DIR, SOUNDS_DIR, TRYTON_ICON
40
42
 
41
43
  try:
@@ -50,6 +52,7 @@ from tryton import __version__
50
52
  from tryton.exceptions import TrytonError, TrytonServerError
51
53
  from tryton.pyson import PYSONEncoder
52
54
 
55
+ from . import tempfile
53
56
  from .underline import set_underline
54
57
  from .widget_style import widget_class
55
58
 
@@ -60,11 +63,15 @@ logger = logging.getLogger(__name__)
60
63
  class IconFactory:
61
64
 
62
65
  batchnum = 10
63
- _tryton_icons = []
64
- _name2id = {}
66
+ _name2id = OrderedDict()
65
67
  _icons = {}
66
68
  _local_icons = {}
67
69
  _pixbufs = defaultdict(dict)
70
+ _url_pixbufs = CacheDict(cache_len=CONFIG['image.cache_size'])
71
+ _empty_pixbufs = {}
72
+ _empty_gif = base64.b64decode(
73
+ 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')
74
+ _executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
68
75
 
69
76
  @classmethod
70
77
  def load_local_icons(cls):
@@ -75,69 +82,61 @@ class IconFactory:
75
82
 
76
83
  @classmethod
77
84
  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
85
  try:
84
86
  icons = rpc.execute('model', 'ir.ui.icon', 'list_icons',
85
87
  rpc.CONTEXT)
86
88
  except TrytonServerError:
87
89
  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
90
+ cls._name2id = name2id = OrderedDict((n, i) for i, n in icons)
91
+ if not refresh:
92
+ cls._icons.clear()
93
+ return name2id
93
94
 
94
95
  @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),
96
+ def _get_icon(cls, iconname):
97
+ data = cls._icons.get(iconname)
98
+ if data is not None:
99
+ return data
100
+ path = cls._local_icons.get(iconname)
101
+ if path is not None:
102
+ with open(path, 'rb') as fp:
103
+ return fp.read()
104
+
105
+ name2id = cls._name2id
106
+ if iconname not in name2id:
107
+ name2id = cls.load_icons(refresh=True)
108
+ if iconname not in name2id:
109
+ logger.error(f"Unknown icon {iconname}")
110
+ cls._icons[iconname] = None
111
+ return
112
+ names = [n for n in name2id if n not in cls._icons or n == iconname]
113
+ idx = names.index(iconname)
114
+ to_load = slice(
115
+ max(0, idx - cls.batchnum // 2),
111
116
  idx + cls.batchnum // 2)
112
- ids = [e[0] for e in cls._tryton_icons[to_load]]
117
+ ids = [name2id[n] for n in names[to_load]]
113
118
  try:
114
119
  icons = rpc.execute('model', 'ir.ui.icon', 'read', ids,
115
120
  ['name', 'icon'], rpc.CONTEXT)
116
121
  except TrytonServerError:
117
122
  icons = []
123
+ data = None
118
124
  for icon in icons:
119
125
  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']]
126
+ icondata = icon['icon'].encode('utf-8')
127
+ cls._icons[name] = icondata
128
+ if name == iconname:
129
+ data = icondata
130
+ return data
124
131
 
125
132
  @classmethod
126
133
  def get_pixbuf(cls, iconname, size=16, color=None, badge=None):
127
134
  if not iconname:
128
135
  return
129
136
  colors = CONFIG['icon.colors'].split(',')
130
- cls.register_icon(iconname)
131
137
  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()
138
+ data = cls._get_icon(iconname)
139
139
  if not data:
140
- logger.error("Unknown icon %s" % iconname)
141
140
  return
142
141
  if not color:
143
142
  color = colors[0]
@@ -199,16 +198,40 @@ class IconFactory:
199
198
  return urllib.parse.urlunsplit(parts)
200
199
 
201
200
  @classmethod
202
- @lru_cache(maxsize=CONFIG['image.cache_size'])
203
- def get_pixbuf_url(cls, url, size=16, size_param=None):
201
+ def _get_pixbuf_url(cls, url, size=16):
204
202
  if not url:
205
203
  return
206
- url = cls._convert_url(url, size, size_param=size_param)
204
+ pixbuf = None
205
+ logger.info(f'GET {url}')
207
206
  try:
208
207
  with urllib.request.urlopen(url) as response:
209
- return data2pixbuf(response.read(), size, size)
208
+ pixbuf = data2pixbuf(response.read(), size, size)
210
209
  except urllib.error.URLError:
211
210
  logger.info("Can not fetch %s", url, exc_info=True)
211
+ cls._url_pixbufs[url] = pixbuf
212
+ return pixbuf
213
+
214
+ @classmethod
215
+ def get_pixbuf_url(cls, url, size=16, size_param=None, callback=None):
216
+ if not url:
217
+ return
218
+
219
+ url = cls._convert_url(url, size, size_param=size_param)
220
+ pixbuf = cls._url_pixbufs.get(url)
221
+ if pixbuf is not None:
222
+ return pixbuf
223
+
224
+ if callback:
225
+ def fetch(url, size):
226
+ pixbuf = cls._get_pixbuf_url(url, size)
227
+ GLib.idle_add(lambda: callback(pixbuf))
228
+ cls._executor.submit(fetch, url, size)
229
+ if size not in cls._empty_pixbufs:
230
+ cls._empty_pixbufs[size] = _data2pixbuf(
231
+ cls._empty_gif, size, size)
232
+ return cls._empty_pixbufs[size]
233
+ else:
234
+ return cls._get_pixbuf_url(url, size, size_param)
212
235
 
213
236
 
214
237
  IconFactory.load_local_icons()
@@ -806,7 +829,7 @@ class ConcurrencyDialog(UniqueDialog):
806
829
  dialog = Gtk.MessageDialog(
807
830
  transient_for=parent, modal=True, destroy_with_parent=True,
808
831
  message_type=Gtk.MessageType.QUESTION,
809
- buttons=Gtk.ButtonsType.NONE, text=_('Concurrency Exception'))
832
+ buttons=Gtk.ButtonsType.NONE, text=_("Concurrency Warning"))
810
833
  dialog.format_secondary_text(
811
834
  _('This record has been modified while you were editing it.'))
812
835
  cancel_button = dialog.add_button(
@@ -851,7 +874,7 @@ class ErrorDialog(UniqueDialog):
851
874
  dialog = Gtk.MessageDialog(
852
875
  transient_for=parent, modal=True, destroy_with_parent=True,
853
876
  message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.NONE)
854
- dialog.set_default_size(600, 400)
877
+ dialog.set_default_size(600, 200)
855
878
  dialog.set_position(Gtk.WindowPosition.CENTER)
856
879
 
857
880
  dialog.add_button(set_underline(_("Close")), Gtk.ResponseType.CANCEL)
@@ -866,6 +889,7 @@ class ErrorDialog(UniqueDialog):
866
889
  scrolledwindow.set_policy(
867
890
  Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
868
891
  scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE)
892
+ scrolledwindow.set_min_content_height(300)
869
893
 
870
894
  viewport = Gtk.Viewport()
871
895
  viewport.set_shadow_type(Gtk.ShadowType.NONE)
@@ -877,8 +901,12 @@ class ErrorDialog(UniqueDialog):
877
901
 
878
902
  viewport.add(textview)
879
903
  scrolledwindow.add(viewport)
904
+ expander = Gtk.Expander()
905
+ expander.set_label(_("Details"))
906
+ expander.add(scrolledwindow)
907
+ expander.set_resize_toplevel(True)
880
908
  dialog.vbox.pack_start(
881
- scrolledwindow, expand=True, fill=True, padding=0)
909
+ expander, expand=False, fill=True, padding=0)
882
910
 
883
911
  button_roundup = Gtk.LinkButton.new_with_label(
884
912
  CONFIG['bug.url'], _("Report Bug"))
@@ -980,10 +1008,13 @@ def process_exception(exception, *args, **kwargs):
980
1008
  name, msg, description = exception.args
981
1009
  res = userwarning(description, msg)
982
1010
  if res in ('always', 'ok'):
983
- RPCExecute(
984
- 'model', 'res.user.warning', 'skip',
985
- name, (res == 'always'),
986
- process_exception=False)
1011
+ try:
1012
+ RPCExecute(
1013
+ 'model', 'res.user.warning', 'skip',
1014
+ name, (res == 'always'),
1015
+ process_exception=False)
1016
+ except RPCException:
1017
+ pass
987
1018
  return rpc_execute(*args)
988
1019
  elif exception.faultCode == 'UserError':
989
1020
  msg, description, domain = exception.args
@@ -1021,12 +1052,11 @@ def process_exception(exception, *args, **kwargs):
1021
1052
  PLOCK.release()
1022
1053
  if args:
1023
1054
  return rpc_execute(*args)
1024
- elif exception.faultCode == str(int(HTTPStatus.TOO_MANY_REQUESTS)):
1055
+ elif exception.faultCode in map(str, HTTPStatus):
1056
+ err_msg = '[%s] %s' % (exception.faultCode, exception.faultString)
1025
1057
  message(
1026
- _('Too many requests. Try again later.'),
1058
+ _('Error: "%s". Try again later.') % err_msg,
1027
1059
  msg_type=Gtk.MessageType.ERROR)
1028
- elif exception.faultCode == str(int(HTTPStatus.NOT_FOUND)):
1029
- message(_("Not found."), msg_type=Gtk.MessageType.ERROR)
1030
1060
  else:
1031
1061
  error(exception, exception.faultString)
1032
1062
  else:
@@ -1111,7 +1141,7 @@ class Login(object):
1111
1141
  if exception.faultCode != 'LoginException':
1112
1142
  raise
1113
1143
  name, msg, type = exception.args
1114
- value = getattr(self, 'get_%s' % type)(msg)
1144
+ value = getattr(self, 'get_%s' % type)(msg, name)
1115
1145
  if value is None:
1116
1146
  raise TrytonError('QueryCanceled')
1117
1147
  parameters[name] = value
@@ -1120,12 +1150,35 @@ class Login(object):
1120
1150
  return
1121
1151
 
1122
1152
  @classmethod
1123
- def get_char(cls, message):
1153
+ def get_char(cls, message, name):
1124
1154
  return ask(message)
1125
1155
 
1126
1156
  @classmethod
1127
- def get_password(cls, message):
1128
- return ask(message, visibility=False)
1157
+ def get_password(cls, message_, name):
1158
+ class AskPasswordDialog(AskDialog):
1159
+ def build_dialog(self, *args, **kwargs):
1160
+ tooltips = Tooltips()
1161
+ dialog = super().build_dialog(*args, **kwargs)
1162
+ box = dialog.get_message_area()
1163
+ button = Gtk.Button.new_with_label(
1164
+ _("Reset forgotten password"))
1165
+ button.set_alignment(0, 0.5)
1166
+ button.set_relief(Gtk.ReliefStyle.NONE)
1167
+ tooltips.set_tip(
1168
+ button, _("Send you an email to reset your password."))
1169
+ button.connect('clicked', self.reset_password)
1170
+ box.pack_start(button, False, False, 0)
1171
+ return dialog
1172
+
1173
+ def reset_password(self, button):
1174
+ rpc.reset_password()
1175
+ message(
1176
+ _("A request to reset your password has been sent.\n"
1177
+ "Please check your mailbox."))
1178
+ self.entry.grab_focus()
1179
+ if name == 'password':
1180
+ ask = AskPasswordDialog()
1181
+ return ask(message_, visibility=False)
1129
1182
 
1130
1183
 
1131
1184
  class Logout:
@@ -1261,9 +1314,7 @@ class RPCProgress(object):
1261
1314
 
1262
1315
  def return_():
1263
1316
  if self.exception:
1264
- if not isinstance(self.exception, RPCException):
1265
- raise RPCException(self.exception)
1266
- raise self.exception
1317
+ raise RPCException(self.exception)
1267
1318
  else:
1268
1319
  return self.res
1269
1320
 
@@ -1284,10 +1335,15 @@ def RPCExecute(*args, **kwargs):
1284
1335
 
1285
1336
 
1286
1337
  def RPCContextReload(callback=None):
1338
+ def clean(context):
1339
+ return {
1340
+ k: v for k, v in context.items()
1341
+ if k != 'locale' and not k.endswith('.rec_name')}
1342
+
1287
1343
  def update(context):
1288
1344
  rpc.context_reset()
1289
1345
  try:
1290
- rpc.CONTEXT.update(context())
1346
+ rpc.CONTEXT.update(clean(context()))
1291
1347
  except RPCException:
1292
1348
  pass
1293
1349
  if callback:
@@ -1297,7 +1353,7 @@ def RPCContextReload(callback=None):
1297
1353
  callback=update if callback else None)
1298
1354
  if not callback:
1299
1355
  rpc.context_reset()
1300
- rpc.CONTEXT.update(context)
1356
+ rpc.CONTEXT.update(clean(context))
1301
1357
 
1302
1358
 
1303
1359
  class Tooltips(object):
@@ -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)
@@ -433,8 +433,7 @@ def unique_value(domain, single_value=True):
433
433
  and operator == 'in' and len(value) == 1))
434
434
  and (not count
435
435
  or (count == 1 and model and name.endswith('.id')))):
436
- if operator == 'in' and single_value:
437
- value = value[0]
436
+ value = value if operator == '=' and single_value else value[0]
438
437
  if model and name.endswith('.id'):
439
438
  model = model[0]
440
439
  value = [model, value]
@@ -128,12 +128,10 @@ def unescape(value, escape='\\'):
128
128
  return value.replace(escape + '%', '%').replace(escape + '_', '_')
129
129
 
130
130
 
131
- def quote(value, empty=False):
131
+ def quote(value):
132
132
  "Quote string if needed"
133
133
  if not isinstance(value, str):
134
134
  return value
135
- if empty and value == '':
136
- return '""'
137
135
  if '\\' in value:
138
136
  value = value.replace('\\', '\\\\')
139
137
  if '"' in value:
@@ -309,7 +307,7 @@ def convert_value(field, value, context=None):
309
307
  return converts.get(field['type'], lambda: value)()
310
308
 
311
309
 
312
- def format_value(field, value, target=None, context=None, _quote_empty=False):
310
+ def format_value(field, value, target=None, context=None):
313
311
  "Format value for field"
314
312
  if context is None:
315
313
  context = {}
@@ -412,11 +410,9 @@ def format_value(field, value, target=None, context=None, _quote_empty=False):
412
410
  'many2one': format_many2one,
413
411
  }
414
412
  if isinstance(value, (list, tuple)):
415
- return ';'.join(
416
- format_value(field, x, context=context, _quote_empty=True)
417
- for x in value)
413
+ return ';'.join(format_value(field, x, context=context) for x in value)
418
414
  return quote(converts.get(field['type'],
419
- lambda: value if value is not None else '')(), empty=_quote_empty)
415
+ lambda: value if value is not None else '')())
420
416
 
421
417
 
422
418
  def complete_value(field, value):
@@ -651,16 +647,10 @@ class DomainParser(object):
651
647
  operator = operator.rstrip(def_operator
652
648
  ).replace('not', '!').strip()
653
649
  if operator.endswith('in'):
654
- if isinstance(value, (list, tuple)) and len(value) == 1:
655
- if operator == 'not in':
656
- operator = '!='
657
- else:
658
- operator = '='
650
+ if operator == 'not in':
651
+ operator = '!'
659
652
  else:
660
- if operator == 'not in':
661
- operator = '!'
662
- else:
663
- operator = ''
653
+ operator = ''
664
654
  formatted_value = format_value(field, value, target, self.context)
665
655
  if (operator in OPERATORS
666
656
  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:
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,
@@ -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
Binary file
@@ -30,6 +30,10 @@ msgstr "укажете адреса на сървъра"
30
30
  msgid "disable thread usage"
31
31
  msgstr ""
32
32
 
33
+ #, python-format
34
+ msgid "Invalid response id (%s) expected %s"
35
+ msgstr ""
36
+
33
37
  #, python-format
34
38
  msgid "Unable to set locale %s"
35
39
  msgstr "Не може да зададе локални настройки %s"
@@ -135,15 +139,27 @@ msgstr ""
135
139
  msgid "Could not get a session."
136
140
  msgstr "Не може да се свърже със сървъра!"
137
141
 
138
- msgid "Too many requests. Try again later."
142
+ #, python-format
143
+ msgid "Error \"%s\". Try again later."
139
144
  msgstr ""
140
145
 
141
- msgid "Not found."
146
+ msgid "Too many requests. Try again later."
142
147
  msgstr ""
143
148
 
144
149
  msgid "Not Found."
145
150
  msgstr ""
146
151
 
152
+ msgid "Reset forgotten password"
153
+ msgstr ""
154
+
155
+ msgid "Send you an email to reset your password."
156
+ msgstr ""
157
+
158
+ msgid ""
159
+ "A request to reset your password has been sent.\n"
160
+ "Please check your mailbox."
161
+ msgstr ""
162
+
147
163
  #, fuzzy
148
164
  msgid "..."
149
165
  msgstr "..."
@@ -1157,7 +1173,16 @@ msgstr "Размера на изображението е много голям!
1157
1173
  msgid "Copy"
1158
1174
  msgstr ""
1159
1175
 
1160
- msgid "Paste"
1176
+ msgid "Copy Row"
1177
+ msgstr ""
1178
+
1179
+ msgid "Copy Rows"
1180
+ msgstr ""
1181
+
1182
+ msgid "Copy Column"
1183
+ msgstr ""
1184
+
1185
+ msgid "Paste Rows"
1161
1186
  msgstr ""
1162
1187
 
1163
1188
  msgid ".."
Binary file