Skip to content

Sale Lead Documentation

Overview

The Sale Lead module (sale.lead) provides comprehensive CRM lead management capabilities for capturing, nurturing, and converting prospective customers. This model supports the entire lead lifecycle from initial contact through qualification, meetings, and eventual conversion to opportunities or archival. It includes sophisticated state tracking, lead scoring, and automated follow-up capabilities essential for modern sales organizations.


Model Information

Model Name: sale.lead Display Name: Lead Key Fields: number

Features

  • ✅ Audit logging enabled (_audit_log)
  • ✅ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ✅ Unique key constraint per lead number

Understanding Key Fields

What are Key Fields?

In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record. Think of them as a business key that ensures data integrity across the system.

For the sale.lead model, the key field is:

_key = ["number"]

This means the lead number must be unique across all leads: - number - Auto-generated unique identifier for the lead (e.g., "LEAD-00001")

Why Key Fields Matter

Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique combinations:

# Examples of valid combinations:
Lead with number="LEAD-00001"   Valid
Lead with number="LEAD-00002"   Valid
Lead with number="LEAD-00003"   Valid

# This would fail - duplicate key:
Another lead with number="LEAD-00001"   ERROR: Lead number already exists!

Database Implementation

The key fields are enforced at the database level using a unique constraint:

# Implemented via sequence-based number generation
# See _get_number() method for implementation

This ensures each lead has a unique, trackable identifier throughout its lifecycle.


Lead Rating System

Type Code Description
Hot hot High-priority lead with strong buying signals and urgency
Warm warm Qualified lead with interest but no immediate purchase timeline
Cold cold Low-priority lead requiring significant nurturing

Note: The rating field is deprecated in favor of the more comprehensive state workflow system.


State Workflow

new → qualified → pending_initial_meeting → waiting_for_client
                                  pending_next_meeting → preparing_quotation → converted
                                          ↓                                         ↓
                                      on_hold                                     won
                                          ↓                                         ↓
                                       lost ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← lost
                                      voided
                                      referred
State Description
new Initial state when lead is first captured from any source
qualified Lead has been reviewed and meets basic qualification criteria
pending_initial_meeting First meeting scheduled with the prospect
waiting_for_client Awaiting response or action from the prospect
pending_next_meeting Follow-up meeting scheduled after initial contact
preparing_quotation Actively preparing a formal quote or proposal
converted Successfully converted to opportunity and/or contact
on_hold Temporarily paused, awaiting future engagement
won Lead resulted in successful business (closed deal)
lost Lead did not convert, opportunity lost to competitor or no decision
voided Invalid lead, spam, or duplicate entry
referred Lead referred to another contact or external party

Key Fields Reference

Header Fields

Field Type Required Description
number Char Auto-generated unique lead identifier
title Char Brief description or subject of the lead
contact_name Char Full name of the primary contact person
state Selection Current stage in lead lifecycle
date Date Date lead was created or captured
user_id Many2One Sales representative who owns this lead
company_id Many2One Company this lead belongs to (multi-company)

Contact Information Fields

Field Type Description
first_name Char Contact's first name (optional, can use contact_name instead)
last_name Char Contact's last name (optional, can use contact_name instead)
email Char Primary email address of the lead
phone Char Primary phone number
company Char Company name the contact works for

Address Fields

Field Type Description
street Char Street address line
city Char City name
province Char State or province
zip Char Postal or zip code
country_id Many2One Reference to country record
addresses One2Many Full address records linked to this lead

Lead Intelligence Fields

Field Type Description
source_id Many2One Where the lead originated (web form, referral, event, etc.)
industry Char Industry sector or vertical
rating Selection Hot/Warm/Cold priority rating (deprecated)
employees Char Number of employees in prospect company
revenue Char Annual revenue of prospect company
website Char Company website URL
categ_id Many2One Sales category classification

Workflow and Tracking Fields

Field Type Description
state_activ One2Many Historical record of state transitions
age_days Integer Number of business days since lead creation (computed)
assigned_id Many2One User assigned to handle this lead
description Text Detailed notes about the lead and context

Referral Fields

Field Type Description
refer_contact_id Many2One Contact this lead was referred to
refer_reason_id Many2One Reason code for why lead was referred

Relationship Fields

Field Type Description
comments One2Many Message comments and internal notes
activities One2Many Sales activities (calls, meetings, tasks)
emails One2Many Email correspondence linked to this lead
documents One2Many Attached files and documents
sale_opports One2Many Opportunities created from this lead

Reporting Fields (SQL Functions)

Field Type Description
year Char Year extracted from date field (for reporting)
quarter Char Quarter extracted from date field (for reporting)
month Char Month extracted from date field (for reporting)
week Char Week extracted from date field (for reporting)

API Methods

1. Create Lead

Method: create(vals, context)

Creates a new lead record with automatic number generation.

Parameters:

vals = {
    "title": str,                      # Required: Lead subject/title
    "contact_name": str,               # Required: Contact person name
    "email": str,                      # Email address
    "phone": str,                      # Phone number
    "company": str,                    # Company name
    "source_id": int,                  # Lead source ID
    "industry": str,                   # Industry sector
    "description": str,                # Detailed notes
    "user_id": int,                    # Lead owner
    "state": str,                      # Default: "new"
    "addresses": [                     # Address records
        ("create", {
            "type": "billing",
            "address": "123 Main St",
            "city": "New York",
            "province_id": 1,
            "country_id": 1
        })
    ]
}

context = {
    "company_id": int,                 # Active company context
}

Returns: int - New lead ID

Example:

# Create a new web form lead
lead_id = get_model("sale.lead").create({
    "title": "Website inquiry - Enterprise CRM",
    "contact_name": "John Smith",
    "email": "john.smith@example.com",
    "phone": "+1-555-0123",
    "company": "Acme Corporation",
    "source_id": 1,  # Web Form source
    "industry": "Technology",
    "description": "Interested in enterprise CRM solution for 500+ users",
    "user_id": 5,  # Assigned to sales rep
    "state": "new"
})


2. Copy to Contact

Method: copy_to_contact(ids, context)

Converts a lead into a contact record, creating both company and person contacts as needed.

Parameters: - ids (list): Lead IDs to convert

Behavior: - Creates organization contact if company name is provided - Creates person contact from lead contact information - Links person to organization if both exist - Checks for existing contacts to avoid duplicates - Preserves lead data in the new contact records

Returns: dict - Navigation to created contact with flash message

Example:

# Convert lead to contact
result = get_model("sale.lead").copy_to_contact([lead_id])
# Returns: {"next": {"name": "contact", "mode": "form", "active_id": contact_id}}


3. Copy to Opportunity

Method: copy_to_opport(ids, context) and copy_to_opport_new(ids, context)

Converts lead to sales opportunity, creating a contact if needed and marking lead as converted.

Parameters: - ids (list): Lead IDs to convert

Behavior: - Creates contact from lead email (required for copy_to_opport) - Creates opportunity with lead details - Transfers emails and documents to opportunity - Updates lead state to "converted" - Links opportunity back to original lead

Returns: dict - Flash message with count of opportunities created

Example:

# Convert qualified leads to opportunities
result = get_model("sale.lead").copy_to_opport([lead_id])
# Returns: {"flash": "1 sales opportunities created"}

Difference between methods: - copy_to_opport(): Requires email address, raises exception if missing - copy_to_opport_new(): Uses "null" as email if missing, more permissive


4. State Transition Methods

4.1 Void Lead

Method: void(ids, context)

Marks leads as voided (invalid, spam, or duplicates).

Parameters: - ids (list): Lead IDs to void

Behavior: - Sets state to "voided" - Preserves lead data for audit purposes - Returns count of voided leads

Example:

get_model("sale.lead").void([spam_lead_id])

4.2 Set to New

Method: set_new(ids, context)

Resets lead state back to "new" for reprocessing.

Parameters: - ids (list): Lead IDs to reset

Example:

get_model("sale.lead").set_new([lead_id])

4.3 Refer Lead

Method: refer(ids, context)

Refers lead to another contact and triggers workflow.

Parameters: - ids (list): Lead IDs to refer

Behavior: - Validates refer_reason_id is set - Updates state to "referred" - Triggers "refer" workflow event

Example:

# First set referral details
get_model("sale.lead").write([lead_id], {
    "refer_contact_id": contact_id,
    "refer_reason_id": reason_id
})

# Then refer
get_model("sale.lead").refer([lead_id])


5. Helper Methods

5.1 Check Spam

Method: check_spam(ids, context)

Automatically voids leads detected as spam based on email analysis.

Behavior: - Checks if first email in lead.emails is marked as spam - Automatically calls void() if spam is detected

Example:

get_model("sale.lead").check_spam([lead_id])

5.2 Forward Email

Method: forward_email(from_addr, to_addrs, context)

Forwards lead email to assigned user or specified addresses.

Parameters: - from_addr (str): Sender address (defaults to lead email) - to_addrs (str): Recipient addresses (defaults to lead owner email)

Context Options:

context = {
    "trigger_ids": [lead_id],          # Lead ID being forwarded
}

Behavior: - Finds most recent received email from lead - Creates forwarded email with lead number in subject - Preserves original email as attachment - Queues for async sending

Example:

get_model("sale.lead").forward_email(
    to_addrs="manager@company.com",
    context={"trigger_ids": [lead_id]}
)

5.3 Send Reminders

Method: send_reminders(template, min_days, company, to_addrs, context)

Sends reminder emails for aging leads based on criteria.

Parameters: - template (str): Email template name - min_days (int): Minimum age in business days - company (str): Filter by company code - to_addrs (str): Override recipient addresses

Behavior: - Finds all leads in "new" state - Filters by company and minimum age - Sends batch reminder email using template - Async email processing

Example:

get_model("sale.lead").send_reminders(
    template="Lead Aging Reminder",
    min_days=7,
    company="ACME",
    to_addrs="sales-team@company.com"
)
# Returns: "15 leads reminded"


Computed Fields Functions

get_age_days(ids, context)

Calculates the number of business days (excluding weekends) since the lead was created.

Returns: dict - {lead_id: days_count}

Example:

ages = get_model("sale.lead").get_age_days([lead_id])
# Returns: {123: 12}  # 12 business days old

get_name(ids, context)

Returns the display name for the lead (uses number or title).

name_get(ids, context)

Standard Netforce method for getting display names.

Returns: List of tuples (id, display_name)

name_search(name, condition, limit, context)

Searches leads by number, title, contact name, or addresses.

Example:

results = get_model("sale.lead").name_search("Acme", limit=10)
# Returns matching leads with "Acme" in any searchable field


Reporting and Analytics Methods

1. Get New Leads Count

Method: get_new_lead(context, month)

Returns count of leads created in specified month.

Parameters: - month (str): Date string in "YYYY-MM-DD" format

Returns: list - Dashboard widget data structure

Example:

data = get_model("sale.lead").get_new_lead(month="2025-01-01")
# Returns: [{"name": "New Lead", "data": 45, "color": "blue"}]

2. Leads Per Day

Method: lead_per_day(context, month)

Returns daily lead creation statistics for current and previous month.

Parameters: - month (str): Date string for month to analyze

Returns: list - Chart data with this month and previous month comparisons

Example:

data = get_model("sale.lead").lead_per_day(month="2025-01-01")
# Returns chart data for dashboard visualization


Search Functions

Search by State

# Find all new leads
new_leads = get_model("sale.lead").search([["state", "=", "new"]])

# Find qualified or converted leads
condition = [["state", "in", ["qualified", "converted"]]]

Search by Owner

# Find leads assigned to specific user
condition = [["user_id", "=", user_id]]

Search by Source

# Find leads from web forms
condition = [["source_id.name", "=", "Web Form"]]

Search by Age

# Find aging leads (requires custom query)
condition = [["date", "<", "2024-01-01"], ["state", "=", "new"]]

Search by Contact Details

# Search by email domain
condition = [["email", "ilike", "%@acme.com"]]

# Search by phone area code
condition = [["phone", "ilike", "555%"]]

Workflow Integration

Trigger Events

The sale.lead module fires workflow triggers:

self.trigger(ids, "refer")    # When lead is referred to another contact

These can be configured in workflow automation to: - Send notification emails - Create tasks for assignees - Update external CRM systems - Log activities


Best Practices

1. Lead Qualification Process

# Bad example: Converting unqualified leads
get_model("sale.lead").copy_to_opport([new_lead_id])  # Lead still in "new" state

# Good example: Follow proper qualification workflow
lead = get_model("sale.lead").browse(new_lead_id)
lead.write({"state": "qualified"})  # Review and qualify first

# Schedule initial meeting
get_model("sale.activ").create({
    "type": "meeting",
    "subject": "Initial discovery call",
    "related_id": "sale.lead,%s" % lead.id,
    "date": "2025-01-10"
})

lead.write({"state": "pending_initial_meeting"})

# After successful meeting, convert to opportunity
get_model("sale.lead").copy_to_opport([lead.id])

2. Lead Source Tracking

Always capture lead source for ROI analysis:

# Good: Track where leads come from
lead_sources = {
    "Web Form": 1,
    "Trade Show": 2,
    "Referral": 3,
    "Cold Call": 4,
    "LinkedIn": 5
}

get_model("sale.lead").create({
    "title": "Trade show inquiry",
    "contact_name": "Jane Doe",
    "source_id": lead_sources["Trade Show"],  # Always track source
    "description": "Met at CRM Expo 2025, booth #123"
})

3. Duplicate Prevention

# Check for existing leads before creating
email = "prospect@company.com"
existing = get_model("sale.lead").search([["email", "=", email]])

if existing:
    # Update existing lead
    get_model("sale.lead").write(existing, {
        "state": "new",  # Re-engage
        "description": "New inquiry received..."
    })
else:
    # Create new lead
    get_model("sale.lead").create({...})

Database Constraints

Unique Number Constraint

_key = ["number"]

Ensures each lead has a unique number. The _get_number() method implements sequence-based generation with collision detection:

# Sequence-based generation prevents duplicates
while 1:
    num = get_model("sequence").get_next_number(seq_id)
    res = self.search([["number", "=", num]])
    if not res:
        return num  # Found unique number
    get_model("sequence").increment_number(seq_id)  # Try next

Model Relationship Description
sale.lead.state.activ One2Many Historical tracking of state transitions
sale.opportunity One2Many Opportunities created from this lead
contact Referenced Contacts created during conversion
lead.source Many2One Source/channel where lead originated
base.user Many2One Lead owner and assigned user
address One2Many Physical and mailing addresses
message One2Many Comments and internal notes
sale.activ One2Many Activities (calls, meetings, tasks)
email.message One2Many Email correspondence
document One2Many Attached files and documents
company Many2One Multi-company support
reason.code Many2One Referral reason codes
sale.categ Many2One Sales category classification

Common Use Cases

Use Case 1: Capture Web Form Lead

# 1. Create lead from web form submission
lead_id = get_model("sale.lead").create({
    "title": "Enterprise CRM Demo Request",
    "contact_name": request.POST["name"],
    "email": request.POST["email"],
    "phone": request.POST["phone"],
    "company": request.POST["company"],
    "source_id": web_form_source_id,
    "description": request.POST["message"],
    "state": "new"
})

# 2. Auto-assign based on round-robin or territory
reps = get_model("base.user").search([["role", "=", "sales_rep"], ["active", "=", True]])
assigned_rep = reps[lead_id % len(reps)]  # Simple round-robin

get_model("sale.lead").write([lead_id], {"user_id": assigned_rep})

# 3. Send notification to assigned rep
get_model("sale.lead").trigger([lead_id], "new_assignment")

Use Case 2: Lead Nurturing Campaign

# Find leads that need follow-up
aging_leads = get_model("sale.lead").search([
    ["state", "=", "new"],
    ["date", "<", "2025-01-01"]
])

# Send reminder campaign
for lead in get_model("sale.lead").browse(aging_leads):
    if lead.age_days >= 7:
        # Create follow-up task
        get_model("sale.activ").create({
            "type": "task",
            "subject": "Follow up on lead: %s" % lead.title,
            "related_id": "sale.lead,%s" % lead.id,
            "user_id": lead.user_id.id,
            "priority": "high",
            "due_date": datetime.now() + timedelta(days=1)
        })

Use Case 3: Complete Lead-to-Opportunity Lifecycle

# 1. Initial lead capture
lead_id = get_model("sale.lead").create({
    "title": "Cloud migration project",
    "contact_name": "Sarah Johnson",
    "email": "sjohnson@company.com",
    "phone": "+1-555-9876",
    "company": "TechStart Inc",
    "source_id": referral_source_id,
    "industry": "SaaS",
    "employees": "50-100",
    "revenue": "$5M-$10M",
    "state": "new"
})

# 2. Initial qualification
lead = get_model("sale.lead").browse(lead_id)
lead.write({"state": "qualified"})

# 3. Schedule discovery meeting
get_model("sale.activ").create({
    "type": "meeting",
    "subject": "Discovery call - Cloud migration needs",
    "related_id": "sale.lead,%s" % lead_id,
    "user_id": lead.user_id.id,
    "date": "2025-01-15 14:00:00"
})
lead.write({"state": "pending_initial_meeting"})

# 4. After successful meeting
lead.write({
    "state": "preparing_quotation",
    "description": "Qualified opportunity: 50 users, $50K ARR potential"
})

# 5. Convert to opportunity
result = get_model("sale.lead").copy_to_opport([lead_id])
# Lead is now converted, opportunity created

Use Case 4: Lead Referral Workflow

# 1. Lead comes in but wrong fit for original rep
lead = get_model("sale.lead").browse(lead_id)

# 2. Find appropriate contact to refer to
specialist_contact = get_model("contact").search([
    ["name", "=", "Enterprise Sales Team"]
])[0]

# 3. Set referral details
lead.write({
    "refer_contact_id": specialist_contact,
    "refer_reason_id": wrong_territory_reason_id
})

# 4. Execute referral (triggers notification)
get_model("sale.lead").refer([lead_id])

Performance Tips

1. Batch Processing for Lead Import

  • Use create() in batches of 100-500 records
  • Disable audit logging temporarily for large imports
  • Use direct SQL for very large datasets (10K+ records)

2. Optimize Age Calculation

# Bad: Calling get_age_days for each lead individually
for lead_id in lead_ids:
    age = get_model("sale.lead").get_age_days([lead_id])

# Good: Batch calculate ages
ages = get_model("sale.lead").get_age_days(lead_ids)

3. Use Search Limits

# Bad: Loading all leads
all_leads = get_model("sale.lead").search([])

# Good: Paginate and limit results
recent_leads = get_model("sale.lead").search(
    [["state", "=", "new"]],
    limit=50,
    order="date desc"
)

Troubleshooting

"Missing email"

Cause: Calling copy_to_opport() on lead without email address Solution: Use copy_to_opport_new() instead, or ensure email is populated before conversion

"Missing refer reason"

Cause: Calling refer() without setting refer_reason_id Solution: Set refer_reason_id before calling refer() method

"Lead number already exists"

Cause: Sequence collision or manual number entry Solution: The _get_number() method automatically resolves collisions; ensure you're not manually setting number field

Cause: Multi-company filtering active Solution: Check active company context matches lead's company_id


Testing Examples

Unit Test: Lead Creation and Conversion

def test_lead_lifecycle():
    # Create lead
    lead_id = get_model("sale.lead").create({
        "title": "Test Lead",
        "contact_name": "Test Contact",
        "email": "test@example.com",
        "state": "new"
    })

    # Verification
    assert lead_id is not None
    lead = get_model("sale.lead").browse(lead_id)
    assert lead.state == "new"
    assert lead.number is not None

    # Qualify lead
    lead.write({"state": "qualified"})
    assert lead.state == "qualified"

    # Convert to opportunity
    result = get_model("sale.lead").copy_to_opport([lead_id])

    # Verify conversion
    lead = get_model("sale.lead").browse(lead_id)
    assert lead.state == "converted"
    assert len(lead.sale_opports) > 0

Security Considerations

Permission Model

  • sale_lead_read - View lead records
  • sale_lead_create - Create new leads
  • sale_lead_write - Modify leads
  • sale_lead_delete - Delete leads
  • sale_lead_convert - Convert leads to opportunities

Data Access

  • Multi-company isolation enforced via company_id
  • Lead owners can be restricted to view only their assigned leads
  • Email forwarding validates recipient addresses
  • Audit log tracks all changes to lead records

Configuration Settings

Required Settings

Setting Location Description
Lead Number Sequence Settings > Sequences Auto-numbering for new leads
Default Lead Source Lead Sources master data Fallback when source not specified

Optional Settings

Setting Default Description
Auto-assign leads Disabled Round-robin or territory-based assignment
Spam detection Enabled Automatic voiding of spam leads
Age threshold for reminders 7 days When to send aging lead reminders

Integration Points

External Systems

  • Email Marketing Platforms: Sync leads to campaign systems
  • Web Forms: Capture leads from website forms
  • Social Media: LinkedIn Lead Gen Forms integration
  • Trade Show Apps: Import event leads

Internal Modules

  • CRM Activities: Track meetings, calls, and tasks
  • Opportunities: Convert qualified leads
  • Contacts: Create person and organization records
  • Email: Integrated email correspondence
  • Workflow: Automated lead routing and follow-up

Version History

Last Updated: 2025-01-05 Model Version: sale_lead.py Framework: Netforce


Additional Resources

  • Sale Opportunity Documentation: sale.opportunity
  • Lead Source Configuration: lead.source
  • Activity Management: sale.activ
  • Contact Management: contact
  • Lead State Activity Tracking: sale.lead.state.activ
  • Lead Conversion Wizard: convert.lead

Support & Feedback

For issues or questions about this module: 1. Check related model documentation for opportunities and contacts 2. Review system logs for detailed error messages 3. Verify lead source and sequence configuration 4. Test conversion workflow in development environment first 5. Check multi-company settings if leads are not visible


This documentation is generated for developer onboarding and reference purposes.