In Odoo’s flexible interface, the Kanban view serves as a powerful visual tool for managing records through different workflow stages. However, standard Odoo implementations often present all stages to all users, which can create confusion when different user roles need to see only relevant workflow stages. This comprehensive guide walks you through implementing conditional Kanban stage visibility in Odoo 15, allowing you to tailor the user experience based on specific criteria like user roles, record states, or custom business rules.
Why Control Kanban Stage Visibility?
Before diving into implementation, let’s understand why this capability matters:
- Reduced Cognitive Load: Users only see stages relevant to their current responsibilities
- Streamlined Workflows: Prevents users from attempting actions they shouldn’t perform
- Enhanced Security: Hides stages that might expose sensitive workflow information
- Improved User Experience: Creates a cleaner, more focused interface for each user type
Unlike simply hiding fields or buttons, stage visibility control operates at the data level—ensuring users never even see stages they shouldn’t interact with, rather than just disabling them after they appear.
Behind the Scenes: How Kanban Stage Visibility Works
Understanding Odoo’s underlying architecture is crucial for implementing this feature correctly. Here’s what happens behind the scenes when controlling Kanban stage visibility:
The Core Approach: Intercepting Data Retrieval
Odoo’s Kanban view displays stages by retrieving data through two critical methods:
read_group()
– Used to fetch the grouped data for Kanban columnssearch()
– Used to fetch records within each stage
This happens by overriding these methods to filter out unwanted stages before the data reaches the user interface. This approach ensures:
- Stages are hidden at the data layer, not just visually
- No unnecessary database queries for hidden stages
- Complete separation of concerns (business logic stays in Python)
- Compatibility with Odoo’s standard caching mechanisms
Visual Representation of the Data Flow
The approach is
User Request
│
▼
Odoo Controller
│
▼
[read_group() & search() methods] ← Your custom logic lives here
│ (Data filtering happens here)
▼
Filtered Data
│
▼
Kanban View Renderer
│
▼
Browser (Only shows permitted stages)
This approach provides significant advantages over client-side filtering:
- Security: Hidden stages never reach the browser
- Performance: Reduced data payload improves loading times
- Consistency: Works across all entry points (not just the main view)
Implementation Guide
Let’s walk through implementing conditional stage visibility in your Odoo 15 module. We’ll focus on a common use case: hiding specific stages based on user roles.
Step 1: Identifying the Target Model
First, identify which model’s Kanban view you want to modify. In our example, we’ll work with a custom model called project.task
(though the same principles apply to any model with stages).
Create a new file in your module: models/project_task.py
Step 2: Overriding read_group() Method
The read_group()
method is responsible for retrieving the stage groups that become your Kanban columns. This is where we’ll filter out unwanted stages:
from odoo import api, models, _
class ProjectTask(models.Model):
_inherit = 'project.task'
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
# First, call the parent method to get the standard results
result = super(ProjectTask, self).read_group(
domain, fields, groupby, offset=offset,
limit=limit, orderby=orderby, lazy=lazy
)
# Only apply our filtering when grouping by stage_id
if 'stage_id' in groupby:
# Get the stage we want to potentially hide
restricted_stage = self.env.ref('your_module_name.restricted_stage_key', raise_if_not_found=False)
# Determine if current user should see this stage
# In this example, we're checking if user is in a specific group
user_can_see_restricted = self.env.user.has_group('your_module_name.group_admin')
# If we found the stage and user shouldn't see it, filter it out
if restricted_stage and not user_can_see_restricted:
# Filter out the restricted stage from results
result = [r for r in result
if not r.get('stage_id') or r['stage_id'][0] != restricted_stage.id]
return result
Step 3: Overriding search() Method
The read_group()
method handles the column headers, but the search()
method retrieves the actual records within each stage. We need to override this too to prevent records from appearing in hidden stages:
@api.model
def search(self, domain, offset=0, limit=None, order=None, count=False):
# Get the stage we want to potentially hide
restricted_stage = self.env.ref('your_module_name.restricted_stage_key', raise_if_not_found=False)
# Determine if current user should see this stage
user_can_see_restricted = self.env.user.has_group('your_module_name.group_admin')
# If stage exists and user shouldn't see it, modify the domain
if restricted_stage and not user_can_see_restricted:
# Add condition to exclude the restricted stage
new_domain = domain + [('stage_id', '!=', restricted_stage.id)]
return super(ProjectTask, self).search(
new_domain, offset=offset,
limit=limit, order=order, count=count
)
# Otherwise, proceed with standard search
return super(ProjectTask, self).search(
domain, offset=offset,
limit=limit, order=order, count=count
)
Key Design Decisions Explained:
- Two-Point Filtering: By overriding both
read_group()
andsearch()
, we ensure:- The stage column never appears (
read_group
) - Records never appear in that column (
search
)
- The stage column never appears (
- Reference-Based Stage Identification: Using
env.ref()
with XML IDs makes the code:- More maintainable (no hardcoded IDs)
- Safer (fails gracefully if stage doesn’t exist)
- Clearer (shows the stage’s purpose via its key)
- Conditional Application: The filtering only applies when:
- The stage exists in the database
- The specific condition (user group check) is met
Step 4: Creating Custom Action with Pre-Filtered Domain
Sometimes you need a dedicated action that always shows filtered stages. Here’s how to create one:
@api.model
def action_filtered_tasks(self):
"""Action to open tasks with restricted stage filtered out"""
# Determine if current user should see restricted stage
user_can_see_restricted = self.env.user.has_group('your_module_name.group_admin')
# Get the restricted stage
restricted_stage = self.env.ref('your_module_name.restricted_stage_key', raise_if_not_found=False)
# Build domain
domain = []
if restricted_stage and not user_can_see_restricted:
domain = [('stage_id', '!=', restricted_stage.id)]
# Build context
context = {
'group_by': 'stage_id',
'default_view_mode': 'kanban',
'user_can_see_restricted': user_can_see_restricted,
}
return {
'name': _('My Tasks'),
'type': 'ir.actions.act_window',
'res_model': 'project.task',
'view_mode': 'kanban,tree,form',
'views': [
(self.env.ref('your_module_name.view_task_kanban').id, 'kanban'),
(False, 'tree'),
(False, 'form')
],
'domain': domain,
'context': context,
'help': '''<p class="o_view_nocontent_smiling_face">No tasks found!</p>
<p>Only tasks in visible stages are displayed here.</p>''',
}
Advanced Implementation Scenarios
Scenario 1: Hiding Stages Based on Record Properties
Sometimes you need to hide stages based on properties of the records themselves, not just user roles:
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
result = super().read_group(domain, fields, groupby, offset, limit, orderby, lazy)
if 'stage_id' in groupby:
# Get current user's department
user_department = self.env.user.department_id
# Find stages associated with departments
department_stages = self.env['project.task.stage'].search([
('department_ids', '!=', False)
])
# Filter out stages not relevant to user's department
if department_stages and user_department:
result = [
r for r in result
if not r.get('stage_id') or
not self.env['project.task.stage'].browse(r['stage_id'][0]).department_ids or
user_department in self.env['project.task.stage'].browse(r['stage_id'][0]).department_ids
]
return result
Scenario 2: Context-Based Stage Visibility
You might want different visibility rules depending on how the view is accessed:
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
result = super().read_group(domain, fields, groupby, offset, limit, orderby, lazy)
if 'stage_id' in groupby:
# Check if we're in a specific context
if self.env.context.get('hide_restricted_stages', True):
restricted_stage = self.env.ref('your_module_name.restricted_stage', False)
if restricted_stage:
result = [r for r in result
if not r.get('stage_id') or r['stage_id'][0] != restricted_stage.id]
return result
This allows you to control visibility through URL parameters or action contexts.
Best Practices and Considerations
Performance Optimization
- Cache Reference Lookups: If you’re using the same stage references repeatedly, cache them at the class level
- Minimize Database Queries: Avoid making database calls inside loops
- Use Efficient Filtering: Prefer list comprehensions over multiple filter calls
# Good practice: Cache the stage reference
RESTRICTED_STAGE_ID = None
@api.model
def _get_restricted_stage_id(self):
global RESTRICTED_STAGE_ID
if RESTRICTED_STAGE_ID is None:
stage = self.env.ref('your_module_name.restricted_stage', raise_if_not_found=False)
RESTRICTED_STAGE_ID = stage.id if stage else None
return RESTRICTED_STAGE_ID
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
result = super().read_group(domain, fields, groupby, offset, limit, orderby, lazy)
if 'stage_id' in groupby:
restricted_stage_id = self._get_restricted_stage_id()
if restricted_stage_id and not self.user_can_access_restricted():
result = [r for r in result
if not r.get('stage_id') or r['stage_id'][0] != restricted_stage_id]
return result
Error Handling and Graceful Degradation
Always design your code to work even if stages are missing:
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
result = super().read_group(domain, fields, groupby, offset, limit, orderby, lazy)
if 'stage_id' in groupby:
try:
# Attempt to get the stage
restricted_stage = self.env.ref('your_module_name.restricted_stage')
# Only filter if user shouldn't see it
if not self.user_can_see_restricted():
result = [r for r in result
if not r.get('stage_id') or r['stage_id'][0] != restricted_stage.id]
except ValueError:
# Stage doesn't exist - proceed without filtering
pass
return result
Conclusion
Implementing conditional Kanban stage visibility in Odoo 15 is a powerful way to tailor the user experience to specific roles and scenarios. By understanding and properly overriding the read_group()
and search()
methods, you can create a more focused, efficient interface that guides users through appropriate workflows while hiding irrelevant options.
The key to success lies in:
- Understanding Odoo’s underlying data flow
- Implementing filtering at the appropriate data layer
- Handling edge cases and missing references gracefully
- Optimizing for performance in your filtering logic
When done correctly, this technique significantly enhances user experience without compromising data integrity or security. Your users will appreciate the cleaner interface, and your business processes will benefit from reduced errors and more focused workflows.
Remember that the most effective implementations are those that blend seamlessly into the Odoo experience—users shouldn’t even notice the filtering is happening, they’ll just find their interface more intuitive and relevant to their daily tasks.