509 lines
16 KiB
Markdown
509 lines
16 KiB
Markdown
# 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:
|
|
```javascript
|
|
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`
|
|
```python
|
|
{
|
|
'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`
|
|
```python
|
|
# -*- 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:**
|
|
```javascript
|
|
/** @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:**
|
|
```javascript
|
|
/** @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
|
|
<?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`
|
|
|
|
```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
|
|
<?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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```mermaid
|
|
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. |