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:
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:
State Workflow¶
| 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:
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:
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:
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:
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:
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:
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:
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:
7.5 Void Quotation¶
Method: void(ids, context)
Voids/cancels the quotation.
Parameters:
- ids (list): Quotation IDs to void
Example:
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:
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:
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¶
9.1 View External Link¶
Method: view_link(ids, context)
Generates external view URL for quotation.
Returns:
Example:
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:
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:
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:
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:
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¶
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")
Related Models¶
| 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.