Sales Opportunity Documentation¶
Overview¶
The Sales Opportunity module (sale.opportunity) manages the sales pipeline and tracks potential revenue opportunities from initial contact through to won/lost outcomes. It provides comprehensive opportunity management with stage progression, probability-based forecasting, competitor tracking, and automated conversion to quotations.
Model Information¶
Model Name: sale.opportunity
Display Name: Opportunity
Key Fields: ["number"]
Features¶
- Audit logging enabled (
_audit_log = True) - Multi-company support (
_multi_company = True) - Full-text content search (
_content_search = True) - Unique sequence-based numbering
- Name field uses
numberfor display
Understanding the Sales Pipeline¶
What is a Sales Opportunity?¶
A Sales Opportunity represents a qualified potential sale that has moved beyond the initial lead stage. Opportunities track: - Expected revenue amount - Probability of closing - Expected close date - Current stage in the sales pipeline - Associated products and competitors - Activities and communications
Sales Funnel Stages¶
Opportunities progress through customizable stages defined in the sale.stage model:
Each stage can have an associated probability percentage that helps forecast expected revenue.
Expected Revenue Calculation¶
Formula: Expected Revenue = Amount × (Probability / 100)
Example: - Opportunity Amount: $100,000 - Probability: 60% - Expected Revenue: $60,000
This allows for realistic revenue forecasting based on pipeline health.
State Workflow¶
┌─────────────────────────────────────────┐
│ open (default) │
└─────────────┬───────────────────────────┘
│
┌───────────┼───────────┬───────────┐
↓ ↓ ↓ ↓
paused won lost canceled
│ │ │ │
│ │ │ │
└───────→ reopen ←──────┴───────────┘
| State | Description |
|---|---|
open |
Active opportunity being pursued (default state) |
won |
Successfully closed and converted to sale |
lost |
Lost to competitor or customer declined (requires reason) |
paused |
Temporarily on hold but may reactivate |
canceled |
Canceled by company (requires reason) |
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
number |
Char | Yes | Auto-generated sequence number (e.g., "OPP-0001") |
name |
Char | Yes | Descriptive opportunity name |
contact_id |
Many2One | Yes | Customer/prospect contact |
user_id |
Many2One | No | Opportunity owner (defaults to current user) |
company_id |
Many2One | No | Company (for multi-company setups) |
date |
Date | Yes | Opportunity open date (defaults to today) |
Sales Pipeline Fields¶
| Field | Type | Description |
|---|---|---|
stage_id |
Many2One | Current sales stage (from sale.stage) |
probability |
Decimal | Win probability percentage (0-100) |
amount |
Decimal | Expected revenue amount |
expected_close_date |
Date | When opportunity is expected to close |
state |
Selection | Opportunity status (open/won/lost/paused/canceled) |
date_close |
Date | Actual close date when won/lost |
Product Fields¶
| Field | Type | Description |
|---|---|---|
product_id |
Many2One | Primary product (legacy single product) |
products |
Many2Many | Multiple products for this opportunity |
qty |
Decimal | Quantity for primary product |
Source & Campaign Fields¶
| Field | Type | Description |
|---|---|---|
lead_id |
Many2One | Original sales lead (if converted from lead) |
source_id |
Many2One | Lead source (web, referral, campaign, etc.) |
lead_source |
Char | Deprecated - use source_id instead |
campaign_id |
Many2One | Marketing campaign that generated opportunity |
Categorization Fields¶
| Field | Type | Description |
|---|---|---|
categ_id |
Many2One | Sales category for classification |
region_id |
Many2One | Geographic region |
industry_id |
Many2One | Customer industry vertical |
Process Fields¶
| Field | Type | Description |
|---|---|---|
next_step |
Char | Description of next action required |
description |
Text | Detailed opportunity description |
remind_date |
Date | Next reminder/follow-up date |
lost_reason_id |
Many2One | Reason code if opportunity is lost |
cancel_reason_id |
Many2One | Reason code if opportunity is canceled |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
quotations |
One2Many | Related quotations generated from opportunity |
competitors |
One2Many | Competitors tracked for this opportunity |
activities |
One2Many | Associated tasks, calls, meetings (sale.activ) |
comments |
One2Many | Internal comments and notes |
emails |
One2Many | Email communications related to opportunity |
documents |
One2Many | Attached documents |
notifs |
One2Many | Reminder notifications |
Computed Fields¶
| Field | Type | Function | Description |
|---|---|---|---|
year |
Char | SQL | Year from close date |
quarter |
Char | SQL | Quarter from close date |
month |
Char | SQL | Month from close date |
week |
Char | SQL | Week from close date |
date_week |
Char | get_date_agg | Week aggregation (YYYY-WNN) |
date_month |
Char | get_date_agg | Month aggregation (YYYY-MM) |
agg_amount |
Decimal | SQL SUM | Total amount (for reporting) |
last_email_days |
Integer | get_last_email_days | Business days since last email |
email_body |
Text | get_email_body | Body of most recent email |
age_days |
Integer | get_age | Days since opportunity opened |
API Methods¶
1. Create Opportunity¶
Method: create(vals, context)
Creates a new sales opportunity with automatic number generation.
Parameters:
vals = {
"name": "Enterprise Software License", # Required
"contact_id": 123, # Required: customer ID
"user_id": 5, # Optional: owner
"amount": 50000.00, # Expected revenue
"probability": 60, # Win probability %
"stage_id": 2, # Sales stage
"expected_close_date": "2026-03-31", # Target close
"product_id": 45, # Primary product
"campaign_id": 10, # Source campaign
"next_step": "Schedule demo presentation",
"description": "Multi-year enterprise license",
"competitors": [ # Track competitors
("create", {
"compet_id": 8,
"strengths": "Lower price point",
"weaknesses": "Limited features"
})
]
}
context = {
"company_id": 1 # For multi-company
}
Returns: int - New opportunity ID
Example:
# Create opportunity with competitor tracking
opport_id = get_model("sale.opportunity").create({
"name": "Cloud Infrastructure Migration",
"contact_id": 456,
"amount": 250000,
"probability": 70,
"stage_id": 3,
"expected_close_date": "2026-06-30",
"products": [("set", [12, 15, 18])], # Multiple products
"competitors": [
("create", {
"compet_id": 5,
"strengths": "Established relationship",
"weaknesses": "Higher cost, slower delivery"
})
],
"next_step": "Technical architecture review"
})
Auto-Generated Fields:
- number: Generated from sequence (e.g., "OPP-0001")
- state: Defaults to "open"
- user_id: Defaults to current user
- company_id: Defaults to active company
- date: Defaults to today's date
2. Copy to Quotation¶
Method: copy_to_quotation(ids, context)
Converts an opportunity into a formal quotation, optionally using a quotation template.
Parameters:
- ids (list): Single opportunity ID
- context (dict): Optional template_id for pre-configured quotations
Context Options:
Behavior:
- With Template: Copies template quotation and updates with opportunity data
- Without Template: Creates new quotation with basic opportunity information
- Links quotation back to opportunity via opport_id
- Transfers contact, user, and reference information
- If product specified, creates quotation line with product details
Returns: dict - Navigation action and flash message
{
"next": {
"name": "quot",
"mode": "form",
"active_id": quot_id
},
"flash": "Quotation QUOT-0123 created from opportunity"
}
Example:
# Convert to quotation using template
result = get_model("sale.opportunity").copy_to_quotation(
[opport_id],
context={"template_id": 10}
)
# Convert to quotation without template (basic)
result = get_model("sale.opportunity").copy_to_quotation([opport_id])
Quotation Field Mapping:
- opport_id ← opportunity ID (link back)
- ref ← opportunity number
- contact_id ← opportunity contact
- user_id ← opportunity owner
- lines ← created from product_id if specified
3. State Transition Methods¶
3.1 Mark as Won¶
Method: won(ids, context)
Marks opportunity as successfully closed and won.
Parameters:
- ids (list): Opportunity IDs to mark as won
Behavior:
- Sets state to "won"
- Typically followed by quotation conversion to sales order
- Updates close date
Example:
3.2 Mark as Lost¶
Method: lost(ids, context)
Marks opportunity as lost, requiring a reason code.
Parameters:
- ids (list): Opportunity IDs to mark as lost
Behavior:
- Validates that lost_reason_id is set
- Raises exception if reason is missing
- Sets state to "lost"
Example:
# First set the lost reason
get_model("sale.opportunity").write([opport_id], {
"lost_reason_id": 3 # Reason code: "Lost to competitor"
})
# Then mark as lost
get_model("sale.opportunity").lost([opport_id])
Error Handling:
# This will raise an exception:
get_model("sale.opportunity").lost([opport_id])
# Exception: "Missing lost reason"
3.3 Pause Opportunity¶
Method: pause(ids, context)
Temporarily pauses an opportunity that may resume later.
Parameters:
- ids (list): Opportunity IDs to pause
Behavior:
- Sets state to "paused"
- Useful for opportunities on hold pending budget approval or timing
Example:
3.4 Cancel Opportunity¶
Method: cancel(ids, context)
Cancels an opportunity, requiring a cancellation reason.
Parameters:
- ids (list): Opportunity IDs to cancel
Behavior:
- Validates that cancel_reason_id is set
- Raises exception if reason is missing
- Sets state to "canceled"
Example:
# Set cancel reason first
get_model("sale.opportunity").write([opport_id], {
"cancel_reason_id": 2 # "Project postponed indefinitely"
})
# Then cancel
get_model("sale.opportunity").cancel([opport_id])
3.5 Reopen Opportunity¶
Method: reopen(ids, context)
Reopens a paused, lost, or canceled opportunity.
Parameters:
- ids (list): Opportunity IDs to reopen
Behavior:
- Sets state back to "open"
- Allows previously closed opportunities to be pursued again
Example:
4. Copy Opportunity¶
Method: copy(ids, context)
Duplicates an existing opportunity with a new number.
Parameters:
- ids (list): Single opportunity ID to copy
Behavior:
- Creates new opportunity with duplicated field values
- Generates new number from sequence
- Does NOT copy: competitors, quotations, activities, emails
- Copies: contact, stage, amount, probability, products, dates
Returns: dict - Navigation to new opportunity
Example:
# Duplicate opportunity for similar deal
result = get_model("sale.opportunity").copy([opport_id])
new_opport_id = result["next"]["active_id"]
5. Email Integration¶
5.1 Create from Email¶
Method: copy_from_email(user_login, context)
Creates opportunity from incoming email (used in email automation).
Parameters:
- user_login (str): Optional user login to assign as owner
- context["trigger_ids"]: Email message ID that triggered creation
Behavior: - Extracts sender information from email - Creates or finds contact based on email address - Creates opportunity with email subject as name - Links email to opportunity - Handles email threading (replies inherit parent opportunity)
Example:
# Called by email trigger workflow
get_model("sale.opportunity").copy_from_email(
user_login="sales@company.com",
context={"trigger_ids": [email_id]}
)
Email Threading Logic:
- If email is a reply (has parent_uid), inherits opportunity from parent
- If new email thread, creates new opportunity and contact if needed
6. Reminder Management¶
6.1 Send Reminders¶
Method: send_reminders(min_days, template, company, to_addrs, context)
Sends reminder emails for opportunities that haven't had contact in specified days.
Parameters:
- min_days (int): Minimum business days since last email
- template (str): Email template name to use
- company (str): Filter by company code
- to_addrs (str): Recipient email addresses
Behavior: - Finds all open opportunities - Filters by company if specified - Checks days since last email communication - Sends reminder email using specified template - Returns count of opportunities reminded
Example:
# Send reminders for opportunities with no contact in 7+ days
result = get_model("sale.opportunity").send_reminders(
min_days=7,
template="Opportunity Follow-up Reminder",
company="ACME",
to_addrs="sales-team@company.com"
)
# Returns: "15 opportunities reminded"
6.2 Update Notifications¶
Method: update_notifs(ids, context)
Updates reminder notifications when remind_date changes.
Parameters:
- ids (list): Opportunity IDs to update
Behavior:
- Deletes existing pending notifications
- Creates new notification if remind_date is set
- Assigns notification to opportunity owner
- Sets reminder title: "Please follow up opportunity OPP-XXXX"
Example:
# Set reminder date (triggers notification automatically)
get_model("sale.opportunity").write([opport_id], {
"remind_date": "2026-02-15"
})
# Notification created automatically via write() override
Auto-Triggered: This method is called automatically when remind_date is modified via the write() method override.
7. Computed Field Functions¶
7.1 get_last_email_days(ids, context)¶
Calculates business days (excluding weekends) since last email communication.
Returns: dict - {opportunity_id: days_count}
Example:
# Used internally for forecasting engagement health
days = obj.last_email_days # 5 (business days since last email)
7.2 get_email_body(ids, context)¶
Returns the body content of the most recent email.
Returns: dict - {opportunity_id: email_body_text}
7.3 get_age(ids, context)¶
Calculates total days since opportunity was opened.
Returns: dict - {opportunity_id: age_in_days}
Example:
if obj.age_days > 90:
# Flag stale opportunities over 90 days old
print(f"Opportunity {obj.number} is {obj.age_days} days old")
7.4 get_date_agg(ids, context)¶
Returns aggregated date fields for reporting (week and month formats).
Returns: dict - {opportunity_id: {"date_week": "2026-W06", "date_month": "2026-02"}}
8. Reporting Query¶
8.1 get_opportunity(context, month)¶
Generates opportunity age analysis for dashboard reporting.
Parameters:
- month (str): Optional month filter in "YYYY-MM-DD" format
Behavior: - Queries opportunities created within specified month (or current month) - Calculates age from creation to close - Returns formatted data for charting
Returns: List of dataset dictionaries for visualization
[{
"name": "New Customer",
"data": [
{"name": "Opportunity 1", "date": "2026-01-15", "remaining": 45},
{"name": "Opportunity 2", "date": "2026-01-20", "remaining": 38}
],
"color": "blue"
}]
Example:
# Get January 2026 opportunities
data = get_model("sale.opportunity").get_opportunity(
month="2026-01-01"
)
Search Functions¶
Search by State¶
# Find all open opportunities
open_opports = get_model("sale.opportunity").search([
["state", "=", "open"]
])
# Find won opportunities in Q1 2026
won_q1 = get_model("sale.opportunity").search([
["state", "=", "won"],
["date_close", ">=", "2026-01-01"],
["date_close", "<=", "2026-03-31"]
])
Search by Amount and Probability¶
# Find high-value opportunities (>$100K) with good probability
big_deals = get_model("sale.opportunity").search([
["amount", ">", 100000],
["probability", ">=", 60],
["state", "=", "open"]
])
Search by Owner and Region¶
# Find opportunities for specific sales rep in APAC
my_apac_opports = get_model("sale.opportunity").search([
["user_id", "=", user_id],
["region_id.code", "=", "APAC"],
["state", "=", "open"]
])
Search by Close Date Range¶
# Opportunities closing this quarter
from datetime import datetime, timedelta
quarter_start = "2026-01-01"
quarter_end = "2026-03-31"
closing_soon = get_model("sale.opportunity").search([
["expected_close_date", ">=", quarter_start],
["expected_close_date", "<=", quarter_end],
["state", "=", "open"]
])
Search by Lead Source¶
# Opportunities from website lead source
web_opports = get_model("sale.opportunity").search([
["source_id.name", "=", "Website"],
["date", ">=", "2026-01-01"]
])
Related Models¶
| Model | Relationship | Description |
|---|---|---|
contact |
Many2One | Customer/prospect contact record |
sale.stage |
Many2One | Sales pipeline stage definition |
sale.lead |
Many2One | Original lead if converted from lead |
lead.source |
Many2One | Lead source tracking (web, referral, etc.) |
mkt.campaign |
Many2One | Marketing campaign that generated lead |
sale.quot |
One2Many | Quotations created from this opportunity |
opport.compet |
One2Many | Competitor tracking records |
sale.activ |
One2Many | Activities (calls, meetings, tasks) |
email.message |
One2Many | Email communications |
message |
One2Many | Internal comments |
document |
One2Many | Attached documents |
product |
Many2One/Many2Many | Products associated with opportunity |
company |
Many2One | Company (multi-company support) |
base.user |
Many2One | Opportunity owner |
region |
Many2One | Geographic region |
industry |
Many2One | Industry vertical |
sale.categ |
Many2One | Sales category |
reason.code |
Many2One | Lost/cancel reason codes |
notif |
One2Many | Reminder notifications |
Common Use Cases¶
Use Case 1: Complete Opportunity Lifecycle¶
# 1. Create opportunity from qualified lead
opport_id = get_model("sale.opportunity").create({
"name": "Enterprise CRM Implementation",
"contact_id": 789,
"lead_id": 456, # Link to original lead
"amount": 150000,
"probability": 40,
"stage_id": 2, # "Qualification" stage
"expected_close_date": "2026-06-30",
"source_id": 3, # "Trade Show"
"campaign_id": 12,
"user_id": 5,
"next_step": "Schedule discovery call"
})
# 2. Add competitor analysis
get_model("opport.compet").create({
"opport_id": opport_id,
"compet_id": 7,
"strengths": "Lower upfront cost, market leader",
"weaknesses": "Limited customization, poor support reviews"
})
# 3. Track activities
get_model("sale.activ").create({
"type": "meeting",
"subject": "Discovery call with stakeholders",
"date": "2026-01-20",
"related_id": f"sale.opportunity,{opport_id}",
"user_id": 5,
"description": "Discuss requirements and timeline"
})
# 4. Progress through stages and update probability
get_model("sale.opportunity").write([opport_id], {
"stage_id": 4, # "Proposal" stage
"probability": 65,
"next_step": "Prepare custom demo"
})
# 5. Convert to quotation
result = get_model("sale.opportunity").copy_to_quotation(
[opport_id],
context={"template_id": 15}
)
quot_id = result["next"]["active_id"]
# 6. Mark as won when quotation accepted
get_model("sale.opportunity").won([opport_id])
Use Case 2: Revenue Forecasting by Stage¶
# Calculate weighted pipeline forecast
stages = get_model("sale.stage").search_browse([])
forecast = {}
for stage in stages:
opports = get_model("sale.opportunity").search_browse([
["stage_id", "=", stage.id],
["state", "=", "open"]
])
total_amount = sum(o.amount or 0 for o in opports)
avg_probability = sum(o.probability or 0 for o in opports) / len(opports) if opports else 0
weighted_forecast = total_amount * (avg_probability / 100)
forecast[stage.name] = {
"count": len(opports),
"total_amount": total_amount,
"avg_probability": avg_probability,
"weighted_forecast": weighted_forecast
}
# Example output:
# {
# "Qualification": {"count": 15, "total_amount": 500000, "weighted_forecast": 200000},
# "Proposal": {"count": 8, "total_amount": 400000, "weighted_forecast": 260000},
# "Negotiation": {"count": 5, "total_amount": 300000, "weighted_forecast": 240000}
# }
Use Case 3: Opportunity Health Dashboard¶
# Identify at-risk opportunities needing attention
at_risk = []
for opport in get_model("sale.opportunity").search_browse([["state", "=", "open"]]):
risk_factors = []
# No activity in 14+ days
if opport.last_email_days and opport.last_email_days > 14:
risk_factors.append(f"No contact in {opport.last_email_days} days")
# Open for 90+ days with low probability
if opport.age_days > 90 and (opport.probability or 0) < 50:
risk_factors.append(f"Stale ({opport.age_days} days old, {opport.probability}% probability)")
# Close date approaching with no quotation
if opport.expected_close_date:
days_to_close = (datetime.strptime(opport.expected_close_date, "%Y-%m-%d") - datetime.today()).days
if days_to_close < 30 and not opport.quotations:
risk_factors.append(f"Closes in {days_to_close} days - no quotation")
if risk_factors:
at_risk.append({
"opportunity": opport.number,
"owner": opport.user_id.name,
"amount": opport.amount,
"risks": risk_factors
})
# Send summary to sales managers
if at_risk:
print(f"⚠️ {len(at_risk)} at-risk opportunities identified")
Use Case 4: Automated Reminder Workflow¶
# Schedule reminders for different scenarios
# 1. Set reminder 3 days before expected close
opport = get_model("sale.opportunity").browse(opport_id)
if opport.expected_close_date:
close_date = datetime.strptime(opport.expected_close_date, "%Y-%m-%d")
remind_date = close_date - timedelta(days=3)
get_model("sale.opportunity").write([opport_id], {
"remind_date": remind_date.strftime("%Y-%m-%d")
})
# Notification automatically created
# 2. Batch reminder for stale opportunities (run as scheduled job)
result = get_model("sale.opportunity").send_reminders(
min_days=7,
template="Opportunity Follow-up",
company="ACME",
to_addrs="sales-managers@company.com"
)
print(result) # "12 opportunities reminded"
Use Case 5: Competitor Analysis Report¶
# Analyze competitors across all opportunities
competitor_stats = {}
for opport in get_model("sale.opportunity").search_browse([
["state", "in", ["open", "won", "lost"]]
]):
for comp in opport.competitors:
compet_name = comp.compet_id.name
if compet_name not in competitor_stats:
competitor_stats[compet_name] = {
"total_opports": 0,
"won": 0,
"lost": 0,
"open": 0,
"total_value": 0
}
competitor_stats[compet_name]["total_opports"] += 1
competitor_stats[compet_name][opport.state] += 1
competitor_stats[compet_name]["total_value"] += opport.amount or 0
# Calculate win rates
for compet, stats in competitor_stats.items():
closed = stats["won"] + stats["lost"]
stats["win_rate"] = (stats["won"] / closed * 100) if closed > 0 else 0
print(f"{compet}: {stats['win_rate']:.1f}% win rate")
Best Practices¶
1. Always Set Probability Based on Stage¶
# Bad: Generic probability not aligned with stage
get_model("sale.opportunity").create({
"name": "Deal",
"contact_id": 123,
"stage_id": 5, # "Negotiation" stage
"probability": 50 # ❌ Too low for negotiation stage
})
# Good: Probability matches stage expectations
get_model("sale.opportunity").create({
"name": "Enterprise License Deal",
"contact_id": 123,
"stage_id": 5, # "Negotiation"
"probability": 75, # ✅ Realistic for this stage
"amount": 100000,
"expected_close_date": "2026-03-31",
"next_step": "Send final contract for review"
})
2. Track Competitors Early¶
Track competitors as soon as they're identified, not just when losing deals.
# Create opportunity with known competitor
opport_id = get_model("sale.opportunity").create({
"name": "Cloud Migration Project",
"contact_id": 456,
"amount": 200000,
"competitors": [
("create", {
"compet_id": 3,
"strengths": "Incumbent vendor, established relationship",
"weaknesses": "Higher pricing, limited cloud expertise"
})
]
})
# Competitor intelligence helps with:
# - Positioning strategy
# - Pricing decisions
# - Feature prioritization
# - Win/loss analysis
3. Maintain Activity Trail¶
Always log activities to maintain engagement history.
# After every significant interaction
get_model("sale.activ").create({
"type": "call",
"subject": "Technical architecture discussion",
"date": "2026-01-15",
"related_id": f"sale.opportunity,{opport_id}",
"user_id": user_id,
"description": "Discussed integration requirements with CTO. Next: provide technical proposal.",
"due_date": "2026-01-22",
"state": "done"
})
# Benefits:
# - Complete audit trail
# - Easy handoffs between team members
# - Accurate last_email_days calculation
# - Better pipeline hygiene
4. Use Reason Codes for Lost/Canceled¶
Always document why opportunities don't close.
# Capture loss intelligence
get_model("sale.opportunity").write([opport_id], {
"lost_reason_id": 5, # "Lost to competitor - price"
"description": "Lost to Competitor X - they offered 15% lower pricing. "
"Customer acknowledged our superior features but budget constrained."
})
get_model("sale.opportunity").lost([opport_id])
# This data enables:
# - Win/loss analysis
# - Pricing strategy refinement
# - Product positioning improvement
# - Competitive intelligence
Performance Tips¶
1. Use search_browse for Iteration¶
# Bad: Two database round trips
opport_ids = get_model("sale.opportunity").search([["state", "=", "open"]])
opports = get_model("sale.opportunity").browse(opport_ids)
# Good: Single query with browse
opports = get_model("sale.opportunity").search_browse([["state", "=", "open"]])
2. Leverage SQL Aggregation Fields¶
# Bad: Python aggregation (slow for large datasets)
opport_ids = get_model("sale.opportunity").search([["state", "=", "won"]])
total = sum(get_model("sale.opportunity").browse(id).amount for id in opport_ids)
# Good: Use SQL aggregation
result = get_model("sale.opportunity").search_read(
[["state", "=", "won"]],
["agg_amount"]
)
total = result[0]["agg_amount"] if result else 0
3. Batch Operations¶
# Bad: Individual updates
for opport_id in opport_ids:
get_model("sale.opportunity").write([opport_id], {"stage_id": 3})
# Good: Batch update
get_model("sale.opportunity").write(opport_ids, {"stage_id": 3})
Troubleshooting¶
"Missing lost reason"¶
Cause: Attempting to mark opportunity as lost without setting lost_reason_id
Solution: Set the reason code before calling lost() method
get_model("sale.opportunity").write([opport_id], {
"lost_reason_id": reason_id
})
get_model("sale.opportunity").lost([opport_id])
"Missing cancel reason"¶
Cause: Attempting to cancel opportunity without setting cancel_reason_id
Solution: Set the reason code before calling cancel() method
get_model("sale.opportunity").write([opport_id], {
"cancel_reason_id": reason_id
})
get_model("sale.opportunity").cancel([opport_id])
Expected Revenue Not Calculating¶
Cause: Missing amount or probability field Solution: Ensure both fields are set
get_model("sale.opportunity").write([opport_id], {
"amount": 50000,
"probability": 60
})
# Expected Revenue = 50000 × 0.60 = 30000
Configuration Settings¶
Required Settings¶
| Setting | Location | Description |
|---|---|---|
| Opportunity Sequence | settings → Sequences |
Sequence for auto-numbering (type: "sale_opport") |
| Sales Stages | sale.stage |
Define pipeline stages with probabilities |
| Reason Codes | reason.code |
Lost/cancel reasons (type: "lost_sale_opport", "cancel_sale_opport") |
Optional Settings¶
| Setting | Default | Description |
|---|---|---|
| Email Template | settings.sale_activity_email_template_id |
Template for activity reminders |
| Default Owner | Current user | User assigned to new opportunities |
| Default Probability | None | Initial probability percentage |
Integration Points¶
Internal Modules¶
- Lead Management: Opportunities created from converted leads via
sale.lead - Quotation Management: Generate quotations via
copy_to_quotation()→sale.quot - Contact Management: All opportunities link to
contactrecords - Activity Tracking: Track tasks, calls, meetings via
sale.activ - Email Integration: Email communications tracked via
email.message - Campaign Management: Track campaign ROI via
mkt.campaignlinkage - Competitor Intelligence: Track competitors via
opport.compet - Document Management: Attach proposals, contracts via
document - Workflow Automation: Automated reminders and notifications
Version History¶
Last Updated: 2026-01-05 Model Version: sale_opportunity.py Framework: Netforce
Additional Resources¶
- Sales Lead Documentation:
sale.lead - Quotation Documentation:
sale.quot - Sales Stage Configuration:
sale.stage - Activity Tracking:
sale.activ - Competitor Tracking:
opport.compet - Convert Opportunity Wizard:
opport.to.quot
Support & Feedback¶
For issues or questions about this module: 1. Check related model documentation (quotations, leads, activities) 2. Review system logs for detailed error messages 3. Verify sales stages and reason codes are configured 4. Ensure sequence is set up for opportunity numbering 5. Test in development environment first
This documentation is generated for developer onboarding and reference purposes.