[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:
xmo-odoo
2016-06-29 17:52:30 +02:00
committed by GitHub
parent 51a8bc1a60
commit b3feb2358c
5 changed files with 231 additions and 16 deletions
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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