Skip to content

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:

_key = ["name"]

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%"]])

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.