Skip to content

Email Message Extension Documentation (Sale Module)

Overview

The Email Message Extension (email.message via _inherit) is a MODEL EXTENSION that adds sales-specific email handling functionality to the core email.message model. This extension enables automatic linking of emails to sales entities (leads, opportunities), subject-based tracking, and automated lead creation from incoming emails.

This is NOT a standalone model - it extends the existing email.message model using the _inherit mechanism to provide CRM email automation.


Model Information

Model Name: email.message (EXTENSION) Base Model: email.message (from email module) Extension Module: netforce_sale Extension Mechanism: _inherit = "email.message" Purpose: Link emails to sale leads/opportunities and automate lead generation

Extension Features

  • Automatic email linking to sale leads and opportunities via subject parsing
  • Lead creation from incoming emails with configurable source and assignment
  • Opportunity conversion automation
  • Support for polymorphic related_id field with sale models
  • Contact extraction and matching
  • Zero new fields added (behavior extension only)

What This Extension Adds

This extension does NOT add any new fields to the email.message model. Instead, it: - Overrides link_emails() method to add sale entity recognition - Adds copy_to_lead() method for automatic lead creation - Adds convert_lead() method for lead-to-opportunity automation - Enables email tracking on sale.lead and sale.opportunity models


Understanding Model Extensions

What is _inherit?

The _inherit mechanism allows one module to extend models defined in other modules without modifying the original code:

class EmailMessage(Model):
    _inherit = "email.message"

    # This class extends the existing email.message model
    # All fields and methods from the base model are available
    # You can add new fields or override existing methods

Extension Architecture

┌──────────────────────────────────────┐
│   email.message (Base Model)        │
│   From: email module                 │
│   - Fields: subject, body, from, etc.│
│   - Methods: link_emails, etc.       │
└──────────────────────────────────────┘
            [_inherit]
┌──────────────────────────────────────┐
│   EmailMessage Extension (Sale)      │
│   Adds: Sale entity linking          │
│   Adds: Lead creation automation     │
│   Overrides: link_emails() method    │
└──────────────────────────────────────┘

Why Extend Instead of Modify?

  1. Modularity: Sales email logic stays in sale module
  2. Maintainability: Base email module remains unchanged
  3. Upgradability: Base model can be updated independently
  4. Separation of Concerns: Email handling vs. CRM integration

Extension Implementation

Base Model Location

  • Module: email (or base)
  • Model: email.message
  • File: Email module models directory

Extension Location

  • Module: netforce_sale
  • Model: email.message (extension)
  • File: /netforce_sale/netforce_sale/models/email_message.py

Extended/Added Methods

The sale module adds sales-specific functionality to email.message:

Method Extension Type Purpose
link_emails() Override with super() call Parse subject for lead/opportunity numbers and auto-link
copy_to_lead() New method Create sale leads from emails automatically
convert_lead() New method Convert linked leads to opportunities

Method Details

Method: link_emails(ids, context)

The extension overrides the link_emails() method to add automatic detection and linking of emails to sale leads and opportunities based on subject line parsing.

Extension Behavior:

The method follows this workflow:

1. Call Parent link_emails() (Process base email linking)
2. Parse Email Subject for Reference Numbers [XXX]
3. Search for Matching Lead Number
4. Search for Matching Opportunity Number
5. Link Email to Discovered Entity
6. Link Email to Entity's Contact

Detailed Algorithm:

def link_emails(self, ids, context={}):
    print("EmailMessage.link_emails", ids)

    # Step 1: Execute base email linking logic
    super().link_emails(ids, context=context)

    # Step 2: Process each email for sale entity linking
    for obj in self.browse(ids):
        lead_id = None
        opport_id = None

        # Step 3: Extract reference number from subject
        # Looks for pattern: [LEAD-001] or [OPP-002]
        m = re.search(r"\[(.*?)\]", obj.subject)
        if m:
            number = m.group(1)  # Extract the number

            # Step 4: Search for lead with this number
            res = get_model("sale.lead").search([["number", "=", number]])
            if res:
                lead_id = res[0]

            # Step 5: Search for opportunity with this number
            res = get_model("sale.opportunity").search([["number", "=", number]])
            if res:
                opport_id = res[0]

        # Step 6: Skip if no match found
        if not lead_id and not opport_id:
            continue

        # Step 7: Link to opportunity (takes precedence over lead)
        if opport_id:
            opport = get_model("sale.opportunity").browse(opport_id)
            obj.write({
                "related_id": "sale.opportunity,%s" % opport.id,
                "name_id": "contact,%s" % opport.contact_id.id
            })
        # Step 8: Link to lead if no opportunity match
        elif lead_id:
            lead = get_model("sale.lead").browse(lead_id)
            obj.write({
                "related_id": "sale.lead,%s" % lead_id
            })

Key Features:

  1. Subject Parsing
  2. Uses regex pattern \[(.*?)\] to extract reference numbers
  3. Example: "Re: Your inquiry [LEAD-001]" extracts "LEAD-001"
  4. Flexible - works with any format inside brackets

  5. Entity Matching

  6. Searches sale.lead by number field
  7. Searches sale.opportunity by number field
  8. Matches exact number only

  9. Priority Handling

  10. Opportunities take precedence over leads
  11. If both match, email links to opportunity

  12. Polymorphic Linking

  13. Sets related_id to "model_name,record_id" format
  14. Links to sale.lead or sale.opportunity
  15. Also links to contact for opportunities

Parameters: - ids (list): Email message IDs to process - context (dict): Optional context information

Example Usage:

# Email received with subject: "Re: Your inquiry [LEAD-123]"
email_id = 456

# Call link_emails
get_model("email.message").link_emails([email_id])

# Extension automatically:
# 1. Extracts "LEAD-123" from subject
# 2. Finds lead with number "LEAD-123"
# 3. Sets email.related_id = "sale.lead,{lead_id}"

# Now email is visible in lead's email history

Integration Example:

# Typical workflow:
# 1. Email arrives via IMAP/SMTP
email_id = get_model("email.message").create({
    "from_addr": "customer@example.com",
    "subject": "Re: Quote request [OPP-045]",
    "body": "When can I expect the quote?",
    "date": "2025-01-05 10:30:00"
})

# 2. System calls link_emails
get_model("email.message").link_emails([email_id])

# 3. Extension finds opportunity OPP-045
# 4. Email appears in opportunity's email thread
# 5. Sales rep sees customer follow-up in context

2. copy_to_lead() - Automated Lead Creation

Method: copy_to_lead(user=None, from_sales=False, lead_source=None, company_code=None, context)

Creates a new sale lead from an email message. Typically called via workflow automation when certain emails arrive.

Parameters: - user (str, optional): User login name to assign the lead to - from_sales (bool, default=False): If True, extracts original sender from orig_from_addr - lead_source (str, optional): Name of lead source to associate - company_code (str, optional): Company code for multi-company setups - context (dict): Must contain trigger_ids (email IDs to process)

Behavior:

def copy_to_lead(self, user=None, from_sales=False,
                 lead_source=None, company_code=None, context={}):
    # Step 1: Get email IDs from trigger context
    trigger_ids = context.get("trigger_ids")
    if trigger_ids is None:
        raise Exception("Missing trigger ids")

    # Step 2: Process each email
    for obj in self.browse(trigger_ids):
        # Skip if email already linked to something
        if obj.related_id:
            return

        # Step 3: Extract sender information
        from_name, from_email = parseaddr(obj.from_addr)

        # Step 4: If from_sales=True, try to get original sender
        if from_sales:
            try:
                orig_from_name, orig_from_email = parseaddr(obj.orig_from_addr)
                if orig_from_email:
                    from_name = orig_from_name
                    from_email = orig_from_email
            except:
                pass

        # Step 5: Resolve company if code provided
        if company_code:
            res = get_model("company").search([["code", "=", company_code]])
            if not res:
                raise Exception("Company not found: %s" % company_code)
            company_id = res[0]
        else:
            company_id = None

        # Step 6: Build lead values from email
        vals = {
            "date": obj.date[:10],  # Date only (no time)
            "title": obj.subject,
            "contact_name": from_name or from_email,
            "email": from_email,
            "description": obj.body,
            "company_id": company_id,
        }

        # Step 7: Assign to user if specified
        if user:
            res = get_model("base.user").search([["login", "=", user]])
            if not res:
                raise Exception("User not found: %s" % user)
            vals["user_id"] = res[0]

        # Step 8: Set lead source if specified
        if lead_source:
            res = get_model("lead.source").search([["name", "=", lead_source]])
            if not res:
                raise Exception("Lead source not found: %s" % lead_source)
            lead_source_id = res[0]
            vals["source_id"] = lead_source_id

        # Step 9: Create the lead
        lead_id = get_model("sale.lead").create(vals)

        # Step 10: Link email to new lead
        obj.write({
            "related_id": "sale.lead,%s" % lead_id,
        })

        # Step 11: Trigger workflow event
        get_model("sale.lead").trigger([lead_id], "new_lead_from_email")

Returns: None (modifies email and creates lead)

Key Features:

  1. Automatic Lead Population
  2. Title from email subject
  3. Contact name from sender
  4. Email address from sender
  5. Description from email body
  6. Date from email date

  7. User Assignment

  8. Optionally assign lead to specific user
  9. Useful for routing rules

  10. Lead Source Tracking

  11. Mark where lead originated
  12. Analytics and reporting

  13. Sales Email Handling

  14. from_sales=True extracts original sender
  15. Useful when forwarding customer emails
  16. Falls back to from_addr if orig_from_addr missing

  17. Workflow Integration

  18. Triggers "new_lead_from_email" event
  19. Can fire additional automations

Example Usage:

# Scenario: Email arrives from potential customer
email_id = 789

# Workflow rule configured to call copy_to_lead
# When: Email from unknown sender
# Action: Create lead

# Method called with context
get_model("email.message").copy_to_lead(
    user="sales_rep@company.com",
    lead_source="Email Inquiry",
    context={"trigger_ids": [email_id]}
)

# Result:
# 1. New lead created with email details
# 2. Lead assigned to sales_rep@company.com
# 3. Lead source set to "Email Inquiry"
# 4. Email linked to lead
# 5. "new_lead_from_email" event triggered

Workflow Configuration Example:

# Workflow: Email to Lead Conversion
# Model: email.message
# Trigger: on_create
# Condition: related_id is None AND from_addr not in team emails
# Action: Call method copy_to_lead
# Parameters:
#   - user: "auto_assign"
#   - lead_source: "Website Contact Form"
#   - from_sales: False

3. convert_lead() - Lead to Opportunity Conversion

Method: convert_lead(lead_source=None, context)

Automatically converts leads linked to emails into opportunities. Typically used in workflow automation.

Parameters: - lead_source (str, optional): Lead source name (currently unused in implementation) - context (dict): Must contain trigger_ids (email IDs to process)

Behavior:

def convert_lead(self, lead_source=None, context={}):
    # Step 1: Get email IDs from trigger context
    trigger_ids = context.get("trigger_ids")
    if trigger_ids is None:
        raise Exception("Missing trigger ids")

    # Step 2: Process each email
    for obj in self.browse(trigger_ids):
        # Step 3: Check if email is linked to a lead
        if not obj.related_id or obj.related_id._model != "sale.lead":
            continue

        # Step 4: Get the linked lead
        lead = obj.related_id

        # Step 5: Only convert if lead is in "new" state
        if lead.state == "new":
            lead.copy_to_opport()

Returns: None (triggers lead conversion)

Key Features:

  1. Conditional Conversion
  2. Only converts leads in "new" state
  3. Prevents reconversion of processed leads

  4. Safety Checks

  5. Verifies email is linked to a lead
  6. Checks related_id model is "sale.lead"

  7. Delegation

  8. Uses lead's own copy_to_opport() method
  9. Maintains lead conversion logic in one place

Example Usage:

# Scenario: Customer replies to lead email
email_id = 456  # Email linked to LEAD-001

# Workflow triggered when customer responds
get_model("email.message").convert_lead(
    context={"trigger_ids": [email_id]}
)

# If LEAD-001 is in "new" state:
# 1. Lead is converted to opportunity
# 2. Opportunity inherits lead data
# 3. Lead state changes to "converted"
# 4. Email now associated with opportunity chain

Workflow Configuration Example:

# Workflow: Auto-Convert Engaged Leads
# Model: email.message
# Trigger: on_create
# Condition: related_id._model == "sale.lead" AND
#            related_id.state == "new" AND
#            is_reply == True
# Action: Call method convert_lead

Understanding Polymorphic References

The related_id field uses a polymorphic pattern to link emails to different entity types:

# Format: "model_name,record_id"

# Email linked to lead
email.related_id = "sale.lead,123"

# Email linked to opportunity
email.related_id = "sale.opportunity,456"

# Email linked to sale order
email.related_id = "sale.order,789"

# Email linked to quotation
email.related_id = "sale.quot,101"
# Get email
email = get_model("email.message").browse(email_id)

# Access polymorphic reference
if email.related_id:
    related_model = email.related_id._model
    related_id = email.related_id.id

    if related_model == "sale.lead":
        # Email is linked to a lead
        lead = email.related_id
        print(lead.title, lead.state)

    elif related_model == "sale.opportunity":
        # Email is linked to an opportunity
        opport = email.related_id
        print(opport.name, opport.amount)

Sale Models Email Integration

Email Tracking on Sale Entities

The extension enables email history tracking on:

Model Email Linking Purpose
sale.lead Via number in subject or copy_to_lead() Track lead communications
sale.opportunity Via number in subject Track opportunity discussions
sale.order Via number in subject (if implemented) Track order communications
sale.quot Via number in subject (if implemented) Track quotation discussions

Email Thread Views

When emails are linked to sale entities, they appear in: - Lead detail view: Email history tab - Opportunity detail view: Communication log - Sale order view: Customer correspondence


Model Relationship Description
email.message Base Model (Extended) Core email message model
sale.lead Polymorphic Link Leads created from or linked to emails
sale.opportunity Polymorphic Link Opportunities linked to emails
sale.order Polymorphic Link Orders linked to emails
contact Referenced Contact associated with email sender
base.user Assignment User assigned to created leads
lead.source Classification Source tracking for leads
company Multi-company Company context for leads

Common Use Cases

Use Case 1: Customer Inquiry Creates Lead Automatically

# Scenario: Customer sends email to sales@company.com
# Subject: "Interested in your product"
# From: customer@example.com

# Email arrives and is created
email_id = get_model("email.message").create({
    "from_addr": "Customer Name <customer@example.com>",
    "to_addr": "sales@company.com",
    "subject": "Interested in your product",
    "body": "I would like to learn more about your enterprise solution...",
    "date": "2025-01-05 14:30:00"
})

# Workflow rule triggers: "Unknown sender email to sales"
# Action: copy_to_lead
get_model("email.message").copy_to_lead(
    user="sales_team_lead",
    lead_source="Email Inquiry",
    context={"trigger_ids": [email_id]}
)

# Result:
# 1. Lead created:
#    - Title: "Interested in your product"
#    - Contact Name: "Customer Name"
#    - Email: customer@example.com
#    - Description: email body
#    - Assigned to: sales_team_lead
#    - Source: "Email Inquiry"
# 2. Email linked to lead
# 3. Sales rep notified via "new_lead_from_email" event

# Scenario: Sales rep sent email to lead LEAD-123
# Subject includes [LEAD-123] reference

# Customer replies
reply_id = get_model("email.message").create({
    "from_addr": "customer@example.com",
    "to_addr": "sales_rep@company.com",
    "subject": "Re: Your proposal [LEAD-123]",
    "body": "Thanks for the information. I have some questions...",
    "date": "2025-01-05 16:45:00"
})

# System calls link_emails (automatically or via workflow)
get_model("email.message").link_emails([reply_id])

# Extension:
# 1. Extracts "LEAD-123" from subject
# 2. Finds lead with number="LEAD-123"
# 3. Sets reply.related_id = "sale.lead,{lead_id}"

# Result:
# - Reply appears in lead's email history
# - Sales rep sees customer response in context
# - Maintains conversation threading

Use Case 3: Lead Progression via Email Engagement

# Scenario: Lead responds to initial contact, showing interest

# Initial email created lead
lead_id = 123  # LEAD-123 exists

# Customer sends follow-up email
followup_id = get_model("email.message").create({
    "from_addr": "customer@example.com",
    "subject": "Re: Product information [LEAD-123]",
    "body": "Very interested. Can we schedule a demo?",
    "date": "2025-01-06 09:00:00"
})

# Link email
get_model("email.message").link_emails([followup_id])
# Email now linked to LEAD-123

# Workflow: "Convert engaged leads"
# Condition: Lead state = "new" AND customer replied
get_model("email.message").convert_lead(
    context={"trigger_ids": [followup_id]}
)

# Result:
# 1. LEAD-123 converted to opportunity OPP-045
# 2. Email history preserved
# 3. Opportunity shows customer engagement
# 4. Sales rep notified to follow up with demo

Use Case 4: Sales Rep Forwards Customer Email

# Scenario: Customer emails sales rep directly, rep forwards to system

# Email forwarded to system email address
forwarded_id = get_model("email.message").create({
    "from_addr": "sales_rep@company.com",  # Rep forwarding
    "orig_from_addr": "Customer <customer@example.com>",  # Original sender
    "subject": "FWD: Partnership opportunity",
    "body": "---- Forwarded message ----\nFrom: customer@example.com\n...",
    "date": "2025-01-05 11:00:00"
})

# Create lead with from_sales=True
get_model("email.message").copy_to_lead(
    user="account_manager",
    lead_source="Partner Referral",
    from_sales=True,  # Extract original sender
    context={"trigger_ids": [forwarded_id]}
)

# Result:
# 1. Lead created with customer@example.com as contact
#    (NOT sales_rep@company.com)
# 2. Lead properly attributed to actual customer
# 3. Assigned to appropriate account manager

Use Case 5: Opportunity Communication Tracking

# Scenario: Track all emails related to an opportunity

# Opportunity created: OPP-045

# Multiple emails exchanged
email_ids = []

# Initial quote sent
email1 = get_model("email.message").create({
    "from_addr": "sales@company.com",
    "to_addr": "customer@example.com",
    "subject": "Quotation for your project [OPP-045]",
    "body": "Please find attached our quotation...",
    "date": "2025-01-05"
})
email_ids.append(email1)

# Customer questions
email2 = get_model("email.message").create({
    "from_addr": "customer@example.com",
    "to_addr": "sales@company.com",
    "subject": "Re: Quotation for your project [OPP-045]",
    "body": "Can you adjust the payment terms?",
    "date": "2025-01-06"
})
email_ids.append(email2)

# Sales reply
email3 = get_model("email.message").create({
    "from_addr": "sales@company.com",
    "to_addr": "customer@example.com",
    "subject": "Re: Quotation for your project [OPP-045]",
    "body": "Yes, we can offer 30-day terms...",
    "date": "2025-01-07"
})
email_ids.append(email3)

# Link all emails
get_model("email.message").link_emails(email_ids)

# Result:
# - All 3 emails linked to OPP-045
# - Complete email thread visible in opportunity
# - Timeline of negotiation documented
# - Context for deal progression

Best Practices

1. Use Consistent Subject Line References

# Good: Include reference in subject for auto-linking
subject = "Re: Your inquiry [LEAD-123]"
subject = "Follow-up regarding [OPP-045]"
subject = "Order details [SO-789]"

# Bad: No reference
subject = "Re: Your inquiry"
# Result: Email won't auto-link to lead

# Ensure your email templates include references:
email_template = """
Subject: Re: {lead.title} [{ lead.number}]

Dear {contact.name},
...
"""

2. Configure Workflow Rules for Automation

# Set up intelligent workflow rules

# Rule 1: Unknown sender to sales → Create lead
# Condition: from_addr not in known_contacts AND to_addr == "sales@"
# Action: copy_to_lead(lead_source="Email Inquiry")

# Rule 2: Customer reply to lead → Convert to opportunity
# Condition: related_id._model == "sale.lead" AND is_reply == True
# Action: convert_lead()

# Rule 3: Email from important domain → Assign to senior rep
# Condition: from_addr ends with "@fortune500.com"
# Action: copy_to_lead(user="senior_rep")

3. Handle orig_from_addr for Forwarded Emails

# When forwarding customer emails, preserve original sender

# Good: Set orig_from_addr
email_id = get_model("email.message").create({
    "from_addr": "rep@company.com",
    "orig_from_addr": "customer@example.com",  # Preserve original
    "subject": "FWD: Inquiry",
    "body": "..."
})

# Use from_sales=True when creating lead
get_model("email.message").copy_to_lead(
    from_sales=True,  # Will use orig_from_addr
    context={"trigger_ids": [email_id]}
)

# Result: Lead contact is customer, not internal rep

# Good: Check if email already linked
email = get_model("email.message").browse(email_id)
if not email.related_id:
    # Email not linked, create lead
    get_model("email.message").copy_to_lead(
        context={"trigger_ids": [email_id]}
    )
else:
    # Email already linked, skip
    pass

# The copy_to_lead() method does this check internally
# But good to check in workflow conditions too

5. Use Lead Sources for Analytics

# Track where leads come from

# Different lead sources
get_model("email.message").copy_to_lead(
    lead_source="Website Contact Form",
    context={"trigger_ids": [email1_id]}
)

get_model("email.message").copy_to_lead(
    lead_source="Email Inquiry",
    context={"trigger_ids": [email2_id]}
)

get_model("email.message").copy_to_lead(
    lead_source="Support Escalation",
    context={"trigger_ids": [email3_id]}
)

# Later, report on lead sources:
# SELECT source_id, COUNT(*) FROM sale_lead GROUP BY source_id

Subject Parsing Logic

Reference Number Patterns

The extension uses regex to extract reference numbers:

pattern = r"\[(.*?)\]"  # Matches content within brackets

# Examples of matches:
"[LEAD-001]"  extracts "LEAD-001"
"[OPP-123]"  extracts "OPP-123"
"Re: Your inquiry [LEAD-999]"  extracts "LEAD-999"
"Multiple [FIRST] [SECOND]"  extracts "FIRST" (first match only)

# Examples of non-matches:
"No brackets"  no match
""  no match
"(LEAD-001)"  no match (uses parentheses, not brackets)

Best Practices for Reference Numbers

# Good: Use brackets in subject
"Re: Your proposal [LEAD-123]"
"Follow-up [OPP-456]"

# Also works: Reference anywhere in subject
"[LEAD-123] Initial contact"
"Discussion about [OPP-789] terms"

# Note: Only first bracketed reference is extracted
"[LEAD-123] converted to [OPP-456]"  matches "LEAD-123"

Workflow Integration

Trigger Events

The extension fires workflow triggers:

get_model("sale.lead").trigger([lead_id], "new_lead_from_email")
# Fired when: Lead is created from email via copy_to_lead()

Workflow Actions You Can Configure

  1. Auto-Assignment Rules
  2. Route leads to sales reps based on criteria
  3. Balance workload across team
  4. Assign by territory, product, or priority

  5. Notification Workflows

  6. Email sales rep when lead is created
  7. Slack notification for high-value leads
  8. SMS alert for urgent inquiries

  9. Lead Qualification

  10. Auto-score leads based on email content
  11. Tag based on keywords
  12. Set priority based on sender domain

  13. Follow-up Automation

  14. Schedule auto-reply email
  15. Create follow-up task
  16. Add to nurture campaign

Email Automation Scenarios

Scenario 1: Support-to-Sales Escalation

# Support email hints at sales opportunity
# "I need this for 500 users" → potential enterprise deal

# Workflow:
# 1. Support rep forwards email to sales@
# 2. Email arrives with orig_from_addr = customer
# 3. copy_to_lead() creates high-priority lead
# 4. Lead assigned to enterprise sales rep
# 5. Rep receives notification
# 6. Rep reaches out with proper context

Scenario 2: Web Form Submission

# Contact form on website sends email to system

# Email format:
# From: website@company.com
# To: leads@company.com
# Subject: Contact Form: {company_name}
# Body: Parsed form fields

# Workflow:
# 1. Email created from form submission
# 2. copy_to_lead() creates lead
# 3. Lead source = "Website Contact Form"
# 4. Auto-assigned by round-robin rule
# 5. Rep contacts within 1 hour

Scenario 3: Trade Show Follow-up

# After trade show, attendees receive email
# Subject: "Thanks for visiting booth [EVENT-2025]"

# Attendees reply:
# Subject: "Re: Thanks for visiting booth [EVENT-2025]"

# Workflow:
# 1. Reply arrives
# 2. link_emails() tries to match "EVENT-2025"
# 3. No match (not a lead/opportunity number)
# 4. Fallback: copy_to_lead() creates lead
# 5. Lead source = "Trade Show 2025"
# 6. Tagged with event name

Troubleshooting

Email Not Linking to Lead/Opportunity

Cause 1: No reference number in subject Solution: Ensure subject contains bracketed reference like [LEAD-123]

Cause 2: Reference number doesn't match any record Solution: Verify lead/opportunity number is correct and record exists

Cause 3: Multiple entities with same number Solution: Ensure lead and opportunity numbers are unique


Lead Not Created from Email

Cause 1: Email already has related_id Solution: copy_to_lead() skips emails already linked. This is intentional to prevent duplicates.

Cause 2: trigger_ids missing from context Solution: Always call with context={"trigger_ids": [email_id]}

Cause 3: User/lead source not found Solution: Check user login and lead source name exist in database


Wrong Contact on Lead

Cause 1: from_sales=True but no orig_from_addr Solution: Provide orig_from_addr when forwarding emails, or use from_sales=False

Cause 2: Email parsing issue Solution: Check from_addr format: "Name email@example.com" is properly parsed


Testing Examples

def test_email_links_to_lead_via_subject():
    # Create a lead
    lead_id = get_model("sale.lead").create({
        "number": "LEAD-001",
        "title": "Test lead",
        "email": "customer@example.com"
    })

    # Create email with reference in subject
    email_id = get_model("email.message").create({
        "from_addr": "customer@example.com",
        "subject": "Re: Your proposal [LEAD-001]",
        "body": "I'm interested",
        "date": "2025-01-05"
    })

    # Link emails
    get_model("email.message").link_emails([email_id])

    # Verify linkage
    email = get_model("email.message").browse(email_id)
    assert email.related_id
    assert email.related_id._model == "sale.lead"
    assert email.related_id.id == lead_id

Unit Test: copy_to_lead Creates Lead

def test_copy_to_lead_creates_lead():
    # Create email from customer
    email_id = get_model("email.message").create({
        "from_addr": "John Doe <john@example.com>",
        "subject": "Product inquiry",
        "body": "I need more information",
        "date": "2025-01-05 10:00:00"
    })

    # Create lead from email
    get_model("email.message").copy_to_lead(
        lead_source="Email Inquiry",
        context={"trigger_ids": [email_id]}
    )

    # Find created lead
    leads = get_model("sale.lead").search([
        ["email", "=", "john@example.com"]
    ])
    assert len(leads) == 1

    # Verify lead details
    lead = get_model("sale.lead").browse(leads[0])
    assert lead.title == "Product inquiry"
    assert lead.contact_name == "John Doe"
    assert lead.email == "john@example.com"
    assert "I need more information" in lead.description

    # Verify email linkage
    email = get_model("email.message").browse(email_id)
    assert email.related_id
    assert email.related_id.id == lead.id

Security Considerations

Permission Model

  • Extension uses same permissions as base email.message model
  • Lead creation requires sale.lead create permissions
  • User assignment respects user access controls

Data Access

  • copy_to_lead() only creates leads from emails in trigger_ids
  • No access to emails outside provided context
  • User assignment validated against actual users

Email Privacy

  • Linked emails visible to users with access to related record
  • Follow standard Netforce permissions for related entities
  • Email content subject to same data access rules

Version History

Last Updated: 2025-01-05 Model Version: email_message.py (Extension) Framework: Netforce Base Model: email.message Extension Module: netforce_sale


Additional Resources

  • Base Model Documentation: email.message
  • Sale Lead Documentation: sale.lead
  • Sale Opportunity Documentation: sale.opportunity
  • Workflow Automation Guide
  • Email Integration Setup

Support & Feedback

For issues or questions about this extension: 1. Check email subject contains bracketed reference [NUMBER] 2. Verify lead/opportunity number matches exactly 3. Ensure workflow trigger_ids are provided in context 4. Test user assignment and lead source lookups 5. Review workflow rule conditions and actions


This documentation covers the sale module's extension of the email.message model for CRM email automation and lead generation.