Lead Source Documentation¶
Overview¶
The Lead Source module (lead.source) tracks where sales leads originate from, enabling marketing attribution analysis and campaign ROI measurement. This master data model helps companies understand which marketing channels and campaigns are most effective at generating qualified leads.
Model Information¶
Model Name: lead.source
Display Name: Lead Source
Key Fields: name (unique)
Features¶
- Unique key constraint per source name
- Search-enabled name and description fields
- Simple tracking structure
- Marketing attribution support
Understanding Key Fields¶
What are Key Fields?¶
For the lead.source model, the key field is:
This means the source name must be unique:
- name - The unique source name (required field)
Uniqueness Guarantee¶
# Examples of valid sources:
{"name": "Website Contact Form"} # Valid
{"name": "Trade Show"} # Valid
{"name": "Google Ads"} # Valid
# This would fail - duplicate key:
{"name": "Website Contact Form"} # ERROR: Source already exists!
Common Lead Source Types¶
| Source Category | Example Sources | Description |
|---|---|---|
| Digital Marketing | Website, Google Ads, Social Media, Email Campaign | Online lead generation |
| Events | Trade Show, Conference, Webinar, Product Launch | Event-based leads |
| Referrals | Customer Referral, Partner Referral, Employee Referral | Word-of-mouth sources |
| Direct | Cold Call, Direct Mail, Sales Visit | Outbound prospecting |
| Traditional | Print Ad, Radio, TV, Billboard | Traditional media |
| Other | Walk-in, Unknown, Other | Miscellaneous sources |
Field Reference¶
Basic Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
name |
Char | Yes | Unique source name (e.g., "Google Ads Campaign") |
description |
Text | No | Detailed description of the lead source |
API Methods¶
1. Create Lead Source¶
Method: create(vals, context)
Example:
# Create a lead source
source_id = get_model("lead.source").create({
"name": "Google Ads - Q1 Campaign",
"description": "Pay-per-click campaign running Q1 2026, targeting enterprise customers"
})
2. Search Sources¶
# Find source by name
source_ids = get_model("lead.source").search([["name", "=", "Website Contact Form"]])
# Search in description
source_ids = get_model("lead.source").search([["description", "ilike", "%google%"]])
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.lead |
Referenced by | Leads track their source via source_id |
sale.opportunity |
Indirect | Opportunities inherit source from converted leads |
Common Use Cases¶
Use Case 1: Initial Source Setup¶
# Create standard lead sources
sources = [
# Digital
{"name": "Website Contact Form", "description": "Inquiries from company website contact page"},
{"name": "Google Ads", "description": "Google AdWords pay-per-click campaigns"},
{"name": "LinkedIn", "description": "LinkedIn advertising and InMail campaigns"},
{"name": "Facebook Ads", "description": "Facebook and Instagram advertising"},
# Events
{"name": "Trade Show", "description": "Industry trade shows and exhibitions"},
{"name": "Webinar", "description": "Online webinars and virtual events"},
# Referrals
{"name": "Customer Referral", "description": "Referred by existing customers"},
{"name": "Partner Referral", "description": "Referred by business partners"},
# Direct
{"name": "Cold Call", "description": "Outbound cold calling campaigns"},
{"name": "Direct Mail", "description": "Physical mail campaigns"}
]
for source in sources:
get_model("lead.source").create(source)
Use Case 2: Lead Capture with Source Tracking¶
# Capture lead with source attribution
def capture_lead_from_form(form_data, source_name):
# Find the source
source_ids = get_model("lead.source").search([["name", "=", source_name]])
if not source_ids:
# Create source if it doesn't exist
source_id = get_model("lead.source").create({"name": source_name})
else:
source_id = source_ids[0]
# Create lead with source
lead_id = get_model("sale.lead").create({
"first_name": form_data["first_name"],
"last_name": form_data["last_name"],
"email": form_data["email"],
"company": form_data["company"],
"source_id": source_id,
"description": f"Lead from {source_name}"
})
return lead_id
# Usage
lead_id = capture_lead_from_form({
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"company": "Acme Corp"
}, "Website Contact Form")
Use Case 3: Source Performance Analysis¶
# Analyze lead sources by conversion rate
def analyze_source_performance(date_from, date_to):
results = []
# Get all sources
source_ids = get_model("lead.source").search([])
sources = get_model("lead.source").browse(source_ids)
for source in sources:
# Count total leads from this source
lead_ids = get_model("sale.lead").search([
["source_id", "=", source.id],
["create_date", ">=", date_from],
["create_date", "<=", date_to]
])
# Count converted leads
converted_ids = get_model("sale.lead").search([
["source_id", "=", source.id],
["state", "=", "converted"],
["create_date", ">=", date_from],
["create_date", "<=", date_to]
])
conversion_rate = (len(converted_ids) / len(lead_ids) * 100) if lead_ids else 0
results.append({
"source": source.name,
"total_leads": len(lead_ids),
"converted": len(converted_ids),
"conversion_rate": conversion_rate
})
# Sort by conversion rate
results.sort(key=lambda x: x["conversion_rate"], reverse=True)
return results
# Generate report
report = analyze_source_performance("2026-01-01", "2026-03-31")
for row in report:
print(f"{row['source']}: {row['conversion_rate']:.1f}% conversion ({row['converted']}/{row['total_leads']})")
Use Case 4: Campaign ROI Tracking¶
# Track campaign costs and revenue by source
def calculate_source_roi(source_name, campaign_cost):
# Find source
source_ids = get_model("lead.source").search([["name", "=", source_name]])
if not source_ids:
return None
source_id = source_ids[0]
# Get all leads from this source
lead_ids = get_model("sale.lead").search([["source_id", "=", source_id]])
# Calculate total revenue from converted leads
total_revenue = 0
for lead_id in lead_ids:
lead = get_model("sale.lead").browse(lead_id)
# If lead was converted, find related opportunities/orders
if lead.state == "converted" and lead.contact_id:
# Find orders for this customer
order_ids = get_model("sale.order").search([
["contact_id", "=", lead.contact_id.id],
["state", "in", ["confirmed", "done"]]
])
orders = get_model("sale.order").browse(order_ids)
total_revenue += sum(o.amount_total for o in orders)
# Calculate ROI
roi = ((total_revenue - campaign_cost) / campaign_cost * 100) if campaign_cost > 0 else 0
return {
"source": source_name,
"leads": len(lead_ids),
"campaign_cost": campaign_cost,
"revenue": total_revenue,
"roi_percent": roi
}
# Example
roi_data = calculate_source_roi("Google Ads - Q1 Campaign", 50000)
print(f"ROI: {roi_data['roi_percent']:.1f}%")
Use Case 5: Multi-Touch Attribution¶
# Track multiple touchpoints in lead journey
def track_lead_touchpoints(lead_id, source_names):
"""
Track when a lead interacts with multiple sources
before conversion
"""
lead = get_model("sale.lead").browse(lead_id)
# Store touchpoint history in description or custom field
touchpoints = []
for source_name in source_names:
source_ids = get_model("lead.source").search([["name", "=", source_name]])
if source_ids:
touchpoints.append(source_name)
# Update lead with all touchpoints
touchpoint_text = " -> ".join(touchpoints)
get_model("sale.lead").write([lead_id], {
"description": f"Lead journey: {touchpoint_text}\n\n{lead.description or ''}"
})
# Usage
track_lead_touchpoints(lead_id, [
"LinkedIn Ad", # First touch
"Website Visit", # Research
"Webinar Registration", # Engagement
"Sales Call" # Conversion
])
Best Practices¶
1. Naming Conventions¶
# Good: Specific, descriptive source names
{"name": "Google Ads - Enterprise Campaign Q1 2026"}
{"name": "LinkedIn - Executive Targeting"}
{"name": "Trade Show - CES 2026"}
# Bad: Vague or generic names
{"name": "Internet"}
{"name": "Source 1"}
{"name": "Other"}
Guidelines: - Be specific about the campaign or channel - Include timeframe for time-limited campaigns - Use consistent naming format - Avoid generic catch-all sources
2. Source Hierarchy¶
# Consider using hierarchical naming for related sources
sources = [
"Google Ads - Search - Brand Keywords",
"Google Ads - Search - Competitor Keywords",
"Google Ads - Display - Retargeting",
"Facebook - Carousel Ads - Product Launch",
"Facebook - Video Ads - Brand Awareness"
]
# This allows grouping in reports by prefix
3. Data Quality¶
Regular cleanup:
# Find and merge duplicate sources
def find_duplicate_sources():
sources = get_model("lead.source").search_browse([])
# Group similar names
similar = {}
for source in sources:
key = source.name.lower().strip()
if key not in similar:
similar[key] = []
similar[key].append(source.id)
# Return potential duplicates
duplicates = {k: v for k, v in similar.items() if len(v) > 1}
return duplicates
4. Source Lifecycle Management¶
Retire old campaigns:
# Archive or mark old campaign sources
def retire_old_sources(cutoff_date):
# Find sources with no recent leads
all_source_ids = get_model("lead.source").search([])
for source_id in all_source_ids:
recent_leads = get_model("sale.lead").search([
["source_id", "=", source_id],
["create_date", ">=", cutoff_date]
])
if not recent_leads:
source = get_model("lead.source").browse(source_id)
# Mark as archived in description
get_model("lead.source").write([source_id], {
"description": f"[ARCHIVED] {source.description or ''}"
})
Performance Tips¶
1. Cache Frequently Used Sources¶
_source_cache = {}
def get_source_by_name(name):
if name not in _source_cache:
ids = get_model("lead.source").search([["name", "=", name]])
if ids:
_source_cache[name] = ids[0]
return _source_cache.get(name)
2. Batch Lead Creation¶
# When importing leads, batch create sources first
def import_leads_with_sources(leads_data):
# Extract unique sources
unique_sources = set(lead["source"] for lead in leads_data)
# Create all sources first
source_map = {}
for source_name in unique_sources:
ids = get_model("lead.source").search([["name", "=", source_name]])
if ids:
source_map[source_name] = ids[0]
else:
source_map[source_name] = get_model("lead.source").create({
"name": source_name
})
# Then create leads with pre-mapped sources
for lead_data in leads_data:
lead_data["source_id"] = source_map[lead_data["source"]]
# Create lead...
Troubleshooting¶
"Duplicate source names with slight variations"¶
Cause: Inconsistent data entry (e.g., "Google Ads" vs "GoogleAds" vs "Google ads") Solution:
# Standardize source names on creation
def standardize_source_name(name):
# Remove extra spaces, standardize case
name = " ".join(name.split()) # Remove multiple spaces
name = name.title() # Title case
return name
# Use when creating sources
standard_name = standardize_source_name(" google ads ")
# Result: "Google Ads"
"Unknown or missing source on leads"¶
Cause: Source not captured during lead creation Solution:
# Create "Unknown" default source
default_source_id = get_model("lead.source").create({
"name": "Unknown",
"description": "Source was not captured or specified"
})
# Assign to leads with missing source
leads_without_source = get_model("sale.lead").search([["source_id", "=", None]])
if leads_without_source:
get_model("sale.lead").write(leads_without_source, {
"source_id": default_source_id
})
Security Considerations¶
Permission Model¶
- Marketing team needs create/edit access
- Sales team needs read access
- Admins can delete unused sources
Data Access¶
- Sources are company-wide master data
- No row-level security needed
Integration Points¶
Internal Modules¶
- sale.lead: Primary consumer of source data
- sale.opportunity: Inherits source from converted leads
- report: Source attribution and marketing analytics
Marketing Integration¶
# Common integrations:
# - Google Ads API: Auto-create sources from campaigns
# - Marketing automation: Track campaign sources
# - Web analytics: Capture UTM parameters as sources
# - CRM imports: Map external source fields
Version History¶
Last Updated: 2026-01-05 Model Version: lead_source.py Framework: Netforce
Additional Resources¶
- Sales Lead Documentation:
sale.lead - Marketing Attribution Guide
- Campaign ROI Analysis
This documentation is generated for developer onboarding and reference purposes.