Skip to content

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:

  1. Pre-Sale Profitability Analysis: Calculate expected profit before creating a sales order
  2. Pricing Strategy: Determine if quoted prices provide adequate margin
  3. Supplier Selection: Compare costs from different suppliers during quoting
  4. Competitive Analysis: Understand cost structure when bidding on projects
  5. Cost-Plus Pricing: Build quotations based on cost plus desired margin
  6. 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

Cost Amount = Quantity × Landed Cost

Where:

Landed Cost = Purchase Price + (Purchase Price × Import Duty %) + (Purchase Price × Shipping %)


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:

amount = (qty or 0) × (landed_cost or 0)

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")

# 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")

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:

get_model("quot.cost").create({
    "quot_id": 123,  # Required
    "description": "Cost description",
})

"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.