Initial import from Docker volume

This commit is contained in:
root
2025-12-26 13:11:43 +00:00
commit 4998dc066a
13336 changed files with 1767801 additions and 0 deletions

26
model/__init__.py Executable file
View File

@@ -0,0 +1,26 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from .active import DeactivableMixin
from .avatar import avatar_mixin
from .descriptors import dualmethod
from .dictschema import DictSchemaMixin
from .digits import DigitsMixin
from .match import MatchMixin
from .model import Model
from .modelsingleton import ModelSingleton
from .modelsql import Check, Exclude, Index, ModelSQL, Unique, convert_from
from .modelstorage import EvalEnvironment, ModelStorage
from .modelview import ModelView
from .multivalue import MultiValueMixin, ValueMixin
from .order import sequence_ordered, sort
from .symbol import SymbolMixin
from .tree import sum_tree, tree
from .union import UnionMixin
from .workflow import Workflow
__all__ = ['Model', 'ModelView', 'ModelStorage', 'ModelSingleton', 'ModelSQL',
'Check', 'Unique', 'Exclude', 'Index', 'convert_from',
'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin', 'dualmethod',
'MultiValueMixin', 'ValueMixin', 'SymbolMixin', 'DigitsMixin',
'EvalEnvironment', 'sequence_ordered', 'sort', 'DeactivableMixin', 'tree',
'sum_tree', 'avatar_mixin']

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

43
model/active.py Executable file
View File

@@ -0,0 +1,43 @@
# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
# repository contains the full copyright notices and license terms.
from trytond.i18n import lazy_gettext
from trytond.pyson import Eval
from . import fields
from .model import Model
from .modelview import ModelView
class DeactivableMixin(Model):
"Mixin to allow to soft deletion of records"
__slots__ = ()
active = fields.Boolean(
lazy_gettext('ir.msg_active'),
help=lazy_gettext('ir.msg_active_help'))
@classmethod
def default_active(cls):
return True
@classmethod
def __post_setup__(cls):
super().__post_setup__()
inactive = ~Eval('active', cls.default_active())
for name, field in cls._fields.items():
if name == 'active':
continue
if 'readonly' in field.states:
field.states['readonly'] |= inactive
else:
field.states['readonly'] = inactive
if issubclass(cls, ModelView):
for states in cls._buttons.values():
if 'readonly' in states:
states['readonly'] |= inactive
else:
states['readonly'] = inactive
if 'active' not in states.setdefault('depends', []):
states['depends'].append('active')

93
model/avatar.py Executable file
View File

@@ -0,0 +1,93 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.i18n import lazy_gettext
from trytond.model import fields
from trytond.pool import Pool
def avatar_mixin(size=64, default=None):
class AvatarMixin:
__slots__ = ()
avatars = fields.One2Many(
'ir.avatar', 'resource', lazy_gettext('ir.msg_avatars'), size=1)
avatar = fields.Function(
fields.Binary(lazy_gettext('ir.msg_avatar')),
'_get_avatar', setter='_set_avatar')
avatar_url = fields.Function(
fields.Char(lazy_gettext('ir.msg_avatar_url')), '_get_avatar_url')
@property
def has_avatar(self):
if self.avatars:
avatar, = self.avatars
return bool(avatar.image_id or avatar.image)
return False
def _get_avatar(self, name):
if self.avatars:
avatar, = self.avatars
return avatar.get(size=size)
return None
@classmethod
def _set_avatar(cls, records, name, value):
pool = Pool()
Avatar = pool.get('ir.avatar')
avatars = []
image = Avatar.convert(value)
for record in records:
if record.avatars:
avatar, = record.avatars
else:
avatar = Avatar(resource=record)
avatars.append(avatar)
Avatar.save(avatars)
# Use write the image to store only once in filestore
Avatar.write(avatars, {
'image': image,
})
def _get_avatar_url(self, name):
if self.avatars:
avatar, = self.avatars
return avatar.url
@classmethod
def generate_avatar(cls, records, field='rec_name'):
from trytond.ir.avatar import PIL, generate
if not PIL:
return
records = [r for r in records if not r.has_avatar]
if not records:
return
for record in records:
avatar = generate(size, getattr(record, field))
if avatar:
record.avatar = avatar
cls.save(records)
@classmethod
def copy(cls, avatars, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('avatars', [])
return super().copy(avatars, default=default)
if default:
@classmethod
def create(cls, vlist):
records = super().create(vlist)
cls.generate_avatar(records, field=default)
return records
@classmethod
def write(cls, *args):
records = sum(args[0:None:2], [])
super().write(*args)
cls.generate_avatar(records, field=default)
return AvatarMixin

34
model/descriptors.py Executable file
View File

@@ -0,0 +1,34 @@
# 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 functools
class dualmethod(object):
"""Descriptor implementing combination of class and instance method
When called on an instance, the class is passed as the first argument and a
list with the instance as the second.
When called on a class, the class itsefl is passed as the first argument.
>>> class Example(object):
... @dualmethod
... def method(cls, instances):
... print(len(instances))
...
>>> Example.method([Example()])
1
>>> Example().method()
1
"""
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
@functools.wraps(self.func)
def newfunc(*args, **kwargs):
if instance:
return self.func(owner, [instance], *args, **kwargs)
else:
return self.func(owner, *args, **kwargs)
return newfunc

244
model/dictschema.py Executable file
View File

@@ -0,0 +1,244 @@
# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
# repository contains the full copyright notices and license terms.
import json
from collections import OrderedDict
from trytond.cache import Cache
from trytond.config import config
from trytond.i18n import gettext, lazy_gettext
from trytond.model import fields
from trytond.model.exceptions import ValidationError
from trytond.pool import Pool
from trytond.pyson import Eval, PYSONDecoder
from trytond.rpc import RPC
from trytond.tools import slugify
from trytond.transaction import Transaction
class DomainError(ValidationError):
pass
class SelectionError(ValidationError):
pass
class DictSchemaMixin(object):
__slots__ = ()
_rec_name = 'string'
name = fields.Char(lazy_gettext('ir.msg_dict_schema_name'), required=True)
string = fields.Char(
lazy_gettext('ir.msg_dict_schema_string'),
translate=True, required=True)
help = fields.Text(
lazy_gettext('ir.msg_dict_schema_help'),
translate=True)
type_ = fields.Selection([
('boolean', lazy_gettext('ir.msg_dict_schema_boolean')),
('integer', lazy_gettext('ir.msg_dict_schema_integer')),
('char', lazy_gettext('ir.msg_dict_schema_char')),
('float', lazy_gettext('ir.msg_dict_schema_float')),
('numeric', lazy_gettext('ir.msg_dict_schema_numeric')),
('date', lazy_gettext('ir.msg_dict_schema_date')),
('datetime', lazy_gettext('ir.msg_dict_schema_datetime')),
('selection', lazy_gettext('ir.msg_dict_schema_selection')),
('multiselection',
lazy_gettext('ir.msg_dict_schema_multiselection')),
], lazy_gettext('ir.msg_dict_schema_type'), required=True)
digits = fields.Integer(
lazy_gettext('ir.msg_dict_schema_digits'),
states={
'invisible': ~Eval('type_').in_(['float', 'numeric']),
}, depends=['type_'])
domain = fields.Char(lazy_gettext('ir.msg_dict_schema_domain'))
selection = fields.Text(
lazy_gettext('ir.msg_dict_schema_selection'),
states={
'invisible': ~Eval('type_').in_(['selection', 'multiselection']),
}, translate=True, depends=['type_'],
help=lazy_gettext('ir.msg_dict_schema_selection_help'))
selection_sorted = fields.Boolean(
lazy_gettext('ir.msg_dict_schema_selection_sorted'),
states={
'invisible': ~Eval('type_').in_(['selection', 'multiselection']),
}, depends=['type_'],
help=lazy_gettext('ir.msg_dict_schema_selection_sorted_help'))
help_selection = fields.Text(
lazy_gettext('ir.msg_dict_schema_help_selection'), translate=True,
states={
'invisible': ~Eval('type_').in_(['selection', 'multiselection']),
},
depends=['type_'],
help=lazy_gettext('ir.msg_dict_schema_help_selection_help'))
selection_json = fields.Function(fields.Char(
lazy_gettext('ir.msg_dict_schema_selection_json'),
states={
'invisible': ~Eval('type_').in_(
['selection', 'multiselection']),
},
depends=['type_']), 'get_selection_json')
help_selection_json = fields.Function(fields.Char(
lazy_gettext('ir.msg_dict_schema_help_selection_json'),
states={
'invisible': ~Eval('type_').in_(
['selection', 'multiselection']),
},
depends=['type_']), 'get_selection_json')
_relation_fields_cache = Cache('_dict_schema_mixin.get_relation_fields')
@classmethod
def __setup__(cls):
super(DictSchemaMixin, cls).__setup__()
cls.__rpc__.update({
'get_keys': RPC(instantiate=0),
'search_get_keys': RPC(),
})
@staticmethod
def default_digits():
return 2
@staticmethod
def default_selection_sorted():
return True
@fields.depends('name', 'string')
def on_change_string(self):
if not self.name and self.string:
self.name = slugify(self.string.lower(), hyphenate='_')
@classmethod
def validate_fields(cls, schemas, field_names):
super().validate_fields(schemas, field_names)
cls.check_domain(schemas, field_names)
cls.check_selection(schemas, field_names)
@classmethod
def check_domain(cls, schemas, field_names=None):
if field_names and 'domain' not in field_names:
return
for schema in schemas:
if not schema.domain:
continue
try:
value = PYSONDecoder().decode(schema.domain)
except Exception:
raise DomainError(
gettext('ir.msg_dict_schema_invalid_domain',
schema=schema.rec_name))
if not isinstance(value, list):
raise DomainError(
gettext('ir.msg_dict_schema_invalid_domain',
schema=schema.rec_name))
@classmethod
def check_selection(cls, schemas, field_names=None):
if field_names and not (field_names & {
'type_', 'selection', 'help_selection'}):
return
for schema in schemas:
if schema.type_ not in {'selection', 'multiselection'}:
continue
for name in ['selection', 'help_selection']:
try:
dict(json.loads(schema.get_selection_json(name + '_json')))
except Exception:
raise SelectionError(
gettext('ir.msg_dict_schema_invalid_%s' % name,
schema=schema.rec_name))
def get_selection_json(self, name):
field = name[:-len('_json')]
db_selection = getattr(self, field) or ''
selection = [[w.strip() for w in v.split(':', 1)]
for v in db_selection.splitlines() if v]
return json.dumps(selection, separators=(',', ':'))
@classmethod
def get_keys(cls, records):
pool = Pool()
Config = pool.get('ir.configuration')
keys = []
for record in records:
new_key = {
'id': record.id,
'name': record.name,
'string': record.string,
'help': record.help,
'type': record.type_,
'domain': record.domain,
'sequence': getattr(record, 'sequence', record.name),
}
if record.type_ in {'selection', 'multiselection'}:
with Transaction().set_context(language=Config.get_language()):
english_key = cls(record.id)
selection = OrderedDict(json.loads(
english_key.selection_json))
selection.update(dict(json.loads(record.selection_json)))
new_key['selection'] = list(selection.items())
new_key['help_selection'] = dict(
json.loads(record.help_selection_json))
new_key['sort'] = record.selection_sorted
elif record.type_ in ('float', 'numeric'):
new_key['digits'] = (16, record.digits)
keys.append(new_key)
return keys
@classmethod
def search_get_keys(cls, domain, limit=None):
schemas = cls.search(domain, limit=limit)
return cls.get_keys(schemas)
@classmethod
def get_relation_fields(cls):
if not config.get('dict', cls.__name__, default=True):
return {}
fields = cls._relation_fields_cache.get(cls.__name__)
if fields is not None:
return fields
keys = cls.get_keys(cls.search([]))
fields = {k['name']: k for k in keys}
cls._relation_fields_cache.set(cls.__name__, fields)
return fields
@classmethod
def create(cls, vlist):
records = super().create(vlist)
cls._relation_fields_cache.clear()
return records
@classmethod
def write(cls, *args):
super().write(*args)
cls._relation_fields_cache.clear()
@classmethod
def delete(cls, records):
super().delete(records)
cls._relation_fields_cache.clear()
def format(self, value, lang=None):
pool = Pool()
Lang = pool.get('ir.lang')
if lang is None:
lang = Lang.get()
if value is None:
return ''
if self.type_ == 'boolean':
if value:
return gettext('ir.msg_dict_yes')
else:
return gettext('ir.msg_dict_no')
elif self.type_ == 'integer':
return lang.format('%i', value)
elif self.type_ in {'float', 'numeric'}:
return lang.format('%.*f', (self.digits, value))
elif self.type_ in {'date', 'datetime'}:
return lang.strftime(value)
elif self.type_ in {'selection', 'multiselection'}:
values = dict(json.loads(self.selection_json))
if self.type_ == 'selection':
return values.get(value, '')
else:
return "; ".join(values.get(v, '') for v in value)
return value

34
model/digits.py Executable file
View File

@@ -0,0 +1,34 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.cache import Cache
from trytond.rpc import RPC
class DigitsMixin:
__slots__ = ()
_digits_cache = Cache('_digits_mixin..get_digits', context=False)
@classmethod
def __setup__(cls):
super().__setup__()
cls.__rpc__.update({
'get_digits': RPC(instantiate=0, cache=dict(days=1)),
})
def get_digits(self):
key = str(self)
digits = self._digits_cache.get(key)
if digits is not None:
return digits
digits = self._get_digits()
self._digits_cache.set(key, digits)
return digits
def _get_digits(self):
return (16, self.digits)
@classmethod
def write(cls, *args):
super().write(*args)
cls._digits_cache.clear()

29
model/exceptions.py Executable file
View File

@@ -0,0 +1,29 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from .modelsql import ForeignKeyError, SQLConstraintError
from .modelstorage import (
AccessError, DigitsValidationError, DomainValidationError,
ForbiddenCharValidationError, ImportDataError, RequiredValidationError,
SelectionValidationError, SizeValidationError, TimeFormatValidationError,
ValidationError)
from .modelview import AccessButtonError, ButtonActionException
from .tree import RecursionError
__all__ = [
AccessButtonError,
AccessError,
ButtonActionException,
DigitsValidationError,
DomainValidationError,
ForeignKeyError,
ImportDataError,
RecursionError,
RequiredValidationError,
SQLConstraintError,
ForbiddenCharValidationError,
SelectionValidationError,
SizeValidationError,
TimeFormatValidationError,
ValidationError,
]

32
model/fields/__init__.py Executable file
View File

@@ -0,0 +1,32 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from .binary import Binary
from .boolean import Boolean
from .char import Char
from .date import Date, DateTime, Time, TimeDelta, Timestamp
from .dict import Dict
from .field import (
SQL_OPERATORS, Field, context_validate, depends, domain_validate,
get_eval_fields, on_change_result, states_validate)
from .float import Float
from .fmany2one import fmany2one
from .function import Function, MultiValue
from .integer import Integer
from .many2many import Many2Many
from .many2one import Many2One
from .multiselection import MultiSelection
from .numeric import Numeric
from .one2many import One2Many
from .one2one import One2One
from .reference import Reference
from .selection import Selection
from .text import FullText, Text
__all__ = [
depends, SQL_OPERATORS, on_change_result,
get_eval_fields, states_validate, domain_validate, context_validate, Field,
Boolean, Integer, Char, Text, FullText, Float, Numeric, Date,
Timestamp, DateTime, Time, TimeDelta, Binary, Selection, Reference,
Many2One, One2Many, Many2Many, Function, MultiValue, One2One, Dict,
MultiSelection, fmany2one]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

133
model/fields/binary.py Executable file
View File

@@ -0,0 +1,133 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from sql import Column, Null
from trytond.filestore import filestore
from trytond.tools import cached_property, grouped_slice, reduce_ids
from trytond.transaction import Transaction
from .field import Field
def caster(d):
if isinstance(d, bytes):
return d
elif isinstance(d, memoryview):
return bytes(d)
return bytes(d, encoding='utf8')
class Binary(Field):
'''
Define a binary field (``bytes``).
'''
_type = 'binary'
_sql_type = 'BLOB'
cast = staticmethod(caster)
def __init__(self, string='', help='', required=False, readonly=False,
domain=None, states=None, on_change=None,
on_change_with=None, depends=None, context=None, loading='lazy',
filename=None, file_id=None, store_prefix=None):
self.filename = filename
self.file_id = file_id
self.store_prefix = store_prefix
super(Binary, self).__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
@cached_property
def display_depends(self):
depends = super().display_depends
if self.filename:
depends.add(self.filename)
return depends
def get(self, ids, model, name, values=None):
'''
Convert the binary value into ``bytes``
:param ids: a list of ids
:param model: a string with the name of the model
:param name: a string with the name of the field
:param values: a dictionary with the read values
:return: a dictionary with ids as key and values as value
'''
if values is None:
values = {}
transaction = Transaction()
res = {}
converter = self.cast
default = None
format_ = Transaction().context.get(
'%s.%s' % (model.__name__, name), '')
if format_ == 'size':
converter = len
default = 0
if self.file_id:
table = model.__table__()
cursor = transaction.connection.cursor()
prefix = self.store_prefix
if prefix is None:
prefix = transaction.database.name
if format_ == 'size':
store_func = filestore.size
else:
def store_func(id, prefix):
return self.cast(filestore.get(id, prefix=prefix))
for sub_ids in grouped_slice(ids):
cursor.execute(*table.select(
table.id, Column(table, self.file_id),
where=reduce_ids(table.id, sub_ids)
& (Column(table, self.file_id) != Null)
& (Column(table, self.file_id) != '')))
for record_id, file_id in cursor:
try:
res[record_id] = store_func(file_id, prefix)
except (IOError, OSError):
pass
for i in values:
if i['id'] in res:
continue
value = i[name]
if value:
value = converter(value)
else:
value = default
res[i['id']] = value
for i in ids:
res.setdefault(i, default)
return res
def set(self, Model, name, ids, value, *args):
transaction = Transaction()
table = Model.__table__()
cursor = transaction.connection.cursor()
prefix = self.store_prefix
if prefix is None:
prefix = transaction.database.name
args = iter((ids, value) + args)
for ids, value in zip(args, args):
if self.file_id:
columns = [Column(table, self.file_id), Column(table, name)]
values = [
filestore.set(value, prefix) if value else None, None]
else:
columns = [Column(table, name)]
values = [self.sql_format(value)]
cursor.execute(*table.update(columns, values,
where=reduce_ids(table.id, ids)))
def definition(self, model, language):
definition = super().definition(model, language)
definition['searchable'] = False
definition['filename'] = self.filename
return definition

38
model/fields/boolean.py Executable file
View File

@@ -0,0 +1,38 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from .field import Field
class Boolean(Field):
'''
Define a boolean field (``True`` or ``False``).
'''
_type = 'boolean'
_sql_type = 'BOOL'
_py_type = bool
def __init__(self, string='', help='', readonly=False, domain=None,
states=None, on_change=None, on_change_with=None,
depends=None, context=None, loading='eager'):
super(Boolean, self).__init__(string=string, help=help, required=False,
readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
__init__.__doc__ = Field.__init__.__doc__
def _domain_add_null(self, column, operator, value, expression):
expression = super(Boolean, self)._domain_add_null(
column, operator, value, expression)
if operator in ('=', '!='):
conv = {
False: None,
None: False,
}
if value is False or value is None:
if operator == '=':
expression |= (column == conv[value])
else:
expression &= (column != conv[value])
return expression

235
model/fields/char.py Executable file
View File

@@ -0,0 +1,235 @@
# 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 string
import warnings
from sql import Expression, Query
from sql.conditionals import Coalesce, NullIf
from sql.functions import Trim
from sql.operators import Not
from trytond.rpc import RPC
from trytond.tools import is_full_text, unescape_wildcard
from trytond.transaction import Transaction
from .field import Field, FieldTranslate, order_method, size_validate
class Char(FieldTranslate):
'''
Define a char field (``unicode``).
'''
_type = 'char'
_py_type = str
forbidden_chars = '\t\n\r\x0b\x0c'
search_unaccented = True
search_full_text = False
def __init__(self, string='', size=None, help='', required=False,
readonly=False, domain=None, states=None, translate=False,
on_change=None, on_change_with=None, depends=None, context=None,
loading=None, autocomplete=None, strip=True):
'''
:param translate: A boolean. If ``True`` the field is translatable.
:param size: A integer. If set defines the maximum size of the values.
'''
if loading is None:
loading = 'lazy' if translate else 'eager'
super(Char, self).__init__(string=string, help=help, required=required,
readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
self.autocomplete = set()
if autocomplete:
warnings.warn('autocomplete argument is deprecated, use the '
'depends decorator', DeprecationWarning, stacklevel=2)
self.autocomplete.update(autocomplete)
self.strip = strip
self.translate = translate
self.__size = None
self.size = size
__init__.__doc__ += Field.__init__.__doc__
def _get_size(self):
return self.__size
def _set_size(self, value):
size_validate(value)
self.__size = value
size = property(_get_size, _set_size)
@property
def strip(self):
return self.__strip
@strip.setter
def strip(self, value):
assert value in {False, True, 'leading', 'trailing'}
self.__strip = value
@property
def _sql_type(self):
if isinstance(self.size, int):
return 'VARCHAR(%s)' % self.size
else:
return 'VARCHAR'
def __set__(self, inst, value):
if isinstance(value, str) and self.strip:
if self.strip == 'leading':
value = value.lstrip()
elif self.strip == 'trailing':
value = value.rstrip()
else:
value = value.strip()
super().__set__(inst, value)
def sql_format(self, value):
if value is not None and self.strip:
if isinstance(value, (Query, Expression)):
if self.strip == 'leading':
position = 'LEADING'
elif self.strip == 'trailing':
position = 'TRAILING'
else:
position = 'BOTH'
value = Trim(
value, position=position, characters=string.whitespace)
else:
if self.strip == 'leading':
value = value.lstrip()
elif self.strip == 'trailing':
value = value.rstrip()
else:
value = value.strip()
return super().sql_format(value)
def set_rpc(self, model):
super(Char, self).set_rpc(model)
if self.autocomplete:
func_name = 'autocomplete_%s' % self.name
assert hasattr(model, func_name), \
'Missing %s on model %s' % (func_name, model.__name__)
model.__rpc__.setdefault(func_name, RPC(instantiate=0))
def _domain_column(self, operator, column):
column = super(Char, self)._domain_column(operator, column)
if self.search_unaccented and operator.endswith('ilike'):
database = Transaction().database
column = database.unaccent(column)
return column
def _domain_value(self, operator, value):
value = super(Char, self)._domain_value(operator, value)
if self.search_unaccented and operator.endswith('ilike'):
database = Transaction().database
value = database.unaccent(value)
return value
def convert_domain(self, domain, tables, Model):
transaction = Transaction()
context = transaction.context
database = transaction.database
expression = super().convert_domain(domain, tables, Model)
name, operator, value = domain
if operator.endswith('ilike'):
table, _ = tables[None]
if self.translate:
language = transaction.language
model, join, column = self._get_translation_column(
Model, name)
column = Coalesce(NullIf(column, ''), self.sql_column(model))
else:
language = None
column = self.sql_column(table)
column = self._domain_column(operator, column)
threshold = context.get(
'%s.%s.search_similarity' % (Model.__name__, name),
context.get('search_similarity'))
if database.has_similarity() and is_full_text(value) and threshold:
sim_value = unescape_wildcard(value)
sim_value = self._domain_value(operator, sim_value)
expression = (
database.similarity(column, sim_value) >= threshold)
if operator.startswith('not'):
expression = Not(expression)
if self.translate:
expression = table.id.in_(
join.select(model.id, where=expression))
key = '%s.%s.search_full_text' % (Model.__name__, name)
if ((self.search_full_text or context.get(key))
and context.get(key, True)
and database.has_search_full_text()):
if context.get(key) or is_full_text(value):
fts_column = database.format_full_text(
column, language=language)
fts_value = value
if key not in context:
fts_value = unescape_wildcard(fts_value)
fts_value = self._domain_value(operator, fts_value)
fts_value = database.format_full_text_query(
fts_value, language=language)
fts = database.search_full_text(fts_column, fts_value)
if operator.startswith('not'):
fts = Not(fts)
if self.translate:
fts = table.id.in_(
join.select(model.id, where=fts))
if database.has_similarity() and is_full_text(value):
if operator.startswith('not'):
expression |= fts
else:
expression &= fts
else:
expression = fts
return expression
@order_method
def convert_order(self, name, tables, Model):
transaction = Transaction()
context = transaction.context
database = transaction.database
key = '%s.%s.order' % (Model.__name__, name)
value = context.get(key)
order = super().convert_order(name, tables, Model)
if value:
expression = None
table, _ = tables[None]
if self.translate:
language = transaction.language
column = self._get_translation_order(tables, Model, name)
else:
language = None
column = self.sql_column(table)
column = self._domain_column('ilike', column)
if database.has_similarity():
sim_value = unescape_wildcard(value)
sim_value = self._domain_value('ilike', sim_value)
expression = database.similarity(column, sim_value)
key = '%s.%s.search_full_text' % (Model.__name__, name)
if ((self.search_full_text or context.get(key))
and database.has_search_full_text()):
column = database.format_full_text(column, language=language)
value = self._domain_value('ilike', value)
value = database.format_full_text_query(
value, language=language)
rank = database.rank_full_text(
column, value, normalize=['rank'])
if expression:
expression += rank
else:
expression = rank
if expression:
order = [expression]
return order
def definition(self, model, language):
definition = super().definition(model, language)
definition['autocomplete'] = list(self.autocomplete)
definition['strip'] = self.strip
if self.size is not None:
definition['size'] = self.size
return definition

215
model/fields/date.py Executable file
View File

@@ -0,0 +1,215 @@
# 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 datetime
from sql.functions import AtTimeZone, Function
from trytond import backend
from trytond.pyson import PYSON, PYSONEncoder
from trytond.tools import cached_property
from .field import Field, get_eval_fields
class SQLite_Date(Function):
__slots__ = ()
_function = 'DATE'
class SQLite_DateTime(Function):
__slots__ = ()
_function = 'DATETIME'
class SQLite_Time(Function):
__slots__ = ()
_function = 'TIME'
class Date(Field):
'''
Define a date field (``date``).
'''
_type = 'date'
_sql_type = 'DATE'
_py_type = datetime.date
def sql_format(self, value):
if isinstance(value, str):
value = datetime.date.fromisoformat(value)
elif isinstance(value, datetime.datetime):
raise ValueError("Date field can not have time")
return super().sql_format(value)
def sql_cast(self, expression, timezone=None):
if backend.name == 'sqlite':
return SQLite_Date(expression)
if timezone:
expression = AtTimeZone(expression, 'utc')
expression = AtTimeZone(expression, timezone)
return super(Date, self).sql_cast(expression)
class FormatMixin(Field):
def definition(self, model, language):
encoder = PYSONEncoder()
definition = super().definition(model, language)
definition['format'] = encoder.encode(self.format)
return definition
@cached_property
def display_depends(self):
depends = super().display_depends
if isinstance(self.format, PYSON):
depends |= get_eval_fields(self.format)
return depends
@cached_property
def validation_depends(self):
depends = super().display_depends
if isinstance(self.format, PYSON):
depends |= get_eval_fields(self.format)
return depends
class Timestamp(FormatMixin, Field):
'''
Define a timestamp field (``datetime``).
'''
_type = 'timestamp'
_sql_type = 'TIMESTAMP'
_py_type = datetime.datetime
format = '%H:%M:%S.%f'
def sql_format(self, value):
if isinstance(value, str):
value = datetime.datetime.fromisoformat(value)
return super().sql_format(value)
def sql_cast(self, expression):
if backend.name == 'sqlite':
return SQLite_DateTime(expression)
return super().sql_cast(expression)
class DateTime(Timestamp):
'''
Define a datetime field (``datetime``).
'''
_type = 'datetime'
_sql_type = 'DATETIME'
def __init__(self, string='', format='%H:%M:%S', help='', required=False,
readonly=False, domain=None, states=None,
on_change=None, on_change_with=None, depends=None,
context=None, loading='eager'):
'''
:param format: The validation format as used by strftime.
'''
super(DateTime, self).__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
self.format = format
__init__.__doc__ += Field.__init__.__doc__
def sql_format(self, value):
value = super().sql_format(value)
if isinstance(value, datetime.datetime):
value = value.replace(microsecond=0)
return value
class Time(FormatMixin, Field):
'''
Define a time field (``time``).
'''
_type = 'time'
_sql_type = 'TIME'
_py_type = datetime.time
def __init__(self, string='', format='%H:%M:%S', help='', required=False,
readonly=False, domain=None, states=None,
on_change=None, on_change_with=None, depends=None,
context=None, loading='eager'):
'''
:param format: The validation format as used by strftime.
'''
super().__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
self.format = format
def sql_format(self, value):
if isinstance(value, str):
value = datetime.time.fromisoformat(value)
value = super().sql_format(value)
if isinstance(value, datetime.time):
value = value.replace(microsecond=0)
return value
def sql_cast(self, expression):
if backend.name == 'sqlite':
return SQLite_Time(expression)
return super(Time, self).sql_cast(expression)
class TimeDelta(Field):
'''
Define a timedelta field (``timedelta``).
'''
_type = 'timedelta'
_sql_type = 'INTERVAL'
_py_type = datetime.timedelta
def __init__(self, string='', converter=None, help='', required=False,
readonly=False, domain=None, states=None,
on_change=None, on_change_with=None, depends=None,
context=None, loading='eager'):
'''
:param converter: The name of the context key containing
the time converter.
'''
super(TimeDelta, self).__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
self.converter = converter
def sql_format(self, value):
if isinstance(value, (int, float)):
value = datetime.timedelta(seconds=value)
elif isinstance(value, str):
if not value.find(':'):
raise ValueError(
"TimeDelta requires a string '%H:%M:%S.%f' or '%H:%M'")
hours, minutes, seconds = (value.split(":") + ['00'])[:3]
value = datetime.timedelta(
hours=int(hours), minutes=int(minutes), seconds=float(seconds))
return super().sql_format(value)
@classmethod
def get(cls, ids, model, name, values=None):
result = {}
for row in values:
value = row[name]
if (value is not None
and not isinstance(value, datetime.timedelta)):
if value >= datetime.timedelta.max.total_seconds():
value = datetime.timedelta.max
elif value <= datetime.timedelta.min.total_seconds():
value = datetime.timedelta.min
else:
value = datetime.timedelta(seconds=value)
result[row['id']] = value
else:
result[row['id']] = value
return result
def definition(self, model, language):
definition = super().definition(model, language)
definition['converter'] = self.converter
return definition

221
model/fields/dict.py Executable file
View File

@@ -0,0 +1,221 @@
# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
# repository contains the full copyright notices and license terms.
import json
from functools import partial
from sql import Cast, CombiningQuery, Literal, Null, Select, operators
from trytond import backend
from trytond.pool import Pool
from trytond.protocols.jsonrpc import JSONDecoder, JSONEncoder
from trytond.tools import grouped_slice
from trytond.tools.immutabledict import ImmutableDict
from trytond.transaction import Transaction
from .field import SQL_OPERATORS, Field, domain_method, order_method
# Use canonical form
dumps = partial(
json.dumps, cls=JSONEncoder, separators=(',', ':'), sort_keys=True,
ensure_ascii=False)
class Dict(Field):
'Define dict field.'
_type = 'dict'
_sql_type = 'JSON'
_py_type = dict
def __init__(self, schema_model, string='', help='', required=False,
readonly=False, domain=None, states=None,
on_change=None, on_change_with=None, depends=None,
context=None, loading='lazy'):
super(Dict, self).__init__(string, help, required, readonly, domain,
states, on_change, on_change_with, depends, context, loading)
self.schema_model = schema_model
self.search_unaccented = True
def get(self, ids, model, name, values=None):
dicts = dict((id, None) for id in ids)
for value in values or []:
data = value[name]
if data:
# If stored as JSON conversion is done on backend
if isinstance(data, str):
data = json.loads(data, object_hook=JSONDecoder())
for key, val in data.items():
if isinstance(val, list):
data[key] = tuple(val)
dicts[value['id']] = ImmutableDict(data)
return dicts
def sql_format(self, value):
value = super().sql_format(value)
if isinstance(value, dict):
d = {}
for k, v in value.items():
if v is None:
continue
if self.schema_model and isinstance(v, (list, tuple)):
if not v:
continue
v = list(sorted(set(v)))
d[k] = v
value = dumps(d)
return value
def __set__(self, inst, value):
if value:
value = ImmutableDict(value)
super().__set__(inst, value)
def translated(self, name=None, type_='values'):
"Return a descriptor for the translated value of the field"
if name is None:
name = self.name
if name is None:
raise ValueError('Missing name argument')
return TranslatedDict(name, type_)
def _domain_column(self, operator, column, key=None):
database = Transaction().database
column = database.json_get(
super()._domain_column(operator, column), key)
if operator.endswith('like'):
column = Cast(column, database.sql_type('VARCHAR').base)
if self.search_unaccented and operator.endswith('ilike'):
column = database.unaccent(column)
return column
def _domain_value(self, operator, value):
if backend.name == 'sqlite' and isinstance(value, bool):
# json_extract returns 0 for JSON false and 1 for JSON true
value = int(value)
if isinstance(value, (Select, CombiningQuery)):
return value
if self.schema_model and isinstance(value, (list, tuple)):
value = sorted(set(value))
if operator.endswith('in'):
return [dumps(v) for v in value]
else:
value = dumps(value)
if self.search_unaccented and operator.endswith('ilike'):
database = Transaction().database
value = database.unaccent(value)
return value
def _domain_add_null(self, column, operator, value, expression):
expression = super()._domain_add_null(
column, operator, value, expression)
if value is None and operator.endswith('='):
if operator == '=':
expression |= (column == Null)
else:
expression &= (column != Null)
return expression
@domain_method
def convert_domain(self, domain, tables, Model):
name, operator, value = domain[:3]
if '.' not in name:
return super().convert_domain(domain, tables, Model)
database = Transaction().database
table, _ = tables[None]
name, key = name.split('.', 1)
Operator = SQL_OPERATORS[operator]
raw_column = self.sql_column(table)
column = self._domain_column(operator, raw_column, key)
expression = Operator(column, self._domain_value(operator, value))
if operator in {'=', '!='}:
# Try to use custom operators in case there is indexes
try:
if value is None:
expression = database.json_key_exists(
raw_column, key)
if operator == '=':
expression = operators.Not(expression)
# we compare on multi-selection by doing an equality check and
# not a contain check
elif not isinstance(value, (list, tuple)):
expression = database.json_contains(
raw_column, dumps({key: value}))
if operator == '!=':
expression = operators.Not(expression)
expression &= database.json_key_exists(
raw_column, key)
return expression
except NotImplementedError:
pass
elif operator.endswith('in'):
# Try to use custom operators in case there is indexes
if not value:
expression = Literal(operator.startswith('not'))
else:
op = '!=' if operator.startswith('not') else '='
try:
in_expr = Literal(False)
for v in value:
in_expr |= database.json_contains(
self._domain_column(op, raw_column, key),
dumps(v))
if operator.startswith('not'):
in_expr = ~in_expr
expression = in_expr
except NotImplementedError:
pass
expression = self._domain_add_null(column, operator, value, expression)
return expression
@order_method
def convert_order(self, name, tables, Model):
fname, _, key = name.partition('.')
if not key:
return super().convert_order(fname, tables, Model)
database = Transaction().database
table, _ = tables[None]
column = self.sql_column(table)
return [database.json_get(column, key)]
def definition(self, model, language):
definition = super().definition(model, language)
definition['schema_model'] = self.schema_model
return definition
class TranslatedDict(object):
'A descriptor for translated values of Dict field'
def __init__(self, name, type_):
assert type_ in ['keys', 'values']
self.name = name
self.type_ = type_
def __get__(self, inst, cls):
if inst is None:
return self
pool = Pool()
schema_model = getattr(cls, self.name).schema_model
SchemaModel = pool.get(schema_model)
value = getattr(inst, self.name)
if not value:
return value
domain = []
if self.type_ == 'values':
domain = [('type_', '=', 'selection')]
records = []
for key_names in grouped_slice(value.keys()):
records += SchemaModel.search([
('name', 'in', key_names),
] + domain)
keys = SchemaModel.get_keys(records)
if self.type_ == 'keys':
return {k['name']: k['string'] for k in keys}
elif self.type_ == 'values':
trans = {k['name']: dict(k['selection']) for k in keys}
return {k: v if k not in trans else trans[k].get(v, v)
for k, v in value.items()}

727
model/fields/field.py Executable file
View File

@@ -0,0 +1,727 @@
# 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 warnings
from functools import partial, wraps
import sql
from sql import (
Cast, Column, CombiningQuery, Expression, Literal, Null, Query, Select,
operators)
from sql.aggregate import Min
from sql.conditionals import Coalesce, NullIf
from sql.operators import Concat
from trytond import backend
from trytond.const import OPERATORS
from trytond.pool import Pool
from trytond.pyson import PYSON, Eval, PYSONDecoder, PYSONEncoder
from trytond.rpc import RPC
from trytond.tools import cached_property
from trytond.tools.string_ import LazyString, StringPartitioned
from trytond.transaction import Transaction
_sql_version = tuple(map(int, sql.__version__.split('.')))
def domain_validate(value):
assert isinstance(value, list), 'domain must be a list'
def test_domain(dom):
for arg in dom:
if isinstance(arg, str):
if arg not in ('AND', 'OR'):
return False
elif (isinstance(arg, tuple)
or (isinstance(arg, list)
and len(arg) > 2
and ((
isinstance(arg[1], str)
and arg[1] in OPERATORS)
or (
isinstance(arg[1], PYSON)
and arg[1].types() == {str})))):
pass
elif isinstance(arg, list):
if not test_domain(arg):
return False
return True
assert test_domain(value), 'invalid domain'
def states_validate(value):
assert isinstance(value, dict), 'states must be a dict'
assert set(value).issubset({'required', 'readonly', 'invisible'}), (
'extra keys "%(keys)s" in states' % {
'keys': set(value) - {'required', 'readonly', 'invisible'},
})
for state in value:
assert isinstance(value[state], (bool, PYSON)), \
'values of states must be PYSON'
if hasattr(value[state], 'types'):
assert value[state].types() == {bool}, \
'values of states must return boolean'
def depends_validate(value):
assert isinstance(value, set), 'depends must be a set'
def context_validate(value):
assert isinstance(value, dict), 'context must be a dict'
def size_validate(value):
if value is not None:
assert isinstance(value, (int, PYSON)), 'size must be PYSON'
if hasattr(value, 'types'):
assert value.types() <= {int, type(None)}, \
'size must return integer'
def search_order_validate(value):
if value is not None:
assert isinstance(value, (list, PYSON)), 'search_order must be PYSON'
if hasattr(value, 'types'):
assert value.types() == {list}, 'search_order must be PYSON'
def _set_value(record, field):
try:
field, nested = field.split('.', 1)
except ValueError:
nested = None
if field.startswith('_parent_'):
field = field[8:] # Strip '_parent_'
if not hasattr(record, field):
default = None
if hasattr(record, '_defaults') and field in record._defaults:
default = record._defaults[field]()
setattr(record, field, default)
elif nested:
parent = getattr(record, field)
if parent:
_set_value(parent, nested)
def depends(*fields, **kwargs):
methods = kwargs.pop('methods', None)
assert not kwargs
def decorator(func):
depends = getattr(func, 'depends', set())
depends.update(fields)
setattr(func, 'depends', depends)
if methods:
depend_methods = getattr(func, 'depend_methods', set())
depend_methods.update(methods)
setattr(func, 'depend_methods', depend_methods)
@wraps(func)
def wrapper(self, *args, **kwargs):
for field in fields:
_set_value(self, field)
return func(self, *args, **kwargs)
return wrapper
return decorator
def _iter_eval_fields(value):
"Iterate over evaluated fields"
if isinstance(value, Eval):
yield value.basename
elif isinstance(value, PYSON):
yield from _iter_eval_fields(value.pyson())
elif isinstance(value, (list, tuple)):
for val in value:
yield from _iter_eval_fields(val)
elif isinstance(value, dict):
for val in value.values():
yield from _iter_eval_fields(val)
def get_eval_fields(value):
"Return fields evaluated"
fields = set(_iter_eval_fields(value))
fields.discard('context') # TODO: remove when context is renamed
return fields
def instanciate_values(Target, value, **extra):
from ..modelstorage import ModelStorage, local_cache
kwargs = {}
ids = []
if issubclass(Target, ModelStorage):
kwargs['_local_cache'] = local_cache(Target)
kwargs['_ids'] = ids
def instance(data):
if isinstance(data, Target):
for k, v in extra.items():
setattr(data, k, v)
return data
elif isinstance(data, dict):
if data.get('id', -1) >= 0:
values = {}
values.update(data)
values.update(kwargs)
ids.append(data['id'])
else:
values = data
values.update(extra)
return Target(**values)
else:
ids.append(data)
return Target(data, **extra, **kwargs)
return tuple(instance(x) for x in (value or []))
def instantiate_context(field, record):
from ..modelstorage import EvalEnvironment
ctx = {}
if field.context:
pyson_context = PYSONEncoder().encode(field.context)
ctx.update(PYSONDecoder(
EvalEnvironment(record, record.__class__)).decode(
pyson_context))
datetime_ = None
if getattr(field, 'datetime_field', None):
datetime_ = getattr(record, field.datetime_field, None)
ctx = {'_datetime': datetime_}
return ctx
def on_change_result(record):
return record._changed_values
def on_change_with_result(fieldname):
def decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
value = func(self, *args, **kwargs)
setattr(self, fieldname, value)
return self._changed_values
return wrapper
return decorator
def domain_method(func):
@wraps(func)
def wrapper(self, domain, tables, Model):
name = domain[0].split('.', 1)[0]
assert name == self.name
method = getattr(Model, f'domain_{name}', None)
if method:
return method(domain, tables)
return func(self, domain, tables, Model)
return wrapper
def order_method(func):
@wraps(func)
def wrapper(self, name, tables, Model):
fname, _, oexpr = name.partition('.')
assert fname == self.name
method = getattr(Model, f'order_{fname}', None)
if not oexpr and method:
return method(tables)
else:
return func(self, name, tables, Model)
return wrapper
SQL_OPERATORS = {
'=': operators.Equal,
'!=': operators.NotEqual,
'like': partial(operators.Like, escape='\\'),
'not like': partial(operators.NotLike, escape='\\'),
'ilike': partial(operators.ILike, escape='\\'),
'not ilike': partial(operators.NotILike, escape='\\'),
'in': operators.In,
'not in': operators.NotIn,
'<=': operators.LessEqual,
'>=': operators.GreaterEqual,
'<': operators.Less,
'>': operators.Greater,
}
class Field(object):
_type = None
_sql_type = None
_py_type = None
def __init__(self, string='', help='', required=False, readonly=False,
domain=None, states=None, on_change=None,
on_change_with=None, depends=None, context=None,
loading='eager'):
'''
:param string: A string for label of the field.
:param help: A multi-line help string.
:param required: A boolean if ``True`` the field is required.
:param readonly: A boolean if ``True`` the field is not editable in
the user interface.
:param domain: A list that defines a domain constraint.
:param states: A dictionary. Possible keys are ``required``,
``readonly`` and ``invisible``. Values are pyson expressions that
will be evaluated with record values. This allows to change
dynamically the attributes of the field.
:param on_change: A list of values. If set, the client will call the
method ``on_change_<field_name>`` when the user changes the field
value. It then passes this list of values as arguments to the
function.
:param on_change_with: A list of values. Like ``on_change``, but
defined the other way around. The list contains all the fields that
must update the current field.
:param depends: A set of field name on which this one depends.
:param context: A dictionary which will be given to open the relation
fields.
:param loading: Define how the field must be loaded:
``lazy`` or ``eager``.
'''
if not isinstance(string, LazyString):
assert string, 'a string is required'
self.string = string
self.help = help
self.required = required
self.readonly = readonly
self.__domain = None
self.domain = domain
self.__states = None
self.states = states or {}
self.on_change = set()
if on_change:
warnings.warn('on_change argument is deprecated, '
'use the depends decorator',
DeprecationWarning, stacklevel=3)
self.on_change.update(on_change)
self.on_change_with = set()
if on_change_with:
warnings.warn('on_change_with argument is deprecated, '
'use the depends decorator',
DeprecationWarning, stacklevel=3)
self.on_change_with.update(on_change_with)
self.__depends = None
self.depends = depends or set()
self.__context = None
self.context = context or {}
assert loading in ('lazy', 'eager'), \
'loading must be "lazy" or "eager"'
self.loading = loading
self.name = None
@property
def string(self):
return self.__string
@string.setter
def string(self, value):
self.__string = StringPartitioned(value)
@property
def help(self):
return self.__help
@help.setter
def help(self, value):
self.__help = StringPartitioned(value)
def _get_domain(self):
return self.__domain
def _set_domain(self, value):
if value is None:
value = []
domain_validate(value)
self.__domain = value
domain = property(_get_domain, _set_domain)
def _get_states(self):
return self.__states
def _set_states(self, value):
states_validate(value)
self.__states = value
states = property(_get_states, _set_states)
def _get_depends(self):
return self.__depends
def _set_depends(self, value):
value = set(value)
depends_validate(value)
self.__depends = value
depends = property(_get_depends, _set_depends)
@cached_property
def display_depends(self):
depends = get_eval_fields(self.states.get('invisible'))
return self.depends | depends
@cached_property
def edition_depends(self):
depends = get_eval_fields(self.domain)
depends |= get_eval_fields(self.states.get('readonly'))
depends |= get_eval_fields(self.states.get('required'))
return self.depends | depends
@cached_property
def validation_depends(self):
depends = get_eval_fields(self.domain)
depends |= get_eval_fields(self.states.get('required'))
return self.depends | depends
def _get_context(self):
return self.__context
def _set_context(self, value):
context_validate(value)
self.__context = value
context = property(_get_context, _set_context)
def __get__(self, inst, cls):
if inst is None:
return self
assert self.name is not None
if self.name == 'id':
return inst._id
return inst.__getattr__(self.name)
def __set__(self, inst, value):
assert self.name is not None
if isinstance(value, (Query, Expression)):
raise ValueError("Can not assign SQL")
if inst._values is None:
inst._values = inst._record()
if (self._py_type and value is not None
and not isinstance(value, self._py_type)):
value = self._py_type(value)
inst._values[self.name] = value
def sql_format(self, value):
if isinstance(value, (Query, Expression)):
return value
assert self._sql_type is not None
database = Transaction().database
if (self._py_type and value is not None
and not isinstance(value, self._py_type)):
value = self._py_type(value)
return database.sql_format(self._sql_type, value)
def sql_type(self):
database = Transaction().database
return database.sql_type(self._sql_type)
def sql_cast(self, expression):
return Cast(expression, self.sql_type().base)
def sql_column(self, table):
return Column(table, self.name)
def _domain_column(self, operator, column):
return column
def _domain_value(self, operator, value):
if isinstance(value, (Select, CombiningQuery)):
return value
if operator in ('in', 'not in'):
return [self.sql_format(v) for v in value if v is not None]
else:
return self.sql_format(value)
def _domain_add_null(self, column, operator, value, expression):
if operator in ('in', 'not in'):
if (not isinstance(value, (Select, CombiningQuery))
and any(v is None for v in value)):
if operator == 'in':
expression |= (column == Null)
else:
expression &= (column != Null)
return expression
@domain_method
def convert_domain(self, domain, tables, Model):
"Return a SQL expression for the domain using tables"
table, _ = tables[None]
name, operator, value = domain
Operator = SQL_OPERATORS[operator]
column = self.sql_column(table)
column = self._domain_column(operator, column)
expression = Operator(column, self._domain_value(operator, value))
if isinstance(expression, operators.In) and not expression.right:
expression = Literal(False)
elif isinstance(expression, operators.NotIn) and not expression.right:
expression = Literal(True)
expression = self._domain_add_null(column, operator, value, expression)
return expression
@order_method
def convert_order(self, name, tables, Model):
"Return a SQL expression to order"
table, _ = tables[None]
return [self.sql_column(table)]
def set_rpc(self, model):
for attribute, decorator, result in (
('on_change', None, on_change_result),
('on_change_with', on_change_with_result(self.name), None),
):
if not getattr(self, attribute):
continue
func_name = '%s_%s' % (attribute, self.name)
assert hasattr(model, func_name), \
'Missing %s on model %s' % (func_name, model.__name__)
model.__rpc__.setdefault(
func_name,
RPC(instantiate=0, decorator=decorator, result=result))
def definition(self, model, language):
pool = Pool()
Translation = pool.get('ir.translation')
encoder = PYSONEncoder()
definition = {
'context': encoder.encode(self.context),
'loading': self.loading,
'name': self.name,
'on_change': list(self.on_change),
'on_change_with': list(self.on_change_with),
'readonly': self.readonly,
'required': self.required,
'states': encoder.encode(self.states),
'type': self._type,
'domain': encoder.encode(self.domain),
'searchable': self.searchable(model),
'sortable': self.sortable(model),
}
# Add id to on_change's if they are not cached
# Not having the id increase the efficiency of the cache
for method in ['on_change', 'on_change_with']:
changes = definition[method]
if changes:
method_name = method + '_' + self.name
if not model.__rpc__[method_name].cache:
changes.append('id')
for name in changes:
target = model
if '.' in name:
prefix, _ = name.rsplit('.', 1)
prefix += '.'
else:
prefix = ''
while name.startswith('_parent_'):
field, name = name.split('.', 1)
target = target._fields[field[8:]].get_target()
field = target._fields[name]
if field and field.context:
eval_fields = get_eval_fields(field.context)
for context_field_name in eval_fields:
prefix_ctx_field_name = (
prefix + context_field_name)
if (context_field_name in field.depends
and prefix_ctx_field_name not in changes):
changes.append(prefix_ctx_field_name)
name = '%s,%s' % (model.__name__, self.name)
for attr, ttype in [('string', 'field'), ('help', 'help')]:
definition[attr] = ''
for source in getattr(self, attr):
if not isinstance(source, LazyString):
source = (
Translation.get_source(name, ttype, language, source)
or source)
definition[attr] += source
return definition
def definition_translations(self, model, language):
"Returns sources used for definition"
name = '%s,%s' % (model.__name__, self.name)
translations = []
for attr, ttype in [('string', 'field'), ('help', 'help')]:
for source in getattr(self, attr):
if not isinstance(source, LazyString):
translations.append((name, ttype, language, source))
return translations
def searchable(self, model):
return hasattr(model, 'search')
def sortable(self, model):
return hasattr(model, 'search')
class FieldTranslate(Field):
def _get_translation_join(
self, Model, name, translation, table, from_, language):
if Model.__name__ == 'ir.model.field':
pool = Pool()
ModelData = pool.get('ir.model.data')
ModelField = pool.get('ir.model.field')
Translation = pool.get('ir.translation')
model_data = ModelData.__table__()
model_field = ModelField.__table__()
msg_trans = Translation.__table__()
if name == 'field_description':
type_ = 'field'
else:
type_ = 'help'
translation = translation.select(
translation.id.as_('id'),
translation.res_id.as_('res_id'),
translation.value.as_('value'),
translation.name.as_('name'),
translation.lang.as_('lang'),
translation.type.as_('type'),
translation.fuzzy.as_('fuzzy'),
)
translation |= (msg_trans
.join(model_data,
condition=(msg_trans.res_id == model_data.db_id)
& (model_data.model == 'ir.message')
& (msg_trans.name == 'ir.message,text'))
.join(model_field,
condition=Concat(
Concat(model_data.module, '.'),
model_data.fs_id) == getattr(model_field, name))
.select(
msg_trans.id.as_('id'),
Literal(-1).as_('res_id'),
msg_trans.value.as_('value'),
Concat(
Concat(model_field.model, ','),
model_field.name).as_('name'),
msg_trans.lang.as_('lang'),
Literal(type_).as_('type'),
msg_trans.fuzzy.as_('fuzzy'),
))
if backend.name == 'postgresql' and _sql_version >= (1, 1, 0):
query = translation.select(
translation.res_id.as_('res_id'),
translation.value.as_('value'),
translation.name.as_('name'),
distinct=True,
distinct_on=[translation.res_id, translation.name],
order_by=[
translation.res_id,
translation.name,
translation.id.desc])
else:
query = translation.select(
translation.res_id.as_('res_id'),
Min(translation.value).as_('value'),
translation.name.as_('name'),
group_by=[translation.res_id, translation.name])
if Model.__name__ == 'ir.model':
name_ = Concat(Concat(table.model, ','), name)
type_ = 'model'
res_id = -1
elif Model.__name__ == 'ir.model.field':
name_ = Concat(Concat(table.model, ','), table.name)
if name == 'field_description':
type_ = 'field'
else:
type_ = 'help'
res_id = -1
else:
name_ = '%s,%s' % (Model.__name__, name)
type_ = 'model'
res_id = table.id
query.where = (
(translation.lang == language)
& (translation.type == type_)
& (translation.fuzzy == Literal(False))
)
return query, from_.join(query, 'LEFT',
condition=(query.res_id == res_id) & (query.name == name_))
def _get_translation_column(self, Model, name):
from trytond.ir.lang import get_parent_language
pool = Pool()
Translation = pool.get('ir.translation')
table = join = Model.__table__()
language = Transaction().language
column = None
while language:
translation = Translation.__table__()
translation, join = self._get_translation_join(
Model, name, translation, table, join, language)
column = Coalesce(NullIf(column, ''), translation.value)
language = get_parent_language(language)
return table, join, column
@domain_method
def convert_domain(self, domain, tables, Model):
if not self.translate:
return super(FieldTranslate, self).convert_domain(
domain, tables, Model)
table, _ = tables[None]
name, operator, value = domain
model, join, column = self._get_translation_column(Model, name)
column = Coalesce(NullIf(column, ''), self.sql_column(model))
column = self._domain_column(operator, column)
Operator = SQL_OPERATORS[operator]
assert name == self.name
where = Operator(column, self._domain_value(operator, value))
if isinstance(where, operators.In) and not where.right:
where = Literal(False)
elif isinstance(where, operators.NotIn) and not where.right:
where = Literal(True)
where = self._domain_add_null(column, operator, value, where)
return table.id.in_(join.select(model.id, where=where))
def _get_translation_order(self, tables, Model, name):
from trytond.ir.lang import get_parent_language
pool = Pool()
Translation = pool.get('ir.translation')
table, _ = tables[None]
join = table
language = Transaction().language
column = None
while language:
key = name + '.translation-' + language
if key not in tables:
translation = Translation.__table__()
translation, join = self._get_translation_join(
Model, name, translation, table, table, language)
if join.left == table:
tables[key] = {
None: (join.right, join.condition),
}
else:
tables[key] = {
None: (join.left.right, join.left.condition),
'translation': {
None: (join.right, join.condition),
},
}
else:
if 'translation' not in tables[key]:
translation, _ = tables[key][None]
else:
translation, _ = tables[key]['translation'][None]
column = Coalesce(NullIf(column, ''), translation.value)
language = get_parent_language(language)
return column
@order_method
def convert_order(self, name, tables, Model):
if not self.translate:
return super().convert_order(name, tables, Model)
assert name == self.name
table, _ = tables[None]
column = self._get_translation_order(tables, Model, name)
return [Coalesce(NullIf(column, ''), self.sql_column(table))]
def definition(self, model, language):
definition = super().definition(model, language)
definition['translate'] = self.translate
return definition

79
model/fields/float.py Executable file
View File

@@ -0,0 +1,79 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pyson import PYSON, PYSONEncoder
from trytond.tools import cached_property
from .field import Field, get_eval_fields
def digits_validate(value):
if value:
assert isinstance(value, (tuple, str)), \
'digits must be a tuple or a string'
if isinstance(value, tuple):
for i in value:
assert isinstance(i, (int, PYSON)), \
'digits must be tuple of integers or PYSON'
if isinstance(i, PYSON):
assert i.types() == {int}, \
'PYSON digits must return an integer'
def _get_digits_depends(field):
if isinstance(field.digits, str):
return {field.digits}
else:
return get_eval_fields(field.digits)
class Float(Field):
'''
Define a float field (``float``).
'''
_type = 'float'
_sql_type = 'FLOAT'
_py_type = float
def __init__(self, string='', digits=None, help='', required=False,
readonly=False, domain=None, states=None,
on_change=None, on_change_with=None, depends=None,
context=None, loading='eager'):
'''
:param digits: a list of two integers defining the total
of digits and the number of decimals of the float.
'''
super(Float, self).__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
self.__digits = None
self.digits = digits
__init__.__doc__ += Field.__init__.__doc__
def _get_digits(self):
return self.__digits
def _set_digits(self, value):
digits_validate(value)
self.__digits = value
digits = property(_get_digits, _set_digits)
def definition(self, model, language):
encoder = PYSONEncoder()
definition = super().definition(model, language)
definition['digits'] = encoder.encode(self.digits)
return definition
@cached_property
def display_depends(self):
return super().display_depends | _get_digits_depends(self)
@cached_property
def edition_depends(self):
return super().edition_depends | _get_digits_depends(self)
@cached_property
def validation_depends(self):
return super().validation_depends | _get_digits_depends(self)

92
model/fields/fmany2one.py Executable file
View File

@@ -0,0 +1,92 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from sql import Column, Literal
from trytond.pool import Pool
from trytond.tools import grouped_slice
from .function import Function
from .many2one import Many2One
def fmany2one(
name, sources, target, string="", ondelete='SET NULL', **kwargs):
sources = sources.split(',')
target_model, target_fields = target.split(',', 1)
target_fields = target_fields.split(',')
assert len(sources) == len(target_fields)
class Mixin:
__slots__ = ()
@classmethod
def __register__(cls, module):
pool = Pool()
Target = pool.get(target_model)
table_h = cls.__table_handler__(module)
super().__register__(module)
table_h.add_fk(
sources, Target._table, target_fields,
on_delete=getattr(cls, name).ondelete)
@classmethod
def getter(cls, records, name):
pool = Pool()
Target = pool.get(target_model)
values = set(filter(all,
(tuple(getattr(r, s) for s in sources) for r in records)))
values2id = {}
for sub_values in grouped_slice(values):
domain = ['OR']
for values in sub_values:
domain.append(
[(f, '=', v) for f, v in zip(target_fields, values)])
targets = Target.search(domain)
values2id.update(
(tuple(getattr(t, f) for f in target_fields), t.id)
for t in targets)
return {r.id: values2id.get(
tuple(getattr(r, s) for s in sources)) for r in records}
@classmethod
def setter(cls, records, name, value):
pool = Pool()
Target = pool.get(target_model)
if value:
value = getattr(Target(value), target_fields[0])
else:
value = None
cls.write(records, {sources[0]: value})
@classmethod
def searcher(cls, clause, tables):
pool = Pool()
Target = pool.get(target_model)
table, _ = tables[None]
if name not in tables:
target = Target.__table__()
join = Literal(True)
for source, target_field in zip(sources, target_fields):
join &= Column(table, source) == Column(target, target_field)
tables[name] = {
None: (target, join),
}
nested = clause[0][len(name) + 1:]
if not nested:
if isinstance(clause[2], str):
nested = 'rec_name'
else:
nested = 'id'
domain = [(nested, *clause[1:])]
tables, clause = Target.search_domain(
domain, tables=tables[name])
return clause
setattr(Mixin, name, Function(
Many2One(target_model, string, ondelete=ondelete, **kwargs),
f'get_{name}', setter=f'set_{name}'))
setattr(Mixin, f'get_{name}', getter)
setattr(Mixin, f'set_{name}', setter)
setattr(Mixin, f'domain_{name}', searcher)
return Mixin

249
model/fields/function.py Executable file
View File

@@ -0,0 +1,249 @@
# 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
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)))
@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)

12
model/fields/integer.py Executable file
View File

@@ -0,0 +1,12 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from .field import Field
class Integer(Field):
'''
Define an integer field (``int``).
'''
_type = 'integer'
_sql_type = 'INTEGER'
_py_type = int

505
model/fields/many2many.py Executable file
View File

@@ -0,0 +1,505 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from collections import defaultdict
from itertools import chain
from sql import Literal, Null
from sql.conditionals import Coalesce
from trytond.pool import Pool
from trytond.pyson import PYSONEncoder
from trytond.tools import cached_property, grouped_slice
from trytond.transaction import Transaction
from .field import (
Field, context_validate, domain_method, domain_validate, get_eval_fields,
instanciate_values, instantiate_context, search_order_validate,
size_validate)
class Many2Many(Field):
'''
Define many2many field (``list``).
'''
_type = 'many2many'
def __init__(self, relation_name, origin, target, string='', order=None,
datetime_field=None, size=None, search_order=None,
search_context=None, help='', required=False, readonly=False,
domain=None, filter=None, states=None, on_change=None,
on_change_with=None, depends=None, context=None, loading='lazy'):
'''
:param relation_name: The name of the relation model
or the name of the target model for ModelView only.
:param origin: The name of the field to store origin ids.
:param target: The name of the field to store target ids.
:param order: a list of tuples that are constructed like this:
``('field name', 'DESC|ASC')``
allowing to specify the order of result
:param datetime_field: The name of the field that contains the datetime
value to read the target records.
:param search_order: The order to use when searching for a record
:param search_context: The context to use when searching for a record
:param filter: A domain to filter target records.
'''
super(Many2Many, self).__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
self.relation_name = relation_name
self.origin = origin
self.target = target
self.order = order
self.datetime_field = datetime_field
self.__size = None
self.size = size
self.__search_order = None
self.search_order = search_order
self.__search_context = None
self.search_context = search_context or {}
self.__filter = None
self.filter = filter
__init__.__doc__ += Field.__init__.__doc__
def _get_size(self):
return self.__size
def _set_size(self, value):
size_validate(value)
self.__size = value
size = property(_get_size, _set_size)
@property
def search_order(self):
return self.__search_order
@search_order.setter
def search_order(self, value):
search_order_validate(value)
self.__search_order = value
@property
def search_context(self):
return self.__search_context
@search_context.setter
def search_context(self, value):
context_validate(value)
self.__search_context = value
@property
def filter(self):
return self.__filter
@filter.setter
def filter(self, value):
if value is not None:
domain_validate(value)
self.__filter = value
@property
def add_remove(self):
return self.domain
@cached_property
def display_depends(self):
depends = super().display_depends
if self.datetime_field:
depends.add(self.datetime_field)
return depends
@cached_property
def edition_depends(self):
depends = super().edition_depends
depends |= get_eval_fields(self.size)
return depends
@cached_property
def validation_depends(self):
depends = super().validation_depends
depends |= get_eval_fields(self.size)
return depends
def sql_type(self):
return None
def get(self, ids, model, name, values=None):
'''
Return target records ordered.
'''
if values is None:
values = {}
res = {}
if not ids:
return res
for i in ids:
res[i] = []
Relation = self.get_relation()
origin_field = Relation._fields[self.origin]
if origin_field.sortable(Relation):
if origin_field._type == 'reference':
order = [(self.origin, None)]
else:
order = [(self.origin + '.id', None)]
else:
order = []
if self.order is None:
order += [(self.target, None)]
else:
order += self.order
relations = []
for sub_ids in grouped_slice(ids):
if origin_field._type == 'reference':
references = ['%s,%s' % (model.__name__, x) for x in sub_ids]
clause = [(self.origin, 'in', references)]
else:
clause = [(self.origin, 'in', list(sub_ids))]
clause += [(self.target, '!=', None)]
if self.filter:
clause.append((self.target, 'where', self.filter))
relations.append(Relation.search(clause, order=order))
relations = Relation.browse(list(chain(*relations)))
for relation in relations:
origin_id = getattr(relation, self.origin).id
res[origin_id].append(getattr(relation, self.target).id)
return dict((key, tuple(value)) for key, value in res.items())
def set(self, Model, name, ids, values, *args):
'''
Set the values.
values: A list of tuples:
(``create``, ``[{<field name>: value}, ...]``),
(``write``, [``<ids>``, ``{<field name>: value}``, ...]),
(``delete``, ``<ids>``),
(``remove``, ``<ids>``),
(``add``, ``<ids>``),
(``copy``, ``<ids>``, ``[{<field name>: value}, ...]``)
'''
Relation = self.get_relation()
Target = self.get_target()
origin_field = Relation._fields[self.origin]
relation_to_create = []
relation_to_delete = []
target_to_write = []
target_to_delete = []
def search_clause(ids):
if origin_field._type == 'reference':
references = ['%s,%s' % (Model.__name__, x) for x in ids]
return (self.origin, 'in', references)
else:
return (self.origin, 'in', ids)
def field_value(record_id):
if origin_field._type == 'reference':
return '%s,%s' % (Model.__name__, record_id)
else:
return record_id
def create(ids, vlist):
for record_id in ids:
for new in Target.create(vlist):
relation_to_create.append({
self.origin: field_value(record_id),
self.target: new.id,
})
def write(_, *args):
actions = iter(args)
target_to_write.extend(sum(((Target.browse(ids), values)
for ids, values in zip(actions, actions)), ()))
def delete(_, target_ids):
target_to_delete.extend(Target.browse(target_ids))
def add(ids, target_ids):
target_ids = list(map(int, target_ids))
if not target_ids:
return
existing_ids = set()
for sub_ids in grouped_slice(target_ids):
relations = Relation.search([
search_clause(ids),
(self.target, 'in', list(sub_ids)),
])
for relation in relations:
existing_ids.add((
getattr(relation, self.origin).id,
getattr(relation, self.target).id))
for new_id in target_ids:
for record_id in ids:
if (record_id, new_id) in existing_ids:
continue
relation_to_create.append({
self.origin: field_value(record_id),
self.target: new_id,
})
def remove(ids, target_ids):
target_ids = list(map(int, target_ids))
if not target_ids:
return
for sub_ids in grouped_slice(target_ids):
relation_to_delete.extend(Relation.search([
search_clause(ids),
(self.target, 'in', list(sub_ids)),
]))
def copy(ids, copy_ids, default=None):
copy_ids = list(map(int, copy_ids))
if default is None:
default = {}
default = default.copy()
copies = Target.browse(copy_ids)
for new in Target.copy(copies, default=default):
for record_id in ids:
relation_to_create.append({
self.origin: field_value(record_id),
self.target: new.id,
})
actions = {
'create': create,
'write': write,
'delete': delete,
'add': add,
'remove': remove,
'copy': copy,
}
args = iter((ids, values) + args)
for ids, values in zip(args, args):
if not values:
continue
for value in values:
action = value[0]
args = value[1:]
actions[action](ids, *args)
# Ordered operations to avoid uniqueness/overlapping constraints
if relation_to_delete:
Relation.delete(relation_to_delete)
if target_to_delete:
Target.delete(target_to_delete)
if target_to_write:
Target.write(*target_to_write)
if relation_to_create:
Relation.create(relation_to_create)
def get_relation(self):
"Return the relation model"
return Pool().get(self.relation_name)
def get_target(self):
'Return the target model'
Relation = self.get_relation()
if not self.target:
return Relation
return Relation._fields[self.target].get_target()
def __set__(self, inst, value):
Target = self.get_target()
ctx = instantiate_context(self, inst)
with Transaction().set_context(ctx):
records = instanciate_values(Target, value)
super(Many2Many, self).__set__(inst, records)
def delete(self, inst, records):
records = set(records)
if inst._deleted is None:
inst._deleted = defaultdict(set)
inst._deleted[self.name].update(map(int, records))
setattr(
inst, self.name,
[r for r in getattr(inst, self.name) if r not in records])
def convert_domain_tree(self, domain, tables):
Target = self.get_target()
table, _ = tables[None]
name, operator, ids = domain
ids = set(ids) # Ensure it is a set for concatenation
def get_child(ids):
if not ids:
return set()
children = Target.search([
(name, 'in', ids),
(name, '!=', None),
], order=[])
child_ids = get_child(set(c.id for c in children))
return ids | child_ids
def get_parent(ids):
if not ids:
return set()
parent_ids = set()
for parent in Target.browse(ids):
parent_ids.update(p.id for p in getattr(parent, name))
return ids | get_parent(parent_ids)
if operator.endswith('child_of'):
ids = list(get_child(ids))
else:
ids = list(get_parent(ids))
if not ids:
expression = Literal(False)
else:
expression = table.id.in_(ids)
if operator.startswith('not'):
return ~expression
return expression
@domain_method
def convert_domain(self, domain, tables, Model):
from ..modelsql import convert_from
pool = Pool()
Rule = pool.get('ir.rule')
Target = self.get_target()
Relation = self.get_relation()
transaction = Transaction()
table, _ = tables[None]
name, operator, value = domain[:3]
assert operator not in {'where', 'not where'} or '.' not in name
if Relation._history and transaction.context.get('_datetime'):
relation = Relation.__table_history__()
history_where = (
Coalesce(relation.write_date, relation.create_date)
<= transaction.context['_datetime'])
else:
relation = Relation.__table__()
history_where = None
origin_field = Relation._fields[self.origin]
origin = getattr(Relation, self.origin).sql_column(relation)
origin_where = None
if origin_field._type == 'reference':
origin_where = origin.like(Model.__name__ + ',%')
origin = origin_field.sql_id(origin, Relation)
target = getattr(Relation, self.target).sql_column(relation)
if '.' not in name:
if operator.endswith('child_of') or operator.endswith('parent_of'):
if Target != Model:
if operator.endswith('child_of'):
target_operator = 'child_of'
else:
target_operator = 'parent_of'
target_domain = [
(domain[3], target_operator, value),
]
if self.filter:
target_domain.append(self.filter)
query = Target.search(target_domain, order=[], query=True)
where = (target.in_(query) & (origin != Null))
if history_where:
where &= history_where
if origin_where:
where &= origin_where
query = relation.select(origin, where=where)
expression = table.id.in_(query)
if operator.startswith('not'):
return ~expression
return expression
if isinstance(value, str):
target_domain = [('rec_name', 'ilike', value)]
if self.filter:
target_domain.append(self.filter)
targets = Target.search(target_domain, order=[])
ids = [t.id for t in targets]
else:
if not isinstance(value, (list, tuple)):
ids = [value]
else:
ids = value
if self.filter:
targets = Target.search(
[('id', 'in', ids), self.filter], order=[])
ids = [t.id for t in targets]
if not ids:
expression = Literal(False)
if operator.startswith('not'):
return ~expression
return expression
else:
return self.convert_domain_tree(
(name, operator, ids), tables)
if value is None:
where = origin != value
if history_where:
where &= history_where
if origin_where:
where &= origin_where
if self.filter:
query = Target.search(self.filter, order=[], query=True)
where &= target.in_(query)
query = relation.select(origin, where=where)
expression = ~table.id.in_(query)
if operator == '!=':
return ~expression
return expression
else:
if isinstance(value, str):
target_name = 'rec_name'
else:
target_name = 'id'
else:
_, target_name = name.split('.', 1)
if operator not in {'where', 'not where'}:
relation_domain = [('%s.%s' % (self.target, target_name),)
+ tuple(domain[1:])]
if origin_field._type == 'reference':
relation_domain.append(
(self.origin, 'like', Model.__name__ + ',%'))
else:
relation_domain = [self.target, 'where', value]
rule_domain = Rule.domain_get(Relation.__name__, mode='read')
if rule_domain:
relation_domain = [relation_domain, rule_domain]
if self.filter:
relation_domain = [
relation_domain,
(self.target, 'where', self.filter),
]
relation_tables = {
None: (relation, None),
}
tables, expression = Relation.search_domain(
relation_domain, tables=relation_tables)
query_table = convert_from(None, relation_tables)
query = query_table.select(origin, where=expression)
expression = table.id.in_(query)
if operator == 'not where':
expression = ~expression
elif operator.startswith('!') or operator.startswith('not '):
expression |= ~table.id.in_(relation.select(origin))
return expression
def definition(self, model, language):
encoder = PYSONEncoder()
definition = super().definition(model, language)
if self.add_remove is not None:
definition['add_remove'] = encoder.encode(self.add_remove)
definition['datetime_field'] = self.datetime_field
if self.filter:
definition['domain'] = encoder.encode(
['AND', self.domain, self.filter])
definition['relation'] = self.get_target().__name__
definition['search_context'] = encoder.encode(self.search_context)
definition['search_order'] = encoder.encode(self.search_order)
definition['order'] = (
getattr(self.get_target(), '_order', None)
if self.order is None else self.order)
if self.size is not None:
definition['size'] = encoder.encode(self.size)
return definition
def sortable(self, model):
return super().sortable(model) and hasattr(model, f'order_{self.name}')

354
model/fields/many2one.py Executable file
View File

@@ -0,0 +1,354 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from sql import As, Column, Expression, Literal, Query, With
from sql.aggregate import Max
from sql.conditionals import Coalesce
from sql.operators import Or
from trytond.config import config
from trytond.pool import Pool
from trytond.pyson import PYSONEncoder
from trytond.tools import cached_property, reduce_ids
from trytond.transaction import Transaction, inactive_records
from .field import (
Field, context_validate, domain_method, instantiate_context, order_method,
search_order_validate)
_subquery_threshold = config.getint('database', 'subquery_threshold')
class Many2One(Field):
'''
Define many2one field (``int``).
'''
_type = 'many2one'
_sql_type = 'INTEGER'
def __init__(self, model_name, string='', left=None, right=None, path=None,
ondelete='SET NULL', datetime_field=None,
search_order=None, search_context=None, help='', required=False,
readonly=False, domain=None, states=None,
on_change=None, on_change_with=None, depends=None, context=None,
loading='eager'):
'''
:param model_name: The name of the target model.
:param left: The name of the field to store the left value for
Modified Preorder Tree Traversal.
See http://en.wikipedia.org/wiki/Tree_traversal
:param right: The name of the field to store the right value. See left
:param path: The name of the field used to store the path.
:param ondelete: Define the behavior of the record when the target
record is deleted. (``CASCADE``, ``RESTRICT``, ``SET NULL``)
``SET NULL`` will be changed into ``RESTRICT`` if required is set.
:param datetime_field: The name of the field that contains the datetime
value to read the target record.
:param target_search: The kind of target search 'subquery' or 'join'
:param search_order: The order to use when searching for a record
:param search_context: The context to use when searching for a record
'''
self.__required = required
if ondelete not in ('CASCADE', 'RESTRICT', 'SET NULL'):
raise Exception('Bad arguments')
self.ondelete = ondelete
super(Many2One, self).__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
self.model_name = model_name
self.left = left
self.right = right
self.path = path
self.datetime_field = datetime_field
self.__search_order = None
self.search_order = search_order
self.__search_context = None
self.search_context = search_context or {}
__init__.__doc__ += Field.__init__.__doc__
def __get_required(self):
return self.__required
def __set_required(self, value):
self.__required = value
if value and self.ondelete == 'SET NULL':
self.ondelete = 'RESTRICT'
required = property(__get_required, __set_required)
@property
def search_order(self):
return self.__search_order
@search_order.setter
def search_order(self, value):
search_order_validate(value)
self.__search_order = value
@property
def search_context(self):
return self.__search_context
@search_context.setter
def search_context(self, value):
context_validate(value)
self.__search_context = value
@cached_property
def display_depends(self):
depends = super().display_depends
if self.datetime_field:
depends.add(self.datetime_field)
return depends
def get_target(self):
'Return the target Model'
return Pool().get(self.model_name)
def __set__(self, inst, value):
Target = self.get_target()
if isinstance(value, (dict, int)):
ctx = instantiate_context(self, inst)
with Transaction().set_context(ctx):
if isinstance(value, dict):
value = Target(**value)
elif isinstance(value, int):
value = Target(value)
assert isinstance(value, (Target, type(None)))
super(Many2One, self).__set__(inst, value)
def sql_format(self, value):
from ..model import Model
assert value is not False
assert (
not isinstance(value, Model) or value.__name__ == self.model_name)
if value and not isinstance(value, (Query, Expression)):
value = int(value)
return super().sql_format(value)
def convert_domain_path(self, domain, tables):
cursor = Transaction().connection.cursor()
table, _ = tables[None]
name, operator, ids = domain
red_sql = reduce_ids(table.id, (i for i in ids if i is not None))
Target = self.get_target()
path_column = getattr(Target, self.path).sql_column(table)
path_column = Coalesce(path_column, '')
cursor.execute(*table.select(path_column, where=red_sql))
if operator.endswith('child_of'):
where = Or()
for path, in cursor:
where.append(path_column.like(path + '%'))
else:
ids = [int(x) for path, in cursor for x in path.split('/')[:-1]]
where = reduce_ids(table.id, ids)
if not where:
where = Literal(False)
if operator.startswith('not'):
return ~where
return where
def convert_domain_mptt(self, domain, tables):
cursor = Transaction().connection.cursor()
table, _ = tables[None]
name, operator, ids = domain
red_sql = reduce_ids(table.id, (i for i in ids if i is not None))
Target = self.get_target()
left = getattr(Target, self.left).sql_column(table)
right = getattr(Target, self.right).sql_column(table)
cursor.execute(*table.select(left, right, where=red_sql))
where = Or()
for l, r in cursor:
if operator.endswith('child_of'):
where.append((left >= l) & (right <= r))
else:
where.append((left <= l) & (right >= r))
if not where:
where = Literal(False)
if operator.startswith('not'):
return ~where
return where
def convert_domain_tree(self, domain, tables):
Target = self.get_target()
target = Target.__table__()
table, _ = tables[None]
name, operator, ids = domain
red_sql = reduce_ids(target.id, (i for i in ids if i is not None))
if operator.endswith('child_of'):
tree = With('id', recursive=True)
tree.query = target.select(target.id, where=red_sql)
tree.query |= (target
.join(tree, condition=Column(target, name) == tree.id)
.select(target.id))
else:
tree = With('id', name, recursive=True)
tree.query = target.select(
target.id, Column(target, name), where=red_sql)
tree.query |= (target
.join(tree, condition=target.id == Column(tree, name))
.select(target.id, Column(target, name)))
expression = table.id.in_(tree.select(tree.id, with_=[tree]))
if operator.startswith('not'):
return ~expression
return expression
@inactive_records
@domain_method
def convert_domain(self, domain, tables, Model):
pool = Pool()
Rule = pool.get('ir.rule')
Target = self.get_target()
table, _ = tables[None]
name, operator, value = domain[:3]
column = self.sql_column(table)
if '.' not in name:
if operator.endswith('child_of') or operator.endswith('parent_of'):
if Target != Model:
if operator.endswith('child_of'):
target_operator = 'child_of'
else:
target_operator = 'parent_of'
query = Target.search([
(domain[3], target_operator, value),
], order=[], query=True)
expression = column.in_(query)
if operator.startswith('not'):
return ~expression
return expression
if isinstance(value, str):
targets = Target.search([('rec_name', 'ilike', value)],
order=[])
ids = [t.id for t in targets]
elif not isinstance(value, (list, tuple)):
ids = [value]
else:
ids = value
if not ids:
expression = Literal(False)
if operator.startswith('not'):
return ~expression
return expression
elif self.left and self.right:
return self.convert_domain_mptt(
(name, operator, ids), tables)
elif self.path:
return self.convert_domain_path(
(name, operator, ids), tables)
else:
return self.convert_domain_tree(
(name, operator, ids), tables)
# Used for Many2Many where clause
if operator.endswith('where'):
query = Target.search(value, order=[], query=True)
target_id, = query.columns
if isinstance(target_id, As):
target_id = target_id.expression
query.where &= target_id == column
expression = column.in_(query)
if operator.startswith('not'):
return ~expression
return expression
if not isinstance(value, str):
return super(Many2One, self).convert_domain(domain, tables,
Model)
else:
target_name = 'rec_name'
else:
_, target_name = name.split('.', 1)
target_domain = [(target_name,) + tuple(domain[1:])]
rule_domain = Rule.domain_get(Target.__name__, mode='read')
if not rule_domain and target_name == 'id':
# No need to join with the target table
return super().convert_domain(
(self.name, operator, value), tables, Model)
elif Target.estimated_count() < _subquery_threshold:
query = Target.search(target_domain, order=[], query=True)
return column.in_(query)
else:
target_domain = [target_domain, rule_domain]
target_tables = self._get_target_tables(tables)
target_table, _ = target_tables[None]
_, expression = Target.search_domain(
target_domain, tables=target_tables)
return expression
@order_method
def convert_order(self, name, tables, Model):
fname, _, oexpr = name.partition('.')
Target = self.get_target()
if oexpr:
oname, _, _ = oexpr.partition('.')
else:
oname = 'id'
if (Target._rec_name in Target._fields
and Target._fields[Target._rec_name].sortable(Target)):
oname = Target._rec_name
if (Target._order_name in Target._fields
and Target._fields[Target._order_name].sortable(Target)):
oname = Target._order_name
oexpr = oname
table, _ = tables[None]
if oname == 'id':
return [self.sql_column(table)]
ofield = Target._fields[oname]
target_tables = self._get_target_tables(tables)
return ofield.convert_order(oexpr, target_tables, Target)
def _get_target_tables(self, tables):
Target = self.get_target()
table, _ = tables[None]
target_tables = tables.get(self.name)
context = Transaction().context
if target_tables is None:
if Target._history and context.get('_datetime'):
target = Target.__table_history__()
target_history = Target.__table_history__()
history_condition = Column(target, '__id').in_(
target_history.select(
Max(Column(target_history, '__id')),
where=Coalesce(
target_history.write_date,
target_history.create_date)
<= context['_datetime'],
group_by=target_history.id))
else:
target = Target.__table__()
history_condition = None
condition = target.id == self.sql_column(table)
if history_condition:
condition &= history_condition
target_tables = {
None: (target, condition),
}
tables[self.name] = target_tables
return target_tables
def definition(self, model, language):
encoder = PYSONEncoder()
target = self.get_target()
relation_fields = [fname for fname, field in target._fields.items()
if field._type == 'one2many'
and field.model_name == model.__name__
and field.field == self.name]
definition = super().definition(model, language)
definition['datetime_field'] = self.datetime_field
definition['relation'] = target.__name__
if len(relation_fields) == 1:
definition['relation_field'], = relation_fields
definition['search_context'] = encoder.encode(self.search_context)
definition['search_order'] = encoder.encode(self.search_order)
return definition

111
model/fields/multiselection.py Executable file
View File

@@ -0,0 +1,111 @@
# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
# repository contains the full copyright notices and license terms.
import json
from functools import partial
from sql import Literal, operators
from trytond.rpc import RPC
from trytond.transaction import Transaction
from .field import Field, domain_method
from .selection import SelectionMixin
# Use canonical form
dumps = partial(json.dumps, separators=(',', ':'), sort_keys=True)
class MultiSelection(SelectionMixin, Field):
"Define a multi-selection field."
_type = 'multiselection'
_sql_type = 'VARCHAR'
_py_type = tuple
def __init__(self, selection, string='', sort=True, translate=True,
help='', help_selection=None, required=False, readonly=False,
domain=None, states=None, on_change=None,
on_change_with=None, depends=None, context=None, loading='eager'):
"""
:param selection: A list or a function name that returns a list.
The list must be a list of tuples. First member is the value
to store and the second is the value to display.
:param sort: A boolean to sort or not the selections.
"""
super().__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,
depends=depends, context=context, loading=loading)
if hasattr(selection, 'copy'):
self.selection = selection.copy()
else:
self.selection = selection
self.selection_change_with = set()
self.sort = sort
self.translate_selection = translate
self.help_selection = help_selection
__init__.__doc__ += Field.__init__.__doc__
def set_rpc(self, model):
super().set_rpc(model)
if not isinstance(self.selection, (list, tuple)):
assert hasattr(model, self.selection), \
'Missing %s on model %s' % (self.selection, model.__name__)
instantiate = 0 if self.selection_change_with else None
cache = dict(days=1) if instantiate is None else None
model.__rpc__.setdefault(
self.selection, RPC(instantiate=instantiate, cache=cache))
def get(self, ids, model, name, values=None):
lists = {id: () for id in ids}
for value in values or []:
data = value[name]
if data:
# If stored as JSON conversion is done on backend
if isinstance(data, str):
data = json.loads(data)
lists[value['id']] = tuple(data)
return lists
def sql_format(self, value):
value = super().sql_format(value)
if isinstance(value, (list, tuple)):
value = dumps(sorted(set(value)))
return value
def _domain_column(self, operator, column):
database = Transaction().database
return database.json_get(super()._domain_column(operator, column))
def _domain_value(self, operator, value):
database = Transaction().database
domain_value = super()._domain_value(operator, value)
if value is not None:
domain_value = database.json_get(domain_value)
return domain_value
@domain_method
def convert_domain(self, domain, tables, Model):
name, operator, value = domain[:3]
if operator not in {'in', 'not in'}:
return super().convert_domain(domain, tables, Model)
database = Transaction().database
table, _ = tables[None]
raw_column = self.sql_column(table)
if isinstance(value, str):
try:
expression = database.json_key_exists(raw_column, value)
except NotImplementedError:
expression = operators.Like(
raw_column, '%' + dumps(value) + '%')
else:
try:
expression = database.json_any_keys_exist(
raw_column, list(value))
except NotImplementedError:
expression = Literal(False)
for item in value:
expression |= operators.Like(
raw_column, '%' + dumps(item) + '%')
if operator == 'not in':
expression = operators.Not(expression)
return expression

47
model/fields/numeric.py Executable file
View File

@@ -0,0 +1,47 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from sql import Cast, CombiningQuery, Literal, Select
from trytond import backend
from .field import order_method
from .float import Float
class Numeric(Float):
'''
Define a numeric field (``decimal``).
'''
_type = 'numeric'
_sql_type = 'NUMERIC'
_py_type = Decimal
@order_method
def convert_order(self, name, tables, Model):
columns = super().convert_order(name, tables, Model)
if backend.name == 'sqlite':
# Must be cast because Decimal is stored as bytes
columns = [Cast(c, self.sql_type().base) for c in columns]
return columns
def _domain_column(self, operator, column):
column = super()._domain_column(operator, column)
if backend.name == 'sqlite':
# Must be casted as Decimal is stored as bytes
column = Cast(column, self.sql_type().base)
return column
def _domain_value(self, operator, value):
value = super(Numeric, self)._domain_value(operator, value)
if backend.name == 'sqlite':
if isinstance(value, (Select, CombiningQuery)):
return value
# Must be casted as Decimal is adapted to bytes
type_ = self.sql_type().base
if operator in ('in', 'not in'):
return [Cast(Literal(v), type_) for v in value]
elif value is not None:
return Cast(Literal(value), type_)
return value

Some files were not shown because too many files have changed in this diff Show More