Sales Forecast Documentation¶
Overview¶
The Sales Forecast module (sale.forecast) provides comprehensive sales forecasting capabilities for period-based planning. It enables businesses to predict future sales volumes, plan inventory requirements, allocate resources, and coordinate production and procurement activities based on anticipated customer demand.
Model Information¶
Model Name: sale.forecast
Display Name: Sales Forecast
Key Fields: number
Features¶
- ❌ Audit logging enabled (
_audit_log) - ❌ Multi-company support (
company_id) - ❌ Full-text content search (
_content_search) - ✅ Unique key constraint per forecast number
Understanding Key Fields¶
What are Key Fields?¶
In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record. Think of them as a business key that ensures data integrity across the system.
For the sale.forecast model, the key field is:
This means the forecast number must be unique:
- number - Auto-generated forecast identifier (e.g., FC0001, FC0002)
Why Key Fields Matter¶
Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique combinations:
# Examples of valid combinations:
number = "FC0001" ✅ Valid
number = "FC0002" ✅ Valid
number = "FC0003" ✅ Valid
# This would fail - duplicate key:
number = "FC0001" ❌ ERROR: Key already exists!
Database Implementation¶
The key fields are enforced at the database level using a unique constraint:
This translates to:
Forecast Period Planning¶
Sales forecasts are organized by time periods, allowing for systematic planning:
| Aspect | Description |
|---|---|
| Period-Based | Define date ranges (monthly, quarterly, annually) |
| Line-Level Detail | Break down by products, customers, locations |
| Stock Integration | Generate stock movements for pending forecasts |
| Production Planning | Convert forecasts to production plans |
| Purchase Planning | Generate purchase orders for raw materials |
State Workflow¶
| State | Description |
|---|---|
open |
Forecast is active and can be modified. Stock movements are generated based on forecast lines. |
closed |
Forecast is finalized and locked. Stock movements are deleted. Used for completed periods. |
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
number |
Char | ✅ | Auto-generated unique forecast number (e.g., FC0001) |
date_from |
Date | ✅ | Start date of the forecast period |
date_to |
Date | ✅ | End date of the forecast period |
location_id |
Many2One | ❌ | Warehouse/location for stock planning (stock.location) |
state |
Selection | ✅ | Current status: open or closed |
Detail Fields¶
| Field | Type | Description |
|---|---|---|
description |
Text | Detailed description or notes about the forecast |
num_lines |
Integer | Count of forecast line items (computed) |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
lines |
One2Many | Forecast line items (sale.forecast.line) with product/customer details |
comments |
One2Many | User comments and notes (message model) |
stock_moves |
One2Many | Pending stock movements generated from forecast |
API Methods¶
1. Create Record¶
Method: create(vals, context)
Creates a new sales forecast record with line items.
Parameters:
vals = {
"number": "FC0001", # Auto-generated if not provided
"date_from": "2026-01-01", # Required: Forecast start date
"date_to": "2026-03-31", # Required: Forecast end date
"location_id": 1, # Optional: Warehouse location
"description": "Q1 2026 Forecast", # Optional: Description
"lines": [ # Forecast line items
("create", {
"product_id": 10,
"customer_id": 5,
"plan_qty": 1000,
"min_shelf_life": "75"
}),
("create", {
"product_id": 15,
"plan_qty": 500
})
]
}
context = {
"company_id": 1 # Current company context
}
Returns: int - New forecast ID
Example:
# Create Q1 2026 sales forecast
forecast_id = get_model("sale.forecast").create({
"date_from": "2026-01-01",
"date_to": "2026-03-31",
"location_id": 1,
"description": "Q1 2026 Sales Forecast",
"lines": [
("create", {
"product_id": 10,
"customer_id": 5,
"plan_qty": 1000,
"min_shelf_life": "75"
})
]
})
2. Update Stock¶
Method: update_stock(ids, context)
Generates pending stock movements based on forecast lines. Creates stock movements from warehouse to customer location for the difference between planned and actual quantities.
Parameters:
- ids (list): Forecast IDs to generate stock movements for
Behavior:
- Validates that location_id is set on the forecast
- Deletes existing stock movements linked to this forecast
- For each forecast line, calculates: diff_qty = plan_qty - actual_qty
- Creates pending stock move if diff_qty > 0
- Stock moves are dated at the start of the forecast period
- Uses pick-out journal from settings
Returns: None
Example:
# Generate stock movements for forecast
forecast = get_model("sale.forecast").browse(123)
forecast.update_stock()
# This creates stock.move records with state="pending"
# Location: From warehouse → To customer location
# Quantity: Planned minus actual sales
Validation:
- Raises exception if location_id is not set: "Missing location in sales forecast {number}"
3. State Transition Methods¶
3.1 Close Forecast¶
Method: close(ids, context)
Closes the forecast period and removes pending stock movements.
Parameters:
- ids (list): Record IDs to close
Behavior: - Deletes all stock movements related to the forecast - Sets state to "closed" - Used when forecast period is completed
Example:
3.2 Reopen Forecast¶
Method: reopen(ids, context)
Reopens a closed forecast and regenerates stock movements.
Parameters:
- ids (list): Record IDs to reopen
Behavior: - Sets state back to "open" - Calls update_stock() to regenerate movements - Useful for adjusting past forecasts
Example:
4. Helper Methods¶
4.1 Copy Forecast¶
Method: copy(ids, context)
Creates a duplicate of an existing forecast with all line items.
Behavior: - Copies header fields: date_from, date_to, description - Copies all forecast lines with: product_id, customer_id, min_shelf_life, plan_qty - Generates new forecast number - Returns navigation to the new forecast form
Example:
result = get_model("sale.forecast").copy([forecast_id])
# Returns: {"next": {"name": "sale_forecast", "mode": "form", "active_id": new_id}}
4.2 Copy to Production Plan¶
Method: copy_to_production_plan(ids, context)
Converts forecast lines into production plans for manufacturing.
Behavior: - For each forecast line, calculates manufacturing date based on product lead time - Finds the Bill of Materials (BoM) for the product - Creates production plan with: product_id, dates, plan_qty, uom_id, bom_id - Returns count of production plans created
Example:
result = get_model("sale.forecast").copy_to_production_plan([forecast_id])
# Returns: {"flash": "N production plans created from sales forecast"}
Validation: - Raises exception if BoM not found for product
4.3 Copy to Raw Material Purchase¶
Method: copy_to_rm_purchase(ids, context)
Generates purchase orders for raw materials based on forecast requirements.
Behavior: - Calculates current stock quantity vs. forecast planned quantity - If shortage exists, determines order quantity - Checks product suppliers (raises error if missing) - For products with BoM, calculates raw material requirements - Groups purchase lines by supplier - Creates purchase orders with reference to forecast number
Example:
result = get_model("sale.forecast").copy_to_rm_purchase([forecast_id])
# Returns: {"next": {"name": "purchase", "search_condition": [["ref", "=", "FC0001"]]}}
Validation: - Raises exception if no suppliers defined for products - Raises exception if no purchase orders needed
4.4 Months to Quantity¶
Method: months_to_qty(product_id, months, min_shelf_life=None)
Calculates forecasted quantity for a product over a specified number of months.
Parameters:
- product_id (int): Product to calculate forecast for
- months (int/float): Number of months to forecast
- min_shelf_life (str, optional): Filter by minimum shelf life requirement
Behavior: - Searches forecast lines for the product within the date range - Calculates proportional quantities based on period overlap - Extrapolates if forecast coverage is incomplete - Returns total quantity as integer
Example:
# Get 3-month forecast for product
qty = get_model("sale.forecast").months_to_qty(
product_id=10,
months=3,
min_shelf_life="75"
)
4.5 Get Number of Lines¶
Method: get_num_lines(ids, context)
Computed field function that counts forecast line items.
Example:
# Automatically calculated when browsing forecast
forecast = get_model("sale.forecast").browse(123)
print(forecast.num_lines) # Returns count of lines
Search Functions¶
Search by Date Range¶
# Find forecasts overlapping a specific date
condition = [
["date_from", "<=", "2026-03-31"],
["date_to", ">=", "2026-01-01"]
]
Search by State¶
Search by Number¶
Search by Location¶
Computed Fields Functions¶
get_num_lines(ids, context)¶
Returns the count of forecast line items for each forecast record.
Best Practices¶
1. Period Definition¶
# Bad example: Overlapping forecast periods
forecast1 = {"date_from": "2026-01-01", "date_to": "2026-03-31"}
forecast2 = {"date_from": "2026-02-01", "date_to": "2026-04-30"} # Overlaps!
# Good example: Sequential non-overlapping periods
forecast_q1 = {"date_from": "2026-01-01", "date_to": "2026-03-31"}
forecast_q2 = {"date_from": "2026-04-01", "date_to": "2026-06-30"}
Why: Non-overlapping periods prevent double-counting and confusion in variance analysis.
2. Location Assignment¶
Always set the warehouse location when creating forecasts:
# Bad: Missing location
forecast = {
"date_from": "2026-01-01",
"date_to": "2026-03-31"
# Missing location_id - will fail when generating stock movements
}
# Good: Location specified
forecast = {
"date_from": "2026-01-01",
"date_to": "2026-03-31",
"location_id": 1 # Warehouse location set
}
Why: Location is required for generating stock movements and purchase planning.
3. Regular Updates¶
Update forecasts regularly based on actual sales:
# Review forecast monthly
forecast = get_model("sale.forecast").browse(forecast_id)
# Check variance between planned and actual
for line in forecast.lines:
variance = line.plan_qty - line.actual_qty
if abs(variance) > line.plan_qty * 0.2: # More than 20% variance
print(f"Significant variance for {line.product_id.name}: {variance}")
# Consider adjusting future forecasts
Why: Regular reviews improve forecast accuracy and planning effectiveness.
4. Bottom-Up vs Top-Down Forecasting¶
Bottom-Up Approach:
# Create detailed forecasts by product and customer
forecast_id = get_model("sale.forecast").create({
"date_from": "2026-01-01",
"date_to": "2026-03-31",
"lines": [
("create", {"product_id": 10, "customer_id": 5, "plan_qty": 1000}),
("create", {"product_id": 10, "customer_id": 8, "plan_qty": 500}),
("create", {"product_id": 15, "customer_id": 5, "plan_qty": 750}),
]
})
Top-Down Approach:
# Start with total forecast, then break down
forecast_id = get_model("sale.forecast").create({
"date_from": "2026-01-01",
"date_to": "2026-03-31",
"lines": [
("create", {"product_id": 10, "plan_qty": 1500}), # Total for product
("create", {"product_id": 15, "plan_qty": 750}),
]
})
Best Practice: Use bottom-up for better accuracy, top-down for speed. Combine both approaches for optimal results.
Database Constraints¶
Unique Number Constraint¶
Ensures each forecast has a unique identifier, preventing duplicate forecast numbers in the system.
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.forecast.line |
One2Many | Detailed forecast lines with product/customer breakdown |
stock.location |
Many2One | Warehouse location for stock planning |
stock.move |
One2Many | Pending stock movements generated from forecast |
message |
One2Many | Comments and discussion threads |
sequence |
System | Auto-generates forecast numbers |
production.plan |
Integration | Production plans created from forecast |
purchase.order |
Integration | Purchase orders generated from forecast requirements |
Common Use Cases¶
Use Case 1: Create Quarterly Sales Forecast¶
# Step-by-step quarterly forecast creation
# 1. Create forecast header for Q1 2026
forecast_id = get_model("sale.forecast").create({
"date_from": "2026-01-01",
"date_to": "2026-03-31",
"location_id": 1,
"description": "Q1 2026 Sales Forecast - Based on historical trends and new contracts"
})
# 2. Add forecast lines by product and customer
forecast = get_model("sale.forecast").browse(forecast_id)
get_model("sale.forecast.line").create({
"forecast_id": forecast_id,
"product_id": 10,
"customer_id": 5,
"plan_qty": 1000,
"min_shelf_life": "75"
})
# 3. Generate stock movements
forecast.update_stock()
# 4. Review and approve forecast
# (Review actual_qty as period progresses)
Use Case 2: Copy Previous Period Forecast¶
# Quickly create next period forecast based on previous period
# 1. Find previous quarter forecast
prev_forecast = get_model("sale.forecast").search_browse([
["date_from", "=", "2025-10-01"],
["date_to", "=", "2025-12-31"]
])
# 2. Copy the forecast
result = get_model("sale.forecast").copy([prev_forecast[0].id])
new_forecast_id = result["next"]["active_id"]
# 3. Update dates for new period
get_model("sale.forecast").write([new_forecast_id], {
"date_from": "2026-01-01",
"date_to": "2026-03-31"
})
# 4. Adjust quantities based on growth expectations
new_forecast = get_model("sale.forecast").browse(new_forecast_id)
for line in new_forecast.lines:
# Increase by 10% for growth
new_qty = line.plan_qty * 1.10
line.write({"plan_qty": new_qty})
Use Case 3: Generate Purchase Orders from Forecast¶
# Create purchase orders for forecasted demand
# 1. Create or select forecast
forecast_id = 123
# 2. Generate raw material purchase orders
result = get_model("sale.forecast").copy_to_rm_purchase([forecast_id])
# This automatically:
# - Calculates current stock levels
# - Determines shortage quantities
# - Checks BoM for raw material requirements
# - Groups by supplier
# - Creates purchase orders
# 3. Review generated purchase orders
po_condition = [["ref", "=", forecast.number]]
purchase_orders = get_model("purchase.order").search_browse(po_condition)
for po in purchase_orders:
print(f"PO {po.number} for {po.contact_id.name}: {po.amount_total}")
Use Case 4: Variance Analysis and Tracking¶
# Monitor forecast accuracy over time
# 1. Get completed forecast
forecast = get_model("sale.forecast").browse(forecast_id)
# 2. Calculate variance for each line
variances = []
for line in forecast.lines:
variance_qty = line.actual_qty - line.plan_qty
variance_pct = (variance_qty / line.plan_qty * 100) if line.plan_qty else 0
variances.append({
"product": line.product_id.name,
"customer": line.customer_id.name if line.customer_id else "All",
"planned": line.plan_qty,
"actual": line.actual_qty,
"variance_qty": variance_qty,
"variance_pct": variance_pct
})
# 3. Identify significant variances
for v in variances:
if abs(v["variance_pct"]) > 20:
print(f"WARNING: {v['product']} variance: {v['variance_pct']:.1f}%")
# 4. Close accurate forecasts
if all(abs(v["variance_pct"]) < 10 for v in variances):
forecast.close()
Use Case 5: Production Planning from Forecast¶
# Convert forecast to production schedule
# 1. Create forecast with manufactured products
forecast_id = get_model("sale.forecast").create({
"date_from": "2026-03-01",
"date_to": "2026-03-31",
"location_id": 1,
"lines": [
("create", {
"product_id": 100, # Manufactured product
"plan_qty": 5000
})
]
})
# 2. Generate production plans
result = get_model("sale.forecast").copy_to_production_plan([forecast_id])
# Automatically creates production.plan records
# Manufacturing date = forecast start date - product lead time
# 3. Review production plans
prod_plans = get_model("production.plan").search_browse([
["product_id", "=", 100],
["date_from", ">=", "2026-02-01"],
["date_to", "<=", "2026-03-31"]
])
for plan in prod_plans:
print(f"Produce {plan.plan_qty} units by {plan.date_to}")
Performance Tips¶
1. Batch Operations¶
- When creating forecasts with many lines, use nested creates rather than separate calls
- Use search_browse() instead of search() + browse() to reduce database queries
2. Optimize Stock Movement Generation¶
# Bad: Generate stock movements frequently
forecast.update_stock() # Called after every line change
forecast.update_stock()
forecast.update_stock()
# Good: Generate once after all changes
# Make all forecast line changes first
for line in lines_data:
get_model("sale.forecast.line").create(line)
# Then generate stock movements once
forecast.update_stock()
3. Use Date Indexing¶
The model orders by date_from desc by default, which is optimized for date-based searches:
# Efficient: Uses indexed field
forecasts = get_model("sale.forecast").search_browse([
["date_from", ">=", "2026-01-01"],
["date_to", "<=", "2026-12-31"]
])
Troubleshooting¶
"Missing location in sales forecast {number}"¶
Cause: Attempting to generate stock movements without setting location_id Solution: Set the warehouse location before calling update_stock():
"Customer location not found"¶
Cause: No stock location with type="customer" exists in the system Solution: Create a customer location in stock.location:
"BoM not found for product {code}"¶
Cause: Attempting to create production plan for product without Bill of Materials Solution: Create BoM for the product or remove it from production planning:
# Create BoM first
bom_id = get_model("bom").create({
"product_id": product_id,
"qty": 1,
"lines": [...]
})
# Then create production plan
"Missing supplier for product {name}"¶
Cause: Generating purchase orders for product without supplier configured Solution: Add supplier to product:
product.write({
"suppliers": [("create", {
"supplier_id": supplier_contact_id,
"price": 10.00
})]
})
Testing Examples¶
Unit Test: Create and Close Forecast¶
def test_forecast_lifecycle():
# Create forecast
forecast_id = get_model("sale.forecast").create({
"date_from": "2026-01-01",
"date_to": "2026-01-31",
"location_id": 1,
"lines": [
("create", {
"product_id": 10,
"plan_qty": 100
})
]
})
# Verify created
forecast = get_model("sale.forecast").browse(forecast_id)
assert forecast.state == "open"
assert forecast.num_lines == 1
# Generate stock movements
forecast.update_stock()
assert len(forecast.stock_moves) > 0
# Close forecast
forecast.close()
assert forecast.state == "closed"
assert len(forecast.stock_moves) == 0
Unit Test: Variance Calculation¶
def test_forecast_variance():
# Create forecast with known quantities
forecast_id = get_model("sale.forecast").create({
"date_from": "2025-12-01",
"date_to": "2025-12-31",
"location_id": 1,
"lines": [
("create", {
"product_id": 10,
"customer_id": 5,
"plan_qty": 1000
})
]
})
# Check actual sales (should be calculated from sale.order.line)
forecast = get_model("sale.forecast").browse(forecast_id)
line = forecast.lines[0]
# Verify actual_qty is computed
assert hasattr(line, "actual_qty")
# Calculate variance
variance = line.actual_qty - line.plan_qty
accuracy = (1 - abs(variance) / line.plan_qty) * 100 if line.plan_qty else 0
print(f"Forecast accuracy: {accuracy:.1f}%")
Security Considerations¶
Permission Model¶
sale.forecast.create- Create new forecastssale.forecast.write- Edit existing forecastssale.forecast.delete- Delete forecastssale.forecast.read- View forecasts
Data Access¶
- Forecasts are visible to all users with read permission
- No company-level isolation (no company_id field)
- Stock movements generated with admin user permissions
- Consider implementing approval workflow for large forecasts
Configuration Settings¶
Required Settings¶
| Setting | Location | Description |
|---|---|---|
pick_out_journal_id |
settings (ID=1) | Stock journal for outbound movements |
sale_forecast sequence |
sequence model | Auto-numbering for forecasts |
Optional Settings¶
| Setting | Default | Description |
|---|---|---|
| Location | None | Default warehouse for new forecasts |
Integration Points¶
Internal Modules¶
- Stock Management: Generates pending stock movements for forecast demand
- Production Planning: Creates production plans based on forecasted quantities
- Purchase Management: Generates purchase orders for raw materials
- Sales Orders: Compares forecast vs actual sales for variance analysis
- Product Management: Links to products and their BoMs, suppliers, lead times
Version History¶
Last Updated: 2026-01-05 Model Version: sale_forecast.py (245 lines) Framework: Netforce
Additional Resources¶
- Sales Forecast Line Documentation:
sale.forecast.line - Sales Target Documentation:
sale.target - Stock Movement Documentation:
stock.move - Production Planning Documentation:
production.plan - Purchase Order Documentation:
purchase.order
Support & Feedback¶
For issues or questions about this module: 1. Check related model documentation (sale.forecast.line, stock.move) 2. Review system logs for detailed error messages 3. Verify location and sequence configuration 4. Ensure products have suppliers and BoMs where required 5. Test in development environment first
This documentation is generated for developer onboarding and reference purposes.