Sale Quotation Line Documentation¶
Overview¶
The Quotation Line module (sale.quot.line) represents individual line items within a sales quotation. Each line contains product information, quantities, pricing, discounts, and tax details. This model is simpler than sales order lines as it focuses on quotation-specific requirements without inventory or shipping concerns. Lines support both regular items and group headers for organizing quotations into sections.
Model Information¶
Model Name: sale.quot.line
Display Name: Quotation Line
Parent Model: sale.quot (belongs to quotation via quot_id)
Features¶
- ❌ Audit logging (inherits from parent quotation)
- ❌ Multi-company support (inherits from parent quotation)
- ❌ Content search (inherits from parent quotation)
- ✅ Cascade delete when parent quotation is deleted
Line Types¶
| Type | Code | Description |
|---|---|---|
| Item | item |
Regular product line with quantity and pricing |
| Group | group |
Section header for organizing lines (no pricing) |
Key Fields Reference¶
Core Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
quot_id |
Many2One | ✅ | Parent quotation (cascade delete on parent removal) |
product_id |
Many2One | ❌ | Product being quoted |
description |
Text | ✅ | Line item description |
qty |
Decimal | ❌ | Quantity being quoted |
uom_id |
Many2One | ❌ | Unit of measure |
unit_price |
Decimal | ❌ | Price per unit (scale: 6 decimal places) |
type |
Selection | ❌ | Line type: "item" or "group" |
Discount Fields¶
| Field | Type | Description |
|---|---|---|
discount |
Decimal | Discount percentage (e.g., 10 for 10%) |
discount_fixed |
Decimal | Fixed discount amount |
Tax & Amount Fields¶
| Field | Type | Description |
|---|---|---|
tax_id |
Many2One | Tax rate to apply |
amount |
Decimal | Line total (computed: qty × unit_price - discounts) |
amount_discount |
Decimal | Total discount amount (computed) |
amount_tax |
Decimal | Tax amount (computed based on quotation tax_type) |
amount_incl_tax |
Decimal | Amount including tax (computed) |
amount_excl_tax |
Decimal | Amount excluding tax (computed) |
Cost & Profit Fields¶
| Field | Type | Description |
|---|---|---|
cost_price |
Decimal | Estimated cost per unit |
cost_amount |
Decimal | Total cost (computed: cost_price × qty) |
profit_amount |
Decimal | Estimated profit (computed: amount - cost_amount) |
margin_percent |
Decimal | Profit margin percentage (computed) |
Sequencing Fields¶
| Field | Type | Description |
|---|---|---|
sequence_no |
Integer | Numeric sequence for ordering lines |
sequence |
Char | String sequence (deprecated, use sequence_no) |
index |
Integer | Position within quotation (computed) |
Related Fields (Inherited from Parent)¶
| Field | Type | Description |
|---|---|---|
contact_id |
Many2One | Customer (from quot_id.contact_id) |
date |
Date | Quotation date (from quot_id.date) |
user_id |
Many2One | Quotation owner (from quot_id.user_id) |
state |
Selection | Quotation state (from quot_id.state) |
product_categs |
Many2Many | Product categories (from product_id.categs) |
Reporting & Aggregation Fields¶
| Field | Type | Description |
|---|---|---|
agg_amount |
Decimal | Sum aggregation of amounts |
agg_qty |
Decimal | Sum aggregation of quantities |
notes |
Text | Additional line notes |
API Methods¶
1. Create Quotation Line¶
Method: create(vals, context)
Creates a new quotation line and updates parent quotation amounts.
Parameters:
vals = {
"quot_id": 123, # Required: Parent quotation
"product_id": 100, # Optional: Product
"description": "Product Name", # Required: Description
"qty": 5, # Optional: Quantity
"uom_id": 1, # Optional: Unit of measure
"unit_price": 150.00, # Optional: Unit price
"discount": 10, # Optional: Discount %
"discount_fixed": 50.00, # Optional: Fixed discount
"tax_id": 1, # Optional: Tax rate
"cost_price": 100.00, # Optional: Cost price
"type": "item", # Optional: Line type
"sequence_no": 10, # Optional: Sequence
"notes": "Special requirements" # Optional: Notes
}
Returns: int - New line ID
Example:
# Create quotation line for a product
line_id = get_model("sale.quot.line").create({
"quot_id": quot_id,
"product_id": 100,
"description": "Premium Laptop",
"qty": 5,
"uom_id": 1,
"unit_price": 1500.00,
"tax_id": 1,
"cost_price": 1000.00
})
Behavior: - Automatically calls function_store to compute amount fields - Parent quotation amounts are recalculated via write trigger - Line ordered by sequence_no and id
2. Update Quotation Line¶
Method: write(ids, vals, context)
Updates quotation line(s) and recalculates amounts.
Parameters:
- ids (list): Line IDs to update
- vals (dict): Fields to update
Example:
# Update quantity and price
get_model("sale.quot.line").write([line_id], {
"qty": 10,
"unit_price": 1400.00,
"discount": 5
})
Behavior: - Calls function_store to recompute amounts - Parent quotation amounts automatically updated - Changes tracked in parent quotation audit log
Computed Fields Functions¶
get_amount(ids, context)¶
Calculates the line total amount after discounts.
Formula:
amount = qty × unit_price
if discount:
amount = amount × (1 - discount/100)
if discount_fixed:
amount = amount - discount_fixed
Returns: Decimal - Line amount or None if qty or unit_price not set
Example:
# qty=10, unit_price=100, discount=10%, discount_fixed=50
# amount = (10 × 100) × (1 - 10/100) - 50
# amount = 1000 × 0.9 - 50 = 900 - 50 = 850
get_amount_discount(ids, context)¶
Calculates total discount amount applied to the line.
Formula:
discount_amt = 0
if discount and qty and unit_price:
discount_amt = qty × unit_price × (discount/100)
if discount_fixed:
discount_amt += discount_fixed
Returns: Decimal - Total discount amount or None
get_profit(ids, context)¶
Calculates cost, profit, and margin for the line.
Formula:
cost_amount = cost_price × qty
profit_amount = amount - cost_amount
margin_percent = (profit_amount / amount) × 100
Returns: Dictionary with:
- cost_amount - Total cost
- profit_amount - Profit amount
- margin_percent - Margin percentage
Example:
# amount=1000, cost_price=600, qty=2
# cost_amount = 600 × 2 = 1200
# profit_amount = 1000 - 1200 = -200 (loss!)
# margin_percent = (-200 / 1000) × 100 = -20%
get_tax_amount(ids, context)¶
Calculates tax amounts based on quotation's tax_type.
Behavior: - Computes base amount for tax calculation - Applies tax rate using account.tax.rate.compute_taxes() - Splits amount into excl_tax and incl_tax based on tax_type
Returns: Dictionary with:
- amount_tax - Tax amount
- amount_incl_tax - Amount including tax
- amount_excl_tax - Amount excluding tax
Tax Type Handling:
if tax_type == "tax_ex":
# Tax exclusive
amount_excl_tax = amount
amount_incl_tax = amount + tax
elif tax_type == "tax_in":
# Tax inclusive
amount_incl_tax = amount
amount_excl_tax = amount - tax
elif tax_type == "no_tax":
# No tax
amount_tax = 0
get_index(ids, context)¶
Calculates the line's position within its quotation.
Returns: Integer - 1-based index position
Example:
get_sequence_old(ids, context)¶
Deprecated function that converts sequence_no to string.
Returns: String version of sequence_no
Search Functions¶
Search by Product¶
# Find all quotation lines for a specific product
line_ids = get_model("sale.quot.line").search([
["product_id", "=", product_id]
])
Search by Customer¶
# Find all quotation lines for a customer
line_ids = get_model("sale.quot.line").search([
["contact_id", "=", contact_id]
])
Search by State¶
# Find all approved quotation lines
line_ids = get_model("sale.quot.line").search([
["state", "=", "approved"]
])
Search by Product Category¶
# Find lines for products in a category
line_ids = get_model("sale.quot.line").search([
["product_categs", "=", categ_id]
])
Search by Date Range¶
# Find lines in date range
line_ids = get_model("sale.quot.line").search([
["date", ">=", "2024-01-01"],
["date", "<=", "2024-12-31"]
])
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.quot |
Many2One (quot_id) | Parent quotation (cascade delete) |
product |
Many2One (product_id) | Product being quoted |
uom |
Many2One (uom_id) | Unit of measure |
account.tax.rate |
Many2One (tax_id) | Tax rate |
contact |
Many2One (computed) | Customer from parent quotation |
base.user |
Many2One (computed) | Owner from parent quotation |
product.categ |
Many2Many (computed) | Product categories |
Common Use Cases¶
Use Case 1: Create Standard Product Line¶
# Add a product line to quotation
# 1. Create line with product
line_id = get_model("sale.quot.line").create({
"quot_id": quot_id,
"product_id": 100,
"description": "Laptop Computer",
"qty": 5,
"uom_id": 1,
"unit_price": 1500.00,
"tax_id": 1,
"cost_price": 1000.00,
"sequence_no": 10
})
# Line amount automatically calculated: 5 × 1500 = 7500
# Parent quotation totals updated automatically
Use Case 2: Create Line with Discounts¶
# Add line with percentage and fixed discount
line_id = get_model("sale.quot.line").create({
"quot_id": quot_id,
"product_id": 101,
"description": "Desktop Computer",
"qty": 10,
"uom_id": 1,
"unit_price": 1000.00,
"discount": 10, # 10% discount
"discount_fixed": 200, # Additional $200 off
"tax_id": 1,
"sequence_no": 20
})
# Calculation:
# Base: 10 × 1000 = 10,000
# After 10%: 10,000 × 0.9 = 9,000
# After fixed: 9,000 - 200 = 8,800
Use Case 3: Create Group Header¶
# Create section header to organize lines
# 1. Create group header
group_id = get_model("sale.quot.line").create({
"quot_id": quot_id,
"type": "group",
"description": "HARDWARE COMPONENTS",
"sequence_no": 100
})
# 2. Add items under the group
line1_id = get_model("sale.quot.line").create({
"quot_id": quot_id,
"product_id": 200,
"description": "CPU",
"qty": 10,
"unit_price": 300.00,
"sequence_no": 110
})
line2_id = get_model("sale.quot.line").create({
"quot_id": quot_id,
"product_id": 201,
"description": "RAM",
"qty": 20,
"unit_price": 100.00,
"sequence_no": 120
})
# Group header appears before items
# Group has no amount (display only)
Use Case 4: Update Line Quantities and Prices¶
# Customer requests quantity change
# 1. Get current line
line = get_model("sale.quot.line").browse(line_id)
print(f"Current: {line.qty} × {line.unit_price} = {line.amount}")
# 2. Update quantity
get_model("sale.quot.line").write([line_id], {
"qty": 15, # Changed from 10
"unit_price": 1400.00 # Negotiated price
})
# 3. Check new amount
line = line.browse()
print(f"Updated: {line.qty} × {line.unit_price} = {line.amount}")
# Output: Updated: 15 × 1400.00 = 21000.00
# Parent quotation totals automatically updated
Use Case 5: Analyze Line Profitability¶
# Check profit margins on quotation lines
# 1. Get all lines with profit data
quot = get_model("sale.quot").browse(quot_id)
for line in quot.lines:
if line.type == "item" and line.amount:
print(f"\nProduct: {line.description}")
print(f" Sale Amount: {line.amount}")
print(f" Cost Amount: {line.cost_amount}")
print(f" Profit: {line.profit_amount}")
print(f" Margin: {line.margin_percent}%")
# Flag low margin items
if line.margin_percent and line.margin_percent < 20:
print(f" ⚠ WARNING: Low margin!")
# Example output:
# Product: Laptop Computer
# Sale Amount: 7500.00
# Cost Amount: 5000.00
# Profit: 2500.00
# Margin: 33.33%
#
# Product: Desktop Computer
# Sale Amount: 8800.00
# Cost Amount: 8500.00
# Profit: 300.00
# Margin: 3.41%
# ⚠ WARNING: Low margin!
Use Case 6: Bulk Line Creation¶
# Add multiple products at once
products = [
{"product_id": 100, "qty": 5, "price": 1500},
{"product_id": 101, "qty": 10, "price": 1000},
{"product_id": 102, "qty": 2, "price": 2500},
]
for i, item in enumerate(products):
prod = get_model("product").browse(item["product_id"])
get_model("sale.quot.line").create({
"quot_id": quot_id,
"product_id": item["product_id"],
"description": prod.name,
"qty": item["qty"],
"uom_id": prod.uom_id.id,
"unit_price": item["price"],
"tax_id": prod.sale_tax_id.id,
"cost_price": prod.cost_price,
"sequence_no": (i + 1) * 10
})
# All lines created with sequential numbering
# Quotation totals updated after all lines added
Use Case 7: Tax Calculation Examples¶
# Different tax scenarios
# Tax Exclusive (tax added on top)
line_id = get_model("sale.quot.line").create({
"quot_id": quot_id, # quot has tax_type="tax_ex"
"product_id": 100,
"description": "Product A",
"qty": 10,
"unit_price": 100,
"tax_id": vat_7_percent_id
})
line = get_model("sale.quot.line").browse(line_id)
# amount = 1000
# amount_excl_tax = 1000
# amount_tax = 70 (7% of 1000)
# amount_incl_tax = 1070
# Tax Inclusive (tax included in price)
line_id = get_model("sale.quot.line").create({
"quot_id": quot_id2, # quot has tax_type="tax_in"
"product_id": 100,
"description": "Product A",
"qty": 10,
"unit_price": 100,
"tax_id": vat_7_percent_id
})
line = get_model("sale.quot.line").browse(line_id)
# amount = 1000
# amount_incl_tax = 1000
# amount_tax = 65.42 (calculated from inclusive)
# amount_excl_tax = 934.58
Best Practices¶
1. Always Set Sequence Numbers¶
# Bad: No sequence control
get_model("sale.quot.line").create({
"quot_id": quot_id,
"description": "Item"
# No sequence_no - unpredictable order
})
# Good: Control line order
get_model("sale.quot.line").create({
"quot_id": quot_id,
"description": "Item",
"sequence_no": 10 # Explicit ordering
})
2. Use Group Headers for Organization¶
# Create well-organized quotations with sections
# Section 1: Hardware
get_model("sale.quot.line").create({
"quot_id": quot_id,
"type": "group",
"description": "HARDWARE",
"sequence_no": 100
})
# ... add hardware items at 110, 120, 130...
# Section 2: Software
get_model("sale.quot.line").create({
"quot_id": quot_id,
"type": "group",
"description": "SOFTWARE",
"sequence_no": 200
})
# ... add software items at 210, 220, 230...
# Section 3: Services
get_model("sale.quot.line").create({
"quot_id": quot_id,
"type": "group",
"description": "SERVICES",
"sequence_no": 300
})
# ... add service items at 310, 320, 330...
3. Track Costs for Profitability¶
# Always set cost_price for profit analysis
# Bad: No cost tracking
get_model("sale.quot.line").create({
"quot_id": quot_id,
"product_id": 100,
"unit_price": 1500
# No cost_price - can't analyze profit
})
# Good: Track costs
prod = get_model("product").browse(100)
get_model("sale.quot.line").create({
"quot_id": quot_id,
"product_id": 100,
"unit_price": 1500,
"cost_price": prod.cost_price # Enable profit analysis
})
4. Consistent Discount Application¶
# Choose either percentage OR fixed discount, not random mix
# Consistent: Use percentage discounts
for line in lines:
line.write({"discount": 10}) # 10% on all
# Or consistent: Use fixed discounts based on tier
for line in lines:
if line.amount > 10000:
line.write({"discount_fixed": 500})
elif line.amount > 5000:
line.write({"discount_fixed": 200})
Performance Tips¶
1. Batch Line Operations¶
# Bad: Creating lines one by one with individual commits
for product in products:
line_id = get_model("sale.quot.line").create({...})
# Each create triggers quotation recalculation!
# Good: Prepare all lines then create
line_vals_list = []
for product in products:
line_vals_list.append({
"quot_id": quot_id,
"product_id": product.id,
...
})
# Create all at once
for vals in line_vals_list:
get_model("sale.quot.line").create(vals)
# Quotation recalculated once after all lines added
2. Minimize Field Updates¶
# Bad: Multiple updates
get_model("sale.quot.line").write([line_id], {"qty": 10})
get_model("sale.quot.line").write([line_id], {"unit_price": 100})
get_model("sale.quot.line").write([line_id], {"discount": 5})
# Triggers 3 recalculations!
# Good: Single update
get_model("sale.quot.line").write([line_id], {
"qty": 10,
"unit_price": 100,
"discount": 5
})
# Triggers 1 recalculation
Troubleshooting¶
Line amounts not calculating¶
Cause: Missing qty or unit_price field. Solution: Ensure both qty and unit_price are set. Amount is None if either is missing.
Tax amounts incorrect¶
Cause: Tax calculation depends on parent quotation's tax_type. Solution: Check quotation.tax_type setting. Verify tax_id is set on line.
Lines appearing in wrong order¶
Cause: sequence_no not set or not sequential. Solution: Set explicit sequence_no values with gaps (10, 20, 30) to allow insertions.
Parent quotation totals not updating¶
Cause: Lines created/updated without triggering parent update. Solution: The write() method should automatically trigger parent update. If not working, call quotation.function_store() manually.
Profit calculations showing None¶
Cause: cost_price not set on line. Solution: Set cost_price field when creating line, typically from product.cost_price.
Database Constraints¶
Cascade Delete¶
When a quotation is deleted, all its lines are automatically deleted.
Required Fields¶
quot_id- Must reference valid quotationdescription- Cannot be empty
Testing Examples¶
Unit Test: Line Amount Calculation¶
def test_line_amount_calculation():
# Create quotation
quot_id = get_model("sale.quot").create({
"contact_id": 1,
"currency_id": 1,
"tax_type": "tax_ex"
})
# Create line with discount
line_id = get_model("sale.quot.line").create({
"quot_id": quot_id,
"description": "Test Product",
"qty": 10,
"unit_price": 100,
"discount": 10,
"discount_fixed": 50
})
# Verify calculation
line = get_model("sale.quot.line").browse(line_id)
# Expected: (10 × 100) × 0.9 - 50 = 1000 × 0.9 - 50 = 850
assert line.amount == 850
# Verify discount amount
# Expected: (10 × 100 × 0.1) + 50 = 100 + 50 = 150
assert line.amount_discount == 150
Unit Test: Profit Calculation¶
def test_profit_calculation():
# Create quotation line with cost
line_id = get_model("sale.quot.line").create({
"quot_id": quot_id,
"description": "Test Product",
"qty": 5,
"unit_price": 200,
"cost_price": 120
})
line = get_model("sale.quot.line").browse(line_id)
# Verify calculations
# amount = 5 × 200 = 1000
assert line.amount == 1000
# cost_amount = 5 × 120 = 600
assert line.cost_amount == 600
# profit = 1000 - 600 = 400
assert line.profit_amount == 400
# margin = 400 / 1000 × 100 = 40%
assert line.margin_percent == 40
Security Considerations¶
Data Access¶
- Line access controlled through parent quotation
- Users can only access lines of quotations they can view
- No direct line-level permissions (inherits from quotation)
Audit Trail¶
- Line changes tracked through parent quotation audit log
- Use quotation audit log to track line modifications
- Cost and profit fields should be restricted from customer views
Version History¶
Last Updated: 2024-01-05 Model Version: sale_quot_line.py (157 lines) Framework: Netforce
Additional Resources¶
- Parent Quotation Documentation:
sale.quot - Product Documentation:
product - Tax Rate Documentation:
account.tax.rate - Unit of Measure Documentation:
uom - See also:
sale.order.linefor sales order line comparison
Support & Feedback¶
For issues or questions about quotation lines:
1. Check parent quotation documentation (sale.quot)
2. Verify product and tax rate setup
3. Review amount calculation formulas
4. Check sequence_no for ordering issues
5. Ensure cost_price is set for profit analysis
6. Test discount combinations in development environment
This documentation is generated for developer onboarding and reference purposes.