← Back to team overview

openerp-community team mailing list archive

Re: reorderable lines on purchase.orders, account.invoice, stock.picking

 

On 10/01/2013 03:16 PM, Olivier Dony wrote:
On 09/24/2013 06:14 PM, ferdinand wrote:
On 09/24/2013 05:05 PM, Olivier Dony wrote:
[1] That's harder than it seems - it requires client-side
logic/grouping because some of the lines in a o2m may be dirty or new,
so the server would be unable to perform the sorting.

that's why the module one2many_sorted was created

Is there any new version of one2many_sorted? The one I can see [1] does not perform any sorting or grouping on the client-side, so it does not seem to address the limitations I was referring to??

[1] http://bazaar.launchpad.net/~camptocamp/c2c-rd-addons/7.0/view/head:/one2many_sorted/__init__.py
no this one does sorting on the server side.

I attach the patches (for 6.0) which allowed to filter and sort not database fields on client side

the code was financed by ChriCar and comes from Panos , so all credits to him.

* add this to the server config file to activate the pythonic sort

[orm]

fallback_search = True

may be this parameter should finally go into res_company


regards
ferdinand

=== modified file 'bin/osv/expression.py'
--- bin/osv/expression.py	2011-01-17 08:41:08 +0000
+++ bin/osv/expression.py	2011-10-07 17:11:08 +0000
@@ -20,10 +20,26 @@
 #
 ##############################################################################
 
-from tools import flatten, reverse_enumerate
+from tools import flatten, reverse_enumerate, config
 import fields
 
 
+def _m2o_cmp(a, b):
+    if a is False:
+        if b:
+            return -1
+        return 0
+    else:
+        if not b:
+            return 1
+        elif isinstance(b, (int, long)):
+            return cmp(a[0], b)
+        elif isinstance(b, basestring):
+            return cmp(a[1], b)
+        else:
+            # Arbitrary: unknown b is greater than all record values
+            return -1
+
 class expression(object):
     """
     parse a domain expression
@@ -32,6 +48,41 @@
     For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
     """
 
+    FALLBACK_OPS = {'=': lambda a, b: bool(a == b),
+            '!=': lambda a, b: bool(a != b),
+            '<>': lambda a, b: bool(a != b),
+            '<=': lambda a, b: bool(a <= b),
+            '<': lambda a, b: bool(a < b),
+            '>': lambda a, b: bool(a > b),
+            '>=': lambda a, b: bool(a >= b),
+            '=?': lambda a, b: b is None or b is False or bool(a == b),
+            #'=like': lambda a, b: , need regexp?
+            #'=ilike': lambda a, b: ,
+            'like': lambda a, b: bool(b in a),
+            'not like': lambda a, b: bool(b not in a),
+            'ilike': lambda a, b: (not b) or (a and bool(b.lower() in a.lower())),
+            'not ilike': lambda a, b: b and ((not a) or bool(b.lower() not in a.lower())),
+            'in': lambda a, b: bool(a in b),
+            'not in': lambda a, b: bool(a not in b),
+            }
+
+    FALLBACK_OPS_M2O = {'=': lambda a, b: _m2o_cmp(a,b) == 0,
+            '!=': _m2o_cmp ,
+            '<>': _m2o_cmp,
+            '<=': lambda a, b: _m2o_cmp(a,b) <= 0,
+            '<': lambda a, b: _m2o_cmp(a, b) < 0,
+            '>': lambda a, b: _m2o_cmp(a, b) > 0,
+            '>=': lambda a, b: _m2o_cmp(a, b) >= 0,
+            '=?': lambda a, b: b is None or b is False or _m2o_cmp(a, b) == 0,
+            #'=like': lambda a, b: , need regexp?
+            #'=ilike': lambda a, b: ,
+            'like': lambda a, b: (not b) or (a and bool(b in a[1])),
+            'not like': lambda a, b: b and ((not a) or (b not in a[1])),
+            'ilike': lambda a, b: (not b) or (a and bool(b.lower() in a[1].lower())),
+            'not ilike': lambda a, b: b and ((not a) or bool(b.lower() not in a[1].lower())),
+            'in': lambda a, b: a and bool(a[1] in b),
+            'not in': lambda a, b: (not a) or bool(a[1] not in b),
+            }
     def _is_operator(self, element):
         return isinstance(element, (str, unicode)) and element in ['&', '|', '!']
 
@@ -162,11 +213,67 @@
             if field._properties and not field.store:
                 # this is a function field that is not stored
                 if not field._fnct_search:
-                    # the function field doesn't provide a search function and doesn't store
-                    # values in the database, so we must ignore it : we generate a dummy leaf
-                    self.__exp[i] = self.__DUMMY_LEAF
+                    do_fallback = None
+                    if hasattr(table, '_fallback_search'):
+                        do_fallback = table._fallback_search
+                    else:
+                        do_fallback = config.get_misc('orm', 'fallback_search', None)
+                    if do_fallback is None:
+                        # the function field doesn't provide a search function and doesn't store
+                        # values in the database, so we must ignore it : we generate a dummy leaf
+                        self.__exp[i] = self.__DUMMY_LEAF
+                    elif do_fallback:
+                        # Do the slow fallback.
+                        # Try to see if the expression so far is a straight (ANDed)
+                        # combination. In that case, we can restrict the query
+                        if field._type == 'many2one':
+                            op_fn = self.FALLBACK_OPS_M2O.get(operator, None)
+                        else:
+                            op_fn = self.FALLBACK_OPS.get(operator, None)
+                        if not op_fn:
+                            raise ValueError('Cannot fallback with operator "%s" !' % operator)
+                        e_so_far = self.__exp[:i]
+                        for e in e_so_far:
+                            if not self._is_leaf(e, internal=True):
+                                e_so_far = []
+                                break
+                        ids_so_far = table.search(cr, uid, e_so_far, context=context)
+                        if not ids_so_far:
+                            self.__exp[i] = ( 'id', '=', 0 )
+                        else:
+                            ids2 = []
+                            if field._multi:
+                                fget_name = [fargs[0],]
+                            else:
+                                fget_name = fargs[0]
+                            for res_id, rval in field.get(cr, table, ids_so_far,
+                                                        name=fget_name, user=uid,
+                                                        context=context).items():
+                                if field._multi:
+                                    rval = rval.get(fargs[0], None)
+                                if rval is None or rval is False:
+                                    pass
+                                elif field._type == 'integer':
+                                    # workaround the str() of fields.function.get() :(
+                                    rval = int(rval)
+                                elif field._type == 'float':
+                                    assert isinstance(rval, float), "%s: %r" %(type(rval), rval)
+                                    if field.digits:
+                                        rval = round(rval, field.digits[1])
+
+                                # TODO: relational fields don't work here, must implement
+                                # special operators between their (id, name) and right
+
+                                if op_fn(rval, right):
+                                    ids2.append(res_id)
+                            self.__exp[i] = ( 'id', 'in', ids2 )
+                    else:
+                        raise NotImplementedError("Cannot compute %s.%s field for filtering" % \
+                                    (table._name, left))
                 else:
                     subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
+                    # Reminder: the field.search() API returns an expression, not a dataset,
+                    # which means that [] => True clause
                     if not subexp:
                         self.__exp[i] = self.__DUMMY_LEAF
                     else:

=== modified file 'bin/osv/orm.py'
--- bin/osv/orm.py	2011-06-20 11:12:35 +0000
+++ bin/osv/orm.py	2011-10-10 19:48:15 +0000
@@ -86,6 +86,52 @@
         self.value = value
         self.args = (name, value)
 
+class pythonOrderBy(list):
+    """ Placeholder class for _generate_order_by() requesting python sorting
+    """
+
+    def get_sort_key(self):
+        """ returns a function to use in sort(key=...)
+        """
+        if len(self) != 1:
+            raise NotImplementedError("Cannot sort by: %s" % ','.join(self))
+
+        _getters = []
+        for x in [ x.split(' ', 1)[0] for x in self]:
+            if x.endswith(':'):
+                _getters.append(lambda k: (k[x[:-1]] and k[x[:-1]][1]) or None) # the visual string of m2o
+            else:
+                _getters.append(lambda k: k[x])
+        def sort_key(k):
+            return tuple([ g(k) for g in _getters])
+        return sort_key
+
+    def get_fields(self):
+        """Return the field components of this order-by list
+        """
+        def _clean(x):
+            x = x.split(' ',1)[0]
+            if x.endswith(':'):
+                x = x[:-1]
+            return x
+        return map(_clean, self)
+
+    def get_direction(self):
+        """ returns the order direction
+            True if ascending (default)
+        """
+        ascending = None
+        for x in self:
+            if ' ' in x:
+                adir = (x.split(' ', 1)[1].lower() != 'desc' )
+            else:
+                adir = True
+            if ascending is None:
+                ascending = adir
+            elif ascending != adir:
+                raise NotImplementedError("Cannot use multiple order directions on python sorting!")
+        return ascending
+
 class BrowseRecordError(Exception):
     pass
 
@@ -1076,6 +1122,12 @@
         for parent in self._inherits:
             res.update(self.pool.get(parent).fields_get(cr, user, allfields, context))
 
+        all_selectable = False
+        if getattr(self, '_fallback_search', False) == True:
+            all_selectable = True
+        elif config.get_misc('orm', 'fallback_search', None) == True:
+            all_selectable = True
+
         if self._columns.keys():
             for f in self._columns.keys():
                 field_col = self._columns[f]
@@ -1137,6 +1189,9 @@
                     res[f]['relation'] = field_col._obj
                     res[f]['domain'] = field_col._domain
                     res[f]['context'] = field_col._context
+                
+                if all_selectable:
+                    res[f]['selectable'] = True
         else:
             #TODO : read the fields from the database
             pass
@@ -2116,6 +2171,17 @@
         return id in self.datas
 
 class orm(orm_template):
+    """ ORM for regular, database-stored models
+
+        @attribute _fallback_search enables *slow*, fallback search for those
+                function fields that do not provide a _fnct_search() .
+                Use with care, as this may perform a read() of the full dataset
+                of that model, in order to compute the search condition.
+                Takes 3 values:
+                    None    the default 'ignore' behavior,
+                    True    use the slow method
+                    False   stop and raise an exception on those fields
+    """
     _sql_constraints = []
     _table = None
     _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
@@ -2330,7 +2396,7 @@
                 pass
             if not val_id:
                 raise except_orm(_('ValidateError'),
-                                 _('Invalid value for reference field "%s" (last part must be a non-zero integer): "%s"') % (field, value))
+                                 _('Invalid value for reference field "%s" of table "%s" (last part must be a non-zero integer): "%s"') % (field, self._table, value))
             val = val_model
         else:
             val = value
@@ -2339,8 +2405,10 @@
                 return
         elif val in dict(self._columns[field].selection(self, cr, uid, context=context)):
             return
+        import sys
+	print >> sys.stderr,'check_sel context',context
         raise except_orm(_('ValidateError'),
-                         _('The value "%s" for the field "%s" is not in the selection') % (value, field))
+			_('The value "%s" for the field "%s" of table "%s" is not in the selection') % (value, field, self._table))
 
     def _check_removed_columns(self, cr, log=False):
         # iterate on the database columns to drop the NOT NULL constraints
@@ -3961,6 +4029,7 @@
         order_by_clause = self._order
         if order_spec:
             order_by_elements = []
+            python_order = False
             self._check_qorder(order_spec)
             for order_part in order_spec.split(','):
                 order_split = order_part.strip().split(' ')
@@ -3973,25 +4042,63 @@
                     order_column = self._columns[order_field]
                     if order_column._classic_read:
                         inner_clause = '"%s"."%s"' % (self._table, order_field)
-                    elif order_column._type == 'many2one':
+                    elif order_column._type == 'many2one' \
+                            and (order_column._classic_write \
+                                or getattr(order_column, 'store', False)):
                         inner_clause = self._generate_m2o_order_by(order_field, query)
                     else:
-                        continue # ignore non-readable or "non-joinable" fields
+                        do_fallback = None
+                        if hasattr(self, '_fallback_search'):
+                            do_fallback = self._fallback_search
+                        else:
+                            do_fallback = config.get_misc('orm', 'fallback_search', None)
+                        if do_fallback is None:
+                            continue # ignore non-readable or "non-joinable" fields
+                        elif do_fallback is True:
+                            inner_clause = order_field
+                            if order_column._type in ('many2one',):
+                                inner_clause += ':'
+                            python_order = True
+                        else:
+                            raise except_orm(_('Error!'), 
+                                    _('Object model %s does not support order by function field "%s"!') % \
+                                     (self._name, order_field))
                 elif order_field in self._inherit_fields:
                     parent_obj = self.pool.get(self._inherit_fields[order_field][0])
                     order_column = parent_obj._columns[order_field]
                     if order_column._classic_read:
                         inner_clause = self._inherits_join_calc(order_field, query)
-                    elif order_column._type == 'many2one':
+                    elif order_column._type == 'many2one' \
+                            and (order_column._classic_write \
+                                or getattr(order_column, 'store', False)):
                         inner_clause = self._generate_m2o_order_by(order_field, query)
                     else:
-                        continue # ignore non-readable or "non-joinable" fields
+                        do_fallback = None
+                        if hasattr(self, '_fallback_search'):
+                            do_fallback = self._fallback_search
+                        elif hasattr(parent_obj, '_fallback_search'):
+                            do_fallback = parent_obj._fallback_search
+                        else:
+                            do_fallback = config.get_misc('orm', 'fallback_search', None)
+                        if do_fallback is None:
+                            continue # ignore non-readable or "non-joinable" fields
+                        elif do_fallback is True:
+                            inner_clause = order_field
+                            if order_column._type in ('many2one',):
+                                inner_clause += ':'
+                            python_order = True
+                        else:
+                            raise except_orm(_('Error!'),
+                                    _('Object model %s does not support order by function field "%s"!') % \
+                                     (self._name, order_field))
                 if inner_clause:
                     if isinstance(inner_clause, list):
                         for clause in inner_clause:
                             order_by_elements.append("%s %s" % (clause, order_direction))
                     else:
                         order_by_elements.append("%s %s" % (inner_clause, order_direction))
+            if python_order and order_by_elements:
+                return pythonOrderBy(order_by_elements)
             if order_by_elements:
                 order_by_clause = ",".join(order_by_elements)
 
@@ -4024,9 +4131,26 @@
             cr.execute('SELECT count("%s".id) FROM ' % self._table + from_clause + where_str + limit_str + offset_str, where_clause_params)
             res = cr.fetchall()
             return res[0][0]
-        cr.execute('SELECT "%s".id FROM ' % self._table + from_clause + where_str + order_by + limit_str + offset_str, where_clause_params)
-        res = cr.fetchall()
-        return [x[0] for x in res]
+        elif isinstance(order_by, pythonOrderBy):
+            # Fall back to pythonic sorting (+ offset, limit)
+            cr.execute('SELECT "%s".id FROM ' % self._table + from_clause +
+                    where_str, # no offset or limit
+                    where_clause_params)
+            res = cr.fetchall()
+            data_res = self.read(cr, user, [x[0] for x in res],
+                        fields=order_by.get_fields(), context=context)
+            data_res.sort(key=order_by.get_sort_key(), reverse=not order_by.get_direction())
+            if offset:
+                data_res = data_res[offset:]
+            if limit:
+                data_res = data_res[:limit]
+            return [x['id'] for x in data_res]
+        else:
+            cr.execute('SELECT "%s".id FROM ' % self._table + from_clause +
+                    where_str + order_by + limit_str + offset_str, 
+                    where_clause_params)
+            res = cr.fetchall()
+            return [x[0] for x in res]
 
     # returns the different values ever entered for one field
     # this is used, for example, in the client when the user hits enter on
=== modified file 'bin/widget_search/custom_filter.py'
--- bin/widget_search/custom_filter.py	2011-09-16 11:49:53 +0000
+++ bin/widget_search/custom_filter.py	2011-10-16 18:15:54 +0000
@@ -101,8 +101,6 @@
             operator = self.op_selection[self.combo_op.get_active_text()]
             right_text =  self.right_text.get_text() or False
 
-            if operator in ['not ilike','<>', 'not in'] and field_type != 'boolean':
-                false_value_domain = ['|', (field_left,'=', False)]
             try:
                 cast_type = True
                 if field_type in type_cast:
@@ -126,11 +124,13 @@
                 self.right_text.modify_bg(gtk.STATE_ACTIVE, gtk.gdk.color_parse("#ff6969"))
                 self.right_text.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ff6969"))
                 return {}
+
+            if operator in ['not ilike','<>', 'not in'] and field_type != 'boolean' and right_text:
+                false_value_domain = ['|', (field_left,'=', False)]
+
             if operator in ['ilike','not ilike']:
                 if field_type in ['integer','float','date','datetime','boolean']:
                     operator = (operator == 'ilike') and '=' or '!='
-                else:
-                    right_text = '%' + right_text + '%'
 
             if operator in ['<','>'] and field_type not in ['integer','float','date','datetime','boolean']:
                     operator = '='


Follow ups

References