Skip to content

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:

# Update stage sequence
get_model("sale.stage").write([stage_id], {
    "sequence": "35"
})


Search Functions

Search by Name

# Exact match
condition = [["name", "=", "Qualified Lead"]]

# Partial match (case-insensitive)
condition = [["name", "ilike", "proposal"]]

Search by Code

# Find by code
condition = [["code", "=", "QUALIFIED"]]

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}")

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:

_order = "sequence,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 stages
  • sale_stage_write - Modify existing stages
  • sale_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.