tryton 7.0.21__py3-none-any.whl → 7.2.1__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 (94) 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 +42 -4
  12. tryton/data/locale/ca/LC_MESSAGES/tryton.mo +0 -0
  13. tryton/data/locale/ca/LC_MESSAGES/tryton.po +47 -8
  14. tryton/data/locale/cs/LC_MESSAGES/tryton.mo +0 -0
  15. tryton/data/locale/cs/LC_MESSAGES/tryton.po +42 -4
  16. tryton/data/locale/de/LC_MESSAGES/tryton.mo +0 -0
  17. tryton/data/locale/de/LC_MESSAGES/tryton.po +45 -6
  18. tryton/data/locale/es/LC_MESSAGES/tryton.mo +0 -0
  19. tryton/data/locale/es/LC_MESSAGES/tryton.po +46 -7
  20. tryton/data/locale/es_419/LC_MESSAGES/tryton.mo +0 -0
  21. tryton/data/locale/es_419/LC_MESSAGES/tryton.po +43 -5
  22. tryton/data/locale/et/LC_MESSAGES/tryton.mo +0 -0
  23. tryton/data/locale/et/LC_MESSAGES/tryton.po +46 -6
  24. tryton/data/locale/fa/LC_MESSAGES/tryton.mo +0 -0
  25. tryton/data/locale/fa/LC_MESSAGES/tryton.po +46 -7
  26. tryton/data/locale/fi/LC_MESSAGES/tryton.mo +0 -0
  27. tryton/data/locale/fi/LC_MESSAGES/tryton.po +41 -4
  28. tryton/data/locale/fr/LC_MESSAGES/tryton.mo +0 -0
  29. tryton/data/locale/fr/LC_MESSAGES/tryton.po +46 -7
  30. tryton/data/locale/hu/LC_MESSAGES/tryton.mo +0 -0
  31. tryton/data/locale/hu/LC_MESSAGES/tryton.po +46 -6
  32. tryton/data/locale/id/LC_MESSAGES/tryton.mo +0 -0
  33. tryton/data/locale/id/LC_MESSAGES/tryton.po +43 -4
  34. tryton/data/locale/it/LC_MESSAGES/tryton.mo +0 -0
  35. tryton/data/locale/it/LC_MESSAGES/tryton.po +45 -6
  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 +45 -6
  39. tryton/data/locale/lt/LC_MESSAGES/tryton.mo +0 -0
  40. tryton/data/locale/lt/LC_MESSAGES/tryton.po +46 -6
  41. tryton/data/locale/nl/LC_MESSAGES/tryton.mo +0 -0
  42. tryton/data/locale/nl/LC_MESSAGES/tryton.po +46 -7
  43. tryton/data/locale/pl/LC_MESSAGES/tryton.mo +0 -0
  44. tryton/data/locale/pl/LC_MESSAGES/tryton.po +84 -60
  45. tryton/data/locale/pt/LC_MESSAGES/tryton.mo +0 -0
  46. tryton/data/locale/pt/LC_MESSAGES/tryton.po +45 -6
  47. tryton/data/locale/ro/LC_MESSAGES/tryton.mo +0 -0
  48. tryton/data/locale/ro/LC_MESSAGES/tryton.po +57 -17
  49. tryton/data/locale/ru/LC_MESSAGES/tryton.mo +0 -0
  50. tryton/data/locale/ru/LC_MESSAGES/tryton.po +43 -6
  51. tryton/data/locale/sl/LC_MESSAGES/tryton.mo +0 -0
  52. tryton/data/locale/sl/LC_MESSAGES/tryton.po +46 -5
  53. tryton/data/locale/tr/LC_MESSAGES/tryton.mo +0 -0
  54. tryton/data/locale/tr/LC_MESSAGES/tryton.po +41 -4
  55. tryton/data/locale/uk/LC_MESSAGES/tryton.mo +0 -0
  56. tryton/data/locale/uk/LC_MESSAGES/tryton.po +46 -6
  57. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.mo +0 -0
  58. tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po +46 -5
  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 +22 -22
  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/binary.py +3 -3
  73. tryton/gui/window/view_form/view/form_gtk/dictionary.py +33 -27
  74. tryton/gui/window/view_form/view/form_gtk/document.py +10 -9
  75. tryton/gui/window/view_form/view/form_gtk/many2many.py +17 -7
  76. tryton/gui/window/view_form/view/form_gtk/many2one.py +21 -13
  77. tryton/gui/window/view_form/view/form_gtk/one2many.py +25 -6
  78. tryton/gui/window/view_form/view/form_gtk/state_widget.py +6 -2
  79. tryton/gui/window/view_form/view/list.py +47 -56
  80. tryton/gui/window/view_form/view/list_gtk/widget.py +36 -23
  81. tryton/gui/window/view_form/view/screen_container.py +5 -3
  82. tryton/gui/window/win_export.py +1 -2
  83. tryton/gui/window/win_form.py +6 -8
  84. tryton/gui/window/wizard.py +11 -10
  85. tryton/jsonrpc.py +41 -27
  86. tryton/pyson.py +54 -4
  87. tryton/rpc.py +18 -0
  88. tryton/tests/test_common_domain_parser.py +0 -8
  89. {tryton-7.0.21.data → tryton-7.2.1.data}/scripts/tryton +1 -2
  90. {tryton-7.0.21.dist-info → tryton-7.2.1.dist-info}/METADATA +6 -20
  91. {tryton-7.0.21.dist-info → tryton-7.2.1.dist-info}/RECORD +94 -92
  92. {tryton-7.0.21.dist-info → tryton-7.2.1.dist-info}/WHEEL +1 -1
  93. {tryton-7.0.21.dist-info → tryton-7.2.1.dist-info}/LICENSE +0 -0
  94. {tryton-7.0.21.dist-info → tryton-7.2.1.dist-info}/top_level.txt +0 -0
@@ -29,6 +29,10 @@ msgstr "设置服务器主机名:端口号"
29
29
  msgid "disable thread usage"
30
30
  msgstr "禁用线程"
31
31
 
32
+ #, python-format
33
+ msgid "Invalid response id (%s) expected %s"
34
+ msgstr ""
35
+
32
36
  #, python-format
33
37
  msgid "Unable to set locale %s"
34
38
  msgstr "无法设置 locale %s"
@@ -79,7 +83,8 @@ msgstr "否"
79
83
  msgid "Yes"
80
84
  msgstr "是"
81
85
 
82
- msgid "Concurrency Exception"
86
+ #, fuzzy
87
+ msgid "Concurrency Warning"
83
88
  msgstr "并行操作异常"
84
89
 
85
90
  msgid "This record has been modified while you were editing it."
@@ -110,6 +115,9 @@ msgstr "关闭"
110
115
  msgid "Application Error"
111
116
  msgstr "程序错误"
112
117
 
118
+ msgid "Details"
119
+ msgstr ""
120
+
113
121
  msgid "Report Bug"
114
122
  msgstr "报告Bug"
115
123
 
@@ -126,18 +134,33 @@ msgstr "有新版本可用!"
126
134
  msgid "Download"
127
135
  msgstr "下载"
128
136
 
137
+ msgid "Concurrency Exception"
138
+ msgstr "并行操作异常"
139
+
129
140
  msgid "Could not get a session."
130
141
  msgstr "无法获取会话。"
131
142
 
132
- msgid "Too many requests. Try again later."
143
+ #, fuzzy, python-format
144
+ msgid "Error: \"%s\". Try again later."
133
145
  msgstr "请求太多,请稍后重试."
134
146
 
135
- msgid "Not found."
136
- msgstr "无符合条件的数据."
147
+ msgid "Too many requests. Try again later."
148
+ msgstr "请求太多,请稍后重试."
137
149
 
138
150
  msgid "Not Found."
139
151
  msgstr "无符合条件的数据."
140
152
 
153
+ msgid "Reset forgotten password"
154
+ msgstr ""
155
+
156
+ msgid "Send you an email to reset your password."
157
+ msgstr ""
158
+
159
+ msgid ""
160
+ "A request to reset your password has been sent.\n"
161
+ "Please check your mailbox."
162
+ msgstr ""
163
+
141
164
  msgid "..."
142
165
  msgstr "..."
143
166
 
@@ -710,6 +733,12 @@ msgstr "模型:"
710
733
  msgid "ID:"
711
734
  msgstr "标识:"
712
735
 
736
+ msgid "Module:"
737
+ msgstr ""
738
+
739
+ msgid "XML ID:"
740
+ msgstr ""
741
+
713
742
  msgid "Created by:"
714
743
  msgstr "创建者:"
715
744
 
@@ -1083,7 +1112,19 @@ msgstr "图片尺寸过大."
1083
1112
  msgid "Copy"
1084
1113
  msgstr "复制"
1085
1114
 
1086
- msgid "Paste"
1115
+ #, fuzzy
1116
+ msgid "Copy Row"
1117
+ msgstr "复制(_C)URL"
1118
+
1119
+ #, fuzzy
1120
+ msgid "Copy Rows"
1121
+ msgstr "复制(_C)URL"
1122
+
1123
+ msgid "Copy Column"
1124
+ msgstr ""
1125
+
1126
+ #, fuzzy
1127
+ msgid "Paste Rows"
1087
1128
  msgstr "粘贴"
1088
1129
 
1089
1130
  msgid ".."
tryton/device_cookie.py CHANGED
@@ -20,7 +20,7 @@ def renew():
20
20
  def set_cookie(new_cookie):
21
21
  try:
22
22
  new_cookie = new_cookie()
23
- except Exception:
23
+ except common.RPCException:
24
24
  logger.error("Cannot renew device cookie", exc_info=True)
25
25
  else:
26
26
  _set(new_cookie)
tryton/gui/main.py CHANGED
@@ -401,7 +401,8 @@ class Main(Gtk.Application):
401
401
 
402
402
  RPCExecute('model', 'ir.model', 'global_search', search_text,
403
403
  CONFIG['client.limit'], self.menu_screen.model_name,
404
- context=self.menu_screen.context, callback=set_result)
404
+ context=self.menu_screen.context, callback=set_result,
405
+ process_exception=False)
405
406
  return False
406
407
 
407
408
  def changed(widget):
@@ -470,7 +471,7 @@ class Main(Gtk.Application):
470
471
  favorites = RPCExecute('model',
471
472
  self.menu_screen.model_name + '.favorite', 'get',
472
473
  process_exception=False)
473
- except Exception:
474
+ except RPCException:
474
475
  return False
475
476
  for id_, name, icon in favorites:
476
477
  menuitem = Gtk.MenuItem(label=name)
@@ -11,7 +11,7 @@ from tryton.common import get_toplevel_window
11
11
  from tryton.config import CONFIG, PIXMAPS_DIR
12
12
 
13
13
  COPYRIGHT = '''\
14
- Copyright (C) 2004-2025 Tryton.
14
+ Copyright (C) 2004-2024 Tryton.
15
15
  '''
16
16
  AUTHORS = [
17
17
  'Bertrand Chenal <bertrand.chenal@b2ck.com>',
@@ -7,8 +7,8 @@ import logging
7
7
  import os
8
8
  import re
9
9
  import shutil
10
- import tempfile
11
10
  import threading
11
+ from tempfile import NamedTemporaryFile
12
12
 
13
13
  from gi.repository import GLib, GObject, Gtk
14
14
 
@@ -501,7 +501,7 @@ class DBLogin(object):
501
501
  # reset self.profiles as parsing errors may leave wrong data in
502
502
  # the parser
503
503
  self.profiles = configparser.ConfigParser()
504
- with tempfile.NamedTemporaryFile(
504
+ with NamedTemporaryFile(
505
505
  delete=False, prefix='profiles_', suffix='.cfg',
506
506
  dir=config_dir) as temp_file:
507
507
  temp_name = temp_file.name
@@ -74,7 +74,7 @@ class EmailEntry(Gtk.Entry):
74
74
  RPCExecute(
75
75
  'model', 'ir.email', 'complete', text, CONFIG['client.limit'],
76
76
  process_exception=False, callback=callback)
77
- except Exception:
77
+ except RPCException:
78
78
  logger.warning(
79
79
  _("Unable to complete email entry"), exc_info=True)
80
80
  return False
tryton/gui/window/form.py CHANGED
@@ -5,7 +5,6 @@ import csv
5
5
  import gettext
6
6
  import locale
7
7
  import os
8
- import tempfile
9
8
  from itertools import zip_longest
10
9
 
11
10
  from gi.repository import Gdk, GLib, Gtk
@@ -13,7 +12,7 @@ from gi.repository import Gdk, GLib, Gtk
13
12
  import tryton.common as common
14
13
  from tryton import plugins
15
14
  from tryton.action import Action
16
- from tryton.common import RPCException, RPCExecute, sur, sur_3b
15
+ from tryton.common import RPCException, RPCExecute, sur, sur_3b, tempfile
17
16
  from tryton.common.common import selection as selection_
18
17
  from tryton.common.popup_menu import popup
19
18
  from tryton.common.underline import set_underline
@@ -106,7 +105,9 @@ class Form(TabContent):
106
105
  def compare(self, model, attributes):
107
106
  if not attributes:
108
107
  return False
109
- return (self.model == model
108
+ return (
109
+ self.screen.view_index == 0
110
+ and self.model == model
110
111
  and self.res_id == attributes.get('res_id')
111
112
  and self.attributes.get('domain') == attributes.get('domain')
112
113
  and self.attributes.get('view_ids') == attributes.get('view_ids')
tryton/gui/window/log.py CHANGED
@@ -24,7 +24,8 @@ class Log(WinForm):
24
24
  log, = RPCExecute(
25
25
  'model', record.model_name, 'read', [record.id],
26
26
  ['create_uid.rec_name', 'create_date',
27
- 'write_uid.rec_name', 'write_date'], context=context)
27
+ 'write_uid.rec_name', 'write_date',
28
+ 'xml_id'], context=context)
28
29
  except RPCException:
29
30
  return
30
31
 
@@ -53,11 +54,32 @@ class Log(WinForm):
53
54
  label_id.set_mnemonic_widget(entry_id)
54
55
  grid.attach(label_id, 2, 1, 1, 1)
55
56
 
57
+ if log.get('xml_id'):
58
+ module, xml_id = log['xml_id'].split('.', 1)
59
+
60
+ entry_module = Gtk.Entry(editable=False)
61
+ entry_module.set_text(module)
62
+ grid.attach(entry_module, 1, 2, 1, 1)
63
+ label_module = Gtk.Label(
64
+ label=set_underline(_("Module:")),
65
+ use_underline=True, halign=Gtk.Align.END)
66
+ label_module.set_mnemonic_widget(entry_module)
67
+ grid.attach(label_module, 0, 2, 1, 1)
68
+
69
+ entry_xml_id = Gtk.Entry(editable=False)
70
+ entry_xml_id.set_text(xml_id)
71
+ grid.attach(entry_xml_id, 3, 2, 1, 1)
72
+ label_xml_id = Gtk.Label(
73
+ label=set_underline(_("XML ID:")),
74
+ use_underline=True, halign=Gtk.Align.END)
75
+ label_xml_id.set_mnemonic_widget(entry_xml_id)
76
+ grid.attach(label_xml_id, 2, 2, 1, 1)
77
+
56
78
  for i, (user, user_label, date, date_label) in enumerate([
57
79
  ('create_uid.', _("Created by:"),
58
80
  'create_date', _("Created at:")),
59
81
  ('write_uid.', _("Last Modified by:"),
60
- 'write_date', _("Last Modified at:"))], 2):
82
+ 'write_date', _("Last Modified at:"))], 3):
61
83
  entry_user = Gtk.Entry(editable=False, width_chars=50)
62
84
  user = log.get(user)
63
85
  if user:
@@ -2,20 +2,21 @@
2
2
  # this repository contains the full copyright notices and license terms.
3
3
  import datetime
4
4
  import decimal
5
+ import functools
5
6
  import locale
6
7
  import logging
7
8
  import math
9
+ import operator
8
10
  import os
9
- import tempfile
10
11
  from decimal import Decimal
11
12
  from itertools import chain
12
- from pathlib import Path
13
13
 
14
14
  import tryton.common as common
15
15
  from tryton.common import (
16
16
  EvalEnvironment, RPCException, RPCExecute, concat, domain_inversion,
17
17
  eval_domain, extract_reference_models, filter_leaf, inverse_leaf,
18
- localize_domain, merge, prepare_reference_domain, simplify, unique_value)
18
+ localize_domain, merge, prepare_reference_domain, simplify, tempfile,
19
+ unique_value)
19
20
  from tryton.common.htmltextbuffer import guess_decode
20
21
  from tryton.config import CONFIG
21
22
  from tryton.pyson import PYSONDecoder
@@ -619,10 +620,12 @@ class O2MField(Field):
619
620
  from .group import Group
620
621
  parent_name = self.attrs.get('relation_field', '')
621
622
  fields = fields or {}
623
+ context = record.expr_eval(self.attrs.get('context', {}))
622
624
  group = Group(self.attrs['relation'], fields,
623
625
  parent=record,
624
626
  parent_name=parent_name,
625
627
  child_name=self.name,
628
+ context=context,
626
629
  parent_datetime_field=self.attrs.get('datetime_field'))
627
630
  if not fields and record.model_name == self.attrs['relation']:
628
631
  group.fields = record.group.fields
@@ -698,7 +701,8 @@ class O2MField(Field):
698
701
  skip={self.attrs.get('relation_field', '')}))
699
702
  return result
700
703
 
701
- def _set_value(self, record, value, default=False, modified=False):
704
+ def _set_value(
705
+ self, record, value, default=False, modified=False, data=None):
702
706
  self._set_default_value(record)
703
707
  group = record.value[self.name]
704
708
  if value is None:
@@ -708,23 +712,42 @@ class O2MField(Field):
708
712
  else:
709
713
  mode = 'list values'
710
714
 
711
- if mode == 'list values':
715
+ if mode == 'list values' or data:
712
716
  context = self.get_context(record)
713
- field_names = set(f for v in value for f in v
714
- if f not in group.fields and '.' not in f)
715
- if field_names:
717
+ if mode == 'list values':
718
+ fields = set(f for v in value for f in v)
719
+ else:
720
+ fields = functools.reduce(
721
+ operator.or_, (d.keys() for d in data), set())
722
+ field_names = {f for f in fields
723
+ if (f not in group.fields
724
+ and '.' not in f
725
+ and ':' not in f
726
+ and not f.startswith('_'))}
727
+ attr_fields = functools.reduce(
728
+ operator.or_,
729
+ (v['fields'] for v in self.attrs.get('views', {}).values()),
730
+ {})
731
+ fields = {n: attr_fields[n]
732
+ for n in field_names
733
+ if n in attr_fields}
734
+ if to_fetch := (field_names - attr_fields.keys()):
716
735
  try:
717
- fields = RPCExecute('model', self.attrs['relation'],
718
- 'fields_get', list(field_names), context=context)
736
+ fields |= RPCExecute('model', self.attrs['relation'],
737
+ 'fields_get', list(to_fetch), context=context)
719
738
  except RPCException:
720
739
  return
740
+
741
+ if fields:
721
742
  group.load_fields(fields)
722
743
 
723
744
  if mode == 'list ids':
724
745
  records_to_remove = [r for r in group if r.id not in value]
725
746
  for record_to_remove in records_to_remove:
726
747
  group.remove(record_to_remove, remove=True, modified=False)
727
- group.load(value, modified=modified or default)
748
+ group.load(
749
+ value, modified=modified or default,
750
+ preloaded={v['id']: v for v in (data or [])})
728
751
  else:
729
752
  for vals in value:
730
753
  if 'id' in vals:
@@ -745,7 +768,7 @@ class O2MField(Field):
745
768
  # Trigger modified only once
746
769
  group.record_modified()
747
770
 
748
- def set(self, record, value, _default=False):
771
+ def set(self, record, value, _default=False, preloaded=None):
749
772
  group = record.value.get(self.name)
750
773
  fields = {}
751
774
  if group is not None:
@@ -765,7 +788,7 @@ class O2MField(Field):
765
788
 
766
789
  # Prevent to trigger group-cleared
767
790
  group.parent = None
768
- self._set_value(record, value, default=_default)
791
+ self._set_value(record, value, default=_default, data=preloaded)
769
792
  group.parent = record
770
793
 
771
794
  def set_client(self, record, value, force_change=False):
@@ -1047,56 +1070,26 @@ class ReferenceField(Field):
1047
1070
 
1048
1071
 
1049
1072
  class _FileCache(object):
1050
- def __init__(self, data=None):
1051
- _, filename = tempfile.mkstemp(prefix='tryton_')
1052
- self.path = Path(filename)
1053
- self.suffixes = {}
1054
- if data:
1055
- with open(self.path, 'wb') as fp:
1056
- fp.write(data)
1057
-
1058
- @property
1059
- def data(self):
1060
- with open(self.path, 'rb') as fp:
1061
- return fp.read()
1073
+ def __init__(self, path):
1074
+ self.path = path
1062
1075
 
1063
1076
  def __del__(self):
1064
1077
  try:
1065
1078
  os.remove(self.path)
1066
1079
  except IOError:
1067
1080
  pass
1068
- for path in self.suffixes.values():
1069
- try:
1070
- os.remove(path)
1071
- except IOError:
1072
- pass
1073
-
1074
- def with_suffix(self, suffix):
1075
- if suffix in self.suffixes:
1076
- return self.suffixes[suffix]
1077
- _, filename = tempfile.mkstemp(prefix='tryton_', suffix=suffix)
1078
- self.suffixes[suffix] = path = Path(filename)
1079
- with open(path, 'wb') as fp:
1080
- fp.write(self.data)
1081
- return path
1082
1081
 
1083
1082
 
1084
1083
  class BinaryField(Field):
1085
1084
 
1086
1085
  _default = None
1087
1086
 
1088
- def _set_file_cache(self, record, data):
1089
- if isinstance(data, str):
1090
- data = data.encode('utf-8')
1091
- file_cache = _FileCache(data)
1092
- self.set(record, file_cache)
1093
- return file_cache
1094
-
1095
1087
  def get(self, record):
1096
1088
  result = record.value.get(self.name, self._default)
1097
1089
  if isinstance(result, _FileCache):
1098
1090
  try:
1099
- result = result.data
1091
+ with open(result.path, 'rb') as fp:
1092
+ result = fp.read()
1100
1093
  except IOError:
1101
1094
  result = self.get_data(record)
1102
1095
  return result
@@ -1105,7 +1098,13 @@ class BinaryField(Field):
1105
1098
  return self.get(record)
1106
1099
 
1107
1100
  def set_client(self, record, value, force_change=False):
1108
- self._set_file_cache(record, value or b'')
1101
+ _, filename = tempfile.mkstemp(prefix='tryton_')
1102
+ data = value or b''
1103
+ if isinstance(data, str):
1104
+ data = data.encode('utf-8')
1105
+ with open(filename, 'wb') as fp:
1106
+ fp.write(data)
1107
+ self.set(record, _FileCache(filename))
1109
1108
  self.sig_changed(record)
1110
1109
  record.validate(softvalidation=True)
1111
1110
  record.set_modified(self.name)
@@ -1129,20 +1128,15 @@ class BinaryField(Field):
1129
1128
  [record.id], [self.name], context=context)
1130
1129
  except RPCException:
1131
1130
  return b''
1132
- self._set_file_cache(record, values[self.name] or b'')
1131
+ _, filename = tempfile.mkstemp(prefix='tryton_')
1132
+ data = values[self.name] or b''
1133
+ if isinstance(data, str):
1134
+ data = data.encode('utf-8')
1135
+ with open(filename, 'wb') as fp:
1136
+ fp.write(data)
1137
+ self.set(record, _FileCache(filename))
1133
1138
  return self.get(record)
1134
1139
 
1135
- def get_filename(self, record, suffix=None):
1136
- data = self.get_data(record)
1137
- file_cache = record.value.get(self.name)
1138
- if not isinstance(file_cache, _FileCache):
1139
- file_cache = self._set_file_cache(record, data)
1140
- if suffix:
1141
- filename = file_cache.with_suffix(suffix)
1142
- else:
1143
- filename = file_cache.path
1144
- return filename
1145
-
1146
1140
 
1147
1141
  class DictField(Field):
1148
1142
 
@@ -1154,10 +1148,10 @@ class DictField(Field):
1154
1148
  self.keys = {}
1155
1149
 
1156
1150
  def get(self, record):
1157
- return (super().get(record) or self._default).copy()
1151
+ return super(DictField, self).get(record) or self._default
1158
1152
 
1159
1153
  def get_client(self, record):
1160
- return super().get_client(record)
1154
+ return super(DictField, self).get_client(record) or self._default
1161
1155
 
1162
1156
  def validation_domains(self, record, pre_validate=None):
1163
1157
  screen_domain, attr_domain = self.domains_get(record, pre_validate)
@@ -239,7 +239,7 @@ class Group(list):
239
239
  return []
240
240
  return list({}.fromkeys(res))
241
241
 
242
- def load(self, ids, modified=False, position=-1):
242
+ def load(self, ids, modified=False, position=-1, preloaded=None):
243
243
  if not ids:
244
244
  return True
245
245
 
@@ -256,6 +256,8 @@ class Group(list):
256
256
  else:
257
257
  self.insert(position, new_record)
258
258
  position += 1
259
+ if preloaded and (already_loaded := preloaded.get(id)):
260
+ new_record.set(already_loaded, modified=False, validate=False)
259
261
  new_records.append(new_record)
260
262
 
261
263
  # Remove previously removed or deleted records
@@ -1,6 +1,8 @@
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
+ import functools
3
4
  import logging
5
+ import operator
4
6
 
5
7
  import tryton.common as common
6
8
  from tryton.common import RPCException, RPCExecute
@@ -12,6 +14,30 @@ from . import field as fields
12
14
  logger = logging.getLogger(__name__)
13
15
 
14
16
 
17
+ def get_x2m_sub_fields(f_attrs, prefix):
18
+ if f_attrs['loading'] == 'eager' and f_attrs.get('views'):
19
+ sub_fields = functools.reduce(
20
+ operator.or_,
21
+ (v.get('fields', {}) for v in f_attrs['views'].values()),
22
+ {})
23
+ x2m_sub_fields = []
24
+ for s_field, f_def in sub_fields.items():
25
+ x2m_sub_fields.append(f"{prefix}.{s_field}")
26
+ if f_def['type'] in {'many2one', 'one2one', 'reference'}:
27
+ x2m_sub_fields.append(f"{prefix}.{s_field}.rec_name")
28
+ elif f_def['type'] in {'selection', 'multiselection'}:
29
+ x2m_sub_fields.append(f"{prefix}.{s_field}:string")
30
+ elif f_def['type'] in {'one2many', 'many2many'}:
31
+ x2m_sub_fields.extend(
32
+ get_x2m_sub_fields(f_def, f"{prefix}.{s_field}"))
33
+ x2m_sub_fields.extend(
34
+ f"{prefix}.{f}"
35
+ for f in ['_timestamp', '_write', '_delete'])
36
+ return x2m_sub_fields
37
+ else:
38
+ return []
39
+
40
+
15
41
  class Record:
16
42
 
17
43
  id = -1
@@ -79,6 +105,7 @@ class Record:
79
105
  fnames = [fname for fname, field in fields
80
106
  if fname not in self._loaded
81
107
  and (not views or (views & field.views))]
108
+ related_read_limit = 0
82
109
  for fname in list(fnames):
83
110
  f_attrs = self.group.fields[fname].attrs
84
111
  if f_attrs['type'] in {'many2one', 'one2one', 'reference'}:
@@ -86,6 +113,11 @@ class Record:
86
113
  elif (f_attrs['type'] in {'selection', 'multiselection'}
87
114
  and f_attrs.get('loading', 'lazy') == 'eager'):
88
115
  fnames.append('%s:string' % fname)
116
+ elif f_attrs['type'] in {'many2many', 'one2many'}:
117
+ sub_fields = get_x2m_sub_fields(f_attrs, fname)
118
+ fnames.extend(sub_fields)
119
+ if sub_fields:
120
+ related_read_limit += len(sub_fields)
89
121
  if 'rec_name' not in fnames:
90
122
  fnames.append('rec_name')
91
123
  fnames.extend(['_timestamp', '_write', '_delete'])
@@ -132,22 +164,24 @@ class Record:
132
164
  ctx.update(dict(('%s.%s' % (self.model_name, fname), 'size')
133
165
  for fname, field in self.group.fields.items()
134
166
  if field.attrs['type'] == 'binary' and fname in fnames))
167
+ if related_read_limit:
168
+ ctx['related_read_limit'] = (
169
+ CONFIG['client.limit'] // min(related_read_limit, 10))
170
+ exception = False
135
171
  try:
136
172
  values = RPCExecute('model', self.model_name, 'read',
137
173
  list(id2record.keys()), fnames, context=ctx,
138
174
  process_exception=process_exception)
139
175
  except RPCException:
140
- for record in id2record.values():
141
- record.exception = True
142
- if process_exception:
143
- values = [{'id': x} for x in id2record]
144
- default_values = {f: None for f in fnames if f != 'id'}
145
- for value in values:
146
- value.update(default_values)
147
- else:
148
- raise
176
+ values = [{'id': x} for x in id2record]
177
+ default_values = dict((f, None) for f in fnames)
178
+ for value in values:
179
+ value.update(default_values)
180
+ self.exception = exception = True
149
181
  id2value = dict((value['id'], value) for value in values)
150
182
  for id, record in id2record.items():
183
+ if not record.exception:
184
+ record.exception = exception
151
185
  value = id2value.get(id)
152
186
  if record and not record.destroyed and value:
153
187
  for key in record.modified_fields:
@@ -376,7 +410,6 @@ class Record:
376
410
  return self.id
377
411
 
378
412
  def default_get(self, defaults=None):
379
- vals = {}
380
413
  if len(self.group.fields):
381
414
  context = self.get_context()
382
415
  if defaults is not None:
@@ -386,7 +419,7 @@ class Record:
386
419
  vals = RPCExecute('model', self.model_name, 'default_get',
387
420
  list(self.group.fields.keys()), context=context)
388
421
  except RPCException:
389
- return vals
422
+ return
390
423
  if (self.parent
391
424
  and self.parent_name in self.group.fields):
392
425
  parent_field = self.group.fields[self.parent_name]
@@ -498,7 +531,9 @@ class Record:
498
531
  self._loaded.add(fieldname)
499
532
  fieldnames.append(fieldname)
500
533
  for fieldname, value in later.items():
501
- self.group.fields[fieldname].set(self, value)
534
+ if isinstance(
535
+ field := self.group.fields[fieldname], fields.O2MField):
536
+ field.set(self, value, preloaded=val.get(f"{fieldname}."))
502
537
  self._loaded.add(fieldname)
503
538
  fieldnames.append(fieldname)
504
539
  if validate:
@@ -682,7 +717,8 @@ class Record:
682
717
  try:
683
718
  res = RPCExecute(
684
719
  'model', self.model_name,
685
- 'autocomplete_' + fieldname, args, context=self.get_context())
720
+ 'autocomplete_' + fieldname, args, context=self.get_context(),
721
+ process_exception=False)
686
722
  except RPCException:
687
723
  # ensure res is a list
688
724
  res = []
@@ -691,9 +727,12 @@ class Record:
691
727
  def on_scan_code(self, code, depends):
692
728
  depends = self.expr_eval(depends)
693
729
  values = self._get_on_change_args(depends)
694
- changes = RPCExecute(
695
- 'model', self.model_name, 'on_scan_code', values, code,
696
- context=self.get_context(), process_exception=False)
730
+ try:
731
+ changes = RPCExecute(
732
+ 'model', self.model_name, 'on_scan_code', values, code,
733
+ context=self.get_context(), process_exception=False)
734
+ except RPCException:
735
+ changes = []
697
736
  self.set_on_change(changes)
698
737
  self.set_modified()
699
738
  return bool(changes)