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.

Files changed (105) hide show
  1. tryton/__init__.py +1 -1
  2. tryton/cache.py +34 -0
  3. tryton/common/common.py +149 -73
  4. tryton/common/completion.py +2 -2
  5. tryton/common/datetime_.py +3 -1
  6. tryton/common/domain_inversion.py +2 -1
  7. tryton/common/domain_parser.py +22 -11
  8. tryton/common/popup_menu.py +1 -1
  9. tryton/common/selection.py +6 -3
  10. tryton/common/tempfile.py +34 -0
  11. tryton/config.py +4 -5
  12. tryton/data/locale/bg/LC_MESSAGES/tryton.mo +0 -0
  13. tryton/data/locale/bg/LC_MESSAGES/tryton.po +69 -20
  14. tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
  15. tryton/data/locale/ca/LC_MESSAGES/tryton.po +70 -25
  16. tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
  17. tryton/data/locale/cs/LC_MESSAGES/tryton.po +68 -21
  18. tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
  19. tryton/data/locale/de/LC_MESSAGES/tryton.po +71 -26
  20. tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
  21. tryton/data/locale/es/LC_MESSAGES/tryton.po +68 -23
  22. tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
  23. tryton/data/locale/es_419/LC_MESSAGES/tryton.po +72 -22
  24. tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
  25. tryton/data/locale/et/LC_MESSAGES/tryton.po +73 -23
  26. tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
  27. tryton/data/locale/fa/LC_MESSAGES/tryton.po +74 -25
  28. tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
  29. tryton/data/locale/fi/LC_MESSAGES/tryton.po +63 -20
  30. tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
  31. tryton/data/locale/fr/LC_MESSAGES/tryton.po +73 -28
  32. tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
  33. tryton/data/locale/hu/LC_MESSAGES/tryton.po +75 -23
  34. tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
  35. tryton/data/locale/id/LC_MESSAGES/tryton.po +72 -23
  36. tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
  37. tryton/data/locale/it/LC_MESSAGES/tryton.po +73 -24
  38. tryton/data/locale/ja_JP/LC_MESSAGES/tryton.mo +0 -0
  39. tryton/data/locale/lo/LC_MESSAGES/tryton.mo +0 -0
  40. tryton/data/locale/lo/LC_MESSAGES/tryton.po +74 -25
  41. tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
  42. tryton/data/locale/lt/LC_MESSAGES/tryton.po +73 -23
  43. tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
  44. tryton/data/locale/nl/LC_MESSAGES/tryton.po +72 -27
  45. tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
  46. tryton/data/locale/pl/LC_MESSAGES/tryton.po +113 -78
  47. tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
  48. tryton/data/locale/pt/LC_MESSAGES/tryton.po +73 -24
  49. tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
  50. tryton/data/locale/ro/LC_MESSAGES/tryton.po +87 -36
  51. tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
  52. tryton/data/locale/ru/LC_MESSAGES/tryton.po +72 -25
  53. tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
  54. tryton/data/locale/sl/LC_MESSAGES/tryton.po +77 -26
  55. tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
  56. tryton/data/locale/tr/LC_MESSAGES/tryton.po +64 -20
  57. tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
  58. tryton/data/locale/uk/LC_MESSAGES/tryton.po +75 -23
  59. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
  60. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +76 -24
  61. tryton/device_cookie.py +1 -1
  62. tryton/gui/main.py +14 -12
  63. tryton/gui/window/about.py +1 -1
  64. tryton/gui/window/dblogin.py +2 -2
  65. tryton/gui/window/email_.py +2 -2
  66. tryton/gui/window/form.py +10 -5
  67. tryton/gui/window/log.py +24 -2
  68. tryton/gui/window/tabcontent.py +2 -2
  69. tryton/gui/window/view_form/model/field.py +84 -34
  70. tryton/gui/window/view_form/model/group.py +7 -2
  71. tryton/gui/window/view_form/model/record.py +70 -31
  72. tryton/gui/window/view_form/screen/screen.py +98 -47
  73. tryton/gui/window/view_form/view/calendar_gtk/calendar_.py +15 -9
  74. tryton/gui/window/view_form/view/form.py +6 -12
  75. tryton/gui/window/view_form/view/form_gtk/char.py +5 -6
  76. tryton/gui/window/view_form/view/form_gtk/dictionary.py +49 -29
  77. tryton/gui/window/view_form/view/form_gtk/document.py +15 -10
  78. tryton/gui/window/view_form/view/form_gtk/many2many.py +49 -7
  79. tryton/gui/window/view_form/view/form_gtk/many2one.py +21 -13
  80. tryton/gui/window/view_form/view/form_gtk/multiselection.py +15 -5
  81. tryton/gui/window/view_form/view/form_gtk/one2many.py +42 -10
  82. tryton/gui/window/view_form/view/form_gtk/state_widget.py +6 -2
  83. tryton/gui/window/view_form/view/form_gtk/url.py +8 -4
  84. tryton/gui/window/view_form/view/graph_gtk/graph.py +3 -1
  85. tryton/gui/window/view_form/view/list.py +116 -48
  86. tryton/gui/window/view_form/view/list_gtk/editabletree.py +2 -1
  87. tryton/gui/window/view_form/view/list_gtk/widget.py +58 -23
  88. tryton/gui/window/view_form/view/screen_container.py +3 -5
  89. tryton/gui/window/win_csv.py +6 -12
  90. tryton/gui/window/win_export.py +49 -26
  91. tryton/gui/window/win_form.py +9 -7
  92. tryton/gui/window/win_import.py +45 -15
  93. tryton/gui/window/wizard.py +13 -10
  94. tryton/jsonrpc.py +75 -34
  95. tryton/plugins/__init__.py +5 -3
  96. tryton/pyson.py +57 -6
  97. tryton/rpc.py +18 -0
  98. tryton/tests/test_common_domain_parser.py +31 -2
  99. tryton/translate.py +5 -2
  100. {tryton-7.0.7.data → tryton-7.4.4.data}/scripts/tryton +8 -7
  101. {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/METADATA +6 -6
  102. {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/RECORD +105 -103
  103. {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/WHEEL +1 -1
  104. {tryton-7.0.7.dist-info → tryton-7.4.4.dist-info}/LICENSE +0 -0
  105. {tryton-7.0.7.dist-info → tryton-7.4.4.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.7"
3
+ __version__ = "7.4.4"
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
 
@@ -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
- i = 0
412
- for val in keys:
442
+ selected = None
443
+ for i, val in enumerate(keys):
413
444
  model.append([str(val), i])
414
- i += 1
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=_('Concurrency Exception'))
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, 400)
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
- scrolledwindow, expand=True, fill=True, padding=0)
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
- RPCExecute(
977
- 'model', 'res.user.warning', 'skip',
978
- name, (res == 'always'),
979
- process_exception=False)
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 == str(int(HTTPStatus.TOO_MANY_REQUESTS)):
1064
+ elif exception.faultCode in map(str, HTTPStatus):
1065
+ err_msg = '[%s] %s' % (exception.faultCode, exception.faultString)
1018
1066
  message(
1019
- _('Too many requests. Try again later.'),
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, message):
1121
- return ask(message, visibility=False)
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 rpc.CONTEXT.get('locale', {}).get('date', '%x')
1542
+ return format_ or translate.DATE or '%x'
1467
1543
 
1468
1544
 
1469
1545
  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
 
@@ -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
- value = value if operator == '=' and single_value else value[0]
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]
@@ -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):
@@ -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.utcnow()
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 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')
@@ -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:
@@ -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=_('E-Mail...'))
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()
@@ -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()