Initial import from Docker volume
This commit is contained in:
26
model/__init__.py
Executable file
26
model/__init__.py
Executable 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']
|
||||
BIN
model/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/__init__.cpython-311.pyc
Executable file
BIN
model/__pycache__/__init__.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/active.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/active.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/active.cpython-311.pyc
Executable file
BIN
model/__pycache__/active.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/avatar.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/avatar.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/avatar.cpython-311.pyc
Executable file
BIN
model/__pycache__/avatar.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/descriptors.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/descriptors.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/descriptors.cpython-311.pyc
Executable file
BIN
model/__pycache__/descriptors.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/dictschema.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/dictschema.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/dictschema.cpython-311.pyc
Executable file
BIN
model/__pycache__/dictschema.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/digits.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/digits.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/digits.cpython-311.pyc
Executable file
BIN
model/__pycache__/digits.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/exceptions.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/exceptions.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/exceptions.cpython-311.pyc
Executable file
BIN
model/__pycache__/exceptions.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/match.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/match.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/match.cpython-311.pyc
Executable file
BIN
model/__pycache__/match.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/model.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/model.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/model.cpython-311.pyc
Executable file
BIN
model/__pycache__/model.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/modelsingleton.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/modelsingleton.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/modelsingleton.cpython-311.pyc
Executable file
BIN
model/__pycache__/modelsingleton.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/modelsql.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/modelsql.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/modelsql.cpython-311.pyc
Executable file
BIN
model/__pycache__/modelsql.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/modelstorage.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/modelstorage.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/modelstorage.cpython-311.pyc
Executable file
BIN
model/__pycache__/modelstorage.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/modelview.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/modelview.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/modelview.cpython-311.pyc
Executable file
BIN
model/__pycache__/modelview.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/multivalue.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/multivalue.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/multivalue.cpython-311.pyc
Executable file
BIN
model/__pycache__/multivalue.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/order.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/order.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/order.cpython-311.pyc
Executable file
BIN
model/__pycache__/order.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/symbol.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/symbol.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/symbol.cpython-311.pyc
Executable file
BIN
model/__pycache__/symbol.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/tree.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/tree.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/tree.cpython-311.pyc
Executable file
BIN
model/__pycache__/tree.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/union.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/union.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/union.cpython-311.pyc
Executable file
BIN
model/__pycache__/union.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/workflow.cpython-311.opt-1.pyc
Executable file
BIN
model/__pycache__/workflow.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/__pycache__/workflow.cpython-311.pyc
Executable file
BIN
model/__pycache__/workflow.cpython-311.pyc
Executable file
Binary file not shown.
43
model/active.py
Executable file
43
model/active.py
Executable 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
93
model/avatar.py
Executable 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
34
model/descriptors.py
Executable 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
244
model/dictschema.py
Executable 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
34
model/digits.py
Executable 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
29
model/exceptions.py
Executable 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
32
model/fields/__init__.py
Executable 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]
|
||||
BIN
model/fields/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/__init__.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/__init__.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/binary.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/binary.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/binary.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/binary.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/boolean.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/boolean.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/boolean.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/boolean.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/char.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/char.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/char.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/char.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/date.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/date.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/date.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/date.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/dict.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/dict.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/dict.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/dict.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/field.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/field.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/field.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/field.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/float.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/float.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/float.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/float.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/fmany2one.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/fmany2one.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/fmany2one.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/fmany2one.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/function.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/function.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/function.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/function.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/integer.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/integer.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/integer.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/integer.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/many2many.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/many2many.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/many2many.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/many2many.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/many2one.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/many2one.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/many2one.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/many2one.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/multiselection.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/multiselection.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/multiselection.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/multiselection.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/numeric.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/numeric.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/numeric.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/numeric.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/one2many.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/one2many.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/one2many.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/one2many.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/one2one.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/one2one.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/one2one.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/one2one.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/reference.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/reference.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/reference.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/reference.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/selection.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/selection.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/selection.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/selection.cpython-311.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/text.cpython-311.opt-1.pyc
Executable file
BIN
model/fields/__pycache__/text.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
model/fields/__pycache__/text.cpython-311.pyc
Executable file
BIN
model/fields/__pycache__/text.cpython-311.pyc
Executable file
Binary file not shown.
133
model/fields/binary.py
Executable file
133
model/fields/binary.py
Executable 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
38
model/fields/boolean.py
Executable 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
235
model/fields/char.py
Executable 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
215
model/fields/date.py
Executable 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
221
model/fields/dict.py
Executable 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
727
model/fields/field.py
Executable 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
79
model/fields/float.py
Executable 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
92
model/fields/fmany2one.py
Executable 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
249
model/fields/function.py
Executable 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
12
model/fields/integer.py
Executable 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
505
model/fields/many2many.py
Executable 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
354
model/fields/many2one.py
Executable 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
111
model/fields/multiselection.py
Executable 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
47
model/fields/numeric.py
Executable 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
Reference in New Issue
Block a user