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:
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:
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:
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:
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:
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:
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:
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¶
Search by Source¶
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:
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¶
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
Related Models¶
| 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
Leads Not Appearing in Search¶
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 recordssale_lead_create- Create new leadssale_lead_write- Modify leadssale_lead_delete- Delete leadssale_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.