Skip to content

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 number for 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:

Lead → Qualification → Needs Analysis → Proposal → Negotiation → Closed Won
                                                                 Closed Lost

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:

context = {
    "template_id": 5              # Optional: quotation template to use
}

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:

get_model("sale.opportunity").won([opport_id])


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:

get_model("sale.opportunity").pause([opport_id])


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:

# Customer changed mind - reopen lost opportunity
get_model("sale.opportunity").reopen([opport_id])


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

{
    "next": {
        "name": "opport_edit",
        "active_id": new_id
    },
    "flash": "Opportunity copied"
}

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

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 contact records
  • Activity Tracking: Track tasks, calls, meetings via sale.activ
  • Email Integration: Email communications tracked via email.message
  • Campaign Management: Track campaign ROI via mkt.campaign linkage
  • 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.