Skip to content

Sale Order Line Documentation

Overview

The Sale Order Line module (sale.order.line) represents individual line items within a sales order. Each line contains product details, quantity, pricing, discounts, taxes, and calculated amounts. This is a child model of sale.order and handles all line-level calculations including amounts, discounts, promotions, taxes, profit margins, and delivered/invoiced quantities.


Model Information

Model Name: sale.order.line Display Name: Sale Order Line Name Field: order_id Ordering: sequence_no, id

Features

  • Multi-currency support (amounts calculated in order currency)
  • Automatic amount calculation with discounts and promotions
  • Tax calculation (tax inclusive and exclusive)
  • Profit and margin tracking (estimated and actual)
  • Quantity tracking (ordered, delivered, invoiced, produced)
  • Stock availability checking
  • UoM (Unit of Measure) support with primary and secondary units
  • Integration with inventory, invoicing, and production modules

Understanding Sale Order Lines

What are Sale Order Lines?

Sale order lines are the detail records that make up a sales order. While the sale.order header contains customer information, dates, and totals, the sale.order.line records contain:

  • Product Selection - What is being sold
  • Quantities - How much is being sold (with UoM support)
  • Pricing - Unit price and calculated amounts
  • Discounts - Both percentage and fixed amount discounts
  • Taxes - Tax rates and calculated tax amounts
  • Tracking - Delivered, invoiced, and produced quantities

Relationship with Sale Order

Each sale order line: - Belongs to exactly one sale.order (Many2One relationship) - Cascades on delete - Lines are automatically deleted when the order is deleted - Inherits tax type from the parent order (tax_ex or tax_in) - Shares currency with the parent order for amount calculations

# The parent relationship
"order_id": fields.Many2One("sale.order", "Sales Order",
    required=True,
    on_delete="cascade",
    search=True
)

Key Fields Reference

Product Information Fields

Field Type Required Description
product_id Many2One(product) No Product being sold (searchable)
description Text Yes Line description (auto-filled from product)
uom_id Many2One(uom) No Unit of measure
uom2_id Many2One(uom) No Secondary unit of measure
lot_id Many2One(stock.lot) No Lot or serial number
packaging_id Many2One(stock.packaging) No Packaging type

Quantity Fields

Field Type Description
qty Decimal Primary quantity ordered
qty2 Decimal Secondary quantity (for dual UoM products)
qty_stock Decimal Quantity in stock UoM
qty_delivered Decimal Quantity shipped/delivered (computed)
qty_invoiced Decimal Quantity invoiced (computed)
qty_produced Decimal Quantity produced (computed)
qty_avail Decimal Available quantity in stock (computed)

Pricing and Amount Fields

Field Type Scale Description
unit_price Decimal 6 Unit price per item
discount Decimal 2 Discount percentage (e.g., 10 for 10%)
discount_amount Decimal 2 Fixed discount amount
amount Decimal 2 Line total after discounts (stored, computed)
amount_cur Decimal 2 Amount in base currency (stored, computed)
amount_discount Decimal 2 Total discount applied (computed)
promotion_amount Decimal 2 Promotion discount applied (computed)

Tax Fields

Field Type Description
tax_id Many2One(account.tax.rate) Tax rate to apply
amount_tax Decimal Calculated tax amount
amount_incl_tax Decimal Amount including tax
amount_excl_tax Decimal Amount excluding tax

Profit and Cost Fields

Field Type Description
cost_price Decimal Unit cost price
cost_amount Decimal Total cost (cost_price * qty)
profit_amount Decimal Profit (amount - cost_amount)
margin_percent Decimal Profit margin percentage
est_cost_amount Float Estimated cost from quotation
est_profit_amount Float Estimated profit
est_margin_percent Float Estimated margin percentage
act_cost_amount Float Actual cost from tracking
act_profit_amount Float Actual profit (stored)
act_margin_percent Float Actual margin percentage

Warehouse and Logistics Fields

Field Type Description
location_id Many2One(stock.location) Warehouse location (internal only)
reserve_location_id Many2One(stock.location) Reservation location
ship_method_id Many2One(ship.method) Shipping method
ship_address_id Many2One(address) Shipping address
ship_tracking Char Tracking numbers (computed)
delivery_slot_id Many2One(delivery.slot) Delivery time slot
due_date Date Line due date

Organization Fields

Field Type Description
sequence_no Integer Line item number (for ordering)
index Integer Display index (computed from position)
type Selection Line type: "item" or "group"
remark Char Short remark
remarks Text Long remarks/notes
notes Text Additional notes
Field Type Description
contact_id Many2One(contact) Customer (from order)
date Date Order date (from order)
user_id Many2One(base.user) Sales owner (from order)
state Selection Order state (from order)

Tracking and Analytics Fields

Field Type Description
track_id Many2One(account.track.categ) Tracking dimension 1
track2_id Many2One(account.track.categ) Tracking dimension 2
production_id Many2One(production.order) Linked production order
supplier_id Many2One(contact) Supplier (for drop shipping)

Industry-Specific Fields

Field Type Description
size Text Product size specification
roll_carton Text Roll or carton specification
packing_style_id Many2One(packing.style) Packing style
papercore_id Many2One(papercore) Papercore specification
gross_weight Decimal(2) Gross weight per unit
total_weight Decimal(2) Total weight for line
addons Many2Many(product.addon) Product addons/accessories

Aggregation Fields

Field Type Description
agg_amount Decimal Sum of all line amounts
agg_qty Decimal Sum of all order quantities
agg_act_profit Decimal Sum of actual profit

API Methods

1. Create Record

Method: create(vals, context)

Creates a new sale order line. The create method automatically triggers amount calculations and stores computed fields.

Parameters:

vals = {
    "order_id": 123,                    # Required: Parent sale order ID
    "product_id": 456,                  # Optional but recommended: Product ID
    "description": "Product Name",      # Required: Line description
    "qty": 10.0,                        # Optional: Quantity
    "uom_id": 1,                        # Optional: Unit of measure
    "unit_price": 100.00,               # Optional: Unit price
    "discount": 5.0,                    # Optional: Discount percentage
    "discount_amount": 50.00,           # Optional: Fixed discount amount
    "tax_id": 7,                        # Optional: Tax rate ID
    "location_id": 8,                   # Optional: Warehouse location
    "sequence_no": 10                   # Optional: Line sequence number
}

Returns: int - New line ID

Example:

# Create a simple order line
line_id = get_model("sale.order.line").create({
    "order_id": 123,
    "product_id": 456,
    "description": "Widget Model X",
    "qty": 5.0,
    "uom_id": 1,
    "unit_price": 250.00,
    "tax_id": 7
})

# Create line with discount
line_id = get_model("sale.order.line").create({
    "order_id": 123,
    "product_id": 789,
    "description": "Premium Service Package",
    "qty": 1.0,
    "unit_price": 1000.00,
    "discount": 10.0,  # 10% discount
    "tax_id": 7,
    "sequence_no": 20
})

Behavior: - Automatically calls function_store([id]) to calculate amounts - Amount calculation considers qty, unit_price, discounts, and promotions - Currency conversion happens if order currency differs from base currency


2. Write (Update) Record

Method: write(ids, vals, context)

Updates existing sale order line(s). Automatically recalculates amounts when pricing fields change.

Parameters:

vals = {
    "qty": 15.0,                        # Update quantity
    "unit_price": 95.00,                # Update price
    "discount": 7.5,                    # Update discount percentage
    "description": "Updated description"
}

Example:

# Update quantity and price
get_model("sale.order.line").write([line_id], {
    "qty": 20.0,
    "unit_price": 90.00
})

# Update discount
get_model("sale.order.line").write([line_id], {
    "discount": 15.0  # Change to 15% discount
})

Behavior: - Calls function_store(ids) to recalculate all computed fields - Updates parent order totals (handled by sale.order model) - Recalculates tax amounts if tax_id or amount changes


3. Amount Calculation

Method: get_amount(ids, context)

Calculates line amounts including discounts and promotions. This is a multi-function that returns amount, amount_cur, amount_discount, and promotion_amount.

Calculation Logic:

# Base calculation
amount = qty * unit_price

# Apply percentage discount
if discount:
    discount_amt = amount * (discount / 100)
    amount -= discount_amt

# Apply fixed discount amount
if discount_amount:
    amount -= discount_amount

# Apply promotions from order
if promotions exist:
    promotion_amt = calculate_promotion_share()
    amount -= promotion_amt

# Convert to base currency
amount_cur = convert_currency(amount, order_currency, base_currency)

Special Handling: - Products with sale_use_qty2_for_amount flag use qty2 instead of qty - Promotion amounts are distributed proportionally across matching products - Promotion percentages can apply to specific products or all products (product_id = None)

Returns: Dictionary with computed values:

{
    line_id: {
        "amount": 950.00,           # Final line amount
        "amount_cur": 950.00,       # Amount in base currency
        "amount_discount": 50.00,   # Total discount applied
        "promotion_amount": 0.00    # Promotion discount
    }
}


4. Quantity Tracking Methods

4.1 Get Delivered Quantity

Method: get_qty_delivered(ids, context)

Calculates the delivered (shipped) quantity based on stock moves linked to the sales order.

Behavior: - Iterates through all stock moves in state "done" - Excludes expanded picking moves (expand_picking_id) - Matches product and location (reserve_location_id or location_id) - Distributes delivered quantities across lines - Handles cases where delivered qty exceeds ordered qty

Example:

line = get_model("sale.order.line").browse(line_id)
print(f"Ordered: {line.qty}, Delivered: {line.qty_delivered}")
# Output: Ordered: 10.0, Delivered: 8.0

4.2 Get Invoiced Quantity

Method: get_qty_invoiced(ids, context)

Calculates the invoiced quantity based on invoice lines linked to the sales order.

Behavior: - Considers invoices in states: draft, waiting_payment, paid - Matches products between order lines and invoice lines - Distributes invoiced quantities proportionally - Handles partial invoicing scenarios

Example:

line = get_model("sale.order.line").browse(line_id)
print(f"Ordered: {line.qty}, Invoiced: {line.qty_invoiced}")
# Output: Ordered: 10.0, Invoiced: 10.0

4.3 Get Produced Quantity

Method: get_qty_produced(ids, context)

Calculates quantity produced through linked production orders.

Behavior: - Sums qty_received from production orders linked to the sale order - Matches product_id between line and production order - Useful for make-to-order scenarios

Example:

line = get_model("sale.order.line").browse(line_id)
print(f"Ordered: {line.qty}, Produced: {line.qty_produced}")
# Output: Ordered: 50.0, Produced: 45.0


5. Stock Availability

Method: get_qty_avail(ids, context)

Retrieves available stock quantity for the product at the specified location.

Behavior: - Requires both product_id and location_id to be set - Calls stock.location.compute_balance() for real-time stock levels - Returns None if product or location not specified

Example:

# Check stock before confirming order
line = get_model("sale.order.line").browse(line_id)
if line.qty_avail and line.qty_avail < line.qty:
    print(f"Warning: Only {line.qty_avail} units available, {line.qty} ordered")


6. Profit and Margin Calculations

6.1 Simple Profit Calculation

Method: get_profit(ids, context)

Calculates profit based on cost_price field (manual or product cost).

Formula:

cost_amount = cost_price * qty
profit_amount = amount - cost_amount
margin_percent = (profit_amount / amount) * 100 if amount else None

Returns:

{
    line_id: {
        "cost_amount": 500.00,
        "profit_amount": 450.00,
        "margin_percent": 47.37
    }
}

6.2 Estimated Profit (from Quotation)

Method: get_est_profit(ids, context)

Calculates estimated profit based on cost estimates from quotation stage.

Behavior: - Reads estimated costs from parent order's est_costs lines - Matches costs by sequence number - Converts currency if cost is in different currency - Used during quotation and early order stages

6.3 Actual Profit (from Tracking)

Method: get_act_profit(ids, context)

Calculates actual profit based on tracked expenses.

Behavior: - Reads actual costs from account tracking entries - Matches by tracking code: "ORDER_NUMBER / SEQUENCE" - Subtracts tracked amounts from line amount - Stored in database for reporting

Example:

line = get_model("sale.order.line").browse(line_id)
print(f"Amount: {line.amount}")
print(f"Estimated Profit: {line.est_profit_amount} ({line.est_margin_percent}%)")
print(f"Actual Profit: {line.act_profit_amount} ({line.act_margin_percent}%)")


7. Tax Calculation

Method: get_tax_amount(ids, context)

Calculates tax amounts based on tax rate and order tax type (tax_ex or tax_in).

Tax Type Handling:

Tax Exclusive (tax_ex):

amount_excl = line.amount
tax_amount = compute_taxes(tax_id, amount_excl)
amount_incl = amount_excl + tax_amount

Tax Inclusive (tax_in):

amount_incl = line.amount
tax_amount = compute_taxes(tax_id, amount_incl)
amount_excl = amount_incl - tax_amount

Returns:

{
    line_id: {
        "amount_tax": 70.00,
        "amount_incl_tax": 1070.00,
        "amount_excl_tax": 1000.00
    }
}

Example:

# Tax exclusive order with 7% VAT
line.amount = 1000.00
line.tax_id = VAT_7_percent
# Result: amount_tax=70.00, amount_incl_tax=1070.00, amount_excl_tax=1000.00

# Tax inclusive order with 7% VAT
line.amount = 1070.00
line.tax_id = VAT_7_percent
# Result: amount_tax=70.00, amount_incl_tax=1070.00, amount_excl_tax=1000.00


8. Helper Methods

8.1 View Sale Order

Method: view_sale(ids, context)

Navigation helper to view the parent sale order from a line.

Returns:

{
    "next": {
        "name": "sale",
        "mode": "form",
        "active_id": order_id
    }
}

Usage: Typically called from UI action buttons

8.2 Get Shipping Tracking

Method: get_ship_tracking(ids, context)

Retrieves tracking numbers for shipments matching the line's due date.

Behavior: - Matches pickings by due_date - Concatenates multiple tracking numbers with ", " - Returns empty string if no due_date or no matching pickings

8.3 Get Index

Method: get_index(ids, context)

Calculates the display position index of each line within its order.

Returns: 1-based index (first line = 1, second line = 2, etc.)


Computed Fields Functions

get_amount(ids, context)

Multi-function that calculates: - amount - Net line total after all discounts - amount_cur - Line total in base currency - amount_discount - Total discount amount applied - promotion_amount - Promotion discount amount

Considers: qty, qty2, unit_price, discount (%), discount_amount, and order promotions.

get_qty_delivered(ids, context)

Returns quantity delivered/shipped based on completed stock moves. Matches product and location, handles multiple deliveries.

get_qty_invoiced(ids, context)

Returns quantity invoiced based on linked invoice lines in valid states (draft, waiting_payment, paid).

get_qty_produced(ids, context)

Returns quantity produced through linked production orders. Useful for make-to-order manufacturing.

get_qty_avail(ids, context)

Returns available stock quantity at the specified location. Requires both product_id and location_id.

get_profit(ids, context)

Multi-function calculating simple profit: - cost_amount = cost_price * qty - profit_amount = amount - cost_amount - margin_percent = (profit / amount) * 100

get_est_profit(ids, context)

Multi-function for estimated profit from quotation costs: - est_cost_amount - Estimated costs - est_profit_amount - Estimated profit - est_margin_percent - Estimated margin

get_act_profit(ids, context)

Multi-function for actual profit from tracked costs: - act_cost_amount - Actual tracked costs - act_profit_amount - Actual profit (stored) - act_margin_percent - Actual margin

get_tax_amount(ids, context)

Multi-function calculating tax amounts: - amount_tax - Tax amount - amount_incl_tax - Amount including tax - amount_excl_tax - Amount excluding tax

Respects order's tax_type (tax_ex or tax_in).

get_ship_tracking(ids, context)

Returns shipping tracking numbers from pickings matching the line's due_date.

get_index(ids, context)

Returns the 1-based position index of the line within its order.

_get_related(ids, context)

Generic function to get related fields from parent order (contact_id, date, user_id, state) or product (categs, categ_id).


Model Relationship Description
sale.order Many2One (parent) Parent sales order - cascade delete
product Many2One Product being sold
uom Many2One Unit of measure for quantity
account.tax.rate Many2One Tax rate applied to line
stock.location Many2One Warehouse location for inventory
contact Many2One (related) Customer from parent order
stock.move Computed Stock movements for delivery tracking
invoice.line Computed Invoice lines for billing tracking
production.order Many2One Linked production order
product.categ Many2Many (related) Product categories
account.track.categ Many2One Tracking dimensions for analytics
stock.lot Many2One Lot/serial number for traceability
ship.method Many2One Shipping method
address Many2One Shipping address
delivery.slot Many2One Delivery time slot

Common Use Cases

Use Case 1: Creating Order Lines with Product Selection

# Scenario: Adding products to a sales order with pricing and discounts

# 1. Get or create a sale order
order_id = get_model("sale.order").search([["number", "=", "SO-2026-001"]])
if not order_id:
    order_id = get_model("sale.order").create({
        "contact_id": 123,
        "date": "2026-01-05",
        "tax_type": "tax_ex"
    })
else:
    order_id = order_id[0]

# 2. Add first product line
line1_id = get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": 456,  # Product: "Laptop Model X"
    "description": "Laptop Model X - Core i7, 16GB RAM",
    "qty": 5.0,
    "uom_id": 1,  # UoM: Unit
    "unit_price": 1200.00,
    "tax_id": 7,  # VAT 7%
    "location_id": 10,  # Main Warehouse
    "sequence_no": 10
})

# 3. Add second product line with discount
line2_id = get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": 789,
    "description": "Wireless Mouse",
    "qty": 5.0,
    "uom_id": 1,
    "unit_price": 50.00,
    "discount": 10.0,  # 10% discount for bulk purchase
    "tax_id": 7,
    "location_id": 10,
    "sequence_no": 20
})

# 4. Add service line with fixed discount amount
line3_id = get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": 101,
    "description": "Extended Warranty - 3 Years",
    "qty": 1.0,
    "unit_price": 500.00,
    "discount_amount": 100.00,  # $100 off promotion
    "tax_id": 7,
    "sequence_no": 30
})

# 5. Review line totals
for line_id in [line1_id, line2_id, line3_id]:
    line = get_model("sale.order.line").browse(line_id)
    print(f"Line {line.sequence_no}: {line.description}")
    print(f"  Qty: {line.qty}, Price: {line.unit_price}")
    print(f"  Discount: {line.amount_discount}")
    print(f"  Amount: {line.amount}")
    print(f"  Tax: {line.amount_tax}")
    print(f"  Total Inc Tax: {line.amount_incl_tax}")

Use Case 2: Tracking Order Fulfillment Status

# Scenario: Monitor which lines are delivered, invoiced, and what's pending

# Get all lines for an order
order = get_model("sale.order").browse(order_id)

print(f"Order: {order.number}")
print(f"Customer: {order.contact_id.name}\n")

for line in order.lines:
    print(f"Product: {line.description}")
    print(f"  Ordered Qty: {line.qty}")
    print(f"  Available in Stock: {line.qty_avail}")
    print(f"  Delivered Qty: {line.qty_delivered}")
    print(f"  Invoiced Qty: {line.qty_invoiced}")

    # Check fulfillment status
    if line.qty_delivered >= line.qty:
        delivery_status = "Fully Delivered"
    elif line.qty_delivered > 0:
        delivery_status = f"Partially Delivered ({line.qty_delivered}/{line.qty})"
    else:
        delivery_status = "Not Delivered"

    if line.qty_invoiced >= line.qty:
        invoice_status = "Fully Invoiced"
    elif line.qty_invoiced > 0:
        invoice_status = f"Partially Invoiced ({line.qty_invoiced}/{line.qty})"
    else:
        invoice_status = "Not Invoiced"

    print(f"  Delivery Status: {delivery_status}")
    print(f"  Invoice Status: {invoice_status}")

    # Check stock availability
    if line.qty_avail is not None and line.qty_avail < (line.qty - line.qty_delivered):
        shortage = (line.qty - line.qty_delivered) - line.qty_avail
        print(f"  WARNING: Stock shortage of {shortage} units!")

    print()

Use Case 3: Analyzing Profitability by Order Line

# Scenario: Review profit margins and identify most profitable products

# Get order lines with profit data
order = get_model("sale.order").browse(order_id)

print(f"Profitability Analysis: {order.number}\n")

total_revenue = 0
total_cost = 0
total_profit = 0

for line in order.lines:
    if not line.product_id:
        continue  # Skip non-product lines

    print(f"Product: {line.description}")
    print(f"  Quantity: {line.qty}")
    print(f"  Unit Price: ${line.unit_price}")
    print(f"  Line Amount: ${line.amount}")

    # Show different profit calculations
    if line.cost_price:
        print(f"\n  Simple Profit Calculation:")
        print(f"    Cost Price: ${line.cost_price}")
        print(f"    Cost Amount: ${line.cost_amount}")
        print(f"    Profit: ${line.profit_amount}")
        print(f"    Margin: {line.margin_percent}%")

    if line.est_profit_amount:
        print(f"\n  Estimated Profit (from quotation):")
        print(f"    Est. Cost: ${line.est_cost_amount}")
        print(f"    Est. Profit: ${line.est_profit_amount}")
        print(f"    Est. Margin: {line.est_margin_percent}%")

    if line.act_profit_amount:
        print(f"\n  Actual Profit (tracked):")
        print(f"    Act. Cost: ${line.act_cost_amount}")
        print(f"    Act. Profit: ${line.act_profit_amount}")
        print(f"    Act. Margin: {line.act_margin_percent}%")

    # Accumulate totals
    total_revenue += line.amount or 0
    if line.cost_amount:
        total_cost += line.cost_amount
        total_profit += line.profit_amount or 0

    print()

# Summary
if total_cost:
    overall_margin = (total_profit / total_revenue * 100) if total_revenue else 0
    print(f"Overall Summary:")
    print(f"  Total Revenue: ${total_revenue}")
    print(f"  Total Cost: ${total_cost}")
    print(f"  Total Profit: ${total_profit}")
    print(f"  Average Margin: {overall_margin:.2f}%")

Best Practices

1. Always Set Sequence Numbers

# Bad: Lines without sequence numbers may display in wrong order
get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": 123,
    "description": "Product A",
    "qty": 1.0,
    "unit_price": 100.00
    # Missing sequence_no - order unpredictable
})

# Good: Explicitly set sequence numbers
lines = [
    {"product_id": 123, "description": "Product A", "qty": 1, "price": 100, "seq": 10},
    {"product_id": 456, "description": "Product B", "qty": 2, "price": 200, "seq": 20},
    {"product_id": 789, "description": "Product C", "qty": 1, "price": 150, "seq": 30}
]

for line_data in lines:
    get_model("sale.order.line").create({
        "order_id": order_id,
        "product_id": line_data["product_id"],
        "description": line_data["description"],
        "qty": line_data["qty"],
        "unit_price": line_data["price"],
        "sequence_no": line_data["seq"]
    })

2. Use Appropriate Discount Fields

# Scenario: Applying discounts correctly

# Use discount (percentage) for standard discounts
get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": 123,
    "qty": 10.0,
    "unit_price": 100.00,
    "discount": 15.0  # 15% off - good for standard discounts
})
# Result: amount = (10 * 100) - (1000 * 0.15) = $850

# Use discount_amount for fixed discounts
get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": 456,
    "qty": 1.0,
    "unit_price": 500.00,
    "discount_amount": 50.00  # $50 off - good for promotions
})
# Result: amount = (1 * 500) - 50 = $450

# Both can be combined
get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": 789,
    "qty": 5.0,
    "unit_price": 200.00,
    "discount": 10.0,          # 10% off
    "discount_amount": 25.00   # Plus $25 off
})
# Result: amount = (5 * 200) - (1000 * 0.10) - 25 = $875

# Bad: Using discount for small fixed amounts
get_model("sale.order.line").create({
    "order_id": order_id,
    "qty": 1.0,
    "unit_price": 1000.00,
    "discount": 5.0  # Should use discount_amount: 50.00 instead
})

3. Check Stock Availability Before Confirming

# Good: Check stock before order confirmation
def validate_stock_availability(order_id):
    order = get_model("sale.order").browse(order_id)

    issues = []
    for line in order.lines:
        if not line.product_id:
            continue

        if not line.location_id:
            issues.append(f"Line {line.sequence_no}: No location specified")
            continue

        if line.qty_avail is None:
            continue  # Product doesn't track inventory

        required_qty = line.qty - (line.qty_delivered or 0)
        if line.qty_avail < required_qty:
            shortage = required_qty - line.qty_avail
            issues.append(
                f"Line {line.sequence_no} ({line.description}): "
                f"Shortage of {shortage} units "
                f"(available: {line.qty_avail}, required: {required_qty})"
            )

    if issues:
        print("Stock Availability Issues:")
        for issue in issues:
            print(f"  - {issue}")
        return False

    return True

# Use before confirming order
if validate_stock_availability(order_id):
    get_model("sale.order").confirm([order_id])
else:
    print("Cannot confirm order - resolve stock issues first")

4. Handle Multi-UoM Products Correctly

# For products with dual UoM (e.g., sold by box but tracked by piece)

# Get product UoM info
product = get_model("product").browse(product_id)

# Create line with both quantities
line_id = get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": product_id,
    "description": product.name,
    "qty": 10.0,              # 10 boxes
    "uom_id": box_uom_id,     # UoM: Box
    "qty2": 120.0,            # 120 pieces (10 boxes * 12 pieces/box)
    "uom2_id": piece_uom_id,  # UoM2: Piece
    "unit_price": 100.00      # $100 per box
})

# For products with sale_use_qty2_for_amount flag
if product.sale_use_qty2_for_amount:
    # Amount will be calculated as: qty2 * unit_price
    # Instead of: qty * unit_price
    print(f"Amount based on qty2: {line.qty2} * {line.unit_price} = {line.amount}")

5. Set Cost Price for Profit Tracking

# Good: Set cost_price when creating lines for immediate profit visibility
product = get_model("product").browse(product_id)

line_id = get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": product_id,
    "description": product.name,
    "qty": 5.0,
    "unit_price": 200.00,
    "cost_price": product.cost_price or 120.00,  # Get from product or specify
    "tax_id": 7
})

# Now profit fields are immediately available
line = get_model("sale.order.line").browse(line_id)
print(f"Revenue: ${line.amount}")
print(f"Cost: ${line.cost_amount}")
print(f"Profit: ${line.profit_amount}")
print(f"Margin: {line.margin_percent}%")

# Bad: Not setting cost_price
line_id = get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": product_id,
    "qty": 5.0,
    "unit_price": 200.00
    # cost_price not set - profit_amount will be None
})

6. Use Tracking Dimensions for Analytics

# Set tracking dimensions for better reporting and cost allocation

# Example: Track by department and project
line_id = get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": product_id,
    "description": "Custom Development Services",
    "qty": 100.0,  # Hours
    "unit_price": 150.00,  # Per hour
    "track_id": sales_dept_track_id,      # Track-1: Sales Department
    "track2_id": project_alpha_track_id,  # Track-2: Project Alpha
})

# Later, costs tracked against this line can be analyzed by dimension
# Reports can show profitability by department and project

Performance Tips

1. Batch Operations for Multiple Lines

When creating multiple lines, create them individually but avoid unnecessary browse() calls:

# Bad: Browsing after each create
for product in products:
    line_id = get_model("sale.order.line").create({...})
    line = get_model("sale.order.line").browse(line_id)  # Unnecessary
    print(line.amount)  # Don't need to browse immediately

# Good: Create all lines first, then browse if needed
line_ids = []
for product in products:
    line_id = get_model("sale.order.line").create({...})
    line_ids.append(line_id)

# Only browse if you need to read the data
if need_to_read_data:
    lines = get_model("sale.order.line").browse(line_ids)
    for line in lines:
        print(line.amount)

2. Use Search with Specific Conditions

Be specific when searching to reduce database load:

# Bad: Getting all lines then filtering in Python
all_lines = get_model("sale.order.line").search([])
high_value_lines = [l for l in get_model("sale.order.line").browse(all_lines)
                    if l.amount > 1000]

# Good: Search with conditions
high_value_line_ids = get_model("sale.order.line").search([
    ["amount", ">", 1000],
    ["state", "=", "confirmed"]
])

3. Avoid Redundant Function Store Calls

The create() and write() methods already call function_store(), don't call it again:

# Bad: Redundant function_store call
line_id = get_model("sale.order.line").create({...})
get_model("sale.order.line").function_store([line_id])  # Already called in create()

# Good: Trust the built-in behavior
line_id = get_model("sale.order.line").create({...})
# Amounts are already calculated

Troubleshooting

"Key error: amount"

Cause: Trying to access amount field immediately after create without the function_store completing Solution: The create method already calls function_store. If accessing in same transaction, refresh the browse:

line_id = get_model("sale.order.line").create({...})
line = get_model("sale.order.line").browse(line_id)
# line.amount is now available

"Negative amount calculated"

Cause: Discount percentage or discount_amount exceeds the line subtotal (qty * unit_price) Solution: Validate discounts before creating lines:

subtotal = qty * unit_price
if discount:
    discount_amt = subtotal * discount / 100
else:
    discount_amt = 0
if discount_amount:
    discount_amt += discount_amount

if discount_amt >= subtotal:
    raise Exception(f"Total discount ({discount_amt}) exceeds line subtotal ({subtotal})")

"qty_delivered shows incorrect value"

Cause: Multiple lines with same product and location, delivery quantities distributed incorrectly Solution: Use reserve_location_id to distinguish lines, or ensure clear product-location mapping:

# Specify reserve_location_id for each line
line1_id = get_model("sale.order.line").create({
    "order_id": order_id,
    "product_id": 123,
    "qty": 10.0,
    "location_id": main_warehouse_id,
    "reserve_location_id": shelf_a_location_id  # Specific reservation
})

"Tax amount not calculating"

Cause: Order tax_type is "no_tax" or tax_id not set on line Solution: Ensure parent order has tax_type set to "tax_ex" or "tax_in" and line has tax_id:

# Check order tax type
order = get_model("sale.order").browse(order_id)
if order.tax_type == "no_tax":
    # Update order tax type first
    get_model("sale.order").write([order_id], {"tax_type": "tax_ex"})

# Set tax_id on line
get_model("sale.order.line").write([line_id], {
    "tax_id": default_tax_id
})

"Profit amounts are None"

Cause: cost_price not set on the line Solution: Set cost_price explicitly or from product:

product = get_model("product").browse(product_id)
get_model("sale.order.line").write([line_id], {
    "cost_price": product.cost_price
})
# Now profit_amount, cost_amount, and margin_percent will calculate

"qty_avail returns None"

Cause: Either product_id or location_id not set on the line Solution: Ensure both fields are populated:

line = get_model("sale.order.line").browse(line_id)
if not line.product_id:
    print("Product not specified")
if not line.location_id:
    print("Location not specified")
    # Set location
    get_model("sale.order.line").write([line_id], {
        "location_id": default_location_id
    })


Testing Examples

Unit Test: Create Line and Verify Amounts

def test_line_amount_calculation():
    # Create a test order
    order_id = get_model("sale.order").create({
        "contact_id": test_customer_id,
        "date": "2026-01-05",
        "tax_type": "tax_ex"
    })

    # Create line with discount
    line_id = get_model("sale.order.line").create({
        "order_id": order_id,
        "product_id": test_product_id,
        "description": "Test Product",
        "qty": 10.0,
        "unit_price": 100.00,
        "discount": 10.0,  # 10% discount
        "tax_id": vat_7_percent_id
    })

    # Verify amount calculation
    line = get_model("sale.order.line").browse(line_id)

    expected_subtotal = 10.0 * 100.00  # 1000.00
    expected_discount = 1000.00 * 0.10  # 100.00
    expected_amount = 1000.00 - 100.00  # 900.00

    assert line.amount == expected_amount, \
        f"Expected amount {expected_amount}, got {line.amount}"
    assert line.amount_discount == expected_discount, \
        f"Expected discount {expected_discount}, got {line.amount_discount}"

    # Verify tax calculation (7% on 900)
    expected_tax = 900.00 * 0.07  # 63.00
    assert line.amount_tax == expected_tax, \
        f"Expected tax {expected_tax}, got {line.amount_tax}"
    assert line.amount_incl_tax == 963.00, \
        f"Expected total {963.00}, got {line.amount_incl_tax}"

    # Cleanup
    get_model("sale.order").delete([order_id])

Integration Test: Order Fulfillment Tracking

def test_qty_tracking():
    # Create order with line
    order_id = get_model("sale.order").create({
        "contact_id": test_customer_id,
        "date": "2026-01-05"
    })

    line_id = get_model("sale.order.line").create({
        "order_id": order_id,
        "product_id": test_product_id,
        "description": "Test Product",
        "qty": 20.0,
        "unit_price": 50.00,
        "location_id": test_location_id
    })

    line = get_model("sale.order.line").browse(line_id)

    # Initially no delivery
    assert line.qty_delivered == 0, "Initial qty_delivered should be 0"

    # Confirm order
    get_model("sale.order").confirm([order_id])

    # Create and validate picking
    picking_ids = get_model("stock.picking").search([
        ["related_id", "=", f"sale.order,{order_id}"]
    ])
    assert len(picking_ids) > 0, "Picking should be created"

    # Set picking to done with partial quantity
    picking = get_model("stock.picking").browse(picking_ids[0])
    for move in picking.lines:
        get_model("stock.move").write([move.id], {"qty": 15.0})  # Partial delivery

    get_model("stock.picking").set_done(picking_ids)

    # Verify qty_delivered updated
    line = get_model("sale.order.line").browse(line_id)
    assert line.qty_delivered == 15.0, \
        f"Expected qty_delivered=15.0, got {line.qty_delivered}"

    # Create invoice
    invoice_id = get_model("sale.order").make_invoice([order_id])[0]
    invoice = get_model("account.invoice").browse(invoice_id)

    # Verify qty_invoiced
    line = get_model("sale.order.line").browse(line_id)
    assert line.qty_invoiced == 20.0, \
        f"Expected qty_invoiced=20.0, got {line.qty_invoiced}"

    # Cleanup
    get_model("account.invoice").delete([invoice_id])
    get_model("sale.order").delete([order_id])

Security Considerations

Permission Model

Sale order lines inherit security from the parent sale.order model: - sale_readonly - View sale orders and lines - sale_manager - Full access to create, edit, delete

Data Access

  • Lines are automatically deleted when parent order is deleted (cascade)
  • Users can only access lines for orders they have permission to view
  • Company filtering applies through parent order's company_id
  • Multi-company environments: Lines visible based on order's company

Best Practices

  • Always validate product_id exists and user has access
  • Check stock availability before confirming to prevent overselling
  • Validate pricing and discounts to prevent negative amounts
  • Use tracking dimensions for audit trails
  • Set appropriate tax_id based on customer location and product type

Configuration Settings

Required Settings

Setting Location Description
Currency settings.currency_id Base currency for currency conversions
Default Tax Rate account.tax.rate Default tax applied to lines
Default Warehouse stock.location Default location for inventory

Optional Settings

Setting Default Description
Auto Sequence True Automatically number lines (10, 20, 30...)
Track Delivered Qty True Calculate qty_delivered from stock moves
Track Invoiced Qty True Calculate qty_invoiced from invoices
Store Actual Profit True Store act_profit_amount for reporting

Integration Points

Stock Management

  • Stock Moves: qty_delivered calculated from completed stock.move records
  • Stock Locations: location_id and reserve_location_id track inventory sources
  • Stock Lots: lot_id links line to specific lot/serial numbers
  • Stock Packaging: packaging_id specifies packaging requirements

Invoicing

  • Invoice Lines: qty_invoiced tracks billing progress
  • Invoice Creation: Lines copied to invoice.line when creating invoices
  • Tax Calculation: Tax amounts flow to invoice lines

Production

  • Production Orders: production_id links to manufacturing orders
  • Produced Quantity: qty_produced tracks manufactured quantities
  • Make-to-Order: Lines trigger production orders when confirmed

Accounting

  • Tracking Categories: track_id and track2_id for dimensional analysis
  • Profit Tracking: act_profit_amount stored for financial reporting
  • Cost Allocation: Track entries link costs to specific lines

Shipping

  • Delivery Slots: delivery_slot_id schedules deliveries
  • Shipping Methods: ship_method_id determines carrier
  • Tracking Numbers: ship_tracking shows courier tracking info
  • Due Dates: due_date controls delivery scheduling

Version History

Last Updated: 2026-01-05 Model File: sale_order_line.py (415 lines) Framework: Netforce Dependencies: sale.order, product, stock, account modules


Additional Resources

  • Sale Order Documentation: sale.order
  • Product Documentation: product
  • Stock Location Documentation: stock.location
  • Tax Rate Documentation: account.tax.rate
  • Invoice Documentation: account.invoice
  • UOM Documentation: uom

Support & Feedback

For issues or questions about sale order lines: 1. Verify parent sale.order exists and is accessible 2. Check product_id, location_id, and tax_id are valid references 3. Review computed fields by calling browse() after create/write 4. Ensure qty, unit_price are numeric and positive 5. Validate discount values don't exceed line subtotal 6. Check order tax_type matches expected tax calculation method 7. Review system logs for detailed error messages


This documentation is generated for developer onboarding and reference purposes.