diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index 1813e76b26c..a33f80e9d9b 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -397,6 +397,12 @@ class IrModelFields(models.Model): "specified as a Python expression defining a list of triplets. " "For example: [('color','=','red')]") groups = fields.Many2many('res.groups', 'ir_model_fields_group_rel', 'field_id', 'group_id') # CLEANME unimplemented field (empty table) + group_expand = fields.Boolean(string="Expand Groups", + help="If checked, all the records of the target model will be included\n" + "in a grouped result (e.g. 'Group By' filters, Kanban columns, etc.).\n" + "Note that it can significantly reduce performance if the target model\n" + "of the field contains a lot of records; usually used on models with\n" + "few records (e.g. Stages, Job Positions, Event Types, etc.).") selectable = fields.Boolean(default=True) modules = fields.Char(compute='_in_modules', string='In Apps', help='List of modules in which the field is defined') relation_table = fields.Char(help="Used for custom many2many fields to define a custom relation table name") @@ -994,6 +1000,7 @@ class IrModelFields(models.Model): attrs['comodel_name'] = field_data['relation'] attrs['ondelete'] = field_data['on_delete'] attrs['domain'] = safe_eval(field_data['domain'] or '[]') + attrs['group_expand'] = '_read_group_expand_full' if field_data['group_expand'] else None elif field_data['ttype'] == 'one2many': if not self.pool.loaded and not ( field_data['relation'] in self.env and ( diff --git a/odoo/addons/base/tests/test_ir_model.py b/odoo/addons/base/tests/test_ir_model.py index eb52e8c8ad6..bfa6ea7fd5f 100644 --- a/odoo/addons/base/tests/test_ir_model.py +++ b/odoo/addons/base/tests/test_ir_model.py @@ -186,6 +186,19 @@ class TestIrModel(SavepointCase): cls.registry.enter_test_mode(cls.cr) cls.addClassCleanup(cls.registry.leave_test_mode) + # model and records for banana stages + cls.env['ir.model'].create({ + 'name': 'Banana Ripeness', + 'model': 'x_banana_ripeness', + 'field_id': [ + (0, 0, {'name': 'x_name', 'ttype': 'char', 'field_description': 'Name'}), + ] + }) + # stage values are pairs (id, display_name) + cls.ripeness_green = cls.env['x_banana_ripeness'].name_create('Green') + cls.ripeness_okay = cls.env['x_banana_ripeness'].name_create('Okay, I guess?') + cls.ripeness_gone = cls.env['x_banana_ripeness'].name_create('Walked away on its own') + # model and records for bananas cls.bananas_model = cls.env['ir.model'].create({ 'name': 'Bananas', @@ -194,6 +207,9 @@ class TestIrModel(SavepointCase): (0, 0, {'name': 'x_name', 'ttype': 'char', 'field_description': 'Name'}), (0, 0, {'name': 'x_length', 'ttype': 'float', 'field_description': 'Length'}), (0, 0, {'name': 'x_color', 'ttype': 'integer', 'field_description': 'Color'}), + (0, 0, {'name': 'x_ripeness_id', 'ttype': 'many2one', + 'field_description': 'Ripeness','relation': 'x_banana_ripeness', + 'group_expand': True}) ] }) # add non-stored field that is not valid in order @@ -206,6 +222,8 @@ class TestIrModel(SavepointCase): 'depends': 'x_color', 'compute': "for banana in self:\n banana['x_is_yellow'] = banana.x_color == 9" }) + # default stage is ripeness_green + cls.env['ir.default'].set('x_bananas', 'x_ripeness_id', cls.ripeness_green[0]) cls.env['x_bananas'].create([{ 'x_name': 'Banana #1', 'x_length': 3.14159, @@ -271,3 +289,23 @@ class TestIrModel(SavepointCase): bananas = self.env['x_bananas'].search([]) self.assertEqual(bananas.mapped('x_name'), names, 'failed to order by %s' % order) + + def test_group_expansion(self): + """Check that the basic custom group expansion works.""" + groups = self.env['x_bananas'].read_group(domain=[], + fields=['x_ripeness_id'], + groupby=['x_ripeness_id']) + expected = [{ + 'x_ripeness_id': self.ripeness_green, + 'x_ripeness_id_count': 3, + '__domain': [('x_ripeness_id', '=', self.ripeness_green[0])], + }, { + 'x_ripeness_id': self.ripeness_okay, + 'x_ripeness_id_count': 0, + '__domain': [('x_ripeness_id', '=', self.ripeness_okay[0])], + }, { + 'x_ripeness_id': self.ripeness_gone, + 'x_ripeness_id_count': 0, + '__domain': [('x_ripeness_id', '=', self.ripeness_gone[0])], + }] + self.assertEqual(groups, expected, 'should include 2 empty ripeness stages') diff --git a/odoo/addons/base/views/ir_model_views.xml b/odoo/addons/base/views/ir_model_views.xml index 1505b517150..f534def632b 100644 --- a/odoo/addons/base/views/ir_model_views.xml +++ b/odoo/addons/base/views/ir_model_views.xml @@ -274,6 +274,9 @@ attrs="{'required': [('ttype','in',['many2one','one2many','many2many'])], 'readonly': [('ttype','not in',['many2one','one2many','many2many'])], 'invisible': [('ttype','not in',['many2one','one2many','many2many'])]}"/> + diff --git a/odoo/models.py b/odoo/models.py index 9eee8d3b354..4b04263bba0 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -1754,6 +1754,11 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): """ cls.pool._clear_cache() + @api.model + def _read_group_expand_full(self, groups, domain, order): + """Extend the group to include all targer records by default.""" + return groups.search([], order=order) + @api.model def _read_group_fill_results(self, domain, groupby, remaining_groupbys, aggregated_fields, count_field,