[IMP] allow group_operator on any field
There's really no reason to limit group_operator to numeric fields and Postgres's flexible aggregate make the ability to aggregate on non-numeric fields (and/or non-numerically) * moves group_operator to Field * adds a default value of `sum` on integer, float and monetary fields * in read_group, filters on fields with a group operator rather than by type (also allow id fields, since Id doesn't have a group_operator by default it will be ignored anyway) (sequence is still specifically excluded for now because they're usually regular Integer fields) Closes #12372
This commit is contained in:
@@ -7,3 +7,12 @@ class GroupOnDate(models.Model):
|
||||
|
||||
date = fields.Date("Date")
|
||||
value = fields.Integer("Value")
|
||||
|
||||
class BooleanAggregate(models.Model):
|
||||
_name = 'test_read_group.aggregate.boolean'
|
||||
_order = 'key DESC'
|
||||
|
||||
key = fields.Integer()
|
||||
bool_and = fields.Boolean(default=False, group_operator='bool_and')
|
||||
bool_or = fields.Boolean(default=False, group_operator='bool_or')
|
||||
bool_array = fields.Boolean(default=False, group_operator='array_agg')
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_empty
|
||||
from . import test_group_operator
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp.tests import common
|
||||
|
||||
|
||||
class TestGroupBooleans(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestGroupBooleans, self).setUp()
|
||||
self.Model = self.env['test_read_group.aggregate.boolean']
|
||||
|
||||
def test_no_value(self):
|
||||
groups = self.Model.read_group(
|
||||
domain=[],
|
||||
fields=['key', 'bool_and', 'bool_or', 'bool_array'],
|
||||
groupby=['key'],
|
||||
)
|
||||
|
||||
self.assertEqual([], groups)
|
||||
|
||||
def test_agg_and(self):
|
||||
# and(true, true)
|
||||
self.Model.create({
|
||||
'key': 1,
|
||||
'bool_and': True
|
||||
})
|
||||
self.Model.create({
|
||||
'key': 1,
|
||||
'bool_and': True
|
||||
})
|
||||
# and(true, false)
|
||||
self.Model.create({'key': 2, 'bool_and': True})
|
||||
self.Model.create({'key': 2, 'bool_and': False})
|
||||
# and(false, false)
|
||||
self.Model.create({'key': 3, 'bool_and': False})
|
||||
self.Model.create({'key': 3, 'bool_and': False})
|
||||
|
||||
groups = self.Model.read_group(
|
||||
domain=[],
|
||||
fields=['key', 'bool_and'],
|
||||
groupby=['key'],
|
||||
)
|
||||
|
||||
self.assertEqual([
|
||||
{
|
||||
'key_count': 2,
|
||||
'__domain': [('key', '=', 1)],
|
||||
'key': 1,
|
||||
'bool_and': True
|
||||
},
|
||||
{
|
||||
'key_count': 2,
|
||||
'__domain': [('key', '=', 2)],
|
||||
'key': 2,
|
||||
'bool_and': False
|
||||
},
|
||||
{
|
||||
'key_count': 2,
|
||||
'__domain': [('key', '=', 3)],
|
||||
'key': 3,
|
||||
'bool_and': False
|
||||
},
|
||||
], groups)
|
||||
|
||||
|
||||
def test_agg_or(self):
|
||||
# or(true, true)
|
||||
self.Model.create({
|
||||
'key': 1,
|
||||
'bool_or': True
|
||||
})
|
||||
self.Model.create({
|
||||
'key': 1,
|
||||
'bool_or': True
|
||||
})
|
||||
# or(true, false)
|
||||
self.Model.create({'key': 2, 'bool_or': True})
|
||||
self.Model.create({'key': 2, 'bool_or': False})
|
||||
# or(false, false)
|
||||
self.Model.create({'key': 3, 'bool_or': False})
|
||||
self.Model.create({'key': 3, 'bool_or': False})
|
||||
|
||||
groups = self.Model.read_group(
|
||||
domain=[],
|
||||
fields=['key', 'bool_or'],
|
||||
groupby=['key'],
|
||||
)
|
||||
|
||||
self.assertEqual([
|
||||
{
|
||||
'key_count': 2,
|
||||
'__domain': [('key', '=', 1)],
|
||||
'key': 1,
|
||||
'bool_or': True
|
||||
},
|
||||
{
|
||||
'key_count': 2,
|
||||
'__domain': [('key', '=', 2)],
|
||||
'key': 2,
|
||||
'bool_or': True
|
||||
},
|
||||
{
|
||||
'key_count': 2,
|
||||
'__domain': [('key', '=', 3)],
|
||||
'key': 3,
|
||||
'bool_or': False
|
||||
},
|
||||
], groups)
|
||||
|
||||
def test_agg_array(self):
|
||||
# array(true, true)
|
||||
self.Model.create({
|
||||
'key': 1,
|
||||
'bool_array': True
|
||||
})
|
||||
self.Model.create({
|
||||
'key': 1,
|
||||
'bool_array': True
|
||||
})
|
||||
# array(true, false)
|
||||
self.Model.create({'key': 2, 'bool_array': True})
|
||||
self.Model.create({'key': 2, 'bool_array': False})
|
||||
# array(false, false)
|
||||
self.Model.create({'key': 3, 'bool_array': False})
|
||||
self.Model.create({'key': 3, 'bool_array': False})
|
||||
|
||||
groups = self.Model.read_group(
|
||||
domain=[],
|
||||
fields=['key', 'bool_array'],
|
||||
groupby=['key'],
|
||||
)
|
||||
|
||||
self.assertEqual([
|
||||
{
|
||||
'key_count': 2,
|
||||
'__domain': [('key', '=', 1)],
|
||||
'key': 1,
|
||||
'bool_array': [True, True]
|
||||
},
|
||||
{
|
||||
'key_count': 2,
|
||||
'__domain': [('key', '=', 2)],
|
||||
'key': 2,
|
||||
'bool_array': [True, False]
|
||||
},
|
||||
{
|
||||
'key_count': 2,
|
||||
'__domain': [('key', '=', 3)],
|
||||
'key': 3,
|
||||
'bool_array': [False, False]
|
||||
},
|
||||
], groups)
|
||||
|
||||
def test_group_by_aggregable(self):
|
||||
self.Model.create({'bool_and': False, 'key': 1, 'bool_array': True})
|
||||
self.Model.create({'bool_and': False, 'key': 2, 'bool_array': True})
|
||||
self.Model.create({'bool_and': False, 'key': 2, 'bool_array': True})
|
||||
self.Model.create({'bool_and': True, 'key': 2, 'bool_array': True})
|
||||
self.Model.create({'bool_and': True, 'key': 3, 'bool_array': True})
|
||||
self.Model.create({'bool_and': True, 'key': 3, 'bool_array': True})
|
||||
|
||||
groups = self.Model.read_group(
|
||||
domain=[],
|
||||
fields=['key', 'bool_and', 'bool_array'],
|
||||
groupby=['bool_and', 'key'],
|
||||
lazy=False
|
||||
)
|
||||
|
||||
self.assertEqual([
|
||||
{
|
||||
'bool_and': False,
|
||||
'key': 1,
|
||||
'bool_array': [True],
|
||||
'__count': 1,
|
||||
'__domain': ['&', ('bool_and', '=', False), ('key', '=', 1)]
|
||||
},
|
||||
{
|
||||
'bool_and': False,
|
||||
'key': 2,
|
||||
'bool_array': [True, True],
|
||||
'__count': 2,
|
||||
'__domain': ['&', ('bool_and', '=', False), ('key', '=', 2)]
|
||||
|
||||
},
|
||||
{
|
||||
'bool_and': True,
|
||||
'key': 2,
|
||||
'bool_array': [True],
|
||||
'__count': 1,
|
||||
'__domain': ['&', ('bool_and', '=', True), ('key', '=', 2)]
|
||||
},
|
||||
{
|
||||
'bool_and': True,
|
||||
'key': 3,
|
||||
'bool_array': [True, True],
|
||||
'__count': 2,
|
||||
'__domain': ['&', ('bool_and', '=', True), ('key', '=', 3)]
|
||||
}
|
||||
], groups)
|
||||
+20
-12
@@ -3,7 +3,7 @@
|
||||
|
||||
""" High-level objects for fields. """
|
||||
|
||||
from collections import OrderedDict, defaultdict
|
||||
from collections import OrderedDict, defaultdict, namedtuple
|
||||
from datetime import date, datetime
|
||||
from functools import partial
|
||||
from operator import attrgetter
|
||||
@@ -342,6 +342,7 @@ class Field(object):
|
||||
'deprecated': None, # whether the field is deprecated
|
||||
|
||||
'related_field': None, # corresponding related field
|
||||
'group_operator': None, # operator for aggregating values
|
||||
}
|
||||
|
||||
def __init__(self, string=Default, **kwargs):
|
||||
@@ -612,6 +613,7 @@ class Field(object):
|
||||
_related_help = property(attrgetter('help'))
|
||||
_related_readonly = property(attrgetter('readonly'))
|
||||
_related_groups = property(attrgetter('groups'))
|
||||
_related_group_operator = property(attrgetter('group_operator'))
|
||||
|
||||
@property
|
||||
def base_field(self):
|
||||
@@ -751,6 +753,7 @@ class Field(object):
|
||||
_column_groups = property(attrgetter('groups'))
|
||||
_column_change_default = property(attrgetter('change_default'))
|
||||
_column_deprecated = property(attrgetter('deprecated'))
|
||||
_column_group_operator = property(attrgetter('group_operator'))
|
||||
|
||||
############################################################################
|
||||
#
|
||||
@@ -1062,12 +1065,11 @@ class Boolean(Field):
|
||||
|
||||
class Integer(Field):
|
||||
type = 'integer'
|
||||
_slots = {
|
||||
'group_operator': None, # operator for aggregating values
|
||||
}
|
||||
|
||||
_related_group_operator = property(attrgetter('group_operator'))
|
||||
_column_group_operator = property(attrgetter('group_operator'))
|
||||
def _setup_regular_base(self, model):
|
||||
super(Integer, self)._setup_regular_base(model)
|
||||
if not self.group_operator:
|
||||
self.group_operator = 'sum'
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
if isinstance(value, dict):
|
||||
@@ -1101,12 +1103,16 @@ class Float(Field):
|
||||
type = 'float'
|
||||
_slots = {
|
||||
'_digits': None, # digits argument passed to class initializer
|
||||
'group_operator': None, # operator for aggregating values
|
||||
}
|
||||
|
||||
def __init__(self, string=Default, digits=Default, **kwargs):
|
||||
super(Float, self).__init__(string=string, _digits=digits, **kwargs)
|
||||
|
||||
def _setup_regular_base(self, model):
|
||||
super(Float, self)._setup_regular_base(model)
|
||||
if not self.group_operator:
|
||||
self.group_operator = 'sum'
|
||||
|
||||
@property
|
||||
def digits(self):
|
||||
if callable(self._digits):
|
||||
@@ -1116,13 +1122,11 @@ class Float(Field):
|
||||
return self._digits
|
||||
|
||||
_related__digits = property(attrgetter('_digits'))
|
||||
_related_group_operator = property(attrgetter('group_operator'))
|
||||
|
||||
_description_digits = property(attrgetter('digits'))
|
||||
|
||||
_column_digits = property(lambda self: not callable(self._digits) and self._digits)
|
||||
_column_digits_compute = property(lambda self: callable(self._digits) and self._digits)
|
||||
_column_group_operator = property(attrgetter('group_operator'))
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
# apply rounding here, otherwise value in cache may be wrong!
|
||||
@@ -1147,24 +1151,23 @@ class Monetary(Field):
|
||||
type = 'monetary'
|
||||
_slots = {
|
||||
'currency_field': None,
|
||||
'group_operator': None, # operator for aggregating values
|
||||
}
|
||||
|
||||
def __init__(self, string=Default, currency_field=Default, **kwargs):
|
||||
super(Monetary, self).__init__(string=string, currency_field=currency_field, **kwargs)
|
||||
|
||||
_related_currency_field = property(attrgetter('currency_field'))
|
||||
_related_group_operator = property(attrgetter('group_operator'))
|
||||
|
||||
_description_currency_field = property(attrgetter('currency_field'))
|
||||
|
||||
_column_currency_field = property(attrgetter('currency_field'))
|
||||
_column_group_operator = property(attrgetter('group_operator'))
|
||||
|
||||
def _setup_regular_base(self, model):
|
||||
super(Monetary, self)._setup_regular_base(model)
|
||||
if not self.currency_field:
|
||||
self.currency_field = 'currency_id'
|
||||
if not self.group_operator:
|
||||
self.group_operator = 'sum'
|
||||
|
||||
def _setup_regular_full(self, model):
|
||||
super(Monetary, self)._setup_regular_full(model)
|
||||
@@ -2067,6 +2070,11 @@ class Id(Field):
|
||||
def __set__(self, record, value):
|
||||
raise TypeError("field 'id' cannot be assigned")
|
||||
|
||||
# null object for fields, extend as necessary for use in context
|
||||
MissingField = namedtuple('MissingField', 'group_operator')
|
||||
missing_field = MissingField(
|
||||
group_operator=None,
|
||||
)
|
||||
|
||||
# imported here to avoid dependency cycle issues
|
||||
from openerp import SUPERUSER_ID
|
||||
|
||||
+3
-4
@@ -2101,10 +2101,9 @@ class BaseModel(object):
|
||||
|
||||
aggregated_fields = [
|
||||
f for f in fields
|
||||
if f not in ('id', 'sequence')
|
||||
if f != 'sequence'
|
||||
if f not in groupby_fields
|
||||
if f in self._fields
|
||||
if self._fields[f].type in ('integer', 'float', 'monetary')
|
||||
if self._fields.get(f, missing_field).group_operator
|
||||
if getattr(self._fields[f].base_field.column, '_classic_write', False)
|
||||
]
|
||||
|
||||
@@ -6249,4 +6248,4 @@ def _normalize_ids(arg, atoms=set(IdType)):
|
||||
|
||||
# keep those imports here to avoid dependency cycle errors
|
||||
from .osv import expression
|
||||
from .fields import Field, SpecialValue, FailedValue
|
||||
from .fields import Field, SpecialValue, FailedValue, missing_field
|
||||
|
||||
Reference in New Issue
Block a user