Sale Lead State Activity Documentation¶
Overview¶
The Sale Lead State Activity module (sale.lead.state.activ) is a lightweight tracking model that records historical state transitions for leads. This model provides a simple audit trail of when leads move through different stages of the sales pipeline, enabling analytics on time-in-state, conversion velocity, and bottleneck identification in the lead management process.
Model Information¶
Model Name: sale.lead.state.activ
Display Name: State
Key Fields: None (simple tracking model)
Features¶
- ❌ Audit logging enabled (
_audit_log) - ❌ Multi-company support (
company_id) - ❌ Full-text content search (
_content_search) - ❌ Unique key constraint
Purpose and Use Case¶
This model serves as a historical state tracker for lead progression. Each time a lead's state changes, a new record is created in this model capturing: - Which lead changed state - What state it entered - When the transition occurred
This simple but powerful design enables: - Time-in-state analysis: Calculate how long leads spend in each stage - Conversion metrics: Measure velocity through the pipeline - Bottleneck detection: Identify stages where leads get stuck - Historical reporting: Reconstruct lead journey over time
Key Fields Reference¶
All Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
lead_id |
Many2One | ❌ | Reference to the sale.lead record |
state |
Selection | ❌ | The state the lead entered |
date |
Date | ❌ | Date when the lead entered this state |
State Values¶
The state field uses the same values as the sale.lead model:
| State | Description |
|---|---|
new |
Lead entered "new" state |
qualified |
Lead was qualified |
pending_initial_meeting |
First meeting scheduled |
waiting_for_client |
Awaiting client response |
pending_next_meeting |
Follow-up meeting scheduled |
preparing_quotation |
Preparing quote/proposal |
converted |
Converted to opportunity |
on_hold |
Placed on hold |
won |
Successfully won |
lost |
Opportunity lost |
voided |
Invalid/spam lead |
referred |
Referred to another party |
How It Works¶
Automatic State Tracking¶
When a lead's state is updated, a new state activity record is automatically created (typically via triggers or model write overrides in the parent sale.lead model):
# Example of state change triggering state activity creation
# (Implementation would be in sale.lead model)
lead.write({"state": "qualified"})
# Automatically creates:
get_model("sale.lead.state.activ").create({
"lead_id": lead.id,
"state": "qualified",
"date": datetime.now().strftime("%Y-%m-%d")
})
Reading State History¶
Access the complete state history of a lead:
lead = get_model("sale.lead").browse(lead_id)
# View all state transitions chronologically
for state_activ in lead.state_activ:
print(f"{state_activ.date}: {state_activ.state}")
# Output:
# 2025-01-01: new
# 2025-01-05: qualified
# 2025-01-08: pending_initial_meeting
# 2025-01-12: converted
API Methods¶
1. Create State Activity Record¶
Method: create(vals, context)
Creates a new state activity record (typically called automatically, not manually).
Parameters:
vals = {
"lead_id": int, # Required: Lead ID
"state": str, # State entered
"date": str # Date in YYYY-MM-DD format
}
Returns: int - New state activity record ID
Example:
# Manual creation (normally automatic)
state_activ_id = get_model("sale.lead.state.activ").create({
"lead_id": 123,
"state": "qualified",
"date": "2025-01-05"
})
2. Query State History¶
Method: search(condition, context) and search_browse(condition, context)
Query state activity records for analysis.
Example:
# Find all state transitions for a specific lead
lead_history = get_model("sale.lead.state.activ").search_browse([
["lead_id", "=", lead_id]
], order="date asc")
for record in lead_history:
print(f"{record.date}: {record.state}")
Search Functions¶
Search by Lead¶
Search by State¶
Search by Date Range¶
# Find state changes in date range
condition = [
["date", ">=", "2025-01-01"],
["date", "<=", "2025-01-31"]
]
Analytics and Reporting¶
Calculate Time in Each State¶
def calculate_time_in_state(lead_id):
"""Calculate days spent in each state for a lead"""
# Get all state transitions ordered by date
states = get_model("sale.lead.state.activ").search_browse(
[["lead_id", "=", lead_id]],
order="date asc"
)
time_in_state = {}
for i, state in enumerate(states):
state_name = state.state
start_date = datetime.strptime(state.date, "%Y-%m-%d")
# Calculate end date (next state or today)
if i < len(states) - 1:
end_date = datetime.strptime(states[i+1].date, "%Y-%m-%d")
else:
end_date = datetime.now()
# Calculate days in this state
days = (end_date - start_date).days
# Accumulate if state was entered multiple times
if state_name in time_in_state:
time_in_state[state_name] += days
else:
time_in_state[state_name] = days
return time_in_state
# Usage
result = calculate_time_in_state(123)
# Returns: {"new": 5, "qualified": 3, "pending_initial_meeting": 7, "converted": 0}
Conversion Velocity Analysis¶
def analyze_conversion_velocity(start_date, end_date):
"""Analyze average time from new to converted"""
# Find all leads converted in date range
converted_states = get_model("sale.lead.state.activ").search_browse([
["state", "=", "converted"],
["date", ">=", start_date],
["date", "<=", end_date]
])
velocities = []
for conv_state in converted_states:
# Find first "new" state for this lead
first_state = get_model("sale.lead.state.activ").search_browse([
["lead_id", "=", conv_state.lead_id.id],
["state", "=", "new"]
], order="date asc", limit=1)
if first_state:
start = datetime.strptime(first_state[0].date, "%Y-%m-%d")
end = datetime.strptime(conv_state.date, "%Y-%m-%d")
days = (end - start).days
velocities.append(days)
if velocities:
avg_days = sum(velocities) / len(velocities)
return {
"avg_conversion_days": avg_days,
"min_conversion_days": min(velocities),
"max_conversion_days": max(velocities),
"total_conversions": len(velocities)
}
return None
# Usage
stats = analyze_conversion_velocity("2025-01-01", "2025-01-31")
# Returns: {"avg_conversion_days": 14.5, "min_conversion_days": 7, ...}
Pipeline Stage Distribution¶
def get_current_stage_distribution():
"""Get count of leads currently in each stage"""
# Get all leads
all_leads = get_model("sale.lead").search([])
distribution = {}
for lead_id in all_leads:
# Get most recent state for this lead
recent_state = get_model("sale.lead.state.activ").search_browse(
[["lead_id", "=", lead_id]],
order="date desc",
limit=1
)
if recent_state:
state_name = recent_state[0].state
distribution[state_name] = distribution.get(state_name, 0) + 1
return distribution
# Usage
pipeline = get_current_stage_distribution()
# Returns: {"new": 45, "qualified": 23, "pending_initial_meeting": 12, ...}
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.lead |
Many2One | Parent lead being tracked |
Common Use Cases¶
Use Case 1: Lead Journey Timeline¶
# Display complete timeline of a lead's journey
lead = get_model("sale.lead").browse(lead_id)
print(f"Timeline for Lead: {lead.title}")
print("-" * 50)
for state_record in lead.state_activ:
print(f"{state_record.date}: {state_record.state}")
# Output:
# Timeline for Lead: Enterprise CRM Inquiry
# --------------------------------------------------
# 2025-01-01: new
# 2025-01-03: qualified
# 2025-01-05: pending_initial_meeting
# 2025-01-10: waiting_for_client
# 2025-01-15: preparing_quotation
# 2025-01-20: converted
Use Case 2: Identify Stalled Leads¶
# Find leads stuck in "waiting_for_client" for more than 7 days
from datetime import datetime, timedelta
threshold_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
# Get all recent "waiting_for_client" states
stalled_states = get_model("sale.lead.state.activ").search_browse([
["state", "=", "waiting_for_client"],
["date", "<=", threshold_date]
])
stalled_leads = []
for state_record in stalled_states:
# Check if lead is still in this state (no newer state exists)
newer_states = get_model("sale.lead.state.activ").search([
["lead_id", "=", state_record.lead_id.id],
["date", ">", state_record.date]
])
if not newer_states:
# Lead is still in waiting_for_client state
days_stalled = (datetime.now() - datetime.strptime(state_record.date, "%Y-%m-%d")).days
stalled_leads.append({
"lead_id": state_record.lead_id.id,
"lead_title": state_record.lead_id.title,
"days_stalled": days_stalled
})
print(f"Found {len(stalled_leads)} stalled leads")
for lead in stalled_leads:
print(f" {lead['lead_title']}: {lead['days_stalled']} days")
Use Case 3: Performance Dashboard Metrics¶
# Calculate key performance metrics for sales dashboard
def get_lead_metrics(start_date, end_date):
"""Generate comprehensive lead performance metrics"""
metrics = {}
# 1. New leads in period
new_leads = get_model("sale.lead.state.activ").search([
["state", "=", "new"],
["date", ">=", start_date],
["date", "<=", end_date]
])
metrics["new_leads_count"] = len(new_leads)
# 2. Conversions in period
conversions = get_model("sale.lead.state.activ").search([
["state", "=", "converted"],
["date", ">=", start_date],
["date", "<=", end_date]
])
metrics["conversions_count"] = len(conversions)
# 3. Conversion rate
if metrics["new_leads_count"] > 0:
metrics["conversion_rate"] = (metrics["conversions_count"] / metrics["new_leads_count"]) * 100
else:
metrics["conversion_rate"] = 0
# 4. Lost leads in period
lost_leads = get_model("sale.lead.state.activ").search([
["state", "=", "lost"],
["date", ">=", start_date],
["date", "<=", end_date]
])
metrics["lost_leads_count"] = len(lost_leads)
# 5. Voided (spam) leads
voided_leads = get_model("sale.lead.state.activ").search([
["state", "=", "voided"],
["date", ">=", start_date],
["date", "<=", end_date]
])
metrics["voided_leads_count"] = len(voided_leads)
return metrics
# Usage
monthly_metrics = get_lead_metrics("2025-01-01", "2025-01-31")
print(f"New Leads: {monthly_metrics['new_leads_count']}")
print(f"Conversions: {monthly_metrics['conversions_count']}")
print(f"Conversion Rate: {monthly_metrics['conversion_rate']:.1f}%")
print(f"Lost Leads: {monthly_metrics['lost_leads_count']}")
print(f"Voided Leads: {monthly_metrics['voided_leads_count']}")
Best Practices¶
1. Don't Delete Historical Records¶
# Bad: Deleting state history loses valuable analytics data
get_model("sale.lead.state.activ").delete([old_record_id])
# Good: Keep all historical records for complete audit trail
# Query with date filters if you only need recent data
recent_states = get_model("sale.lead.state.activ").search([
["date", ">=", "2025-01-01"]
])
2. Use Ordered Queries for Timeline Analysis¶
# Bad: Unordered query makes timeline analysis difficult
states = get_model("sale.lead.state.activ").search_browse([["lead_id", "=", lead_id]])
# Good: Always order by date when analyzing progression
states = get_model("sale.lead.state.activ").search_browse(
[["lead_id", "=", lead_id]],
order="date asc" # Chronological order
)
3. Aggregate for Performance¶
# Bad: Loading all records for simple counts
all_records = get_model("sale.lead.state.activ").search_browse([])
new_count = len([r for r in all_records if r.state == "new"])
# Good: Use search with conditions
new_count = len(get_model("sale.lead.state.activ").search([["state", "=", "new"]]))
# Better: Use database aggregation for large datasets
db = database.get_connection()
result = db.query("SELECT state, COUNT(*) as count FROM sale_lead_state_activ GROUP BY state")
Performance Tips¶
1. Index on Common Query Fields¶
The model should have database indexes on:
- lead_id (for lead-specific queries)
- state (for state-based analytics)
- date (for date range queries)
2. Limit Historical Queries¶
# When only recent data is needed, use date filters
recent_activities = get_model("sale.lead.state.activ").search([
["date", ">=", "2024-01-01"] # Only last year
])
3. Batch Analytics Processing¶
# For large-scale analytics, use direct SQL
db = database.get_connection()
query = """
SELECT
lead_id,
state,
date,
LEAD(date) OVER (PARTITION BY lead_id ORDER BY date) as next_date
FROM sale_lead_state_activ
WHERE date >= %s AND date <= %s
ORDER BY lead_id, date
"""
results = db.query(query, start_date, end_date)
Troubleshooting¶
"No state activity records found"¶
Cause: State activity records may not be automatically created if lead state changes don't trigger creation
Solution: Verify that sale.lead model properly creates state activity records on state changes, or manually backfill historical data
"Duplicate state entries on same date"¶
Cause: Multiple state changes on the same day, or manual record creation Solution: This is normal behavior; use time component if finer granularity is needed (modify date field to datetime)
"Incomplete timeline for lead"¶
Cause: State activity tracking was not enabled from the beginning, or records were deleted Solution: Historical data cannot be recovered; ensure future state changes are tracked properly
Integration Points¶
Internal Modules¶
- sale.lead: Parent model being tracked
- Reporting dashboards: Source data for analytics widgets
- Workflow automation: Triggers based on time-in-state
- Sales analytics: Pipeline velocity and conversion metrics
Version History¶
Last Updated: 2025-01-05 Model Version: sale_lead_state_activ.py Framework: Netforce
Additional Resources¶
- Sale Lead Documentation:
sale.lead - Lead Analytics Guide: Dashboard configuration
- Workflow Automation: Time-based triggers
Support & Feedback¶
For issues or questions about this module:
1. Check parent sale.lead model for state change logic
2. Verify state activity records are being created on state transitions
3. Review database indexes for query performance
4. Use date filters to improve query performance on large datasets
This documentation is generated for developer onboarding and reference purposes.