Add MultiValueMixin and ValueMixin and remove Propery
issue2349 review23351002
This commit is contained in:
1 parent
aa9c9cc9e7
commit
a908e8cc74
25 files changed
+447
-806
No files matched your search
@@ -1,3 +1,5 @@
|
||||
* Remove Property field
|
||||
* Add MultiValueMixin and ValueMixin
|
||||
* Use sql type in column creation
|
||||
* Use generic SQL type in field and let backend determine the SQLType
|
||||
* Add filter to xxx2Many fields
|
||||
|
||||
+15
-19
@@ -819,29 +819,25 @@ Instance methods:
|
||||
:class:`~trytond.model.Model` instance of the field, `name` is the name of
|
||||
the field, `clause` is a clause of :ref:`domain <topics-domain>`.
|
||||
|
||||
Property
|
||||
--------
|
||||
MultiValue
|
||||
----------
|
||||
|
||||
.. class:: Property(field)
|
||||
.. class:: MultiValue(field)
|
||||
|
||||
A property field that is like a :class:`Function` field but with predifined
|
||||
:attr:`~Function.getter`, :attr:`~Function.setter` and
|
||||
:attr:`~Function.searcher` that use the :class:`~trytond.model.ModelSQL`
|
||||
`ir.property` to store values.
|
||||
A multivalue field that is like a :class:`Function` field but with predefined
|
||||
:attr:`~Function.getter` and :attr:`~Function.setter` that use the
|
||||
:class:`~trytond.model.MultiValueMixin` for stored values.
|
||||
|
||||
Instance methods:
|
||||
.. warning::
|
||||
The :meth:`~trytond.model.MultiValueMixin.get_multivalue` and
|
||||
:meth:`~trytond.model.MultiValueMixin.set_multivalue` should be prefered
|
||||
over the descriptors of the field.
|
||||
..
|
||||
|
||||
.. method:: Property.get(ids, model, name[, values])
|
||||
|
||||
Same as :meth:`Function.get`.
|
||||
|
||||
.. method:: Property.set(ids, model, name, value)
|
||||
|
||||
Same as :meth:`Function.set`.
|
||||
|
||||
.. method:: Property.search(model, name, clause)
|
||||
|
||||
Same as :meth:`Function.search`.
|
||||
.. warning::
|
||||
The :ref:`default <topics-fields_default_value>` method of the field must
|
||||
accept pattern as keyword argument.
|
||||
..
|
||||
|
||||
Dict
|
||||
----
|
||||
|
||||
@@ -647,9 +647,10 @@ record is equal or not defined.
|
||||
|
||||
Instance methods:
|
||||
|
||||
.. method:: MatchMixin.match(pattern)
|
||||
.. method:: MatchMixin.match(pattern[, match_none])
|
||||
|
||||
Return if the instance match the pattern
|
||||
Return if the instance match the pattern. If `match_none` is set `None`
|
||||
value of the instance will be compared.
|
||||
|
||||
==========
|
||||
UnionMixin
|
||||
@@ -697,6 +698,60 @@ the field to be created and its default values is `sequence`. field_label
|
||||
defines the label which will be used by the field and defaults to `Sequence`.
|
||||
Order specifies the order direction and defaults to `ASC NULLS FIRST`.
|
||||
|
||||
===============
|
||||
MultiValueMixin
|
||||
===============
|
||||
|
||||
.. class:: MultiValueMixin
|
||||
|
||||
A mixin_ for :class:`Model` to help having
|
||||
:class:`trytond.model.fields.MultiValue` fields with multi-values on a
|
||||
:class:`ValueMixin`. The values are stored by creating one record per pattern.
|
||||
The patterns are the same as those on :class:`MatchMixin`.
|
||||
|
||||
Class methods:
|
||||
|
||||
.. classmethod:: MultiValueMixin.multivalue_model(field)
|
||||
|
||||
Return the :class:`ValueMixin` on which the values are stored for the
|
||||
field name. The default is class name suffixed by the field name.
|
||||
|
||||
.. classmethod:: MultiValueMixin.setter_multivalue(records, name, value, \*\*pattern)
|
||||
|
||||
The setter method for the :class:`trytond.model.fields.Function` fields.
|
||||
|
||||
Instance methods:
|
||||
|
||||
.. method:: MultiValueMixin.multivalue_records(field)
|
||||
|
||||
Return the list of all :class:`ValueMixin` records linked to the instance.
|
||||
By default, it returns the value of the first found
|
||||
:class:`trytond.model.fields.One2Many` linked to the multivalue model or
|
||||
all the records of this one.
|
||||
|
||||
.. method:: MultiValueMixin.multivalue_record(field, \*\*pattern)
|
||||
|
||||
Return a new record of :class:`ValueMixin` linked to the instance.
|
||||
|
||||
.. method:: MultiValueMixin.get_multivalue(name, \*\*pattern)
|
||||
|
||||
Return the value of the field `name` for the pattern.
|
||||
|
||||
.. method:: MultiValueMixin.set_multivalue(name, value, \*\*pattern)
|
||||
|
||||
Store the value of the field `name` for the pattern.
|
||||
|
||||
.. warning::
|
||||
To customize the pattern, both methods must be override the same way.
|
||||
..
|
||||
|
||||
==========
|
||||
ValueMixin
|
||||
==========
|
||||
|
||||
.. class:: ValueMixin
|
||||
|
||||
A mixin_ to store the values of :class:`MultiValueMixin`.
|
||||
|
||||
.. _mixin: http://en.wikipedia.org/wiki/Mixin
|
||||
.. _JSON: http://en.wikipedia.org/wiki/Json
|
||||
|
||||
@@ -7,7 +7,6 @@ from .sequence import *
|
||||
from .ui.menu import *
|
||||
from .ui.view import *
|
||||
from .ui.icon import *
|
||||
from .property import *
|
||||
from .action import *
|
||||
from .model import *
|
||||
from .attachment import *
|
||||
@@ -45,7 +44,6 @@ def register():
|
||||
ViewTreeState,
|
||||
ViewSearch,
|
||||
Icon,
|
||||
Property,
|
||||
Action,
|
||||
ActionKeyword,
|
||||
ActionReport,
|
||||
|
||||
@@ -77,8 +77,6 @@ class Model(ModelSQL, ModelView):
|
||||
|
||||
@classmethod
|
||||
def register(cls, model, module_name):
|
||||
pool = Pool()
|
||||
Property = pool.get('ir.property')
|
||||
cursor = Transaction().connection.cursor()
|
||||
|
||||
ir_model = cls.__table__()
|
||||
@@ -97,7 +95,6 @@ class Model(ModelSQL, ModelView):
|
||||
ir_model.module],
|
||||
[[model.__name__, model._get_name(), model.__doc__,
|
||||
module_name]]))
|
||||
Property._models_get_cache.clear()
|
||||
cursor.execute(*ir_model.select(ir_model.id,
|
||||
where=ir_model.model == model.__name__))
|
||||
(model_id,) = cursor.fetchone()
|
||||
@@ -124,31 +121,6 @@ class Model(ModelSQL, ModelView):
|
||||
return [name for name, model in Pool().iterobject()
|
||||
if getattr(model, '_history', False)]
|
||||
|
||||
@classmethod
|
||||
def create(cls, vlist):
|
||||
pool = Pool()
|
||||
Property = pool.get('ir.property')
|
||||
res = super(Model, cls).create(vlist)
|
||||
# Restart the cache of models_get
|
||||
Property._models_get_cache.clear()
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def write(cls, models, values, *args):
|
||||
pool = Pool()
|
||||
Property = pool.get('ir.property')
|
||||
super(Model, cls).write(models, values, *args)
|
||||
# Restart the cache of models_get
|
||||
Property._models_get_cache.clear()
|
||||
|
||||
@classmethod
|
||||
def delete(cls, models):
|
||||
pool = Pool()
|
||||
Property = pool.get('ir.property')
|
||||
super(Model, cls).delete(models)
|
||||
# Restart the cache of models_get
|
||||
Property._models_get_cache.clear()
|
||||
|
||||
@classmethod
|
||||
def global_search(cls, text, limit, menu='ir.ui.menu'):
|
||||
"""
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
# 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 ..model import ModelView, ModelSQL, fields
|
||||
from ..transaction import Transaction
|
||||
from ..cache import Cache
|
||||
from ..pool import Pool
|
||||
|
||||
__all__ = [
|
||||
'Property',
|
||||
]
|
||||
|
||||
_CAST = {
|
||||
'numeric': Decimal,
|
||||
'integer': int,
|
||||
'float': float,
|
||||
}
|
||||
|
||||
|
||||
class Property(ModelSQL, ModelView):
|
||||
"Property"
|
||||
__name__ = 'ir.property'
|
||||
_rec_name = 'field'
|
||||
value = fields.Reference('Value', selection='models_get')
|
||||
res = fields.Reference('Resource', selection='models_get', select=True)
|
||||
field = fields.Many2One('ir.model.field', 'Field',
|
||||
ondelete='CASCADE', required=True, select=True)
|
||||
_models_get_cache = Cache('ir_property.models_get', context=False)
|
||||
|
||||
@classmethod
|
||||
def models_get(cls):
|
||||
pool = Pool()
|
||||
Model = pool.get('ir.model')
|
||||
models = cls._models_get_cache.get(None)
|
||||
if models:
|
||||
return models
|
||||
cursor = Transaction().connection.cursor()
|
||||
model = Model.__table__()
|
||||
cursor.execute(*model.select(model.model, model.name,
|
||||
order_by=model.name.asc))
|
||||
models = cursor.fetchall() + [('', '')]
|
||||
cls._models_get_cache.set(None, models)
|
||||
return models
|
||||
|
||||
@classmethod
|
||||
def get(cls, names, model, res_ids=None):
|
||||
"""
|
||||
Return named property values for each res_ids of model
|
||||
"""
|
||||
pool = Pool()
|
||||
ModelAccess = pool.get('ir.model.access')
|
||||
res = {}
|
||||
|
||||
ModelAccess.check(model, 'read')
|
||||
|
||||
names_list = True
|
||||
if not isinstance(names, list):
|
||||
names_list = False
|
||||
names = [names]
|
||||
if res_ids is None:
|
||||
res_ids = []
|
||||
|
||||
properties = cls.search([
|
||||
('field.name', 'in', names),
|
||||
['OR',
|
||||
('res', '=', None),
|
||||
('res', 'in', ['%s,%s' % (model, x) for x in res_ids]),
|
||||
],
|
||||
], order=[])
|
||||
|
||||
default_vals = dict((x, None) for x in names)
|
||||
for property_ in (x for x in properties if not x.res):
|
||||
value = property_.value
|
||||
val = None
|
||||
if value is not None:
|
||||
if not isinstance(value, basestring):
|
||||
val = int(value)
|
||||
else:
|
||||
if property_.field.ttype in _CAST:
|
||||
cast = _CAST[property_.field.ttype]
|
||||
val = cast(value.split(',')[1])
|
||||
elif property_.field.ttype in ('char', 'selection'):
|
||||
val = value.split(',')[1]
|
||||
else:
|
||||
raise Exception('Not implemented')
|
||||
default_vals[property_.field.name] = val
|
||||
|
||||
if not res_ids:
|
||||
if not names_list:
|
||||
return default_vals[names[0]]
|
||||
return default_vals
|
||||
|
||||
for name in names:
|
||||
res[name] = dict((x, default_vals[name]) for x in res_ids)
|
||||
|
||||
for property_ in (x for x in properties if x.res):
|
||||
val = None
|
||||
if property_.value is not None:
|
||||
if not isinstance(property_.value, basestring):
|
||||
val = int(property_.value)
|
||||
else:
|
||||
if property_.field.ttype in _CAST:
|
||||
cast = _CAST[property_.field.ttype]
|
||||
val = cast(property_.value.split(',')[1])
|
||||
elif property_.field.ttype in ('char', 'selection'):
|
||||
val = property_.value.split(',')[1]
|
||||
else:
|
||||
raise Exception('Not implemented')
|
||||
res[property_.field.name][int(property_.res)] = val
|
||||
|
||||
if not names_list:
|
||||
return res[names[0]]
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _set_values(model, res_id, val, field_id):
|
||||
return {
|
||||
'value': val,
|
||||
'res': model + ',' + str(res_id),
|
||||
'field': field_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def set(cls, name, model, ids, val):
|
||||
"""
|
||||
Set named property value for ids of model
|
||||
Return the id of the record created
|
||||
"""
|
||||
pool = Pool()
|
||||
ModelField = pool.get('ir.model.field')
|
||||
ModelAccess = pool.get('ir.model.access')
|
||||
|
||||
ModelAccess.check(model, 'write')
|
||||
|
||||
model_field, = ModelField.search([
|
||||
('name', '=', name),
|
||||
('model.model', '=', model),
|
||||
], order=[], limit=1)
|
||||
Model = pool.get(model)
|
||||
field = Model._fields[name]
|
||||
|
||||
properties = cls.search([
|
||||
('field', '=', model_field.id),
|
||||
('res', 'in', [model + ',' + str(res_id) for res_id in ids]),
|
||||
], order=[])
|
||||
cls.delete(properties)
|
||||
|
||||
defaults = cls.search([
|
||||
('field', '=', model_field.id),
|
||||
('res', '=', None),
|
||||
], order=[], limit=1)
|
||||
default_val = None
|
||||
if defaults:
|
||||
value = cls(defaults[0].id).value
|
||||
default_val = None
|
||||
if value is not None:
|
||||
if not isinstance(value, basestring):
|
||||
default_val = int(value)
|
||||
else:
|
||||
if field._type in _CAST:
|
||||
cast = _CAST[field._type]
|
||||
default_val = cast(value.split(',')[1])
|
||||
elif field._type in ('char', 'selection'):
|
||||
default_val = value.split(',')[1]
|
||||
else:
|
||||
raise Exception('Not implemented')
|
||||
|
||||
if (val != default_val):
|
||||
to_create = []
|
||||
for res_id in ids:
|
||||
vals = cls._set_values(model, res_id, val, model_field.id)
|
||||
to_create.append(vals)
|
||||
cls.create(to_create)
|
||||
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="property_view_form">
|
||||
<field name="model">ir.property</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">property_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="property_view_tree">
|
||||
<field name="model">ir.property</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">property_list</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_property_form_default">
|
||||
<field name="name">Default Properties</field>
|
||||
<field name="type">ir.action.act_window</field>
|
||||
<field name="res_model">ir.property</field>
|
||||
<field name="domain" eval="[('res', '=', None)]" pyson="1"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_property_form_view1_default">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view" ref="property_view_tree"/>
|
||||
<field name="act_window" ref="act_property_form_default"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_property_form_view2_default">
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="view" ref="property_view_form"/>
|
||||
<field name="act_window" ref="act_property_form_default"/>
|
||||
</record>
|
||||
<menuitem parent="ir.menu_models"
|
||||
action="act_property_form_default" id="menu_property_form_default"/>
|
||||
<record model="ir.action.act_window" id="act_property_form">
|
||||
<field name="name">Properties</field>
|
||||
<field name="type">ir.action.act_window</field>
|
||||
<field name="res_model">ir.property</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_property_form_view1">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view" ref="property_view_tree"/>
|
||||
<field name="act_window" ref="act_property_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view"
|
||||
id="act_property_form_view2">
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="view" ref="property_view_form"/>
|
||||
<field name="act_window" ref="act_property_form"/>
|
||||
</record>
|
||||
<menuitem parent="ir.menu_models"
|
||||
action="act_property_form" id="menu_property_form"/>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -15,6 +15,5 @@ xml:
|
||||
translation.xml
|
||||
export.xml
|
||||
rule.xml
|
||||
property.xml
|
||||
module.xml
|
||||
trigger.xml
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="field"/>
|
||||
<field name="field" colspan="3"/>
|
||||
<label name="value"/>
|
||||
<field name="value" colspan="3"/>
|
||||
<label name="res"/>
|
||||
<field name="res" colspan="3"/>
|
||||
</form>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="field"/>
|
||||
<field name="value"/>
|
||||
<field name="res"/>
|
||||
</tree>
|
||||
@@ -9,10 +9,12 @@ from .workflow import Workflow
|
||||
from .dictschema import DictSchemaMixin
|
||||
from .match import MatchMixin
|
||||
from .union import UnionMixin
|
||||
from .multivalue import MultiValueMixin, ValueMixin
|
||||
from .descriptors import dualmethod
|
||||
from .order import sequence_ordered
|
||||
|
||||
__all__ = ['Model', 'ModelView', 'ModelStorage', 'ModelSingleton', 'ModelSQL',
|
||||
'Check', 'Unique',
|
||||
'Workflow', 'DictSchemaMixin', 'MatchMixin', 'UnionMixin', 'dualmethod',
|
||||
'MultiValueMixin', 'ValueMixin',
|
||||
'EvalEnvironment', 'sequence_ordered']
|
||||
@@ -16,6 +16,5 @@ from .many2one import *
|
||||
from .one2many import *
|
||||
from .many2many import *
|
||||
from .function import *
|
||||
from .property import *
|
||||
from .one2one import *
|
||||
from .dict import *
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import inspect
|
||||
import copy
|
||||
|
||||
from trytond.model.fields.field import Field
|
||||
from trytond.tools import is_instance_method
|
||||
from trytond.transaction import Transaction
|
||||
@@ -116,3 +117,18 @@ class Function(Field):
|
||||
|
||||
def __set__(self, inst, value):
|
||||
self._field.__set__(inst, value)
|
||||
|
||||
|
||||
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)
|
||||
@@ -1,183 +0,0 @@
|
||||
# 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
|
||||
|
||||
from sql import Cast, Literal, Null
|
||||
from sql.functions import Substring, Position
|
||||
from sql.conditionals import Case
|
||||
|
||||
from .function import Function
|
||||
from .field import Field, SQL_OPERATORS
|
||||
from .numeric import Numeric
|
||||
from .integer import Integer
|
||||
from .float import Float
|
||||
from ...transaction import Transaction
|
||||
from ...pool import Pool
|
||||
|
||||
|
||||
class Property(Function):
|
||||
'''
|
||||
Define a property field that is stored in ir.property (any).
|
||||
'''
|
||||
|
||||
def __init__(self, field):
|
||||
'''
|
||||
:param field: The field of the function.
|
||||
'''
|
||||
super(Property, self).__init__(field, True, True, True)
|
||||
|
||||
__init__.__doc__ += Field.__init__.__doc__
|
||||
|
||||
def __copy__(self):
|
||||
return Property(copy.copy(self._field))
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return Property(copy.deepcopy(self._field))
|
||||
|
||||
def get(self, ids, model, name, values=None):
|
||||
'''
|
||||
Retreive the property.
|
||||
|
||||
:param ids: A list of ids.
|
||||
:param model: The model.
|
||||
:param name: The name of the field or a list of name field.
|
||||
:param values:
|
||||
:return: a dictionary with ids as key and values as value
|
||||
'''
|
||||
with Transaction().set_context(_check_access=False):
|
||||
pool = Pool()
|
||||
Property = pool.get('ir.property')
|
||||
return Property.get(name, model.__name__, ids)
|
||||
|
||||
def set(self, Model, name, ids, value, *args):
|
||||
'''
|
||||
Set the property.
|
||||
'''
|
||||
with Transaction().set_context(_check_access=False):
|
||||
pool = Pool()
|
||||
Property = pool.get('ir.property')
|
||||
args = iter((ids, value) + args)
|
||||
for ids, value in zip(args, args):
|
||||
if value is not None:
|
||||
prop_value = '%s,%s' % (getattr(self, 'model_name', ''),
|
||||
str(value))
|
||||
else:
|
||||
prop_value = None
|
||||
# TODO change set API to use sequence of records, value
|
||||
Property.set(name, Model.__name__, ids, prop_value)
|
||||
|
||||
def convert_domain(self, domain, tables, Model):
|
||||
pool = Pool()
|
||||
Rule = pool.get('ir.rule')
|
||||
Property = pool.get('ir.property')
|
||||
IrModel = pool.get('ir.model')
|
||||
Field = pool.get('ir.model.field')
|
||||
cursor = Transaction().connection.cursor()
|
||||
|
||||
name, operator, value = domain
|
||||
|
||||
sql_type = self._field.sql_type().base
|
||||
|
||||
property_cond = Rule.query_get('ir.property')
|
||||
|
||||
property_ = Property.__table__()
|
||||
model_field = Field.__table__()
|
||||
model = IrModel.__table__()
|
||||
|
||||
# Fetch res ids that comply with the domain
|
||||
join = property_.join(model_field,
|
||||
condition=model_field.id == property_.field)
|
||||
join = join.join(model,
|
||||
condition=model.id == model_field.model)
|
||||
cond = ((model.model == Model.__name__)
|
||||
& (model_field.name == name))
|
||||
if property_cond:
|
||||
cond &= property_.id.in_(property_cond)
|
||||
cursor.execute(*join.select(
|
||||
Cast(Substring(property_.res,
|
||||
Position(',', property_.res) + Literal(1)),
|
||||
Model.id.sql_type().base),
|
||||
property_.id,
|
||||
# Use a Case because the condition created by get_condition
|
||||
# could result in an invalid Cast
|
||||
where=Case(
|
||||
(cond, self.get_condition(sql_type, domain, property_)),
|
||||
else_=(Literal(1) == Literal(0)))))
|
||||
|
||||
props = cursor.fetchall()
|
||||
default = None
|
||||
for prop in props:
|
||||
if not prop[0]:
|
||||
default = prop[1]
|
||||
break
|
||||
|
||||
if (not default
|
||||
or ((value is False or value is None)
|
||||
and operator in ['=', '!='])
|
||||
or (operator in ['not like', 'not ilike', 'not in', '!='])):
|
||||
dom_operator = 'in' # default operator
|
||||
if (((value is False or value is None) and operator == '=')
|
||||
or ((value is not False and value is not None)
|
||||
and operator in [
|
||||
'not like', 'not ilike', 'not in', '!='])):
|
||||
dom_operator = 'not in'
|
||||
return [('id', dom_operator, [x[0] for x in props])]
|
||||
|
||||
# Fetch the res ids that doesn't use the default value
|
||||
cursor.execute(*property_.select(
|
||||
Cast(Substring(property_.res,
|
||||
Position(',', property_.res) + Literal(1)),
|
||||
Model.id.sql_type().base),
|
||||
where=property_cond & (property_.res != Null)))
|
||||
|
||||
fetchall = cursor.fetchall()
|
||||
if not fetchall:
|
||||
return [('id', 'in', [x[0] for x in props])]
|
||||
|
||||
else:
|
||||
other_ids = [x[0] for x in fetchall]
|
||||
|
||||
res_ids = Model.search(['OR',
|
||||
('id', 'in', [x[0] for x in props]),
|
||||
('id', 'not in', other_ids)
|
||||
])
|
||||
|
||||
return [('id', 'in', res_ids)]
|
||||
|
||||
@staticmethod
|
||||
def get_condition(sql_type, clause, table):
|
||||
operator = clause[1]
|
||||
value = clause[2]
|
||||
|
||||
sql_types = [
|
||||
Numeric('numeric').sql_type().base,
|
||||
Integer('integer').sql_type().base,
|
||||
Float('float').sql_type().base,
|
||||
]
|
||||
if sql_type in sql_types and value:
|
||||
if isinstance(value, (list, tuple)):
|
||||
value = [Cast(v, sql_type) for v in value]
|
||||
else:
|
||||
value = Cast(value, sql_type)
|
||||
|
||||
if value is None:
|
||||
value = False
|
||||
|
||||
column = Cast(Substring(table.value,
|
||||
Position(',', table.value) + Literal(1)),
|
||||
sql_type)
|
||||
Operator = SQL_OPERATORS[operator]
|
||||
|
||||
# All negative clauses will be negated later
|
||||
if operator in ('in', 'not in'):
|
||||
return column.in_(value)
|
||||
elif ((value is False or value is None)
|
||||
and operator in ('=', '!=')):
|
||||
return column != Null
|
||||
elif operator == 'not like':
|
||||
return column.like(value)
|
||||
elif operator == 'not ilike':
|
||||
return column.ilike(value)
|
||||
elif operator == '!=':
|
||||
return column == value
|
||||
return Operator(column, value)
|
||||
@@ -4,16 +4,16 @@
|
||||
|
||||
class MatchMixin(object):
|
||||
|
||||
def match(self, pattern):
|
||||
def match(self, pattern, match_none=False):
|
||||
'''Match on pattern
|
||||
pattern is a dictionary with model field as key
|
||||
and matching value as value'''
|
||||
for field, pattern_value in pattern.iteritems():
|
||||
value = getattr(self, field)
|
||||
if value is None:
|
||||
if not match_none and value is None:
|
||||
continue
|
||||
if self._fields[field]._type == 'many2one':
|
||||
value = value.id
|
||||
value = value.id if value else value
|
||||
if value != pattern_value:
|
||||
return False
|
||||
return True
|
||||
@@ -123,7 +123,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
|
||||
If with_rec_name is True, rec_name will be added.
|
||||
'''
|
||||
pool = Pool()
|
||||
Property = pool.get('ir.property')
|
||||
value = {}
|
||||
|
||||
default_rec_name = Transaction().context.get('default_rec_name')
|
||||
@@ -140,8 +139,6 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
|
||||
if (field._type == 'boolean'
|
||||
and field_name not in value):
|
||||
value[field_name] = False
|
||||
if isinstance(field, fields.Property):
|
||||
value[field_name] = Property.get(field_name, cls.__name__)
|
||||
if (with_rec_name
|
||||
and field._type in ('many2one',)
|
||||
and value.get(field_name)):
|
||||
@@ -243,10 +240,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
|
||||
if ((isinstance(cls._fields[field], fields.Function)
|
||||
and not (cls._fields[field].searcher
|
||||
or getattr(cls, 'domain_%s' % field, None)))
|
||||
or (cls._fields[field]._type in ('binary', 'sha'))
|
||||
or (isinstance(cls._fields[field], fields.Property)
|
||||
and isinstance(cls._fields[field]._field,
|
||||
fields.Many2One))):
|
||||
or (cls._fields[field]._type in ('binary', 'sha'))):
|
||||
res[field]['searchable'] = False
|
||||
else:
|
||||
res[field]['searchable'] = True
|
||||
|
||||
@@ -286,8 +286,8 @@ class ModelStorage(Model):
|
||||
if field_name in default:
|
||||
data[field_name] = default[field_name]
|
||||
elif (isinstance(cls._fields[field_name], fields.Function)
|
||||
and not isinstance(cls._fields[field_name],
|
||||
fields.Property)):
|
||||
and not isinstance(
|
||||
cls._fields[field_name], fields.MultiValue)):
|
||||
del data[field_name]
|
||||
elif ftype in ('many2one', 'one2one'):
|
||||
try:
|
||||
@@ -315,7 +315,7 @@ class ModelStorage(Model):
|
||||
mptt.add(field.right)
|
||||
fields_names = [n for n, f in cls._fields.iteritems()
|
||||
if (not isinstance(f, fields.Function)
|
||||
or isinstance(f, fields.Property))
|
||||
or isinstance(f, fields.MultiValue))
|
||||
and n not in mptt]
|
||||
ids = map(int, records)
|
||||
datas = cls.read(ids, fields_names=fields_names)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
|
||||
# repository contains the full copyright notices and license terms.
|
||||
from ..pool import Pool
|
||||
from .model import Model
|
||||
from .match import MatchMixin
|
||||
|
||||
|
||||
class MultiValueMixin(object):
|
||||
|
||||
@classmethod
|
||||
def multivalue_model(cls, field):
|
||||
pool = Pool()
|
||||
Value = pool.get('%s.%s' % (cls.__name__, field))
|
||||
assert issubclass(Value, ValueMixin), (
|
||||
"%s is not a subclass of ValueMixin" % Value)
|
||||
return Value
|
||||
|
||||
def multivalue_records(self, field):
|
||||
Value = self.multivalue_model(field)
|
||||
for fname, field in self._fields.iteritems():
|
||||
if (field._type == 'one2many'
|
||||
and field.model_name == Value.__name__):
|
||||
return getattr(self, fname)
|
||||
return Value.search([])
|
||||
|
||||
def multivalue_record(self, field, **pattern):
|
||||
Value = self.multivalue_model(field)
|
||||
for fname, field in Value._fields.iteritems():
|
||||
if (field._type == 'many2one'
|
||||
and field.model_name == self.__name__):
|
||||
pattern = pattern.copy()
|
||||
pattern[fname] = self
|
||||
break
|
||||
return Value(**pattern)
|
||||
|
||||
def __values(self, field, pattern, match_none=True):
|
||||
return [v for v in self.multivalue_records(field)
|
||||
if v.match(pattern, match_none=match_none)]
|
||||
|
||||
def get_multivalue(self, name, **pattern):
|
||||
values = self.__values(name, pattern, match_none=False)
|
||||
if not values:
|
||||
Value = self.multivalue_model(name)
|
||||
value = Value(**pattern)
|
||||
func = getattr(self, 'default_%s' % name, lambda **kw: None)
|
||||
setattr(value, name, func(**pattern))
|
||||
else:
|
||||
value = values[0]
|
||||
return getattr(value, name)
|
||||
|
||||
def _multivalue_getter(self, name):
|
||||
Value = self.multivalue_model(name)
|
||||
value = self.get_multivalue(name)
|
||||
if isinstance(value, Model):
|
||||
if Value._fields[name]._type == 'reference':
|
||||
return str(value)
|
||||
return value.id
|
||||
elif isinstance(value, (list, tuple)):
|
||||
return [r.id for r in value]
|
||||
else:
|
||||
return value
|
||||
|
||||
def set_multivalue(self, name, value, _save=True, **pattern):
|
||||
Value = self.multivalue_model(name)
|
||||
values = self.__values(name, pattern, match_none=True)
|
||||
if not values:
|
||||
values = [self.multivalue_record(name, **pattern)]
|
||||
for record in values:
|
||||
setattr(record, name, value)
|
||||
if _save:
|
||||
Value.save(values)
|
||||
else:
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def _multivalue_setter(cls, records, name, val):
|
||||
Value = cls.multivalue_model(name)
|
||||
to_save = []
|
||||
for record in records:
|
||||
to_save.extend(record.set_multivalue(name, val, _save=False))
|
||||
Value.save(to_save)
|
||||
|
||||
|
||||
class ValueMixin(MatchMixin):
|
||||
|
||||
def match(self, pattern, match_none=True):
|
||||
return super(ValueMixin, self).match(pattern, match_none=match_none)
|
||||
@@ -371,21 +371,6 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_ir_property">
|
||||
<field name="model" search="[('model', '=', 'ir.property')]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_ir_property_admin">
|
||||
<field name="model" search="[('model', '=', 'ir.property')]"/>
|
||||
<field name="group" ref="group_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_ir_module">
|
||||
<field name="model" search="[('model', '=', 'ir.module')]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
@@ -694,14 +679,6 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="menu" ref="ir.menu_rule_group_form"/>
|
||||
<field name="group" ref="group_admin"/>
|
||||
</record>
|
||||
<record model="ir.ui.menu-res.group" id="menu_property_form_default_group_admin">
|
||||
<field name="menu" ref="ir.menu_property_form_default"/>
|
||||
<field name="group" ref="group_admin"/>
|
||||
</record>
|
||||
<record model="ir.ui.menu-res.group" id="menu_property_form_group_admin">
|
||||
<field name="menu" ref="ir.menu_property_form"/>
|
||||
<field name="group" ref="group_admin"/>
|
||||
</record>
|
||||
<record model="ir.ui.menu-res.group" id="menu_modules_group_admin">
|
||||
<field name="menu" ref="ir.menu_modules"/>
|
||||
<field name="group" ref="group_admin"/>
|
||||
|
||||
@@ -15,6 +15,7 @@ from .workflow import *
|
||||
from .copy_ import *
|
||||
from history import *
|
||||
from .field_context import *
|
||||
from . import multivalue
|
||||
|
||||
|
||||
def register():
|
||||
@@ -101,7 +102,6 @@ def register():
|
||||
Reference,
|
||||
ReferenceTarget,
|
||||
ReferenceRequired,
|
||||
Property,
|
||||
Selection,
|
||||
SelectionRequired,
|
||||
DictSchema,
|
||||
@@ -194,6 +194,8 @@ def register():
|
||||
TestWizard,
|
||||
module='tests', type_='wizard')
|
||||
|
||||
multivalue.register('tests')
|
||||
|
||||
|
||||
def suite():
|
||||
from .test_tryton import all_suite
|
||||
|
||||
@@ -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 trytond.model import ModelSQL, MultiValueMixin, ValueMixin, fields
|
||||
from trytond.pool import Pool
|
||||
|
||||
|
||||
class ModelMultiValue(ModelSQL, MultiValueMixin):
|
||||
"Model MultiValue"
|
||||
__name__ = 'test.model_multivalue'
|
||||
value = fields.MultiValue(fields.Char("Value"))
|
||||
values = fields.One2Many(
|
||||
'test.model_multivalue.value', 'record', "Values")
|
||||
|
||||
@classmethod
|
||||
def default_value(cls, **pattern):
|
||||
return "default"
|
||||
|
||||
|
||||
class ModelValue(ModelSQL, ValueMixin):
|
||||
"Model Value"
|
||||
__name__ = 'test.model_multivalue.value'
|
||||
record = fields.Many2One(
|
||||
'test.model_multivalue', "Record")
|
||||
condition = fields.Char("Condition")
|
||||
value = fields.Char("Value")
|
||||
|
||||
|
||||
def register(module):
|
||||
Pool.register(
|
||||
ModelMultiValue,
|
||||
ModelValue,
|
||||
module=module, type_='model')
|
||||
@@ -38,7 +38,6 @@ __all__ = [
|
||||
'Many2ManyFilterDomain', 'Many2ManyFilterDomainTarget',
|
||||
'Many2ManyFilterDomainRelation',
|
||||
'Reference', 'ReferenceTarget', 'ReferenceRequired',
|
||||
'Property',
|
||||
'Selection', 'SelectionRequired',
|
||||
'DictSchema', 'Dict', 'DictDefault', 'DictRequired',
|
||||
'Binary', 'BinaryDefault', 'BinaryRequired', 'BinaryFileStorage',
|
||||
@@ -720,20 +719,6 @@ class ReferenceRequired(ModelSQL):
|
||||
], required=True)
|
||||
|
||||
|
||||
class Property(ModelSQL):
|
||||
'Property'
|
||||
__name__ = 'test.property'
|
||||
char = fields.Property(fields.Char('Test Char'))
|
||||
many2one = fields.Property(fields.Many2One('test.char',
|
||||
'Test Many2One'))
|
||||
numeric = fields.Property(fields.Numeric('Test Numeric'))
|
||||
selection = fields.Property(fields.Selection([
|
||||
(None, ''),
|
||||
('option_a', 'Option A'),
|
||||
('option_b', 'Option B')
|
||||
], 'Test Selection'))
|
||||
|
||||
|
||||
class Selection(ModelSQL):
|
||||
'Selection'
|
||||
__name__ = 'test.selection'
|
||||
|
||||
@@ -2999,276 +2999,6 @@ class FieldsTestCase(unittest.TestCase):
|
||||
}])
|
||||
self.assert_(reference4)
|
||||
|
||||
@with_transaction()
|
||||
def test_property(self):
|
||||
'Test Property with supported field types'
|
||||
pool = Pool()
|
||||
Property = pool.get('test.property')
|
||||
IrProperty = pool.get('ir.property')
|
||||
ModelField = pool.get('ir.model.field')
|
||||
Char = pool.get('test.char')
|
||||
transaction = Transaction()
|
||||
|
||||
# Test Char
|
||||
prop_a, = Property.create([{'char': 'Test'}])
|
||||
self.assert_(prop_a)
|
||||
self.assertEqual(prop_a.char, 'Test')
|
||||
|
||||
prop_b, = Property.create([{}])
|
||||
self.assert_(prop_b)
|
||||
self.assertEqual(prop_b.char, None)
|
||||
|
||||
prop_c, = Property.create([{'char': 'FooBar'}])
|
||||
self.assert_(prop_c)
|
||||
self.assertEqual(prop_c.char, 'FooBar')
|
||||
|
||||
props = Property.search([('char', '=', 'Test')])
|
||||
self.assertEqual(props, [prop_a])
|
||||
|
||||
props = Property.search([('char', '=', None)])
|
||||
self.assertEqual(props, [prop_b])
|
||||
|
||||
props = Property.search([('char', '!=', None)])
|
||||
self.assertEqual(props, [prop_a, prop_c])
|
||||
|
||||
props = Property.search([('char', 'like', 'Tes%')])
|
||||
self.assertEqual(props, [prop_a])
|
||||
|
||||
props = Property.search([('char', 'like', '%Bar')])
|
||||
self.assertEqual(props, [prop_c])
|
||||
|
||||
props = Property.search([('char', 'not like', 'Tes%')])
|
||||
self.assertEqual(props, [prop_b, prop_c])
|
||||
|
||||
props = Property.search([('char', 'ilike', 'tes%')])
|
||||
self.assert_(props, [prop_a])
|
||||
|
||||
props = Property.search([('char', 'ilike', '%bar')])
|
||||
self.assertEqual(props, [prop_c])
|
||||
|
||||
props = Property.search([('char', 'not ilike', 'tes%')])
|
||||
self.assertEqual(props, [prop_b, prop_c])
|
||||
|
||||
props = Property.search([('char', 'in', ['Test'])])
|
||||
self.assertEqual(props, [prop_a])
|
||||
|
||||
props = Property.search([
|
||||
('char', 'in', ['Test', 'FooBar'])])
|
||||
self.assertEqual(props, [prop_a, prop_c])
|
||||
|
||||
props = Property.search([
|
||||
('char', 'not in', ['Test', 'FooBar'])])
|
||||
self.assertEqual(props, [prop_b])
|
||||
|
||||
# Test default value
|
||||
property_field, = ModelField.search([
|
||||
('model.model', '=', 'test.property'),
|
||||
('name', '=', 'char'),
|
||||
], limit=1)
|
||||
IrProperty.create([{
|
||||
'field': property_field.id,
|
||||
'value': ',DEFAULT_VALUE',
|
||||
}])
|
||||
|
||||
prop_d, = Property.create([{}])
|
||||
self.assert_(prop_d)
|
||||
self.assertEqual(prop_d.char, 'DEFAULT_VALUE')
|
||||
|
||||
props = Property.search([('char', '!=', None)])
|
||||
self.assertEqual(props, [prop_a, prop_c, prop_d])
|
||||
|
||||
Property.write([prop_a], {'char': None})
|
||||
self.assertEqual(prop_a.char, None)
|
||||
|
||||
Property.write([prop_b], {'char': 'Test'})
|
||||
self.assertEqual(prop_b.char, 'Test')
|
||||
|
||||
transaction.rollback()
|
||||
|
||||
# Test Many2One
|
||||
char_a, = Char.create([{'char': 'Test'}])
|
||||
self.assert_(char_a)
|
||||
|
||||
char_b, = Char.create([{'char': 'FooBar'}])
|
||||
self.assert_(char_b)
|
||||
|
||||
prop_a, = Property.create([{'many2one': char_a.id}])
|
||||
self.assert_(prop_a)
|
||||
self.assertEqual(prop_a.many2one, char_a)
|
||||
|
||||
prop_b, = Property.create([{'many2one': char_b.id}])
|
||||
self.assert_(prop_b)
|
||||
self.assertEqual(prop_b.many2one, char_b)
|
||||
|
||||
prop_c, = Property.create([{}])
|
||||
self.assert_(prop_c)
|
||||
self.assertEqual(prop_c.many2one, None)
|
||||
|
||||
props = Property.search([('many2one', '=', char_a.id)])
|
||||
self.assertEqual(props, [prop_a])
|
||||
|
||||
props = Property.search([('many2one', '!=', None)])
|
||||
self.assertEqual(props, [prop_a, prop_b])
|
||||
|
||||
props = Property.search([('many2one', '=', None)])
|
||||
self.assertEqual(props, [prop_c])
|
||||
|
||||
self.assertEqual(prop_a.many2one, char_a)
|
||||
|
||||
props = Property.search([
|
||||
('many2one', 'in', [char_a.id, char_b.id])])
|
||||
self.assertEqual(props, [prop_a, prop_b])
|
||||
|
||||
props = Property.search([
|
||||
('many2one', 'not in', [char_a.id, char_b.id])])
|
||||
self.assertEqual(props, [prop_c])
|
||||
|
||||
Property.write([prop_b], {'many2one': char_a.id})
|
||||
self.assertEqual(prop_b.many2one, char_a)
|
||||
|
||||
transaction.rollback()
|
||||
|
||||
# Test Numeric
|
||||
prop_a, = Property.create([{'numeric': Decimal('1.1')}])
|
||||
self.assert_(prop_a)
|
||||
self.assertEqual(prop_a.numeric, Decimal('1.1'))
|
||||
|
||||
prop_b, = Property.create([{'numeric': Decimal('2.6')}])
|
||||
self.assert_(prop_b)
|
||||
self.assertEqual(prop_b.numeric, Decimal('2.6'))
|
||||
|
||||
prop_c, = Property.create([{}])
|
||||
self.assert_(prop_c)
|
||||
self.assertEqual(prop_c.numeric, None)
|
||||
|
||||
props = Property.search([('numeric', '!=', None)])
|
||||
self.assertEqual(props, [prop_a, prop_b])
|
||||
|
||||
props = Property.search([('numeric', '=', None)])
|
||||
self.assertEqual(props, [prop_c])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', '=', Decimal('1.1')),
|
||||
])
|
||||
self.assertEqual(props, [prop_a])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', '!=', Decimal('1.1'))])
|
||||
self.assertEqual(props, [prop_b, prop_c])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', '<', Decimal('2.6')),
|
||||
])
|
||||
self.assertEqual(props, [prop_a])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', '<=', Decimal('2.6'))])
|
||||
self.assertEqual(props, [prop_a, prop_b])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', '>', Decimal('1.1')),
|
||||
])
|
||||
self.assertEqual(props, [prop_b])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', '>=', Decimal('1.1'))])
|
||||
self.assertEqual(props, [prop_a, prop_b])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', 'in', [Decimal('1.1')])])
|
||||
self.assertEqual(props, [prop_a])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', 'in', [Decimal('1.1'), Decimal('2.6')])])
|
||||
self.assertEqual(props, [prop_a, prop_b])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', 'not in', [Decimal('1.1')])])
|
||||
self.assertEqual(props, [prop_b, prop_c])
|
||||
|
||||
props = Property.search([
|
||||
('numeric', 'not in', [Decimal('1.1'), Decimal('2.6')])])
|
||||
self.assertEqual(props, [prop_c])
|
||||
|
||||
# Test default value
|
||||
property_field, = ModelField.search([
|
||||
('model.model', '=', 'test.property'),
|
||||
('name', '=', 'numeric'),
|
||||
], limit=1)
|
||||
IrProperty.create([{
|
||||
'field': property_field.id,
|
||||
'value': ',3.7',
|
||||
}])
|
||||
|
||||
prop_d, = Property.create([{}])
|
||||
self.assert_(prop_d)
|
||||
self.assertEqual(prop_d.numeric, Decimal('3.7'))
|
||||
|
||||
Property.write([prop_a], {'numeric': None})
|
||||
self.assertEqual(prop_a.numeric, None)
|
||||
|
||||
Property.write([prop_b], {'numeric': Decimal('3.11')})
|
||||
self.assertEqual(prop_b.numeric, Decimal('3.11'))
|
||||
|
||||
transaction.rollback()
|
||||
|
||||
# Test Selection
|
||||
prop_a, = Property.create([{'selection': 'option_a'}])
|
||||
self.assert_(prop_a)
|
||||
self.assertEqual(prop_a.selection, 'option_a')
|
||||
|
||||
prop_b, = Property.create([{'selection': 'option_b'}])
|
||||
self.assert_(prop_b)
|
||||
self.assertEqual(prop_b.selection, 'option_b')
|
||||
|
||||
prop_c, = Property.create([{}])
|
||||
self.assert_(prop_c)
|
||||
self.assertEqual(prop_c.selection, None)
|
||||
|
||||
props = Property.search([('selection', '=', 'option_a')])
|
||||
self.assertEqual(props, [prop_a])
|
||||
|
||||
props = Property.search([('selection', '!=', None)])
|
||||
self.assertEqual(props, [prop_a, prop_b])
|
||||
|
||||
props = Property.search([('selection', '=', None)])
|
||||
self.assertEqual(props, [prop_c])
|
||||
|
||||
props = Property.search([('selection', '!=', 'option_a')])
|
||||
self.assertEqual(props, [prop_b, prop_c])
|
||||
|
||||
props = Property.search([
|
||||
('selection', 'in', ['option_a'])])
|
||||
self.assertEqual(props, [prop_a])
|
||||
|
||||
props = Property.search([
|
||||
('selection', 'in', ['option_a', 'option_b'])])
|
||||
self.assertEqual(props, [prop_a, prop_b])
|
||||
|
||||
props = Property.search([
|
||||
('selection', 'not in', ['option_a'])])
|
||||
self.assertEqual(props, [prop_b, prop_c])
|
||||
|
||||
# Test default value
|
||||
property_field, = ModelField.search([
|
||||
('model.model', '=', 'test.property'),
|
||||
('name', '=', 'selection'),
|
||||
], limit=1)
|
||||
IrProperty.create([{
|
||||
'field': property_field.id,
|
||||
'value': ',option_a',
|
||||
}])
|
||||
|
||||
prop_d, = Property.create([{}])
|
||||
self.assert_(prop_d)
|
||||
self.assertEqual(prop_d.selection, 'option_a')
|
||||
|
||||
Property.write([prop_a], {'selection': None})
|
||||
self.assertEqual(prop_a.selection, None)
|
||||
|
||||
Property.write([prop_c], {'selection': 'option_b'})
|
||||
self.assertEqual(prop_c.selection, 'option_b')
|
||||
|
||||
@with_transaction()
|
||||
def test_selection(self):
|
||||
'Test Selection'
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# 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 unittest
|
||||
|
||||
from trytond.tests.test_tryton import activate_module, with_transaction
|
||||
from trytond.pool import Pool
|
||||
|
||||
|
||||
class MultiValueTestCase(unittest.TestCase):
|
||||
"Test MultiValue"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
activate_module('tests')
|
||||
|
||||
@with_transaction()
|
||||
def test_get_multivalue(self):
|
||||
"Test get_multivalue"
|
||||
pool = Pool()
|
||||
ModelMultiValue = pool.get('test.model_multivalue')
|
||||
ModelValue = pool.get('test.model_multivalue.value')
|
||||
|
||||
record = ModelMultiValue()
|
||||
record.save()
|
||||
value, = ModelValue.search([])
|
||||
value.condition = "foo"
|
||||
value.value = "bar"
|
||||
value.save()
|
||||
|
||||
self.assertEqual(
|
||||
record.get_multivalue('value', condition="foo"),
|
||||
"bar")
|
||||
|
||||
@with_transaction()
|
||||
def test_get_multivalue_default(self):
|
||||
"Test get_multivalue default value"
|
||||
pool = Pool()
|
||||
ModelMultiValue = pool.get('test.model_multivalue')
|
||||
|
||||
record = ModelMultiValue()
|
||||
record.save()
|
||||
|
||||
self.assertEqual(record.get_multivalue('value'), "default")
|
||||
|
||||
@with_transaction()
|
||||
def test_get_multivalue_match_none(self):
|
||||
"Test get_multivalue does not match None"
|
||||
pool = Pool()
|
||||
ModelMultiValue = pool.get('test.model_multivalue')
|
||||
ModelValue = pool.get('test.model_multivalue.value')
|
||||
|
||||
record = ModelMultiValue()
|
||||
record.save()
|
||||
value = ModelValue(record=record, condition="foo", value="bar")
|
||||
value.save()
|
||||
|
||||
self.assertEqual(
|
||||
record.get_multivalue('value', condition="test"),
|
||||
"default")
|
||||
|
||||
@with_transaction()
|
||||
def test_set_multivalue(self):
|
||||
"Test set_multivalue"
|
||||
pool = Pool()
|
||||
ModelMultiValue = pool.get('test.model_multivalue')
|
||||
ModelValue = pool.get('test.model_multivalue.value')
|
||||
|
||||
record = ModelMultiValue()
|
||||
record.save()
|
||||
record.set_multivalue('value', "set", condition="test")
|
||||
|
||||
value, = ModelValue.search([('condition', '=', "test")])
|
||||
self.assertEqual(value.record, record)
|
||||
self.assertEqual(value.value, "set")
|
||||
|
||||
@with_transaction()
|
||||
def test_set_multivalue_match_none(self):
|
||||
"Test set_multivalue matches None"
|
||||
pool = Pool()
|
||||
ModelMultiValue = pool.get('test.model_multivalue')
|
||||
ModelValue = pool.get('test.model_multivalue.value')
|
||||
|
||||
record = ModelMultiValue()
|
||||
record.save()
|
||||
record.set_multivalue('value', "set", condition="test")
|
||||
|
||||
self.assertEqual(len(ModelValue.search([])), 2)
|
||||
|
||||
@with_transaction()
|
||||
def test_mutlivalue_setter(self):
|
||||
"Test multivalue setter"
|
||||
pool = Pool()
|
||||
ModelMultiValue = pool.get('test.model_multivalue')
|
||||
|
||||
record, = ModelMultiValue.create([{
|
||||
'value': "setter",
|
||||
}])
|
||||
|
||||
self.assertEqual(record.get_multivalue('value'), "setter")
|
||||
|
||||
@with_transaction()
|
||||
def test_mutlivalue_getter(self):
|
||||
"Test multivalue getter"
|
||||
pool = Pool()
|
||||
ModelMultiValue = pool.get('test.model_multivalue')
|
||||
|
||||
record = ModelMultiValue(value="getter")
|
||||
record.save()
|
||||
|
||||
read, = ModelMultiValue.read([record.id], ['value'])
|
||||
self.assertEqual(read['value'], "getter")
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromTestCase(MultiValueTestCase)
|
||||
@@ -0,0 +1,111 @@
|
||||
# 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 Table, Column, Literal, Union, Null
|
||||
from sql.aggregate import Max
|
||||
|
||||
from trytond import backend
|
||||
from trytond.pool import Pool
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
def migrate_property(
|
||||
model_name, field_names, ValueModel, value_names,
|
||||
parent=None, fields=None):
|
||||
"Migrate property from model_name.field_name to ValueModel.value_name"
|
||||
pool = Pool()
|
||||
Field = pool.get('ir.model.field')
|
||||
Model = pool.get('ir.model')
|
||||
TableHandler = backend.get('TableHandler')
|
||||
if not TableHandler.table_exist('ir_property'):
|
||||
return
|
||||
cursor = Transaction().connection.cursor()
|
||||
field = Field.__table__()
|
||||
model = Model.__table__()
|
||||
table = ValueModel.__table__()
|
||||
|
||||
if fields is None:
|
||||
fields = []
|
||||
if isinstance(field_names, basestring):
|
||||
field_names = [field_names]
|
||||
if isinstance(value_names, basestring):
|
||||
value_names = [value_names]
|
||||
|
||||
def split_value(value):
|
||||
return value.split(',')[1]
|
||||
cast_funcs = {
|
||||
'numeric': lambda v: Decimal(split_value(v)) if v else None,
|
||||
'integer': lambda v: int(split_value(v)) if v else None,
|
||||
'float': lambda v: float(split_value(v)) if v else None,
|
||||
'char': lambda v: split_value(v) if v else None,
|
||||
'selection': lambda v: split_value(v) if v else None,
|
||||
'many2one': lambda v: int(split_value(v)) if v else None,
|
||||
'reference': lambda v: v,
|
||||
}
|
||||
|
||||
casts = []
|
||||
queries = []
|
||||
for field_name, value_name in zip(field_names, value_names):
|
||||
value_field = getattr(ValueModel, value_name)
|
||||
casts.append(cast_funcs[value_field._type])
|
||||
|
||||
property_ = Table('ir_property')
|
||||
columns = [
|
||||
Literal(None).as_(f) if f != value_name
|
||||
else property_.value.as_(value_name)
|
||||
for f in value_names]
|
||||
if parent:
|
||||
columns.append(property_.res.as_(parent))
|
||||
where = property_.res.like(model_name + ',%')
|
||||
else:
|
||||
where = property_.res == Null
|
||||
columns.extend([Column(property_, f).as_(f) for f in fields])
|
||||
query = property_.join(field,
|
||||
condition=property_.field == field.id
|
||||
).join(model,
|
||||
condition=field.model == model.id
|
||||
).select(*columns,
|
||||
where=where
|
||||
& (field.name == field_name)
|
||||
& (model.model == model_name))
|
||||
queries.append(query)
|
||||
|
||||
union = Union(*queries)
|
||||
columns = [Max(Column(union, f)).as_(f) for f in value_names]
|
||||
if parent:
|
||||
columns.append(Column(union, parent).as_(parent))
|
||||
pcolumns = [Column(union, parent)]
|
||||
else:
|
||||
pcolumns = []
|
||||
vcolumns = [Column(union, f).as_(f) for f in fields]
|
||||
cursor.execute(
|
||||
*union.select(*(columns + vcolumns), group_by=pcolumns + vcolumns))
|
||||
|
||||
columns = [Column(table, f) for f in value_names]
|
||||
if parent:
|
||||
pcolumns = [Column(table, parent)]
|
||||
else:
|
||||
pcolumns = []
|
||||
vcolumns = [Column(table, f) for f in fields]
|
||||
values = []
|
||||
l = len(value_names)
|
||||
for row in cursor.fetchall():
|
||||
value = [c(v) for v, c in zip(row, casts)]
|
||||
if parent:
|
||||
value.append(int(row[l].split(',')[1]) if row[l] else None)
|
||||
i = 1
|
||||
else:
|
||||
i = 0
|
||||
value.extend(row[l + i:])
|
||||
values.append(value)
|
||||
if (values and not (
|
||||
# No property defined
|
||||
len(values) == 1
|
||||
and all(x is None for x in values[0][:len(columns)]))):
|
||||
|
||||
# Delete previous migrated values
|
||||
cursor.execute(*table.delete())
|
||||
|
||||
cursor.execute(*table.insert(
|
||||
columns + pcolumns + vcolumns, values=values))
|
||||
Reference in new issue
Block a user