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
- Group Header Checkbox - Add a checkbox in each group header to select all records in that group
- Recursive Selection - Selecting a parent group selects all records in nested sub-groups
- Action Menu Integration - Add "Select All Groups" option in the action menu
- Visual Feedback - Show selection state (none/partial/full) for each group
- 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:
selectAllgetter 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:
-
getAllRecordsInGroup(group)- Recursive helper- Traverses group hierarchy
- Collects all records including nested sub-groups
- Returns flat array of all records
-
toggleGroupSelection(group, ev)- Main selection logic- Gets all records using
getAllRecordsInGroup() - Toggles selection state for all records
- Updates selection state tracking
- Gets all records using
-
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)
-
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:
-
Extend
getStaticActionMenuItems()- Add new "Select All in Visible Groups" action
- Position it appropriately (sequence ~5, before Export)
- Only show when list is grouped
-
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.GroupRowtemplate - Add checkbox element before the group name
- Bind to
toggleGroupSelectionmethod - 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
-
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
-
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
-
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
-
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
-
Bulk Operations
- Select one or more groups
- Export selected records
- Archive selected records
- Delete selected records
- Verify correct record count in confirmations
-
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.selectedproperty - 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
-
Recursive Traversal
- Cache group record collections if needed
- Limit recursion depth (Odoo typically has 2-3 group levels max)
-
Selection Updates
- Use batch updates where possible
- Avoid unnecessary re-renders
-
Large Datasets
- Respects existing pagination
- Only affects visible records
- Domain selection still available for very large datasets
User Documentation (README.md)
Installation
- Copy module to
addons/orcustom_addons/ - Update apps list
- Install "List View Group Select"
Usage
- Open any list view and group by field(s)
- Group headers will show selection checkboxes
- Click checkbox to select all records in that group (including sub-groups)
- Use action menu "Select All in Visible Groups" to select everything
- 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)
-
Configuration Options
- Enable/disable per model
- Customize checkbox position
- Add keyboard shortcuts
-
Advanced Features
- "Select visible records" in folded groups
- Selection persistence across page loads
- Batch operations on specific groups
-
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.