Product Waste Documentation¶
Overview¶
The Product Waste module (product.waste) records and tracks waste or scrap of inventory items. It creates automatic stock movements from internal locations to waste locations, maintaining accurate inventory records for waste management and accounting purposes.
Model Information¶
Model Name: product.waste
Display Name: Product Waste
Name Field: number
Key Fields: N/A
Features¶
- ✅ Automated sequence numbering
- ✅ State workflow management
- ✅ Automatic stock movement creation
- ✅ Waste location tracking
State Workflow¶
| State | Description |
|---|---|
draft |
Initial state, not yet validated |
done |
Validated and stock moved to waste |
voided |
Cancelled/voided |
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
number |
Char | ✅ | Auto-generated waste number |
date |
Date | ✅ | Waste date |
location_id |
Many2One | ✅ | Source location (stock.location, internal) |
product_id |
Many2One | ✅ | Product being wasted (product, stock type) |
lot_id |
Many2One | ❌ | Lot/Serial number (stock.lot) |
qty |
Decimal | ✅ | Waste quantity |
uom_id |
Many2One | ✅ | Unit of measure (uom) |
user_id |
Many2One | ❌ | User who recorded waste (base.user) |
state |
Selection | ✅ | Current workflow state |
notes |
Text | ❌ | Notes about waste reason |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
stock_moves |
One2Many | Stock movements created (stock.move) |
API Methods¶
1. Create Waste Record¶
Method: create(vals, context)
Creates a new waste record with auto-generated number.
Parameters:
vals = {
"date": "2025-10-27", # Required: Waste date
"location_id": 123, # Required: Source location
"product_id": 456, # Required: Product ID
"qty": 5.0, # Required: Quantity
"uom_id": 1, # Required: UoM
"lot_id": 789, # Optional: Lot/serial
"notes": "Damaged during handling" # Optional: Reason
}
Returns: int - New waste record ID
Example:
# Record waste for damaged products
waste_id = get_model("product.waste").create({
"date": "2025-10-27",
"location_id": warehouse_loc_id,
"product_id": product_id,
"qty": 3,
"uom_id": uom_id,
"lot_id": lot_id,
"notes": "Dropped during transfer, packaging damaged"
})
2. Validate Waste¶
Method: validate(ids, context)
Validates waste and creates stock movements to waste location.
Parameters:
- ids (list): Waste record IDs
Behavior: - Finds waste location (type="waste") - Gets waste journal from settings - Creates stock move from source to waste location - Sets move to done immediately - Updates waste state to "done"
Example:
# Validate waste to move stock
get_model("product.waste").validate([waste_id])
# Verify stock moved
waste = get_model("product.waste").browse(waste_id)
assert waste.state == "done"
assert len(waste.stock_moves) > 0
3. Revert to Draft¶
Method: to_draft(ids, context)
Reverts validated waste back to draft, deleting stock movements.
Parameters:
- ids (list): Waste record IDs
Example:
# Revert to draft if validated by mistake
get_model("product.waste").to_draft([waste_id])
# Verify reverted
waste = get_model("product.waste").browse(waste_id)
assert waste.state == "draft"
assert len(waste.stock_moves) == 0
4. Void Waste¶
Method: void(ids, context)
Voids the waste record, deleting any stock movements.
Parameters:
- ids (list): Waste record IDs
Example:
5. Delete Waste¶
Method: delete(ids, context)
Deletes waste record. Only allowed in certain states.
Validation: - Cannot delete if not in appropriate state
Example:
# Delete draft waste
try:
get_model("product.waste").delete([waste_id])
except Exception as e:
print(f"Cannot delete: {e}")
UI Events (onchange methods)¶
onchange_product¶
Triggered when product is selected. Updates: - UoM from product's default UoM
Usage:
data = {"product_id": 123}
result = get_model("product.waste").onchange_product(
context={"data": data}
)
# Result updates uom_id
Configuration Settings¶
Required Settings¶
| Setting | Location | Description |
|---|---|---|
waste_journal_id |
settings (ID:1) | Journal for waste transactions |
Required System Data¶
- Waste location must exist (type="waste")
- Waste sequence must be configured
Setup Example:
# Ensure waste location exists
res = get_model("stock.location").search([["type", "=", "waste"]])
if not res:
waste_loc_id = get_model("stock.location").create({
"name": "Waste/Scrap",
"type": "waste"
})
# Configure waste journal
settings = get_model("settings").browse(1)
settings.write({"waste_journal_id": journal_id})
Related Models¶
| Model | Relationship | Description |
|---|---|---|
stock.location |
Many2One | Source and waste locations |
product |
Many2One | Product being wasted |
stock.lot |
Many2One | Lot/serial tracking |
uom |
Many2One | Unit of measure |
stock.move |
One2Many | Stock movements created |
base.user |
Many2One | User who recorded waste |
Common Use Cases¶
Use Case 1: Record Damaged Inventory¶
# Product damaged during operations
waste_id = get_model("product.waste").create({
"date": "2025-10-27",
"location_id": production_loc_id,
"product_id": damaged_product_id,
"qty": 10,
"uom_id": uom_id,
"notes": "Damaged during production - quality control rejection"
})
# Validate to move to waste
get_model("product.waste").validate([waste_id])
Use Case 2: Record Expired Product by Lot¶
# Record expired products
expired_lot_id = 456
waste_id = get_model("product.waste").create({
"date": "2025-10-27",
"location_id": warehouse_loc_id,
"product_id": product_id,
"lot_id": expired_lot_id,
"qty": 50,
"uom_id": uom_id,
"notes": "Expired - past use-by date"
})
get_model("product.waste").validate([waste_id])
Use Case 3: Batch Waste Recording¶
# Record multiple damaged items
damaged_items = [
{"product_id": prod1_id, "qty": 5, "notes": "Broken packaging"},
{"product_id": prod2_id, "qty": 3, "notes": "Water damage"},
{"product_id": prod3_id, "qty": 2, "notes": "Quality defect"}
]
waste_ids = []
for item in damaged_items:
waste_id = get_model("product.waste").create({
"date": "2025-10-27",
"location_id": warehouse_loc_id,
"product_id": item["product_id"],
"qty": item["qty"],
"uom_id": uom_id,
"notes": item["notes"]
})
waste_ids.append(waste_id)
# Validate all at once
for waste_id in waste_ids:
get_model("product.waste").validate([waste_id])
Best Practices¶
1. Always Provide Waste Reason¶
# Good: Document why waste occurred
waste_vals = {
"notes": "Quality control failure - wrong color specification" # ✅ Clear reason
}
# Bad: No explanation
waste_vals = {
"notes": "" # ❌ No audit trail
}
2. Use Lot Tracking for Traceability¶
# Good: Track which lot was wasted
waste_vals = {
"product_id": product_id,
"lot_id": lot_id, # ✅ Can trace source
"qty": 5
}
# Bad: No lot information for tracked product
waste_vals = {
"product_id": product_id, # Product requires lot tracking
"qty": 5
# ❌ Missing lot_id - cannot trace
}
3. Validate Immediately After Recording¶
# Good: Validate right away for accurate inventory
waste_id = get_model("product.waste").create({...})
get_model("product.waste").validate([waste_id]) # ✅ Immediate stock update
# Bad: Leave in draft for extended period
waste_id = get_model("product.waste").create({...})
# ❌ Inventory inaccurate until validated
Search Functions¶
Search by Date Range¶
# Find waste recorded this month
condition = [
["date", ">=", "2025-10-01"],
["date", "<=", "2025-10-31"]
]
waste_records = get_model("product.waste").search(condition)
Search by Product¶
# Find all waste for specific product
condition = [["product_id", "=", product_id]]
waste_records = get_model("product.waste").search(condition)
Search by Location¶
# Find waste from specific location
condition = [["location_id", "=", location_id]]
waste_records = get_model("product.waste").search(condition)
Search by State¶
# Find pending waste records
condition = [["state", "=", "draft"]]
pending_waste = get_model("product.waste").search(condition)
Performance Tips¶
1. Batch Validation¶
# Efficient: Validate multiple waste records
waste_ids = get_model("product.waste").search([["state", "=", "draft"]])
for waste_id in waste_ids:
get_model("product.waste").validate([waste_id])
2. Index on Search Fields¶
- Fields like
date,location_id,product_id, andnotesare indexed withsearch=True - Use these in search conditions for better performance
Troubleshooting¶
"Waste location not found"¶
Cause: No location with type="waste" exists
Solution: Create waste location in system
"Waste journal not found"¶
Cause: waste_journal_id not configured in settings
Solution: Configure waste journal in inventory settings
"Can not delete waste products in this status"¶
Cause: Trying to delete non-draft waste
Solution: Only delete draft records; void or revert to draft first
Stock not moving to waste location¶
Cause: Waste not validated
Solution: Call validate() method after creating waste record
Testing Examples¶
Unit Test: Create and Validate Waste¶
def test_waste_creation_and_validation():
# Create waste
waste_id = get_model("product.waste").create({
"date": "2025-10-27",
"location_id": loc_id,
"product_id": prod_id,
"qty": 5,
"uom_id": uom_id,
"notes": "Test waste"
})
# Verify draft state
waste = get_model("product.waste").browse(waste_id)
assert waste.state == "draft"
assert len(waste.stock_moves) == 0
# Validate
get_model("product.waste").validate([waste_id])
# Verify done state and stock move created
waste = get_model("product.waste").browse(waste_id)
assert waste.state == "done"
assert len(waste.stock_moves) == 1
assert waste.stock_moves[0].state == "done"
Unit Test: Revert to Draft¶
def test_revert_to_draft():
# Create and validate waste
waste_id = get_model("product.waste").create({
"date": "2025-10-27",
"location_id": loc_id,
"product_id": prod_id,
"qty": 5,
"uom_id": uom_id
})
get_model("product.waste").validate([waste_id])
# Revert to draft
get_model("product.waste").to_draft([waste_id])
# Verify reverted
waste = get_model("product.waste").browse(waste_id)
assert waste.state == "draft"
assert len(waste.stock_moves) == 0
Unit Test: Void Waste¶
def test_void_waste():
# Create and validate waste
waste_id = get_model("product.waste").create({
"date": "2025-10-27",
"location_id": loc_id,
"product_id": prod_id,
"qty": 5,
"uom_id": uom_id
})
get_model("product.waste").validate([waste_id])
# Void
get_model("product.waste").void([waste_id])
# Verify voided
waste = get_model("product.waste").browse(waste_id)
assert waste.state == "voided"
assert len(waste.stock_moves) == 0
Reporting Examples¶
Report: Waste by Product¶
def get_waste_by_product_report(start_date, end_date):
"""Generate waste report grouped by product"""
waste_records = get_model("product.waste").search_browse([
["date", ">=", start_date],
["date", "<=", end_date],
["state", "=", "done"]
])
waste_by_product = {}
for waste in waste_records:
prod_id = waste.product_id.id
prod_name = waste.product_id.name
if prod_id not in waste_by_product:
waste_by_product[prod_id] = {
"product_name": prod_name,
"total_qty": 0,
"count": 0
}
waste_by_product[prod_id]["total_qty"] += waste.qty
waste_by_product[prod_id]["count"] += 1
return waste_by_product
# Usage
report = get_waste_by_product_report("2025-10-01", "2025-10-31")
for prod_id, data in report.items():
print(f"{data['product_name']}: {data['total_qty']} units wasted ({data['count']} records)")
Report: Waste by Reason¶
def get_waste_by_reason_report(start_date, end_date):
"""Analyze waste reasons"""
waste_records = get_model("product.waste").search_browse([
["date", ">=", start_date],
["date", "<=", end_date],
["state", "=", "done"]
])
reasons = {}
for waste in waste_records:
reason = waste.notes or "No reason specified"
if reason not in reasons:
reasons[reason] = {"qty": 0, "count": 0}
reasons[reason]["qty"] += waste.qty
reasons[reason]["count"] += 1
return reasons
# Usage
report = get_waste_by_reason_report("2025-10-01", "2025-10-31")
for reason, data in sorted(reasons.items(), key=lambda x: x[1]["qty"], reverse=True):
print(f"{reason}: {data['qty']} units ({data['count']} occurrences)")
Version History¶
Last Updated: 2025-10-27
Model Version: product_waste.py
Framework: Netforce
Additional Resources¶
- Stock Move Documentation:
stock.move - Stock Location Documentation:
stock.location - Product Documentation:
product - Settings Configuration: Inventory Settings
This documentation is generated for developer onboarding and reference purposes.