Skip to content

Sales Order Cost Tracking Documentation

Overview

The Sales Order Cost module (sale.cost) tracks estimated costs for sales orders to enable profitability analysis and margin calculations. This model allows businesses to track product/material costs, supplier pricing, and calculate expected profit margins before and after order fulfillment.


Model Information

Model Name: sale.cost Display Name: Cost Key Fields: None (child records linked to sales orders)

Features

  • ❌ Audit logging enabled (_audit_log)
  • ❌ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ✅ Cascade deletion when parent sales order is deleted
  • ✅ Searchable description field
  • ✅ Multi-currency support for international suppliers
  • ✅ Computed amount field based on quantity and landed cost

Understanding Sales Order Costs

What are Sales Order Costs?

Sales Order Costs represent the estimated costs associated with fulfilling a sales order. These costs are critical for:

  1. Profitability Analysis: Calculate estimated profit before order fulfillment
  2. Margin Calculations: Determine profit margins on sales
  3. Supplier Comparison: Compare costs from different suppliers
  4. Cost-Plus Pricing: Set prices based on cost plus desired margin
  5. Financial Planning: Forecast expenses and revenues

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


Key Fields Reference

Header Fields

Field Type Required Description
sale_id Many2One Link to parent sales order (sale.order)
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 sales order.

Parameters:

vals = {
    "sale_id": 123,                    # Required: Sales order 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("sale.cost").create({
    "sale_id": 123,
    "description": "Widget A - Supplier XYZ",
    "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
# When creating/updating:
cost = get_model("sale.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 Sales Orders

Estimated Profit Calculation

Sales orders use cost records to calculate estimated profitability:

# From sale.order model get_est_profit() method
def get_est_profit(self, ids, context={}):
    for obj in self.browse(ids):
        # Sum all cost amounts
        cost = sum([c.amount or 0 for c in obj.est_costs])

        # Calculate profit and margin
        profit = obj.amount_subtotal - cost
        margin = profit * 100 / obj.amount_subtotal if obj.amount_subtotal else None

        return {
            "est_cost_amount": cost,
            "est_profit_amount": profit,
            "est_margin_percent": margin
        }

Sales Order Fields Updated

Cost records automatically update these sales order fields: - est_cost_amount: Total of all cost amounts - est_profit_amount: Revenue minus estimated costs - est_margin_percent: Profit as percentage of revenue

Currency Conversion

When costs are in foreign currency, they are automatically converted to the sales order's currency:

# From sale.order get_profit_old() method
for cost in obj.costs:
    amt = cost.amount or 0
    if cost.currency_id:
        # Convert foreign currency to order currency
        amt = get_model("currency").convert(
            amt,
            cost.currency_id.id,
            obj.currency_id.id
        )
    est_cost_total += amt

Common Use Cases

Use Case 1: Track Simple Product Cost

# Single supplier, single currency, no import costs
cost_id = get_model("sale.cost").create({
    "sale_id": 123,
    "description": "Standard Widget A",
    "product_id": 456,
    "supplier_id": 789,
    "landed_cost": 50.00,
    "qty": 20,
    "sequence": "1"
})

# Result: amount = 1000.00 (20 × 50.00)

Use Case 2: Import Cost with Duty and Shipping

# Product imported with duty and shipping charges
cost_id = get_model("sale.cost").create({
    "sale_id": 123,
    "description": "Imported Component B",
    "product_id": 457,
    "supplier_id": 790,
    "list_price": 100.00,
    "purchase_price": 95.00,           # Negotiated price
    "purchase_duty_percent": 12.5,     # 12.5% import duty
    "purchase_ship_percent": 8.0,      # 8% shipping
    "landed_cost": 114.48,             # 95 + (95*0.125) + (95*0.08)
    "qty": 50,
    "currency_id": 2,                  # USD
    "sequence": "2"
})

# Result: amount = 5724.00 (50 × 114.48)

Use Case 3: Compare Multiple Supplier Costs

# Get sales order with costs
order = get_model("sale.order").browse(123)

# Add cost from Supplier A
get_model("sale.cost").create({
    "sale_id": order.id,
    "description": "Widget - Supplier A",
    "supplier_id": 101,
    "landed_cost": 50.00,
    "qty": 100,
})

# Add cost from Supplier B
get_model("sale.cost").create({
    "sale_id": order.id,
    "description": "Widget - Supplier B",
    "supplier_id": 102,
    "landed_cost": 48.00,
    "qty": 100,
})

# Compare costs
for cost in order.est_costs:
    print(f"{cost.supplier_id.name}: ${cost.amount:.2f}")

# Output:
# Supplier A: $5000.00
# Supplier B: $4800.00  <- Lower cost option

Use Case 4: Calculate Required Selling Price

# Cost-plus pricing strategy
target_margin = 25  # 25% profit margin

# Create cost record
cost_id = get_model("sale.cost").create({
    "sale_id": 123,
    "description": "Product costs",
    "landed_cost": 80.00,
    "qty": 10,
})

# Calculate: amount = 800.00
cost = get_model("sale.cost").browse(cost_id)
total_cost = cost.amount

# Calculate required selling price for 25% margin
# margin = (revenue - cost) / revenue
# 0.25 = (revenue - 800) / revenue
# revenue = 800 / 0.75 = 1066.67
required_revenue = total_cost / (1 - target_margin/100)
unit_price = required_revenue / cost.qty

print(f"Total Cost: ${total_cost:.2f}")
print(f"Required Revenue (25% margin): ${required_revenue:.2f}")
print(f"Required Unit Price: ${unit_price:.2f}")

# Output:
# Total Cost: $800.00
# Required Revenue (25% margin): $1066.67
# Required Unit Price: $106.67

Use Case 5: Track Cost by Line Item

# Track costs for each sales order line item
order = get_model("sale.order").browse(123)

for idx, line in enumerate(order.lines, 1):
    if line.product_id:
        prod = line.product_id
        get_model("sale.cost").create({
            "sale_id": order.id,
            "sequence": str(idx),              # Link to line item
            "description": f"{prod.name} - Line {idx}",
            "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,
        })

# Now cost breakdown matches sales order line items

Use Case 6: Multi-Currency Cost Analysis

# Order in THB, costs from multiple currencies
order = get_model("sale.order").browse(123)  # Currency: THB

# Cost in USD
get_model("sale.cost").create({
    "sale_id": order.id,
    "description": "US Supplier",
    "landed_cost": 100.00,  # USD
    "qty": 10,
    "currency_id": 2,        # USD
})

# Cost in EUR
get_model("sale.cost").create({
    "sale_id": order.id,
    "description": "EU Supplier",
    "landed_cost": 90.00,    # EUR
    "qty": 5,
    "currency_id": 3,        # EUR
})

# Cost in THB
get_model("sale.cost").create({
    "sale_id": order.id,
    "description": "Local Supplier",
    "landed_cost": 3000.00,  # THB
    "qty": 15,
    "currency_id": 1,        # THB
})

# All costs automatically converted to THB for profit calculation
print(f"Total Cost (THB): {order.est_cost_amount:.2f}")
print(f"Estimated Profit (THB): {order.est_profit_amount:.2f}")
print(f"Margin: {order.est_margin_percent:.1f}%")

Search Functions

Search by Sales Order

# Find all costs for a specific sales order
condition = [["sale_id", "=", 123]]
cost_ids = get_model("sale.cost").search(condition)

Search by Product

# Find all costs for a specific product
condition = [["product_id", "=", 456]]
cost_ids = get_model("sale.cost").search(condition)

Search by Supplier

# Find all costs from a specific supplier
condition = [["supplier_id", "=", 789]]
cost_ids = get_model("sale.cost").search(condition)

Search by Description

# Description field is searchable - find costs by keyword
condition = [["description", "ilike", "%imported%"]]
cost_ids = get_model("sale.cost").search(condition)

Search High-Cost Items

# Find costs exceeding threshold
# Note: amount is computed, so search after records are loaded
all_costs = get_model("sale.cost").search([])
high_costs = [c for c in get_model("sale.cost").browse(all_costs)
              if (c.amount or 0) > 10000]

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.


Best Practices

1. Accurate Cost Estimation

# Bad: Using list price without considering discounts and charges
get_model("sale.cost").create({
    "sale_id": 123,
    "landed_cost": 100.00,  # Just using list price
    "qty": 10
})

# Good: Include all cost components
list_price = 100.00
discount = 5.00  # 5% discount
duty = 10.0      # 10% duty
shipping = 5.0   # 5% shipping

purchase_price = list_price * (1 - discount/100)  # 95.00
landed_cost = purchase_price * (1 + (duty + shipping)/100)  # 109.25

get_model("sale.cost").create({
    "sale_id": 123,
    "list_price": list_price,
    "purchase_price": purchase_price,
    "purchase_duty_percent": duty,
    "purchase_ship_percent": shipping,
    "landed_cost": landed_cost,
    "qty": 10
})

2. Regular Cost Updates

Update cost records when: - Supplier prices change - Exchange rates fluctuate significantly - Import duty rates change - Shipping costs change - Better supplier pricing is negotiated

# Update cost when supplier price changes
order = get_model("sale.order").browse(123)
for cost in order.est_costs:
    if cost.supplier_id.id == 789:  # Specific supplier
        prod = cost.product_id
        # Get updated pricing from product
        cost.write({
            "purchase_price": prod.purchase_price,
            "landed_cost": prod.landed_cost,
        })

3. Multi-Supplier Cost Comparison

Always track costs from multiple suppliers to ensure best pricing:

# Create multiple cost records for comparison
suppliers = [
    (101, 50.00),
    (102, 48.00),
    (103, 52.00),
]

for supplier_id, unit_cost in suppliers:
    get_model("sale.cost").create({
        "sale_id": 123,
        "description": f"Quote from {get_model('contact').browse(supplier_id).name}",
        "supplier_id": supplier_id,
        "landed_cost": unit_cost,
        "qty": 100,
    })

# Select best option based on total cost
# Note: Only use one supplier's costs in final profit calculation

Use the sequence field to track which line item each cost applies to:

# Create costs that match sales order line items
order = get_model("sale.order").browse(123)

for line in order.lines:
    get_model("sale.cost").create({
        "sale_id": order.id,
        "sequence": str(line.sequence),  # Link to line item number
        "description": f"Cost for {line.product_id.name}",
        "product_id": line.product_id.id,
        "landed_cost": line.product_id.landed_cost,
        "qty": line.qty,
    })

5. Currency Consistency

When using foreign currencies, ensure currency is set correctly:

# Good: Specify currency for foreign suppliers
get_model("sale.cost").create({
    "sale_id": 123,
    "supplier_id": 789,  # US Supplier
    "landed_cost": 100.00,
    "currency_id": 2,    # USD currency ID
    "qty": 10
})

# System will automatically convert to order currency for profit calculation

Model Relationship Description
sale.order Many2One (parent) Parent sales order this cost belongs to
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)

Profit Analysis Integration

Sales Order Profit Fields

Cost records are used to calculate these sales order fields:

# Estimated Profit (using sale.cost records)
est_cost_amount = sum([c.amount for c in order.est_costs])
est_profit_amount = order.amount_subtotal - est_cost_amount
est_margin_percent = est_profit_amount * 100 / order.amount_subtotal

# Actual Profit (using actual costs from invoices/goods receipts)
act_cost_amount = sum([actual costs from system])
act_profit_amount = order.amount_subtotal - act_cost_amount
act_margin_percent = act_profit_amount * 100 / order.amount_subtotal

Profit Comparison

# Compare estimated vs actual profitability
order = get_model("sale.order").browse(123)

print("=== Profitability Analysis ===")
print(f"Revenue: ${order.amount_subtotal:.2f}")
print(f"\nEstimated:")
print(f"  Cost: ${order.est_cost_amount:.2f}")
print(f"  Profit: ${order.est_profit_amount:.2f}")
print(f"  Margin: {order.est_margin_percent:.1f}%")
print(f"\nActual:")
print(f"  Cost: ${order.act_cost_amount:.2f}")
print(f"  Profit: ${order.act_profit_amount:.2f}")
print(f"  Margin: {order.act_margin_percent:.1f}%")
print(f"\nVariance:")
cost_var = order.act_cost_amount - order.est_cost_amount
profit_var = order.act_profit_amount - order.est_profit_amount
print(f"  Cost Variance: ${cost_var:.2f}")
print(f"  Profit Variance: ${profit_var:.2f}")

Performance Tips

1. Bulk Cost Creation

When creating multiple costs, do it in a loop rather than individual operations:

# Good: Create multiple costs in sequence
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("sale.cost").create({
        "sale_id": 123,
        **data
    })

2. Minimize Currency Conversions

Group costs by currency when possible to reduce conversion overhead:

# Good: Group costs by currency
usd_costs = [costs in USD]
eur_costs = [costs in EUR]
thb_costs = [costs in THB]

# Process each currency group together

3. Use Product Defaults

Pre-populate cost data from product records to save time:

# Good: Use product cost information
prod = get_model("product").browse(456)

get_model("sale.cost").create({
    "sale_id": 123,
    "product_id": prod.id,
    "description": prod.name,
    "supplier_id": prod.suppliers[0].supplier_id.id if prod.suppliers else None,
    "landed_cost": prod.landed_cost,
    "purchase_price": prod.purchase_price,
    "purchase_duty_percent": prod.purchase_duty_percent,
    "purchase_ship_percent": prod.purchase_ship_percent,
    "currency_id": prod.purchase_currency_id.id,
    "qty": 10,
})

Troubleshooting

"Required field missing: sale_id"

Cause: Attempting to create cost record without linking to a sales order Solution: Always provide a valid sale_id when creating cost records:

get_model("sale.cost").create({
    "sale_id": 123,  # Required
    "description": "Cost description",
    # ... other fields
})

"Required field missing: description"

Cause: Attempting to create cost record without description Solution: Always provide a meaningful description:

get_model("sale.cost").create({
    "sale_id": 123,
    "description": "Widget A - Supplier XYZ",  # Required
    # ... other fields
})

Amount Not Calculating

Cause: Missing qty or landed_cost values Solution: Ensure both fields are set:

get_model("sale.cost").create({
    "sale_id": 123,
    "description": "Cost",
    "landed_cost": 50.00,  # Required for amount calculation
    "qty": 10,             # Required for amount calculation
})

Currency Conversion Errors

Cause: Invalid currency_id or missing exchange rates Solution: Verify currency exists and has exchange rates defined:

# Check currency exists
currency = get_model("currency").browse(currency_id)
if not currency:
    raise Exception("Invalid currency")

# Verify exchange rates are configured
# System admin should maintain currency exchange rates

Costs Not Showing in Profit Calculation

Cause: Costs may be linked to wrong sales order or deleted Solution: Verify cost records exist and are linked correctly:

order = get_model("sale.order").browse(123)
print(f"Number of cost records: {len(order.est_costs)}")
for cost in order.est_costs:
    print(f"  {cost.description}: {cost.amount}")


Testing Examples

Unit Test: Create and Calculate Cost Amount

def test_cost_amount_calculation():
    # Create sales order
    order_id = get_model("sale.order").create({
        "contact_id": 1,
        "date": "2025-01-05",
    })

    # Create cost record
    cost_id = get_model("sale.cost").create({
        "sale_id": order_id,
        "description": "Test Cost",
        "landed_cost": 50.00,
        "qty": 10,
    })

    # Verify amount calculation
    cost = get_model("sale.cost").browse(cost_id)
    assert cost.amount == 500.00, f"Expected 500.00, got {cost.amount}"

    # Update quantity
    cost.write({"qty": 20})

    # Verify amount recalculated
    cost = get_model("sale.cost").browse(cost_id)
    assert cost.amount == 1000.00, f"Expected 1000.00, got {cost.amount}"

Unit Test: Multi-Currency Cost Handling

def test_multi_currency_costs():
    # Create sales order in THB
    order_id = get_model("sale.order").create({
        "contact_id": 1,
        "date": "2025-01-05",
        "currency_id": 1,  # THB
    })

    # Create cost in USD
    cost_id = get_model("sale.cost").create({
        "sale_id": order_id,
        "description": "USD Cost",
        "landed_cost": 100.00,
        "qty": 10,
        "currency_id": 2,  # USD
    })

    # Verify cost amount in USD
    cost = get_model("sale.cost").browse(cost_id)
    assert cost.amount == 1000.00

    # Verify conversion to THB in order profit calculation
    order = get_model("sale.order").browse(order_id)
    # est_cost_amount should be in THB (converted from USD)
    assert order.est_cost_amount > 1000.00  # Should be higher in THB

Security Considerations

Permission Model

  • Create/write access should be restricted to sales managers and accounting staff
  • Read access can be broader but should respect sales order permissions
  • Cost data may be commercially sensitive - protect from unauthorized access

Data Access

  • Users should only see costs for sales orders they have permission to view
  • Supplier pricing information is confidential - limit access appropriately
  • Cost amounts directly impact profitability reports - ensure data integrity
  • Foreign currency costs require access to exchange rate data

Version History

Last Updated: 2025-01-05 Model Version: sale_cost.py Framework: Netforce


Additional Resources

  • Sales Order Documentation: sale.order
  • Quotation Cost Documentation: quot.cost (similar model for quotations)
  • 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 sales order profit fields to verify costs are being calculated 2. Review currency exchange rate configuration for multi-currency scenarios 3. Verify product cost data is up-to-date 4. Test cost calculations in development environment before production use


This documentation is generated for developer onboarding and reference purposes.