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_idfield 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?¶
- Modularity: Sales email logic stays in sale module
- Maintainability: Base email module remains unchanged
- Upgradability: Base model can be updated independently
- 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¶
1. link_emails() - Enhanced Email Linking¶
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:
- Subject Parsing
- Uses regex pattern
\[(.*?)\]to extract reference numbers - Example: "Re: Your inquiry [LEAD-001]" extracts "LEAD-001"
-
Flexible - works with any format inside brackets
-
Entity Matching
- Searches sale.lead by number field
- Searches sale.opportunity by number field
-
Matches exact number only
-
Priority Handling
- Opportunities take precedence over leads
-
If both match, email links to opportunity
-
Polymorphic Linking
- Sets
related_idto "model_name,record_id" format - Links to sale.lead or sale.opportunity
- 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:
- Automatic Lead Population
- Title from email subject
- Contact name from sender
- Email address from sender
- Description from email body
-
Date from email date
-
User Assignment
- Optionally assign lead to specific user
-
Useful for routing rules
-
Lead Source Tracking
- Mark where lead originated
-
Analytics and reporting
-
Sales Email Handling
from_sales=Trueextracts original sender- Useful when forwarding customer emails
-
Falls back to from_addr if orig_from_addr missing
-
Workflow Integration
- Triggers "new_lead_from_email" event
- 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:
- Conditional Conversion
- Only converts leads in "new" state
-
Prevents reconversion of processed leads
-
Safety Checks
- Verifies email is linked to a lead
-
Checks related_id model is "sale.lead"
-
Delegation
- Uses lead's own
copy_to_opport()method - 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
Polymorphic related_id Field¶
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"
Accessing Related Records¶
# 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
Related Models¶
| 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
Use Case 2: Customer Replies to Lead Email (Auto-Link)¶
# 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
4. Check related_id Before Creating Leads¶
# 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¶
- Auto-Assignment Rules
- Route leads to sales reps based on criteria
- Balance workload across team
-
Assign by territory, product, or priority
-
Notification Workflows
- Email sales rep when lead is created
- Slack notification for high-value leads
-
SMS alert for urgent inquiries
-
Lead Qualification
- Auto-score leads based on email content
- Tag based on keywords
-
Set priority based on sender domain
-
Follow-up Automation
- Schedule auto-reply email
- Create follow-up task
- 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¶
Unit Test: Email Links to Lead via Subject¶
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.messagemodel - 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.