Quotation Cost Tracking Documentation¶
Overview¶
The Quotation Cost module (quot.cost) tracks estimated costs for quotations to enable profitability analysis and margin calculations before order creation. This model is nearly identical to sale.cost but operates at the quotation stage, allowing businesses to evaluate pricing strategies, compare supplier costs, and ensure adequate profit margins during the quoting process.
Model Information¶
Model Name: quot.cost
Display Name: Cost
Key Fields: None (child records linked to quotations)
Features¶
- ❌ Audit logging enabled (
_audit_log) - ❌ Multi-company support (
company_id) - ❌ Full-text content search (
_content_search) - ✅ Cascade deletion when parent quotation is deleted
- ✅ Searchable description field
- ✅ Multi-currency support for international suppliers
- ✅ Computed amount field based on quantity and landed cost
Understanding Quotation Costs¶
What are Quotation Costs?¶
Quotation Costs represent the estimated costs associated with fulfilling a customer quotation. These costs are critical for:
- Pre-Sale Profitability Analysis: Calculate expected profit before creating a sales order
- Pricing Strategy: Determine if quoted prices provide adequate margin
- Supplier Selection: Compare costs from different suppliers during quoting
- Competitive Analysis: Understand cost structure when bidding on projects
- Cost-Plus Pricing: Build quotations based on cost plus desired margin
- Risk Assessment: Identify potential profit/loss scenarios before committing
Cost Breakdown Structure¶
Each cost record can track: - Product Costs: Material and product costs - Supplier Pricing: List price from specific suppliers - Import Duties: Percentage-based import duty costs - Shipping Charges: Percentage-based shipping costs - Landed Cost: Final cost per unit including all charges - Quantity: Number of units at this cost - Currency: Support for foreign currency costs
Cost Calculation Formula¶
Where:
Relationship Between quot.cost and sale.cost¶
Model Similarities¶
Both quot.cost and sale.cost are structurally identical with the same fields and calculation methods. The key difference is their parent:
| Feature | quot.cost | sale.cost |
|---|---|---|
| Parent Model | sale.quot |
sale.order |
| Stage | Quotation/Pre-sale | Order/Post-sale |
| Purpose | Estimate costs before order | Track costs after order |
| Flexibility | High - easy to modify | Lower - order committed |
| Conversion | Copied to sale.cost when order created | Final costs |
Cost Flow Through Sales Process¶
Quotation Stage Order Stage
┌─────────────┐ ┌─────────────┐
│ sale.quot │ Convert │ sale.order │
│ │────────────>│ │
│ quot.cost │ │ sale.cost │
└─────────────┘ └─────────────┘
│ │
v v
est_costs field est_costs field
(cost estimation) (cost tracking)
When a quotation is converted to a sales order, cost records can be:
1. Copied from quot.cost to sale.cost
2. Modified based on negotiated pricing
3. Updated with actual supplier quotes
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
quot_id |
Many2One | ✅ | Link to parent quotation (sale.quot) |
description |
Text | ✅ | Description of the cost item (searchable) |
sequence |
Char | ❌ | Item number this cost applies to |
Cost Fields¶
| Field | Type | Description |
|---|---|---|
product_id |
Many2One | Product this cost is for (links to product model) |
supplier_id |
Many2One | Supplier providing this cost (links to contact) |
list_price |
Decimal | Original list price from supplier |
purchase_price |
Decimal | Actual purchase price per unit |
purchase_duty_percent |
Decimal | Import duty percentage (e.g., 10 for 10%) |
purchase_ship_percent |
Decimal | Shipping charge percentage (e.g., 5 for 5%) |
landed_cost |
Decimal | Final cost per unit including all charges |
qty |
Decimal | Quantity of units at this cost |
amount |
Decimal | Total cost amount (computed: qty × landed_cost) |
Currency Fields¶
| Field | Type | Description |
|---|---|---|
currency_id |
Many2One | Currency for this cost (links to currency model) |
currency_rate |
Decimal | Currency exchange rate (deprecated) |
Deprecated Fields¶
| Field | Status | Replacement |
|---|---|---|
unit_price |
Deprecated | Use landed_cost instead |
uom_id |
Deprecated | UoM now managed at product level |
currency_rate |
Deprecated | Currency conversion handled by currency model |
API Methods¶
1. Create Cost Record¶
Method: create(vals, context)
Creates a new cost record for a quotation.
Parameters:
vals = {
"quot_id": 123, # Required: Quotation ID
"description": "Widget A Parts", # Required: Cost description
"product_id": 456, # Optional: Product ID
"supplier_id": 789, # Optional: Supplier contact ID
"list_price": 100.00, # Optional: Supplier list price
"purchase_price": 95.00, # Optional: Actual purchase price
"purchase_duty_percent": 10.0, # Optional: Import duty %
"purchase_ship_percent": 5.0, # Optional: Shipping %
"landed_cost": 109.25, # Optional: Final unit cost
"qty": 10, # Optional: Quantity
"currency_id": 2, # Optional: Currency ID (e.g., USD)
"sequence": "1" # Optional: Item number
}
context = {}
Returns: int - New cost record ID
Example:
# Create cost for imported product with duty and shipping
cost_id = get_model("quot.cost").create({
"quot_id": 123,
"description": "Widget A - Supplier XYZ Quote",
"product_id": 456,
"supplier_id": 789,
"list_price": 100.00,
"purchase_price": 95.00,
"purchase_duty_percent": 10.0, # 10% import duty
"purchase_ship_percent": 5.0, # 5% shipping
"landed_cost": 109.25, # 95 + (95*0.10) + (95*0.05)
"qty": 10,
"currency_id": 2, # USD
"sequence": "1"
})
2. Get Amount (Computed Field)¶
Method: get_amount(ids, context)
Calculates the total cost amount for each cost record.
Parameters:
- ids (list): Cost record IDs to calculate amounts for
Behavior: - Multiplies quantity by landed cost - Returns 0 if either qty or landed_cost is missing - Automatically recalculated when qty or landed_cost changes
Calculation:
Returns: dict - Dictionary mapping record IDs to computed amounts
Example:
# Automatic calculation - no need to call directly
cost = get_model("quot.cost").browse(cost_id)
print(f"Quantity: {cost.qty}") # Output: 10
print(f"Landed Cost: {cost.landed_cost}") # Output: 109.25
print(f"Total Amount: {cost.amount}") # Output: 1092.50 (computed)
Integration with Quotations¶
Quotation Profit Calculation¶
Quotations use cost records to calculate estimated profitability:
# From sale.quot model get_profit() method
def get_profit(self, ids, context={}):
for obj in self.browse(ids):
# Sum all cost amounts from quotation lines
total_cost = sum([line.cost_amount or 0 for line in obj.lines])
# Calculate profit and margin
profit = (obj.amount_subtotal or 0) - total_cost
margin = profit * 100 / obj.amount_subtotal if obj.amount_subtotal else None
return {
"cost_amount": total_cost,
"profit_amount": profit,
"margin_percent": margin
}
Quotation Fields Updated¶
Cost records are used alongside line item cost_price to calculate these quotation fields:
- cost_amount: Total costs (from line items and est_costs)
- profit_amount: Revenue minus total costs
- margin_percent: Profit as percentage of revenue
Auto-Generate Costs from Products¶
Quotations can automatically create cost records from product data:
# From sale.quot.create_est_costs() method
def create_est_costs(self, ids, context={}):
obj = self.browse(ids[0])
# Delete existing product-linked costs
del_ids = [cost.id for cost in obj.est_costs if cost.product_id]
get_model("quot.cost").delete(del_ids)
# Create costs from quotation lines
for line in obj.lines:
prod = line.product_id
if not prod or not prod.purchase_ok:
continue
vals = {
"quot_id": obj.id,
"sequence": line.sequence,
"product_id": prod.id,
"description": prod.name,
"supplier_id": prod.suppliers[0].supplier_id.id if prod.suppliers else None,
"qty": line.qty,
"list_price": prod.purchase_price,
"purchase_price": prod.purchase_price,
"landed_cost": prod.landed_cost,
"purchase_duty_percent": prod.purchase_duty_percent,
"purchase_ship_percent": prod.purchase_ship_percent,
"uom_id": prod.uom_id.id,
"currency_id": prod.purchase_currency_id.id,
}
get_model("quot.cost").create(vals)
Common Use Cases¶
Use Case 1: Create Quotation with Cost Analysis¶
# Create quotation
quot_id = get_model("sale.quot").create({
"contact_id": 123,
"date": "2025-01-05",
"lines": [
("create", {
"product_id": 456,
"description": "Widget A",
"qty": 10,
"unit_price": 150.00,
})
]
})
# Add cost estimate
get_model("quot.cost").create({
"quot_id": quot_id,
"description": "Widget A - Cost estimate",
"product_id": 456,
"supplier_id": 789,
"landed_cost": 100.00,
"qty": 10,
"sequence": "1"
})
# Check profitability
quot = get_model("sale.quot").browse(quot_id)
print(f"Revenue: ${quot.amount_subtotal:.2f}")
print(f"Cost: ${quot.cost_amount:.2f}")
print(f"Profit: ${quot.profit_amount:.2f}")
print(f"Margin: {quot.margin_percent:.1f}%")
# Output:
# Revenue: $1500.00
# Cost: $1000.00
# Profit: $500.00
# Margin: 33.3%
Use Case 2: Auto-Generate Costs from Products¶
# Create quotation with product lines
quot_id = get_model("sale.quot").create({
"contact_id": 123,
"lines": [
("create", {
"product_id": 456,
"qty": 10,
"unit_price": 150.00,
}),
("create", {
"product_id": 457,
"qty": 5,
"unit_price": 200.00,
})
]
})
# Auto-generate cost estimates from product data
get_model("sale.quot").create_est_costs([quot_id])
# Review generated costs
quot = get_model("sale.quot").browse(quot_id)
for cost in quot.est_costs:
print(f"{cost.product_id.name}: {cost.qty} × ${cost.landed_cost:.2f} = ${cost.amount:.2f}")
# Output:
# Widget A: 10 × $100.00 = $1000.00
# Widget B: 5 × $150.00 = $750.00
Use Case 3: Compare Supplier Quotes¶
# Get multiple quotes from different suppliers
quot = get_model("sale.quot").browse(123)
# Supplier A quote
get_model("quot.cost").create({
"quot_id": quot.id,
"description": "Widget A - Supplier A",
"supplier_id": 101,
"landed_cost": 95.00,
"qty": 100,
})
# Supplier B quote
get_model("quot.cost").create({
"quot_id": quot.id,
"description": "Widget A - Supplier B",
"supplier_id": 102,
"landed_cost": 92.00,
"qty": 100,
})
# Supplier C quote
get_model("quot.cost").create({
"quot_id": quot.id,
"description": "Widget A - Supplier C",
"supplier_id": 103,
"landed_cost": 98.00,
"qty": 100,
})
# Compare and select best option
best_cost = None
for cost in quot.est_costs:
print(f"{cost.supplier_id.name}: ${cost.amount:.2f}")
if not best_cost or cost.amount < best_cost.amount:
best_cost = cost
print(f"\nBest option: {best_cost.supplier_id.name} at ${best_cost.amount:.2f}")
# Output:
# Supplier A: $9500.00
# Supplier B: $9200.00 <- Best option
# Supplier C: $9800.00
#
# Best option: Supplier B at $9200.00
Use Case 4: Adjust Pricing Based on Target Margin¶
# Calculate required selling price for target margin
target_margin = 30 # 30% margin
# Create cost estimate
cost_id = get_model("quot.cost").create({
"quot_id": 123,
"description": "Product costs",
"landed_cost": 70.00,
"qty": 10,
})
cost = get_model("quot.cost").browse(cost_id)
total_cost = cost.amount
# Calculate required revenue for 30% margin
# margin = (revenue - cost) / revenue
# revenue = cost / (1 - margin%)
required_revenue = total_cost / (1 - target_margin/100)
required_unit_price = required_revenue / cost.qty
print(f"Total Cost: ${total_cost:.2f}")
print(f"Target Margin: {target_margin}%")
print(f"Required Revenue: ${required_revenue:.2f}")
print(f"Required Unit Price: ${required_unit_price:.2f}")
# Update quotation line with calculated price
quot = get_model("sale.quot").browse(123)
for line in quot.lines:
if line.sequence == "1":
line.write({"unit_price": required_unit_price})
# Output:
# Total Cost: $700.00
# Target Margin: 30%
# Required Revenue: $1000.00
# Required Unit Price: $100.00
Use Case 5: Multi-Currency Quotation Costs¶
# Quotation in THB with costs from multiple currencies
quot_id = get_model("sale.quot").create({
"contact_id": 123,
"currency_id": 1, # THB
"lines": [
("create", {
"description": "Product Bundle",
"qty": 1,
"unit_price": 100000.00, # THB
})
]
})
# Cost from US supplier in USD
get_model("quot.cost").create({
"quot_id": quot_id,
"description": "Component A - US Supplier",
"landed_cost": 500.00, # USD
"qty": 10,
"currency_id": 2, # USD
})
# Cost from EU supplier in EUR
get_model("quot.cost").create({
"quot_id": quot_id,
"description": "Component B - EU Supplier",
"landed_cost": 400.00, # EUR
"qty": 5,
"currency_id": 3, # EUR
})
# Cost from local supplier in THB
get_model("quot.cost").create({
"quot_id": quot_id,
"description": "Assembly - Local",
"landed_cost": 5000.00, # THB
"qty": 1,
"currency_id": 1, # THB
})
# All costs converted to quotation currency (THB)
quot = get_model("sale.quot").browse(quot_id)
print(f"Total Cost (THB): {quot.cost_amount:.2f}")
print(f"Profit (THB): {quot.profit_amount:.2f}")
print(f"Margin: {quot.margin_percent:.1f}%")
Use Case 6: Cost Transfer to Sales Order¶
# Create quotation with costs
quot_id = get_model("sale.quot").create({
"contact_id": 123,
"lines": [
("create", {
"product_id": 456,
"qty": 10,
"unit_price": 150.00,
})
]
})
# Add cost estimates
get_model("quot.cost").create({
"quot_id": quot_id,
"description": "Product cost",
"product_id": 456,
"landed_cost": 100.00,
"qty": 10,
})
# Convert quotation to sales order
quot = get_model("sale.quot").browse(quot_id)
order_id = quot.to_sale_order()
# Verify costs were transferred
order = get_model("sale.order").browse(order_id)
print(f"Order costs: {len(order.est_costs)}")
for cost in order.est_costs:
print(f" {cost.description}: ${cost.amount:.2f}")
# Costs can be adjusted after order creation if needed
Use Case 7: Merge Quotations with Costs¶
# When merging multiple quotations, costs are combined
quot_ids = [101, 102, 103]
# Merge quotations
new_quot_id = get_model("sale.quot").merge_quotations(quot_ids)
# All costs from source quotations are copied to merged quotation
new_quot = get_model("sale.quot").browse(new_quot_id)
print(f"Merged quotation has {len(new_quot.est_costs)} cost records")
# Sequence numbers are adjusted to match merged line items
for cost in new_quot.est_costs:
print(f"Seq {cost.sequence}: {cost.description} - ${cost.amount:.2f}")
Search Functions¶
Search by Quotation¶
# Find all costs for a specific quotation
condition = [["quot_id", "=", 123]]
cost_ids = get_model("quot.cost").search(condition)
Search by Product¶
# Find all costs for a specific product
condition = [["product_id", "=", 456]]
cost_ids = get_model("quot.cost").search(condition)
Search by Supplier¶
# Find all costs from a specific supplier
condition = [["supplier_id", "=", 789]]
cost_ids = get_model("quot.cost").search(condition)
Search by Description¶
# Description field is searchable - find costs by keyword
condition = [["description", "ilike", "%imported%"]]
cost_ids = get_model("quot.cost").search(condition)
Search Recent Quotation Costs¶
# Find costs for recent quotations
from datetime import datetime, timedelta
recent_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
quot_ids = get_model("sale.quot").search([["date", ">=", recent_date]])
cost_ids = get_model("quot.cost").search([["quot_id", "in", quot_ids]])
Computed Fields Functions¶
get_amount(ids, context)¶
Calculates the total cost amount for each cost record by multiplying quantity by landed cost. Returns dictionary mapping record ID to computed amount. Handles null values by treating them as zero. Identical implementation to sale.cost.get_amount().
Integration with onchange_cost_product¶
Automatic Cost Population¶
When a product is selected for a cost line, the system automatically populates cost fields from product data:
# From sale.quot.onchange_cost_product() method
def onchange_cost_product(self, context):
data = context["data"]
path = context["path"]
line = get_data_path(data, path, parent=True)
prod_id = line.get("product_id")
if prod_id:
prod = get_model("product").browse(prod_id)
line["description"] = prod.name
line["list_price"] = prod.purchase_price
line["purchase_price"] = prod.purchase_price
line["landed_cost"] = prod.landed_cost
line["qty"] = 1
line["uom_id"] = prod.uom_id.id
line["supplier_id"] = prod.suppliers[0].supplier_id.id if prod.suppliers else None
line["purchase_duty_percent"] = prod.purchase_duty_percent
line["purchase_ship_percent"] = prod.purchase_ship_percent
line["currency_id"] = prod.purchase_currency_id.id
return {"data": data}
Best Practices¶
1. Use Auto-Generate for Initial Estimates¶
# Good: Start with product defaults, then refine
quot = get_model("sale.quot").browse(123)
# Auto-generate costs from products
quot.create_est_costs()
# Then refine specific costs with actual quotes
for cost in quot.est_costs:
if cost.product_id.code == "WIDGET-A":
# Update with actual supplier quote
cost.write({
"supplier_id": 789,
"landed_cost": 92.50, # Better price negotiated
})
2. Regular Cost Review During Quoting¶
Review and update costs throughout the quoting process:
# Review cost estimates at different quote stages
def review_quotation_costs(quot_id):
quot = get_model("sale.quot").browse(quot_id)
print(f"=== Quotation {quot.number} Cost Review ===")
print(f"Customer: {quot.contact_id.name}")
print(f"\nLine Items:")
for line in quot.lines:
print(f" {line.description}: {line.qty} × ${line.unit_price:.2f} = ${line.amount:.2f}")
print(f"\nEstimated Costs:")
for cost in quot.est_costs:
print(f" {cost.description}: {cost.qty} × ${cost.landed_cost:.2f} = ${cost.amount:.2f}")
print(f"\nProfitability:")
print(f" Revenue: ${quot.amount_subtotal:.2f}")
print(f" Cost: ${quot.cost_amount:.2f}")
print(f" Profit: ${quot.profit_amount:.2f}")
print(f" Margin: {quot.margin_percent:.1f}%")
# Flag low margin quotations
if quot.margin_percent < 20:
print(f"\n⚠️ WARNING: Low margin! Consider adjusting pricing.")
return quot.margin_percent >= 20
# Use during quote approval process
if review_quotation_costs(123):
print("✓ Quote approved for sending")
else:
print("✗ Quote needs pricing adjustment")
3. Link Costs to Quotation Line Items¶
# Good: Track costs per line item using sequence
quot = get_model("sale.quot").browse(123)
for line in quot.lines:
if line.product_id:
prod = line.product_id
get_model("quot.cost").create({
"quot_id": quot.id,
"sequence": str(line.sequence), # Link to line
"description": f"Cost for {prod.name}",
"product_id": prod.id,
"supplier_id": prod.suppliers[0].supplier_id.id if prod.suppliers else None,
"landed_cost": prod.landed_cost,
"qty": line.qty,
"currency_id": prod.purchase_currency_id.id,
})
4. Document Cost Assumptions¶
# Good: Include detailed descriptions explaining cost basis
get_model("quot.cost").create({
"quot_id": 123,
"description": "Widget A - Quote from Supplier XYZ dated 2025-01-05, valid 30 days, MOQ 100 units, lead time 4 weeks",
"supplier_id": 789,
"landed_cost": 95.00,
"qty": 100,
})
# This helps when reviewing the quotation later
# and when converting to sales order
5. Use Multiple Cost Scenarios¶
# Create multiple cost scenarios for risk analysis
quot = get_model("sale.quot").browse(123)
scenarios = [
{"name": "Best Case", "cost": 90.00, "supplier": 101},
{"name": "Expected", "cost": 95.00, "supplier": 102},
{"name": "Worst Case", "cost": 105.00, "supplier": 103},
]
for scenario in scenarios:
get_model("quot.cost").create({
"quot_id": quot.id,
"description": f"Widget A - {scenario['name']} Scenario",
"supplier_id": scenario["supplier"],
"landed_cost": scenario["cost"],
"qty": 100,
})
# Calculate margins for each scenario
# Select most realistic scenario for final quote
6. Validate Before Converting to Order¶
# Good: Validate costs before converting quotation to order
def validate_quotation_costs(quot_id):
quot = get_model("sale.quot").browse(quot_id)
# Check if costs are defined
if not quot.est_costs:
print("⚠️ No costs defined - profitability unknown")
return False
# Check margin is acceptable
min_margin = 15 # Minimum 15% margin
if quot.margin_percent < min_margin:
print(f"⚠️ Margin too low: {quot.margin_percent:.1f}% (min: {min_margin}%)")
return False
# Check all costs have suppliers
for cost in quot.est_costs:
if not cost.supplier_id:
print(f"⚠️ Cost '{cost.description}' has no supplier")
return False
print("✓ Quotation costs validated")
return True
# Use before creating sales order
if validate_quotation_costs(123):
quot = get_model("sale.quot").browse(123)
order_id = quot.to_sale_order()
print(f"Order created: {order_id}")
else:
print("Fix cost issues before creating order")
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.quot |
Many2One (parent) | Parent quotation this cost belongs to |
sale.cost |
Parallel Model | Similar model for sales order costs |
product |
Many2One | Product this cost is for |
contact |
Many2One | Supplier providing this cost |
currency |
Many2One | Currency for cost amount |
uom |
Many2One | Unit of measure (deprecated) |
Quotation to Order Cost Transfer¶
How Costs Transfer¶
When converting a quotation to a sales order:
# From sale.quot.to_sale_order() method
def to_sale_order(self, ids, context={}):
obj = self.browse(ids[0])
# Create sales order
vals = {
"contact_id": obj.contact_id.id,
"date": obj.date,
"lines": [],
"est_costs": [], # Cost array
}
# Copy quotation lines
for line in obj.lines:
line_vals = {
"product_id": line.product_id.id,
"description": line.description,
"qty": line.qty,
"unit_price": line.unit_price,
# ... other fields
}
vals["lines"].append(("create", line_vals))
# Copy cost estimates
for cost in obj.est_costs:
cost_vals = {
"sequence": cost.sequence,
"product_id": cost.product_id.id,
"description": cost.description,
"supplier_id": cost.supplier_id.id,
"list_price": cost.list_price,
"purchase_price": cost.purchase_price,
"landed_cost": cost.landed_cost,
"qty": cost.qty,
"currency_id": cost.currency_id.id,
}
vals["est_costs"].append(("create", cost_vals))
# Create order
order_id = get_model("sale.order").create(vals, context=context)
return order_id
Post-Conversion Cost Management¶
After conversion, costs can diverge:
# Quotation costs remain unchanged
quot = get_model("sale.quot").browse(123)
print(f"Quote estimated cost: ${quot.cost_amount:.2f}")
# Order costs can be updated with actual supplier quotes
order = get_model("sale.order").browse(456) # Converted from quot 123
for cost in order.est_costs:
# Update with actual negotiated prices
if cost.supplier_id:
cost.write({"landed_cost": actual_negotiated_price})
print(f"Order actual cost: ${order.est_cost_amount:.2f}")
Performance Tips¶
1. Bulk Cost Creation¶
# When creating quotation with multiple costs
cost_data = [
{"description": "Part A", "landed_cost": 50.00, "qty": 10},
{"description": "Part B", "landed_cost": 75.00, "qty": 5},
{"description": "Part C", "landed_cost": 30.00, "qty": 20},
]
for data in cost_data:
get_model("quot.cost").create({
"quot_id": 123,
**data
})
2. Use create_est_costs() for Products¶
# Good: Use built-in method to auto-generate from products
quot = get_model("sale.quot").browse(123)
quot.create_est_costs() # Fast batch creation
# Better than manually creating for each product
3. Cache Product Cost Data¶
# When creating costs for many quotations, cache product data
products = {}
for prod in get_model("product").search_browse([]):
products[prod.id] = {
"landed_cost": prod.landed_cost,
"supplier_id": prod.suppliers[0].supplier_id.id if prod.suppliers else None,
"currency_id": prod.purchase_currency_id.id,
}
# Use cached data when creating costs
for quot_id in quotation_ids:
prod_data = products[product_id]
get_model("quot.cost").create({
"quot_id": quot_id,
**prod_data
})
Troubleshooting¶
"Required field missing: quot_id"¶
Cause: Attempting to create cost record without linking to a quotation Solution: Always provide a valid quot_id:
"Required field missing: description"¶
Cause: Attempting to create cost record without description Solution: Always provide a meaningful description:
get_model("quot.cost").create({
"quot_id": 123,
"description": "Widget A - Supplier XYZ", # Required
})
Costs Not Auto-Generating¶
Cause: Products may not have purchase_ok flag set or missing cost data
Solution: Verify product configuration:
prod = get_model("product").browse(456)
print(f"Purchase OK: {prod.purchase_ok}")
print(f"Landed Cost: {prod.landed_cost}")
print(f"Purchase Price: {prod.purchase_price}")
# Only products with purchase_ok=True are included in auto-generation
Margin Calculation Incorrect¶
Cause: Costs may be in different currency than quotation, or not all costs included Solution: Verify all costs and currency consistency:
quot = get_model("sale.quot").browse(123)
print(f"Quotation currency: {quot.currency_id.name}")
total_cost = 0
for cost in quot.est_costs:
print(f"Cost: {cost.description}")
print(f" Currency: {cost.currency_id.name if cost.currency_id else 'None'}")
print(f" Amount: {cost.amount}")
total_cost += cost.amount or 0
print(f"Total: {total_cost}")
Costs Not Transferred to Sales Order¶
Cause: Cost records may have been deleted or quotation not using standard conversion Solution: Verify costs exist before conversion:
quot = get_model("sale.quot").browse(123)
print(f"Number of cost records: {len(quot.est_costs)}")
if len(quot.est_costs) == 0:
# Regenerate costs before converting
quot.create_est_costs()
# Then convert to order
order_id = quot.to_sale_order()
Testing Examples¶
Unit Test: Auto-Generate Costs¶
def test_auto_generate_quotation_costs():
# Create product with cost data
prod_id = get_model("product").create({
"name": "Test Product",
"type": "stock",
"purchase_ok": True,
"landed_cost": 75.00,
"purchase_price": 70.00,
})
# Create quotation with product line
quot_id = get_model("sale.quot").create({
"contact_id": 1,
"lines": [
("create", {
"product_id": prod_id,
"qty": 10,
"unit_price": 100.00,
})
]
})
# Auto-generate costs
get_model("sale.quot").create_est_costs([quot_id])
# Verify cost was created
quot = get_model("sale.quot").browse(quot_id)
assert len(quot.est_costs) == 1
cost = quot.est_costs[0]
assert cost.product_id.id == prod_id
assert cost.landed_cost == 75.00
assert cost.qty == 10
assert cost.amount == 750.00
Unit Test: Cost Transfer to Sales Order¶
def test_quotation_cost_transfer():
# Create quotation with cost
quot_id = get_model("sale.quot").create({
"contact_id": 1,
"lines": [
("create", {
"description": "Test Item",
"qty": 5,
"unit_price": 100.00,
})
]
})
cost_id = get_model("quot.cost").create({
"quot_id": quot_id,
"description": "Test Cost",
"landed_cost": 60.00,
"qty": 5,
})
# Convert to sales order
quot = get_model("sale.quot").browse(quot_id)
order_id = quot.to_sale_order()
# Verify cost transferred
order = get_model("sale.order").browse(order_id)
assert len(order.est_costs) == 1
order_cost = order.est_costs[0]
assert order_cost.description == "Test Cost"
assert order_cost.landed_cost == 60.00
assert order_cost.qty == 5
assert order_cost.amount == 300.00
Security Considerations¶
Permission Model¶
- Create/write access should be restricted to sales staff and managers
- Read access should respect quotation permissions
- Cost data is commercially sensitive - protect from competitors
- Only authorized personnel should see supplier pricing
Data Access¶
- Users should only see costs for quotations they have access to
- Supplier pricing is confidential business information
- Cost estimates may affect pricing strategy - limit access
- Export/report access should be controlled
Version History¶
Last Updated: 2025-01-05 Model Version: quot_cost.py Framework: Netforce
Additional Resources¶
- Quotation Documentation:
sale.quot - Sales Order Cost Documentation:
sale.cost(parallel model for orders) - Sales Order Documentation:
sale.order(cost transfer target) - Product Documentation:
product(cost price and supplier information) - Currency Documentation:
currency(exchange rates and conversions)
Support & Feedback¶
For issues or questions about this module: 1. Check quotation profit fields to verify costs are being calculated 2. Review product cost configuration for auto-generation issues 3. Verify costs transfer correctly when converting to sales orders 4. Test cost scenarios in development environment before quoting customers
This documentation is generated for developer onboarding and reference purposes.