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.
- tryton/__init__.py +1 -1
- tryton/cache.py +34 -0
- tryton/common/common.py +125 -69
- tryton/common/completion.py +2 -2
- tryton/common/domain_inversion.py +1 -2
- tryton/common/domain_parser.py +7 -17
- tryton/common/selection.py +6 -3
- tryton/common/tempfile.py +34 -0
- tryton/config.py +3 -2
- 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 +4 -3
- tryton/gui/window/log.py +24 -2
- tryton/gui/window/view_form/model/field.py +56 -62
- tryton/gui/window/view_form/model/group.py +3 -1
- tryton/gui/window/view_form/model/record.py +55 -16
- tryton/gui/window/view_form/screen/screen.py +22 -22
- tryton/gui/window/view_form/view/calendar_gtk/calendar_.py +7 -12
- tryton/gui/window/view_form/view/form.py +4 -14
- tryton/gui/window/view_form/view/form_gtk/binary.py +3 -3
- tryton/gui/window/view_form/view/form_gtk/dictionary.py +33 -27
- tryton/gui/window/view_form/view/form_gtk/document.py +10 -9
- 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/list.py +47 -56
- tryton/gui/window/view_form/view/list_gtk/widget.py +36 -23
- tryton/gui/window/view_form/view/screen_container.py +5 -3
- tryton/gui/window/win_export.py +1 -2
- tryton/gui/window/win_form.py +6 -8
- tryton/gui/window/wizard.py +11 -10
- tryton/jsonrpc.py +41 -27
- tryton/pyson.py +54 -4
- tryton/rpc.py +18 -0
- tryton/tests/test_common_domain_parser.py +0 -8
- {tryton-7.0.21.data → tryton-7.2.1.data}/scripts/tryton +1 -2
- {tryton-7.0.21.dist-info → tryton-7.2.1.dist-info}/METADATA +6 -20
- {tryton-7.0.21.dist-info → tryton-7.2.1.dist-info}/RECORD +94 -92
- {tryton-7.0.21.dist-info → tryton-7.2.1.dist-info}/WHEEL +1 -1
- {tryton-7.0.21.dist-info → tryton-7.2.1.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
143
|
+
#, fuzzy, python-format
|
|
144
|
+
msgid "Error: \"%s\". Try again later."
|
|
133
145
|
msgstr "请求太多,请稍后重试."
|
|
134
146
|
|
|
135
|
-
msgid "
|
|
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
|
-
|
|
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
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
|
|
474
|
+
except RPCException:
|
|
474
475
|
return False
|
|
475
476
|
for id_, name, icon in favorites:
|
|
476
477
|
menuitem = Gtk.MenuItem(label=name)
|
tryton/gui/window/about.py
CHANGED
tryton/gui/window/dblogin.py
CHANGED
|
@@ -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
|
|
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
|
tryton/gui/window/email_.py
CHANGED
|
@@ -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
|
|
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 (
|
|
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'
|
|
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:"))],
|
|
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,
|
|
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(
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
|
718
|
-
'fields_get', list(
|
|
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(
|
|
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,
|
|
1051
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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)
|