1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import copy
import inspect
from functools import wraps
from trytond.i18n import gettext
from trytond.tools import is_instance_method
from trytond.transaction import Transaction, without_check_access
from .field import Field, domain_method, order_method
def getter_context(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if not self.getter_with_context:
transaction = Transaction()
context = {
k: v for k, v in transaction.context.items()
if k in transaction.cache_keys}
with transaction.reset_context(), \
transaction.set_context(context):
return func(self, *args, **kwargs)
else:
return func(self, *args, **kwargs)
return wrapper
class Function(Field):
'''
Define function field (any).
'''
def __init__(self, field, getter, setter=None, searcher=None,
getter_with_context=True, loading='lazy'):
'''
:param field: The field of the function.
:param getter: The name of the function for getting values.
:param setter: The name of the function to set value.
:param searcher: The name of the function to search.
:param loading: Define how the field must be loaded:
``lazy`` or ``eager``.
'''
assert isinstance(field, Field)
self._field = field
self._type = field._type
self.getter = getter
self.getter_with_context = getter_with_context
self.setter = setter
if not self.setter:
self._field.readonly = True
self.searcher = searcher
assert loading in ('lazy', 'eager'), \
'loading must be "lazy" or "eager"'
self.loading = loading
__init__.__doc__ += Field.__init__.__doc__
def __copy__(self):
return Function(copy.copy(self._field), self.getter,
setter=self.setter, searcher=self.searcher,
getter_with_context=self.getter_with_context,
loading=self.loading)
def __deepcopy__(self, memo):
return Function(copy.deepcopy(self._field, memo), self.getter,
setter=self.setter, searcher=self.searcher,
getter_with_context=self.getter_with_context,
loading=self.loading)
def __getattr__(self, name):
return getattr(self._field, name)
def __getitem__(self, name):
return self._field[name]
def __setattr__(self, name, value):
if name in ('_field', '_type', 'getter', 'setter', 'searcher', 'name'):
object.__setattr__(self, name, value)
if name != 'name':
return
setattr(self._field, name, value)
def set_rpc(self, model):
self._field.set_rpc(model)
def sql_format(self, value):
return self._field.sql_format(value)
def sql_type(self):
return None
@domain_method
def convert_domain(self, domain, tables, Model):
if self.searcher:
return getattr(Model, self.searcher)(self.name, domain)
raise NotImplementedError(gettext(
'ir.msg_search_function_missing',
**Model.__names__(self.name)))
@order_method
def convert_order(self, name, tables, Model):
raise NotImplementedError
@getter_context
@without_check_access
def get(self, ids, Model, name, values=None):
'''
Call the getter.
If the function has ``names`` in the function definition then
it will call it with a list of name.
'''
method = getattr(Model, self.getter)
instance_method = is_instance_method(Model, self.getter)
multiple = self.getter_multiple(method)
records = Model.browse(ids)
for record, value in zip(records, values):
assert record.id == value['id']
for fname, val in value.items():
field = Model._fields.get(fname)
if field and field._type not in {
'many2one', 'reference',
'one2many', 'many2many', 'one2one'}:
record._local_cache[record.id][fname] = val
def call(name):
if not instance_method:
values = method(records, name)
if isinstance(name, str):
return convert_dict(values, name)
else:
return {n: convert_dict(values[n], n) for n in name}
else:
if isinstance(name, str):
return {
r.id: convert(method(r, name), name) for r in records}
else:
results = {n: {} for n in name}
for r in records:
values = method(r, name)
for n in name:
results[n][r.id] = values[n]
return results
def convert(value, name):
from ..model import Model as BaseModel
field = Model._fields[name]._field
if field._type in {'many2one', 'one2one', 'reference'}:
if isinstance(value, BaseModel):
if field._type == 'reference':
value = str(value)
else:
value = int(value)
elif field._type in {'one2many', 'many2many'}:
if value:
value = [int(r) for r in value]
return value
def convert_dict(values, name):
# Keep the same class
values = values.copy()
values.update((k, convert(v, name)) for k, v in values.items())
return values
if isinstance(name, list):
names = name
if multiple:
return call(names)
return dict((name, call(name)) for name in names)
else:
if multiple:
name = [name]
return call(name)
@without_check_access
def set(self, Model, name, ids, value, *args):
'''
Call the setter.
'''
if self.setter:
# TODO change setter API to use sequence of records, value
setter = getattr(Model, self.setter)
args = iter((ids, value) + args)
for ids, value in zip(args, args):
setter(Model.browse(ids), name, value)
else:
raise NotImplementedError(gettext(
'ir.msg_setter_function_missing',
**Model.__names__(self.name)))
def __get__(self, inst, cls):
try:
return super().__get__(inst, cls)
except AttributeError:
if not self.getter.startswith('on_change_with'):
raise
value = getattr(inst, self.getter)(self.name)
# Use temporary instance to not modify instance values
temp_inst = cls()
# Set the value to have proper type
self.__set__(temp_inst, value)
return super().__get__(temp_inst, cls)
def __set__(self, inst, value):
self._field.__set__(inst, value)
def definition(self, model, language):
definition = self._field.definition(model, language)
definition['searchable'] = self.searchable(model)
definition['sortable'] = self.sortable(model)
return definition
def searchable(self, model):
return super().searchable(model) and (
bool(self.searcher) or hasattr(model, f'domain_{self.name}'))
def sortable(self, model):
return super().sortable(model) and hasattr(model, f'order_{self.name}')
def getter_multiple(self, method):
"Returns True if getter function accepts multiple fields"
signature = inspect.signature(method)
return 'names' in signature.parameters
for name in [
'string', 'help', 'domain', 'states', 'depends', 'display_depends',
'edition_depends', 'validation_depends', 'context']:
def getter(name):
return lambda self: getattr(self._field, name)
def setter(name):
return lambda self, value: setattr(self._field, name, value)
setattr(Function, name, property(getter(name), setter(name)))
class MultiValue(Function):
def __init__(self, field, loading='lazy'):
super(MultiValue, self).__init__(
field, '_multivalue_getter', setter='_multivalue_setter',
loading=loading)
def __copy__(self):
return MultiValue(copy.copy(self._field), loading=self.loading)
def __deepcopy__(self, memo):
return MultiValue(
copy.deepcopy(self._field, memo), loading=self.loading)
|