Skip to content

Opportunity to Quotation Wizard Documentation

Overview

The Opportunity to Quotation module (opport.to.quot) is a transient wizard model that facilitates converting sales opportunities into formal quotations. This wizard provides a user-friendly interface for selecting optional quotation templates and streamlines the opportunity-to-quotation conversion process.


Model Information

Model Name: opport.to.quot Display Name: (Not specified - wizard model) Model Type: Transient/Wizard (not persisted in database)

Features

  • No audit logging (transient model)
  • No multi-company support (inherits from opportunity)
  • Wizard pattern for user-guided process
  • Optional template selection for pre-configured quotations

Understanding Wizard Models

What is a Transient Model?

A transient model (wizard) is a temporary model used for user interactions that don't need permanent storage:

Characteristics: - Records exist only during user session - Used for multi-step processes or selections - Automatically cleaned up after completion - Provides guided user experience

Common Uses: - Data import wizards - Record conversion tools - Batch operation dialogs - Report parameter selection

The Conversion Pattern

This wizard implements a common pattern seen throughout Netforce:

Source Record (Opportunity)
Wizard (opport.to.quot) - User selects options
Target Record (Quotation) - Created with configuration

Similar wizards: - convert.lead - Lead to Opportunity - import.wizard - Data import - report.wizard - Report generation


Key Fields Reference

Wizard Fields

Field Type Required Description
opport_id Many2One No Source opportunity to convert
template_id Many2One No Optional quotation template to use

Field Details

opport_id: - Link to the sales opportunity being converted - Defaults from context when wizard is opened from opportunity form - Provides source data for quotation creation

template_id: - Reference to pre-configured quotation template - Condition: [["is_template", "=", True]] - only shows template quotations - When selected, copies template structure instead of creating basic quotation - Optional - if not selected, creates simple quotation from opportunity data


API Methods

1. Create Wizard Instance

Method: create(vals, context)

Creates wizard instance (automatically called when opening wizard).

Default Behavior:

_defaults = {
    "opport_id": lambda self, ctx: ctx.get("refer_id")
}

When wizard is opened from an opportunity form, the opportunity ID is automatically populated from the refer_id context variable.

Example:

# Wizard automatically created with opportunity context
# User opens wizard from opportunity form
# System creates wizard with opport_id pre-filled


2. Create Quotation

Method: create_quot(ids, context)

Executes the conversion from opportunity to quotation.

Parameters: - ids (list): Wizard record ID (single ID) - context (dict): Execution context

Behavior: 1. Retrieves wizard record 2. Gets associated opportunity 3. Calls opportunity's copy_to_quotation() method 4. Passes template_id if specified 5. Returns navigation to new quotation

Returns: dict - Navigation action to newly created quotation

{
    "next": {
        "name": "quot",
        "mode": "form",
        "active_id": quot_id
    },
    "flash": "Quotation QUOT-0123 created from opportunity"
}

Example:

# Called when user clicks "Create Quotation" button
wizard = get_model("opport.to.quot").browse(wizard_id)
result = wizard.create_quot([wizard_id])

# System navigates to new quotation form automatically
# Flash message confirms creation


Conversion Logic

Without Template

When no template is selected, the opportunity's copy_to_quotation() creates a basic quotation:

Field Mapping:

Quotation Fields           Opportunity Fields
-----------------            ------------------
opport_id                  opportunity.id
ref                        opportunity.number
contact_id                 opportunity.contact_id
user_id                    opportunity.user_id

# If product specified:
lines[0].product_id        opportunity.product_id
lines[0].qty               opportunity.qty (or 1)
lines[0].unit_price        opportunity.amount or product.sale_price
lines[0].uom_id            product.uom_id
lines[0].description       product name


With Template

When template is selected, quotation is created by copying template:

Process: 1. Copy entire quotation template (preserving all lines, terms, etc.) 2. Update header fields with opportunity data: - opport_id ← opportunity ID - ref ← opportunity number - contact_id ← opportunity contact - user_id ← opportunity owner

Benefits: - Pre-configured product lines - Standard terms and conditions - Consistent pricing and discounts - Company-specific quotation formats - Faster quotation generation


Model Relationship Description
sale.opportunity Many2One Source opportunity being converted
sale.quot Via method Target quotation created
sale.quot (template) Many2One Optional template for structured quotations

Common Use Cases

Use Case 1: Simple Opportunity Conversion

# User workflow:
# 1. Open opportunity form
# 2. Click "Convert to Quotation" button
# 3. Wizard opens with opportunity pre-selected
# 4. User leaves template blank (simple quotation)
# 5. User clicks "Create"

# Behind the scenes:
opport_id = 123

# System creates wizard with context
wizard_id = get_model("opport.to.quot").create(
    {},
    context={"refer_id": opport_id}
)

# Wizard has opport_id = 123 from default

# User clicks Create button
result = get_model("opport.to.quot").create_quot([wizard_id])

# Creates basic quotation:
# - Header from opportunity
# - Single line if product specified
# - Links back to opportunity

# User is navigated to quotation form

Use Case 2: Template-Based Quotation

# Scenario: Standard product quotation with terms
# Company has quotation templates for common offerings

# User workflow:
# 1. Open opportunity for "Enterprise License" deal
# 2. Click "Convert to Quotation"
# 3. Wizard opens
# 4. User selects "Enterprise License Template"
# 5. User clicks "Create"

# Template quotation contains:
# - Standard product bundle lines
# - Volume-based pricing tiers
# - Payment terms (30% upfront, 70% on delivery)
# - Warranty and support terms
# - Company-specific T&Cs

# Behind the scenes:
wizard_id = get_model("opport.to.quot").create({
    "template_id": 15  # "Enterprise License Template"
}, context={"refer_id": opport_id})

result = get_model("opport.to.quot").create_quot([wizard_id])

# Creates quotation by:
# 1. Copying template quotation (all lines, terms)
# 2. Updating header with opportunity data
# 3. Linking to opportunity

# Result: Professional quotation ready to send
# Only needs customer-specific customization

Use Case 3: Multiple Quotations from One Opportunity

# Scenario: Provide different options to customer

# Create first quotation - Basic package
wizard_id = get_model("opport.to.quot").create({
    "template_id": 10  # "Basic Package"
}, context={"refer_id": opport_id})

result1 = get_model("opport.to.quot").create_quot([wizard_id])
basic_quot_id = result1["next"]["active_id"]

# Create second quotation - Premium package
wizard_id = get_model("opport.to.quot").create({
    "template_id": 11  # "Premium Package"
}, context={"refer_id": opport_id})

result2 = get_model("opport.to.quot").create_quot([wizard_id])
premium_quot_id = result2["next"]["active_id"]

# Create third quotation - Enterprise package
wizard_id = get_model("opport.to.quot").create({
    "template_id": 12  # "Enterprise Package"
}, context={"refer_id": opport_id})

result3 = get_model("opport.to.quot").create_quot([wizard_id])
enterprise_quot_id = result3["next"]["active_id"]

# All three quotations linked to same opportunity
# Customer can compare options
# Track which quotation is accepted

Use Case 4: Programmatic Conversion

# Automated quotation generation (e.g., from workflow)

def auto_convert_high_value_opportunities():
    """
    Automatically create quotations for opportunities
    over $100K that reach "Proposal" stage
    """

    # Find qualifying opportunities
    opports = get_model("sale.opportunity").search_browse([
        ["stage_id.name", "=", "Proposal"],
        ["amount", ">", 100000],
        ["quotations", "=", []]  # No quotations yet
    ])

    for opport in opports:
        # Determine appropriate template
        if opport.amount > 500000:
            template_name = "Enterprise Template"
        elif opport.amount > 250000:
            template_name = "Corporate Template"
        else:
            template_name = "Standard Template"

        # Find template
        templates = get_model("sale.quot").search([
            ["name", "=", template_name],
            ["is_template", "=", True]
        ])

        if templates:
            template_id = templates[0]
        else:
            template_id = None

        # Create quotation via wizard
        wizard_id = get_model("opport.to.quot").create({
            "template_id": template_id
        }, context={"refer_id": opport.id})

        result = get_model("opport.to.quot").create_quot([wizard_id])

        print(f"Created quotation for opportunity {opport.number}: "
              f"{result['flash']}")

Best Practices

1. Use Templates for Standardization

# Bad: Always creating blank quotations
# Requires manual entry of standard terms, pricing, etc.

# Good: Create templates for common scenarios
templates_to_create = [
    {
        "name": "Software License - Small Business",
        "is_template": True,
        "lines": [
            # Standard product bundle
            # Pre-configured pricing
            # Volume discounts
        ],
        "payment_terms": "Net 30",
        "notes": "Standard T&Cs for SMB customers"
    },
    {
        "name": "Software License - Enterprise",
        "is_template": True,
        "lines": [
            # Enterprise product bundle
            # Enterprise pricing
            # Custom integration hours
        ],
        "payment_terms": "30% upfront, 70% on delivery",
        "notes": "Enterprise T&Cs with SLA guarantees"
    }
]

# Sales reps use appropriate template
# Ensures consistency and saves time

2. Name Templates Clearly

# Bad: Unclear template names
"Template 1"
"Template A"
"New Template"

# Good: Descriptive template names
"Enterprise Software License - Standard Terms"
"SMB Package - Monthly Subscription"
"Professional Services - Hourly Rate"
"Product Bundle - Annual Contract"

# Benefits:
# - Easy to find right template
# - Self-documenting
# - Reduces errors

# The wizard automatically links quotation to opportunity
# This enables tracking:

opport = get_model("sale.opportunity").browse(opport_id)

# View all quotations for this opportunity
for quot in opport.quotations:
    print(f"Quotation: {quot.number}")
    print(f"  Amount: ${quot.amount_total}")
    print(f"  State: {quot.state}")
    print(f"  Validity: {quot.expiration_date}")

# Track conversion rate
opports_with_quots = get_model("sale.opportunity").search_count([
    ["quotations", "!=", []]
])
total_opports = get_model("sale.opportunity").search_count([])
conversion_rate = opports_with_quots / total_opports * 100

print(f"Quotation conversion rate: {conversion_rate:.1f}%")

4. Update Opportunity After Quotation Sent

# After creating and sending quotation
result = get_model("opport.to.quot").create_quot([wizard_id])
quot_id = result["next"]["active_id"]

# Update opportunity to reflect progress
get_model("sale.opportunity").write([opport_id], {
    "stage_id": next_stage_id,  # Move to "Proposal Sent" stage
    "probability": 70,           # Increase probability
    "next_step": "Follow up on quotation in 3 days"
})

# Log activity
get_model("sale.activ").create({
    "type": "task",
    "subject": "Follow up on quotation",
    "date": (datetime.today() + timedelta(days=3)).strftime("%Y-%m-%d"),
    "related_id": f"sale.opportunity,{opport_id}",
    "user_id": user_id,
    "description": f"Follow up on quotation {quot.number}"
})

Performance Tips

1. Template Caching

# Bad: Searching for template every time
wizard_id = get_model("opport.to.quot").create({
    "template_id": get_model("sale.quot").search([
        ["name", "=", "Standard Template"],
        ["is_template", "=", True]
    ])[0]
})

# Good: Cache template IDs
class QuotationConverter:
    _template_cache = {}

    @classmethod
    def get_template_id(cls, template_name):
        if template_name not in cls._template_cache:
            templates = get_model("sale.quot").search([
                ["name", "=", template_name],
                ["is_template", "=", True]
            ])
            cls._template_cache[template_name] = templates[0] if templates else None
        return cls._template_cache[template_name]

# Use cached lookup
template_id = QuotationConverter.get_template_id("Standard Template")

2. Batch Conversions

# When converting multiple opportunities
opport_ids = [123, 124, 125]

for opport_id in opport_ids:
    wizard_id = get_model("opport.to.quot").create(
        {"template_id": template_id},
        context={"refer_id": opport_id}
    )
    get_model("opport.to.quot").create_quot([wizard_id])
    # Wizard records automatically cleaned up

Troubleshooting

Wizard Not Pre-Filling Opportunity

Cause: Missing refer_id in context when opening wizard Solution: Ensure wizard is called with proper context

# From opportunity form button:
{
    "action": "opport_to_quot",
    "refer_id": "active_id"  # This passes opportunity ID
}

# Programmatically:
wizard_id = get_model("opport.to.quot").create(
    {},
    context={"refer_id": opport_id}
)

Template Not Available in Selection

Cause: Quotation not marked as template Solution: Ensure quotation has is_template = True

# Verify template flag
quot = get_model("sale.quot").browse(quot_id)
if not quot.is_template:
    quot.write({"is_template": True})

# Or create proper template
template_id = get_model("sale.quot").create({
    "name": "Standard Template",
    "is_template": True,
    "contact_id": demo_contact_id,  # Dummy contact
    "lines": [
        # Template lines
    ]
})

Quotation Not Linking to Opportunity

Cause: Error in conversion process or missing opportunity reference Solution: Verify quotation has opport_id field set

# Check quotation link
quot = get_model("sale.quot").browse(quot_id)
if not quot.opport_id:
    # Manually link if needed
    quot.write({"opport_id": opport_id})

# Verify from opportunity side
opport = get_model("sale.opportunity").browse(opport_id)
print(f"Linked quotations: {len(opport.quotations)}")

Configuration Settings

Required Settings

Setting Location Description
Quotation Templates sale.quot Create template quotations with is_template = True

Optional Settings

Setting Default Description
Default Template None Can be configured per opportunity type/stage
Template Categories None Organize templates by product line or customer segment

Integration Points

Internal Modules

  • Opportunity Management: Source data from sale.opportunity
  • Quotation Management: Creates records in sale.quot
  • Template System: Uses quotation templates for standardization
  • UI Framework: Wizard pattern for guided user experience

Version History

Last Updated: 2026-01-05 Model Version: opport_to_quot.py Framework: Netforce


Additional Resources

  • Opportunity Documentation: sale.opportunity
  • Quotation Documentation: sale.quot
  • Quotation Template Configuration
  • Lead Conversion Wizard: convert.lead (similar pattern)

Support & Feedback

For issues or questions about this module: 1. Verify quotation templates are properly configured 2. Check that wizard is called with correct context 3. Ensure opportunity has required fields populated 4. Review quotation creation for proper field mapping 5. Test template-based and non-template conversions


This documentation is generated for developer onboarding and reference purposes.