tryton 7.0.7__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.
- tryton/__init__.py +1 -1
- tryton/cache.py +34 -0
- tryton/common/common.py +137 -69
- tryton/common/completion.py +2 -2
- tryton/common/datetime_.py +3 -1
- tryton/common/domain_inversion.py +2 -1
- tryton/common/domain_parser.py +17 -7
- tryton/common/selection.py +6 -3
- tryton/common/tempfile.py +34 -0
- tryton/config.py +4 -5
- tryton/data/locale/bg/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/bg/LC_MESSAGES/tryton.po +42 -4
- tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ca/LC_MESSAGES/tryton.po +47 -8
- tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/cs/LC_MESSAGES/tryton.po +42 -4
- tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/de/LC_MESSAGES/tryton.po +45 -6
- tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/es/LC_MESSAGES/tryton.po +46 -7
- tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/es_419/LC_MESSAGES/tryton.po +43 -5
- tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/et/LC_MESSAGES/tryton.po +46 -6
- tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fa/LC_MESSAGES/tryton.po +46 -7
- tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fi/LC_MESSAGES/tryton.po +41 -4
- tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fr/LC_MESSAGES/tryton.po +46 -7
- tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/hu/LC_MESSAGES/tryton.po +46 -6
- tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/id/LC_MESSAGES/tryton.po +43 -4
- tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/it/LC_MESSAGES/tryton.po +45 -6
- tryton/data/locale/ja_JP/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/lo/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/lo/LC_MESSAGES/tryton.po +45 -6
- tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/lt/LC_MESSAGES/tryton.po +46 -6
- tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/nl/LC_MESSAGES/tryton.po +46 -7
- tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/pl/LC_MESSAGES/tryton.po +84 -60
- tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/pt/LC_MESSAGES/tryton.po +45 -6
- tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ro/LC_MESSAGES/tryton.po +57 -17
- tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ru/LC_MESSAGES/tryton.po +43 -6
- tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/sl/LC_MESSAGES/tryton.po +46 -5
- tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/tr/LC_MESSAGES/tryton.po +41 -4
- tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/uk/LC_MESSAGES/tryton.po +46 -6
- tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +46 -5
- tryton/device_cookie.py +1 -1
- tryton/gui/main.py +3 -2
- tryton/gui/window/about.py +1 -1
- tryton/gui/window/dblogin.py +2 -2
- tryton/gui/window/email_.py +1 -1
- tryton/gui/window/form.py +6 -4
- tryton/gui/window/log.py +24 -2
- tryton/gui/window/view_form/model/field.py +84 -34
- tryton/gui/window/view_form/model/group.py +3 -1
- tryton/gui/window/view_form/model/record.py +64 -15
- tryton/gui/window/view_form/screen/screen.py +83 -46
- tryton/gui/window/view_form/view/calendar_gtk/calendar_.py +15 -9
- tryton/gui/window/view_form/view/form.py +6 -12
- tryton/gui/window/view_form/view/form_gtk/char.py +5 -6
- tryton/gui/window/view_form/view/form_gtk/dictionary.py +37 -24
- tryton/gui/window/view_form/view/form_gtk/document.py +9 -10
- tryton/gui/window/view_form/view/form_gtk/many2many.py +17 -7
- tryton/gui/window/view_form/view/form_gtk/many2one.py +21 -13
- tryton/gui/window/view_form/view/form_gtk/one2many.py +25 -6
- tryton/gui/window/view_form/view/form_gtk/state_widget.py +6 -2
- tryton/gui/window/view_form/view/graph_gtk/graph.py +3 -1
- tryton/gui/window/view_form/view/list.py +68 -35
- tryton/gui/window/view_form/view/list_gtk/editabletree.py +2 -1
- tryton/gui/window/view_form/view/list_gtk/widget.py +58 -23
- tryton/gui/window/view_form/view/screen_container.py +3 -5
- tryton/gui/window/win_export.py +1 -2
- tryton/gui/window/win_form.py +9 -7
- tryton/gui/window/win_import.py +9 -4
- tryton/gui/window/wizard.py +13 -10
- tryton/jsonrpc.py +46 -28
- tryton/plugins/__init__.py +5 -3
- tryton/pyson.py +55 -5
- tryton/rpc.py +18 -0
- tryton/tests/test_common_domain_parser.py +8 -0
- tryton/translate.py +5 -2
- {tryton-7.0.7.data → tryton-7.2.13.data}/scripts/tryton +8 -7
- {tryton-7.0.7.dist-info → tryton-7.2.13.dist-info}/METADATA +6 -6
- {tryton-7.0.7.dist-info → tryton-7.2.13.dist-info}/RECORD +100 -98
- {tryton-7.0.7.dist-info → tryton-7.2.13.dist-info}/WHEEL +1 -1
- {tryton-7.0.7.dist-info → tryton-7.2.13.dist-info}/LICENSE +0 -0
- {tryton-7.0.7.dist-info → tryton-7.2.13.dist-info}/top_level.txt +0 -0
tryton/__init__.py
CHANGED
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
|
|
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
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 = [
|
|
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
|
-
|
|
121
|
-
cls._icons[name] =
|
|
122
|
-
|
|
123
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
205
|
+
pixbuf = None
|
|
206
|
+
logger.info(f'GET {url}')
|
|
207
207
|
try:
|
|
208
208
|
with urllib.request.urlopen(url) as response:
|
|
209
|
-
|
|
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
|
-
|
|
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=_(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
|
1056
|
+
elif exception.faultCode in map(str, HTTPStatus):
|
|
1057
|
+
err_msg = '[%s] %s' % (exception.faultCode, exception.faultString)
|
|
1018
1058
|
message(
|
|
1019
|
-
_('
|
|
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,
|
|
1121
|
-
|
|
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
|
|
1534
|
+
return format_ or translate.DATE or '%x'
|
|
1467
1535
|
|
|
1468
1536
|
|
|
1469
1537
|
def idle_add(func):
|
tryton/common/completion.py
CHANGED
|
@@ -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
|
|
75
|
+
except RPCException:
|
|
76
76
|
logger.warning(
|
|
77
77
|
"Unable to search for completion of %s", model,
|
|
78
78
|
exc_info=True)
|
tryton/common/datetime_.py
CHANGED
|
@@ -9,7 +9,9 @@ from gi.repository import Gdk, GObject, Gtk
|
|
|
9
9
|
|
|
10
10
|
from .common import IconFactory
|
|
11
11
|
|
|
12
|
-
__all__ = [
|
|
12
|
+
__all__ = [
|
|
13
|
+
'Date', 'CellRendererDate', 'Time', 'CellRendererTime', 'DateTime',
|
|
14
|
+
'date_parse']
|
|
13
15
|
|
|
14
16
|
_ = gettext.gettext
|
|
15
17
|
|
|
@@ -433,7 +433,8 @@ 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
|
-
|
|
436
|
+
if operator == 'in' and single_value:
|
|
437
|
+
value = value[0]
|
|
437
438
|
if model and name.endswith('.id'):
|
|
438
439
|
model = model[0]
|
|
439
440
|
value = [model, value]
|
tryton/common/domain_parser.py
CHANGED
|
@@ -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(
|
|
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
|
|
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')
|
tryton/common/selection.py
CHANGED
|
@@ -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(
|
|
34
|
-
value
|
|
33
|
+
selection = RPCExecute(
|
|
34
|
+
'model', self.model_name, selection, value,
|
|
35
|
+
process_exception=False)
|
|
35
36
|
else:
|
|
36
|
-
selection = RPCExecute(
|
|
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
|
|
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
|
|
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
|
|
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
|