list_view_group_select/ARCHITECTURE.md

16 KiB

List View Group Select - Architecture & Implementation Plan

Module Overview

Name: list_view_group_select
Version: 17.0.1.0.0
Category: Web
Summary: Adds group selection feature to Odoo 17 list views with recursive selection support

Features

  1. Group Header Checkbox - Add a checkbox in each group header to select all records in that group
  2. Recursive Selection - Selecting a parent group selects all records in nested sub-groups
  3. Action Menu Integration - Add "Select All Groups" option in the action menu
  4. Visual Feedback - Show selection state (none/partial/full) for each group
  5. Compatible with Bulk Operations - Works seamlessly with Export, Archive, Delete, etc.

Technical Architecture

Component Analysis (from Odoo Core)

1. ListRenderer (list_renderer.js)

Key Observations:

  • Line 674-682: selectAll getter checks if all records are selected
  • Line 1867-1873: toggleSelection() handles selection of all records
  • Line 1875-1887: toggleRecordSelection() handles individual record selection
  • Line 1889-1900: toggleRecordShiftSelection() handles range selection
  • Line 661-673: nbRecordsInGroup() recursively counts records in groups
  • Line 1852-1857: onGroupHeaderClicked() handles group header clicks

2. ListController (list_controller.js)

Key Observations:

  • Line 317-362: getStaticActionMenuItems() defines action menu items
  • Line 393-406: onSelectDomain() handles domain selection
  • Line 408-413: onUnselectAll() clears all selections

3. Group Structure

Groups have the following structure:

group = {
    list: {
        records: [],      // Direct records in this group
        groups: [],       // Sub-groups (for nested grouping)
        isGrouped: bool   // Whether this level has sub-groups
    },
    isFolded: bool,
    aggregates: {},
    displayName: string
}

Module File Structure

customaddons/list_view_group_select/
├── __init__.py                          # Python package init (empty)
├── __manifest__.py                      # Module manifest
├── README.md                            # User documentation
├── ARCHITECTURE.md                      # This file
├── static/
│   ├── description/
│   │   └── icon.png                    # Module icon (optional)
│   └── src/
│       ├── js/
│       │   ├── list_renderer_group_select.js     # Extended ListRenderer
│       │   └── list_controller_group_select.js   # Extended ListController
│       ├── xml/
│       │   └── list_renderer_templates.xml       # Template inheritance
│       └── scss/
│           └── list_view_group_select.scss       # Styling
└── views/
    └── webclient_templates.xml          # Asset bundle definition

Implementation Plan

Phase 1: Module Structure Setup

File: __manifest__.py

{
    'name': 'List View Group Select',
    'version': '17.0.1.0.0',
    'category': 'Web',
    'summary': 'Add group selection feature to list views',
    'description': """
        List View Group Select
        ======================
        Extends Odoo list views with group selection capabilities:
        * Add checkbox to group headers for selecting all records in a group
        * Recursive selection for nested groups
        * "Select All Groups" action in the action menu
        * Compatible with all bulk operations (Export, Archive, Delete, etc.)
    """,
    'author': 'Your Company',
    'website': 'https://www.example.com',
    'license': 'LGPL-3',
    'depends': ['web'],
    'data': [
        'views/webclient_templates.xml',
    ],
    'assets': {
        'web.assets_backend': [
            'list_view_group_select/static/src/js/list_renderer_group_select.js',
            'list_view_group_select/static/src/js/list_controller_group_select.js',
            'list_view_group_select/static/src/xml/list_renderer_templates.xml',
            'list_view_group_select/static/src/scss/list_view_group_select.scss',
        ],
    },
    'installable': True,
    'application': False,
    'auto_install': False,
}

File: __init__.py

# -*- coding: utf-8 -*-
# Empty file - this is a JavaScript-only module

Phase 2: JavaScript Implementation

File: static/src/js/list_renderer_group_select.js

Key Functions to Implement:

  1. getAllRecordsInGroup(group) - Recursive helper

    • Traverses group hierarchy
    • Collects all records including nested sub-groups
    • Returns flat array of all records
  2. toggleGroupSelection(group, ev) - Main selection logic

    • Gets all records using getAllRecordsInGroup()
    • Toggles selection state for all records
    • Updates selection state tracking
  3. getGroupSelectionState(group) - Visual feedback

    • Returns: 'none', 'partial', 'full'
    • Checks if 0, some, or all records are selected
    • Used for checkbox visual state (unchecked/indeterminate/checked)
  4. isGroupSelected(group) - Helper for templates

    • Returns boolean for checkbox state
    • Handles partially selected state

Inheritance Pattern:

/** @odoo-module **/

import { ListRenderer } from "@web/views/list/list_renderer";
import { patch } from "@web/core/utils/patch";

patch(ListRenderer.prototype, {
    // Override/extend methods here
    
    getAllRecordsInGroup(group) {
        // Recursive implementation
    },
    
    toggleGroupSelection(group, ev) {
        // Main selection logic
    },
    
    getGroupSelectionState(group) {
        // Return 'none', 'partial', or 'full'
    },
    
    // Other helper methods...
});

File: static/src/js/list_controller_group_select.js

Key Functions to Implement:

  1. Extend getStaticActionMenuItems()

    • Add new "Select All in Visible Groups" action
    • Position it appropriately (sequence ~5, before Export)
    • Only show when list is grouped
  2. onSelectAllGroups() - New method

    • Iterates through all visible groups
    • Uses getAllRecordsInGroup() from renderer
    • Selects all records in all groups

Inheritance Pattern:

/** @odoo-module **/

import { ListController } from "@web/views/list/list_controller";
import { patch } from "@web/core/utils/patch";
import { _t } from "@web/core/l10n/translation";

patch(ListController.prototype, {
    getStaticActionMenuItems() {
        const items = super.getStaticActionMenuItems();
        
        // Add group selection item
        items.selectAllGroups = {
            isAvailable: () => this.model.root.isGrouped,
            sequence: 5,
            icon: "fa fa-check-square-o",
            description: _t("Select All in Visible Groups"),
            callback: () => this.onSelectAllGroups(),
        };
        
        return items;
    },
    
    onSelectAllGroups() {
        // Implementation
    },
});

Phase 3: XML Template Extensions

File: static/src/xml/list_renderer_templates.xml

Approach:

  • Inherit/extend the web.ListRenderer.GroupRow template
  • Add checkbox element before the group name
  • Bind to toggleGroupSelection method
  • Apply appropriate CSS classes based on selection state

Template Structure:

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <!-- Extend group header to add selection checkbox -->
    <t t-name="web.ListRenderer.GroupRow" t-inherit="web.ListRenderer.GroupRow" t-inherit-mode="extension">
        <!-- Add checkbox before group name -->
        <xpath expr="//th[@t-att-colspan='getGroupNameCellColSpan(group)']" position="inside">
            <t t-if="hasSelectors">
                <div class="o_list_group_selector">
                    <input 
                        type="checkbox" 
                        class="o_list_record_selector"
                        t-att-checked="isGroupSelected(group) ? 'checked' : undefined"
                        t-att-indeterminate="getGroupSelectionState(group) === 'partial' ? 'indeterminate' : undefined"
                        t-on-click.stop="(ev) => this.toggleGroupSelection(group, ev)"
                        t-att-disabled="!canSelectRecord ? 'disabled' : undefined"
                    />
                </div>
            </t>
        </xpath>
    </t>
</templates>

Phase 4: Styling

File: static/src/scss/list_view_group_select.scss

.o_list_group_selector {
    display: inline-block;
    margin-right: 8px;
    vertical-align: middle;
    
    input[type="checkbox"] {
        cursor: pointer;
        
        &:disabled {
            cursor: not-allowed;
            opacity: 0.5;
        }
        
        // Indeterminate state styling
        &:indeterminate {
            opacity: 0.7;
        }
    }
}

// Ensure group header has proper alignment
.o_group_header th {
    .o_list_group_selector {
        + * {
            display: inline-block;
        }
    }
}

Phase 5: Asset Bundle Registration

File: views/webclient_templates.xml

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <template id="assets_backend" inherit_id="web.assets_backend">
        <xpath expr="." position="inside">
            <script type="text/javascript" src="/list_view_group_select/static/src/js/list_renderer_group_select.js"/>
            <script type="text/javascript" src="/list_view_group_select/static/src/js/list_controller_group_select.js"/>
            <link rel="stylesheet" type="text/scss" href="/list_view_group_select/static/src/scss/list_view_group_select.scss"/>
        </xpath>
    </template>
</odoo>

Key Implementation Details

Recursive Record Collection Algorithm

getAllRecordsInGroup(group) {
    const records = [];
    
    if (group.list.isGrouped) {
        // Has sub-groups - recurse
        for (const subGroup of group.list.groups) {
            records.push(...this.getAllRecordsInGroup(subGroup));
        }
    } else {
        // Leaf group - collect direct records
        records.push(...group.list.records);
    }
    
    return records;
}

Selection State Calculation

getGroupSelectionState(group) {
    const allRecords = this.getAllRecordsInGroup(group);
    if (allRecords.length === 0) {
        return 'none';
    }
    
    const selectedCount = allRecords.filter(r => r.selected).length;
    
    if (selectedCount === 0) {
        return 'none';
    } else if (selectedCount === allRecords.length) {
        return 'full';
    } else {
        return 'partial';
    }
}

Group Selection Toggle

async toggleGroupSelection(group, ev) {
    if (!this.canSelectRecord) {
        return;
    }
    
    ev.stopPropagation(); // Prevent group fold/unfold
    
    const allRecords = this.getAllRecordsInGroup(group);
    const state = this.getGroupSelectionState(group);
    
    // If none or partial selected, select all; if all selected, deselect all
    const shouldSelect = state !== 'full';
    
    for (const record of allRecords) {
        record.toggleSelection(shouldSelect);
    }
    
    // Ensure domain selection is turned off
    this.props.list.selectDomain(false);
}

Testing Strategy

Test Cases

  1. Single-Level Grouping

    • Group by single field (e.g., Status)
    • Select one group
    • Verify all records in that group are selected
    • Verify bulk actions work correctly
  2. Multi-Level Grouping

    • Group by Country, then City
    • Select parent group (Country)
    • Verify all records in all cities are selected
    • Verify nested sub-groups are handled correctly
  3. Partial Selection Visual Feedback

    • Select some records in a group manually
    • Verify group checkbox shows indeterminate state
    • Click group checkbox
    • Verify all records in group become selected
  4. Action Menu Integration

    • Select multiple groups using checkboxes
    • Use "Select All in Visible Groups" action
    • Verify all visible groups are selected
    • Test with folded groups
  5. Bulk Operations

    • Select one or more groups
    • Export selected records
    • Archive selected records
    • Delete selected records
    • Verify correct record count in confirmations
  6. Edge Cases

    • Empty groups
    • Mixed selection (some groups + individual records)
    • Pagination within groups
    • Group folding/unfolding with selection

Integration Points

With Odoo Core

  • ListRenderer.toggleSelection() - Keep existing functionality
  • ListController actions - Extend, don't replace
  • Selection state - Use existing record.selected property
  • Bulk operations - No changes needed, they use selected records

Compatibility

  • Works with all models that support list view
  • Compatible with existing list view configurations
  • No database changes required (JavaScript-only)
  • Can be enabled/disabled per module install

Performance Considerations

  1. Recursive Traversal

    • Cache group record collections if needed
    • Limit recursion depth (Odoo typically has 2-3 group levels max)
  2. Selection Updates

    • Use batch updates where possible
    • Avoid unnecessary re-renders
  3. Large Datasets

    • Respects existing pagination
    • Only affects visible records
    • Domain selection still available for very large datasets

User Documentation (README.md)

Installation

  1. Copy module to addons/ or custom_addons/
  2. Update apps list
  3. Install "List View Group Select"

Usage

  1. Open any list view and group by field(s)
  2. Group headers will show selection checkboxes
  3. Click checkbox to select all records in that group (including sub-groups)
  4. Use action menu "Select All in Visible Groups" to select everything
  5. Use standard bulk operations (Export, Archive, Delete) on selected records

Features

  • ✓ Single-level group selection
  • ✓ Multi-level nested group selection
  • ✓ Visual feedback (indeterminate state for partial selection)
  • ✓ Action menu integration
  • ✓ Works with all bulk operations
  • ✓ Compatible with existing selection methods

Future Enhancements (Optional)

  1. Configuration Options

    • Enable/disable per model
    • Customize checkbox position
    • Add keyboard shortcuts
  2. Advanced Features

    • "Select visible records" in folded groups
    • Selection persistence across page loads
    • Batch operations on specific groups
  3. UI Improvements

    • Group selection summary in action menu
    • Quick selection dropdown
    • Selection highlights

Workflow Diagram

graph TD
    A[User clicks group checkbox] --> B{Can select?}
    B -->|No| C[Show notification]
    B -->|Yes| D[Get all records in group]
    D --> E{Is grouped?}
    E -->|Yes| F[Recurse into sub-groups]
    F --> D
    E -->|No| G[Collect leaf records]
    G --> H[Calculate current state]
    H --> I{State?}
    I -->|None/Partial| J[Select all records]
    I -->|Full| K[Deselect all records]
    J --> L[Update UI]
    K --> L
    L --> M[Enable bulk actions]

Summary

This architecture provides a clean, maintainable solution that:

  • Extends Odoo core functionality without modifying it
  • Uses standard Odoo patterns (patch, inheritance)
  • Maintains compatibility with existing features
  • Provides intuitive user experience
  • Handles edge cases properly
  • Is performant and scalable

The implementation follows Odoo 17 best practices and OWL component patterns.