tryton 7.0.7__py3-none-any.whl → 7.4.4__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 +149 -73
- tryton/common/completion.py +2 -2
- tryton/common/datetime_.py +3 -1
- tryton/common/domain_inversion.py +2 -1
- tryton/common/domain_parser.py +22 -11
- tryton/common/popup_menu.py +1 -1
- 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 +69 -20
- tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ca/LC_MESSAGES/tryton.po +70 -25
- tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/cs/LC_MESSAGES/tryton.po +68 -21
- tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/de/LC_MESSAGES/tryton.po +71 -26
- tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/es/LC_MESSAGES/tryton.po +68 -23
- tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/es_419/LC_MESSAGES/tryton.po +72 -22
- tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/et/LC_MESSAGES/tryton.po +73 -23
- tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fa/LC_MESSAGES/tryton.po +74 -25
- tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fi/LC_MESSAGES/tryton.po +63 -20
- tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/fr/LC_MESSAGES/tryton.po +73 -28
- tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/hu/LC_MESSAGES/tryton.po +75 -23
- tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/id/LC_MESSAGES/tryton.po +72 -23
- tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/it/LC_MESSAGES/tryton.po +73 -24
- 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 +74 -25
- tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/lt/LC_MESSAGES/tryton.po +73 -23
- tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/nl/LC_MESSAGES/tryton.po +72 -27
- tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/pl/LC_MESSAGES/tryton.po +113 -78
- tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/pt/LC_MESSAGES/tryton.po +73 -24
- tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ro/LC_MESSAGES/tryton.po +87 -36
- tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/ru/LC_MESSAGES/tryton.po +72 -25
- tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/sl/LC_MESSAGES/tryton.po +77 -26
- tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/tr/LC_MESSAGES/tryton.po +64 -20
- tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/uk/LC_MESSAGES/tryton.po +75 -23
- tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
- tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +76 -24
- tryton/device_cookie.py +1 -1
- tryton/gui/main.py +14 -12
- tryton/gui/window/about.py +1 -1
- tryton/gui/window/dblogin.py +2 -2
- tryton/gui/window/email_.py +2 -2
- tryton/gui/window/form.py +10 -5
- tryton/gui/window/log.py +24 -2
- tryton/gui/window/tabcontent.py +2 -2
- tryton/gui/window/view_form/model/field.py +84 -34
- tryton/gui/window/view_form/model/group.py +7 -2
- tryton/gui/window/view_form/model/record.py +70 -31
- tryton/gui/window/view_form/screen/screen.py +98 -47
- 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 +49 -29
- tryton/gui/window/view_form/view/form_gtk/document.py +15 -10
- tryton/gui/window/view_form/view/form_gtk/many2many.py +49 -7
- tryton/gui/window/view_form/view/form_gtk/many2one.py +21 -13
- tryton/gui/window/view_form/view/form_gtk/multiselection.py +15 -5
- tryton/gui/window/view_form/view/form_gtk/one2many.py +42 -10
- tryton/gui/window/view_form/view/form_gtk/state_widget.py +6 -2
- tryton/gui/window/view_form/view/form_gtk/url.py +8 -4
- tryton/gui/window/view_form/view/graph_gtk/graph.py +3 -1
- tryton/gui/window/view_form/view/list.py +116 -48
- 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_csv.py +6 -12
- tryton/gui/window/win_export.py +49 -26
- tryton/gui/window/win_form.py +9 -7
- tryton/gui/window/win_import.py +45 -15
- tryton/gui/window/wizard.py +13 -10
- tryton/jsonrpc.py +75 -34
- tryton/plugins/__init__.py +5 -3
- tryton/pyson.py +57 -6
- tryton/rpc.py +18 -0
- tryton/tests/test_common_domain_parser.py +31 -2
- tryton/translate.py +5 -2
- {tryton-7.0.7.data → tryton-7.4.4.data}/scripts/tryton +8 -7
- {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/METADATA +6 -6
- {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/RECORD +105 -103
- {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/WHEEL +1 -1
- {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/LICENSE +0 -0
- {tryton-7.0.7.dist-info → tryton-7.4.4.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
|
|
|
@@ -369,7 +400,7 @@ def get_sensible_widget(window):
|
|
|
369
400
|
return window
|
|
370
401
|
|
|
371
402
|
|
|
372
|
-
def selection(title, values, alwaysask=False):
|
|
403
|
+
def selection(title, values, alwaysask=False, default=None):
|
|
373
404
|
if not values or len(values) == 0:
|
|
374
405
|
return None
|
|
375
406
|
elif len(values) == 1 and (not alwaysask):
|
|
@@ -408,14 +439,18 @@ def selection(title, values, alwaysask=False):
|
|
|
408
439
|
model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT)
|
|
409
440
|
keys = list(values.keys())
|
|
410
441
|
keys.sort()
|
|
411
|
-
|
|
412
|
-
for val in keys:
|
|
442
|
+
selected = None
|
|
443
|
+
for i, val in enumerate(keys):
|
|
413
444
|
model.append([str(val), i])
|
|
414
|
-
|
|
445
|
+
if default:
|
|
446
|
+
if values[keys[i]] == default:
|
|
447
|
+
selected = i
|
|
415
448
|
|
|
416
449
|
treeview.set_model(model)
|
|
417
450
|
treeview.connect('row-activated',
|
|
418
451
|
lambda x, y, z: dialog.response(Gtk.ResponseType.OK) or True)
|
|
452
|
+
if selected is not None:
|
|
453
|
+
treeview.get_selection().select_path(selected)
|
|
419
454
|
|
|
420
455
|
dialog.show_all()
|
|
421
456
|
response = dialog.run()
|
|
@@ -679,6 +714,10 @@ class MessageDialog(UniqueDialog):
|
|
|
679
714
|
message_type=msg_type, buttons=buttons, text=message)
|
|
680
715
|
if secondary:
|
|
681
716
|
dialog.format_secondary_text(secondary)
|
|
717
|
+
area = dialog.get_message_area()
|
|
718
|
+
for child in area.get_children():
|
|
719
|
+
if isinstance(child, Gtk.Label):
|
|
720
|
+
child.set_selectable(True)
|
|
682
721
|
return dialog
|
|
683
722
|
|
|
684
723
|
def __call__(self, message, *args, **kwargs):
|
|
@@ -799,7 +838,7 @@ class ConcurrencyDialog(UniqueDialog):
|
|
|
799
838
|
dialog = Gtk.MessageDialog(
|
|
800
839
|
transient_for=parent, modal=True, destroy_with_parent=True,
|
|
801
840
|
message_type=Gtk.MessageType.QUESTION,
|
|
802
|
-
buttons=Gtk.ButtonsType.NONE, text=_(
|
|
841
|
+
buttons=Gtk.ButtonsType.NONE, text=_("Concurrency Warning"))
|
|
803
842
|
dialog.format_secondary_text(
|
|
804
843
|
_('This record has been modified while you were editing it.'))
|
|
805
844
|
cancel_button = dialog.add_button(
|
|
@@ -844,7 +883,7 @@ class ErrorDialog(UniqueDialog):
|
|
|
844
883
|
dialog = Gtk.MessageDialog(
|
|
845
884
|
transient_for=parent, modal=True, destroy_with_parent=True,
|
|
846
885
|
message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.NONE)
|
|
847
|
-
dialog.set_default_size(600,
|
|
886
|
+
dialog.set_default_size(600, 200)
|
|
848
887
|
dialog.set_position(Gtk.WindowPosition.CENTER)
|
|
849
888
|
|
|
850
889
|
dialog.add_button(set_underline(_("Close")), Gtk.ResponseType.CANCEL)
|
|
@@ -859,6 +898,7 @@ class ErrorDialog(UniqueDialog):
|
|
|
859
898
|
scrolledwindow.set_policy(
|
|
860
899
|
Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
|
861
900
|
scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE)
|
|
901
|
+
scrolledwindow.set_min_content_height(300)
|
|
862
902
|
|
|
863
903
|
viewport = Gtk.Viewport()
|
|
864
904
|
viewport.set_shadow_type(Gtk.ShadowType.NONE)
|
|
@@ -870,8 +910,12 @@ class ErrorDialog(UniqueDialog):
|
|
|
870
910
|
|
|
871
911
|
viewport.add(textview)
|
|
872
912
|
scrolledwindow.add(viewport)
|
|
913
|
+
expander = Gtk.Expander()
|
|
914
|
+
expander.set_label(_("Details"))
|
|
915
|
+
expander.add(scrolledwindow)
|
|
916
|
+
expander.set_resize_toplevel(True)
|
|
873
917
|
dialog.vbox.pack_start(
|
|
874
|
-
|
|
918
|
+
expander, expand=False, fill=True, padding=0)
|
|
875
919
|
|
|
876
920
|
button_roundup = Gtk.LinkButton.new_with_label(
|
|
877
921
|
CONFIG['bug.url'], _("Report Bug"))
|
|
@@ -973,10 +1017,13 @@ def process_exception(exception, *args, **kwargs):
|
|
|
973
1017
|
name, msg, description = exception.args
|
|
974
1018
|
res = userwarning(description, msg)
|
|
975
1019
|
if res in ('always', 'ok'):
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1020
|
+
try:
|
|
1021
|
+
RPCExecute(
|
|
1022
|
+
'model', 'res.user.warning', 'skip',
|
|
1023
|
+
name, (res == 'always'),
|
|
1024
|
+
process_exception=False)
|
|
1025
|
+
except RPCException:
|
|
1026
|
+
pass
|
|
980
1027
|
return rpc_execute(*args)
|
|
981
1028
|
elif exception.faultCode == 'UserError':
|
|
982
1029
|
msg, description, domain = exception.args
|
|
@@ -1014,12 +1061,11 @@ def process_exception(exception, *args, **kwargs):
|
|
|
1014
1061
|
PLOCK.release()
|
|
1015
1062
|
if args:
|
|
1016
1063
|
return rpc_execute(*args)
|
|
1017
|
-
elif exception.faultCode
|
|
1064
|
+
elif exception.faultCode in map(str, HTTPStatus):
|
|
1065
|
+
err_msg = '[%s] %s' % (exception.faultCode, exception.faultString)
|
|
1018
1066
|
message(
|
|
1019
|
-
_('
|
|
1067
|
+
_('Error: "%s". Try again later.') % err_msg,
|
|
1020
1068
|
msg_type=Gtk.MessageType.ERROR)
|
|
1021
|
-
elif exception.faultCode == str(int(HTTPStatus.NOT_FOUND)):
|
|
1022
|
-
message(_("Not found."), msg_type=Gtk.MessageType.ERROR)
|
|
1023
1069
|
else:
|
|
1024
1070
|
error(exception, exception.faultString)
|
|
1025
1071
|
else:
|
|
@@ -1104,7 +1150,7 @@ class Login(object):
|
|
|
1104
1150
|
if exception.faultCode != 'LoginException':
|
|
1105
1151
|
raise
|
|
1106
1152
|
name, msg, type = exception.args
|
|
1107
|
-
value = getattr(self, 'get_%s' % type)(msg)
|
|
1153
|
+
value = getattr(self, 'get_%s' % type)(msg, name)
|
|
1108
1154
|
if value is None:
|
|
1109
1155
|
raise TrytonError('QueryCanceled')
|
|
1110
1156
|
parameters[name] = value
|
|
@@ -1113,12 +1159,35 @@ class Login(object):
|
|
|
1113
1159
|
return
|
|
1114
1160
|
|
|
1115
1161
|
@classmethod
|
|
1116
|
-
def get_char(cls, message):
|
|
1162
|
+
def get_char(cls, message, name):
|
|
1117
1163
|
return ask(message)
|
|
1118
1164
|
|
|
1119
1165
|
@classmethod
|
|
1120
|
-
def get_password(cls,
|
|
1121
|
-
|
|
1166
|
+
def get_password(cls, message_, name):
|
|
1167
|
+
class AskPasswordDialog(AskDialog):
|
|
1168
|
+
def build_dialog(self, *args, **kwargs):
|
|
1169
|
+
tooltips = Tooltips()
|
|
1170
|
+
dialog = super().build_dialog(*args, **kwargs)
|
|
1171
|
+
box = dialog.get_message_area()
|
|
1172
|
+
button = Gtk.Button.new_with_label(
|
|
1173
|
+
_("Reset forgotten password"))
|
|
1174
|
+
button.set_alignment(0, 0.5)
|
|
1175
|
+
button.set_relief(Gtk.ReliefStyle.NONE)
|
|
1176
|
+
tooltips.set_tip(
|
|
1177
|
+
button, _("Send you an email to reset your password."))
|
|
1178
|
+
button.connect('clicked', self.reset_password)
|
|
1179
|
+
box.pack_start(button, False, False, 0)
|
|
1180
|
+
return dialog
|
|
1181
|
+
|
|
1182
|
+
def reset_password(self, button):
|
|
1183
|
+
rpc.reset_password()
|
|
1184
|
+
message(
|
|
1185
|
+
_("A request to reset your password has been sent.\n"
|
|
1186
|
+
"Please check your mailbox."))
|
|
1187
|
+
self.entry.grab_focus()
|
|
1188
|
+
if name == 'password':
|
|
1189
|
+
ask = AskPasswordDialog()
|
|
1190
|
+
return ask(message_, visibility=False)
|
|
1122
1191
|
|
|
1123
1192
|
|
|
1124
1193
|
class Logout:
|
|
@@ -1254,6 +1323,8 @@ class RPCProgress(object):
|
|
|
1254
1323
|
|
|
1255
1324
|
def return_():
|
|
1256
1325
|
if self.exception:
|
|
1326
|
+
if not isinstance(self.exception, RPCException):
|
|
1327
|
+
raise RPCException(self.exception)
|
|
1257
1328
|
raise self.exception
|
|
1258
1329
|
else:
|
|
1259
1330
|
return self.res
|
|
@@ -1275,10 +1346,15 @@ def RPCExecute(*args, **kwargs):
|
|
|
1275
1346
|
|
|
1276
1347
|
|
|
1277
1348
|
def RPCContextReload(callback=None):
|
|
1349
|
+
def clean(context):
|
|
1350
|
+
return {
|
|
1351
|
+
k: v for k, v in context.items()
|
|
1352
|
+
if k != 'locale' and not k.endswith('.rec_name')}
|
|
1353
|
+
|
|
1278
1354
|
def update(context):
|
|
1279
1355
|
rpc.context_reset()
|
|
1280
1356
|
try:
|
|
1281
|
-
rpc.CONTEXT.update(context())
|
|
1357
|
+
rpc.CONTEXT.update(clean(context()))
|
|
1282
1358
|
except RPCException:
|
|
1283
1359
|
pass
|
|
1284
1360
|
if callback:
|
|
@@ -1288,7 +1364,7 @@ def RPCContextReload(callback=None):
|
|
|
1288
1364
|
callback=update if callback else None)
|
|
1289
1365
|
if not callback:
|
|
1290
1366
|
rpc.context_reset()
|
|
1291
|
-
rpc.CONTEXT.update(context)
|
|
1367
|
+
rpc.CONTEXT.update(clean(context))
|
|
1292
1368
|
|
|
1293
1369
|
|
|
1294
1370
|
class Tooltips(object):
|
|
@@ -1463,7 +1539,7 @@ def get_align(float_, expand=True):
|
|
|
1463
1539
|
|
|
1464
1540
|
|
|
1465
1541
|
def date_format(format_=None):
|
|
1466
|
-
return format_ or
|
|
1542
|
+
return format_ or translate.DATE or '%x'
|
|
1467
1543
|
|
|
1468
1544
|
|
|
1469
1545
|
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):
|
|
@@ -453,7 +457,7 @@ def complete_value(field, value):
|
|
|
453
457
|
|
|
454
458
|
def complete_datetime():
|
|
455
459
|
yield datetime.date.today()
|
|
456
|
-
yield datetime.datetime.
|
|
460
|
+
yield datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
|
|
457
461
|
|
|
458
462
|
def complete_date():
|
|
459
463
|
yield datetime.date.today()
|
|
@@ -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')
|
|
@@ -867,12 +877,13 @@ class DomainParser(object):
|
|
|
867
877
|
(field_name, '<=', rvalue),
|
|
868
878
|
])
|
|
869
879
|
continue
|
|
880
|
+
if field['type'] in {
|
|
881
|
+
'many2one', 'one2many', 'many2many', 'one2one',
|
|
882
|
+
}:
|
|
883
|
+
field_name += '.rec_name'
|
|
870
884
|
if isinstance(value, list):
|
|
871
885
|
value = [convert_value(field, v, self.context)
|
|
872
886
|
for v in value]
|
|
873
|
-
if field['type'] in ('many2one', 'one2many',
|
|
874
|
-
'many2many', 'one2one'):
|
|
875
|
-
field_name += '.rec_name'
|
|
876
887
|
else:
|
|
877
888
|
value = convert_value(field, value, self.context)
|
|
878
889
|
if 'like' in operator:
|
tryton/common/popup_menu.py
CHANGED
|
@@ -146,7 +146,7 @@ def populate(menu, model, record, title='', field=None, context=None):
|
|
|
146
146
|
item.connect('activate', activate, action, atype)
|
|
147
147
|
menu.show_all()
|
|
148
148
|
|
|
149
|
-
email_item = Gtk.MenuItem(label=_('
|
|
149
|
+
email_item = Gtk.MenuItem(label=_('Email...'))
|
|
150
150
|
action_menu.append(email_item)
|
|
151
151
|
email_item.connect('activate', email, toolbar)
|
|
152
152
|
menu.show_all()
|
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()
|