diff --git a/openerp/addons/test_read_group/models.py b/openerp/addons/test_read_group/models.py index 98b56a81195..c8456edd5a9 100644 --- a/openerp/addons/test_read_group/models.py +++ b/openerp/addons/test_read_group/models.py @@ -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') diff --git a/openerp/addons/test_read_group/tests/__init__.py b/openerp/addons/test_read_group/tests/__init__.py index 6923cb9047e..e0cde2af5e1 100644 --- a/openerp/addons/test_read_group/tests/__init__.py +++ b/openerp/addons/test_read_group/tests/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- from . import test_empty +from . import test_group_operator diff --git a/openerp/addons/test_read_group/tests/test_group_operator.py b/openerp/addons/test_read_group/tests/test_group_operator.py new file mode 100644 index 00000000000..dcdaf1159e8 --- /dev/null +++ b/openerp/addons/test_read_group/tests/test_group_operator.py @@ -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) diff --git a/openerp/fields.py b/openerp/fields.py index 8e995a8f344..a5da8a039b3 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -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 diff --git a/openerp/models.py b/openerp/models.py index ae966291c27..8f6f1de0e1d 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -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