Skip to content

Sales Forecast Line Documentation

Overview

The Sales Forecast Line module (sale.forecast.line) represents individual line items within a sales forecast. It enables granular forecasting at the product and customer level, tracks actual sales performance against planned quantities, and provides variance analysis capabilities for accurate demand planning and performance measurement.


Model Information

Model Name: sale.forecast.line Display Name: Sales Forecast Line Key Fields: None (relies on parent forecast uniqueness)

Features

  • ❌ Audit logging enabled (_audit_log)
  • ❌ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ✅ Cascade deletion when parent forecast is deleted

Forecast Line Segmentation

Forecast lines provide detailed breakdown capabilities:

Segmentation Description
Product-Level Forecast specific products or SKUs
Customer-Level Forecast demand by customer or customer group
Location-Level Forecast by warehouse or distribution center
Shelf Life Track shelf life requirements for perishable goods
Variance Tracking Compare planned vs actual quantities
Historical Comparison Compare with previous period forecasts

Key Fields Reference

Header Fields

Field Type Required Description
forecast_id Many2One Parent sales forecast (sale.forecast)
product_id Many2One Product being forecasted
plan_qty Decimal Planned/forecasted sales quantity
sequence Integer Display order within forecast

Segmentation Fields

Field Type Description
customer_id Many2One Specific customer for this forecast line (contact model)
location_id Many2One Warehouse location for this line (stock.location)
min_shelf_life Selection Minimum shelf life requirement: "50" (50%) or "75" (75%)
uom_id Many2One Unit of measure (deprecated - use product UoM)

Computed Fields

Field Type Description
actual_qty Decimal Actual sales quantity from confirmed orders (computed)
plan_out_qty Decimal Planned issue quantity from stock movements (computed)
plan_remain_qty Decimal Remaining stock quantity at end of period (computed)
prev_line_id Many2One Previous period's forecast line for same product (computed)
prev_plan_qty Decimal Previous period's planned quantity (computed)
prev_diff_percent Decimal Percentage change from previous period (computed)

Relationship Fields

Field Type Description
comments One2Many User comments and notes (message model)

API Methods

1. Create Record

Method: create(vals, context)

Creates a new forecast line item within a sales forecast.

Parameters:

vals = {
    "forecast_id": 123,                # Required: Parent forecast ID
    "product_id": 10,                  # Required: Product to forecast
    "plan_qty": 1000,                  # Required: Planned quantity
    "customer_id": 5,                  # Optional: Specific customer
    "location_id": 1,                  # Optional: Warehouse location
    "min_shelf_life": "75",            # Optional: Shelf life requirement
    "sequence": 10                     # Optional: Display order
}

context = {
    "company_id": 1                    # Current company context
}

Returns: int - New line ID

Example:

# Add product forecast line for specific customer
line_id = get_model("sale.forecast.line").create({
    "forecast_id": forecast_id,
    "product_id": 10,
    "customer_id": 5,
    "plan_qty": 1000,
    "min_shelf_life": "75"
})


2. Get Actual Quantity

Method: get_actual_qty(ids, context)

Computed field function that calculates actual sales from confirmed orders.

Behavior: - Searches sale.order.line for orders in confirmed/done state - Filters by forecast period dates (date_from to date_to) - Filters by product_id - Optionally filters by customer_id if specified - Converts quantities to product's base UoM - Returns total actual sales quantity

Example:

# Automatically calculated when browsing line
line = get_model("sale.forecast.line").browse(line_id)
print(f"Planned: {line.plan_qty}, Actual: {line.actual_qty}")
variance = line.plan_qty - line.actual_qty


3. Get Planned Issue Quantity

Method: get_plan_out_qty(ids, context)

Computed field function that calculates planned stock issues from pending movements.

Behavior: - Searches stock.move for movements in pending/approved/done states - Filters by forecast period dates - Filters by product_id and location_from_id - Optionally filters by customer contact_id - Converts quantities to line's UoM - Returns total planned outbound quantity

Example:

# Check planned stock issues
line = get_model("sale.forecast.line").browse(line_id)
print(f"Planned issues: {line.plan_out_qty}")


4. Get Planned Remaining Quantity

Method: get_plan_remain_qty(ids, context)

Computed field function that calculates remaining stock at end of forecast period.

Behavior: - Queries stock.move for all movements up to period end date - Calculates inbound quantities (location_to_id = line location) - Calculates outbound quantities (location_from_id = line location) - Returns balance: inbound - outbound - Uses direct database queries for performance

Example:

# Check projected remaining stock
line = get_model("sale.forecast.line").browse(line_id)
print(f"Remaining stock at period end: {line.plan_remain_qty}")


5. Get Previous Line

Method: get_prev_line(ids, context)

Computed field function that finds the previous period's forecast line.

Behavior: - Searches for forecast line with same product_id and min_shelf_life - Filters for forecasts with date_to before current forecast - Orders by date_to descending (most recent first) - Returns most recent previous line or None

Example:

# Compare with previous period
line = get_model("sale.forecast.line").browse(line_id)
if line.prev_line_id:
    print(f"Previous period: {line.prev_plan_qty}")
    print(f"Change: {line.prev_diff_percent}%")


6. Get Previous Period Data

Method: get_prev(ids, context)

Computed field function that calculates previous period comparison metrics.

Behavior: - Uses prev_line_id to get previous period's line - Calculates prev_plan_qty from previous line - Calculates prev_diff_percent: (current - previous) / previous * 100 - Returns None if no previous period exists

Returns: dict - Multi-field computed values:

{
    line_id: {
        "prev_plan_qty": Decimal or None,
        "prev_diff_percent": Decimal or None
    }
}


UI Events (onchange methods)

onchange_product

Triggered when product is selected. Updates: - uom_id - Sets to product's default unit of measure

Usage:

data = {
    "product_id": 10,
    "plan_qty": 100
}
result = get_model("sale.forecast.line").onchange_product(
    context={"data": data}
)
# Returns updated data with uom_id set


onchange_customer

Triggered when customer is selected. Updates: - min_shelf_life - Sets to customer's minimum shelf life requirement

Usage:

data = {
    "forecast_id": 123,
    "product_id": 10,
    "customer_id": 5
}
result = get_model("sale.forecast.line").onchange_customer(
    context={"data": data}
)
# Returns updated data with min_shelf_life set from customer


Search Functions

Search by Forecast

# Find all lines for a forecast
condition = [["forecast_id", "=", 123]]

Search by Product

# Find all forecast lines for a product
condition = [["product_id", "=", 10]]

Search by Customer

# Find forecast lines for a specific customer
condition = [["customer_id", "=", 5]]

Search by Period and Product

# Find forecast lines for product in date range
condition = [
    ["product_id", "=", 10],
    ["forecast_id.date_from", ">=", "2026-01-01"],
    ["forecast_id.date_to", "<=", "2026-03-31"]
]

Search by Shelf Life Requirement

# Find lines with specific shelf life requirements
condition = [["min_shelf_life", "=", "75"]]

Computed Fields Functions

get_actual_qty(ids, context)

Calculates actual sales quantity from confirmed sale orders during the forecast period, filtered by product and optionally by customer.

get_plan_out_qty(ids, context)

Calculates planned stock issues from pending/approved stock movements during the forecast period.

get_plan_remain_qty(ids, context)

Calculates projected remaining stock quantity at the end of the forecast period based on all planned movements.

get_prev_line(ids, context)

Finds the forecast line from the previous period for the same product and shelf life requirement.

get_prev(ids, context)

Calculates comparison metrics (quantity and percentage change) versus the previous period's forecast.


Best Practices

1. Product-Customer Segmentation

# Bad: Single aggregate line
line = {
    "product_id": 10,
    "plan_qty": 5000  # Total for all customers
}

# Good: Segment by major customers
lines = [
    {"product_id": 10, "customer_id": 5, "plan_qty": 2000},   # Customer A
    {"product_id": 10, "customer_id": 8, "plan_qty": 1500},   # Customer B
    {"product_id": 10, "plan_qty": 1500}                       # Others
]

Why: Customer-level segmentation enables better variance analysis and account management.


2. Shelf Life Management

For perishable products, always specify shelf life requirements:

# Good: Explicit shelf life requirements
line = {
    "product_id": 10,
    "customer_id": 5,
    "plan_qty": 1000,
    "min_shelf_life": "75"  # Customer requires 75% remaining shelf life
}

Why: Ensures proper inventory rotation and customer satisfaction for perishable goods.


3. Regular Variance Review

Monitor actual vs planned quantities throughout the forecast period:

# Review forecast accuracy mid-period
forecast = get_model("sale.forecast").browse(forecast_id)

for line in forecast.lines:
    if line.actual_qty > 0:  # Has sales data
        variance_pct = ((line.actual_qty - line.plan_qty) / line.plan_qty) * 100

        if abs(variance_pct) > 20:
            print(f"ALERT: {line.product_id.name}")
            print(f"  Customer: {line.customer_id.name if line.customer_id else 'All'}")
            print(f"  Variance: {variance_pct:.1f}%")
            print(f"  Planned: {line.plan_qty}, Actual: {line.actual_qty}")

Why: Early detection of variances enables proactive adjustments to inventory and production.


4. Sequence Management

Use sequence field to organize lines logically:

# Organize by product category, then customer importance
lines = [
    # Category A products
    {"product_id": 10, "customer_id": 5, "plan_qty": 1000, "sequence": 10},
    {"product_id": 10, "customer_id": 8, "plan_qty": 500, "sequence": 20},

    # Category B products
    {"product_id": 20, "customer_id": 5, "plan_qty": 750, "sequence": 30},
    {"product_id": 20, "customer_id": 8, "plan_qty": 250, "sequence": 40}
]

Why: Logical organization improves readability and makes forecast review more efficient.


5. Historical Trend Analysis

Leverage previous period comparison for better forecasting:

# Analyze trends and adjust forecasts
lines = get_model("sale.forecast.line").search_browse([
    ["forecast_id", "=", current_forecast_id]
])

for line in lines:
    if line.prev_line_id:
        # Check if previous forecast was accurate
        prev_variance = line.prev_line_id.actual_qty - line.prev_line_id.plan_qty
        prev_variance_pct = (prev_variance / line.prev_line_id.plan_qty) * 100

        # Adjust current forecast based on historical accuracy
        if abs(prev_variance_pct) > 10:
            print(f"Consider adjusting {line.product_id.name}:")
            print(f"  Previous variance: {prev_variance_pct:.1f}%")
            print(f"  Current vs previous: {line.prev_diff_percent:.1f}%")

Why: Historical accuracy data improves future forecast precision.


Database Constraints

Cascade Delete Constraint

"forecast_id": fields.Many2One("sale.forecast", "Sales Forecast",
                               required=True, on_delete="cascade")

When a forecast is deleted, all its lines are automatically deleted to maintain referential integrity.


Model Relationship Description
sale.forecast Many2One Parent sales forecast header
product Many2One Product being forecasted
contact Many2One Customer for segmented forecasting
stock.location Many2One Warehouse location for the forecast line
uom Many2One Unit of measure (deprecated)
sale.order.line Computed Source of actual sales quantities
stock.move Computed Source of planned stock movements
message One2Many Comments and notes

Common Use Cases

Use Case 1: Create Product-Customer Forecast Matrix

# Create detailed forecast with product-customer breakdown

# 1. Define forecast matrix
forecast_matrix = [
    {"product": 10, "customer": 5, "qty": 1000},
    {"product": 10, "customer": 8, "qty": 500},
    {"product": 15, "customer": 5, "qty": 750},
    {"product": 15, "customer": 8, "qty": 250},
]

# 2. Create forecast header
forecast_id = get_model("sale.forecast").create({
    "date_from": "2026-01-01",
    "date_to": "2026-03-31",
    "location_id": 1
})

# 3. Create forecast lines
for item in forecast_matrix:
    get_model("sale.forecast.line").create({
        "forecast_id": forecast_id,
        "product_id": item["product"],
        "customer_id": item["customer"],
        "plan_qty": item["qty"]
    })

# 4. Generate stock movements
forecast = get_model("sale.forecast").browse(forecast_id)
forecast.update_stock()

Use Case 2: Variance Analysis Dashboard

# Build variance analysis report for management

# 1. Get current period forecast
forecast = get_model("sale.forecast").browse(forecast_id)

# 2. Calculate variances
variances = []
for line in forecast.lines:
    if line.plan_qty > 0:
        variance_qty = line.actual_qty - line.plan_qty
        variance_pct = (variance_qty / line.plan_qty) * 100
        accuracy = 100 - abs(variance_pct)

        variances.append({
            "product": line.product_id.name,
            "product_code": line.product_id.code,
            "customer": line.customer_id.name if line.customer_id else "All Customers",
            "planned": float(line.plan_qty),
            "actual": float(line.actual_qty),
            "variance_qty": float(variance_qty),
            "variance_pct": float(variance_pct),
            "accuracy": float(accuracy)
        })

# 3. Sort by absolute variance
variances.sort(key=lambda x: abs(x["variance_pct"]), reverse=True)

# 4. Generate report
print("FORECAST VARIANCE ANALYSIS")
print(f"Period: {forecast.date_from} to {forecast.date_to}")
print("\nTop Variances:")
for v in variances[:10]:
    print(f"\n{v['product']} ({v['product_code']}) - {v['customer']}")
    print(f"  Planned: {v['planned']:.0f}, Actual: {v['actual']:.0f}")
    print(f"  Variance: {v['variance_pct']:.1f}% (Accuracy: {v['accuracy']:.1f}%)")

# 5. Summary statistics
total_planned = sum(v["planned"] for v in variances)
total_actual = sum(v["actual"] for v in variances)
overall_accuracy = (1 - abs(total_actual - total_planned) / total_planned) * 100

print(f"\nOverall Accuracy: {overall_accuracy:.1f}%")

Use Case 3: Copy and Adjust from Previous Period

# Create new forecast based on previous period with adjustments

# 1. Get previous period lines
prev_forecast = get_model("sale.forecast").search_browse([
    ["date_to", "=", "2025-12-31"]
])[0]

# 2. Create new forecast header
new_forecast_id = get_model("sale.forecast").create({
    "date_from": "2026-01-01",
    "date_to": "2026-03-31",
    "location_id": prev_forecast.location_id.id
})

# 3. Copy lines with growth adjustment
for prev_line in prev_forecast.lines:
    # Calculate growth based on previous variance
    if prev_line.actual_qty > 0:
        growth_factor = prev_line.actual_qty / prev_line.plan_qty
    else:
        growth_factor = 1.0

    # Apply growth and add 5% optimistic increase
    new_qty = prev_line.plan_qty * growth_factor * 1.05

    get_model("sale.forecast.line").create({
        "forecast_id": new_forecast_id,
        "product_id": prev_line.product_id.id,
        "customer_id": prev_line.customer_id.id if prev_line.customer_id else None,
        "plan_qty": new_qty,
        "min_shelf_life": prev_line.min_shelf_life
    })

print(f"Created new forecast with {len(prev_forecast.lines)} lines")

Use Case 4: Shelf Life Requirement Tracking

# Manage products with shelf life requirements

# 1. Find all forecast lines for perishable products
perishable_lines = get_model("sale.forecast.line").search_browse([
    ["forecast_id.state", "=", "open"],
    ["min_shelf_life", "!=", None]
])

# 2. Group by shelf life requirement
shelf_life_75 = [l for l in perishable_lines if l.min_shelf_life == "75"]
shelf_life_50 = [l for l in perishable_lines if l.min_shelf_life == "50"]

# 3. Report requirements
print("SHELF LIFE REQUIREMENTS")
print(f"\n75% Minimum Shelf Life ({len(shelf_life_75)} lines):")
for line in shelf_life_75:
    print(f"  {line.product_id.name}: {line.plan_qty} units")
    print(f"    Customer: {line.customer_id.name if line.customer_id else 'Various'}")

print(f"\n50% Minimum Shelf Life ({len(shelf_life_50)} lines):")
for line in shelf_life_50:
    print(f"  {line.product_id.name}: {line.plan_qty} units")
    print(f"    Customer: {line.customer_id.name if line.customer_id else 'Various'}")

# 4. Check stock expiration planning
for line in perishable_lines:
    # This would integrate with lot tracking
    print(f"\nAlert: Check expiration dates for {line.product_id.name}")
    print(f"  Required shelf life: {line.min_shelf_life}%")
    print(f"  Forecasted demand: {line.plan_qty}")

Use Case 5: Multi-Location Forecasting

# Forecast same product across multiple locations

# 1. Define product across locations
product_id = 10
locations = [
    {"location_id": 1, "name": "Main Warehouse", "qty": 2000},
    {"location_id": 2, "name": "Regional DC", "qty": 1000},
    {"location_id": 3, "name": "Retail Store", "qty": 500}
]

# 2. Create forecast
forecast_id = get_model("sale.forecast").create({
    "date_from": "2026-01-01",
    "date_to": "2026-03-31"
})

# 3. Create lines per location
for loc in locations:
    get_model("sale.forecast.line").create({
        "forecast_id": forecast_id,
        "product_id": product_id,
        "location_id": loc["location_id"],
        "plan_qty": loc["qty"]
    })

# 4. Analyze location performance
forecast = get_model("sale.forecast").browse(forecast_id)
for line in forecast.lines:
    location_name = line.location_id.name if line.location_id else "N/A"
    variance = line.actual_qty - line.plan_qty
    print(f"{location_name}: Planned {line.plan_qty}, Actual {line.actual_qty}, Variance {variance}")

Performance Tips

1. Minimize Computed Field Calls

# Bad: Multiple database queries
for line in forecast.lines:
    print(line.actual_qty)      # Query
    print(line.plan_out_qty)    # Query
    print(line.plan_remain_qty) # Query

# Good: Access computed fields once
for line in forecast.lines:
    actual = line.actual_qty    # Single query per field type
    planned = line.plan_out_qty
    remain = line.plan_remain_qty
    print(f"Actual: {actual}, Planned: {planned}, Remain: {remain}")

2. Use Bulk Operations

# Bad: Create lines one by one
for item in items:
    get_model("sale.forecast.line").create({
        "forecast_id": forecast_id,
        "product_id": item["product_id"],
        "plan_qty": item["qty"]
    })

# Good: Create during forecast creation
get_model("sale.forecast").create({
    "date_from": "2026-01-01",
    "date_to": "2026-03-31",
    "lines": [
        ("create", {"product_id": item["product_id"], "plan_qty": item["qty"]})
        for item in items
    ]
})

3. Efficient Searches

The model orders by sequence,id by default:

# Efficient: Uses indexed fields
lines = get_model("sale.forecast.line").search_browse([
    ["forecast_id", "=", 123],
    ["product_id", "=", 10]
])

Troubleshooting

"Forecast line showing zero actual_qty despite sales"

Cause: Sales orders may not be in confirmed/done state, or dates don't match forecast period Solution: Verify order states and dates:

# Check sales orders in period
orders = get_model("sale.order.line").search_browse([
    ["product_id", "=", line.product_id.id],
    ["order_id.due_date", ">=", line.forecast_id.date_from],
    ["order_id.due_date", "<=", line.forecast_id.date_to]
])
for order in orders:
    print(f"Order {order.order_id.number}: State={order.order_id.state}, Qty={order.qty}")

"prev_line_id is None but previous forecasts exist"

Cause: Product ID or min_shelf_life doesn't match previous periods Solution: Check exact product and shelf life matching:

# Find potential previous lines
prev_lines = get_model("sale.forecast.line").search_browse([
    ["product_id", "=", line.product_id.id],
    ["forecast_id.date_to", "<", line.forecast_id.date_to]
], order="forecast_id.date_to desc")

for prev in prev_lines:
    print(f"Previous: Date={prev.forecast_id.date_to}, Shelf Life={prev.min_shelf_life}")

"UoM conversion errors in actual_qty"

Cause: Sale order lines using different UoM than product Solution: Ensure UoM conversion is properly configured in uom model


Testing Examples

Unit Test: Variance Calculation

def test_line_variance():
    # Create forecast with line
    forecast_id = get_model("sale.forecast").create({
        "date_from": "2025-12-01",
        "date_to": "2025-12-31",
        "lines": [
            ("create", {
                "product_id": 10,
                "plan_qty": 1000
            })
        ]
    })

    # Get line
    line = get_model("sale.forecast.line").search_browse([
        ["forecast_id", "=", forecast_id]
    ])[0]

    # Verify computed fields
    assert hasattr(line, "actual_qty")
    assert hasattr(line, "plan_out_qty")

    # Calculate variance
    variance = line.actual_qty - line.plan_qty
    assert isinstance(variance, (int, float, Decimal))

Security Considerations

Permission Model

  • Inherits permissions from parent sale.forecast model
  • sale.forecast.line.create - Create new lines
  • sale.forecast.line.write - Edit existing lines
  • sale.forecast.line.delete - Delete lines
  • sale.forecast.line.read - View lines

Data Access

  • Lines are accessible if parent forecast is accessible
  • No separate company-level isolation
  • Cascade delete ensures referential integrity

Configuration Settings

Required Settings

Setting Location Description
Parent Forecast sale.forecast Must exist before creating lines
Product product Valid product with UoM configuration

Integration Points

Internal Modules

  • Sales Forecast: Parent forecast header
  • Sales Orders: Source of actual sales data for variance analysis
  • Stock Management: Source of planned stock movements and remaining quantities
  • Product Management: Links to products, UoMs, and specifications
  • Customer Management: Links to customers for segmented forecasting

Version History

Last Updated: 2026-01-05 Model Version: sale_forecast_line.py (134 lines) Framework: Netforce


Additional Resources

  • Sales Forecast Documentation: sale.forecast
  • Sales Target Documentation: sale.target
  • Sales Order Line Documentation: sale.order.line
  • Stock Movement Documentation: stock.move
  • Product Documentation: product

Support & Feedback

For issues or questions about this module: 1. Check parent sale.forecast documentation 2. Verify product and customer configurations 3. Review computed field calculations for variance analysis 4. Ensure date ranges align with sales order due dates 5. Test in development environment first


This documentation is generated for developer onboarding and reference purposes.