Skip to content

Sale Quotation Documentation

Overview

The Quotation module (sale.quot) manages sales quotations throughout their lifecycle from draft creation to conversion into sales orders. It provides a comprehensive approval workflow, expiration date management, integration with opportunities, and supports quote-to-order conversion. This module is essential for tracking potential sales and managing the quotation approval process before converting quotes into confirmed orders.


Model Information

Model Name: sale.quot Display Name: Quotation Key Fields: ["number"]

Features

  • ✅ Audit logging enabled (_audit_log)
  • ✅ Multi-company support (company_id)
  • ✅ Full-text content search (_content_search)
  • ✅ Unique key constraint per quotation number

Understanding Key Fields

What are Key Fields?

In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record. Think of them as a business key that ensures data integrity across the system.

For the sale.quot model, the key fields are:

_key = ["number"]

This means the quotation number must be unique: - number - Unique quotation identifier (e.g., "QT-2024-001")

Why Key Fields Matter

Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique combinations:

# Examples of valid combinations:
"QT-2024-001"   Valid
"QT-2024-002"   Valid
"QT-2024-003"   Valid

# This would fail - duplicate key:
"QT-2024-001"   ERROR: Quotation number already exists!

Database Implementation

The key fields are enforced at the database level using a unique constraint:

_sql_constraints = [
    ("sale_quot_number_unique",
     "unique (number)",
     "Quotation number must be unique")
]

This translates to:

CREATE UNIQUE INDEX sale_quot_number_unique
    ON sale_quot (number);

State Workflow

draft → waiting_approval → approved → won
                              ↓       ↓
                            lost    revised
                           voided
State Description
draft Initial state when quotation is created, can be edited freely
waiting_approval Submitted for approval, awaiting management review
approved Approved and ready to be sent to customer or converted to order
won Customer accepted the quotation, usually converted to sale order
lost Customer declined the quotation
revised Original quotation was revised and a new version created
voided Quotation has been cancelled/voided

Key Fields Reference

Header Fields

Field Type Required Description
number Char Unique quotation number (auto-generated from sequence)
ref Char External reference number
contact_id Many2One Customer contact (contact model)
contact_person_id Many2One Specific contact person at customer
date Date Quotation date
exp_date Date Expiration date - quote is valid until this date
state Selection Current workflow state (computed and stored)
currency_id Many2One Currency for quotation amounts
tax_type Selection Tax calculation type: tax_ex, tax_in, or no_tax

Opportunity & Sales Process Fields

Field Type Description
opport_id Many2One Related sales opportunity
user_id Many2One Salesperson/owner of quotation
seller_id Many2One Seller entity
seller_contact_id Many2One Seller contact person
sale_categ_id Many2One Sales category for reporting
lost_sale_code_id Many2One Reason code if quotation is lost

Amount Fields (Computed)

Field Type Description
amount_subtotal Decimal Subtotal before tax (stored, computed)
amount_tax Decimal Total tax amount (stored, computed)
amount_total Decimal Total including tax (stored, computed)
amount_discount Decimal Total discount amount (computed)
amount_before_discount Decimal Amount before any discounts applied
amount_after_discount Decimal Amount after discount and before extra charges
amount_total_words Char Total amount in words (for printing)
qty_total Decimal Total quantity of all line items

Terms & Conditions Fields

Field Type Description
pay_term_id Many2One Payment terms (replaces deprecated payment_terms)
payment_terms Text Deprecated - use pay_term_id instead
ship_term_id Many2One Shipping/delivery terms
validity_id Many2One Validity period template
other_info Text Additional information and notes

Pricing & Discount Fields

Field Type Description
price_list_id Many2One Price list to use for product pricing
discount Decimal Overall discount percentage
amount_after_discount Decimal Target amount after discount
extra_amount Decimal Extra charges to add
extra_discount Decimal Extra discount amount
extra_product_id Many2One Product to use for extra amount line
extra_discount_product_id Many2One Product to use for extra discount line

Cost & Profit Analysis Fields

Field Type Description
est_costs One2Many Estimated costs (deprecated - quot.cost model)
cost_amount Decimal Total estimated cost (computed)
profit_amount Decimal Estimated profit (computed)
margin_percent Decimal Profit margin percentage (computed)

Address Fields

Field Type Description
bill_address_id Many2One Billing address
ship_address_id Many2One Shipping address

Relationship Fields

Field Type Description
lines One2Many Quotation line items (sale.quot.line)
sales One2Many Related sales orders created from this quotation
comments One2Many Comments and notes (message model)
activities One2Many Related sales activities
documents One2Many Attached documents
emails One2Many Email communications related to quotation
currency_rates One2Many Custom currency exchange rates

Configuration & System Fields

Field Type Description
uuid Char Unique identifier for external access/links
company_id Many2One Company (multi-company support)
sequence_id Many2One Number sequence used for generation
job_template_id Many2One Service order template for conversion
is_template Boolean Mark quotation as reusable template
related_id Reference Generic relation to other records (e.g., issues)
project_id Many2One Related project
track_id Many2One Accounting tracking code
inquiry_date Date Date of customer inquiry

Reporting & Aggregation Fields

Field Type Description
agg_amount_total Decimal Sum aggregation of total amounts
agg_amount_subtotal Decimal Sum aggregation of subtotals
year Char Year extracted from date (SQL function)
quarter Char Quarter extracted from date (SQL function)
month Char Month extracted from date (SQL function)
week Char Week extracted from date (SQL function)
groups Json Grouped line items by category (computed)
tax_details Json Tax breakdown details (computed)

API Methods

1. Create Quotation

Method: create(vals, context)

Creates a new quotation record with automatic number generation.

Parameters:

vals = {
    "contact_id": 123,                    # Required: Customer contact
    "date": "2024-01-15",                 # Required: Quotation date
    "currency_id": 1,                     # Required: Currency
    "tax_type": "tax_ex",                 # Required: Tax type
    "exp_date": "2024-02-15",             # Optional: Expiration date
    "opport_id": 456,                     # Optional: Related opportunity
    "price_list_id": 10,                  # Optional: Price list
    "pay_term_id": 5,                     # Optional: Payment terms
    "lines": [                            # Quotation lines
        ("create", {
            "product_id": 100,
            "description": "Product Name",
            "qty": 5,
            "uom_id": 1,
            "unit_price": 100.00,
            "tax_id": 1
        }),
        ("create", {
            "product_id": 101,
            "description": "Another Product",
            "qty": 2,
            "uom_id": 1,
            "unit_price": 250.00,
            "tax_id": 1
        })
    ]
}

context = {
    "company_id": 1                       # Multi-company context
}

Returns: int - New quotation ID

Example:

# Create a quotation for customer with 2 products
quot_id = get_model("sale.quot").create({
    "contact_id": 123,
    "date": "2024-01-15",
    "exp_date": "2024-02-15",
    "currency_id": 1,
    "tax_type": "tax_ex",
    "opport_id": 456,
    "lines": [
        ("create", {
            "product_id": 100,
            "description": "Laptop Computer",
            "qty": 5,
            "uom_id": 1,
            "unit_price": 1200.00,
            "tax_id": 1
        })
    ]
})

Behavior: - Automatically generates quotation number from sequence - Generates UUID for external access - Sets default currency from settings if not provided - Sets user_id to current user - Calls function_store to compute amount fields


2. Submit for Approval

Method: submit_for_approval(ids, context)

Transitions quotation from draft to waiting_approval state.

Parameters: - ids (list): Quotation IDs to submit

Behavior: - Validates current state is "draft" - Updates state to "waiting_approval" - Fires workflow trigger "submit_for_approval"

Example:

get_model("sale.quot").submit_for_approval([quot_id])


3. Approve Quotation

Method: approve(ids, context)

Approves quotation after validation checks.

Parameters: - ids (list): Quotation IDs to approve

Behavior: - Checks permission "approve_quotation" if settings.approve_quot is enabled - Validates state is "draft" or "waiting_approval" - Validates all products meet minimum sale prices - Updates state to "approved"

Example:

get_model("sale.quot").approve([quot_id])

Permission Requirements: - approve_quotation: Required if approval workflow is enabled in settings

Validations: - Each product's unit_price must be >= product.sale_min_price - Raises exception if minimum price validation fails


4. Convert to Sales Order

Method: copy_to_sale_order(ids, context)

Converts approved quotation into one or more sales orders.

Parameters: - ids (list): Quotation ID to convert (only first ID used)

Context Options:

context = {
    "company_id": 1                       # Company context
}

Behavior: - Validates no sales order already exists for this quotation - Splits lines by product type if settings.sale_split_service is enabled - Creates separate sales orders for different product types (service vs non-service) - Copies all quotation lines to sales order lines - Adds extra_amount and extra_discount as separate lines if present - Copies custom currency rates - Sets quot_id reference on created sales orders - Links sale order back to quotation via related_id

Returns:

{
    "next": {
        "name": "sale",
        "mode": "form",
        "active_id": <sale_order_id>
    },
    "flash": "N sales orders created"
}

Example:

# Convert quotation to sales order
result = get_model("sale.quot").copy_to_sale_order([quot_id])
# Redirects to newly created sales order

Field Mapping:

# Quotation → Sales Order
number  ref
contact_id  contact_id
currency_id  currency_id
tax_type  tax_type
user_id  user_id
other_info  other_info
pay_term_id  pay_term_id
price_list_id  price_list_id
job_template_id  job_template_id
seller_id  seller_id

# Line fields copied:
product_id, description, qty, uom_id, unit_price,
cost_price, discount, discount_fixed, tax_id,
sequence, type, notes


5. Copy Quotation

Method: copy(ids, context)

Creates a duplicate copy of an existing quotation.

Parameters: - ids (list): Quotation ID to copy

Context Options:

context = {
    "revise": True                        # If True, treats as revision
}

Behavior: - Generates new quotation number - Copies header fields: contact_id, currency_id, tax_type, payment_terms, other_info, exp_date, opport_id - If revise=True in context, also copies: ref, sale_categ_id, project_id, pay_term_id, contact_person_id - Copies all quotation lines with their details - Does NOT copy: state (reset to draft), sales orders, comments, activities

Returns:

{
    "new_id": <new_quot_id>,
    "next": {
        "name": "quot",
        "mode": "form",
        "active_id": <new_quot_id>
    },
    "flash": "Quotation QT-XXX copied from QT-YYY"
}

Example:

# Regular copy
result = get_model("sale.quot").copy([quot_id])

# Copy as revision
result = get_model("sale.quot").copy([quot_id], context={"revise": True})


6. Revise Quotation

Method: revise(ids, context)

Creates a revised version of the quotation and marks original as "revised".

Parameters: - ids (list): Original quotation ID to revise

Behavior: - Calls copy() with revise=True context - Sets original quotation state to "revised" - Creates new quotation with incremented number - Preserves additional fields: ref, sale_categ_id, project_id, pay_term_id

Returns: Same as copy() method

Example:

# Create revision of quotation
result = get_model("sale.quot").revise([quot_id])
# Original quot_id is now in "revised" state
# New quotation created with new number


7. State Transition Methods

7.1 Mark as Won

Method: do_won(ids, context)

Marks approved quotation as won (customer accepted).

Parameters: - ids (list): Quotation IDs to mark as won

Behavior: - Validates state is "approved" - Updates state to "won"

Example:

get_model("sale.quot").do_won([quot_id])


7.2 Mark as Lost

Method: do_lost(ids, context)

Marks approved quotation as lost (customer declined).

Parameters: - ids (list): Quotation IDs to mark as lost

Behavior: - Validates state is "approved" - Updates state to "lost" - Should set lost_sale_code_id for tracking reasons

Example:

get_model("sale.quot").do_lost([quot_id])


7.3 Reopen Quotation

Method: do_reopen(ids, context)

Reopens won or lost quotation back to approved state.

Parameters: - ids (list): Quotation IDs to reopen

Behavior: - Validates state is "won" or "lost" - Updates state back to "approved"

Example:

get_model("sale.quot").do_reopen([quot_id])


7.4 Return to Draft

Method: to_draft(ids, context)

Returns quotation to draft state for editing.

Parameters: - ids (list): Quotation IDs to return to draft

Example:

get_model("sale.quot").to_draft([quot_id])


7.5 Void Quotation

Method: void(ids, context)

Voids/cancels the quotation.

Parameters: - ids (list): Quotation IDs to void

Example:

get_model("sale.quot").void([quot_id])


8. Cost & Profit Methods

8.1 Create Estimated Costs

Method: create_est_costs(ids, context)

Automatically generates estimated cost records based on quotation lines.

Parameters: - ids (list): Quotation ID

Behavior: - Deletes existing product-based cost estimates - Creates quot.cost record for each line with a product - Only includes products with purchase_price set - Skips bundle products - Populates cost details from product: purchase_price, landed_cost, supplier, currency

Example:

get_model("sale.quot").create_est_costs([quot_id])


8.2 Apply Discount

Method: apply_discount(ids, context)

Applies the quotation-level discount percentage to all lines.

Parameters: - ids (list): Quotation ID

Behavior: - Applies discount percentage from quot.discount field to each line - Only applies to lines with unit_price set - Updates line.discount field

Returns:

{
    "alert": "Discount applied successfully"
}

Example:

# Set discount percentage
get_model("sale.quot").write([quot_id], {"discount": 10})
# Apply to all lines
get_model("sale.quot").apply_discount([quot_id])


8.3 Calculate Discount

Method: calc_discount(ids, context)

Reverse-calculates discount percentage needed to reach target amount.

Parameters: - ids (list): Quotation ID

Behavior: - Uses amount_after_discount field as target - Resets discount to 0 and recalculates - Calculates discount percentage needed - Applies discount to lines - Calculates extra_discount for rounding differences

Returns:

{
    "alert": "Old amount: X, new amount: Y, discount amount: Z => discount percent: P%, extra discount: E"
}

Example:

# Set target amount after discount
get_model("sale.quot").write([quot_id], {"amount_after_discount": 9500})
# Calculate and apply discount
get_model("sale.quot").calc_discount([quot_id])


9. Utility Methods

Method: view_link(ids, context)

Generates external view URL for quotation.

Returns:

{
    "next": {
        "type": "url",
        "url": "/view_quot?dbname=<db>&uuid=<uuid>"
    }
}

Example:

result = get_model("sale.quot").view_link([quot_id])
# Returns URL for customer to view quotation


9.2 Get Template Form

Method: get_template_quot_form(ids, context)

Determines which print template to use based on quotation content.

Returns: - "quot_form_disc" - if any lines have discounts - "quot_form" - standard template

Example:

template = get_model("sale.quot").get_template_quot_form([quot_id])


9.3 Merge Quotations

Method: merge_quotations(ids, context)

Merges multiple quotations into a single new quotation.

Parameters: - ids (list): Quotation IDs to merge (minimum 2)

Behavior: - Validates all quotations have same customer - Validates all quotations have same currency - Validates all quotations have same tax_type - Creates new quotation with combined lines - Renumbers sequences sequentially - Merges estimated costs

Returns:

{
    "next": {
        "name": "quot",
        "mode": "form",
        "active_id": <new_quot_id>
    },
    "flash": "Quotations merged"
}

Example:

# Merge 3 quotations
result = get_model("sale.quot").merge_quotations([quot_id1, quot_id2, quot_id3])


9.4 Get Relative Currency Rate

Method: get_relative_currency_rate(ids, currency_id)

Gets exchange rate for a specific currency from quotation's currency rates or system rates.

Parameters: - ids (list): Quotation ID - currency_id (int): Target currency ID

Returns: Decimal - Exchange rate

Example:

rate = get_model("sale.quot").get_relative_currency_rate([quot_id], currency_id=2)


9.5 Check Minimum Prices

Method: check_min_prices(ids, context)

Validates all products meet their minimum sale prices.

Behavior: - Checks each line's unit_price against product.sale_min_price - Raises exception if any price is below minimum

Example:

get_model("sale.quot").check_min_prices([quot_id])


UI Events (onchange methods)

onchange_product

Triggered when product is selected in a quotation line. Updates: - Description from product name/description - Quantity to 1 - UoM from product default - Unit price from price list or product sale price (with currency conversion) - Tax from product default sale tax - Cost price from product cost price - Recalculates amounts

Usage:

data = {
    "contact_id": 123,
    "currency_id": 1,
    "price_list_id": 10,
    "lines": [
        {
            "product_id": 100  # Triggers onchange
        }
    ]
}
result = get_model("sale.quot").onchange_product(
    context={
        "data": data,
        "path": "lines.0"
    }
)


onchange_qty

Triggered when quantity changes. Updates: - Unit price from price list if quantity-based pricing exists - Recalculates line amount - Updates quotation totals

Usage:

data = {
    "price_list_id": 10,
    "currency_id": 1,
    "lines": [
        {
            "product_id": 100,
            "qty": 10  # Triggers onchange
        }
    ]
}
result = get_model("sale.quot").onchange_qty(
    context={
        "data": data,
        "path": "lines.0"
    }
)


onchange_contact

Triggered when customer contact is selected. Updates: - Payment terms from contact default - Price list from contact default - Currency from contact or system default - Seller contact from contact settings - Contact person from contact default

Usage:

data = {
    "contact_id": 123  # Triggers onchange
}
result = get_model("sale.quot").onchange_contact(
    context={"data": data}
)


onchange_uom

Triggered when unit of measure changes. Updates: - Unit price adjusted by UoM ratio - Recalculates amounts

Usage:

data = {
    "lines": [
        {
            "product_id": 100,
            "uom_id": 2  # Triggers onchange
        }
    ]
}
result = get_model("sale.quot").onchange_uom(
    context={
        "data": data,
        "path": "lines.0"
    }
)


onchange_sequence

Triggered when sequence is manually selected. Updates: - Generates next number from sequence

Usage:

data = {
    "sequence_id": 5  # Triggers onchange
}
result = get_model("sale.quot").onchange_sequence(
    context={"data": data}
)


onchange_validity

Triggered when validity period is selected. Updates: - Expiration date based on quotation date + validity period days

Usage:

data = {
    "date": "2024-01-15",
    "validity_id": 3  # 30 days validity
}
result = get_model("sale.quot").onchange_validity(
    context={"data": data}
)
# Sets exp_date to "2024-02-14"


onchange_cost_product

Triggered when selecting product in estimated costs. Updates cost fields with product purchase information.

Usage:

data = {
    "est_costs": [
        {
            "product_id": 100  # Triggers onchange
        }
    ]
}
result = get_model("sale.quot").onchange_cost_product(
    context={
        "data": data,
        "path": "est_costs.0"
    }
)


Computed Fields Functions

get_state(ids, context)

Computes quotation state dynamically. If state is "approved", checks if any related sales orders are confirmed/done and automatically sets state to "won".

get_amount(ids, context)

Calculates amount_subtotal, amount_tax, amount_total, amount_discount, amount_before_discount, amount_after_discount2. Handles tax-inclusive and tax-exclusive calculations. Includes extra_amount and extra_discount in calculations.

get_qty_total(ids, context)

Sums total quantity across all quotation lines.

get_amount_total_words(ids, context)

Converts amount_total to words for printing on quotation forms. Includes currency cents if applicable.

get_profit(ids, context)

Calculates cost_amount, profit_amount, and margin_percent by comparing line amounts with cost prices.

get_opport_email(ids, context)

Returns the latest email from related opportunity if exists.

get_groups(ids, context)

Groups quotation lines by group type for hierarchical display.

get_tax_details(ids, context)

Breaks down tax amounts by tax component for detailed tax reporting.


Workflow Integration

Trigger Events

The quotation module fires workflow triggers:

self.trigger(ids, "submit_for_approval")    # When quotation submitted

These can be configured in workflow automation to send notifications, create tasks, etc.


Best Practices

1. Quotation Numbering

# Bad example - manually setting number
quot_id = get_model("sale.quot").create({
    "number": "QT-001",  # May conflict!
    "contact_id": 123
})

# Good example - let system generate
quot_id = get_model("sale.quot").create({
    # number auto-generated from sequence
    "contact_id": 123
})

2. Expiration Date Management

Always set expiration dates on quotations to prevent confusion:

# Set validity period to auto-calculate exp_date
get_model("sale.quot").create({
    "contact_id": 123,
    "date": "2024-01-15",
    "validity_id": 3,  # References valid.period with 30 days
    # exp_date will be auto-set to 2024-02-14 via onchange
})

3. Quote-to-Order Conversion

Always approve quotation before converting to sales order:

# Bad: Converting without approval
get_model("sale.quot").copy_to_sale_order([quot_id])  # May fail

# Good: Proper workflow
get_model("sale.quot").approve([quot_id])
get_model("sale.quot").copy_to_sale_order([quot_id])

4. Opportunity Integration

Link quotations to opportunities for better sales tracking:

quot_id = get_model("sale.quot").create({
    "contact_id": 123,
    "opport_id": 456,  # Link to opportunity
    "date": "2024-01-15"
})
# Opportunity amounts will auto-update

5. Multi-Currency Handling

Set custom currency rates when needed:

get_model("sale.quot").create({
    "contact_id": 123,
    "currency_id": 2,  # Non-default currency
    "currency_rates": [
        ("create", {
            "currency_id": 2,
            "rate": 35.50  # Custom rate
        })
    ]
})

Database Constraints

Unique Number Constraint

CREATE UNIQUE INDEX sale_quot_number_key
    ON sale_quot (number);

Ensures each quotation has a unique number to prevent duplicates.


Check Constraint

The check_fields constraint validates: - Quotations in "waiting_approval" or "approved" state must have at least one line item

if obj.state in ("waiting_approval", "approved"):
    if not obj.lines:
        raise Exception("No lines in quotation")

Model Relationship Description
sale.quot.line One2Many (lines) Quotation line items with products, quantities, prices
contact Many2One (contact_id) Customer receiving the quotation
sale.opportunity Many2One (opport_id) Related sales opportunity
sale.order One2Many (sales) Sales orders created from this quotation
currency Many2One (currency_id) Quotation currency
price.list Many2One (price_list_id) Price list for product pricing
payment.term Many2One (pay_term_id) Payment terms
ship.term Many2One (ship_term_id) Shipping terms
valid.period Many2One (validity_id) Validity period template
base.user Many2One (user_id) Quotation owner/salesperson
sequence Many2One (sequence_id) Number generation sequence
quot.cost One2Many (est_costs) Estimated costs (deprecated)
company Many2One (company_id) Company for multi-company
message One2Many (comments) Comments and notes
document One2Many (documents) Attached documents
email.message One2Many (emails) Related emails
sale.activ One2Many (activities) Related activities
custom.currency.rate One2Many (currency_rates) Custom exchange rates
job.template Many2One (job_template_id) Service order template
reason.code Many2One (lost_sale_code_id) Lost sale reason
sale.categ Many2One (sale_categ_id) Sales category
project Many2One (project_id) Related project
address Many2One (bill_address_id, ship_address_id) Billing and shipping addresses
seller Many2One (seller_id) Seller entity

Common Use Cases

Use Case 1: Create Quotation from Opportunity

# Complete workflow from opportunity to quotation

# 1. Create or get opportunity
opport_id = get_model("sale.opportunity").create({
    "contact_id": 123,
    "subject": "New laptop order inquiry"
})

# 2. Create quotation linked to opportunity
quot_id = get_model("sale.quot").create({
    "opport_id": opport_id,
    "contact_id": 123,
    "date": "2024-01-15",
    "exp_date": "2024-02-15",
    "currency_id": 1,
    "tax_type": "tax_ex",
    "price_list_id": 10,
    "lines": [
        ("create", {
            "product_id": 100,
            "description": "Premium Laptop",
            "qty": 10,
            "uom_id": 1,
            "unit_price": 1500.00,
            "tax_id": 1
        })
    ]
})

# 3. Submit for approval
get_model("sale.quot").submit_for_approval([quot_id])

# 4. Approve (requires permission)
get_model("sale.quot").approve([quot_id])

# 5. Convert to sales order when customer accepts
result = get_model("sale.quot").copy_to_sale_order([quot_id])
sale_id = result["next"]["active_id"]

# 6. Mark quotation as won
get_model("sale.quot").do_won([quot_id])

Use Case 2: Revise Quotation After Customer Feedback

# Customer wants changes to quotation

# 1. Create revision
result = get_model("sale.quot").revise([original_quot_id])
new_quot_id = result["new_id"]

# 2. Update the revision with changes
get_model("sale.quot").write([new_quot_id], {
    "lines": [
        # Update existing line
        ("write", line_id, {
            "qty": 15,  # Increased quantity
            "unit_price": 1400.00  # Negotiated price
        }),
        # Add new line
        ("create", {
            "product_id": 101,
            "description": "Laptop Bags",
            "qty": 15,
            "uom_id": 1,
            "unit_price": 50.00,
            "tax_id": 1
        })
    ]
})

# 3. Submit and approve new version
get_model("sale.quot").submit_for_approval([new_quot_id])
get_model("sale.quot").approve([new_quot_id])

# Original quotation is now in "revised" state
# New quotation has new number

Use Case 3: Apply Bulk Discount

# Apply 10% discount to entire quotation

# 1. Set discount percentage
get_model("sale.quot").write([quot_id], {
    "discount": 10  # 10% discount
})

# 2. Apply to all lines
result = get_model("sale.quot").apply_discount([quot_id])

# All lines now have 10% discount applied
# Amount totals automatically recalculated

Use Case 4: Quotation with Expiration Management

# Create quotation with automatic expiration

# 1. Create quotation with validity period
quot_id = get_model("sale.quot").create({
    "contact_id": 123,
    "date": "2024-01-15",
    "validity_id": 3,  # References valid.period with 30 days
    "currency_id": 1,
    "tax_type": "tax_ex"
})

# exp_date automatically set to 30 days from date

# 2. Check expiration
quot = get_model("sale.quot").browse(quot_id)
from datetime import datetime
today = datetime.now().date()
exp_date = datetime.strptime(quot.exp_date, "%Y-%m-%d").date()

if today > exp_date:
    # Quotation expired - create new revision
    result = get_model("sale.quot").revise([quot_id])
    new_quot_id = result["new_id"]

Use Case 5: Track Quotation Profitability

# Create quotation with cost tracking

# 1. Create quotation with cost prices on lines
quot_id = get_model("sale.quot").create({
    "contact_id": 123,
    "date": "2024-01-15",
    "currency_id": 1,
    "tax_type": "tax_ex",
    "lines": [
        ("create", {
            "product_id": 100,
            "description": "Product A",
            "qty": 10,
            "uom_id": 1,
            "unit_price": 150.00,
            "cost_price": 100.00,  # Cost tracking
            "tax_id": 1
        })
    ]
})

# 2. Generate estimated costs
get_model("sale.quot").create_est_costs([quot_id])

# 3. Check profitability
quot = get_model("sale.quot").browse(quot_id)
print(f"Revenue: {quot.amount_subtotal}")
print(f"Cost: {quot.cost_amount}")
print(f"Profit: {quot.profit_amount}")
print(f"Margin: {quot.margin_percent}%")

# Decide whether to approve based on margin
if quot.margin_percent < 20:
    print("WARNING: Low margin quotation")

Use Case 6: Multi-Company Quotation

# Create quotation for specific company

# 1. Set company context
context = {"company_id": 2}

# 2. Create quotation
quot_id = get_model("sale.quot").create({
    "contact_id": 123,
    "date": "2024-01-15",
    "currency_id": 1,
    "tax_type": "tax_ex",
    "company_id": 2  # Explicit company
}, context=context)

# Quotation belongs to company 2
# Number sequence uses company 2's sequence

Performance Tips

1. Batch Operations

When processing multiple quotations, use batch operations:

# Bad: Individual operations
for quot_id in quot_ids:
    get_model("sale.quot").approve([quot_id])

# Good: Batch operation
get_model("sale.quot").approve(quot_ids)

2. Minimize Function Store Calls

The create and write methods automatically call function_store to compute amounts. Avoid calling it manually unless needed:

# Bad: Unnecessary function_store
quot_id = get_model("sale.quot").create({...})
get_model("sale.quot").function_store([quot_id])  # Already called!

# Good: Let create handle it
quot_id = get_model("sale.quot").create({...})

3. Use Stored Computed Fields

Amount fields are stored in database for performance. Search using these fields directly:

# Good: Search on stored field
quot_ids = get_model("sale.quot").search([
    ["amount_total", ">", 10000],
    ["state", "=", "approved"]
])

Troubleshooting

"No lines in quotation"

Cause: Attempting to submit or approve quotation without any line items. Solution: Add at least one quotation line before submitting for approval.

"User does not have permission to approve quotations"

Cause: Current user lacks the "approve_quotation" permission and settings.approve_quot is enabled. Solution: Grant user the "approve_quotation" permission or disable approval requirement in settings.

"Product X is below the minimum sales price"

Cause: A quotation line has unit_price less than the product's sale_min_price. Solution: Increase the unit price on the quotation line or update the product's minimum price.

"Sales order already created for quotation X"

Cause: Attempting to convert a quotation that has already been converted to a sales order. Solution: Check quotation.sales to see existing orders. Use do_reopen if needed to reset state.

"Quotation number already exists"

Cause: Attempting to create quotation with duplicate number. Solution: Use automatic number generation instead of manual entry. Check sequence configuration.

"Quotation is empty"

Cause: Converting quotation to sales order but no valid lines found. Solution: Ensure quotation has at least one line with product information.


Security Considerations

Permission Model

  • approve_quotation - Required to approve quotations when approval workflow is enabled
  • Multi-company access controls based on company_id field
  • Audit log tracks all changes to quotation records

Data Access

  • Quotations are company-specific when multi-company is enabled
  • Users can only access quotations in their assigned companies
  • External access via UUID should be restricted to customer-facing views only
  • Sensitive cost and profit information should be restricted from customer views

Configuration Settings

Required Settings

Setting Location Description
currency_id settings model Default system currency
Number sequence sequence model Sequence for quotation numbering (type: sale_quot)

Optional Settings

Setting Default Description
approve_quot False Enable approval workflow requirement
sale_split_service False Split service and non-service products into separate orders

Integration Points

External Systems

  • External Views: UUID-based links for customer quotation viewing
  • Email Integration: Send quotations via email.message integration
  • Document Management: Attach supporting documents via document model

Internal Modules

  • Opportunities: Link quotations to sales opportunities for pipeline tracking
  • Sales Orders: Convert quotations to confirmed sales orders
  • Job Management: Create service orders using job_template_id
  • Inventory: Product pricing and availability
  • Accounting: Tax calculations and currency conversions
  • Projects: Link quotations to projects for project-based sales

Version History

Last Updated: 2024-01-05 Model Version: sale_quot.py (923 lines) Framework: Netforce


Additional Resources

  • Quotation Line Documentation: sale.quot.line
  • Sales Order Documentation: sale.order
  • Opportunity Documentation: sale.opportunity
  • Price List Documentation: price.list
  • Payment Terms Documentation: payment.term

Support & Feedback

For issues or questions about this module: 1. Check related model documentation (sale.quot.line, sale.order) 2. Review system logs for detailed error messages 3. Verify permissions and settings configuration (approve_quot, sequences) 4. Test in development environment first 5. Ensure opportunity integration is properly configured 6. Validate product minimum prices are set correctly


This documentation is generated for developer onboarding and reference purposes.