Stock Reports Documentation¶
Overview¶
This document provides comprehensive documentation for all stock reporting models in the Netforce stock management system. These transient models generate various analytical reports for inventory tracking, forecasting, aging analysis, and movement monitoring.
Report Models Summary¶
| Report Model | Purpose | Key Features |
|---|---|---|
report.stock.aging |
Stock aging analysis | Tracks inventory age by periods |
report.stock.card |
Stock card (ledger) | Complete movement history |
report.stock.expire |
Expiry tracking | Monitors expiring lots by month |
report.stock.forecast |
Inventory forecast | Projects future stock levels |
report.stock.invoice |
Invoice reconciliation | Compares receipts vs invoices |
report.stock.move |
Movement report | Filtered stock movements |
report.stock.plan |
Procurement planning | Identifies reorder needs |
report.stock.project |
Project tracking | Stock by project |
report.stock.summary |
Stock summary | Opening/closing balances |
report.forecast.details |
Detailed forecast | Visual forecast charts |
report.forecast.summary |
Forecast summary | Multi-product forecast |
report.master.product |
Master product report | Variant aggregation |
report.ship.cost |
Shipping costs | Shipping expense tracking |
1. Stock Aging Report¶
Model Information¶
Model Name: report.stock.aging
Display Name: Stock Aging Report
Type: Transient (temporary) model
Purpose: Analyzes how long inventory has been in stock
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date |
Date | ✅ | Reference date for aging calculation |
location_id |
Many2One | ❌ | Filter by location |
product_id |
Many2One | ❌ | Filter by product |
categ_id |
Many2One | ❌ | Filter by product category |
period_days |
Integer | ✅ | Days per aging period (default: 30) |
num_periods |
Integer | ✅ | Number of periods to show (default: 3) |
Key Concepts¶
Aging Periods: Inventory is grouped into time periods showing how long it has been in stock: - Period 1: 0-30 days old - Period 2: 31-60 days old - Period 3: 61-90 days old - Older: 90+ days old
FIFO Assumption: Uses First-In-First-Out logic to allocate oldest stock first.
Report Data Structure¶
{
"company_name": "Company Name",
"location_name": "Warehouse A",
"product_name": "Product XYZ",
"date": "2024-01-31",
"periods": [
{
"date_from": "2024-01-01",
"date_to": "2024-01-30",
"period_name": "0-29 days"
},
# ... more periods
],
"lines": [
{
"product_id": 123,
"product_name": "Product Name",
"location_id": 456,
"location_name": "Location Name",
"cur_qty": 100.0, # Current quantity
"periods": [
{"date_from": "...", "date_to": "...", "qty": 50.0},
{"date_from": "...", "date_to": "...", "qty": 30.0}
],
"older_qty": 20.0 # Quantity older than all periods
}
],
"older_date_to": "2023-12-31"
}
Usage Example¶
# Generate stock aging report
report_id = get_model("report.stock.aging").create({
"date": "2024-01-31",
"location_id": warehouse_id,
"period_days": 30,
"num_periods": 4
})
data = get_model("report.stock.aging").get_report_data([report_id])
Best Practices¶
- Run monthly to identify slow-moving inventory
- Use for obsolescence analysis
- Compare across periods to track inventory turnover
- Filter by category for specific product groups
2. Stock Card Report¶
Model Information¶
Model Name: report.stock.card
Display Name: Stock Card (Stock Ledger)
Type: Transient model
Purpose: Detailed movement history showing all ins/outs for products
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date_from |
Date | ✅ | Start date |
date_to |
Date | ✅ | End date |
product_id |
Many2One | ❌ | Filter by product |
categ_id |
Many2One | ❌ | Filter by category |
location_id |
Many2One | ❌ | Filter by location |
uom_id |
Many2One | ❌ | Unit of measure filter |
lot_id |
Many2One | ❌ | Filter by lot/serial |
invoice_id |
Many2One | ❌ | Filter by invoice |
show_pending |
Boolean | ❌ | Include pending moves |
show_qty2 |
Boolean | ❌ | Show secondary quantities |
hide_zero |
Boolean | ❌ | Hide zero-quantity lines |
hide_cost |
Boolean | ❌ | Hide cost information |
Report Data Structure¶
{
"company_name": "Company",
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"groups": [
{
"product_id": 123,
"location_id": 456,
"product_name": "Product Name",
"product_code": "PROD001",
"location_name": "Warehouse A",
"location_code": "WH-A",
"lines": [
{
"date": "2024-01-15",
"bal_qty": 50.0,
"bal_cost_amount": 5000.0,
"bal_cost_price": 100.0
},
{
"id": 789,
"date": "2024-01-16",
"ref": "GR001",
"in_qty": 10.0,
"in_cost_price": 105.0,
"in_amount": 1050.0,
"bal_qty": 60.0,
"bal_cost_amount": 6050.0,
"bal_cost_price": 100.83
}
],
"total_in_qty": 10.0,
"total_out_qty": 0.0,
"total_in_amount": 1050.0,
"total_out_amount": 0.0
}
]
}
Usage Example¶
# Generate stock card for a product
report_id = get_model("report.stock.card").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"product_id": product_id,
"location_id": location_id,
"show_pending": False
})
data = get_model("report.stock.card").get_report_data([report_id])
# Print movement summary
for group in data["groups"]:
print(f"{group['product_code']}: In={group['total_in_qty']}, Out={group['total_out_qty']}")
Performance Considerations¶
- Use specific filters to reduce query scope
- For large datasets, limit date range
- Query can be slow with many movements (uses caching in production)
3. Stock Expiry Report¶
Model Information¶
Model Name: report.stock.expire
Display Name: Stock Expiry Report
Type: Transient model
Purpose: Tracks expiring lots over upcoming months
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date |
Date | ✅ | Starting date |
forecast_days |
Integer | ✅ | Days to forecast (default: 180) |
product_id |
Many2One | ❌ | Filter by product |
location_id |
Many2One | ❌ | Filter by location |
Report Data Structure¶
{
"company_name": "Company",
"date": "2024-01-01",
"months": [
{
"month_name": "Jan 2024",
"date_from": "2024-01-01",
"date_to": "2024-01-31"
},
# ... 24 months
],
"lines": [
{
"prod_id": 123,
"prod_code": "PROD001",
"prod_name": "Product Name",
"months": [
{
"exp_qty": 5, # Number of lots expiring
"link_options": "{...}" # For drill-down
},
# ... one entry per month
],
"link_options": "{...}"
}
]
}
Usage Example¶
# Generate expiry report
report_id = get_model("report.stock.expire").create({
"date": "2024-01-01",
"forecast_days": 365,
"location_id": warehouse_id
})
data = get_model("report.stock.expire").get_report_data([report_id])
# Find products expiring in next 3 months
for line in data["lines"]:
expiring_soon = sum(m["exp_qty"] for m in line["months"][:3])
if expiring_soon > 0:
print(f"{line['prod_code']}: {expiring_soon} lots expiring soon")
Best Practices¶
- Run weekly to monitor upcoming expirations
- Set up alerts for critical products
- Use for FEFO (First-Expired-First-Out) planning
- Coordinate with sales to prioritize expiring stock
4. Stock Forecast Report¶
Model Information¶
Model Name: report.stock.forecast
Display Name: Stock Forecast Report
Type: Transient model
Purpose: Projects future stock levels based on pending movements
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date |
Date | ✅ | Starting date |
location_id |
Many2One | ❌ | Location to forecast |
categ_id |
Many2One | ❌ | Product category filter |
product_id |
Many2One | ❌ | Specific product filter |
period_days |
Integer | ✅ | Days per period (default: 7) |
num_periods |
Integer | ✅ | Number of periods (default: 15) |
show_lot |
Boolean | ❌ | Show by lot |
show_location |
Boolean | ❌ | Show by location |
Report Data Structure¶
{
"company_name": "Company",
"date": "2024-01-01",
"period_days": 7,
"periods": [
{
"date_from": "2024-01-01",
"date_to": "2024-01-07",
"period_name": "0-6 days"
},
# ... more periods
],
"lines": [
{
"product_id": 123,
"product_name": "Product",
"code": "PROD001",
"location_id": 456,
"location_name": "Warehouse",
"lot_id": 789,
"lot_num": "LOT001",
"qty": 100.0, # Current quantity
"periods": [
{
"date_from": "2024-01-01",
"date_to": "2024-01-07",
"qty": 95.0, # Projected quantity
"warning": False # True if negative
},
# ... one per period
]
}
]
}
Usage Example¶
# Generate 12-week forecast
report_id = get_model("report.stock.forecast").create({
"date": "2024-01-01",
"location_id": warehouse_id,
"period_days": 7,
"num_periods": 12
})
data = get_model("report.stock.forecast").get_report_data([report_id])
# Find products that will run out
for line in data["lines"]:
for period in line["periods"]:
if period["warning"]: # Negative stock projected
print(f"WARNING: {line['code']} will run out by {period['date_to']}")
break
States Included in Forecast¶
done- Completed movementspending- Planned movementsapproved- Approved movementsforecast- Forecasted movements
5. Stock Invoice Report¶
Model Information¶
Model Name: report.stock.invoice
Display Name: Stock vs Invoice Report
Type: Transient model
Purpose: Reconciles goods received with supplier invoices
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date_from |
Date | ✅ | Start date |
date_to |
Date | ✅ | End date |
product_id |
Many2One | ❌ | Filter by product |
categ_id |
Many2One | ❌ | Filter by category |
pick_id |
Many2One | ❌ | Specific goods receipt |
products |
Many2Many | ❌ | Multiple products |
Report Data Structure¶
{
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"lines": [
{
"prod_id": 123,
"prod_code": "PROD001",
"prod_name": "Product Name",
"qty_received": 100.0, # Goods receipt quantity
"qty_returned": 5.0, # Return quantity
"qty_invoiced": 90.0, # Invoiced quantity
"qty_remain": 5.0 # Not yet invoiced (100-5-90)
}
]
}
Usage Example¶
# Find uninvoiced receipts
report_id = get_model("report.stock.invoice").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31"
})
data = get_model("report.stock.invoice").get_report_data([report_id])
# Print products with uninvoiced quantities
for line in data["lines"]:
if line["qty_remain"] > 0:
print(f"{line['prod_code']}: {line['qty_remain']} not invoiced")
Use Cases¶
- Invoice Matching: Verify all receipts have corresponding invoices
- Accrual Accounting: Identify goods received but not invoiced (GRN)
- Return Processing: Track returned goods vs credit notes
- Supplier Reconciliation: Match receipts with supplier statements
6. Stock Movement Report¶
Model Information¶
Model Name: report.stock.move
Display Name: Stock Movement Report
Type: Transient model
Purpose: Lists filtered stock movements by type
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
pick_type |
Selection | ✅ | in/internal/out |
date_from |
Date | ❌ | Start date |
date_to |
Date | ❌ | End date |
location_from_id |
Many2One | ❌ | Source location |
location_to_id |
Many2One | ❌ | Destination location |
ref |
Char | ❌ | Reference filter |
related_id |
Reference | ❌ | Related document |
show_loss_only |
Boolean | ❌ | Show only loss movements |
Pick Types¶
| Type | Description | Use Case |
|---|---|---|
in |
Goods Receipt | Receiving from suppliers |
internal |
Goods Transfer | Internal movements |
out |
Goods Issue | Shipping to customers |
Report Data Structure¶
{
"title": "Goods Receive Report",
"lines": [
{
"_item_no": 1,
"number": "GR001",
"date": "2024-01-15",
"related": "PO-001",
"product_code": "PROD001",
"product_name": "Product Name",
"location_from": "Supplier",
"qty": 100.0,
"uom": "PCS",
"location_to": "Warehouse",
"qty_loss": 2.0, # Loss quantity if any
"container_from": "CONT001",
"lot": "LOT001",
"state": "Completed",
"ref": "GR001"
}
]
}
Usage Example¶
# Get all goods receipts for a month
report_id = get_model("report.stock.move").create({
"pick_type": "in",
"date_from": "2024-01-01",
"date_to": "2024-01-31"
}, context={"pick_type": "in"})
data = get_model("report.stock.move").get_report_data([report_id])
# Calculate total received
total_qty = sum(line["qty"] for line in data["lines"])
print(f"Total received: {total_qty}")
Loss Tracking¶
When show_loss_only=True, only movements with associated losses are shown. Losses are tracked by movements to inventory (loss) locations.
7. Stock Planning Report¶
Model Information¶
Model Name: report.stock.plan
Display Name: Stock Planning Report
Type: Transient model
Purpose: Identifies products needing reordering based on min/max levels
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
product_categ_id |
Many2One | ❌ | Filter by category |
supplier_id |
Many2One | ❌ | Filter by supplier |
plan_horizon |
Integer | ❌ | Planning days (default: 5) |
Report Data Structure¶
{
"company_name": "Company",
"plan_horizon": 5,
"lines": [
{
"product_id": 123,
"product_code": "PROD001",
"product_name": "Product Name",
"plan_qty_horiz": 45.0, # Projected quantity
"min_qty": 50.0, # Minimum stock level
"req_qty": 5.0, # Required quantity (min - projected)
"stock_uom_name": "PCS", # Stock UoM
"order_qty": 50.0, # Suggested order quantity
"order_uom_name": "BOX", # Order UoM
"order_lead_time": 7, # Lead time in days
"supply_method": "Purchase", # Purchase/Production
"supplier_name": "Supplier A"
}
]
}
Planning Logic¶
- Calculate projected stock at planning horizon date
- Compare with minimum stock levels
- Calculate required quantity to reach minimum
- Apply ordering constraints:
- Minimum order quantity
- Order quantity multiples
- Lead time considerations
Usage Example¶
# Generate procurement plan for next 10 days
report_id = get_model("report.stock.plan").create({
"plan_horizon": 10,
"product_categ_id": category_id
})
data = get_model("report.stock.plan").get_report_data([report_id])
# Create purchase orders for items below minimum
for line in data["lines"]:
if line["supply_method"] == "Purchase" and line["order_qty"] > 0:
print(f"Order {line['order_qty']} {line['order_uom_name']} of {line['product_code']}")
print(f" From: {line['supplier_name']}")
print(f" Lead time: {line['order_lead_time']} days")
Supply Methods¶
| Method | Description | Used For |
|---|---|---|
| Purchase | Buy from supplier | Purchased items |
| Production | Manufacture internally | Manufactured items |
8. Stock Project Report¶
Model Information¶
Model Name: report.stock.project
Display Name: Stock by Project Report
Type: Transient model
Purpose: Tracks stock movements by project/tracking category
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date_from |
Date | ❌ | Start date |
date_to |
Date | ❌ | End date |
track_id |
Many2One | ❌ | Project/tracking category |
contact_id |
Many2One | ❌ | Contact filter |
number |
Char | ❌ | Document number filter |
status |
Selection | ❌ | Delivered/Not Delivered |
Report Data Structure¶
{
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"lines": [
{
"id": 123,
"purchase_order_id": 456,
"project_code": "PROJ-001",
"project_name": "Project Alpha",
"date": "2024-01-15",
"number": "GR-001",
"contact_code": "CUST001",
"contact_name": "Customer Name",
"related_to": "SO-001",
"qty_in": 100.0, # Received quantity
"qty_out": 80.0, # Issued quantity
"state": "Delivered" # Delivery status
}
]
}
Delivery Status Logic¶
The system determines delivery status by comparing lot quantities: - Delivered: All received quantity has been issued - Not Delivered: Some received quantity remains in stock
Usage Example¶
# Get project stock movements
report_id = get_model("report.stock.project").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"track_id": project_track_id,
"status": "Not Delivered"
})
data = get_model("report.stock.project").get_report_data([report_id])
# Find pending deliveries
for line in data["lines"]:
pending = line["qty_in"] - line["qty_out"]
if pending > 0:
print(f"{line['project_code']}: {pending} units pending delivery")
9. Stock Summary Report¶
Model Information¶
Model Name: report.stock.summary
Display Name: Stock Summary Report
Type: Transient model
Purpose: Opening/closing stock balances with period movements
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date_from |
Date | ✅ | Period start |
date_to |
Date | ✅ | Period end |
location_id |
Many2One | ❌ | Location filter |
product_id |
Many2One | ❌ | Product filter |
master_product_id |
Many2One | ❌ | Master product (for variants) |
lot_id |
Many2One | ❌ | Lot filter |
container_id |
Many2One | ❌ | Container filter |
prod_code |
Char | ❌ | Product code search |
prod_categ_id |
Many2One | ❌ | Category filter |
brand_id |
Many2One | ❌ | Brand filter |
track_id |
Many2One | ❌ | Tracking filter |
show_lot |
Boolean | ❌ | Show lot details |
show_container |
Boolean | ❌ | Show container details |
show_qty2 |
Boolean | ❌ | Show secondary qty |
show_prod_desc |
Boolean | ❌ | Show descriptions |
show_track |
Boolean | ❌ | Show tracking |
show_area |
Boolean | ❌ | Show area calculations |
show_image |
Boolean | ❌ | Include images |
only_closing |
Boolean | ❌ | Only closing balances |
hide_cost |
Boolean | ❌ | Hide cost columns |
hide_zero_qty |
Boolean | ❌ | Hide zero quantities |
Report Data Structure¶
{
"company_name": "Company",
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"lines": [
{
"prod_id": 123,
"prod_name": "Product",
"prod_code": "PROD001",
"prod_desc": "Description",
"prod_img": "image.jpg",
"lot_id": 456,
"lot_num": "LOT001",
"loc_id": 789,
"loc_name": "Warehouse A",
"uom_name": "PCS",
# Opening balance
"open_qty": 100.0,
"open_amt": 10000.0,
"open_qty2": 200.0,
# Period movements
"period_in_qty": 50.0,
"period_in_amt": 5250.0,
"period_in_qty2": 100.0,
"period_out_qty": 30.0,
"period_out_amt": 3000.0,
"period_out_qty2": 60.0,
# Closing balance
"close_qty": 120.0, # 100 + 50 - 30
"close_amt": 12250.0, # 10000 + 5250 - 3000
"close_qty2": 240.0, # 200 + 100 - 60
# Optional fields
"close_area": 240.0, # If show_area=True
"cont_name": "CONT001", # If show_container=True
"track_name": "PROJ-A" # If show_track=True
}
],
"total_open_amt": 10000.0,
"total_period_in_amt": 5250.0,
"total_period_out_amt": 3000.0,
"total_close_qty": 120.0,
"total_close_amt": 12250.0
}
Performance Features¶
- Redis Caching: Results cached for 5 minutes on production systems
- Snapshot Support: Uses stock snapshots for faster opening balance calculation
- Optimized Queries: Aggregates movements at database level
Usage Example¶
# Monthly stock summary
report_id = get_model("report.stock.summary").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"location_id": warehouse_id,
"show_lot": True,
"hide_cost": False
})
data = get_model("report.stock.summary").get_report_data([report_id])
# Print summary
print(f"Opening Stock Value: {data['total_open_amt']}")
print(f"Receipts: +{data['total_period_in_amt']}")
print(f"Issues: -{data['total_period_out_amt']}")
print(f"Closing Stock Value: {data['total_close_amt']}")
Area Calculations¶
When show_area=True, calculates area/volume based on product dimensions:
Method 1 (calc_method=1):
Method 2 (calc_method=2):
10. Forecast Details Report¶
Model Information¶
Model Name: report.forecast.details
Display Name: Forecast Details Report
Type: Transient model
Purpose: Visual forecast chart for single product with matplotlib
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date |
Date | ✅ | Forecast start date |
product_id |
Many2One | ✅ | Product to forecast |
forecast_days |
Integer | ✅ | Days ahead (default: 120) |
show_shelf_life |
Boolean | ❌ | Show shelf life zones |
Report Data Structure¶
{
"company_name": "Company",
"date": "2024-01-01",
"product_code": "PROD001",
"product_name": "Product Name",
"svg_data": "<svg>...</svg>", # SVG chart
"svg_data_base64": "base64_encoded..." # Base64 for embedding
}
Chart Features¶
- Forecast Line: Projected quantity over time
- Min/Max Lines: Reorder and maximum stock levels
- Shortage Zones: Red shading when below minimum
- Order Date: Vertical line showing when to order
- Shelf Life Zones: 50% and 75% remaining shelf life (optional)
FEFO Logic¶
When show_shelf_life=True, tracks:
- Life 50%: Lots with less than 50% shelf life remaining
- Life 75%: Lots with less than 75% shelf life remaining
Usage Example¶
# Generate visual forecast
report_id = get_model("report.forecast.details").create({
"date": "2024-01-01",
"product_id": product_id,
"forecast_days": 90,
"show_shelf_life": True
})
data = get_model("report.forecast.details").get_report_data([report_id])
# Save chart to file
with open("forecast.svg", "w") as f:
f.write(data["svg_data"])
Dependencies¶
Requires matplotlib library:
11. Forecast Summary Report¶
Model Information¶
Model Name: report.forecast.summary
Display Name: Forecast Summary Report
Type: Transient model
Purpose: Multi-product forecast showing reorder recommendations
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date |
Date | ✅ | Forecast start date |
forecast_days |
Integer | ✅ | Days ahead (default: 180) |
show_shelf_life |
Boolean | ❌ | Consider shelf life |
order_only |
Boolean | ❌ | Show only products to order |
product_id |
Many2One | ❌ | Single product filter |
Report Data Structure¶
{
"company_name": "Company",
"date": "2024-01-01",
"show_shelf_life": True,
"order_only": False,
"lines": [
{
"prod_id": 123,
"prod_code": "PROD001",
"prod_name": "Product Name",
"current_qty": 100.0, # Current stock
"min_qty": 50.0, # Minimum level
"min_qty_50": 75.0, # Min with 50% shelf life
"min_qty_75": 100.0, # Min with 75% shelf life
"min_qty_date": "2024-02-15", # Date below minimum
"min_qty_months": 1.5, # Months until below min
"lead_time": 14, # Lead time days
"order_date": "2024-02-01", # Suggested order date
"max_qty": 200.0, # Maximum level
"order_qty": 100.0, # Suggested order qty
"show_alert": True # Order now!
}
]
}
Alert Logic¶
show_alert=True when:
- Projected stock will fall below minimum within forecast period
- Order date is today or in the past
- Considers shelf life constraints if enabled
Usage Example¶
# Generate urgent orders list
report_id = get_model("report.forecast.summary").create({
"date": "2024-01-01",
"forecast_days": 90,
"show_shelf_life": True,
"order_only": True # Only show items needing orders
})
data = get_model("report.forecast.summary").get_report_data([report_id])
# Print purchase recommendations
for line in data["lines"]:
if line["show_alert"]:
print(f"URGENT: Order {line['order_qty']} of {line['prod_code']}")
print(f" Current: {line['current_qty']}")
print(f" Min: {line['min_qty']}")
print(f" Will run out: {line['min_qty_date']}")
print(f" Lead time: {line['lead_time']} days")
12. Master Product Report¶
Model Information¶
Model Name: report.master.product
Display Name: Master Product Report
Type: Transient model
Purpose: Aggregates stock data for product variants under master products
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date_from |
Date | ✅ | Period start |
date_to |
Date | ✅ | Period end |
master_product_id |
Many2One | ✅ | Master product |
location_id |
Many2One | ❌ | Location filter |
width |
Float | ❌ | Width filter |
length |
Float | ❌ | Length filter |
sort_by |
Selection | ❌ | Sort order |
hide_product_code |
Boolean | ❌ | Hide codes |
hide_product_name |
Boolean | ❌ | Hide names |
hide_qty1 |
Boolean | ❌ | Hide quantity |
only_show_closing |
Boolean | ❌ | Only closing |
hide_zero_qty |
Boolean | ❌ | Hide zeros |
show_area |
Boolean | ❌ | Show area calcs |
Sort Options¶
| Option | Description |
|---|---|
date |
By receiving date |
prod_code |
By product code |
prod_name |
By product name |
width |
By width dimension |
length |
By length dimension |
*_desc |
Descending variants |
Report Data Structure¶
{
"company_name": "Company",
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"master_products": [
{
"master_product_code": "MASTER-001",
"master_product_name": "Master Product",
"master_product_desc": "Description",
"lines": [
{
"prod_id": 123,
"prod_code": "VAR-001",
"prod_name": "Variant 1",
"width": 1.5,
"length": 2.0,
"in_date": "2024-01-15",
"lot_nums": "LOT001, LOT002",
"num_lots_open": 2,
"num_lots_close": 2,
"open_qty": 100.0,
"open_area": 300.0,
"period_in_qty": 50.0,
"in_area": 150.0,
"period_out_qty": 30.0,
"out_area": 90.0,
"close_qty": 120.0,
"close_area": 360.0,
"total_length_close": 240.0,
"pack_net_weight": 25.5
}
],
"total_open_qty": 100.0,
"total_open_m2": 300.0,
"total_lots_open": 2,
"total_close_qty": 120.0,
"total_close_area": 360.0,
"total_close_lots": 2,
"total_sum_length_close": 240.0
}
]
}
Aggregation Logic¶
For each master product: 1. Find all variant products 2. Group movements by (product, location, date, width, length) 3. Sum quantities for same grouping 4. Calculate totals per master product 5. Apply sorting and filters
Usage Example¶
# Master product stock summary
report_id = get_model("report.master.product").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"master_product_id": master_id,
"location_id": warehouse_id,
"show_area": True,
"sort_by": "width",
"hide_zero_qty": True
})
data = get_model("report.master.product").get_report_data([report_id])
# Print by master product
for master in data["master_products"]:
print(f"\n{master['master_product_code']}: {master['master_product_name']}")
print(f" Total Closing: {master['total_close_qty']} units, {master['total_close_area']} m²")
print(f" Number of lots: {master['total_close_lots']}")
for line in master["lines"]:
print(f" {line['width']}x{line['length']}: {line['close_qty']} units")
Length Calculations¶
For products with uom_name="CTN":
For other UoMs:
13. Ship Cost Report¶
Model Information¶
Model Name: report.ship.cost
Display Name: Shipping Cost Report
Type: Transient model
Purpose: Tracks shipping costs and methods
Fields Reference¶
| Field | Type | Required | Description |
|---|---|---|---|
date_from |
Date | ✅ | Start date |
date_to |
Date | ✅ | End date |
contact_id |
Many2One | ❌ | Contact filter |
ship_pay_by |
Selection | ❌ | Payment party |
ship_method_id |
Many2One | ❌ | Shipping method |
Payment Options¶
| Option | Description |
|---|---|
company |
Paid by company |
customer |
Paid by customer |
supplier |
Paid by supplier |
Report Data Structure¶
{
"company_name": "Company",
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"lines": [
{
"id": 123,
"date": "2024-01-15",
"number": "GI-001",
"contact": "Customer Name",
"ship_method": "Express Courier",
"ship_tracking": "TRACK123456",
"ship_cost": 45.50,
"ship_pay_by": "customer",
"related": "SO-001"
}
],
"totals": {
"ship_cost": 1250.75 # Sum of all shipping costs
}
}
Usage Example¶
# Monthly shipping cost analysis
report_id = get_model("report.ship.cost").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"ship_pay_by": "company"
})
data = get_model("report.ship.cost").get_report_data([report_id])
# Analyze by method
from collections import defaultdict
by_method = defaultdict(float)
for line in data["lines"]:
by_method[line["ship_method"]] += line["ship_cost"]
print("Shipping Costs by Method:")
for method, cost in by_method.items():
print(f" {method}: ${cost:.2f}")
print(f"\nTotal: ${data['totals']['ship_cost']:.2f}")
Common Use Cases¶
1. Month-End Stock Valuation¶
# Generate stock summary for month-end
report_id = get_model("report.stock.summary").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"only_closing": True,
"hide_cost": False
})
data = get_model("report.stock.summary").get_report_data([report_id])
print(f"Closing Stock Value: ${data['total_close_amt']:.2f}")
2. Weekly Procurement Planning¶
# Generate weekly planning report
report_id = get_model("report.stock.plan").create({
"plan_horizon": 7
})
data = get_model("report.stock.plan").get_report_data([report_id])
# Create purchase requisitions
for line in data["lines"]:
if line["order_qty"] > 0:
# Create PR or PO
pass
3. Expiry Management¶
# Check expiring inventory
report_id = get_model("report.stock.expire").create({
"date": date.today().strftime("%Y-%m-%d"),
"forecast_days": 90
})
data = get_model("report.stock.expire").get_report_data([report_id])
# Alert for expiring lots
for line in data["lines"]:
expiring_3mo = sum(m["exp_qty"] for m in line["months"][:3])
if expiring_3mo > 5:
print(f"ALERT: {line['prod_code']} has {expiring_3mo} lots expiring in 3 months")
4. Slow-Moving Analysis¶
# Generate aging report
report_id = get_model("report.stock.aging").create({
"date": date.today().strftime("%Y-%m-%d"),
"period_days": 90,
"num_periods": 4
})
data = get_model("report.stock.aging").get_report_data([report_id])
# Find slow movers (>270 days old)
for line in data["lines"]:
if line["older_qty"] > 0:
print(f"SLOW MOVER: {line['product_code']} has {line['older_qty']} units >270 days old")
5. Product Movement Tracking¶
# Track specific product movements
report_id = get_model("report.stock.card").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"product_id": product_id,
"show_pending": True
})
data = get_model("report.stock.card").get_report_data([report_id])
for group in data["groups"]:
print(f"\n{group['product_code']} at {group['location_name']}")
print("Date | In | Out | Balance")
print("-" * 40)
for line in group["lines"]:
in_qty = line.get("in_qty", 0)
out_qty = line.get("out_qty", 0)
bal = line["bal_qty"]
print(f"{line['date']} | {in_qty:5.0f} | {out_qty:5.0f} | {bal:7.0f}")
Performance Tips¶
1. Use Date Ranges Wisely¶
# BAD: No date filter
report_id = get_model("report.stock.card").create({
"product_id": product_id
})
# GOOD: Specific date range
report_id = get_model("report.stock.card").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"product_id": product_id
})
2. Filter Aggressively¶
# Combine filters to reduce result set
report_id = get_model("report.stock.summary").create({
"date_from": "2024-01-01",
"date_to": "2024-01-31",
"location_id": warehouse_id,
"prod_categ_id": category_id,
"only_closing": True,
"hide_zero_qty": True
})
3. Use Caching¶
Stock summary and master product reports use Redis caching: - Cache key includes all parameters - Cache expires after 5 minutes - Identical requests within 5 minutes return cached results
4. Leverage Snapshots¶
Stock snapshots dramatically improve performance:
# Without snapshot: calculates from all historical movements
# With snapshot: calculates from last snapshot forward
Create regular snapshots:
Troubleshooting¶
"Report takes too long to run"¶
Causes: - Large movement history - No date filters - Missing snapshots
Solutions: - Add strict date ranges - Create stock snapshots - Use specific product/location filters - Enable caching (production only)
"Opening balance incorrect"¶
Causes: - Missing or outdated snapshot - Incorrect date_from selection
Solutions: - Create snapshot for period start - Verify date_from is correct - Check for backdated movements
"Forecast shows incorrect quantities"¶
Causes: - Pending movements not included - Incorrect stock movement states
Solutions: - Verify movement states (done/pending/approved/forecast) - Check date ranges on movements - Ensure forecast movements are properly created
"Master product report missing variants"¶
Causes: - Variants not linked to master - Products inactive - Zero quantities filtered out
Solutions:
- Verify parent_id on variants
- Check product active status
- Disable hide_zero_qty filter
Best Practices¶
1. Regular Snapshot Creation¶
# Monthly snapshots
from dateutil.relativedelta import relativedelta
def create_monthly_snapshots():
d = date.today()
for i in range(12):
snap_date = (d - relativedelta(months=i)).strftime("%Y-%m-01")
get_model("stock.snap").create_snap(date=snap_date)
2. Scheduled Report Generation¶
# Daily aging report
def daily_aging_report():
report_id = get_model("report.stock.aging").create({
"date": date.today().strftime("%Y-%m-%d"),
"period_days": 30,
"num_periods": 4
})
data = get_model("report.stock.aging").get_report_data([report_id])
# Email or save report
send_aging_report(data)
3. Performance Monitoring¶
import time
start = time.time()
data = get_model("report.stock.summary").get_report_data([report_id])
elapsed = time.time() - start
if elapsed > 10:
# Log slow report
print(f"WARNING: Report took {elapsed:.2f} seconds")
4. Data Validation¶
# Validate stock summary balances
data = get_model("report.stock.summary").get_report_data([report_id])
for line in data["lines"]:
# Verify formula
expected_close = line["open_qty"] + line["period_in_qty"] - line["period_out_qty"]
actual_close = line["close_qty"]
if abs(expected_close - actual_close) > 0.01:
print(f"ERROR: Balance mismatch for {line['prod_code']}")
Security Considerations¶
Permission Requirements¶
All reports respect standard Netforce permissions: - User must have read access to stock.move - User must have read access to products - Multi-company filtering applies automatically
Data Sensitivity¶
Cost information can be hidden:
report_id = get_model("report.stock.summary").create({
"hide_cost": True, # Hide cost columns
# ... other params
})
Audit Logging¶
Report generation is not logged, but: - Underlying data access is audited if audit_log enabled - Cache hits/misses logged in production - Performance metrics logged to /tmp/report.log
API Reference¶
Common Parameters¶
Most reports share these parameters:
| Parameter | Type | Common Default | Description |
|---|---|---|---|
date_from |
Date | First of month | Period start |
date_to |
Date | Last of month | Period end |
product_id |
Integer | None | Product filter |
location_id |
Integer | None | Location filter |
Common Methods¶
All report models implement:
def get_report_data(self, ids, context={}):
"""
Generate report data.
Args:
ids: List of report record IDs
context: Context dictionary
Returns:
Dictionary containing report data
"""
Helper Functions¶
# Get movement totals
totals = get_totals(
date_from="2024-01-01",
date_to="2024-01-31",
product_id=123,
location_id=456,
states=["done", "pending"]
)
# Get aging periods
periods = get_periods(
date="2024-01-31",
period_days=30,
num_periods=3
)
Version History¶
| Date | Version | Changes |
|---|---|---|
| Oct 2024 | 1.0 | Initial documentation |
Related Documentation¶
Last Updated: October 2024
Maintainer: Development Team