Sales Stage Documentation¶
Overview¶
The Sales Stage module (sale.stage) defines the pipeline stages for tracking sales opportunities through their lifecycle. This master data model enables companies to structure their sales process, track progression through predefined stages, and analyze conversion rates at each stage of the sales funnel.
Model Information¶
Model Name: sale.stage
Display Name: Sales Stage
Key Fields: None (multiple stages can have same name in different contexts)
Features¶
- Sequential ordering via sequence field
- Search-enabled fields for quick lookups
- Comment tracking on stages
- Flexible stage naming and organization
Common Sales Pipeline Stages¶
| Stage Type | Example Sequence | Typical Description |
|---|---|---|
| Lead | 10 | Initial contact or inquiry |
| Qualified | 20 | Lead has been qualified and shows genuine interest |
| Proposal | 30 | Proposal or quotation sent to customer |
| Negotiation | 40 | Price and terms being negotiated |
| Won | 90 | Deal closed successfully |
| Lost | 99 | Opportunity lost to competitor or cancelled |
Field Reference¶
Basic Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
name |
Char | Yes | Stage name (e.g., "Qualified Lead", "Proposal Sent") |
code |
Char | No | Short code for identification and filtering |
sequence |
Char | No | Numeric value for ordering stages (e.g., "10", "20", "30") |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
comments |
One2Many | Comments and notes associated with this stage |
API Methods¶
1. Create Sales Stage¶
Method: create(vals, context)
Creates a new sales stage record.
Parameters:
vals = {
"name": "Proposal Sent", # Required: Stage name
"code": "PROPOSAL", # Optional: Short code
"sequence": "30" # Optional: Ordering sequence
}
Returns: int - New stage ID
Example:
# Create a proposal stage
stage_id = get_model("sale.stage").create({
"name": "Proposal Sent",
"code": "PROPOSAL",
"sequence": "30"
})
2. Search Stages¶
Method: search(condition, context)
Find sales stages by various criteria.
Examples:
# Find stage by name
stage_ids = get_model("sale.stage").search([["name", "=", "Proposal Sent"]])
# Find stages by code
stage_ids = get_model("sale.stage").search([["code", "=", "PROPOSAL"]])
# Get all stages ordered by sequence
stage_ids = get_model("sale.stage").search([], order="sequence,name")
3. Update Stage¶
Method: write(ids, vals, context)
Update existing stage records.
Example:
Search Functions¶
Search by Name¶
# Exact match
condition = [["name", "=", "Qualified Lead"]]
# Partial match (case-insensitive)
condition = [["name", "ilike", "proposal"]]
Search by Code¶
Search with Ordering¶
# Get all stages in pipeline order
stages = get_model("sale.stage").search_browse([], order="sequence,name")
for stage in stages:
print(f"{stage.sequence}: {stage.name}")
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.opportunity |
Referenced by | Opportunities track current stage |
sale.lead |
Referenced by | Leads may use stages for qualification |
message |
One2Many | Comments and discussions about stage |
Common Use Cases¶
Use Case 1: Initial Pipeline Setup¶
# 1. Create standard sales pipeline stages
pipeline_stages = [
{"name": "New Lead", "code": "LEAD", "sequence": "10"},
{"name": "Qualified", "code": "QUALIFIED", "sequence": "20"},
{"name": "Proposal Sent", "code": "PROPOSAL", "sequence": "30"},
{"name": "Negotiation", "code": "NEGOTIATION", "sequence": "40"},
{"name": "Verbal Commitment", "code": "VERBAL", "sequence": "50"},
{"name": "Won", "code": "WON", "sequence": "90"},
{"name": "Lost", "code": "LOST", "sequence": "99"}
]
for stage in pipeline_stages:
get_model("sale.stage").create(stage)
# 2. Verify created stages
all_stages = get_model("sale.stage").search_browse([], order="sequence")
print(f"Created {len(all_stages)} pipeline stages")
for s in all_stages:
print(f"{s.sequence}: {s.name}")
Use Case 2: Track Opportunity Through Pipeline¶
# Move opportunity through sales stages
def advance_opportunity(opport_id, new_stage_code):
# Find the target stage
stage_ids = get_model("sale.stage").search([["code", "=", new_stage_code]])
if not stage_ids:
raise Exception(f"Stage {new_stage_code} not found")
# Update opportunity stage
get_model("sale.opportunity").write([opport_id], {
"stage_id": stage_ids[0]
})
print(f"Opportunity {opport_id} moved to {new_stage_code}")
# Usage example
opport_id = 123
# Move through pipeline
advance_opportunity(opport_id, "QUALIFIED") # Lead qualified
advance_opportunity(opport_id, "PROPOSAL") # Proposal sent
advance_opportunity(opport_id, "NEGOTIATION") # In negotiation
advance_opportunity(opport_id, "WON") # Deal closed
Use Case 3: Pipeline Analytics¶
# Analyze opportunities by stage
def get_pipeline_report():
results = []
# Get all stages in order
stage_ids = get_model("sale.stage").search([], order="sequence")
stages = get_model("sale.stage").browse(stage_ids)
for stage in stages:
# Find opportunities in this stage
opport_ids = get_model("sale.opportunity").search([
["stage_id", "=", stage.id],
["state", "!=", "cancelled"]
])
opports = get_model("sale.opportunity").browse(opport_ids)
total_value = sum(o.amount for o in opports)
results.append({
"stage": stage.name,
"code": stage.code,
"sequence": stage.sequence,
"count": len(opports),
"total_value": total_value,
"avg_value": total_value / len(opports) if opports else 0
})
return results
# Generate report
report = get_pipeline_report()
for row in report:
print(f"{row['stage']}: {row['count']} opportunities, ${row['total_value']:,.2f}")
Use Case 4: Conversion Rate Analysis¶
# Calculate conversion rates between stages
def calculate_conversion_rates(date_from, date_to):
stages = get_model("sale.stage").search_browse([], order="sequence")
conversions = []
for i in range(len(stages) - 1):
current_stage = stages[i]
next_stage = stages[i + 1]
# Count opportunities that reached current stage
current_ids = get_model("sale.opportunity").search([
["stage_id", "=", current_stage.id],
["date", ">=", date_from],
["date", "<=", date_to]
])
# Count how many progressed to next stage
progressed_ids = get_model("sale.opportunity").search([
["stage_id", "=", next_stage.id],
["date", ">=", date_from],
["date", "<=", date_to]
])
conversion_rate = (len(progressed_ids) / len(current_ids) * 100) if current_ids else 0
conversions.append({
"from_stage": current_stage.name,
"to_stage": next_stage.name,
"opportunities_in": len(current_ids),
"opportunities_out": len(progressed_ids),
"conversion_rate": conversion_rate
})
return conversions
Use Case 5: Custom Stage Workflow¶
# Create industry-specific stages
def create_manufacturing_pipeline():
# Manufacturing sales pipeline
stages = [
{"name": "Inquiry Received", "code": "INQUIRY", "sequence": "10"},
{"name": "Technical Review", "code": "TECH_REVIEW", "sequence": "20"},
{"name": "Sample Sent", "code": "SAMPLE", "sequence": "30"},
{"name": "Quotation Sent", "code": "QUOTE", "sequence": "40"},
{"name": "Negotiating MOQ", "code": "MOQ", "sequence": "50"},
{"name": "PO Received", "code": "PO", "sequence": "60"},
{"name": "Production Scheduled", "code": "PRODUCTION", "sequence": "70"},
{"name": "Order Fulfilled", "code": "FULFILLED", "sequence": "90"},
{"name": "Lost", "code": "LOST", "sequence": "99"}
]
created = []
for stage in stages:
stage_id = get_model("sale.stage").create(stage)
created.append(stage_id)
return created
Best Practices¶
1. Sequence Numbering¶
# Good: Use increments of 10 for flexibility
stages = [
{"name": "Lead", "sequence": "10"}, # Can insert at 5 or 15 later
{"name": "Qualified", "sequence": "20"},
{"name": "Proposal", "sequence": "30"},
{"name": "Won", "sequence": "90"} # Final stages at 90+
]
# Bad: Sequential numbering with no gaps
stages = [
{"name": "Lead", "sequence": "1"}, # Hard to insert new stages
{"name": "Qualified", "sequence": "2"},
{"name": "Proposal", "sequence": "3"}
]
Guidelines: - Use increments of 10 (10, 20, 30...) - Reserve 90+ for terminal stages (Won, Lost, Cancelled) - Leave gaps for future stage insertions - Keep sequence as string type for flexibility
2. Stage Naming¶
# Good: Action-oriented, clear names
"Proposal Sent" # Clear what happened
"Qualified Lead" # Clear status
"Negotiating Terms" # Clear activity
"Verbal Commitment" # Clear milestone
# Bad: Vague or generic names
"Stage 1" # Not descriptive
"In Progress" # Too generic
"Waiting" # Unclear what for
"Step 3" # Not meaningful
Guidelines: - Use action verbs or status indicators - Be specific and descriptive - Avoid generic terms like "Stage 1", "Phase 2" - Use business language your team understands
3. Pipeline Design¶
Keep it simple:
# Good: 5-7 core stages
stages = ["Lead", "Qualified", "Proposal", "Negotiation", "Won", "Lost"]
# Bad: Too many micro-stages
stages = [
"Initial Contact", "First Follow-up", "Second Follow-up",
"Qualification Call Scheduled", "Qualification Complete",
"Demo Scheduled", "Demo Complete", "Proposal Draft",
"Proposal Review", "Proposal Sent", # ... 10 more stages
]
Guidelines: - Aim for 5-8 stages maximum - Each stage should represent meaningful progress - Don't create stages for every minor activity - Stages should be mutually exclusive
4. Stage Codes¶
# Good: Consistent, meaningful codes
{"name": "Proposal Sent", "code": "PROPOSAL"}
{"name": "Negotiation", "code": "NEGOTIATION"}
{"name": "Won", "code": "WON"}
# Bad: Inconsistent or unclear codes
{"name": "Proposal Sent", "code": "PS"}
{"name": "Negotiation", "code": "neg"}
{"name": "Won", "code": "STAGE_WIN"}
Guidelines: - Use UPPERCASE for codes - Make codes short but meaningful - Be consistent in abbreviation style - Avoid special characters
Database Constraints¶
Ordering¶
The model uses a combined ordering of sequence and name:
This ensures: - Stages are primarily sorted by sequence number - Stages with same sequence are sorted alphabetically by name - NULL sequences appear last
Performance Tips¶
1. Cache Pipeline Stages¶
# Cache stages to avoid repeated queries
_stage_cache = None
def get_pipeline_stages():
global _stage_cache
if _stage_cache is None:
stage_ids = get_model("sale.stage").search([], order="sequence,name")
_stage_cache = get_model("sale.stage").browse(stage_ids)
return _stage_cache
# Clear cache when stages are modified
def clear_stage_cache():
global _stage_cache
_stage_cache = None
2. Use Codes for Lookups¶
# Good: Lookup by unique code
stage_ids = get_model("sale.stage").search([["code", "=", "PROPOSAL"]])
# Less optimal: Lookup by partial name
stage_ids = get_model("sale.stage").search([["name", "ilike", "%proposal%"]])
3. Limit Stage Count¶
- Keep total stages under 15
- Too many stages complicate reporting and analytics
- Users get confused with too many options
Troubleshooting¶
"Opportunities stuck in old stages"¶
Cause: Stages were deleted or renamed without updating opportunities. Solution:
# Find opportunities with missing stages
opport_ids = get_model("sale.opportunity").search([["stage_id", "=", None]])
# Or find opportunities in deleted stage
# Assign them to a default stage
default_stage = get_model("sale.stage").search([["code", "=", "LEAD"]])
if opport_ids and default_stage:
get_model("sale.opportunity").write(opport_ids, {
"stage_id": default_stage[0]
})
"Stage sequence not working"¶
Cause: Sequence values are not properly set or are non-numeric strings. Solution:
# Fix sequence values
stages = get_model("sale.stage").search_browse([])
for i, stage in enumerate(stages):
get_model("sale.stage").write([stage.id], {
"sequence": str((i + 1) * 10)
})
"Cannot delete stage - referenced by opportunities"¶
Cause: Stage is being used by active opportunities. Solution:
# Move opportunities to another stage first
def migrate_stage(old_stage_id, new_stage_id):
opport_ids = get_model("sale.opportunity").search([
["stage_id", "=", old_stage_id]
])
if opport_ids:
get_model("sale.opportunity").write(opport_ids, {
"stage_id": new_stage_id
})
# Now safe to delete
get_model("sale.stage").delete([old_stage_id])
Testing Examples¶
Unit Test: Create Stage¶
def test_create_sales_stage():
# Create stage
stage_id = get_model("sale.stage").create({
"name": "Test Proposal",
"code": "TEST_PROP",
"sequence": "30"
})
# Verification
assert stage_id > 0
# Read back
stage = get_model("sale.stage").browse(stage_id)
assert stage.name == "Test Proposal"
assert stage.code == "TEST_PROP"
assert stage.sequence == "30"
# Cleanup
get_model("sale.stage").delete([stage_id])
Unit Test: Stage Ordering¶
def test_stage_ordering():
# Create stages with different sequences
stage1_id = get_model("sale.stage").create({
"name": "Stage C",
"sequence": "30"
})
stage2_id = get_model("sale.stage").create({
"name": "Stage A",
"sequence": "10"
})
stage3_id = get_model("sale.stage").create({
"name": "Stage B",
"sequence": "20"
})
# Retrieve in order
stages = get_model("sale.stage").search_browse(
[["id", "in", [stage1_id, stage2_id, stage3_id]]],
order="sequence,name"
)
# Verify order
assert stages[0].sequence == "10"
assert stages[1].sequence == "20"
assert stages[2].sequence == "30"
# Cleanup
get_model("sale.stage").delete([stage1_id, stage2_id, stage3_id])
Security Considerations¶
Permission Model¶
sale_stage_create- Create new sales stagessale_stage_write- Modify existing stagessale_stage_delete- Delete stages (check for opportunity references first)sale_stage_read- View stage information
Data Access¶
- Stages are company-wide master data
- All sales users need read access
- Only sales managers should create/modify stages
- Protect against accidental deletion of active stages
Integration Points¶
Internal Modules¶
- sale.opportunity: Primary consumer of stages for pipeline tracking
- sale.lead: May use stages for lead qualification workflow
- report: Stages used for pipeline reports and conversion analytics
- message: Comment tracking on stage discussions
Workflow Integration¶
# Stages commonly used in:
# - Sales Pipeline Dashboard
# - Conversion Rate Reports
# - Sales Funnel Analysis
# - Team Performance Metrics
# - Forecast Reports (based on stage probability)
Version History¶
Last Updated: 2026-01-05 Model Version: sale_stage.py Framework: Netforce
Note: Current implementation uses Char type for sequence field. Future versions may convert to Integer type as noted in source code comment.
Additional Resources¶
- Sales Opportunity Documentation:
sale.opportunity - Sales Lead Documentation:
sale.lead - Pipeline Management Guide
- Sales Reporting Guide
Support & Feedback¶
For issues or questions about this module: 1. Ensure stages have proper sequence values for correct ordering 2. Use codes for programmatic stage lookups 3. Test stage changes with sample opportunities first 4. Keep pipeline design simple and user-friendly
This documentation is generated for developer onboarding and reference purposes.